diff --git a/autotests/jobtest.cpp b/autotests/jobtest.cpp index 51a14647..9bbf716a 100644 --- a/autotests/jobtest.cpp +++ b/autotests/jobtest.cpp @@ -1,2112 +1,2112 @@ /* This file is part of the KDE project Copyright (C) 2004-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. */ #include "jobtest.h" #include #include #include #include #include #include #include #include #include #include #include #include #include -#include +#include #include #include #include #include #include #include #include #include "kiotesthelper.h" // createTestFile etc. #ifndef Q_OS_WIN #include // for readlink #endif QTEST_MAIN(JobTest) // The code comes partly from kdebase/kioslave/trash/testtrash.cpp static QString otherTmpDir() { #ifdef Q_OS_WIN return QDir::tempPath() + "/jobtest/"; #else // This one needs to be on another partition return QStringLiteral("/tmp/jobtest/"); #endif } static bool otherTmpDirIsOnSamePartition() // true on CI because it's a LXC container { KMountPoint::Ptr srcMountPoint = KMountPoint::currentMountPoints().findByPath(homeTmpDir()); KMountPoint::Ptr destMountPoint = KMountPoint::currentMountPoints().findByPath(otherTmpDir()); Q_ASSERT(srcMountPoint); Q_ASSERT(destMountPoint); return srcMountPoint->mountedFrom() == destMountPoint->mountedFrom(); } #if 0 static QUrl systemTmpDir() { #ifdef Q_OS_WIN return QUrl("system:" + QDir::homePath() + "/.kde-unit-test/jobtest-system/"); #else return QUrl("system:/home/.kde-unit-test/jobtest-system/"); #endif } static QString realSystemPath() { return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/jobtest-system/"; } #endif void JobTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); QCoreApplication::instance()->setApplicationName("kio/jobtest"); // testing for #357499 // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-30); // 30 seconds ago // Start with a clean base dir cleanupTestCase(); homeTmpDir(); // create it if (!QFile::exists(otherTmpDir())) { bool ok = QDir().mkdir(otherTmpDir()); if (!ok) { qFatal("couldn't create %s", qPrintable(otherTmpDir())); } } #if 0 if (KProtocolInfo::isKnownProtocol("system")) { if (!QFile::exists(realSystemPath())) { bool ok = dir.mkdir(realSystemPath()); if (!ok) { qFatal("couldn't create %s", qPrintable(realSystemPath())); } } } #endif qRegisterMetaType("KJob*"); qRegisterMetaType("KIO::Job*"); qRegisterMetaType("QDateTime"); } void JobTest::cleanupTestCase() { QDir(homeTmpDir()).removeRecursively(); QDir(otherTmpDir()).removeRecursively(); #if 0 if (KProtocolInfo::isKnownProtocol("system")) { delDir(systemTmpDir()); } #endif } void JobTest::enterLoop() { QEventLoop eventLoop; connect(this, &JobTest::exitLoop, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } void JobTest::storedGet() { qDebug(); const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); QUrl u = QUrl::fromLocalFile(filePath); m_result = -1; KIO::StoredTransferJob *job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); job->setUiDelegate(nullptr); connect(job, &KJob::result, this, &JobTest::slotGetResult); enterLoop(); QCOMPARE(m_result, 0); // no error QCOMPARE(m_data, QByteArray("Hello\0world", 11)); QCOMPARE(m_data.size(), 11); QVERIFY(!spyPercent.isEmpty()); } void JobTest::slotGetResult(KJob *job) { m_result = job->error(); m_data = static_cast(job)->data(); emit exitLoop(); } void JobTest::put() { const QString filePath = homeTmpDir() + "fileFromHome"; QUrl u = QUrl::fromLocalFile(filePath); KIO::TransferJob *job = KIO::put(u, 0600, KIO::Overwrite | KIO::HideProgressInfo); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setUiDelegate(nullptr); connect(job, &KJob::result, this, &JobTest::slotResult); connect(job, &KIO::TransferJob::dataReq, this, &JobTest::slotDataReq); m_result = -1; m_dataReqCount = 0; enterLoop(); QVERIFY(m_result == 0); // no error QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), 30LL); // "This is a test for KIO::put()\n" QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); } void JobTest::slotDataReq(KIO::Job *, QByteArray &data) { // Really not the way you'd write a slotDataReq usually :) switch (m_dataReqCount++) { case 0: data = "This is a test for "; break; case 1: data = "KIO::put()\n"; break; case 2: data = QByteArray(); break; } } void JobTest::slotResult(KJob *job) { m_result = job->error(); emit exitLoop(); } void JobTest::storedPut() { const QString filePath = homeTmpDir() + "fileFromHome"; QUrl u = QUrl::fromLocalFile(filePath); QByteArray putData = "This is the put data"; KIO::TransferJob *job = KIO::storedPut(putData, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putData.size()); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::storedPutIODevice() { const QString filePath = homeTmpDir() + "fileFromHome"; QBuffer putData; putData.setData("This is the put data"); QVERIFY(putData.open(QIODevice::ReadOnly)); KIO::TransferJob *job = KIO::storedPut(&putData, QUrl::fromLocalFile(filePath), 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putData.size()); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::storedPutIODeviceFile() { // Given a source file and a destination file const QString src = homeTmpDir() + "fileFromHome"; createTestFile(src); QVERIFY(QFile::exists(src)); QFile srcFile(src); QVERIFY(srcFile.open(QIODevice::ReadOnly)); const QString dest = homeTmpDir() + "fileFromHome_copied"; QFile::remove(dest); const QUrl destUrl = QUrl::fromLocalFile(dest); // When using storedPut with the QFile as argument KIO::StoredTransferJob *job = KIO::storedPut(&srcFile, destUrl, 0600, KIO::Overwrite | KIO::HideProgressInfo); // Then the copy should succeed and the dest file exist QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QCOMPARE(QFileInfo(src).size(), QFileInfo(dest).size()); QFile::remove(dest); } void JobTest::storedPutIODeviceTempFile() { // Create a temp file in the current dir. QTemporaryFile tempFile(QStringLiteral("jobtest-tmp")); QVERIFY(tempFile.open()); // Write something into the file. QTextStream stream(&tempFile); stream << QStringLiteral("This is the put data"); stream.flush(); QVERIFY(QFileInfo(tempFile).size() > 0); const QString dest = homeTmpDir() + QLatin1String("tmpfile-dest"); const QUrl destUrl = QUrl::fromLocalFile(dest); // QTemporaryFiles are open in ReadWrite mode, // so we don't need to close and reopen, // but we need to rewind to the beginning. tempFile.seek(0); auto job = KIO::storedPut(&tempFile, destUrl, -1); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo::exists(dest)); QCOMPARE(QFileInfo(dest).size(), QFileInfo(tempFile).size()); QVERIFY(QFile::remove(dest)); } void JobTest::storedPutIODeviceFastDevice() { const QString filePath = homeTmpDir() + "fileFromHome"; const QUrl u = QUrl::fromLocalFile(filePath); const QByteArray putDataContents = "This is the put data"; QBuffer putDataBuffer; QVERIFY(putDataBuffer.open(QIODevice::ReadWrite)); KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setTotalSize(putDataContents.size()); job->setUiDelegate(nullptr); job->setAsyncDataEnabled(true); // Emit the readChannelFinished even before the job has had time to start const auto pos = putDataBuffer.pos(); int size = putDataBuffer.write(putDataContents); putDataBuffer.seek(pos); putDataBuffer.readChannelFinished(); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(size, putDataContents.size()); QCOMPARE(putDataBuffer.bytesAvailable(), 0); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putDataContents.size()); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::storedPutIODeviceSlowDevice() { const QString filePath = homeTmpDir() + "fileFromHome"; const QUrl u = QUrl::fromLocalFile(filePath); const QByteArray putDataContents = "This is the put data"; QBuffer putDataBuffer; QVERIFY(putDataBuffer.open(QIODevice::ReadWrite)); KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setTotalSize(putDataContents.size()); job->setUiDelegate(nullptr); job->setAsyncDataEnabled(true); int size = 0; const auto writeOnce = [&putDataBuffer, &size, putDataContents]() { const auto pos = putDataBuffer.pos(); size += putDataBuffer.write(putDataContents); putDataBuffer.seek(pos); // qDebug() << "written" << size; }; QTimer::singleShot(200, this, writeOnce); QTimer::singleShot(400, this, writeOnce); // Simulate the transfer is done QTimer::singleShot(450, this, [&putDataBuffer](){ putDataBuffer.readChannelFinished(); }); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(size, putDataContents.size() * 2); QCOMPARE(putDataBuffer.bytesAvailable(), 0); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putDataContents.size() * 2); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::storedPutIODeviceSlowDeviceBigChunk() { const QString filePath = homeTmpDir() + "fileFromHome"; const QUrl u = QUrl::fromLocalFile(filePath); const QByteArray putDataContents(300000, 'K'); // Make sure the 300000 is bigger than MAX_READ_BUF_SIZE QBuffer putDataBuffer; QVERIFY(putDataBuffer.open(QIODevice::ReadWrite)); KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setTotalSize(putDataContents.size()); job->setUiDelegate(nullptr); job->setAsyncDataEnabled(true); int size = 0; const auto writeOnce = [&putDataBuffer, &size, putDataContents]() { const auto pos = putDataBuffer.pos(); size += putDataBuffer.write(putDataContents); putDataBuffer.seek(pos); // qDebug() << "written" << size; }; QTimer::singleShot(200, this, writeOnce); // Simulate the transfer is done QTimer::singleShot(450, this, [&putDataBuffer](){ putDataBuffer.readChannelFinished(); }); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(size, putDataContents.size()); QCOMPARE(putDataBuffer.bytesAvailable(), 0); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putDataContents.size()); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::asyncStoredPutReadyReadAfterFinish() { const QString filePath = homeTmpDir() + "fileFromHome"; const QUrl u = QUrl::fromLocalFile(filePath); QBuffer putDataBuffer; QVERIFY(putDataBuffer.open(QIODevice::ReadWrite)); KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); job->setAsyncDataEnabled(true); bool jobFinished = false; connect(job, &KJob::finished, [&jobFinished, &putDataBuffer] { putDataBuffer.readyRead(); jobFinished = true; }); QTimer::singleShot(200, this, [job]() { job->kill(); }); QTRY_VERIFY(jobFinished); } //// void JobTest::copyLocalFile(const QString &src, const QString &dest) { const QUrl u = QUrl::fromLocalFile(src); const QUrl d = QUrl::fromLocalFile(dest); const int perms = 0666; // copy the file with file_copy KIO::Job *job = KIO::file_copy(u, d, perms, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there QCOMPARE(int(QFileInfo(dest).permissions()), int(0x6666)); { // check that the timestamp is the same (#24443) // Note: this only works because of copy() in kio_file. // The datapump solution ignores mtime, the app has to call FileCopyJob::setModificationTime() QFileInfo srcInfo(src); QFileInfo destInfo(dest); #ifdef Q_OS_WIN // win32 time may differs in msec part QCOMPARE(srcInfo.lastModified().toString("dd.MM.yyyy hh:mm"), destInfo.lastModified().toString("dd.MM.yyyy hh:mm")); #else QCOMPARE(srcInfo.lastModified(), destInfo.lastModified()); #endif } // cleanup and retry with KIO::copy() QFile::remove(dest); job = KIO::copy(u, d, KIO::HideProgressInfo); QSignalSpy spyCopyingDone(job, SIGNAL(copyingDone(KIO::Job*,QUrl,QUrl,QDateTime,bool,bool))); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there { // check that the timestamp is the same (#24443) QFileInfo srcInfo(src); QFileInfo destInfo(dest); #ifdef Q_OS_WIN // win32 time may differs in msec part QCOMPARE(srcInfo.lastModified().toString("dd.MM.yyyy hh:mm"), destInfo.lastModified().toString("dd.MM.yyyy hh:mm")); #else QCOMPARE(srcInfo.lastModified(), destInfo.lastModified()); #endif } QCOMPARE(spyCopyingDone.count(), 1); // cleanup and retry with KIO::copyAs() QFile::remove(dest); job = KIO::copyAs(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there // Do it again, with Overwrite. job = KIO::copyAs(u, d, KIO::Overwrite | KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there // Do it again, without Overwrite (should fail). job = KIO::copyAs(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(!job->exec()); // Clean up QFile::remove(dest); } void JobTest::copyLocalDirectory(const QString &src, const QString &_dest, int flags) { QVERIFY(QFileInfo(src).isDir()); QVERIFY(QFileInfo(src + "/testfile").isFile()); QUrl u = QUrl::fromLocalFile(src); QString dest(_dest); QUrl d = QUrl::fromLocalFile(dest); if (flags & AlreadyExists) { QVERIFY(QFile::exists(dest)); } else { QVERIFY(!QFile::exists(dest)); } KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFileInfo(dest).isDir()); QVERIFY(QFileInfo(dest + "/testfile").isFile()); QVERIFY(QFile::exists(src)); // still there if (flags & AlreadyExists) { dest += '/' + u.fileName(); //qDebug() << "Expecting dest=" << dest; } // CopyJob::setNextDirAttribute isn't implemented for Windows currently. #ifndef Q_OS_WIN { // Check that the timestamp is the same (#24443) QFileInfo srcInfo(src); QFileInfo destInfo(dest); QCOMPARE(srcInfo.lastModified(), destInfo.lastModified()); } #endif // Do it again, with Overwrite. // Use copyAs, we don't want a subdir inside d. job = KIO::copyAs(u, d, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); // Do it again, without Overwrite (should fail). job = KIO::copyAs(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(!job->exec()); } #ifndef Q_OS_WIN static QString linkTarget(const QString &path) { // Use readlink on Unix because symLinkTarget turns relative targets into absolute (#352927) char linkTargetBuffer[4096]; const int n = readlink(QFile::encodeName(path).constData(), linkTargetBuffer, sizeof(linkTargetBuffer) - 1); if (n != -1) { linkTargetBuffer[n] = 0; } return QFile::decodeName(linkTargetBuffer); } static void copyLocalSymlink(const QString &src, const QString &dest, const QString &expectedLinkTarget) { QT_STATBUF buf; QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); // copy the symlink KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(QString::number(job->error()))); QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) == 0); // dest exists QCOMPARE(linkTarget(dest), expectedLinkTarget); // cleanup QFile::remove(dest); } #endif void JobTest::copyFileToSamePartition() { const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); } void JobTest::copyDirectoryToSamePartition() { qDebug(); const QString src = homeTmpDir() + "dirFromHome"; const QString dest = homeTmpDir() + "dirFromHome_copied"; createTestDirectory(src); copyLocalDirectory(src, dest); } void JobTest::copyDirectoryToExistingDirectory() { qDebug(); // just the same as copyDirectoryToSamePartition, but this time dest exists. // So we get a subdir, "dirFromHome_copy/dirFromHome" const QString src = homeTmpDir() + "dirFromHome"; const QString dest = homeTmpDir() + "dirFromHome_copied"; createTestDirectory(src); createTestDirectory(dest); copyLocalDirectory(src, dest, AlreadyExists); } void JobTest::copyFileToOtherPartition() { qDebug(); const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = otherTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); } void JobTest::copyDirectoryToOtherPartition() { qDebug(); const QString src = homeTmpDir() + "dirFromHome"; const QString dest = otherTmpDir() + "dirFromHome_copied"; createTestDirectory(src); copyLocalDirectory(src, dest); } void JobTest::copyRelativeSymlinkToSamePartition() // #352927 { #ifdef Q_OS_WIN QSKIP("Skipping symlink test on Windows"); #else const QString filePath = homeTmpDir() + "testlink"; const QString dest = homeTmpDir() + "testlink_copied"; createTestSymlink(filePath, "relative"); copyLocalSymlink(filePath, dest, QStringLiteral("relative")); QFile::remove(filePath); #endif } void JobTest::copyAbsoluteSymlinkToOtherPartition() { #ifdef Q_OS_WIN QSKIP("Skipping symlink test on Windows"); #else const QString filePath = homeTmpDir() + "testlink"; const QString dest = otherTmpDir() + "testlink_copied"; createTestSymlink(filePath, QFile::encodeName(homeTmpDir())); copyLocalSymlink(filePath, dest, homeTmpDir()); QFile::remove(filePath); #endif } void JobTest::copyFolderWithUnaccessibleSubfolder() { #ifdef Q_OS_WIN QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder"); #endif const QString src_dir = homeTmpDir() + "srcHome"; const QString dst_dir = homeTmpDir() + "dstHome"; QDir().remove(src_dir); QDir().remove(dst_dir); createTestDirectory(src_dir); createTestDirectory(src_dir + "/folder1"); QString inaccessible = src_dir + "/folder1/inaccessible"; createTestDirectory(inaccessible); QFile(inaccessible).setPermissions(QFile::Permissions()); // Make it inaccessible //Copying should throw some warnings, as it cannot access some folders KIO::CopyJob *job = KIO::copy(QUrl::fromLocalFile(src_dir), QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo); QSignalSpy spy(job, SIGNAL(warning(KJob*,QString,QString))); job->setUiDelegate(nullptr); // no skip dialog, thanks QVERIFY2(job->exec(), qPrintable(job->errorString())); QFile(inaccessible).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)); KIO::DeleteJob *deljob1 = KIO::del(QUrl::fromLocalFile(src_dir), KIO::HideProgressInfo); deljob1->setUiDelegate(nullptr); // no skip dialog, thanks QVERIFY(deljob1->exec()); KIO::DeleteJob *deljob2 = KIO::del(QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo); deljob2->setUiDelegate(nullptr); // no skip dialog, thanks QVERIFY(deljob2->exec()); QCOMPARE(spy.count(), 1); // one warning should be emitted by the copy job } void JobTest::copyDataUrl() { // GIVEN const QString dst_dir = homeTmpDir(); QVERIFY(!QFileInfo::exists(dst_dir + "/data")); // WHEN KIO::CopyJob *job = KIO::copy(QUrl("data:,Hello%2C%20World!"), QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); // THEN QVERIFY(QFileInfo(dst_dir + "/data").isFile()); QFile::remove(dst_dir + "/data"); } void JobTest::suspendFileCopy() { const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); const QUrl u = QUrl::fromLocalFile(filePath); const QUrl d = QUrl::fromLocalFile(dest); KIO::Job *job = KIO::file_copy(u, d, KIO::HideProgressInfo); QSignalSpy spyResult(job, SIGNAL(result(KJob*))); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(job->suspend()); QVERIFY(!spyResult.wait(300)); QVERIFY(job->resume()); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QFile::remove(dest); } void JobTest::suspendCopy() { const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); const QUrl u = QUrl::fromLocalFile(filePath); const QUrl d = QUrl::fromLocalFile(dest); KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); QSignalSpy spyResult(job, SIGNAL(result(KJob*))); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(job->suspend()); QVERIFY(!spyResult.wait(300)); QVERIFY(job->resume()); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QFile::remove(dest); } void JobTest::moveLocalFile(const QString &src, const QString &dest) { QVERIFY(QFile::exists(src)); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); // move the file with file_move KIO::Job *job = KIO::file_move(u, d, 0666, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(!QFile::exists(src)); // not there anymore QCOMPARE(int(QFileInfo(dest).permissions()), int(0x6666)); // move it back with KIO::move() job = KIO::move(d, u, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dest)); QVERIFY(QFile::exists(src)); // it's back } static void moveLocalSymlink(const QString &src, const QString &dest) { QT_STATBUF buf; QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); // move the symlink with move, NOT with file_move KIO::Job *job = KIO::move(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) == 0); QVERIFY(!QFile::exists(src)); // not there anymore // move it back with KIO::move() job = KIO::move(d, u, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) != 0); // doesn't exist anymore QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0); // it's back } void JobTest::moveLocalDirectory(const QString &src, const QString &dest) { qDebug() << src << " " << dest; QVERIFY(QFile::exists(src)); QVERIFY(QFileInfo(src).isDir()); QVERIFY(QFileInfo(src + "/testfile").isFile()); #ifndef Q_OS_WIN QVERIFY(QFileInfo(src + "/testlink").isSymLink()); #endif QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); KIO::Job *job = KIO::move(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFileInfo(dest).isDir()); QVERIFY(QFileInfo(dest + "/testfile").isFile()); QVERIFY(!QFile::exists(src)); // not there anymore #ifndef Q_OS_WIN QVERIFY(QFileInfo(dest + "/testlink").isSymLink()); #endif } void JobTest::moveFileToSamePartition() { qDebug(); const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_moved"; createTestFile(filePath); moveLocalFile(filePath, dest); } void JobTest::moveDirectoryToSamePartition() { qDebug(); const QString src = homeTmpDir() + "dirFromHome"; const QString dest = homeTmpDir() + "dirFromHome_moved"; createTestDirectory(src); moveLocalDirectory(src, dest); } void JobTest::moveDirectoryIntoItself() { qDebug(); const QString src = homeTmpDir() + "dirFromHome"; const QString dest = src + "/foo"; createTestDirectory(src); QVERIFY(QFile::exists(src)); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); KIO::CopyJob *job = KIO::move(u, d); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_CANNOT_MOVE_INTO_ITSELF); QCOMPARE(job->errorString(), i18n("A folder cannot be moved into itself")); QDir(dest).removeRecursively(); } void JobTest::moveFileToOtherPartition() { qDebug(); const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = otherTmpDir() + "fileFromHome_moved"; createTestFile(filePath); moveLocalFile(filePath, dest); } void JobTest::moveSymlinkToOtherPartition() { #ifndef Q_OS_WIN qDebug(); const QString filePath = homeTmpDir() + "testlink"; const QString dest = otherTmpDir() + "testlink_moved"; createTestSymlink(filePath); moveLocalSymlink(filePath, dest); #endif } void JobTest::moveDirectoryToOtherPartition() { qDebug(); #ifndef Q_OS_WIN const QString src = homeTmpDir() + "dirFromHome"; const QString dest = otherTmpDir() + "dirFromHome_moved"; createTestDirectory(src); moveLocalDirectory(src, dest); #endif } void JobTest::moveFileNoPermissions() { #ifdef Q_OS_WIN QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder"); #endif // Given a file that cannot be moved (subdir has no permissions) const QString subdir = homeTmpDir() + "subdir"; QVERIFY(QDir().mkpath(subdir)); const QString src = subdir + "/thefile"; createTestFile(src); QVERIFY(QFile(subdir).setPermissions(QFile::Permissions())); // Make it inaccessible // When trying to move it const QString dest = homeTmpDir() + "dest"; KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); // no skip dialog, thanks // The job should fail with "access denied" QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_ACCESS_DENIED); // Note that, just like mv(1), KIO's behavior depends on whether // a direct rename(2) was used, or a full copy+del. In the first case // there is no destination file created, but in the second case the // destination file remains. // In this test it's the same partition, so no dest created. QVERIFY(!QFile::exists(dest)); // Cleanup QVERIFY(QFile(subdir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner))); QVERIFY(QFile::exists(src)); QVERIFY(QDir(subdir).removeRecursively()); } void JobTest::moveDirectoryNoPermissions() { #ifdef Q_OS_WIN QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder"); #endif // Given a dir that cannot be moved (parent dir has no permissions) const QString subdir = homeTmpDir() + "subdir"; const QString src = subdir + "/thedir"; QVERIFY(QDir().mkpath(src)); QVERIFY(QFileInfo(src).isDir()); QVERIFY(QFile(subdir).setPermissions(QFile::Permissions())); // Make it inaccessible // When trying to move it const QString dest = homeTmpDir() + "mdnp"; KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); // no skip dialog, thanks // The job should fail with "access denied" QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_ACCESS_DENIED); QVERIFY(!QFile::exists(dest)); // Cleanup QVERIFY(QFile(subdir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner))); QVERIFY(QFile::exists(src)); QVERIFY(QDir(subdir).removeRecursively()); } void JobTest::listRecursive() { // Note: many other tests must have been run before since we rely on the files they created const QString src = homeTmpDir(); #ifndef Q_OS_WIN // Add a symlink to a dir, to make sure we don't recurse into those bool symlinkOk = symlink("dirFromHome", QFile::encodeName(src + "/dirFromHome_link").constData()) == 0; QVERIFY(symlinkOk); #endif KIO::ListJob *job = KIO::listRecursive(QUrl::fromLocalFile(src), KIO::HideProgressInfo); job->setUiDelegate(nullptr); connect(job, &KIO::ListJob::entries, this, &JobTest::slotEntries); QVERIFY2(job->exec(), qPrintable(job->errorString())); m_names.sort(); QByteArray ref_names = QByteArray(".,..," "dirFromHome,dirFromHome/testfile," "dirFromHome/testlink," // exists on Windows too, see createTestDirectory "dirFromHome_copied," "dirFromHome_copied/dirFromHome,dirFromHome_copied/dirFromHome/testfile," "dirFromHome_copied/dirFromHome/testlink," "dirFromHome_copied/testfile," "dirFromHome_copied/testlink," #ifndef Q_OS_WIN "dirFromHome_link," #endif "fileFromHome"); const QString joinedNames = m_names.join(QLatin1Char(',')); if (joinedNames.toLatin1() != ref_names) { qDebug("%s", qPrintable(joinedNames)); qDebug("%s", ref_names.data()); } QCOMPARE(joinedNames.toLatin1(), ref_names); } void JobTest::listFile() { const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); KIO::ListJob *job = KIO::listDir(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY(!job->exec()); QCOMPARE(job->error(), static_cast(KIO::ERR_IS_FILE)); // And list something that doesn't exist const QString path = homeTmpDir() + "fileFromHomeDoesNotExist"; job = KIO::listDir(QUrl::fromLocalFile(path), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY(!job->exec()); QCOMPARE(job->error(), static_cast(KIO::ERR_DOES_NOT_EXIST)); } void JobTest::killJob() { const QString src = homeTmpDir(); KIO::ListJob *job = KIO::listDir(QUrl::fromLocalFile(src), KIO::HideProgressInfo); QVERIFY(job->isAutoDelete()); QPointer ptr(job); job->setUiDelegate(nullptr); qApp->processEvents(); // let the job start, it's no fun otherwise job->kill(); qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); // process the deferred delete of the job QVERIFY(ptr.isNull()); } void JobTest::killJobBeforeStart() { const QString src = homeTmpDir(); KIO::Job *job = KIO::stat(QUrl::fromLocalFile(src), KIO::HideProgressInfo); QVERIFY(job->isAutoDelete()); QPointer ptr(job); job->setUiDelegate(nullptr); job->kill(); qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); // process the deferred delete of the job QVERIFY(ptr.isNull()); qApp->processEvents(); // does KIO scheduler crash here? nope. } void JobTest::deleteJobBeforeStart() // #163171 { const QString src = homeTmpDir(); KIO::Job *job = KIO::stat(QUrl::fromLocalFile(src), KIO::HideProgressInfo); QVERIFY(job->isAutoDelete()); job->setUiDelegate(nullptr); delete job; qApp->processEvents(); // does KIO scheduler crash here? } void JobTest::directorySize() { // Note: many other tests must have been run before since we rely on the files they created const QString src = homeTmpDir(); KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(src)); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); qDebug() << "totalSize: " << job->totalSize(); qDebug() << "totalFiles: " << job->totalFiles(); qDebug() << "totalSubdirs: " << job->totalSubdirs(); #ifdef Q_OS_WIN QCOMPARE(job->totalFiles(), 5ULL); // see expected result in listRecursive() above QCOMPARE(job->totalSubdirs(), 3ULL); // see expected result in listRecursive() above QVERIFY(job->totalSize() > 54); #else QCOMPARE(job->totalFiles(), 7ULL); // see expected result in listRecursive() above QCOMPARE(job->totalSubdirs(), 4ULL); // see expected result in listRecursive() above QVERIFY2(job->totalSize() >= 60, qPrintable(QString("totalSize was %1").arg(job->totalSize()))); // size of subdir entries is filesystem dependent. E.g. this is 16428 with ext4 but only 272 with xfs, and 63 on FreeBSD #endif qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); } void JobTest::directorySizeError() { KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(QStringLiteral("/I/Dont/Exist"))); job->setUiDelegate(nullptr); QVERIFY(!job->exec()); qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); } void JobTest::slotEntries(KIO::Job *, const KIO::UDSEntryList &lst) { for (KIO::UDSEntryList::ConstIterator it = lst.begin(); it != lst.end(); ++it) { QString displayName = (*it).stringValue(KIO::UDSEntry::UDS_NAME); //QUrl url = (*it).stringValue( KIO::UDSEntry::UDS_URL ); m_names.append(displayName); } } void JobTest::calculateRemainingSeconds() { unsigned int seconds = KIO::calculateRemainingSeconds(2 * 86400 - 60, 0, 1); QCOMPARE(seconds, static_cast(2 * 86400 - 60)); QString text = KIO::convertSeconds(seconds); QCOMPARE(text, i18n("1 day 23:59:00")); seconds = KIO::calculateRemainingSeconds(520, 20, 10); QCOMPARE(seconds, static_cast(50)); text = KIO::convertSeconds(seconds); QCOMPARE(text, i18n("00:00:50")); } #if 0 void JobTest::copyFileToSystem() { if (!KProtocolInfo::isKnownProtocol("system")) { qDebug() << "no kio_system, skipping test"; return; } // First test with support for UDS_LOCAL_PATH copyFileToSystem(true); QString dest = realSystemPath() + "fileFromHome_copied"; QFile::remove(dest); // Then disable support for UDS_LOCAL_PATH, i.e. test what would // happen for ftp, smb, http etc. copyFileToSystem(false); } void JobTest::copyFileToSystem(bool resolve_local_urls) { qDebug() << resolve_local_urls; extern KIOCORE_EXPORT bool kio_resolve_local_urls; kio_resolve_local_urls = resolve_local_urls; const QString src = homeTmpDir() + "fileFromHome"; createTestFile(src); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(systemTmpDir()); d.addPath("fileFromHome_copied"); qDebug() << "copying " << u << " to " << d; // copy the file with file_copy m_mimetype.clear(); KIO::FileCopyJob *job = KIO::file_copy(u, d, -1, KIO::HideProgressInfo); job->setUiDelegate(0); connect(job, SIGNAL(mimetype(KIO::Job*,QString)), this, SLOT(slotMimetype(KIO::Job*,QString))); QVERIFY2(job->exec(), qPrintable(job->errorString())); QString dest = realSystemPath() + "fileFromHome_copied"; QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there { // do NOT check that the timestamp is the same. // It can't work with file_copy when it uses the datapump, // unless we use setModificationTime in the app code. } // Check mimetype QCOMPARE(m_mimetype, QString("text/plain")); // cleanup and retry with KIO::copy() QFile::remove(dest); job = KIO::copy(u, d, KIO::HideProgressInfo); job->setUiDelegate(0); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there { // check that the timestamp is the same (#79937) QFileInfo srcInfo(src); QFileInfo destInfo(dest); QCOMPARE(srcInfo.lastModified(), destInfo.lastModified()); } // restore normal behavior kio_resolve_local_urls = true; } #endif void JobTest::getInvalidUrl() { QUrl url(QStringLiteral("http://strange/")); QVERIFY(!url.isValid()); KIO::SimpleJob *job = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo); QVERIFY(job != nullptr); job->setUiDelegate(nullptr); KIO::Scheduler::setJobPriority(job, 1); // shouldn't crash (#135456) QVERIFY(!job->exec()); // it should fail :) } void JobTest::slotMimetype(KIO::Job *job, const QString &type) { QVERIFY(job != nullptr); m_mimetype = type; } void JobTest::deleteFile() { const QString dest = otherTmpDir() + "fileFromHome_copied"; createTestFile(dest); KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dest)); } void JobTest::deleteDirectory() { const QString dest = otherTmpDir() + "dirFromHome_copied"; if (!QFile::exists(dest)) { createTestDirectory(dest); } // Let's put a few things in there to see if the recursive deletion works correctly // A hidden file: createTestFile(dest + "/.hidden"); #ifndef Q_OS_WIN // A broken symlink: createTestSymlink(dest + "/broken_symlink"); // A symlink to a dir: bool symlink_ok = symlink(QFile::encodeName(QFileInfo(QFINDTESTDATA("jobtest.cpp")).absolutePath()).constData(), QFile::encodeName(dest + "/symlink_to_dir").constData()) == 0; if (!symlink_ok) { qFatal("couldn't create symlink: %s", strerror(errno)); } #endif KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dest)); } void JobTest::deleteSymlink(bool using_fast_path) { extern KIOCORE_EXPORT bool kio_resolve_local_urls; kio_resolve_local_urls = !using_fast_path; #ifndef Q_OS_WIN const QString src = homeTmpDir() + "dirFromHome"; createTestDirectory(src); QVERIFY(QFile::exists(src)); const QString dest = homeTmpDir() + "/dirFromHome_link"; if (!QFile::exists(dest)) { // Add a symlink to a dir, to make sure we don't recurse into those bool symlinkOk = symlink(QFile::encodeName(src).constData(), QFile::encodeName(dest).constData()) == 0; QVERIFY(symlinkOk); QVERIFY(QFile::exists(dest)); } KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dest)); QVERIFY(QFile::exists(src)); #endif kio_resolve_local_urls = true; } void JobTest::deleteSymlink() { #ifndef Q_OS_WIN deleteSymlink(true); deleteSymlink(false); #endif } void JobTest::deleteManyDirs(bool using_fast_path) { extern KIOCORE_EXPORT bool kio_resolve_local_urls; kio_resolve_local_urls = !using_fast_path; const int numDirs = 50; QList dirs; for (int i = 0; i < numDirs; ++i) { const QString dir = homeTmpDir() + "dir" + QString::number(i); createTestDirectory(dir); dirs << QUrl::fromLocalFile(dir); } - QTime dt; + QElapsedTimer dt; dt.start(); KIO::Job *job = KIO::del(dirs, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); for (const QUrl &dir : qAsConst(dirs)) { QVERIFY(!QFile::exists(dir.toLocalFile())); } qDebug() << "Deleted" << numDirs << "dirs in" << dt.elapsed() << "milliseconds"; kio_resolve_local_urls = true; } void JobTest::deleteManyDirs() { deleteManyDirs(true); deleteManyDirs(false); } static QList createManyFiles(const QString &baseDir, int numFiles) { QList ret; ret.reserve(numFiles); for (int i = 0; i < numFiles; ++i) { // create empty file const QString file = baseDir + QString::number(i); QFile f(file); bool ok = f.open(QIODevice::WriteOnly); if (ok) { f.write("Hello"); ret.append(QUrl::fromLocalFile(file)); } } return ret; } void JobTest::deleteManyFilesIndependently() { - QTime dt; + QElapsedTimer dt; dt.start(); const int numFiles = 100; // Use 1000 for performance testing const QString baseDir = homeTmpDir(); const QList urls = createManyFiles(baseDir, numFiles); QCOMPARE(urls.count(), numFiles); for (int i = 0; i < numFiles; ++i) { // delete each file independently. lots of jobs. this stress-tests kio scheduling. const QUrl url = urls.at(i); const QString file = url.toLocalFile(); QVERIFY(QFile::exists(file)); //qDebug() << file; KIO::Job *job = KIO::del(url, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(file)); } qDebug() << "Deleted" << numFiles << "files in" << dt.elapsed() << "milliseconds"; } void JobTest::deleteManyFilesTogether(bool using_fast_path) { extern KIOCORE_EXPORT bool kio_resolve_local_urls; kio_resolve_local_urls = !using_fast_path; - QTime dt; + QElapsedTimer dt; dt.start(); const int numFiles = 100; // Use 1000 for performance testing const QString baseDir = homeTmpDir(); const QList urls = createManyFiles(baseDir, numFiles); QCOMPARE(urls.count(), numFiles); //qDebug() << file; KIO::Job *job = KIO::del(urls, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); qDebug() << "Deleted" << numFiles << "files in" << dt.elapsed() << "milliseconds"; kio_resolve_local_urls = true; } void JobTest::deleteManyFilesTogether() { deleteManyFilesTogether(true); deleteManyFilesTogether(false); } void JobTest::rmdirEmpty() { const QString dir = homeTmpDir() + "dir"; QDir().mkdir(dir); QVERIFY(QFile::exists(dir)); KIO::Job *job = KIO::rmdir(QUrl::fromLocalFile(dir)); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dir)); } void JobTest::rmdirNotEmpty() { const QString dir = homeTmpDir() + "dir"; createTestDirectory(dir); createTestDirectory(dir + "/subdir"); KIO::Job *job = KIO::rmdir(QUrl::fromLocalFile(dir)); QVERIFY(!job->exec()); QVERIFY(QFile::exists(dir)); } void JobTest::stat() { #if 1 const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); const QUrl url(QUrl::fromLocalFile(filePath)); KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); // TODO set setSide, setDetails const KIO::UDSEntry &entry = job->statResult(); QVERIFY(!entry.isDir()); QVERIFY(!entry.isLink()); QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("fileFromHome")); // Compare what we get via kio_file and what we get when KFileItem stat()s directly const KFileItem kioItem(entry, url); const KFileItem fileItem(url); QCOMPARE(kioItem.name(), fileItem.name()); QCOMPARE(kioItem.url(), fileItem.url()); QCOMPARE(kioItem.size(), fileItem.size()); QCOMPARE(kioItem.user(), fileItem.user()); QCOMPARE(kioItem.group(), fileItem.group()); QCOMPARE(kioItem.mimetype(), fileItem.mimetype()); QCOMPARE(kioItem.permissions(), fileItem.permissions()); QCOMPARE(kioItem.time(KFileItem::ModificationTime), fileItem.time(KFileItem::ModificationTime)); QCOMPARE(kioItem.time(KFileItem::AccessTime), fileItem.time(KFileItem::AccessTime)); #else // Testing stat over HTTP KIO::StatJob *job = KIO::stat(QUrl("http://www.kde.org"), KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); // TODO set setSide, setDetails const KIO::UDSEntry &entry = job->statResult(); QVERIFY(!entry.isDir()); QVERIFY(!entry.isLink()); QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QString()); #endif } #ifndef Q_OS_WIN void JobTest::statSymlink() { const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); const QString symlink = otherTmpDir() + "link"; QVERIFY(QFile(filePath).link(symlink)); QVERIFY(QFile::exists(symlink)); setTimeStamp(symlink, QDateTime::currentDateTime().addSecs(-20)); // differentiate link time and source file time const QUrl url(QUrl::fromLocalFile(symlink)); KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); // TODO set setSide, setDetails const KIO::UDSEntry &entry = job->statResult(); QVERIFY(!entry.isDir()); QVERIFY(entry.isLink()); QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("link")); // Compare what we get via kio_file and what we get when KFileItem stat()s directly const KFileItem kioItem(entry, url); const KFileItem fileItem(url); QCOMPARE(kioItem.name(), fileItem.name()); QCOMPARE(kioItem.url(), fileItem.url()); QVERIFY(kioItem.isLink()); QVERIFY(fileItem.isLink()); QCOMPARE(kioItem.linkDest(), fileItem.linkDest()); QCOMPARE(kioItem.size(), fileItem.size()); QCOMPARE(kioItem.user(), fileItem.user()); QCOMPARE(kioItem.group(), fileItem.group()); QCOMPARE(kioItem.mimetype(), fileItem.mimetype()); QCOMPARE(kioItem.permissions(), fileItem.permissions()); QCOMPARE(kioItem.time(KFileItem::ModificationTime), fileItem.time(KFileItem::ModificationTime)); QCOMPARE(kioItem.time(KFileItem::AccessTime), fileItem.time(KFileItem::AccessTime)); } #endif void JobTest::mostLocalUrl() { const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); KIO::StatJob *job = KIO::mostLocalUrl(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(job->mostLocalUrl().toLocalFile(), filePath); } void JobTest::chmodFile() { const QString filePath = homeTmpDir() + "fileForChmod"; createTestFile(filePath); KFileItem item(QUrl::fromLocalFile(filePath)); const mode_t origPerm = item.permissions(); mode_t newPerm = origPerm ^ S_IWGRP; QVERIFY(newPerm != origPerm); KFileItemList items; items << item; KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QString(), QString(), false, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); KFileItem newItem(QUrl::fromLocalFile(filePath)); QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(newPerm, 8)); QFile::remove(filePath); } #ifdef Q_OS_UNIX void JobTest::chmodSticky() { const QString dirPath = homeTmpDir() + "dirForChmodSticky"; QDir().mkpath(dirPath); KFileItem item(QUrl::fromLocalFile(dirPath)); const mode_t origPerm = item.permissions(); mode_t newPerm = origPerm ^ S_ISVTX; QVERIFY(newPerm != origPerm); KFileItemList items({item}); KIO::Job *job = KIO::chmod(items, newPerm, S_ISVTX, QString(), QString(), false, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); KFileItem newItem(QUrl::fromLocalFile(dirPath)); QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(newPerm, 8)); QVERIFY(QDir().rmdir(dirPath)); } #endif void JobTest::chmodFileError() { // chown(root) should fail const QString filePath = homeTmpDir() + "fileForChmod"; createTestFile(filePath); KFileItem item(QUrl::fromLocalFile(filePath)); const mode_t origPerm = item.permissions(); mode_t newPerm = origPerm ^ S_IWGRP; QVERIFY(newPerm != origPerm); KFileItemList items; items << item; KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QStringLiteral("root"), QString(), false, KIO::HideProgressInfo); // Simulate the user pressing "Skip" in the dialog. PredefinedAnswerJobUiDelegate extension; extension.m_skipResult = KIO::S_SKIP; job->setUiDelegateExtension(&extension); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(extension.m_askSkipCalled, 1); KFileItem newItem(QUrl::fromLocalFile(filePath)); // We skipped, so the chmod didn't happen. QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(origPerm, 8)); QFile::remove(filePath); } void JobTest::mimeType() { #if 1 const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); KIO::MimetypeJob *job = KIO::mimetype(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo); QVERIFY(job); QSignalSpy spyMimeType(job, SIGNAL(mimetype(KIO::Job*,QString))); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(spyMimeType.count(), 1); QCOMPARE(spyMimeType[0][0], QVariant::fromValue(static_cast(job))); QCOMPARE(spyMimeType[0][1].toString(), QStringLiteral("application/octet-stream")); #else // Testing mimetype over HTTP KIO::MimetypeJob *job = KIO::mimetype(QUrl("http://www.kde.org"), KIO::HideProgressInfo); QVERIFY(job); QSignalSpy spyMimeType(job, SIGNAL(mimetype(KIO::Job*,QString))); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(spyMimeType.count(), 1); QCOMPARE(spyMimeType[0][0], QVariant::fromValue(static_cast(job))); QCOMPARE(spyMimeType[0][1].toString(), QString("text/html")); #endif } void JobTest::mimeTypeError() { // KIO::mimetype() on a file that doesn't exist const QString filePath = homeTmpDir() + "doesNotExist"; KIO::MimetypeJob *job = KIO::mimetype(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo); QVERIFY(job); QSignalSpy spyMimeType(job, SIGNAL(mimetype(KIO::Job*,QString))); QSignalSpy spyResult(job, SIGNAL(result(KJob*))); QVERIFY(!job->exec()); QCOMPARE(spyMimeType.count(), 0); QCOMPARE(spyResult.count(), 1); } void JobTest::moveFileDestAlreadyExists() // #157601 { const QString file1 = homeTmpDir() + "fileFromHome"; createTestFile(file1); const QString file2 = homeTmpDir() + "anotherFile"; createTestFile(file2); const QString existingDest = otherTmpDir() + "fileFromHome"; createTestFile(existingDest); QList urls; urls << QUrl::fromLocalFile(file1) << QUrl::fromLocalFile(file2); KIO::CopyJob *job = KIO::move(urls, QUrl::fromLocalFile(otherTmpDir()), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); job->setAutoSkip(true); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(file1)); // it was skipped QVERIFY(!QFile::exists(file2)); // it was moved } void JobTest::moveDestAlreadyExistsAutoRename_data() { QTest::addColumn("samePartition"); QTest::addColumn("moveDirs"); QTest::newRow("files same partition") << true << false; QTest::newRow("files other partition") << false << false; QTest::newRow("dirs same partition") << true << true; QTest::newRow("dirs other partition") << false << true; } void JobTest::moveDestAlreadyExistsAutoRename() { QFETCH(bool, samePartition); QFETCH(bool, moveDirs); QString dir; if (samePartition) { dir = homeTmpDir() + "dir/"; QVERIFY(QDir(dir).exists() || QDir().mkdir(dir)); } else { dir = otherTmpDir(); } moveDestAlreadyExistsAutoRename(dir, moveDirs); if (samePartition) { // cleanup KIO::Job *job = KIO::del(QUrl::fromLocalFile(dir), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dir)); } } void JobTest::moveDestAlreadyExistsAutoRename(const QString &destDir, bool moveDirs) // #256650 { const QString prefix = moveDirs ? QStringLiteral("dir ") : QStringLiteral("file "); const QString file1 = homeTmpDir() + prefix + "(1)"; const QString file2 = homeTmpDir() + prefix + "(2)"; const QString existingDest1 = destDir + prefix + "(1)"; const QString existingDest2 = destDir + prefix + "(2)"; const QStringList sources = QStringList() << file1 << file2 << existingDest1 << existingDest2; for (const QString &source : sources) { if (moveDirs) { QVERIFY(QDir().mkdir(source)); } else { createTestFile(source); } } QList urls; urls << QUrl::fromLocalFile(file1) << QUrl::fromLocalFile(file2); KIO::CopyJob *job = KIO::move(urls, QUrl::fromLocalFile(destDir), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); job->setAutoRename(true); //qDebug() << QDir(destDir).entryList(); QVERIFY2(job->exec(), qPrintable(job->errorString())); qDebug() << QDir(destDir).entryList(); QVERIFY(!QFile::exists(file1)); // it was moved QVERIFY(!QFile::exists(file2)); // it was moved QVERIFY(QFile::exists(existingDest1)); QVERIFY(QFile::exists(existingDest2)); const QString file3 = destDir + prefix + "(3)"; const QString file4 = destDir + prefix + "(4)"; QVERIFY(QFile::exists(file3)); QVERIFY(QFile::exists(file4)); if (moveDirs) { QDir().rmdir(file1); QDir().rmdir(file2); QDir().rmdir(file3); QDir().rmdir(file4); } else { QFile::remove(file1); QFile::remove(file2); QFile::remove(file3); QFile::remove(file4); } } void JobTest::copyDirectoryAlreadyExistsSkip() { // when copying a directory (which contains at least one file) to some location, and then // copying the same dir to the same location again, and clicking "Skip" there should be no // segmentation fault, bug 408350 const QString src = homeTmpDir() + "a"; createTestDirectory(src); const QString dest = homeTmpDir() + "dest"; createTestDirectory(dest); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest + QStringLiteral("/a/testfile"))); job = KIO::copy(u, d, KIO::HideProgressInfo); // Simulate the user pressing "Skip" in the dialog. PredefinedAnswerJobUiDelegate extension; extension.m_skipResult = KIO::S_SKIP; job->setUiDelegateExtension(&extension); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest + QStringLiteral("/a/testfile"))); QDir(src).removeRecursively(); QDir(dest).removeRecursively(); } void JobTest::safeOverwrite_data() { QTest::addColumn("destFileExists"); QTest::newRow("dest file exists") << true; QTest::newRow("dest file doesn't exist") << false; } void JobTest::safeOverwrite() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif QFETCH(bool, destFileExists); const QString srcDir = homeTmpDir() + "overwrite"; const QString srcFile = srcDir + "/testfile"; const QString destDir = otherTmpDir() + "overwrite_other"; const QString destFile = destDir + "/testfile"; const QString destPartFile = destFile + ".part"; createTestDirectory(srcDir); createTestDirectory(destDir); QVERIFY(QFile::resize(srcFile, 1000000)); //~1MB if (!destFileExists) { QVERIFY(QFile::remove(destFile)); } if (otherTmpDirIsOnSamePartition()) { QSKIP(qPrintable(QStringLiteral("This test requires %1 and %2 to be on different partitions").arg(srcDir, destDir))); } KIO::FileCopyJob *job = KIO::file_move(QUrl::fromLocalFile(srcFile), QUrl::fromLocalFile(destFile), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QSignalSpy spyTotalSize(job, &KIO::FileCopyJob::totalSize); connect(job, &KIO::FileCopyJob::totalSize, this, [destFileExists, destPartFile](KJob *job, qulonglong totalSize) { Q_UNUSED(job); Q_UNUSED(totalSize); QCOMPARE(destFileExists, QFile::exists(destPartFile)); }); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(destFile)); QVERIFY(!QFile::exists(srcFile)); QVERIFY(!QFile::exists(destPartFile)); QCOMPARE(spyTotalSize.count(), 1); QDir(srcDir).removeRecursively(); QDir(destDir).removeRecursively(); } void JobTest::moveAndOverwrite() { const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); QString existingDest = otherTmpDir() + "fileFromHome"; createTestFile(existingDest); KIO::FileCopyJob *job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(sourceFile)); // it was moved #ifndef Q_OS_WIN // Now same thing when the target is a symlink to the source createTestFile(sourceFile); createTestSymlink(existingDest, QFile::encodeName(sourceFile)); QVERIFY(QFile::exists(existingDest)); job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(sourceFile)); // it was moved // Now same thing when the target is a symlink to another file createTestFile(sourceFile); createTestFile(sourceFile + QLatin1Char('2')); createTestSymlink(existingDest, QFile::encodeName(sourceFile + QLatin1Char('2'))); QVERIFY(QFile::exists(existingDest)); job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(sourceFile)); // it was moved // Now same thing when the target is a _broken_ symlink createTestFile(sourceFile); createTestSymlink(existingDest); QVERIFY(!QFile::exists(existingDest)); // it exists, but it's broken... job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(sourceFile)); // it was moved #endif } void JobTest::moveOverSymlinkToSelf() // #169547 { #ifndef Q_OS_WIN const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString existingDest = homeTmpDir() + "testlink"; createTestSymlink(existingDest, QFile::encodeName(sourceFile)); QVERIFY(QFile::exists(existingDest)); KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); // and not ERR_IDENTICAL_FILES! QVERIFY(QFile::exists(sourceFile)); // it not moved #endif } void JobTest::createSymlink() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString destDir = homeTmpDir() + "dest"; QVERIFY(QDir().mkpath(destDir)); // With KIO::link (high-level) KIO::CopyJob *job = KIO::link(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(destDir), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo::exists(sourceFile)); const QString dest = destDir + "/fileFromHome"; QVERIFY(QFileInfo(dest).isSymLink()); QCOMPARE(QFileInfo(dest).symLinkTarget(), sourceFile); QFile::remove(dest); // With KIO::symlink (low-level) const QString linkPath = destDir + "/link"; KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(linkPath), KIO::HideProgressInfo); QVERIFY2(symlinkJob->exec(), qPrintable(symlinkJob->errorString())); QVERIFY(QFileInfo::exists(sourceFile)); QVERIFY(QFileInfo(linkPath).isSymLink()); QCOMPARE(QFileInfo(linkPath).symLinkTarget(), sourceFile); // Cleanup QVERIFY(QDir(destDir).removeRecursively()); } void JobTest::createSymlinkTargetDirDoesntExist() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString destDir = homeTmpDir() + "dest/does/not/exist"; KIO::CopyJob *job = KIO::link(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(destDir), KIO::HideProgressInfo); QVERIFY(!job->exec()); QCOMPARE(job->error(), static_cast(KIO::ERR_CANNOT_SYMLINK)); } void JobTest::createSymlinkAsShouldSucceed() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString dest = homeTmpDir() + "testlink"; QFile::remove(dest); // just in case KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo::exists(sourceFile)); QVERIFY(QFileInfo(dest).isSymLink()); QVERIFY(QFile::remove(dest)); } void JobTest::createSymlinkAsShouldFailDirectoryExists() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString dest = homeTmpDir() + "dest"; QVERIFY(QDir().mkpath(dest)); // dest exists as a directory // With KIO::link (high-level) KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_DIR_ALREADY_EXIST); QVERIFY(QFileInfo::exists(sourceFile)); QVERIFY(!QFileInfo::exists(dest + "/fileFromHome")); // With KIO::symlink (low-level) KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!symlinkJob->exec()); QCOMPARE(symlinkJob->error(), (int)KIO::ERR_DIR_ALREADY_EXIST); QVERIFY(QFileInfo::exists(sourceFile)); // Cleanup QVERIFY(QDir().rmdir(dest)); } void JobTest::createSymlinkAsShouldFailFileExists() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString dest = homeTmpDir() + "testlink"; QFile::remove(dest); // just in case // First time works KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); // Second time fails (already exists) job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); // KIO::symlink fails too KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!symlinkJob->exec()); QCOMPARE(symlinkJob->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); // Cleanup QVERIFY(QFile::remove(sourceFile)); QVERIFY(QFile::remove(dest)); } void JobTest::createSymlinkWithOverwriteShouldWork() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString dest = homeTmpDir() + "testlink"; QFile::remove(dest); // just in case // First time works KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); // Changing the link target, with overwrite, works job = KIO::linkAs(QUrl::fromLocalFile(sourceFile + QLatin1Char('2')), QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + QLatin1Char('2'))); // Changing the link target using KIO::symlink, with overwrite, works KIO::Job *symlinkJob = KIO::symlink(sourceFile + QLatin1Char('3'), QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo); QVERIFY2(symlinkJob->exec(), qPrintable(symlinkJob->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + QLatin1Char('3'))); // Cleanup QVERIFY(QFile::remove(dest)); QVERIFY(QFile::remove(sourceFile)); } void JobTest::createBrokenSymlink() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = "/does/not/exist"; const QString dest = homeTmpDir() + "testlink"; QFile::remove(dest); // just in case KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); // Second time fails (already exists) job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); QVERIFY(QFile::remove(dest)); } void JobTest::multiGet() { const int numFiles = 10; const QString baseDir = homeTmpDir(); const QList urls = createManyFiles(baseDir, numFiles); QCOMPARE(urls.count(), numFiles); //qDebug() << file; KIO::MultiGetJob *job = KIO::multi_get(0, urls.at(0), KIO::MetaData()); // TODO: missing KIO::HideProgressInfo QSignalSpy spyData(job, SIGNAL(data(long,QByteArray))); QSignalSpy spyMimeType(job, SIGNAL(mimetype(long,QString))); QSignalSpy spyResultId(job, SIGNAL(result(long))); QSignalSpy spyResult(job, SIGNAL(result(KJob*))); job->setUiDelegate(nullptr); for (int i = 1; i < numFiles; ++i) { const QUrl url = urls.at(i); job->get(i, url, KIO::MetaData()); } //connect(job, &KIO::MultiGetJob::result, [=] (long id) { qDebug() << "ID I got" << id;}); //connect(job, &KJob::result, [this](KJob* ) {qDebug() << "END";}); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(spyResult.count(), 1); QCOMPARE(spyResultId.count(), numFiles); QCOMPARE(spyMimeType.count(), numFiles); QCOMPARE(spyData.count(), numFiles * 2); for (int i = 0; i < numFiles; ++i) { QCOMPARE(spyResultId.at(i).at(0).toInt(), i); QCOMPARE(spyMimeType.at(i).at(0).toInt(), i); QCOMPARE(spyMimeType.at(i).at(1).toString(), QStringLiteral("text/plain")); QCOMPARE(spyData.at(i * 2).at(0).toInt(), i); QCOMPARE(QString(spyData.at(i * 2).at(1).toByteArray()), QStringLiteral("Hello")); QCOMPARE(spyData.at(i * 2 + 1).at(0).toInt(), i); QCOMPARE(QString(spyData.at(i * 2 + 1).at(1).toByteArray()), QLatin1String("")); } } void JobTest::cancelCopyAndCleanDest_data() { QTest::addColumn("suspend"); QTest::addColumn("overwrite"); QTest::newRow("suspend_no_overwrite") << true << false; QTest::newRow("no_suspend_no_overwrite") << false << false; #ifndef Q_OS_WIN QTest::newRow("suspend_with_overwrite") << true << true; QTest::newRow("no_suspend_with_overwrite") << false << true; #endif } void JobTest::cancelCopyAndCleanDest() { QFETCH(bool, suspend); QFETCH(bool, overwrite); const QString baseDir = homeTmpDir(); const QString srcTemplate = baseDir + QStringLiteral("testfile_XXXXXX"); const QString destFile = baseDir + QStringLiteral("testfile_copy"); QTemporaryFile f(srcTemplate); if (!f.open()) { qFatal("Couldn't open %s", qPrintable(f.fileName())); } f.seek(9999999); f.write("0"); f.close(); QCOMPARE(f.size(), 10000000); //~10MB if (overwrite) { createTestFile(destFile); } KIO::JobFlag m_overwriteFlag = overwrite ? KIO::Overwrite : KIO::DefaultFlags; KIO::FileCopyJob *copyJob = KIO::file_copy(QUrl::fromLocalFile(f.fileName()), QUrl::fromLocalFile(destFile), -1, KIO::HideProgressInfo | m_overwriteFlag); copyJob->setUiDelegate(nullptr); QSignalSpy spyProcessedSize(copyJob, &KIO::Job::processedSize); connect(copyJob, &KIO::Job::processedSize, this, [destFile, suspend, overwrite](KJob *job, qulonglong processedSize) { if (processedSize > 0) { const QString destToCheck = (!overwrite) ? destFile : destFile + QStringLiteral(".part"); QVERIFY2(QFile::exists(destToCheck), qPrintable(destToCheck)); if (suspend) { job->suspend(); } job->kill(); QVERIFY(!QFile::exists(destToCheck)); } }); QVERIFY(!copyJob->exec()); QCOMPARE(spyProcessedSize.count(), 1); // Give time to the kioslave to finish copy() and warn about chown/chmod failing (because FileCopyJob::doKill removed the file) // Less confusing if the warnings show here. QTest::qWait(500); QVERIFY(!QFile::exists(destFile)); } diff --git a/autotests/kdirlistertest.cpp b/autotests/kdirlistertest.cpp index 24f48ba7..a7494597 100644 --- a/autotests/kdirlistertest.cpp +++ b/autotests/kdirlistertest.cpp @@ -1,1423 +1,1423 @@ /* This file is part of the KDE project Copyright (C) 2007 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License 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 "kdirlistertest.h" #include #include #include QTEST_MAIN(KDirListerTest) #include #include "kiotesthelper.h" #include #include #include #include #include #define WORKAROUND_BROKEN_INOTIFY 0 void MyDirLister::handleError(KIO::Job *job) { // Currently we don't expect any errors. qCritical() << "KDirLister called handleError!" << job << job->error() << job->errorString(); qFatal("aborting"); } void KDirListerTest::initTestCase() { // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); // To avoid failing on broken locally defined mime types QStandardPaths::setTestModeEnabled(true); KIO::setDefaultJobUiDelegateExtension(nullptr); // no "skip" dialogs m_exitCount = 1; s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-120); // 2 minutes ago // Create test data: /* * PATH/toplevelfile_1 * PATH/toplevelfile_2 * PATH/toplevelfile_3 * PATH/subdir * PATH/subdir/testfile * PATH/subdir/subsubdir * PATH/subdir/subsubdir/testfile */ const QString path = m_tempDir.path() + '/'; createTestFile(path + "toplevelfile_1"); createTestFile(path + "toplevelfile_2"); createTestFile(path + "toplevelfile_3"); createTestDirectory(path + "subdir"); createTestDirectory(path + "subdir/subsubdir"); qRegisterMetaType > >(); } void KDirListerTest::cleanup() { m_dirLister.clearSpies(); disconnect(&m_dirLister, nullptr, this, nullptr); } void KDirListerTest::testOpenUrl() { m_items.clear(); const QString path = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); // The call to openUrl itself, emits started m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_dirLister.spyRedirection.count(), 0); QCOMPARE(m_items.count(), 0); QVERIFY(!m_dirLister.isFinished()); // then wait for completed qDebug("waiting for completed"); QTRY_COMPARE(m_dirLister.spyStarted.count(), 1); QTRY_COMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_dirLister.spyRedirection.count(), 0); //qDebug() << m_items; //qDebug() << "In dir" << QDir(path).entryList( QDir::AllEntries | QDir::NoDotAndDotDot); QCOMPARE(m_items.count(), fileCount()); QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); const QString fileName = QStringLiteral("toplevelfile_3"); const QUrl itemUrl = QUrl::fromLocalFile(path + fileName); KFileItem byName = m_dirLister.findByName(fileName); QVERIFY(!byName.isNull()); QCOMPARE(byName.url().toString(), itemUrl.toString()); QCOMPARE(byName.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); KFileItem byUrl = m_dirLister.findByUrl(itemUrl); QVERIFY(!byUrl.isNull()); QCOMPARE(byUrl.url().toString(), itemUrl.toString()); QCOMPARE(byUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().toString(), itemUrl.toString()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); KFileItem rootByUrl = m_dirLister.findByUrl(QUrl::fromLocalFile(path)); QVERIFY(!rootByUrl.isNull()); QCOMPARE(QString(rootByUrl.url().toLocalFile() + '/'), path); m_dirLister.clearSpies(); // for the tests that call testOpenUrl for setup } // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. void KDirListerTest::testOpenUrlFromCache() { // Do the same again, it should behave the same, even with the items in the cache testOpenUrl(); // Get into the case where another dirlister is holding the items { m_items.clear(); const QString path = m_tempDir.path() + '/'; MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QCOMPARE(secondDirLister.spyStarted.count(), 1); QCOMPARE(secondDirLister.spyCompleted.count(), 0); QCOMPARE(secondDirLister.spyCompletedQUrl.count(), 0); QCOMPARE(secondDirLister.spyCanceled.count(), 0); QCOMPARE(secondDirLister.spyCanceledQUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 1); QCOMPARE(secondDirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 0); QVERIFY(!secondDirLister.isFinished()); // then wait for completed qDebug("waiting for completed"); QTRY_COMPARE(secondDirLister.spyStarted.count(), 1); QTRY_COMPARE(secondDirLister.spyCompleted.count(), 1); QCOMPARE(secondDirLister.spyCompletedQUrl.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); QCOMPARE(secondDirLister.spyCanceledQUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 1); QCOMPARE(secondDirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 4); QVERIFY(secondDirLister.isFinished()); } disconnect(&m_dirLister, nullptr, this, nullptr); } // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. // This test creates 1 file in the temporary directory void KDirListerTest::testNewItem() { QCOMPARE(m_items.count(), 4); const QString path = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); qDebug() << "Creating a new file"; const QString fileName = QStringLiteral("toplevelfile_new"); createSimpleFile(path + fileName); QTRY_COMPARE(m_items.count(), 5); QCOMPARE(m_dirLister.spyStarted.count(), 1); // Updates call started QCOMPARE(m_dirLister.spyCompleted.count(), 1); // and completed QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); const QUrl itemUrl = QUrl::fromLocalFile(path + fileName); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().toString(), itemUrl.toString()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); disconnect(&m_dirLister, nullptr, this, nullptr); } // This test assumes testNewItem was run before. So m_dirLister is holding the items already. // This test creates 100 more files in the temporary directory in reverse order void KDirListerTest::testNewItems() { QCOMPARE(m_items.count(), 5); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); const QString path = m_tempDir.path() + '/'; qDebug() << "Creating 100 new files"; for (int i = 50; i > 0; i--) { createSimpleFile(path + QString("toplevelfile_new_%1").arg(i)); } QTest::qWait(1000); // Create them with 1s difference for (int i = 100; i > 50; i--) { createSimpleFile(path + QString("toplevelfile_new_%1").arg(i)); } // choose one of the new created files const QString fileName = QStringLiteral("toplevelfile_new_50"); QTRY_COMPARE(m_items.count(), 105); QVERIFY(m_dirLister.spyStarted.count() > 0 && m_dirLister.spyStarted.count() < 3); // Updates call started, probably twice QVERIFY(m_dirLister.spyCompleted.count() > 0 && m_dirLister.spyCompleted.count() < 3); // and completed, probably twice QVERIFY(m_dirLister.spyCompletedQUrl.count() < 3); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); const QUrl itemUrl = QUrl::fromLocalFile(path + fileName); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().toString(), itemUrl.toString()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); } void KDirListerTest::benchFindByUrl() { // The time used should be in the order of O(100*log2(100)) const QString path = m_tempDir.path() + '/'; QBENCHMARK { for (int i = 100; i > 0; i--) { KFileItem cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(path + QString("toplevelfile_new_%1").arg(i))); QVERIFY(!cachedItem.isNull()); } } } void KDirListerTest::testNewItemByCopy() { // This test creates a file using KIO::copyAs, like knewmenu.cpp does. // Useful for testing #192185, i.e. whether we catch the kdirwatch event and avoid // a KFileItem::refresh(). const int origItemCount = m_items.count(); const QString path = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); QTest::qWait(1000); // We need a 1s timestamp difference on the dir, otherwise FAM won't notice anything. const QString fileName = QStringLiteral("toplevelfile_copy"); const QUrl itemUrl = QUrl::fromLocalFile(path + fileName); KIO::CopyJob *job = KIO::copyAs(QUrl::fromLocalFile(path + "toplevelfile_3"), itemUrl, KIO::HideProgressInfo); job->exec(); // Give time for KDirWatch/KDirNotify to notify us QTRY_COMPARE(m_items.count(), origItemCount + 1); QCOMPARE(m_dirLister.spyStarted.count(), 1); // Updates call started QCOMPARE(m_dirLister.spyCompleted.count(), 1); // and completed QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); // Give some time to KDirWatch QTest::qWait(1000); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().toString(), itemUrl.toString()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); } void KDirListerTest::testNewItemsInSymlink() // #213799 { const int origItemCount = m_items.count(); QCOMPARE(fileCount(), origItemCount); const QString path = m_tempDir.path() + '/'; QTemporaryFile tempFile; QVERIFY(tempFile.open()); const QString symPath = tempFile.fileName() + "_link"; tempFile.close(); const bool symlinkOk = KIOPrivate::createSymlink(path, symPath); if (!symlinkOk) { const QString error = QString::fromLatin1("Failed to create symlink '%1' pointing to '%2': %3") .arg(symPath, path, QString::fromLocal8Bit(strerror(errno))); QVERIFY2(symlinkOk, qPrintable(error)); } MyDirLister dirLister2; m_items2.clear(); connect(&dirLister2, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); // The initial listing dirLister2.openUrl(QUrl::fromLocalFile(symPath), KDirLister::NoFlags); QTRY_COMPARE(m_items.count(), origItemCount); QTRY_VERIFY(dirLister2.isFinished()); QTest::qWait(1000); // We need a 1s timestamp difference on the dir, otherwise FAM won't notice anything. qDebug() << "Creating new file"; const QString fileName = QStringLiteral("toplevelfile_newinlink"); createSimpleFile(path + fileName); #if WORKAROUND_BROKEN_INOTIFY org::kde::KDirNotify::emitFilesAdded(path); #endif // Give time for KDirWatch to notify us QTRY_COMPARE(m_items2.count(), origItemCount + 1); QTRY_COMPARE(m_items.count(), origItemCount + 1); // Now create an item using the symlink-path const QString fileName2 = QStringLiteral("toplevelfile_newinlink2"); { createSimpleFile(path + fileName2); // Give time for KDirWatch to notify us QTRY_COMPARE(m_items2.count(), origItemCount + 2); QTRY_COMPARE(m_items.count(), origItemCount + 2); } QCOMPARE(fileCount(), m_items.count()); // Test file deletion { qDebug() << "Deleting" << (path + fileName); QTest::qWait(1000); // for timestamp difference QFile::remove(path + fileName); QTRY_COMPARE(dirLister2.spyItemsDeleted.count(), 1); const KFileItem item = dirLister2.spyItemsDeleted[0][0].value().at(0); QCOMPARE(item.url().toLocalFile(), QString(symPath + '/' + fileName)); } // TODO: test file update. disconnect(&m_dirLister, nullptr, this, nullptr); QFile::remove(symPath); } // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. // Modifies one of the files to have html content void KDirListerTest::testRefreshItems() { m_refreshedItems.clear(); const QString path = m_tempDir.path() + '/'; const QString fileName = path + "toplevelfile_1"; KFileItem cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(fileName)); QVERIFY(!cachedItem.isNull()); QCOMPARE(cachedItem.mimetype(), QString("application/octet-stream")); connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems); QFile file(fileName); QVERIFY(file.open(QIODevice::Append)); file.write(QByteArray("")); file.close(); QCOMPARE(QFileInfo(fileName).size(), 11LL /*Hello world*/ + 6 /**/); QTRY_VERIFY(!m_refreshedItems.isEmpty()); QCOMPARE(m_dirLister.spyStarted.count(), 0); // fast path: no directory listing needed QVERIFY(m_dirLister.spyCompleted.count() < 2); QVERIFY(m_dirLister.spyCompletedQUrl.count() < 2); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_refreshedItems.count(), 1); QPair entry = m_refreshedItems.first(); QCOMPARE(entry.first.url().toLocalFile(), fileName); QCOMPARE(entry.first.size(), KIO::filesize_t(11)); QCOMPARE(entry.first.mimetype(), QString("application/octet-stream")); QCOMPARE(entry.second.url().toLocalFile(), fileName); QCOMPARE(entry.second.size(), KIO::filesize_t(11 /*Hello world*/ + 6 /**/)); QCOMPARE(entry.second.mimetype(), QString("text/html")); // Let's see what KDirLister has in cache now cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(fileName)); QCOMPARE(cachedItem.size(), KIO::filesize_t(11 /*Hello world*/ + 6 /**/)); m_refreshedItems.clear(); } // Refresh the root item, plus a hidden file, e.g. changing its icon. #190535 void KDirListerTest::testRefreshRootItem() { // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. m_refreshedItems.clear(); m_refreshedItems2.clear(); // The item will be the root item of dirLister2, but also a child item // of m_dirLister. // In #190535 it would show "." instead of the subdir name, after a refresh... const QString path = m_tempDir.path() + '/' + "subdir"; MyDirLister dirLister2; fillDirLister2(dirLister2, path); // Change the subdir by creating a file in it waitUntilMTimeChange(path); const QString foobar = path + "/.foobar"; createSimpleFile(foobar); connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems); // Arguably, the mtime change of "subdir" should lead to a refreshItem of subdir in the root dir. // So the next line shouldn't be necessary, if KDirLister did this correctly. This isn't what this test is about though. org::kde::KDirNotify::emitFilesChanged(QList() << QUrl::fromLocalFile(path)); QTRY_VERIFY(!m_refreshedItems.isEmpty()); QCOMPARE(m_dirLister.spyStarted.count(), 0); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_refreshedItems.count(), 1); QPair entry = m_refreshedItems.first(); QCOMPARE(entry.first.url().toLocalFile(), path); QCOMPARE(entry.first.name(), QString("subdir")); QCOMPARE(entry.second.url().toLocalFile(), path); QCOMPARE(entry.second.name(), QString("subdir")); QCOMPARE(m_refreshedItems2.count(), 1); entry = m_refreshedItems2.first(); QCOMPARE(entry.first.url().toLocalFile(), path); QCOMPARE(entry.second.url().toLocalFile(), path); // item name() doesn't matter here, it's the root item. m_refreshedItems.clear(); m_refreshedItems2.clear(); waitUntilMTimeChange(path); const QString directoryFile = path + "/.directory"; createSimpleFile(directoryFile); org::kde::KDirNotify::emitFilesAdded(QUrl::fromLocalFile(path)); QTest::qWait(200); // The order of these two is not deterministic org::kde::KDirNotify::emitFilesChanged(QList() << QUrl::fromLocalFile(directoryFile)); org::kde::KDirNotify::emitFilesChanged(QList() << QUrl::fromLocalFile(path)); QTRY_VERIFY(!m_refreshedItems.isEmpty()); QCOMPARE(m_refreshedItems.count(), 1); entry = m_refreshedItems.first(); QCOMPARE(entry.first.url().toLocalFile(), path); QCOMPARE(entry.second.url().toLocalFile(), path); m_refreshedItems.clear(); m_refreshedItems2.clear(); // Note: this test leaves the .directory file as a side effect. // Hidden though, shouldn't matter. } void KDirListerTest::testDeleteItem() { testOpenUrl(); // ensure m_items is uptodate const int origItemCount = m_items.count(); QCOMPARE(fileCount(), origItemCount); const QString path = m_tempDir.path() + '/'; //qDebug() << "Removing " << path+"toplevelfile_new"; QFile::remove(path + QString("toplevelfile_new")); // the remove() doesn't always trigger kdirwatch in stat mode, if this all happens in the same second KDirWatch::self()->setDirty(path); // The signal should be emitted once with the deleted file QTRY_COMPARE(m_dirLister.spyItemsDeleted.count(), 1); // OK now kdirlister told us the file was deleted, let's try a re-listing m_items.clear(); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QVERIFY(!m_dirLister.isFinished()); QTRY_COMPARE(m_items.count(), origItemCount - 1); QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); QCOMPARE(fileCount(), m_items.count()); } void KDirListerTest::testDeleteItems() { testOpenUrl(); // ensure m_items is uptodate const int origItemCount = m_items.count(); QCOMPARE(fileCount(), origItemCount); const QString path = m_tempDir.path() + '/'; qDebug() << "Removing 100 files from " << path; for (int i=0; i <= 100; ++i) { QFile::remove(path + QString("toplevelfile_new_%1").arg(i)); } // the remove() doesn't always trigger kdirwatch in stat mode, if this all happens in the same second KDirWatch::self()->setDirty(path); // The signal could be emitted 1 time with all the deleted files or more times QTRY_VERIFY(m_dirLister.spyItemsDeleted.count() > 0); // OK now kdirlister told us the file was deleted, let's try a re-listing m_items.clear(); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QTRY_COMPARE(m_items.count(), origItemCount - 100); QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); QCOMPARE(fileCount(), m_items.count()); } void KDirListerTest::testRenameItem() { m_refreshedItems2.clear(); const QString dirPath = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems2); const QString path = dirPath + "toplevelfile_2"; const QString newPath = dirPath + "toplevelfile_2.renamed.cpp"; KIO::SimpleJob *job = KIO::rename(QUrl::fromLocalFile(path), QUrl::fromLocalFile(newPath), KIO::HideProgressInfo); QVERIFY(job->exec()); QSignalSpy spyRefreshItems(&m_dirLister, SIGNAL(refreshItems(QList>))); QVERIFY(spyRefreshItems.wait(2000)); QTRY_COMPARE(m_refreshedItems2.count(), 1); QPair entry = m_refreshedItems2.first(); QCOMPARE(entry.first.url().toLocalFile(), path); QCOMPARE(entry.first.mimetype(), QString("application/octet-stream")); QCOMPARE(entry.second.url().toLocalFile(), newPath); QCOMPARE(entry.second.mimetype(), QString("text/x-c++src")); disconnect(&m_dirLister, nullptr, this, nullptr); // Let's see what KDirLister has in cache now KFileItem cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(newPath)); QVERIFY(!cachedItem.isNull()); QCOMPARE(cachedItem.url().toLocalFile(), newPath); KFileItem oldCachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(path)); QVERIFY(oldCachedItem.isNull()); m_refreshedItems2.clear(); } void KDirListerTest::testRenameAndOverwrite() // has to be run after testRenameItem { // Rename toplevelfile_2.renamed.html to toplevelfile_2, overwriting it. const QString dirPath = m_tempDir.path() + '/'; const QString path = dirPath + "toplevelfile_2"; createTestFile(path); #if WORKAROUND_BROKEN_INOTIFY org::kde::KDirNotify::emitFilesAdded(dirPath); #endif KFileItem existingItem; while (existingItem.isNull()) { QTest::qWait(100); existingItem = m_dirLister.findByUrl(QUrl::fromLocalFile(path)); }; QCOMPARE(existingItem.url().toLocalFile(), path); m_refreshedItems.clear(); connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems); const QString newPath = dirPath + "toplevelfile_2.renamed.cpp"; KIO::SimpleJob *job = KIO::rename(QUrl::fromLocalFile(newPath), QUrl::fromLocalFile(path), KIO::Overwrite | KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); if (m_refreshedItems.isEmpty()) { QTRY_VERIFY(!m_refreshedItems.isEmpty()); // could come from KDirWatch or KDirNotify. } // Check that itemsDeleted was emitted -- preferably BEFORE refreshItems, // but we can't easily check that with QSignalSpy... QCOMPARE(m_dirLister.spyItemsDeleted.count(), 1); QCOMPARE(m_refreshedItems.count(), 1); QPair entry = m_refreshedItems.first(); QCOMPARE(entry.first.url().toLocalFile(), newPath); QCOMPARE(entry.second.url().toLocalFile(), path); disconnect(&m_dirLister, nullptr, this, nullptr); // Let's see what KDirLister has in cache now KFileItem cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(path)); QCOMPARE(cachedItem.url().toLocalFile(), path); KFileItem oldCachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(newPath)); QVERIFY(oldCachedItem.isNull()); m_refreshedItems.clear(); } void KDirListerTest::testConcurrentListing() { const int origItemCount = m_items.count(); QCOMPARE(fileCount(), origItemCount); m_items.clear(); m_items2.clear(); MyDirLister dirLister2; const QString path = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); connect(&dirLister2, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); // Before dirLister2 has time to emit the items, let's make m_dirLister move to another dir. // This reproduces the use case "clicking on a folder in dolphin iconview, and dirlister2 // is the one used by the "folder panel". m_dirLister is going to list the subdir, // while dirLister2 wants to list the folder that m_dirLister has just left. dirLister2.stop(); // like dolphin does, noop. dirLister2.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); m_dirLister.openUrl(QUrl::fromLocalFile(path + "subdir"), KDirLister::NoFlags); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 0); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), 0); QVERIFY(!m_dirLister.isFinished()); QVERIFY(!dirLister2.isFinished()); // then wait for completed qDebug("waiting for completed"); //QCOMPARE(m_dirLister.spyStarted.count(), 1); // 2 when subdir is already in cache. QTRY_COMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 3); QTRY_COMPARE(dirLister2.spyStarted.count(), 1); QTRY_COMPARE(dirLister2.spyCompleted.count(), 1); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 1); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), origItemCount); if (!m_dirLister.isFinished()) { // false when an update is running because subdir is already in cache // TODO check why this fails QVERIFY(m_dirLister.spyCanceled.wait(1000)); QTest::qWait(1000); } disconnect(&m_dirLister, nullptr, this, nullptr); disconnect(&dirLister2, nullptr, this, nullptr); } void KDirListerTest::testConcurrentHoldingListing() { // #167851. // A dirlister holding the items, and a second dirlister does // openUrl(reload) (which triggers updateDirectory()) // and the first lister immediately does openUrl() (which emits cached items). testOpenUrl(); // ensure m_dirLister holds the items. const int origItemCount = m_items.count(); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_items.clear(); m_items2.clear(); const QString path = m_tempDir.path() + '/'; MyDirLister dirLister2; connect(&dirLister2, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); dirLister2.openUrl(QUrl::fromLocalFile(path), KDirLister::Reload); // will start a list job QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(m_items2.count(), 0); qDebug("calling m_dirLister.openUrl"); m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); // should emit cached items, and then "join" the running listjob QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(m_items2.count(), 0); qDebug("waiting for completed"); QTRY_COMPARE(dirLister2.spyStarted.count(), 1); QTRY_COMPARE(dirLister2.spyCompleted.count(), 1); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 1); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), origItemCount); QTRY_COMPARE(m_dirLister.spyStarted.count(), 1); QTRY_COMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QVERIFY(dirLister2.isFinished()); QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); QCOMPARE(m_items.count(), origItemCount); } void KDirListerTest::testConcurrentListingAndStop() { m_items.clear(); m_items2.clear(); MyDirLister dirLister2; // Use a new tempdir for this test, so that we don't use the cache at all. QTemporaryDir tempDir; const QString path = tempDir.path() + '/'; createTestFile(path + "file_1"); createTestFile(path + "file_2"); createTestFile(path + "file_3"); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); connect(&dirLister2, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); // Before m_dirLister has time to emit the items, let's make dirLister2 call stop(). // This should not stop the list job for m_dirLister (#267709). dirLister2.openUrl(QUrl::fromLocalFile(path), KDirLister::Reload); m_dirLister.openUrl(QUrl::fromLocalFile(path)/*, KDirLister::Reload*/); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 0); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), 0); QVERIFY(!m_dirLister.isFinished()); QVERIFY(!dirLister2.isFinished()); dirLister2.stop(); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 0); QCOMPARE(dirLister2.spyCanceled.count(), 1); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 1); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), 0); // then wait for completed qDebug("waiting for completed"); QTRY_COMPARE(m_items.count(), 3); QTRY_COMPARE(m_items2.count(), 0); QTRY_VERIFY(m_dirLister.isFinished()); //QCOMPARE(m_dirLister.spyStarted.count(), 1); // 2 when in cache QCOMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); disconnect(&m_dirLister, nullptr, this, nullptr); } void KDirListerTest::testDeleteListerEarly() { // Do the same again, it should behave the same, even with the items in the cache testOpenUrl(); // Start a second lister, it will get a cached items job, but delete it before the job can run //qDebug() << "=========================================="; { m_items.clear(); const QString path = m_tempDir.path() + '/'; MyDirLister secondDirLister; secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QVERIFY(!secondDirLister.isFinished()); } //qDebug() << "=========================================="; // Check if we didn't keep the deleted dirlister in one of our lists. // I guess the best way to do that is to just list the same dir again. testOpenUrl(); } void KDirListerTest::testOpenUrlTwice() { // Calling openUrl(reload)+openUrl(normal) before listing even starts. const int origItemCount = m_items.count(); m_items.clear(); const QString path = m_tempDir.path() + '/'; MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::Reload); // will start QCOMPARE(secondDirLister.spyStarted.count(), 1); QCOMPARE(secondDirLister.spyCompleted.count(), 0); qDebug("calling openUrl again"); secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); // will stop + start qDebug("waiting for completed"); QTRY_COMPARE(secondDirLister.spyStarted.count(), 2); QTRY_COMPARE(secondDirLister.spyCompleted.count(), 1); QCOMPARE(secondDirLister.spyCompletedQUrl.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); // should not be emitted, see next test QCOMPARE(secondDirLister.spyCanceledQUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 2); QCOMPARE(secondDirLister.spyClearQUrl.count(), 0); if (origItemCount) { // 0 if running this test separately QCOMPARE(m_items.count(), origItemCount); } QVERIFY(secondDirLister.isFinished()); disconnect(&secondDirLister, nullptr, this, nullptr); } void KDirListerTest::testOpenUrlTwiceWithKeep() { // Calling openUrl(reload)+openUrl(keep) on a new dir, // before listing even starts (#177387) // Well, in 177387 the second openUrl call was made from within slotCanceled // called by the first openUrl // (slotLoadingFinished -> setCurrentItem -> expandToUrl -> listDir), // which messed things up in kdirlister (unexpected reentrancy). m_items.clear(); const QString path = m_tempDir.path() + "/newsubdir"; QDir().mkdir(path); MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(QUrl::fromLocalFile(path)); // will start a list job QCOMPARE(secondDirLister.spyStarted.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); QCOMPARE(secondDirLister.spyCompleted.count(), 0); qDebug("calling openUrl again"); secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::Keep); // stops and restarts the job qDebug("waiting for completed"); QTRY_COMPARE(secondDirLister.spyStarted.count(), 2); QTRY_COMPARE(secondDirLister.spyCompleted.count(), 1); QCOMPARE(secondDirLister.spyCompletedQUrl.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); // should not be emitted, it led to recursion QCOMPARE(secondDirLister.spyCanceledQUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 1); QCOMPARE(secondDirLister.spyClearQUrl.count(), 1); QCOMPARE(m_items.count(), 0); QVERIFY(secondDirLister.isFinished()); disconnect(&secondDirLister, nullptr, this, nullptr); QDir().remove(path); } void KDirListerTest::testOpenAndStop() { m_items.clear(); const QString path = QStringLiteral("/"); // better not use a directory that we already listed! connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); qDebug() << "Calling stop!"; m_dirLister.stop(); // we should also test stop(QUrl::fromLocalFile(path))... QCOMPARE(m_dirLister.spyStarted.count(), 1); // The call to openUrl itself, emits started QCOMPARE(m_dirLister.spyCompleted.count(), 0); // we had time to stop before the job even started QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 1); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 1); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 0); // we had time to stop before the job even started QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); } // A bug in the decAutoUpdate/incAutoUpdate logic made KDirLister stop watching a directory for changes, // and never watch it again when opening it from the cache. void KDirListerTest::testBug211472() { m_items.clear(); QTemporaryDir newDir; const QString path = newDir.path() + "/newsubdir/"; QDir().mkdir(path); MyDirLister dirLister; connect(&dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); dirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QVERIFY(dirLister.isFinished()); QVERIFY(m_items.isEmpty()); if (true) { // This block is required to trigger bug 211472. // Go 'up' to the parent of 'newsubdir'. dirLister.openUrl(QUrl::fromLocalFile(newDir.path())); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(dirLister.isFinished()); QTRY_VERIFY(!m_items.isEmpty()); m_items.clear(); // Create a file in 'newsubdir' while we are listing its parent dir. createTestFile(path + "newFile-1"); // At this point, newsubdir is not used, so it's moved to the cache. // This happens in checkUpdate, called when receiving a notification for the cached dir, // this is why this unittest needs to create a test file in the subdir. // wait a second and ensure the list is still empty afterwards QTest::qWait(1000); QTRY_VERIFY(m_items.isEmpty()); // Return to 'newsubdir'. It will be emitted from the cache, then an update will happen. dirLister.openUrl(QUrl::fromLocalFile(path)); // Check that completed is emitted twice QVERIFY(spyCompleted.wait(1000)); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(dirLister.isFinished()); QTRY_COMPARE(m_items.count(), 1); m_items.clear(); } // Now try to create a second file in 'newsubdir' and verify that the // dir lister notices it. QTest::qWait(1000); // We need a 1s timestamp difference on the dir, otherwise FAM won't notice anything. createTestFile(path + "newFile-2"); QTRY_COMPARE(m_items.count(), 1); newDir.remove(); QSignalSpy spyClear(&dirLister, SIGNAL(clear())); QVERIFY(spyClear.wait(1000)); } void KDirListerTest::testRenameCurrentDir() // #294445 { m_items.clear(); const QString path = m_tempDir.path() + "/newsubdir-1"; QVERIFY(QDir().mkdir(path)); MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&secondDirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QVERIFY(secondDirLister.isFinished()); QVERIFY(m_items.empty()); QCOMPARE(secondDirLister.rootItem().url().toLocalFile(), path); const QString newPath = m_tempDir.path() + "/newsubdir-2"; QVERIFY(QDir().rename(path, newPath)); org::kde::KDirNotify::emitFileRenamed(QUrl::fromLocalFile(path), QUrl::fromLocalFile(newPath)); QSignalSpy spyRedirection(&secondDirLister, SIGNAL(redirection(QUrl,QUrl))); QVERIFY(spyRedirection.wait(1000)); // Check that the URL of the root item got updated QCOMPARE(secondDirLister.rootItem().url().toLocalFile(), newPath); disconnect(&secondDirLister, nullptr, this, nullptr); QDir().rmdir(newPath); } void KDirListerTest::slotOpenUrlOnRename(const QUrl &newUrl) { QVERIFY(m_dirLister.openUrl(newUrl)); } //This tests for a crash if you connect redirects to openUrl, due //to internal data being inconsistently exposed. //Matches usage in gwenview. void KDirListerTest::testRenameCurrentDirOpenUrl() { m_items.clear(); const QString path = m_tempDir.path() + "/newsubdir-1/"; QVERIFY(QDir().mkdir(path)); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); // Wait for the signal completed to be emitted QVERIFY(spyCompleted.wait(1000)); QVERIFY(m_dirLister.isFinished()); const QString newPath = m_tempDir.path() + "/newsubdir-2"; QVERIFY(QDir().rename(path, newPath)); org::kde::KDirNotify::emitFileRenamed(QUrl::fromLocalFile(path), QUrl::fromLocalFile(newPath)); //Connect the redirection to openURL, so that on a rename the new location is opened. //This matches usage in gwenview, and crashes connect(&m_dirLister, QOverload::of(&KCoreDirLister::redirection), this, &KDirListerTest::slotOpenUrlOnRename); QTRY_VERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); QDir().rmdir(newPath); } void KDirListerTest::testRedirection() { m_items.clear(); const QUrl url(QStringLiteral("file://somemachine/")); if (!KProtocolInfo::isKnownProtocol(QStringLiteral("smb"))) { QSKIP("smb not installed"); } connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); // The call to openUrl itself, emits started m_dirLister.openUrl(url, KDirLister::NoFlags); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_dirLister.spyRedirection.count(), 0); QCOMPARE(m_items.count(), 0); QVERIFY(!m_dirLister.isFinished()); // then wait for the redirection signal qDebug("waiting for redirection"); QTRY_COMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); // we stopped before the listing. QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QTRY_COMPARE(m_dirLister.spyClear.count(), 2); // redirection cleared a second time (just in case...) QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QTRY_COMPARE(m_dirLister.spyRedirection.count(), 1); QVERIFY(m_items.isEmpty()); QVERIFY(!m_dirLister.isFinished()); m_dirLister.stop(url); QVERIFY(!m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); } void KDirListerTest::testListEmptyDirFromCache() // #278431 { m_items.clear(); QTemporaryDir newDir; const QUrl url = QUrl::fromLocalFile(newDir.path()); // List and watch an empty dir connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(url); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QVERIFY(m_dirLister.isFinished()); QVERIFY(m_items.isEmpty()); // List it with two more dirlisters (one will create a cached items job, the second should also benefit from it) MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(url); MyDirLister thirdDirLister; connect(&thirdDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); thirdDirLister.openUrl(url); // The point of this test is that (with DEBUG_CACHE enabled) it used to assert here // with "HUH? Lister KDirLister(0x7ffd1f044260) is supposed to be listing, but has no job!" // due to the if (!itemU->lstItems.isEmpty()) check which is now removed. QVERIFY(!secondDirLister.isFinished()); // we didn't go to the event loop yet QSignalSpy spySecondCompleted(&secondDirLister, SIGNAL(completed())); QVERIFY(spySecondCompleted.wait(1000)); if (!thirdDirLister.isFinished()) { QSignalSpy spyThirdCompleted(&thirdDirLister, SIGNAL(completed())); QVERIFY(spyThirdCompleted.wait(1000)); } } void KDirListerTest::testWatchingAfterCopyJob() // #331582 { m_items.clear(); QTemporaryDir newDir; const QString path = newDir.path() + '/'; // List and watch an empty dir connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QVERIFY(m_dirLister.isFinished()); QVERIFY(m_items.isEmpty()); // Create three subfolders. QVERIFY(QDir().mkdir(path + "New Folder")); QVERIFY(QDir().mkdir(path + "New Folder 1")); QVERIFY(QDir().mkdir(path + "New Folder 2")); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(m_dirLister.isFinished()); QTRY_COMPARE(m_items.count(), 3); // Create a new file and verify that the dir lister notices it. m_items.clear(); createTestFile(path + QLatin1Char('a')); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(m_dirLister.isFinished()); QTRY_COMPARE(m_items.count(), 1); // Rename one of the subfolders. const QString oldPath = path + "New Folder 1"; const QString newPath = path + "New Folder 1a"; // NOTE: The following two lines are required to trigger the bug! KIO::Job *job = KIO::moveAs(QUrl::fromLocalFile(oldPath), QUrl::fromLocalFile(newPath), KIO::HideProgressInfo); job->exec(); // Now try to create a second new file and verify that the // dir lister notices it. m_items.clear(); createTestFile(path + QLatin1Char('b')); // This should end up in "KCoreDirListerCache::slotFileDirty" QTRY_COMPARE(m_items.count(), 1); newDir.remove(); QSignalSpy clearSpy(&m_dirLister, SIGNAL(clear())); QVERIFY(clearSpy.wait(1000)); } void KDirListerTest::testRemoveWatchedDirectory() { m_items.clear(); QTemporaryDir newDir; const QString path = newDir.path() + '/'; // List and watch an empty dir connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(m_dirLister.isFinished()); QTRY_VERIFY(m_items.isEmpty()); // Create a subfolder. const QString subDirPath = path + "abc"; QVERIFY(QDir().mkdir(subDirPath)); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(m_dirLister.isFinished()); QTRY_COMPARE(m_items.count(), 1); const KFileItem item = m_items.at(0); // Watch the subfolder for changes, independently. // This is what triggers the bug. // (Technically, this could become a KDirWatch unittest, but if one day we use QFSW, good to have the tests here) KDirWatch watcher; watcher.addDir(subDirPath); // Remove the subfolder. m_items.clear(); QVERIFY(QDir().rmdir(path + "abc")); // This should trigger an update. QVERIFY(spyCompleted.wait(1000)); QVERIFY(m_dirLister.isFinished()); QCOMPARE(m_items.count(), 0); QCOMPARE(m_dirLister.spyItemsDeleted.count(), 1); const KFileItem deletedItem = m_dirLister.spyItemsDeleted.at(0).at(0).value().at(0); QCOMPARE(item, deletedItem); } void KDirListerTest::testDirPermissionChange() { QTemporaryDir tempDir; const QString path = tempDir.path() + '/'; const QString subdir = path + QLatin1String("subdir"); QVERIFY(QDir().mkdir(subdir)); MyDirLister mylister; mylister.openUrl(QUrl::fromLocalFile(tempDir.path())); QSignalSpy spyCompleted(&mylister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); KFileItemList list = mylister.items(); QVERIFY(mylister.isFinished()); QCOMPARE(list.count(), 1); QCOMPARE(mylister.rootItem().url().toLocalFile(), tempDir.path()); const mode_t permissions = (S_IRUSR | S_IWUSR | S_IXUSR); KIO::SimpleJob *job = KIO::chmod(list.first().url(), permissions); QVERIFY(job->exec()); QSignalSpy spyRefreshItems(&mylister, SIGNAL(refreshItems(QList>))); QVERIFY(spyRefreshItems.wait(2000)); list = mylister.items(); QCOMPARE(list.first().permissions(), permissions); QVERIFY(QDir().rmdir(subdir)); } void KDirListerTest::slotNewItems(const KFileItemList &lst) { m_items += lst; } void KDirListerTest::slotNewItems2(const KFileItemList &lst) { m_items2 += lst; } void KDirListerTest::slotRefreshItems(const QList > &lst) { m_refreshedItems += lst; emit refreshItemsReceived(); } void KDirListerTest::slotRefreshItems2(const QList > &lst) { m_refreshedItems2 += lst; } void KDirListerTest::testCopyAfterListingAndMove() // #353195 { const QString dirA = m_tempDir.path() + "/a"; QVERIFY(QDir().mkdir(dirA)); const QString dirB = m_tempDir.path() + "/b"; QVERIFY(QDir().mkdir(dirB)); // ensure m_dirLister holds the items. m_dirLister.openUrl(QUrl::fromLocalFile(path()), KDirLister::NoFlags); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait()); // Move b into a KIO::Job *moveJob = KIO::move(QUrl::fromLocalFile(dirB), QUrl::fromLocalFile(dirA)); moveJob->setUiDelegate(nullptr); QVERIFY(moveJob->exec()); QVERIFY(QFileInfo(m_tempDir.path() + "/a/b").isDir()); // Give some time to processPendingUpdates QTest::qWait(1000); // Copy folder a elsewhere const QString dest = m_tempDir.path() + "/subdir"; KIO::Job *copyJob = KIO::copy(QUrl::fromLocalFile(dirA), QUrl::fromLocalFile(dest)); copyJob->setUiDelegate(nullptr); QVERIFY(copyJob->exec()); QVERIFY(QFileInfo(m_tempDir.path() + "/subdir/a/b").isDir()); } void KDirListerTest::testRenameDirectory() // #401552 { // Create the directory structure to reproduce the bug in a reliable way const QString dirW = m_tempDir.path() + "/w"; QVERIFY(QDir().mkdir(dirW)); const QString dirW1 = m_tempDir.path() + "/w/Files"; QVERIFY(QDir().mkdir(dirW1)); const QString dirW2 = m_tempDir.path() + "/w/Files/Files"; QVERIFY(QDir().mkdir(dirW2)); // Place some empty files in each directory for (int i=0; i < 50; i++) { createSimpleFile(dirW + QString("t_%1").arg(i)); } for (int i=0; i < 50; i++) { createSimpleFile(dirW + QString("z_%1").arg(i)); } // Place some empty files with prefix Files in w. Note that / is missing. for (int i=0; i < 50; i++) { createSimpleFile(dirW1 + QString("t_%1").arg(i)); } for (int i=0; i < 50; i++) { createSimpleFile(dirW1 + QString("z_%1").arg(i)); } // Place some empty files with prefix Files in w/Files. Note that / is missing. for (int i=0; i < 50; i++) { createSimpleFile(dirW2 + QString("t_%1").arg(i)); } for (int i=0; i < 50; i++) { createSimpleFile(dirW2 + QString("z_%1").arg(i)); } // Listen to the w directory m_dirLister.openUrl(QUrl::fromLocalFile(dirW), KDirLister::NoFlags); // Try to reproduce the bug #401552 renaming the w directory several times if needed const QStringList dirs = { dirW + "___", dirW + QLatin1Char('_'), dirW + "______", dirW + "_c", dirW + "___", dirW + "_________" }; QString currDir = dirW; KIO::SimpleJob *job = nullptr; //Connect the redirection to openURL, so that on a rename the new location is opened. connect(&m_dirLister, QOverload::of(&KCoreDirLister::redirection), this, &KDirListerTest::slotOpenUrlOnRename); for (int i=0; i < dirs.size(); i++) { // Wait for the listener to get all files QTRY_VERIFY(m_dirLister.isFinished()); // Do the rename QString newDir = dirs.at(i); job = KIO::rename(QUrl::fromLocalFile(currDir), QUrl::fromLocalFile(newDir), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QTest::qWait(500); // Without the delay the crash doesn't happen currDir = newDir; } disconnect(&m_dirLister, nullptr, this, nullptr); } void KDirListerTest::testDeleteCurrentDir() { // ensure m_dirLister holds the items. m_dirLister.openUrl(QUrl::fromLocalFile(path()), KDirLister::NoFlags); m_dirLister.clearSpies(); KIO::DeleteJob *job = KIO::del(QUrl::fromLocalFile(path()), KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); QTRY_COMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QList deletedUrls; for (int i = 0; i < m_dirLister.spyItemsDeleted.count(); ++i) { deletedUrls += m_dirLister.spyItemsDeleted[i][0].value().urlList(); } //qDebug() << deletedUrls; QUrl currentDirUrl = QUrl::fromLocalFile(path()).adjusted(QUrl::StripTrailingSlash); // Sometimes I get ("current/subdir", "current") here, but that seems ok. QVERIFY(deletedUrls.contains(currentDirUrl)); } int KDirListerTest::fileCount() const { return QDir(path()).entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(); } void KDirListerTest::createSimpleFile(const QString &fileName) { QFile file(fileName); QVERIFY(file.open(QIODevice::WriteOnly)); file.write(QByteArray("foo")); file.close(); } void KDirListerTest::fillDirLister2(MyDirLister &lister, const QString &path) { m_items2.clear(); connect(&lister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems2); lister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QTRY_VERIFY(lister.isFinished()); } void KDirListerTest::waitUntilMTimeChange(const QString &path) { // Wait until the current second is more than the file's mtime // otherwise this change will go unnoticed QFileInfo fi(path); QVERIFY(fi.exists()); - const QDateTime ctime = qMax(fi.lastModified(), fi.created()); - waitUntilAfter(ctime); + const QDateTime mtime = fi.lastModified(); + waitUntilAfter(mtime); } void KDirListerTest::waitUntilAfter(const QDateTime &ctime) { int totalWait = 0; QDateTime now; Q_FOREVER { now = QDateTime::currentDateTime(); - if (now.toTime_t() == ctime.toTime_t()) { // truncate milliseconds + if (now.toSecsSinceEpoch() == ctime.toSecsSinceEpoch()) { // truncate milliseconds totalWait += 50; QTest::qWait(50); } else { QVERIFY(now > ctime); // can't go back in time ;) QTest::qWait(50); // be safe break; } } //if (totalWait > 0) qDebug() << "Waited" << totalWait << "ms so that now" << now.toString(Qt::ISODate) << "is >" << ctime.toString(Qt::ISODate); } #include "moc_kdirlistertest.cpp" diff --git a/autotests/kdirmodeltest.cpp b/autotests/kdirmodeltest.cpp index 823a3f6e..0b1676b8 100644 --- a/autotests/kdirmodeltest.cpp +++ b/autotests/kdirmodeltest.cpp @@ -1,1522 +1,1522 @@ /* This file is part of the KDE project Copyright 2006 - 2007 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License 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 "kdirmodeltest.h" #include #include #include #include #include #include //TODO #include "../../kdeui/tests/proxymodeltestsuite/modelspy.h" #include #ifdef Q_OS_UNIX #include #endif #include #include #include #include #include "kiotesthelper.h" #include QTEST_MAIN(KDirModelTest) #ifndef USE_QTESTEVENTLOOP #define exitLoop quit #endif #ifndef Q_OS_WIN #define SPECIALCHARS " special chars%:.pdf" #else #define SPECIALCHARS " special chars%.pdf" #endif Q_DECLARE_METATYPE(KFileItemList) void KDirModelTest::initTestCase() { qputenv("LC_ALL", "en_US.UTF-8"); // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); qRegisterMetaType("KFileItemList"); m_dirModelForExpand = nullptr; m_dirModel = nullptr; s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-30); // 30 seconds ago m_tempDir = nullptr; m_topLevelFileNames << QStringLiteral("toplevelfile_1") << QStringLiteral("toplevelfile_2") << QStringLiteral("toplevelfile_3") << SPECIALCHARS ; recreateTestData(); fillModel(false); } void KDirModelTest::recreateTestData() { if (m_tempDir) { qDebug() << "Deleting old tempdir" << m_tempDir->path(); delete m_tempDir; qApp->processEvents(); // process inotify events so they don't pollute us later on } m_tempDir = new QTemporaryDir; qDebug() << "new tmp dir:" << m_tempDir->path(); // Create test data: /* * PATH/toplevelfile_1 * PATH/toplevelfile_2 * PATH/toplevelfile_3 * PATH/special chars%:.pdf * PATH/.hiddenfile * PATH/.hiddenfile2 * PATH/subdir * PATH/subdir/testfile * PATH/subdir/testsymlink * PATH/subdir/subsubdir * PATH/subdir/subsubdir/testfile */ const QString path = m_tempDir->path() + '/'; for (const QString &f : qAsConst(m_topLevelFileNames)) { createTestFile(path + f); } createTestFile(path + ".hiddenfile"); createTestFile(path + ".hiddenfile2"); createTestDirectory(path + "subdir"); createTestDirectory(path + "subdir/subsubdir", NoSymlink); m_dirIndex = QModelIndex(); m_fileIndex = QModelIndex(); m_secondFileIndex = QModelIndex(); } void KDirModelTest::cleanupTestCase() { delete m_tempDir; m_tempDir = nullptr; delete m_dirModel; m_dirModel = nullptr; delete m_dirModelForExpand; m_dirModelForExpand = nullptr; } void KDirModelTest::fillModel(bool reload, bool expectAllIndexes) { if (!m_dirModel) { m_dirModel = new KDirModel; } m_dirModel->dirLister()->setAutoErrorHandlingEnabled(false, nullptr); const QString path = m_tempDir->path() + '/'; KDirLister *dirLister = m_dirModel->dirLister(); qDebug() << "Calling openUrl"; dirLister->openUrl(QUrl::fromLocalFile(path), reload ? KDirLister::Reload : KDirLister::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); qDebug() << "enterLoop, waiting for completed()"; enterLoop(); if (expectAllIndexes) { collectKnownIndexes(); } disconnect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); } // Called after test function void KDirModelTest::cleanup() { if (m_dirModel) { disconnect(m_dirModel, nullptr, &m_eventLoop, nullptr); disconnect(m_dirModel->dirLister(), nullptr, this, nullptr); m_dirModel->dirLister()->setNameFilter(QString()); m_dirModel->dirLister()->setMimeFilter(QStringList()); m_dirModel->dirLister()->emitChanges(); } } void KDirModelTest::collectKnownIndexes() { m_dirIndex = QModelIndex(); m_fileIndex = QModelIndex(); m_secondFileIndex = QModelIndex(); // Create the indexes once and for all // The trouble is that the order of listing is undefined, one can get 1/2/3/subdir or subdir/3/2/1 for instance. for (int row = 0; row < m_topLevelFileNames.count() + 1 /*subdir*/; ++row) { QModelIndex idx = m_dirModel->index(row, 0, QModelIndex()); QVERIFY(idx.isValid()); KFileItem item = m_dirModel->itemForIndex(idx); qDebug() << item.url() << "isDir=" << item.isDir(); QString fileName = item.url().fileName(); if (item.isDir()) { m_dirIndex = idx; } else if (fileName == QLatin1String("toplevelfile_1")) { m_fileIndex = idx; } else if (fileName == QLatin1String("toplevelfile_2")) { m_secondFileIndex = idx; } else if (fileName.startsWith(QLatin1String(" special"))) { m_specialFileIndex = idx; } } QVERIFY(m_dirIndex.isValid()); QVERIFY(m_fileIndex.isValid()); QVERIFY(m_secondFileIndex.isValid()); QVERIFY(m_specialFileIndex.isValid()); // Now list subdir/ QVERIFY(m_dirModel->canFetchMore(m_dirIndex)); m_dirModel->fetchMore(m_dirIndex); qDebug() << "Listing subdir/"; enterLoop(); // Index of a file inside a directory (subdir/testfile) QModelIndex subdirIndex; m_fileInDirIndex = QModelIndex(); for (int row = 0; row < 3; ++row) { QModelIndex idx = m_dirModel->index(row, 0, m_dirIndex); if (m_dirModel->itemForIndex(idx).isDir()) { subdirIndex = idx; } else if (m_dirModel->itemForIndex(idx).name() == QLatin1String("testfile")) { m_fileInDirIndex = idx; } } // List subdir/subsubdir QVERIFY(m_dirModel->canFetchMore(subdirIndex)); qDebug() << "Listing subdir/subsubdir"; m_dirModel->fetchMore(subdirIndex); enterLoop(); // Index of ... well, subdir/subsubdir/testfile m_fileInSubdirIndex = m_dirModel->index(0, 0, subdirIndex); } void KDirModelTest::enterLoop() { #ifdef USE_QTESTEVENTLOOP m_eventLoop.enterLoop(10 /*seconds max*/); QVERIFY(!m_eventLoop.timeout()); #else m_eventLoop.exec(); #endif } void KDirModelTest::slotListingCompleted() { qDebug(); #ifdef USE_QTESTEVENTLOOP m_eventLoop.exitLoop(); #else m_eventLoop.quit(); #endif } void KDirModelTest::testRowCount() { const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, m_topLevelFileNames.count() + 1 /*subdir*/); const int subdirRowCount = m_dirModel->rowCount(m_dirIndex); QCOMPARE(subdirRowCount, 3); QVERIFY(m_fileIndex.isValid()); const int fileRowCount = m_dirModel->rowCount(m_fileIndex); // #176555 QCOMPARE(fileRowCount, 0); } void KDirModelTest::testIndex() { QVERIFY(m_dirModel->hasChildren()); // Index of the first file QVERIFY(m_fileIndex.isValid()); QCOMPARE(m_fileIndex.model(), static_cast(m_dirModel)); //QCOMPARE(m_fileIndex.row(), 0); QCOMPARE(m_fileIndex.column(), 0); QVERIFY(!m_fileIndex.parent().isValid()); QVERIFY(!m_dirModel->hasChildren(m_fileIndex)); // Index of a directory QVERIFY(m_dirIndex.isValid()); QCOMPARE(m_dirIndex.model(), static_cast(m_dirModel)); //QCOMPARE(m_dirIndex.row(), 3); // ordering isn't guaranteed QCOMPARE(m_dirIndex.column(), 0); QVERIFY(!m_dirIndex.parent().isValid()); QVERIFY(m_dirModel->hasChildren(m_dirIndex)); // Index of a file inside a directory (subdir/testfile) QVERIFY(m_fileInDirIndex.isValid()); QCOMPARE(m_fileInDirIndex.model(), static_cast(m_dirModel)); //QCOMPARE(m_fileInDirIndex.row(), 0); // ordering isn't guaranteed QCOMPARE(m_fileInDirIndex.column(), 0); QVERIFY(m_fileInDirIndex.parent() == m_dirIndex); QVERIFY(!m_dirModel->hasChildren(m_fileInDirIndex)); // Index of subdir/subsubdir/testfile QVERIFY(m_fileInSubdirIndex.isValid()); QCOMPARE(m_fileInSubdirIndex.model(), static_cast(m_dirModel)); QCOMPARE(m_fileInSubdirIndex.row(), 0); // we can check it because it's the only file there QCOMPARE(m_fileInSubdirIndex.column(), 0); QVERIFY(m_fileInSubdirIndex.parent().parent() == m_dirIndex); QVERIFY(!m_dirModel->hasChildren(m_fileInSubdirIndex)); // Test sibling() by going from subdir/testfile to subdir/subsubdir const QModelIndex subsubdirIndex = m_fileInSubdirIndex.parent(); QVERIFY(subsubdirIndex.isValid()); QModelIndex sibling1 = m_dirModel->sibling(subsubdirIndex.row(), 0, m_fileInDirIndex); QVERIFY(sibling1.isValid()); QVERIFY(sibling1 == subsubdirIndex); // Invalid sibling call QVERIFY(!m_dirModel->sibling(1, 0, m_fileInSubdirIndex).isValid()); // Test index() with a valid parent (dir). QModelIndex index2 = m_dirModel->index(m_fileInSubdirIndex.row(), m_fileInSubdirIndex.column(), subsubdirIndex); QVERIFY(index2.isValid()); QVERIFY(index2 == m_fileInSubdirIndex); // Test index() with a non-parent (file). QModelIndex index3 = m_dirModel->index(m_fileInSubdirIndex.row(), m_fileInSubdirIndex.column(), m_fileIndex); QVERIFY(!index3.isValid()); } void KDirModelTest::testNames() { QString fileName = m_dirModel->data(m_fileIndex, Qt::DisplayRole).toString(); QCOMPARE(fileName, QString("toplevelfile_1")); QString specialFileName = m_dirModel->data(m_specialFileIndex, Qt::DisplayRole).toString(); QCOMPARE(specialFileName, QString(SPECIALCHARS)); QString dirName = m_dirModel->data(m_dirIndex, Qt::DisplayRole).toString(); QCOMPARE(dirName, QString("subdir")); QString fileInDirName = m_dirModel->data(m_fileInDirIndex, Qt::DisplayRole).toString(); QCOMPARE(fileInDirName, QString("testfile")); QString fileInSubdirName = m_dirModel->data(m_fileInSubdirIndex, Qt::DisplayRole).toString(); QCOMPARE(fileInSubdirName, QString("testfile")); } void KDirModelTest::testItemForIndex() { // root item KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex()); QVERIFY(!rootItem.isNull()); QCOMPARE(rootItem.name(), QString(".")); KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex); QVERIFY(!fileItem.isNull()); QCOMPARE(fileItem.name(), QString("toplevelfile_1")); QVERIFY(!fileItem.isDir()); QCOMPARE(fileItem.url().toLocalFile(), QString(m_tempDir->path() + "/toplevelfile_1")); KFileItem dirItem = m_dirModel->itemForIndex(m_dirIndex); QVERIFY(!dirItem.isNull()); QCOMPARE(dirItem.name(), QString("subdir")); QVERIFY(dirItem.isDir()); QCOMPARE(dirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir")); KFileItem fileInDirItem = m_dirModel->itemForIndex(m_fileInDirIndex); QVERIFY(!fileInDirItem.isNull()); QCOMPARE(fileInDirItem.name(), QString("testfile")); QVERIFY(!fileInDirItem.isDir()); QCOMPARE(fileInDirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir/testfile")); KFileItem fileInSubdirItem = m_dirModel->itemForIndex(m_fileInSubdirIndex); QVERIFY(!fileInSubdirItem.isNull()); QCOMPARE(fileInSubdirItem.name(), QString("testfile")); QVERIFY(!fileInSubdirItem.isDir()); QCOMPARE(fileInSubdirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir/subsubdir/testfile")); } void KDirModelTest::testIndexForItem() { KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex()); QModelIndex rootIndex = m_dirModel->indexForItem(rootItem); QVERIFY(!rootIndex.isValid()); KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex); QModelIndex fileIndex = m_dirModel->indexForItem(fileItem); QCOMPARE(fileIndex, m_fileIndex); KFileItem dirItem = m_dirModel->itemForIndex(m_dirIndex); QModelIndex dirIndex = m_dirModel->indexForItem(dirItem); QCOMPARE(dirIndex, m_dirIndex); KFileItem fileInDirItem = m_dirModel->itemForIndex(m_fileInDirIndex); QModelIndex fileInDirIndex = m_dirModel->indexForItem(fileInDirItem); QCOMPARE(fileInDirIndex, m_fileInDirIndex); KFileItem fileInSubdirItem = m_dirModel->itemForIndex(m_fileInSubdirIndex); QModelIndex fileInSubdirIndex = m_dirModel->indexForItem(fileInSubdirItem); QCOMPARE(fileInSubdirIndex, m_fileInSubdirIndex); } void KDirModelTest::testData() { // First file QModelIndex idx1col0 = m_dirModel->index(m_fileIndex.row(), 0, QModelIndex()); QCOMPARE(idx1col0.data().toString(), QString("toplevelfile_1")); QModelIndex idx1col1 = m_dirModel->index(m_fileIndex.row(), 1, QModelIndex()); QString size1 = m_dirModel->data(idx1col1, Qt::DisplayRole).toString(); QCOMPARE(size1, QString("11 B")); KFileItem item = m_dirModel->data(m_fileIndex, KDirModel::FileItemRole).value(); KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex); QCOMPARE(item, fileItem); QCOMPARE(m_dirModel->data(m_fileIndex, KDirModel::ChildCountRole).toInt(), (int)KDirModel::ChildCountUnknown); // Second file QModelIndex idx2col0 = m_dirModel->index(m_secondFileIndex.row(), 0, QModelIndex()); QString display2 = m_dirModel->data(idx2col0, Qt::DisplayRole).toString(); QCOMPARE(display2, QString("toplevelfile_2")); // Subdir: check child count QCOMPARE(m_dirModel->data(m_dirIndex, KDirModel::ChildCountRole).toInt(), 3); // Subsubdir: check child count QCOMPARE(m_dirModel->data(m_fileInSubdirIndex.parent(), KDirModel::ChildCountRole).toInt(), 1); } void KDirModelTest::testReload() { fillModel(true); testItemForIndex(); } // We want more info than just "the values differ", if they do. #define COMPARE_INDEXES(a, b) \ QCOMPARE(a.row(), b.row()); \ QCOMPARE(a.column(), b.column()); \ QCOMPARE(a.model(), b.model()); \ QCOMPARE(a.parent().isValid(), b.parent().isValid()); \ QCOMPARE(a, b); void KDirModelTest::testModifyFile() { const QString file = m_tempDir->path() + "/toplevelfile_2"; const QUrl url = QUrl::fromLocalFile(file); #if 1 QSignalSpy spyDataChanged(m_dirModel, SIGNAL(dataChanged(QModelIndex,QModelIndex))); #else ModelSpy modelSpy(m_dirModel); #endif connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); // "Touch" the file setTimeStamp(file, s_referenceTimeStamp.addSecs(20)); // In stat mode, kdirwatch doesn't notice file changes; we need to trigger it // by creating a file. //createTestFile(m_tempDir->path() + "/toplevelfile_5"); KDirWatch::self()->setDirty(m_tempDir->path()); // Wait for KDirWatch to notify the change (especially when using Stat) enterLoop(); // If we come here, then dataChanged() was emitted - all good. #if 0 QCOMPARE(modelSpy.count(), 1); const QVariantList dataChanged = modelSpy.first(); #else const QVariantList dataChanged = spyDataChanged[0]; #endif QModelIndex receivedIndex = dataChanged[0].value(); COMPARE_INDEXES(receivedIndex, m_secondFileIndex); receivedIndex = dataChanged[1].value(); QCOMPARE(receivedIndex.row(), m_secondFileIndex.row()); // only compare row; column is count-1 disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); } void KDirModelTest::testRenameFile() { const QUrl url = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2"); const QUrl newUrl = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2_renamed"); QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged); connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged enterLoop(); // If we come here, then dataChanged() was emitted - all good. QCOMPARE(spyDataChanged.count(), 1); COMPARE_INDEXES(spyDataChanged[0][0].value(), m_secondFileIndex); QModelIndex receivedIndex = spyDataChanged[0][1].value(); QCOMPARE(receivedIndex.row(), m_secondFileIndex.row()); // only compare row; column is count-1 // check renaming happened QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), newUrl.toString()); // check that KDirLister::cachedItemForUrl won't give a bad name if copying that item (#195385) KFileItem cachedItem = KDirLister::cachedItemForUrl(newUrl); QVERIFY(!cachedItem.isNull()); QCOMPARE(cachedItem.name(), QString("toplevelfile_2_renamed")); QCOMPARE(cachedItem.entry().stringValue(KIO::UDSEntry::UDS_NAME), QString("toplevelfile_2_renamed")); // Put things back to normal job = KIO::rename(newUrl, url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged enterLoop(); QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), url.toString()); disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); } void KDirModelTest::testMoveDirectory() { testMoveDirectory(QStringLiteral("subdir")); } void KDirModelTest::testMoveDirectory(const QString &dir /*just a dir name, no slash*/) { const QString path = m_tempDir->path() + '/'; const QString srcdir = path + dir; QVERIFY(QDir(srcdir).exists()); QTemporaryDir destDir; const QString dest = destDir.path() + '/'; QVERIFY(QDir(dest).exists()); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); // Move qDebug() << "Moving" << srcdir << "to" << dest; KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(srcdir), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(job->exec()); // wait for kdirnotify enterLoop(); disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid()); connect(m_dirModel, &QAbstractItemModel::rowsInserted, &m_eventLoop, &QTestEventLoop::exitLoop); // Move back qDebug() << "Moving" << dest + dir << "back to" << srcdir; job = KIO::move(QUrl::fromLocalFile(dest + dir), QUrl::fromLocalFile(srcdir), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(job->exec()); enterLoop(); QVERIFY(QDir(srcdir).exists()); disconnect(m_dirModel, &QAbstractItemModel::rowsInserted, &m_eventLoop, &QTestEventLoop::exitLoop); // m_dirIndex is invalid after the above... fillModel(true); } void KDirModelTest::testRenameDirectory() // #172945, #174703, (and #180156) { const QString path = m_tempDir->path() + '/'; const QUrl url = QUrl::fromLocalFile(path + "subdir"); const QUrl newUrl = QUrl::fromLocalFile(path + "subdir_renamed"); // For #180156 we need a second kdirmodel, viewing the subdir being renamed. // I'm abusing m_dirModelForExpand for that purpose. delete m_dirModelForExpand; m_dirModelForExpand = new KDirModel; KDirLister *dirListerForExpand = m_dirModelForExpand->dirLister(); connect(dirListerForExpand, QOverload<>::of(&KDirLister::completed), this, &KDirModelTest::slotListingCompleted); dirListerForExpand->openUrl(url); // async enterLoop(); // Now do the renaming QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged); connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged enterLoop(); // If we come here, then dataChanged() was emitted - all good. //QCOMPARE(spyDataChanged.count(), 1); // it was in fact emitted 5 times... //COMPARE_INDEXES(spyDataChanged[0][0].value(), m_dirIndex); //QModelIndex receivedIndex = spyDataChanged[0][1].value(); //QCOMPARE(receivedIndex.row(), m_dirIndex.row()); // only compare row; column is count-1 // check renaming happened QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), newUrl.toString()); qDebug() << newUrl << "indexForUrl=" << m_dirModel->indexForUrl(newUrl) << "m_dirIndex=" << m_dirIndex; QCOMPARE(m_dirModel->indexForUrl(newUrl), m_dirIndex); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/testfile")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir/testfile")).isValid()); // Check the other kdirmodel got redirected QCOMPARE(dirListerForExpand->url().toLocalFile(), QString(path + "subdir_renamed")); qDebug() << "calling testMoveDirectory(subdir_renamed)"; // Test moving the renamed directory; if something inside KDirModel // wasn't properly updated by the renaming, this would detect it and crash (#180673) testMoveDirectory(QStringLiteral("subdir_renamed")); // Put things back to normal job = KIO::rename(newUrl, url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged enterLoop(); QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), url.toString()); disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), url.toString()); QCOMPARE(m_dirModel->indexForUrl(url), m_dirIndex); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/testfile")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir/testfile")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/testfile")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir/testfile")).isValid()); // TODO INVESTIGATE // QCOMPARE(dirListerForExpand->url().toLocalFile(), path+"subdir"); delete m_dirModelForExpand; m_dirModelForExpand = nullptr; } void KDirModelTest::testRenameDirectoryInCache() // #188807 { // Ensure the stuff is in cache. fillModel(true); const QString path = m_tempDir->path() + '/'; QVERIFY(!m_dirModel->dirLister()->findByUrl(QUrl::fromLocalFile(path)).isNull()); // No more dirmodel nor dirlister. delete m_dirModel; m_dirModel = nullptr; // Now let's rename a directory that is in KCoreDirListerCache const QUrl url = QUrl::fromLocalFile(path); QUrl newUrl = url.adjusted(QUrl::StripTrailingSlash); newUrl.setPath(newUrl.path() + "_renamed"); qDebug() << newUrl; KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo); QVERIFY(job->exec()); // Put things back to normal job = KIO::rename(newUrl, url, KIO::HideProgressInfo); QVERIFY(job->exec()); // KDirNotify emits FileRenamed for each rename() above, which in turn // re-lists the directory. We need to wait for both signals to be emitted // otherwise the dirlister will not be in the state we expect. QTest::qWait(200); fillModel(true); QVERIFY(m_dirIndex.isValid()); KFileItem rootItem = m_dirModel->dirLister()->findByUrl(QUrl::fromLocalFile(path)); QVERIFY(!rootItem.isNull()); } void KDirModelTest::testChmodDirectory() // #53397 { QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged); connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); const QString path = m_tempDir->path() + '/'; KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex()); const mode_t origPerm = rootItem.permissions(); mode_t newPerm = origPerm ^ S_IWGRP; //const QFile::Permissions origPerm = rootItem.filePermissions(); //QVERIFY(origPerm & QFile::ReadOwner); //const QFile::Permissions newPerm = origPerm ^ QFile::WriteGroup; QVERIFY(newPerm != origPerm); KFileItemList items; items << rootItem; KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QString(), QString(), false, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY(job->exec()); // ChmodJob doesn't talk to KDirNotify, kpropertiesdialog does. // [this allows to group notifications after all the changes one can make in the dialog] org::kde::KDirNotify::emitFilesChanged(QList() << QUrl::fromLocalFile(path)); // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved enterLoop(); // If we come here, then dataChanged() was emitted - all good. QCOMPARE(spyDataChanged.count(), 1); QModelIndex receivedIndex = spyDataChanged[0][0].value(); qDebug() << receivedIndex; QVERIFY(!receivedIndex.isValid()); const KFileItem newRootItem = m_dirModel->itemForIndex(QModelIndex()); QVERIFY(!newRootItem.isNull()); QCOMPARE(QString::number(newRootItem.permissions(), 16), QString::number(newPerm, 16)); disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); } enum { NoFlag = 0, NewDir = 1, // whether to re-create a new QTemporaryDir completely, to avoid cached fileitems ListFinalDir = 2, // whether to list the target dir at the same time, like k3b, for #193364 Recreate = 4, CacheSubdir = 8, // put subdir in the cache before expandToUrl // flags, next item is 16! }; void KDirModelTest::testExpandToUrl_data() { QTest::addColumn("flags"); // see enum above QTest::addColumn("expandToPath"); // relative path QTest::addColumn("expectedExpandSignals"); QTest::newRow("the root, nothing to do") << int(NoFlag) << QString() << QStringList(); QTest::newRow(".") << int(NoFlag) << "." << (QStringList()); QTest::newRow("subdir") << int(NoFlag) << "subdir" << (QStringList() << QStringLiteral("subdir")); QTest::newRow("subdir/.") << int(NoFlag) << "subdir/." << (QStringList() << QStringLiteral("subdir")); const QString subsubdir = QStringLiteral("subdir/subsubdir"); // Must list root, emit expand for subdir, list subdir, emit expand for subsubdir. QTest::newRow("subdir/subsubdir") << int(NoFlag) << subsubdir << (QStringList() << QStringLiteral("subdir") << subsubdir); // Must list root, emit expand for subdir, list subdir, emit expand for subsubdir, list subsubdir. const QString subsubdirfile = subsubdir + "/testfile"; QTest::newRow("subdir/subsubdir/testfile sync") << int(NoFlag) << subsubdirfile << (QStringList() << QStringLiteral("subdir") << subsubdir << subsubdirfile); #ifndef Q_OS_WIN // Expand a symlink to a directory (#219547) const QString dirlink = m_tempDir->path() + "/dirlink"; createTestSymlink(dirlink, "subdir"); // dirlink -> subdir QVERIFY(QFileInfo(dirlink).isSymLink()); // If this test fails, your first move should be to enable all debug output and see if KDirWatch says inotify failed QTest::newRow("dirlink") << int(NoFlag) << "dirlink/subsubdir" << (QStringList() << QStringLiteral("dirlink") << QStringLiteral("dirlink/subsubdir")); #endif // Do a cold-cache test too, but nowadays it doesn't change anything anymore, // apart from testing different code paths inside KDirLister. QTest::newRow("subdir/subsubdir/testfile with reload") << int(NewDir) << subsubdirfile << (QStringList() << QStringLiteral("subdir") << subsubdir << subsubdirfile); QTest::newRow("hold dest dir") // #193364 << int(NewDir | ListFinalDir) << subsubdirfile << (QStringList() << QStringLiteral("subdir") << subsubdir << subsubdirfile); // Put subdir in cache too (#175035) QTest::newRow("hold subdir and dest dir") << int(NewDir | CacheSubdir | ListFinalDir | Recreate) << subsubdirfile << (QStringList() << QStringLiteral("subdir") << subsubdir << subsubdirfile); // Make sure the last test has the Recreate option set, for the subsequent test methods. } void KDirModelTest::testExpandToUrl() { QFETCH(int, flags); QFETCH(QString, expandToPath); // relative QFETCH(QStringList, expectedExpandSignals); if (flags & NewDir) { recreateTestData(); // WARNING! m_dirIndex, m_fileIndex, m_secondFileIndex etc. are not valid anymore after this point! } const QString path = m_tempDir->path() + '/'; if (flags & CacheSubdir) { // This way, the listDir for subdir will find items in cache, and will schedule a CachedItemsJob m_dirModel->dirLister()->openUrl(QUrl::fromLocalFile(path + "subdir")); QSignalSpy completedSpy(m_dirModel->dirLister(), SIGNAL(completed())); QVERIFY(completedSpy.wait(2000)); } if (flags & ListFinalDir) { // This way, the last listDir will find items in cache, and will schedule a CachedItemsJob m_dirModel->dirLister()->openUrl(QUrl::fromLocalFile(path + "subdir/subsubdir")); QSignalSpy completedSpy(m_dirModel->dirLister(), SIGNAL(completed())); QVERIFY(completedSpy.wait(2000)); } if (!m_dirModelForExpand || (flags & NewDir)) { delete m_dirModelForExpand; m_dirModelForExpand = new KDirModel; connect(m_dirModelForExpand, &KDirModel::expand, this, &KDirModelTest::slotExpand); connect(m_dirModelForExpand, &QAbstractItemModel::rowsInserted, this, &KDirModelTest::slotRowsInserted); KDirLister *dirListerForExpand = m_dirModelForExpand->dirLister(); dirListerForExpand->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); // async } m_rowsInsertedEmitted = false; m_expectedExpandSignals = expectedExpandSignals; m_nextExpectedExpandSignals = 0; QSignalSpy spyExpand(m_dirModelForExpand, SIGNAL(expand(QModelIndex))); m_urlToExpandTo = QUrl::fromLocalFile(path + expandToPath); // If KDirModel doesn't know this URL yet, then we want to see rowsInserted signals // being emitted, so that the slots can get the index to that url then. m_expectRowsInserted = !expandToPath.isEmpty() && !m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid(); QVERIFY(QFileInfo::exists(m_urlToExpandTo.toLocalFile())); m_dirModelForExpand->expandToUrl(m_urlToExpandTo); if (expectedExpandSignals.isEmpty()) { QTest::qWait(20); // to make sure we process queued connection calls, otherwise spyExpand.count() is always 0 even if there's a bug... QCOMPARE(spyExpand.count(), 0); } else { if (spyExpand.count() < expectedExpandSignals.count()) { enterLoop(); QCOMPARE(spyExpand.count(), expectedExpandSignals.count()); } if (m_expectRowsInserted) { QVERIFY(m_rowsInsertedEmitted); } } // Now it should exist if (!expandToPath.isEmpty() && expandToPath != QLatin1String(".")) { qDebug() << "Do I know" << m_urlToExpandTo << "?"; QVERIFY(m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid()); } if (flags & ListFinalDir) { testUpdateParentAfterExpand(); } if (flags & Recreate) { // Clean up, for the next tests recreateTestData(); fillModel(false); } } void KDirModelTest::slotExpand(const QModelIndex &index) { QVERIFY(index.isValid()); const QString path = m_tempDir->path() + '/'; KFileItem item = m_dirModelForExpand->itemForIndex(index); QVERIFY(!item.isNull()); qDebug() << item.url().toLocalFile(); QCOMPARE(item.url().toLocalFile(), QString(path + m_expectedExpandSignals[m_nextExpectedExpandSignals++])); // if rowsInserted wasn't emitted yet, then any proxy model would be unable to do anything with index at this point if (item.url() == m_urlToExpandTo) { QVERIFY(m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid()); if (m_expectRowsInserted) { QVERIFY(m_rowsInsertedEmitted); } } if (m_nextExpectedExpandSignals == m_expectedExpandSignals.count()) { m_eventLoop.exitLoop(); // done } } void KDirModelTest::slotRowsInserted(const QModelIndex &, int, int) { m_rowsInsertedEmitted = true; } // This code is called by testExpandToUrl void KDirModelTest::testUpdateParentAfterExpand() // #193364 { const QString path = m_tempDir->path() + '/'; const QString file = path + "subdir/aNewFile"; qDebug() << "Creating" << file; QVERIFY(!QFile::exists(file)); createTestFile(file); QSignalSpy spyRowsInserted(m_dirModelForExpand, SIGNAL(rowsInserted(QModelIndex,int,int))); QVERIFY(spyRowsInserted.wait(1000)); } void KDirModelTest::testFilter() { QVERIFY(m_dirIndex.isValid()); const int oldTopLevelRowCount = m_dirModel->rowCount(); const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex); QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), SIGNAL(itemsFilteredByMime(KFileItemList))); QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), SIGNAL(itemsDeleted(KFileItemList))); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel*")); QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet m_dirModel->dirLister()->emitChanges(); QCOMPARE(m_dirModel->rowCount(), 4); // 3 toplevel* files, one subdir QCOMPARE(m_dirModel->rowCount(m_dirIndex), 1); // the files get filtered out, the subdir remains // In the subdir, we can get rowsRemoved signals like (1,2) or (0,0)+(2,2), // depending on the order of the files in the model. // So QCOMPARE(spyRowsRemoved.count(), 3) is fragile, we rather need // to sum up the removed rows per parent directory. QMap rowsRemovedPerDir; for (int i = 0; i < spyRowsRemoved.count(); ++i) { const QVariantList args = spyRowsRemoved[i]; const QModelIndex parentIdx = args[0].value(); QString dirName; if (parentIdx.isValid()) { const KFileItem item = m_dirModel->itemForIndex(parentIdx); dirName = item.name(); } else { dirName = QStringLiteral("root"); } rowsRemovedPerDir[dirName] += args[2].toInt() - args[1].toInt() + 1; //qDebug() << parentIdx << args[1].toInt() << args[2].toInt(); } QCOMPARE(rowsRemovedPerDir.count(), 3); // once for every dir QCOMPARE(rowsRemovedPerDir.value("root"), 1); // one from toplevel ('special chars') QCOMPARE(rowsRemovedPerDir.value("subdir"), 2); // two from subdir QCOMPARE(rowsRemovedPerDir.value("subsubdir"), 1); // one from subsubdir QCOMPARE(spyItemsDeleted.count(), 3); // once for every dir QCOMPARE(spyItemsDeleted[0][0].value().count(), 1); // one from toplevel ('special chars') QCOMPARE(spyItemsDeleted[1][0].value().count(), 2); // two from subdir QCOMPARE(spyItemsDeleted[2][0].value().count(), 1); // one from subsubdir QCOMPARE(spyItemsFilteredByMime.count(), 0); spyItemsDeleted.clear(); spyItemsFilteredByMime.clear(); // Reset the filter qDebug() << "reset to no filter"; m_dirModel->dirLister()->setNameFilter(QString()); m_dirModel->dirLister()->emitChanges(); QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); QCOMPARE(spyItemsDeleted.count(), 0); QCOMPARE(spyItemsFilteredByMime.count(), 0); // The order of things changed because of filtering. // Fill again, so that m_fileIndex etc. are correct again. fillModel(true); } void KDirModelTest::testMimeFilter() { QVERIFY(m_dirIndex.isValid()); const int oldTopLevelRowCount = m_dirModel->rowCount(); const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex); QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), SIGNAL(itemsFilteredByMime(KFileItemList))); QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), SIGNAL(itemsDeleted(KFileItemList))); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); m_dirModel->dirLister()->setMimeFilter(QStringList() << QStringLiteral("application/pdf")); QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet m_dirModel->dirLister()->emitChanges(); QCOMPARE(m_dirModel->rowCount(), 1); // 1 pdf files, no subdir anymore QVERIFY(spyRowsRemoved.count() >= 1); // depends on contiguity... QVERIFY(spyItemsDeleted.count() >= 1); // once for every dir // Maybe it would make sense to have those items in itemsFilteredByMime, // but well, for the only existing use of that signal (mime filter plugin), // it's not really necessary, the plugin has seen those files before anyway. // The signal is mostly useful for the case of listing a dir with a mime filter set. //QCOMPARE(spyItemsFilteredByMime.count(), 1); //QCOMPARE(spyItemsFilteredByMime[0][0].value().count(), 4); spyItemsDeleted.clear(); spyItemsFilteredByMime.clear(); // Reset the filter qDebug() << "reset to no filter"; m_dirModel->dirLister()->setMimeFilter(QStringList()); m_dirModel->dirLister()->emitChanges(); QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); QCOMPARE(spyItemsDeleted.count(), 0); QCOMPARE(spyItemsFilteredByMime.count(), 0); // The order of things changed because of filtering. // Fill again, so that m_fileIndex etc. are correct again. fillModel(true); } void KDirModelTest::testShowHiddenFiles() // #174788 { KDirLister *dirLister = m_dirModel->dirLister(); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); QSignalSpy spyNewItems(dirLister, SIGNAL(newItems(KFileItemList))); QSignalSpy spyRowsInserted(m_dirModel, SIGNAL(rowsInserted(QModelIndex,int,int))); dirLister->setShowingDotFiles(true); dirLister->emitChanges(); const int numberOfDotFiles = 2; QCOMPARE(spyNewItems.count(), 1); QCOMPARE(spyNewItems[0][0].value().count(), numberOfDotFiles); QCOMPARE(spyRowsInserted.count(), 1); QCOMPARE(spyRowsRemoved.count(), 0); spyNewItems.clear(); spyRowsInserted.clear(); dirLister->setShowingDotFiles(false); dirLister->emitChanges(); QCOMPARE(spyNewItems.count(), 0); QCOMPARE(spyRowsInserted.count(), 0); QCOMPARE(spyRowsRemoved.count(), 1); } void KDirModelTest::testMultipleSlashes() { const QString path = m_tempDir->path() + '/'; QModelIndex index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//testfile")); QVERIFY(index.isValid()); index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//subsubdir//")); QVERIFY(index.isValid()); index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir///subsubdir////testfile")); QVERIFY(index.isValid()); } void KDirModelTest::testUrlWithRef() // #171117 { const QString path = m_tempDir->path() + '/'; KDirLister *dirLister = m_dirModel->dirLister(); QUrl url = QUrl::fromLocalFile(path); url.setFragment(QStringLiteral("ref")); QVERIFY(url.url().endsWith(QLatin1String("#ref"))); dirLister->openUrl(url, KDirLister::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); enterLoop(); QCOMPARE(dirLister->url().toString(), url.toString(QUrl::StripTrailingSlash)); collectKnownIndexes(); disconnect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); } //void KDirModelTest::testFontUrlWithHost() // #160057 --> moved to kio_fonts (kfontinst/kio/autotests) void KDirModelTest::testRemoteUrlWithHost() // #178416 { if (!KProtocolInfo::isKnownProtocol(QStringLiteral("remote"))) { QSKIP("kio_remote not installed"); } QUrl url(QStringLiteral("remote://foo")); KDirLister *dirLister = m_dirModel->dirLister(); dirLister->openUrl(url, KDirLister::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); enterLoop(); QCOMPARE(dirLister->url().toString(), QString("remote:")); } void KDirModelTest::testZipFile() // # 171721 { const QString path = QFileInfo(QFINDTESTDATA("wronglocalsizes.zip")).absolutePath(); KDirLister *dirLister = m_dirModel->dirLister(); dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); enterLoop(); disconnect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); QUrl zipUrl(QUrl::fromLocalFile(path)); zipUrl.setPath(zipUrl.path() + "/wronglocalsizes.zip"); // just a zip file lying here for other reasons QVERIFY(QFile::exists(zipUrl.toLocalFile())); zipUrl.setScheme(QStringLiteral("zip")); QModelIndex index = m_dirModel->indexForUrl(zipUrl); QVERIFY(!index.isValid()); // protocol mismatch, can't find it! zipUrl.setScheme(QStringLiteral("file")); index = m_dirModel->indexForUrl(zipUrl); QVERIFY(index.isValid()); } void KDirModelTest::testSmb() { const QUrl smbUrl(QStringLiteral("smb:/")); // TODO: feed a KDirModel without using a KDirLister. // Calling the slots directly. // This requires that KDirModel does not ask the KDirLister for its rootItem anymore, // but that KDirLister emits the root item whenever it changes. if (!KProtocolInfo::isKnownProtocol(QStringLiteral("smb"))) { QSKIP("kio_smb not installed"); } KDirLister *dirLister = m_dirModel->dirLister(); dirLister->openUrl(smbUrl, KDirLister::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); connect(dirLister, SIGNAL(canceled()), this, SLOT(slotListingCompleted())); QSignalSpy spyCanceled(dirLister, SIGNAL(canceled())); enterLoop(); // wait for completed signal if (!spyCanceled.isEmpty()) { QSKIP("smb:/ returns an error, probably no network available"); } QModelIndex index = m_dirModel->index(0, 0); if (index.isValid()) { QVERIFY(m_dirModel->canFetchMore(index)); m_dirModel->fetchMore(index); enterLoop(); // wait for completed signal disconnect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); } } class MyDirLister : public KDirLister { public: void emitItemsDeleted(const KFileItemList &items) { emit itemsDeleted(items); } }; void KDirModelTest::testBug196695() { KFileItem rootItem(QUrl::fromLocalFile(m_tempDir->path()), QString(), KFileItem::Unknown); KFileItem childItem(QUrl::fromLocalFile(QString(m_tempDir->path() + "/toplevelfile_1")), QString(), KFileItem::Unknown); KFileItemList list; // Important: the root item must not be first in the list to trigger bug 196695 list << childItem << rootItem; MyDirLister *dirLister = static_cast(m_dirModel->dirLister()); dirLister->emitItemsDeleted(list); fillModel(true); } void KDirModelTest::testMimeData() { QModelIndex index0 = m_dirModel->index(0, 0); QVERIFY(index0.isValid()); QModelIndex index1 = m_dirModel->index(1, 0); QVERIFY(index1.isValid()); QList indexes; indexes << index0 << index1; QMimeData *mimeData = m_dirModel->mimeData(indexes); QVERIFY(mimeData); QVERIFY(mimeData->hasUrls()); const QList urls = mimeData->urls(); QCOMPARE(urls.count(), indexes.count()); delete mimeData; } void KDirModelTest::testDotHiddenFile_data() { QTest::addColumn("fileContents"); QTest::addColumn("expectedListing"); QStringList allItems; allItems << QStringLiteral("toplevelfile_1") << QStringLiteral("toplevelfile_2") << QStringLiteral("toplevelfile_3") << SPECIALCHARS << QStringLiteral("subdir"); QTest::newRow("empty_file") << QStringList() << allItems; QTest::newRow("simple_name") << (QStringList() << QStringLiteral("toplevelfile_1")) << QStringList(allItems.mid(1)); QStringList allButSpecialChars = allItems; allButSpecialChars.removeAt(3); QTest::newRow("special_chars") << (QStringList() << SPECIALCHARS) << allButSpecialChars; QStringList allButSubdir = allItems; allButSubdir.removeAt(4); QTest::newRow("subdir") << (QStringList() << QStringLiteral("subdir")) << allButSubdir; QTest::newRow("many_lines") << (QStringList() << QStringLiteral("subdir") << QStringLiteral("toplevelfile_1") << QStringLiteral("toplevelfile_3") << QStringLiteral("toplevelfile_2")) << (QStringList() << SPECIALCHARS); } void KDirModelTest::testDotHiddenFile() { QFETCH(QStringList, fileContents); QFETCH(QStringList, expectedListing); const QString path = m_tempDir->path() + '/'; const QString dotHiddenFile = path + ".hidden"; QTest::qWait(1000); // mtime-based cache, so we need to wait for 1 second QFile dh(dotHiddenFile); QVERIFY(dh.open(QIODevice::WriteOnly)); dh.write(fileContents.join('\n').toUtf8()); dh.close(); // Do it twice: once to read from the file and once to use the cache for (int i = 0; i < 2; ++i) { fillModel(true, false); QStringList files; for (int row = 0; row < m_dirModel->rowCount(); ++row) { files.append(m_dirModel->index(row, KDirModel::Name).data().toString()); } files.sort(); expectedListing.sort(); QCOMPARE(files, expectedListing); } dh.remove(); } void KDirModelTest::testDeleteFile() { fillModel(true); QVERIFY(m_fileIndex.isValid()); const int oldTopLevelRowCount = m_dirModel->rowCount(); const QString path = m_tempDir->path() + '/'; const QString file = path + "toplevelfile_1"; const QUrl url = QUrl::fromLocalFile(file); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved enterLoop(); // If we come here, then rowsRemoved() was emitted - all good. const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before QCOMPARE(spyRowsRemoved.count(), 1); QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row()); QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row()); disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1")); QVERIFY(!fileIndex.isValid()); // Recreate the file, for consistency in the next tests // So the second part of this test is a "testCreateFile" createTestFile(file); // Tricky problem - KDirLister::openUrl will emit items from cache // and then schedule an update; so just calling fillModel would // not wait enough, it would abort due to not finding toplevelfile_1 // in the items from cache. This progressive-emitting behavior is fine // for GUIs but not for unit tests ;-) fillModel(true, false); fillModel(false); } void KDirModelTest::testDeleteFileWhileListing() // doesn't really test that yet, the kdirwatch deleted signal comes too late { const int oldTopLevelRowCount = m_dirModel->rowCount(); const QString path = m_tempDir->path() + '/'; const QString file = path + "toplevelfile_1"; const QUrl url = QUrl::fromLocalFile(file); KDirLister *dirLister = m_dirModel->dirLister(); QSignalSpy spyCompleted(dirLister, SIGNAL(completed())); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); if (!spyCompleted.isEmpty()) { QSKIP("listing completed too early"); } QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); QVERIFY(job->exec()); if (spyCompleted.isEmpty()) { enterLoop(); } QVERIFY(spyRowsRemoved.wait(1000)); const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before QCOMPARE(spyRowsRemoved.count(), 1); QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row()); QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row()); QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1")); QVERIFY(!fileIndex.isValid()); qDebug() << "Test done, recreating file"; // Recreate the file, for consistency in the next tests // So the second part of this test is a "testCreateFile" createTestFile(file); fillModel(true, false); // see testDeleteFile fillModel(false); } void KDirModelTest::testOverwriteFileWithDir() // #151851 c4 { fillModel(false); const QString path = m_tempDir->path() + '/'; const QString dir = path + "subdir"; const QString file = path + "toplevelfile_1"; const int oldTopLevelRowCount = m_dirModel->rowCount(); bool removalWithinTopLevel = false; bool dataChangedAtFirstLevel = false; auto rrc = connect(m_dirModel, &KDirModel::rowsRemoved, this, [&removalWithinTopLevel](const QModelIndex &index) { if (!index.isValid()) { // yes, that's what we have been waiting for removalWithinTopLevel = true; } }); auto dcc = connect(m_dirModel, &KDirModel::dataChanged, this, [&dataChangedAtFirstLevel](const QModelIndex &index) { if (index.isValid() && !index.parent().isValid()) { // a change of a node whose parent is root, yay, that's it dataChangedAtFirstLevel = true; } }); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::Job *job = KIO::move(QUrl::fromLocalFile(dir), QUrl::fromLocalFile(file), KIO::HideProgressInfo); job->setUiDelegate(nullptr); PredefinedAnswerJobUiDelegate extension; extension.m_renameResult = KIO::R_OVERWRITE; job->setUiDelegateExtension(&extension); QVERIFY(job->exec()); QCOMPARE(extension.m_askFileRenameCalled, 1); // Wait for a removal within the top level (that's for the old file going away), and also // for a dataChanged which notifies us that a file has become a directory int retries = 0; while ((!removalWithinTopLevel || !dataChangedAtFirstLevel) && retries < 100) { QTest::qWait(10); ++retries; } QVERIFY(removalWithinTopLevel); QVERIFY(dataChangedAtFirstLevel); m_dirModel->disconnect(rrc); m_dirModel->disconnect(dcc); // If we come here, then rowsRemoved() was emitted - all good. const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(dir)).isValid()); QModelIndex newIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1")); QVERIFY(newIndex.isValid()); KFileItem newItem = m_dirModel->itemForIndex(newIndex); QVERIFY(newItem.isDir()); // yes, the file is a dir now ;-) qDebug() << "========= Test done, recreating test data ========="; recreateTestData(); fillModel(false); } void KDirModelTest::testDeleteFiles() { const int oldTopLevelRowCount = m_dirModel->rowCount(); const QString file = m_tempDir->path() + "/toplevelfile_"; QList urls; urls << QUrl::fromLocalFile(file + '1') << QUrl::fromLocalFile(file + '2') << QUrl::fromLocalFile(file + '3'); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); KIO::DeleteJob *job = KIO::del(urls, KIO::HideProgressInfo); QVERIFY(job->exec()); int numRowsRemoved = 0; while (numRowsRemoved < 3) { QTest::qWait(20); numRowsRemoved = 0; for (int sigNum = 0; sigNum < spyRowsRemoved.count(); ++sigNum) { numRowsRemoved += spyRowsRemoved[sigNum][2].toInt() - spyRowsRemoved[sigNum][1].toInt() + 1; } qDebug() << "numRowsRemoved=" << numRowsRemoved; } const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 3); // three less than before qDebug() << "Recreating test data"; recreateTestData(); qDebug() << "Re-filling model"; fillModel(false); } // A renaming that looks more like a deletion to the model void KDirModelTest::testRenameFileToHidden() // #174721 { const QUrl url = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2"); const QUrl newUrl = QUrl::fromLocalFile(m_tempDir->path() + "/.toplevelfile_2"); QSignalSpy spyDataChanged(m_dirModel, SIGNAL(dataChanged(QModelIndex,QModelIndex))); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); QSignalSpy spyRowsInserted(m_dirModel, SIGNAL(rowsInserted(QModelIndex,int,int))); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister enterLoop(); // If we come here, then rowsRemoved() was emitted - all good. QCOMPARE(spyDataChanged.count(), 0); QCOMPARE(spyRowsRemoved.count(), 1); QCOMPARE(spyRowsInserted.count(), 0); COMPARE_INDEXES(spyRowsRemoved[0][0].value(), QModelIndex()); // parent is invalid const int row = spyRowsRemoved[0][1].toInt(); QCOMPARE(row, m_secondFileIndex.row()); // only compare row disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); spyRowsRemoved.clear(); // Put things back to normal, should make the file reappear connect(m_dirModel, &QAbstractItemModel::rowsInserted, &m_eventLoop, &QTestEventLoop::exitLoop); job = KIO::rename(newUrl, url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister enterLoop(); QCOMPARE(spyDataChanged.count(), 0); QCOMPARE(spyRowsRemoved.count(), 0); QCOMPARE(spyRowsInserted.count(), 1); int newRow = spyRowsInserted[0][1].toInt(); m_secondFileIndex = m_dirModel->index(newRow, 0); QVERIFY(m_secondFileIndex.isValid()); QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), url.toString()); } void KDirModelTest::testDeleteDirectory() { const QString path = m_tempDir->path() + '/'; const QUrl url = QUrl::fromLocalFile(path + "subdir/subsubdir"); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QSignalSpy spyDirWatchDeleted(KDirWatch::self(), SIGNAL(deleted(QString))); KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved enterLoop(); // If we come here, then rowsRemoved() was emitted - all good. QCOMPARE(spyRowsRemoved.count(), 1); disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QModelIndex deletedDirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir")); QVERIFY(!deletedDirIndex.isValid()); QModelIndex dirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")); QVERIFY(dirIndex.isValid()); // TODO!!! Bug in KDirWatch? ### // QCOMPARE(spyDirWatchDeleted.count(), 1); } void KDirModelTest::testDeleteCurrentDirectory() { const int oldTopLevelRowCount = m_dirModel->rowCount(); const QString path = m_tempDir->path() + '/'; const QUrl url = QUrl::fromLocalFile(path); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); KDirWatch::self()->statistics(); KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved enterLoop(); // If we come here, then rowsRemoved() was emitted - all good. const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, 0); // empty // We can get rowsRemoved for subdirs first, since kdirwatch notices that. QVERIFY(spyRowsRemoved.count() >= 1); // Look for the signal(s) that had QModelIndex() as parent. int i; int numDeleted = 0; for (i = 0; i < spyRowsRemoved.count(); ++i) { const int from = spyRowsRemoved[i][1].toInt(); const int to = spyRowsRemoved[i][2].toInt(); qDebug() << spyRowsRemoved[i][0].value() << from << to; if (!spyRowsRemoved[i][0].value().isValid()) { numDeleted += (to - from) + 1; } } QCOMPARE(numDeleted, oldTopLevelRowCount); disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1")); QVERIFY(!fileIndex.isValid()); } void KDirModelTest::testQUrlHash() { const int count = 3000; // Prepare an array of QUrls so that url constructing isn't part of the timing QVector urls; urls.resize(count); for (int i = 0; i < count; ++i) { urls[i] = QUrl("http://www.kde.org/path/" + QString::number(i)); } QHash qurlHash; QHash kurlHash; - QTime dt; dt.start(); + QElapsedTimer dt; dt.start(); for (int i = 0; i < count; ++i) { qurlHash.insert(urls[i], i); } //qDebug() << "inserting" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs"; dt.start(); for (int i = 0; i < count; ++i) { kurlHash.insert(urls[i], i); } //qDebug() << "inserting" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs"; // Nice results: for count=30000 I got 4515 (before) and 103 (after) dt.start(); for (int i = 0; i < count; ++i) { QCOMPARE(qurlHash.value(urls[i]), i); } //qDebug() << "looking up" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs"; dt.start(); for (int i = 0; i < count; ++i) { QCOMPARE(kurlHash.value(urls[i]), i); } //qDebug() << "looking up" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs"; // Nice results: for count=30000 I got 4296 (before) and 63 (after) } diff --git a/autotests/kurifiltertest.cpp b/autotests/kurifiltertest.cpp index 4de1ef4a..8659a825 100644 --- a/autotests/kurifiltertest.cpp +++ b/autotests/kurifiltertest.cpp @@ -1,495 +1,494 @@ /* * Copyright (C) 2002, 2003 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 "kurifiltertest.h" #include #include #include #include #include #include #include #include #include QTEST_MAIN(KUriFilterTest) static const char *const s_uritypes[] = { "NetProtocol", "LOCAL_FILE", "LOCAL_DIR", "EXECUTABLE", "HELP", "SHELL", "BLOCKED", "ERROR", "UNKNOWN" }; #define NO_FILTERING -2 static void setupColumns() { QTest::addColumn("input"); QTest::addColumn("expectedResult"); QTest::addColumn("expectedUriType"); QTest::addColumn("list"); QTest::addColumn("absPath"); QTest::addColumn("checkForExecutables"); } static void addRow(const char *input, const QString &expectedResult = QString(), int expectedUriType = -1, const QStringList &list = QStringList(), const QString &absPath = QString(), bool checkForExecutables = true) { QTest::newRow(input) << input << expectedResult << expectedUriType << list << absPath << checkForExecutables; } static void runFilterTest(const QString &a, const QString &expectedResult = nullptr, int expectedUriType = -1, const QStringList &list = QStringList(), const QString &absPath = nullptr, bool checkForExecutables = true) { KUriFilterData *filterData = new KUriFilterData; filterData->setData(a); filterData->setCheckForExecutables(checkForExecutables); if (!absPath.isEmpty()) { filterData->setAbsolutePath(absPath); qDebug() << "Filtering: " << a << " with absPath=" << absPath; } else { qDebug() << "Filtering: " << a; } if (KUriFilter::self()->filterUri(*filterData, list)) { if (expectedUriType == NO_FILTERING) { qCritical() << a << "Did not expect filtering. Got" << filterData->uri(); QVERIFY(expectedUriType != NO_FILTERING); // fail the test } // Copied from minicli... QString cmd; QUrl uri = filterData->uri(); if (uri.isLocalFile() && !uri.hasFragment() && !uri.hasQuery() && (filterData->uriType() != KUriFilterData::NetProtocol)) { cmd = uri.toLocalFile(); } else { cmd = uri.url(QUrl::FullyEncoded); } switch (filterData->uriType()) { case KUriFilterData::LocalFile: case KUriFilterData::LocalDir: qDebug() << "*** Result: Local Resource => '" << filterData->uri().toLocalFile() << "'" << endl; break; case KUriFilterData::Help: qDebug() << "*** Result: Local Resource => '" << filterData->uri().url() << "'" << endl; break; case KUriFilterData::NetProtocol: qDebug() << "*** Result: Network Resource => '" << filterData->uri().url() << "'" << endl; break; case KUriFilterData::Shell: case KUriFilterData::Executable: if (filterData->hasArgsAndOptions()) { cmd += filterData->argsAndOptions(); } qDebug() << "*** Result: Executable/Shell => '" << cmd << "'"; break; case KUriFilterData::Error: qDebug() << "*** Result: Encountered error => '" << cmd << "'"; qDebug() << "Reason:" << filterData->errorMsg(); break; default: qDebug() << "*** Result: Unknown or invalid resource."; } if (!expectedResult.isEmpty()) { // Hack for other locales than english, normalize google hosts to google.com cmd.replace(QRegExp(QStringLiteral("www\\.google\\.[^/]*/")), QStringLiteral("www.google.com/")); if (cmd != expectedResult) { qWarning() << a; QCOMPARE(cmd, expectedResult); } } if (expectedUriType != -1 && expectedUriType != filterData->uriType()) { qWarning() << a << "Got URI type" << s_uritypes[filterData->uriType()] << "expected" << s_uritypes[expectedUriType]; QCOMPARE(s_uritypes[filterData->uriType()], s_uritypes[expectedUriType]); } } else { if (expectedUriType == NO_FILTERING) { qDebug() << "*** No filtering required."; } else { qDebug() << "*** Could not be filtered."; if (expectedUriType != filterData->uriType()) { QCOMPARE(s_uritypes[filterData->uriType()], s_uritypes[expectedUriType]); } } } delete filterData; qDebug() << "-----"; } static void runFilterTest() { QFETCH(QString, input); QFETCH(QString, expectedResult); QFETCH(int, expectedUriType); QFETCH(QStringList, list); QFETCH(QString, absPath); QFETCH(bool, checkForExecutables); runFilterTest(input, expectedResult, expectedUriType, list, absPath, checkForExecutables); } static void testLocalFile(const QString &filename) { QFile tmpFile(filename); // Yeah, I know, security risk blah blah. This is a test prog! if (tmpFile.open(QIODevice::ReadWrite)) { QByteArray fname = QFile::encodeName(tmpFile.fileName()); runFilterTest(fname, fname, KUriFilterData::LocalFile); tmpFile.close(); tmpFile.remove(); } else { qDebug() << "Couldn't create " << tmpFile.fileName() << ", skipping test"; } } static char s_delimiter = ':'; // the alternative is ' ' void KUriFilterTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); minicliFilters << QStringLiteral("kshorturifilter") << QStringLiteral("kurisearchfilter") << QStringLiteral("localdomainurifilter"); qtdir = qgetenv("QTDIR"); home = qgetenv("HOME"); qputenv("DATAHOME", QFile::encodeName(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation))); datahome = qgetenv("DATAHOME"); qDebug() << "libpaths" << QCoreApplication::libraryPaths(); qputenv("KDE_FORK_SLAVES", "yes"); // simpler, for the final cleanup QLoggingCategory::setFilterRules(QStringLiteral("org.kde.kurifilter-*=true")); QString searchProvidersDir = QFINDTESTDATA("../src/urifilters/ikws/searchproviders/google.desktop").section('/', 0, -2); QVERIFY(!searchProvidersDir.isEmpty()); qputenv("KIO_SEARCHPROVIDERS_DIR", QFile::encodeName(searchProvidersDir)); // Allow testing of the search engine using both delimiters... const char *envDelimiter = ::getenv("KURIFILTERTEST_DELIMITER"); if (envDelimiter) { s_delimiter = envDelimiter[0]; } // Many tests check the "default search engine" feature. // There is no default search engine by default (since it was annoying when making typos), // so the user has to set it up, which we do here. { KConfigGroup cfg(KSharedConfig::openConfig(QStringLiteral("kuriikwsfilterrc"), KConfig::SimpleConfig), "General"); cfg.writeEntry("DefaultWebShortcut", "google"); cfg.writeEntry("KeywordDelimiter", QString(s_delimiter)); cfg.sync(); } // Copy kshorturifilterrc from the src dir so we don't depend on make install / env vars. { const QString rcFile = QFINDTESTDATA("../src/urifilters/shorturi/kshorturifilterrc"); QVERIFY(!rcFile.isEmpty()); const QString localFile = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/kshorturifilterrc"; QFile::remove(localFile); QVERIFY(QFile(rcFile).copy(localFile)); } QDir().mkpath(datahome + QStringLiteral("/urifilter")); } void KUriFilterTest::pluginNames() { const QStringList plugins = KUriFilter::self()->pluginNames(); qDebug() << plugins; const QByteArray debugString = plugins.join(',').toLatin1(); // To make it possible to have external plugins (if there's any...) // we don't just have an expected result list, we just probe it for specific entries. QVERIFY2(plugins.contains("kshorturifilter"), debugString.constData()); QVERIFY2(plugins.contains("kurisearchfilter"), debugString.constData()); QVERIFY2(plugins.contains("localdomainurifilter"), debugString.constData()); QVERIFY2(plugins.contains("fixhosturifilter"), debugString.constData()); QVERIFY2(plugins.contains("kuriikwsfilter"), debugString.constData()); // No duplicates QCOMPARE(plugins.count("kshorturifilter"), 1); } void KUriFilterTest::noFiltering_data() { setupColumns(); // URI that should require no filtering addRow("http://www.kde.org", QStringLiteral("http://www.kde.org"), KUriFilterData::NetProtocol); #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 1) // qtbase commit eaf4438b3511c preserves the double slashes addRow("http://www.kde.org/developer//index.html", QStringLiteral("http://www.kde.org/developer//index.html"), KUriFilterData::NetProtocol); #else addRow("http://www.kde.org/developer//index.html", QStringLiteral("http://www.kde.org/developer/index.html"), KUriFilterData::NetProtocol); #endif addRow("file:///", QStringLiteral("/"), KUriFilterData::LocalDir); addRow("file:///etc", QStringLiteral("/etc"), KUriFilterData::LocalDir); addRow("file:///etc/passwd", QStringLiteral("/etc/passwd"), KUriFilterData::LocalFile); } void KUriFilterTest::noFiltering() { runFilterTest(); } void KUriFilterTest::localFiles_data() { setupColumns(); addRow("/", QStringLiteral("/"), KUriFilterData::LocalDir); addRow("/", QStringLiteral("/"), KUriFilterData::LocalDir, QStringList(QStringLiteral("kshorturifilter"))); addRow("//", QStringLiteral("/"), KUriFilterData::LocalDir); addRow("///", QStringLiteral("/"), KUriFilterData::LocalDir); addRow("////", QStringLiteral("/"), KUriFilterData::LocalDir); addRow("///tmp", QStringLiteral("/tmp"), KUriFilterData::LocalDir); addRow("///tmp/", QStringLiteral("/tmp/"), KUriFilterData::LocalDir); addRow("///tmp//", QStringLiteral("/tmp/"), KUriFilterData::LocalDir); addRow("///tmp///", QStringLiteral("/tmp/"), KUriFilterData::LocalDir); if (QFile::exists(QDir::homePath() + QLatin1String("/.bashrc"))) { addRow("~/.bashrc", QDir::homePath() + QStringLiteral("/.bashrc"), KUriFilterData::LocalFile, QStringList(QStringLiteral("kshorturifilter"))); } addRow("~", QDir::homePath(), KUriFilterData::LocalDir, QStringList(QStringLiteral("kshorturifilter")), QStringLiteral("/tmp")); addRow("~bin", nullptr, KUriFilterData::LocalDir, QStringList(QStringLiteral("kshorturifilter"))); addRow("~does_not_exist", nullptr, KUriFilterData::Error, QStringList(QStringLiteral("kshorturifilter"))); addRow("~/does_not_exist", QDir::homePath() + "/does_not_exist", KUriFilterData::LocalFile, QStringList(QStringLiteral("kshorturifilter"))); // Absolute Path tests for kshorturifilter const QStringList kshorturifilter(QStringLiteral("kshorturifilter")); addRow("./", datahome, KUriFilterData::LocalDir, kshorturifilter, datahome + QStringLiteral("/")); // cleanPath removes the trailing slash const QString parentDir = QDir().cleanPath(datahome + QStringLiteral("/..")); addRow("../", QFile::encodeName(parentDir), KUriFilterData::LocalDir, kshorturifilter, datahome); addRow("share", datahome, KUriFilterData::LocalDir, kshorturifilter, QFile::encodeName(parentDir)); // Invalid URLs addRow("http://a[b]", QStringLiteral("http://a[b]"), KUriFilterData::Unknown, kshorturifilter, QStringLiteral("/")); } void KUriFilterTest::localFiles() { runFilterTest(); } void KUriFilterTest::refOrQuery_data() { setupColumns(); // URL with reference addRow("http://www.kde.org/index.html#q8", QStringLiteral("http://www.kde.org/index.html#q8"), KUriFilterData::NetProtocol); // local file with reference addRow("file:/etc/passwd#q8", QStringLiteral("file:///etc/passwd#q8"), KUriFilterData::LocalFile); addRow("file:///etc/passwd#q8", QStringLiteral("file:///etc/passwd#q8"), KUriFilterData::LocalFile); addRow("/etc/passwd#q8", QStringLiteral("file:///etc/passwd#q8"), KUriFilterData::LocalFile); // local file with query (can be used by javascript) addRow("file:/etc/passwd?foo=bar", QStringLiteral("file:///etc/passwd?foo=bar"), KUriFilterData::LocalFile); testLocalFile(QStringLiteral("/tmp/kurifiltertest?foo")); // local file with ? in the name (#58990) testLocalFile(QStringLiteral("/tmp/kurlfiltertest#foo")); // local file with '#' in the name testLocalFile(QStringLiteral("/tmp/kurlfiltertest#foo?bar")); // local file with both testLocalFile(QStringLiteral("/tmp/kurlfiltertest?foo#bar")); // local file with both, the other way round } void KUriFilterTest::refOrQuery() { runFilterTest(); } void KUriFilterTest::shortUris_data() { setupColumns(); // hostnames are lowercased by QUrl #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 1) // qtbase commit eaf4438b3511c preserves the double slashes addRow("http://www.myDomain.commyPort/ViewObjectRes//Default:name=hello", QStringLiteral("http://www.mydomain.commyport/ViewObjectRes//Default:name=hello"), KUriFilterData::NetProtocol); #else addRow("http://www.myDomain.commyPort/ViewObjectRes//Default:name=hello", QStringLiteral("http://www.mydomain.commyport/ViewObjectRes/Default:name=hello"), KUriFilterData::NetProtocol); #endif addRow("http://www.myDomain.commyPort/ViewObjectRes/Default:name=hello?a=a///////", QStringLiteral("http://www.mydomain.commyport/ViewObjectRes/Default:name=hello?a=a///////"), KUriFilterData::NetProtocol); addRow("ftp://ftp.kde.org", QStringLiteral("ftp://ftp.kde.org"), KUriFilterData::NetProtocol); addRow("ftp://username@ftp.kde.org:500", QStringLiteral("ftp://username@ftp.kde.org:500"), KUriFilterData::NetProtocol); // ShortURI/LocalDomain filter tests. addRow("linuxtoday.com", QStringLiteral("http://linuxtoday.com"), KUriFilterData::NetProtocol); addRow("LINUXTODAY.COM", QStringLiteral("http://linuxtoday.com"), KUriFilterData::NetProtocol); addRow("kde.org", QStringLiteral("http://kde.org"), KUriFilterData::NetProtocol); addRow("ftp.kde.org", QStringLiteral("ftp://ftp.kde.org"), KUriFilterData::NetProtocol); addRow("ftp.kde.org:21", QStringLiteral("ftp://ftp.kde.org:21"), KUriFilterData::NetProtocol); addRow("cr.yp.to", QStringLiteral("http://cr.yp.to"), KUriFilterData::NetProtocol); addRow("www.kde.org:21", QStringLiteral("http://www.kde.org:21"), KUriFilterData::NetProtocol); // This one passes but the DNS lookup takes 5 seconds to fail //addRow("foobar.local:8000", QStringLiteral("http://foobar.local:8000"), KUriFilterData::NetProtocol); addRow("foo@bar.com", QStringLiteral("mailto:foo@bar.com"), KUriFilterData::NetProtocol); addRow("firstname.lastname@x.foo.bar", QStringLiteral("mailto:firstname.lastname@x.foo.bar"), KUriFilterData::NetProtocol); addRow("mailto:foo@bar.com", QStringLiteral("mailto:foo@bar.com"), KUriFilterData::NetProtocol); addRow("www.123.foo", QStringLiteral("http://www.123.foo"), KUriFilterData::NetProtocol); addRow("user@www.123.foo:3128", QStringLiteral("http://user@www.123.foo:3128"), KUriFilterData::NetProtocol); addRow("ftp://user@user@www.123.foo:3128", QStringLiteral("ftp://user%40user@www.123.foo:3128"), KUriFilterData::NetProtocol); addRow("user@user@www.123.foo:3128", QStringLiteral("http://user%40user@www.123.foo:3128"), KUriFilterData::NetProtocol); // IPv4 address formats... addRow("user@192.168.1.0:3128", QStringLiteral("http://user@192.168.1.0:3128"), KUriFilterData::NetProtocol); addRow("127.0.0.1", QStringLiteral("http://127.0.0.1"), KUriFilterData::NetProtocol); addRow("127.0.0.1:3128", QStringLiteral("http://127.0.0.1:3128"), KUriFilterData::NetProtocol); addRow("127.1", QStringLiteral("http://127.0.0.1"), KUriFilterData::NetProtocol); // Qt5: QUrl resolves to 127.0.0.1 addRow("127.0.1", QStringLiteral("http://127.0.0.1"), KUriFilterData::NetProtocol); // Qt5: QUrl resolves to 127.0.0.1 // IPv6 address formats (taken from RFC 2732)... addRow("[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html", QStringLiteral("http://[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:80/index.html"), KUriFilterData::NetProtocol); addRow("[1080:0:0:0:8:800:200C:417A]/index.html", QStringLiteral("http://[1080::8:800:200c:417a]/index.html"), KUriFilterData::NetProtocol); // Qt5 QUrl change addRow("[3ffe:2a00:100:7031::1]", QStringLiteral("http://[3ffe:2a00:100:7031::1]"), KUriFilterData::NetProtocol); addRow("[1080::8:800:200C:417A]/foo", QStringLiteral("http://[1080::8:800:200c:417a]/foo"), KUriFilterData::NetProtocol); addRow("[::192.9.5.5]/ipng", QStringLiteral("http://[::192.9.5.5]/ipng"), KUriFilterData::NetProtocol); addRow("[::FFFF:129.144.52.38]:80/index.html", QStringLiteral("http://[::ffff:129.144.52.38]:80/index.html"), KUriFilterData::NetProtocol); addRow("[2010:836B:4179::836B:4179]", QStringLiteral("http://[2010:836b:4179::836b:4179]"), KUriFilterData::NetProtocol); // Local domain filter - If you uncomment these test, make sure you // you adjust it based on the localhost entry in your /etc/hosts file. // addRow( "localhost:3128", "http://localhost.localdomain:3128", KUriFilterData::NetProtocol ); // addRow( "localhost", "http://localhost.localdomain", KUriFilterData::NetProtocol ); // addRow( "localhost/~blah", "http://localhost.localdomain/~blah", KUriFilterData::NetProtocol ); addRow("user@host.domain", QStringLiteral("mailto:user@host.domain"), KUriFilterData::NetProtocol); // new in KDE-3.2 // Windows style SMB (UNC) URL. Should be converted into the valid smb format... addRow("\\\\mainserver\\share\\file", QStringLiteral("smb://mainserver/share/file"), KUriFilterData::NetProtocol); // KDE3: was not be filtered at all. All valid protocols of this form were be ignored. // KDE4: parsed as "network protocol", seems fine to me (DF) addRow("ftp:", QStringLiteral("ftp:"), KUriFilterData::NetProtocol); addRow("http:", QStringLiteral("http:"), KUriFilterData::NetProtocol); // The default search engine is set to 'Google' //this may fail if your DNS knows domains KDE or FTP addRow("gg:", QLatin1String(""), KUriFilterData::NetProtocol); // see bug 56218 addRow("KDE", QStringLiteral("https://www.google.com/search?q=KDE&ie=UTF-8"), KUriFilterData::NetProtocol); addRow("HTTP", QStringLiteral("https://www.google.com/search?q=HTTP&ie=UTF-8"), KUriFilterData::NetProtocol); } void KUriFilterTest::shortUris() { runFilterTest(); } void KUriFilterTest::executables_data() { setupColumns(); // Executable tests - No IKWS in minicli addRow("cp", QStringLiteral("cp"), KUriFilterData::Executable, minicliFilters); addRow("kbuildsycoca5", QStringLiteral("kbuildsycoca5"), KUriFilterData::Executable, minicliFilters); addRow("KDE", QStringLiteral("KDE"), NO_FILTERING, minicliFilters); addRow("does/not/exist", QStringLiteral("does/not/exist"), NO_FILTERING, minicliFilters); addRow("/does/not/exist", QStringLiteral("/does/not/exist"), KUriFilterData::LocalFile, minicliFilters); addRow("/does/not/exist#a", QStringLiteral("/does/not/exist#a"), KUriFilterData::LocalFile, minicliFilters); addRow("kbuildsycoca5 --help", QStringLiteral("kbuildsycoca5 --help"), KUriFilterData::Executable, minicliFilters); // the args are in argsAndOptions() addRow("/bin/sh", QStringLiteral("/bin/sh"), KUriFilterData::Executable, minicliFilters); addRow("/bin/sh -q -option arg1", QStringLiteral("/bin/sh -q -option arg1"), KUriFilterData::Executable, minicliFilters); // the args are in argsAndOptions() // Typing 'cp' or any other valid unix command in konq's location bar should result in // a search using the default search engine // 'ls' is a bit of a special case though, due to the toplevel domain called 'ls' addRow("cp", QStringLiteral("https://www.google.com/search?q=cp&ie=UTF-8"), KUriFilterData::NetProtocol, QStringList(), nullptr, false /* don't check for executables, see konq_misc.cc */); } void KUriFilterTest::executables() { runFilterTest(); } void KUriFilterTest::environmentVariables_data() { setupColumns(); // ENVIRONMENT variable qputenv("SOMEVAR", "/somevar"); qputenv("ETC", "/etc"); addRow("$SOMEVAR/kdelibs/kio", "/somevar/kdelibs/kio", KUriFilterData::LocalFile); // note: this dir doesn't exist... addRow("$ETC/passwd", QStringLiteral("/etc/passwd"), KUriFilterData::LocalFile); QString qtdocPath = qtdir + QStringLiteral("/doc/html/functions.html"); if (QFile::exists(qtdocPath)) { QString expectedUrl = QUrl::fromLocalFile(qtdocPath).toString() + "#s"; addRow("$QTDIR/doc/html/functions.html#s", expectedUrl.toUtf8(), KUriFilterData::LocalFile); } addRow("http://www.kde.org/$USER", QStringLiteral("http://www.kde.org/$USER"), KUriFilterData::NetProtocol); // no expansion addRow("$DATAHOME", datahome, KUriFilterData::LocalDir); QDir().mkpath(datahome + QStringLiteral("/urifilter/a+plus")); addRow("$DATAHOME/urifilter/a+plus", datahome + QStringLiteral("/urifilter/a+plus"), KUriFilterData::LocalDir); // BR 27788 QDir().mkpath(datahome + QStringLiteral("/Dir With Space")); addRow("$DATAHOME/Dir With Space", datahome + QStringLiteral("/Dir With Space"), KUriFilterData::LocalDir); // support for name filters (BR 93825) addRow("$DATAHOME/*.txt", datahome + QStringLiteral("/*.txt"), KUriFilterData::LocalDir); addRow("$DATAHOME/[a-b]*.txt", datahome + QStringLiteral("/[a-b]*.txt"), KUriFilterData::LocalDir); addRow("$DATAHOME/a?c.txt", datahome + QStringLiteral("/a?c.txt"), KUriFilterData::LocalDir); addRow("$DATAHOME/?c.txt", datahome + QStringLiteral("/?c.txt"), KUriFilterData::LocalDir); // but let's check that a directory with * in the name still works QDir().mkpath(datahome + QStringLiteral("/share/Dir*With*Stars")); addRow("$DATAHOME/Dir*With*Stars", datahome + QStringLiteral("/Dir*With*Stars"), KUriFilterData::LocalDir); QDir().mkpath(datahome + QStringLiteral("/Dir?QuestionMark")); addRow("$DATAHOME/Dir?QuestionMark", datahome + QStringLiteral("/Dir?QuestionMark"), KUriFilterData::LocalDir); QDir().mkpath(datahome + QStringLiteral("/Dir[Bracket")); addRow("$DATAHOME/Dir[Bracket", datahome + QStringLiteral("/Dir[Bracket"), KUriFilterData::LocalDir); addRow("$HOME/$KDEDIR/kdebase/kcontrol/ebrowsing", "", KUriFilterData::LocalFile); addRow("$1/$2/$3", QStringLiteral("https://www.google.com/search?q=%241%2F%242%2F%243&ie=UTF-8"), KUriFilterData::NetProtocol); // can be used as bogus or valid test. Currently triggers default search, i.e. google addRow("$$$$", QStringLiteral("https://www.google.com/search?q=%24%24%24%24&ie=UTF-8"), KUriFilterData::NetProtocol); // worst case scenarios. if (!qtdir.isEmpty()) { addRow("$QTDIR", qtdir, KUriFilterData::LocalDir, QStringList(QStringLiteral("kshorturifilter"))); //use specific filter. } addRow("$HOME", home, KUriFilterData::LocalDir, QStringList(QStringLiteral("kshorturifilter"))); //use specific filter. } void KUriFilterTest::environmentVariables() { runFilterTest(); } void KUriFilterTest::internetKeywords_data() { setupColumns(); - QString sc; - addRow(sc.sprintf("gg%cfoo bar", s_delimiter).toUtf8(), QStringLiteral("https://www.google.com/search?q=foo+bar&ie=UTF-8"), KUriFilterData::NetProtocol); - addRow(sc.sprintf("bug%c55798", s_delimiter).toUtf8(), QStringLiteral("https://bugs.kde.org/show_bug.cgi?id=55798"), KUriFilterData::NetProtocol); - - addRow(sc.sprintf("gg%cC++", s_delimiter).toUtf8(), QStringLiteral("https://www.google.com/search?q=C%2B%2B&ie=UTF-8"), KUriFilterData::NetProtocol); - addRow(sc.sprintf("gg%cC#", s_delimiter).toUtf8(), QStringLiteral("https://www.google.com/search?q=C%23&ie=UTF-8"), KUriFilterData::NetProtocol); - addRow(sc.sprintf("ya%cfoo bar was here", s_delimiter).toUtf8(), nullptr, -1); // this triggers default search, i.e. google - addRow(sc.sprintf("gg%cwww.kde.org", s_delimiter).toUtf8(), QStringLiteral("https://www.google.com/search?q=www.kde.org&ie=UTF-8"), KUriFilterData::NetProtocol); + addRow(QString::asprintf("gg%cfoo bar", s_delimiter).toUtf8(), QStringLiteral("https://www.google.com/search?q=foo+bar&ie=UTF-8"), KUriFilterData::NetProtocol); + addRow(QString::asprintf("bug%c55798", s_delimiter).toUtf8(), QStringLiteral("https://bugs.kde.org/show_bug.cgi?id=55798"), KUriFilterData::NetProtocol); + + addRow(QString::asprintf("gg%cC++", s_delimiter).toUtf8(), QStringLiteral("https://www.google.com/search?q=C%2B%2B&ie=UTF-8"), KUriFilterData::NetProtocol); + addRow(QString::asprintf("gg%cC#", s_delimiter).toUtf8(), QStringLiteral("https://www.google.com/search?q=C%23&ie=UTF-8"), KUriFilterData::NetProtocol); + addRow(QString::asprintf("ya%cfoo bar was here", s_delimiter).toUtf8(), nullptr, -1); // this triggers default search, i.e. google + addRow(QString::asprintf("gg%cwww.kde.org", s_delimiter).toUtf8(), QStringLiteral("https://www.google.com/search?q=www.kde.org&ie=UTF-8"), KUriFilterData::NetProtocol); addRow(QStringLiteral("gg%1é").arg(s_delimiter).toUtf8() /*eaccent in utf8*/, QStringLiteral("https://www.google.com/search?q=%C3%A9&ie=UTF-8"), KUriFilterData::NetProtocol); addRow(QStringLiteral("gg%1прйвет").arg(s_delimiter).toUtf8() /* greetings in russian utf-8*/, QStringLiteral("https://www.google.com/search?q=%D0%BF%D1%80%D0%B9%D0%B2%D0%B5%D1%82&ie=UTF-8"), KUriFilterData::NetProtocol); } void KUriFilterTest::internetKeywords() { runFilterTest(); } void KUriFilterTest::localdomain() { const QString host = QHostInfo::localHostName(); if (host.isEmpty()) { const QString expected = QLatin1String("http://") + host; runFilterTest(host, expected, KUriFilterData::NetProtocol, QStringList() << QStringLiteral("localdomainurifilter"), nullptr, false); } } diff --git a/autotests/udsentry_benchmark.cpp b/autotests/udsentry_benchmark.cpp index 212b34d3..68d701d2 100644 --- a/autotests/udsentry_benchmark.cpp +++ b/autotests/udsentry_benchmark.cpp @@ -1,694 +1,694 @@ /* This file is part of the KDE project Copyright (C) 2004-2014 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 #include #include #include #include #include #include // filesize_t /* This is to compare the old list-of-lists API vs a QMap/QHash-based API in terms of performance. The number of atoms and their type map to what kio_file would put in for any normal file. The lookups are done for two atoms that are present, and for one that is not. */ class UdsEntryBenchmark : public QObject { Q_OBJECT public: UdsEntryBenchmark() : nameStr(QStringLiteral("name")), now(QDateTime::currentDateTime()), - now_time_t(now.toTime_t()) + now_time_t(now.toSecsSinceEpoch()) {} private Q_SLOTS: void testKDE3Slave(); void testKDE3App(); void testHashVariantSlave(); void testHashVariantApp(); void testHashStructSlave(); void testHashStructApp(); void testMapStructSlave(); void testMapStructApp(); void testTwoVectorsSlaveFill(); void testTwoVectorsSlaveCompare(); void testTwoVectorsApp(); void testAnotherSlaveFill(); void testAnotherSlaveCompare(); void testAnotherApp(); void testAnotherV2SlaveFill(); void testAnotherV2SlaveCompare(); void testAnotherV2App(); private: const QString nameStr; const QDateTime now; const time_t now_time_t; }; class OldUDSAtom { public: QString m_str; long long m_long; unsigned int m_uds; }; typedef QList OldUDSEntry; // well it was a QValueList :) static void fillOldUDSEntry(OldUDSEntry &entry, time_t now_time_t, const QString &nameStr) { OldUDSAtom atom; atom.m_uds = KIO::UDSEntry::UDS_NAME; atom.m_str = nameStr; entry.append(atom); atom.m_uds = KIO::UDSEntry::UDS_SIZE; atom.m_long = 123456ULL; entry.append(atom); atom.m_uds = KIO::UDSEntry::UDS_MODIFICATION_TIME; atom.m_long = now_time_t; entry.append(atom); atom.m_uds = KIO::UDSEntry::UDS_ACCESS_TIME; atom.m_long = now_time_t; entry.append(atom); atom.m_uds = KIO::UDSEntry::UDS_FILE_TYPE; atom.m_long = S_IFREG; entry.append(atom); atom.m_uds = KIO::UDSEntry::UDS_ACCESS; atom.m_long = 0644; entry.append(atom); atom.m_uds = KIO::UDSEntry::UDS_USER; atom.m_str = nameStr; entry.append(atom); atom.m_uds = KIO::UDSEntry::UDS_GROUP; atom.m_str = nameStr; entry.append(atom); } void UdsEntryBenchmark::testKDE3Slave() { QBENCHMARK { OldUDSEntry entry; fillOldUDSEntry(entry, now_time_t, nameStr); QCOMPARE(entry.count(), 8); } } void UdsEntryBenchmark::testKDE3App() { OldUDSEntry entry; fillOldUDSEntry(entry, now_time_t, nameStr); QString displayName; KIO::filesize_t size; QString url; QBENCHMARK { OldUDSEntry::ConstIterator it2 = entry.constBegin(); for (; it2 != entry.constEnd(); it2++) { switch ((*it2).m_uds) { case KIO::UDSEntry::UDS_NAME: displayName = (*it2).m_str; break; case KIO::UDSEntry::UDS_URL: url = (*it2).m_str; break; case KIO::UDSEntry::UDS_SIZE: size = (*it2).m_long; break; } } QCOMPARE(size, 123456ULL); QCOMPARE(displayName, QStringLiteral("name")); QVERIFY(url.isEmpty()); } } // QHash or QMap? doesn't seem to make much difference. typedef QHash UDSEntryHV; // This uses QDateTime instead of time_t static void fillUDSEntryHV(UDSEntryHV &entry, const QDateTime &now, const QString &nameStr) { entry.reserve(8); entry.insert(KIO::UDSEntry::UDS_NAME, nameStr); // we might need a method to make sure people use unsigned long long entry.insert(KIO::UDSEntry::UDS_SIZE, 123456ULL); entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, now); entry.insert(KIO::UDSEntry::UDS_ACCESS_TIME, now); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); entry.insert(KIO::UDSEntry::UDS_ACCESS, 0644); entry.insert(KIO::UDSEntry::UDS_USER, nameStr); entry.insert(KIO::UDSEntry::UDS_GROUP, nameStr); } void UdsEntryBenchmark::testHashVariantSlave() { const QDateTime now = QDateTime::currentDateTime(); QBENCHMARK { UDSEntryHV entry; fillUDSEntryHV(entry, now, nameStr); QCOMPARE(entry.count(), 8); } } void UdsEntryBenchmark::testHashVariantApp() { // Normally the code would look like this, but let's change it to time it like the old api /* QString displayName = entry.value( KIO::UDSEntry::UDS_NAME ).toString(); QUrl url = entry.value( KIO::UDSEntry::UDS_URL ).toString(); KIO::filesize_t size = entry.value( KIO::UDSEntry::UDS_SIZE ).toULongLong(); */ UDSEntryHV entry; fillUDSEntryHV(entry, now, nameStr); QString displayName; KIO::filesize_t size; QString url; QBENCHMARK { // For a field that we assume to always be there displayName = entry.value(KIO::UDSEntry::UDS_NAME).toString(); // For a field that might not be there UDSEntryHV::const_iterator it = entry.constFind(KIO::UDSEntry::UDS_URL); const UDSEntryHV::const_iterator end = entry.constEnd(); if (it != end) { url = it.value().toString(); } it = entry.constFind(KIO::UDSEntry::UDS_SIZE); if (it != end) { size = it.value().toULongLong(); } QCOMPARE(size, 123456ULL); QCOMPARE(displayName, QStringLiteral("name")); QVERIFY(url.isEmpty()); } } // The KDE4 solution: QHash+struct // Which one is used depends on UDS_STRING vs UDS_LONG struct UDSAtom4 { // can't be a union due to qstring... UDSAtom4() {} // for QHash or QMap UDSAtom4(const QString &s) : m_str(s) {} UDSAtom4(long long l) : m_long(l) {} QString m_str; long long m_long; }; // Another possibility, to save on QVariant costs typedef QHash UDSEntryHS; // hash+struct static void fillQHashStructEntry(UDSEntryHS &entry, time_t now_time_t, const QString &nameStr) { entry.reserve(8); entry.insert(KIO::UDSEntry::UDS_NAME, nameStr); entry.insert(KIO::UDSEntry::UDS_SIZE, 123456ULL); entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, now_time_t); entry.insert(KIO::UDSEntry::UDS_ACCESS_TIME, now_time_t); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); entry.insert(KIO::UDSEntry::UDS_ACCESS, 0644); entry.insert(KIO::UDSEntry::UDS_USER, nameStr); entry.insert(KIO::UDSEntry::UDS_GROUP, nameStr); } void UdsEntryBenchmark::testHashStructSlave() { QBENCHMARK { UDSEntryHS entry; fillQHashStructEntry(entry, now_time_t, nameStr); QCOMPARE(entry.count(), 8); } } void UdsEntryBenchmark::testHashStructApp() { UDSEntryHS entry; fillQHashStructEntry(entry, now_time_t, nameStr); QString displayName; KIO::filesize_t size; QString url; QBENCHMARK { // For a field that we assume to always be there displayName = entry.value(KIO::UDSEntry::UDS_NAME).m_str; // For a field that might not be there UDSEntryHS::const_iterator it = entry.constFind(KIO::UDSEntry::UDS_URL); const UDSEntryHS::const_iterator end = entry.constEnd(); if (it != end) { url = it.value().m_str; } it = entry.constFind(KIO::UDSEntry::UDS_SIZE); if (it != end) { size = it.value().m_long; } QCOMPARE(size, 123456ULL); QCOMPARE(displayName, QStringLiteral("name")); QVERIFY(url.isEmpty()); } } // Let's see if QMap makes any difference typedef QMap UDSEntryMS; // map+struct static void fillQMapStructEntry(UDSEntryMS &entry, time_t now_time_t, const QString &nameStr) { entry.insert(KIO::UDSEntry::UDS_NAME, nameStr); entry.insert(KIO::UDSEntry::UDS_SIZE, 123456ULL); entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, now_time_t); entry.insert(KIO::UDSEntry::UDS_ACCESS_TIME, now_time_t); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); entry.insert(KIO::UDSEntry::UDS_ACCESS, 0644); entry.insert(KIO::UDSEntry::UDS_USER, nameStr); entry.insert(KIO::UDSEntry::UDS_GROUP, nameStr); } void UdsEntryBenchmark::testMapStructSlave() { QBENCHMARK { UDSEntryMS entry; fillQMapStructEntry(entry, now_time_t, nameStr); QCOMPARE(entry.count(), 8); } } void UdsEntryBenchmark::testMapStructApp() { UDSEntryMS entry; fillQMapStructEntry(entry, now_time_t, nameStr); QString displayName; KIO::filesize_t size; QString url; QBENCHMARK { // For a field that we assume to always be there displayName = entry.value(KIO::UDSEntry::UDS_NAME).m_str; // For a field that might not be there UDSEntryMS::const_iterator it = entry.constFind(KIO::UDSEntry::UDS_URL); const UDSEntryMS::const_iterator end = entry.constEnd(); if (it != end) { url = it.value().m_str; } it = entry.constFind(KIO::UDSEntry::UDS_SIZE); if (it != end) { size = it.value().m_long; } QCOMPARE(size, 123456ULL); QCOMPARE(displayName, QStringLiteral("name")); QVERIFY(url.isEmpty()); } } // Frank's suggestion in https://git.reviewboard.kde.org/r/118452/ class FrankUDSEntry { public: class Field { public: inline Field(const QString &value) : m_str(value), m_long(0) {} inline Field(long long value = 0) : m_long(value) { } QString m_str; long long m_long; }; QVector fields; // If udsIndexes[i] == uds, then fields[i] contains the value for 'uds'. QVector udsIndexes; void reserve(int size) { fields.reserve(size); udsIndexes.reserve(size); } void insert(uint udsField, const QString &value) { const int index = udsIndexes.indexOf(udsField); if (index >= 0) { fields[index] = Field(value); } else { udsIndexes.append(udsField); fields.append(Field(value)); } } void replaceOrInsert(uint udsField, const QString &value) { insert(udsField, value); } void insert(uint udsField, long long value) { const int index = udsIndexes.indexOf(udsField); if (index >= 0) { fields[index] = Field(value); } else { udsIndexes.append(udsField); fields.append(Field(value)); } } void replaceOrInsert(uint udsField, long long value) { insert(udsField, value); } int count() const { return udsIndexes.count(); } QString stringValue(uint udsField) const { const int index = udsIndexes.indexOf(udsField); if (index >= 0) { return fields.at(index).m_str; } else { return QString(); } } long long numberValue(uint udsField, long long defaultValue = -1) const { const int index = udsIndexes.indexOf(udsField); if (index >= 0) { return fields.at(index).m_long; } else { return defaultValue; } } }; template static void fillUDSEntries(T &entry, time_t now_time_t, const QString &nameStr) { entry.reserve(8); // In random order of index entry.insert(KIO::UDSEntry::UDS_ACCESS_TIME, now_time_t); entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, now_time_t); entry.insert(KIO::UDSEntry::UDS_SIZE, 123456ULL); entry.insert(KIO::UDSEntry::UDS_NAME, nameStr); entry.insert(KIO::UDSEntry::UDS_GROUP, nameStr); entry.insert(KIO::UDSEntry::UDS_USER, nameStr); entry.insert(KIO::UDSEntry::UDS_ACCESS, 0644); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); } template void testFill(time_t now_time_t, const QString &nameStr) { QBENCHMARK { T entry; fillUDSEntries (entry, now_time_t, nameStr); QCOMPARE(entry.count(), 8); } } template void testCompare(time_t now_time_t, const QString &nameStr) { T entry; T entry2; fillUDSEntries (entry, now_time_t, nameStr); fillUDSEntries (entry2, now_time_t, nameStr); QCOMPARE(entry.count(), 8); QCOMPARE(entry2.count(), 8); QBENCHMARK { bool equal = entry.stringValue(KIO::UDSEntry::UDS_NAME) == entry2.stringValue(KIO::UDSEntry::UDS_NAME) && entry.numberValue(KIO::UDSEntry::UDS_SIZE) == entry2.numberValue(KIO::UDSEntry::UDS_SIZE) && entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME) == entry2.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME) && entry.numberValue(KIO::UDSEntry::UDS_ACCESS_TIME) == entry2.numberValue(KIO::UDSEntry::UDS_ACCESS_TIME) && entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE) == entry2.numberValue(KIO::UDSEntry::UDS_FILE_TYPE) && entry.numberValue(KIO::UDSEntry::UDS_ACCESS) == entry2.numberValue(KIO::UDSEntry::UDS_ACCESS) && entry.stringValue(KIO::UDSEntry::UDS_USER) == entry2.stringValue(KIO::UDSEntry::UDS_USER) && entry.stringValue(KIO::UDSEntry::UDS_GROUP) == entry2.stringValue(KIO::UDSEntry::UDS_GROUP); QVERIFY(equal); } } template void testApp(time_t now_time_t, const QString &nameStr) { T entry; fillUDSEntries (entry, now_time_t, nameStr); QString displayName; KIO::filesize_t size; QString url; QBENCHMARK { displayName = entry.stringValue(KIO::UDSEntry::UDS_NAME); url = entry.stringValue(KIO::UDSEntry::UDS_URL); size = entry.numberValue(KIO::UDSEntry::UDS_SIZE); QCOMPARE(size, 123456ULL); QCOMPARE(displayName, QStringLiteral("name")); QVERIFY(url.isEmpty()); } } void UdsEntryBenchmark::testTwoVectorsSlaveFill() { testFill(now_time_t, nameStr); } void UdsEntryBenchmark::testTwoVectorsSlaveCompare() { testCompare(now_time_t, nameStr); } void UdsEntryBenchmark::testTwoVectorsApp() { testApp(now_time_t, nameStr); } // Instead of two vectors, use only one class AnotherUDSEntry { private: struct Field { inline Field() {} inline Field(const uint index, const QString &value) : m_str(value), m_index(index) {} inline Field(const uint index, long long value = 0) : m_long(value), m_index(index) {} // This operator helps to gain 1ms just comparing the key inline bool operator == (const Field &other) const { return m_index == other.m_index; } QString m_str; long long m_long = LLONG_MIN; uint m_index = 0; }; std::vector storage; public: void reserve(int size) { storage.reserve(size); } void insert(uint udsField, const QString &value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_STRING); Q_ASSERT(std::find_if(storage.cbegin(), storage.cend(), [udsField](const Field &entry) {return entry.m_index == udsField;}) == storage.cend()); storage.emplace_back(udsField, value); } void replaceOrInsert(uint udsField, const QString &value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_STRING); auto it = std::find_if(storage.begin(), storage.end(), [udsField](const Field &entry) {return entry.m_index == udsField;}); if (it != storage.end()) { it->m_str = value; return; } storage.emplace_back(udsField, value); } void insert(uint udsField, long long value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_NUMBER); Q_ASSERT(std::find_if(storage.cbegin(), storage.cend(), [udsField](const Field &entry) {return entry.m_index == udsField;}) == storage.cend()); storage.emplace_back(udsField, value); } void replaceOrInsert(uint udsField, long long value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_NUMBER); auto it = std::find_if(storage.begin(), storage.end(), [udsField](const Field &entry) {return entry.m_index == udsField;}); if (it != storage.end()) { it->m_long = value; return; } storage.emplace_back(udsField, value); } int count() const { return storage.size(); } QString stringValue(uint udsField) const { auto it = std::find_if(storage.cbegin(), storage.cend(), [udsField](const Field &entry) {return entry.m_index == udsField;}); if (it != storage.cend()) { return it->m_str; } return QString(); } long long numberValue(uint udsField, long long defaultValue = -1) const { auto it = std::find_if(storage.cbegin(), storage.cend(), [udsField](const Field &entry) {return entry.m_index == udsField;}); if (it != storage.cend()) { return it->m_long; } return defaultValue; } }; Q_DECLARE_TYPEINFO(AnotherUDSEntry, Q_MOVABLE_TYPE); void UdsEntryBenchmark::testAnotherSlaveFill() { testFill(now_time_t, nameStr); } void UdsEntryBenchmark::testAnotherSlaveCompare() { testCompare(now_time_t, nameStr); } void UdsEntryBenchmark::testAnotherApp() { testApp(now_time_t, nameStr); } // Instead of two vectors, use only one sorted by index and accessed using a binary search. class AnotherV2UDSEntry { private: struct Field { inline Field() {} inline Field(const uint index, const QString &value) : m_str(value), m_index(index) {} inline Field(const uint index, long long value = 0) : m_long(value), m_index(index) { } // This operator helps to gain 1ms just comparing the key inline bool operator == (const Field &other) const { return m_index == other.m_index; } QString m_str; long long m_long = LLONG_MIN; uint m_index = 0; }; std::vector storage; private: static inline bool less (const Field &other, const uint index) { return other.m_index < index; } public: void reserve(int size) { storage.reserve(size); } void insert(uint udsField, const QString &value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_STRING); auto it = std::lower_bound(storage.cbegin(), storage.cend(), udsField, less); Q_ASSERT(it == storage.cend() || it->m_index != udsField); storage.insert(it, Field(udsField, value)); } void replaceOrInsert(uint udsField, const QString &value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_STRING); auto it = std::lower_bound(storage.begin(), storage.end(), udsField, less); if (it != storage.end() && it->m_index == udsField ) { it->m_str = value; return; } storage.insert(it, Field(udsField, value)); } void insert(uint udsField, long long value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_NUMBER); auto it = std::lower_bound(storage.cbegin(), storage.cend(), udsField, less); Q_ASSERT(it == storage.end() || it->m_index != udsField); storage.insert(it, Field(udsField, value)); } void replaceOrInsert(uint udsField, long long value) { Q_ASSERT(udsField & KIO::UDSEntry::UDS_NUMBER); auto it = std::lower_bound(storage.begin(), storage.end(), udsField, less); if (it != storage.end() && it->m_index == udsField ) { it->m_long = value; return; } storage.insert(it, Field(udsField, value)); } int count() const { return storage.size(); } QString stringValue(uint udsField) const { auto it = std::lower_bound(storage.cbegin(), storage.cend(), udsField, less); if (it != storage.end() && it->m_index == udsField ) { return it->m_str; } return QString(); } long long numberValue(uint udsField, long long defaultValue = -1) const { auto it = std::lower_bound(storage.cbegin(), storage.cend(), udsField, less); if (it != storage.end() && it->m_index == udsField ) { return it->m_long; } return defaultValue; } }; Q_DECLARE_TYPEINFO(AnotherV2UDSEntry, Q_MOVABLE_TYPE); void UdsEntryBenchmark::testAnotherV2SlaveFill() { testFill(now_time_t, nameStr); } void UdsEntryBenchmark::testAnotherV2SlaveCompare() { testCompare(now_time_t, nameStr); } void UdsEntryBenchmark::testAnotherV2App() { testApp(now_time_t, nameStr); } QTEST_MAIN(UdsEntryBenchmark) #include "udsentry_benchmark.moc" diff --git a/src/core/copyjob.cpp b/src/core/copyjob.cpp index 8f8be53b..ab76cc3c 100644 --- a/src/core/copyjob.cpp +++ b/src/core/copyjob.cpp @@ -1,2320 +1,2320 @@ /* This file is part of the KDE libraries Copyright 2000 Stephan Kulow Copyright 2000-2006 David Faure Copyright 2000 Waldo Bastian This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "copyjob.h" #include "kiocoredebug.h" #include #include "kcoredirlister.h" #include "kfileitem.h" #include "job.h" // buildErrorString #include "mkdirjob.h" #include "listjob.h" #include "statjob.h" #include "deletejob.h" #include "filecopyjob.h" #include "../pathhelpers_p.h" #include #include #include #include "slave.h" #include "scheduler.h" #include "kdirwatch.h" #include "kprotocolmanager.h" #include #include #include #ifdef Q_OS_UNIX #include #endif #include #include #include #include #include #include // mode_t #include #include "job_p.h" #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG) Q_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG, "kf5.kio.core.copyjob", QtWarningMsg) using namespace KIO; //this will update the report dialog with 5 Hz, I think this is fast enough, aleXXX #define REPORT_TIMEOUT 200 #if !defined(NAME_MAX) #if defined(_MAX_FNAME) #define NAME_MAX _MAX_FNAME //For Windows #else #define NAME_MAX 0 #endif #endif enum DestinationState { DEST_NOT_STATED, DEST_IS_DIR, DEST_IS_FILE, DEST_DOESNT_EXIST }; /** * States: * STATE_INITIAL the constructor was called * STATE_STATING for the dest * statCurrentSrc then does, for each src url: * STATE_RENAMING if direct rename looks possible * (on already exists, and user chooses rename, TODO: go to STATE_RENAMING again) * STATE_STATING * and then, if dir -> STATE_LISTING (filling 'd->dirs' and 'd->files') * STATE_CREATING_DIRS (createNextDir, iterating over 'd->dirs') * if conflict: STATE_CONFLICT_CREATING_DIRS * STATE_COPYING_FILES (copyNextFile, iterating over 'd->files') * if conflict: STATE_CONFLICT_COPYING_FILES * STATE_DELETING_DIRS (deleteNextDir) (if moving) * STATE_SETTING_DIR_ATTRIBUTES (setNextDirAttribute, iterating over d->m_directoriesCopied) * done. */ enum CopyJobState { STATE_INITIAL, STATE_STATING, STATE_RENAMING, STATE_LISTING, STATE_CREATING_DIRS, STATE_CONFLICT_CREATING_DIRS, STATE_COPYING_FILES, STATE_CONFLICT_COPYING_FILES, STATE_DELETING_DIRS, STATE_SETTING_DIR_ATTRIBUTES }; static QUrl addPathToUrl(const QUrl &url, const QString &relPath) { QUrl u(url); u.setPath(concatPaths(url.path(), relPath)); return u; } /** @internal */ class KIO::CopyJobPrivate: public KIO::JobPrivate { public: CopyJobPrivate(const QList &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod) : m_globalDest(dest) , m_globalDestinationState(DEST_NOT_STATED) , m_defaultPermissions(false) , m_bURLDirty(false) , m_mode(mode) , m_asMethod(asMethod) , destinationState(DEST_NOT_STATED) , state(STATE_INITIAL) , m_freeSpace(-1) , m_totalSize(0) , m_processedSize(0) , m_fileProcessedSize(0) , m_processedFiles(0) , m_processedDirs(0) , m_srcList(src) , m_currentStatSrc(m_srcList.constBegin()) , m_bCurrentOperationIsLink(false) , m_bSingleFileCopy(false) , m_bOnlyRenames(mode == CopyJob::Move) , m_dest(dest) , m_bAutoRenameFiles(false) , m_bAutoRenameDirs(false) , m_bAutoSkipFiles(false) , m_bAutoSkipDirs(false) , m_bOverwriteAllFiles(false) , m_bOverwriteAllDirs(false) , m_conflictError(0) , m_reportTimer(nullptr) { } // This is the dest URL that was initially given to CopyJob // It is copied into m_dest, which can be changed for a given src URL // (when using the RENAME dialog in slotResult), // and which will be reset for the next src URL. QUrl m_globalDest; // The state info about that global dest DestinationState m_globalDestinationState; // See setDefaultPermissions bool m_defaultPermissions; // Whether URLs changed (and need to be emitted by the next slotReport call) bool m_bURLDirty; // Used after copying all the files into the dirs, to set mtime (TODO: and permissions?) // after the copy is done QLinkedList m_directoriesCopied; QLinkedList::const_iterator m_directoriesCopiedIterator; CopyJob::CopyMode m_mode; bool m_asMethod; DestinationState destinationState; CopyJobState state; KIO::filesize_t m_freeSpace; KIO::filesize_t m_totalSize; KIO::filesize_t m_processedSize; KIO::filesize_t m_fileProcessedSize; int m_processedFiles; int m_processedDirs; QList files; QList dirs; QList dirsToRemove; QList m_srcList; QList m_successSrcList; // Entries in m_srcList that have successfully been moved QList::const_iterator m_currentStatSrc; bool m_bCurrentSrcIsDir; bool m_bCurrentOperationIsLink; bool m_bSingleFileCopy; bool m_bOnlyRenames; QUrl m_dest; QUrl m_currentDest; // set during listing, used by slotEntries // QStringList m_skipList; QSet m_overwriteList; bool m_bAutoRenameFiles; bool m_bAutoRenameDirs; bool m_bAutoSkipFiles; bool m_bAutoSkipDirs; bool m_bOverwriteAllFiles; bool m_bOverwriteAllDirs; int m_conflictError; QTimer *m_reportTimer; // The current src url being stat'ed or copied // During the stat phase, this is initially equal to *m_currentStatSrc but it can be resolved to a local file equivalent (#188903). QUrl m_currentSrcURL; QUrl m_currentDestURL; QSet m_parentDirs; void statCurrentSrc(); void statNextSrc(); // Those aren't slots but submethods for slotResult. void slotResultStating(KJob *job); void startListing(const QUrl &src); void slotResultCreatingDirs(KJob *job); void slotResultConflictCreatingDirs(KJob *job); void createNextDir(); void slotResultCopyingFiles(KJob *job); void slotResultErrorCopyingFiles(KJob *job); // KIO::Job* linkNextFile( const QUrl& uSource, const QUrl& uDest, bool overwrite ); KIO::Job *linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags); void copyNextFile(); void slotResultDeletingDirs(KJob *job); void deleteNextDir(); void sourceStated(const UDSEntry &entry, const QUrl &sourceUrl); void skip(const QUrl &sourceURL, bool isDir); void slotResultRenaming(KJob *job); void slotResultSettingDirAttributes(KJob *job); void setNextDirAttribute(); void startRenameJob(const QUrl &slave_url); bool shouldOverwriteDir(const QString &path) const; bool shouldOverwriteFile(const QString &path) const; bool shouldSkip(const QString &path) const; void skipSrc(bool isDir); void renameDirectory(const QList::iterator &it, const QUrl &newUrl); QUrl finalDestUrl(const QUrl &src, const QUrl &dest) const; void slotStart(); void slotEntries(KIO::Job *, const KIO::UDSEntryList &list); void slotSubError(KIO::ListJob *job, KIO::ListJob *subJob); void addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl ¤tDest); /** * Forward signal from subjob */ void slotProcessedSize(KJob *, qulonglong data_size); /** * Forward signal from subjob * @param size the total size */ void slotTotalSize(KJob *, qulonglong size); void slotReport(); Q_DECLARE_PUBLIC(CopyJob) static inline CopyJob *newJob(const QList &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod, JobFlags flags) { CopyJob *job = new CopyJob(*new CopyJobPrivate(src, dest, mode, asMethod)); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); if (!(flags & HideProgressInfo)) { KIO::getJobTracker()->registerJob(job); } if (flags & KIO::Overwrite) { job->d_func()->m_bOverwriteAllDirs = true; job->d_func()->m_bOverwriteAllFiles = true; } if (!(flags & KIO::NoPrivilegeExecution)) { job->d_func()->m_privilegeExecutionEnabled = true; FileOperationType copyType; switch (mode) { case CopyJob::Copy: copyType = Copy; break; case CopyJob::Move: copyType = Move; break; case CopyJob::Link: copyType = Symlink; break; } job->d_func()->m_operationType = copyType; } return job; } }; CopyJob::CopyJob(CopyJobPrivate &dd) : Job(dd) { Q_D(CopyJob); setProperty("destUrl", d_func()->m_dest.toString()); QTimer::singleShot(0, this, [d]() { d->slotStart(); }); qRegisterMetaType(); } CopyJob::~CopyJob() { } QList CopyJob::srcUrls() const { return d_func()->m_srcList; } QUrl CopyJob::destUrl() const { return d_func()->m_dest; } void CopyJobPrivate::slotStart() { Q_Q(CopyJob); if (q->isSuspended()) { return; } if (m_mode == CopyJob::CopyMode::Move) { Q_FOREACH (const QUrl &url, m_srcList) { if (m_dest.scheme() == url.scheme() && m_dest.host() == url.host()) { QString srcPath = url.path(); if (!srcPath.endsWith(QLatin1Char('/'))) srcPath += QLatin1Char('/'); if (m_dest.path().startsWith(srcPath)) { q->setError(KIO::ERR_CANNOT_MOVE_INTO_ITSELF); q->emitResult(); return; } } } } /** We call the functions directly instead of using signals. Calling a function via a signal takes approx. 65 times the time compared to calling it directly (at least on my machine). aleXXX */ m_reportTimer = new QTimer(q); q->connect(m_reportTimer, &QTimer::timeout, q, [this]() { slotReport(); }); m_reportTimer->start(REPORT_TIMEOUT); // Stat the dest state = STATE_STATING; const QUrl dest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest; KIO::Job *job = KIO::stat(dest, StatJob::DestinationSide, 2, KIO::HideProgressInfo); qCDebug(KIO_COPYJOB_DEBUG) << "CopyJob: stating the dest" << m_dest; q->addSubjob(job); } // For unit test purposes KIOCORE_EXPORT bool kio_resolve_local_urls = true; void CopyJobPrivate::slotResultStating(KJob *job) { Q_Q(CopyJob); qCDebug(KIO_COPYJOB_DEBUG); // Was there an error while stating the src ? if (job->error() && destinationState != DEST_NOT_STATED) { const QUrl srcurl = static_cast(job)->url(); if (!srcurl.isLocalFile()) { // Probably : src doesn't exist. Well, over some protocols (e.g. FTP) // this info isn't really reliable (thanks to MS FTP servers). // We'll assume a file, and try to download anyway. qCDebug(KIO_COPYJOB_DEBUG) << "Error while stating source. Activating hack"; q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... struct CopyInfo info; info.permissions = (mode_t) - 1; info.size = (KIO::filesize_t) - 1; info.uSource = srcurl; info.uDest = m_dest; // Append filename or dirname to destination URL, if allowed if (destinationState == DEST_IS_DIR && !m_asMethod) { const QString fileName = srcurl.scheme() == QLatin1String("data") ? QStringLiteral("data") : srcurl.fileName(); // #379093 info.uDest = addPathToUrl(info.uDest, fileName); } files.append(info); statNextSrc(); return; } // Local file. If stat fails, the file definitely doesn't exist. // yes, q->Job::, because we don't want to call our override q->Job::slotResult(job); // will set the error and emit result(this) return; } // Keep copy of the stat result const UDSEntry entry = static_cast(job)->statResult(); if (destinationState == DEST_NOT_STATED) { if (m_dest.isLocalFile()) { //works for dirs as well QString path(m_dest.toLocalFile()); QFileInfo fileInfo(path); if (m_asMethod || !fileInfo.exists()) { // In copy-as mode, we want to check the directory to which we're // copying. The target file or directory does not exist yet, which // might confuse KDiskFreeSpaceInfo. path = fileInfo.absolutePath(); } KDiskFreeSpaceInfo freeSpaceInfo = KDiskFreeSpaceInfo::freeSpaceInfo(path); if (freeSpaceInfo.isValid()) { m_freeSpace = freeSpaceInfo.available(); } else { qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't determine free space information for" << path; } //TODO actually preliminary check is even more valuable for slow NFS/SMB mounts, //but we need to find a way to report connection errors to user } const bool isGlobalDest = m_dest == m_globalDest; const bool isDir = entry.isDir(); // we were stating the dest if (job->error()) { destinationState = DEST_DOESNT_EXIST; qCDebug(KIO_COPYJOB_DEBUG) << "dest does not exist"; } else { // Treat symlinks to dirs as dirs here, so no test on isLink destinationState = isDir ? DEST_IS_DIR : DEST_IS_FILE; qCDebug(KIO_COPYJOB_DEBUG) << "dest is dir:" << isDir; const QString sLocalPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); if (!sLocalPath.isEmpty() && kio_resolve_local_urls && destinationState != DEST_DOESNT_EXIST) { const QString fileName = m_dest.fileName(); m_dest = QUrl::fromLocalFile(sLocalPath); if (m_asMethod) { m_dest = addPathToUrl(m_dest, fileName); } qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to the local path:" << sLocalPath; if (isGlobalDest) { m_globalDest = m_dest; } } } if (isGlobalDest) { m_globalDestinationState = destinationState; } q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); // After knowing what the dest is, we can start stat'ing the first src. statCurrentSrc(); } else { sourceStated(entry, static_cast(job)->url()); q->removeSubjob(job); } } void CopyJobPrivate::sourceStated(const UDSEntry &entry, const QUrl &sourceUrl) { const QString sLocalPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); const bool isDir = entry.isDir(); // We were stating the current source URL // Is it a file or a dir ? // There 6 cases, and all end up calling addCopyInfoFromUDSEntry first : // 1 - src is a dir, destination is a directory, // slotEntries will append the source-dir-name to the destination // 2 - src is a dir, destination is a file -- will offer to overwrite, later on. // 3 - src is a dir, destination doesn't exist, then it's the destination dirname, // so slotEntries will use it as destination. // 4 - src is a file, destination is a directory, // slotEntries will append the filename to the destination. // 5 - src is a file, destination is a file, m_dest is the exact destination name // 6 - src is a file, destination doesn't exist, m_dest is the exact destination name QUrl srcurl; if (!sLocalPath.isEmpty() && destinationState != DEST_DOESNT_EXIST) { qCDebug(KIO_COPYJOB_DEBUG) << "Using sLocalPath. destinationState=" << destinationState; // Prefer the local path -- but only if we were able to stat() the dest. // Otherwise, renaming a desktop:/ url would copy from src=file to dest=desktop (#218719) srcurl = QUrl::fromLocalFile(sLocalPath); } else { srcurl = sourceUrl; } addCopyInfoFromUDSEntry(entry, srcurl, false, m_dest); m_currentDest = m_dest; m_bCurrentSrcIsDir = false; if (isDir // treat symlinks as files (no recursion) && !entry.isLink() && m_mode != CopyJob::Link) { // No recursion in Link mode either. qCDebug(KIO_COPYJOB_DEBUG) << "Source is a directory"; if (srcurl.isLocalFile()) { const QString parentDir = srcurl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); m_parentDirs.insert(parentDir); } m_bCurrentSrcIsDir = true; // used by slotEntries if (destinationState == DEST_IS_DIR) { // (case 1) if (!m_asMethod) { // Use / as destination, from now on QString directory = srcurl.fileName(); const QString sName = entry.stringValue(KIO::UDSEntry::UDS_NAME); KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(srcurl); if (fnu == KProtocolInfo::Name) { if (!sName.isEmpty()) { directory = sName; } } else if (fnu == KProtocolInfo::DisplayName) { const QString dispName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); if (!dispName.isEmpty()) { directory = dispName; } else if (!sName.isEmpty()) { directory = sName; } } m_currentDest = addPathToUrl(m_currentDest, directory); } } else { // (case 3) // otherwise dest is new name for toplevel dir // so the destination exists, in fact, from now on. // (This even works with other src urls in the list, since the // dir has effectively been created) destinationState = DEST_IS_DIR; if (m_dest == m_globalDest) { m_globalDestinationState = destinationState; } } startListing(srcurl); } else { qCDebug(KIO_COPYJOB_DEBUG) << "Source is a file (or a symlink), or we are linking -> no recursive listing"; if (srcurl.isLocalFile()) { const QString parentDir = srcurl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); m_parentDirs.insert(parentDir); } statNextSrc(); } } bool CopyJob::doSuspend() { Q_D(CopyJob); d->slotReport(); return Job::doSuspend(); } bool CopyJob::doResume() { Q_D(CopyJob); switch (d->state) { case STATE_INITIAL: QTimer::singleShot(0, this, [d]() { d->slotStart(); }); break; default: // not implemented break; } return Job::doResume(); } void CopyJobPrivate::slotReport() { Q_Q(CopyJob); if (q->isSuspended()) { return; } // If showProgressInfo was set, progressId() is > 0. switch (state) { case STATE_RENAMING: q->setTotalAmount(KJob::Files, m_srcList.count()); // fall-through intended Q_FALLTHROUGH(); case STATE_COPYING_FILES: q->setProcessedAmount(KJob::Files, m_processedFiles); q->setProcessedAmount(KJob::Bytes, m_processedSize + m_fileProcessedSize); if (m_bURLDirty) { // Only emit urls when they changed. This saves time, and fixes #66281 m_bURLDirty = false; if (m_mode == CopyJob::Move) { emitMoving(q, m_currentSrcURL, m_currentDestURL); emit q->moving(q, m_currentSrcURL, m_currentDestURL); } else if (m_mode == CopyJob::Link) { emitCopying(q, m_currentSrcURL, m_currentDestURL); // we don't have a delegate->linking emit q->linking(q, m_currentSrcURL.path(), m_currentDestURL); } else { emitCopying(q, m_currentSrcURL, m_currentDestURL); emit q->copying(q, m_currentSrcURL, m_currentDestURL); } } break; case STATE_CREATING_DIRS: q->setProcessedAmount(KJob::Directories, m_processedDirs); if (m_bURLDirty) { m_bURLDirty = false; emit q->creatingDir(q, m_currentDestURL); emitCreatingDir(q, m_currentDestURL); } break; case STATE_STATING: case STATE_LISTING: if (m_bURLDirty) { m_bURLDirty = false; if (m_mode == CopyJob::Move) { emitMoving(q, m_currentSrcURL, m_currentDestURL); } else { emitCopying(q, m_currentSrcURL, m_currentDestURL); } } q->setTotalAmount(KJob::Bytes, m_totalSize); q->setTotalAmount(KJob::Files, files.count()); q->setTotalAmount(KJob::Directories, dirs.count()); break; default: break; } } void CopyJobPrivate::slotEntries(KIO::Job *job, const UDSEntryList &list) { //Q_Q(CopyJob); UDSEntryList::ConstIterator it = list.constBegin(); UDSEntryList::ConstIterator end = list.constEnd(); for (; it != end; ++it) { const UDSEntry &entry = *it; addCopyInfoFromUDSEntry(entry, static_cast(job)->url(), m_bCurrentSrcIsDir, m_currentDest); } } void CopyJobPrivate::slotSubError(ListJob *job, ListJob *subJob) { const QUrl url = subJob->url(); qCWarning(KIO_CORE) << url << subJob->errorString(); Q_Q(CopyJob); emit q->warning(job, subJob->errorString(), QString()); skip(url, true); } void CopyJobPrivate::addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl ¤tDest) { struct CopyInfo info; info.permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1); const auto timeVal = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); if (timeVal != -1) { info.mtime = QDateTime::fromMSecsSinceEpoch(1000 * timeVal, Qt::UTC); } info.ctime = QDateTime::fromMSecsSinceEpoch(1000 * entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), Qt::UTC); info.size = static_cast(entry.numberValue(KIO::UDSEntry::UDS_SIZE, -1)); if (info.size != (KIO::filesize_t) - 1) { m_totalSize += info.size; } // recursive listing, displayName can be a/b/c/d const QString fileName = entry.stringValue(KIO::UDSEntry::UDS_NAME); const QString urlStr = entry.stringValue(KIO::UDSEntry::UDS_URL); QUrl url; if (!urlStr.isEmpty()) { url = QUrl(urlStr); } QString localPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); const bool isDir = entry.isDir(); info.linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); if (fileName != QLatin1String("..") && fileName != QLatin1String(".")) { const bool hasCustomURL = !url.isEmpty() || !localPath.isEmpty(); if (!hasCustomURL) { // Make URL from displayName url = srcUrl; if (srcIsDir) { // Only if src is a directory. Otherwise uSource is fine as is qCDebug(KIO_COPYJOB_DEBUG) << "adding path" << fileName; url = addPathToUrl(url, fileName); } } qCDebug(KIO_COPYJOB_DEBUG) << "fileName=" << fileName << "url=" << url; if (!localPath.isEmpty() && kio_resolve_local_urls && destinationState != DEST_DOESNT_EXIST) { url = QUrl::fromLocalFile(localPath); } info.uSource = url; info.uDest = currentDest; qCDebug(KIO_COPYJOB_DEBUG) << "uSource=" << info.uSource << "uDest(1)=" << info.uDest; // Append filename or dirname to destination URL, if allowed if (destinationState == DEST_IS_DIR && // "copy/move as " means 'foo' is the dest for the base srcurl // (passed here during stating) but not its children (during listing) (!(m_asMethod && state == STATE_STATING))) { QString destFileName; KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(url); if (hasCustomURL && fnu == KProtocolInfo::FromUrl) { //destFileName = url.fileName(); // Doesn't work for recursive listing // Count the number of prefixes used by the recursive listjob int numberOfSlashes = fileName.count(QLatin1Char('/')); // don't make this a find()! QString path = url.path(); int pos = 0; for (int n = 0; n < numberOfSlashes + 1; ++n) { pos = path.lastIndexOf(QLatin1Char('/'), pos - 1); if (pos == -1) { // error qCWarning(KIO_CORE) << "kioslave bug: not enough slashes in UDS_URL" << path << "- looking for" << numberOfSlashes << "slashes"; break; } } if (pos >= 0) { destFileName = path.mid(pos + 1); } } else if (fnu == KProtocolInfo::Name) { // destination filename taken from UDS_NAME destFileName = fileName; } else { // from display name (with fallback to name) const QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); destFileName = displayName.isEmpty() ? fileName : displayName; } // Here we _really_ have to add some filename to the dest. // Otherwise, we end up with e.g. dest=..../Desktop/ itself. // (This can happen when dropping a link to a webpage with no path) if (destFileName.isEmpty()) { destFileName = KIO::encodeFileName(info.uSource.toDisplayString()); } qCDebug(KIO_COPYJOB_DEBUG) << " adding destFileName=" << destFileName; info.uDest = addPathToUrl(info.uDest, destFileName); } qCDebug(KIO_COPYJOB_DEBUG) << " uDest(2)=" << info.uDest; qCDebug(KIO_COPYJOB_DEBUG) << " " << info.uSource << "->" << info.uDest; if (info.linkDest.isEmpty() && isDir && m_mode != CopyJob::Link) { // Dir dirs.append(info); // Directories if (m_mode == CopyJob::Move) { dirsToRemove.append(info.uSource); } } else { files.append(info); // Files and any symlinks } } } // Adjust for kio_trash choosing its own dest url... QUrl CopyJobPrivate::finalDestUrl(const QUrl& src, const QUrl &dest) const { Q_Q(const CopyJob); if (dest.scheme() == QLatin1String("trash")) { const QMap& metaData = q->metaData(); QMap::ConstIterator it = metaData.find(QLatin1String("trashURL-") + src.path()); if (it != metaData.constEnd()) { qCDebug(KIO_COPYJOB_DEBUG) << "finalDestUrl=" << it.value(); return QUrl(it.value()); } } return dest; } void CopyJobPrivate::skipSrc(bool isDir) { m_dest = m_globalDest; destinationState = m_globalDestinationState; skip(*m_currentStatSrc, isDir); ++m_currentStatSrc; statCurrentSrc(); } void CopyJobPrivate::statNextSrc() { /* Revert to the global destination, the one that applies to all source urls. * Imagine you copy the items a b and c into /d, but /d/b exists so the user uses "Rename" to put it in /foo/b instead. * d->m_dest is /foo/b for b, but we have to revert to /d for item c and following. */ m_dest = m_globalDest; qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to" << m_dest; destinationState = m_globalDestinationState; ++m_currentStatSrc; statCurrentSrc(); } void CopyJobPrivate::statCurrentSrc() { Q_Q(CopyJob); if (m_currentStatSrc != m_srcList.constEnd()) { m_currentSrcURL = (*m_currentStatSrc); m_bURLDirty = true; if (m_mode == CopyJob::Link) { // Skip the "stating the source" stage, we don't need it for linking m_currentDest = m_dest; struct CopyInfo info; info.permissions = -1; info.size = (KIO::filesize_t) - 1; info.uSource = m_currentSrcURL; info.uDest = m_currentDest; // Append filename or dirname to destination URL, if allowed if (destinationState == DEST_IS_DIR && !m_asMethod) { if ( (m_currentSrcURL.scheme() == info.uDest.scheme()) && (m_currentSrcURL.host() == info.uDest.host()) && (m_currentSrcURL.port() == info.uDest.port()) && (m_currentSrcURL.userName() == info.uDest.userName()) && (m_currentSrcURL.password() == info.uDest.password())) { // This is the case of creating a real symlink info.uDest = addPathToUrl(info.uDest, m_currentSrcURL.fileName()); } else { // Different protocols, we'll create a .desktop file // We have to change the extension anyway, so while we're at it, // name the file like the URL QByteArray encodedFilename = QFile::encodeName(m_currentSrcURL.toDisplayString()); const int truncatePos = NAME_MAX - (info.uDest.toDisplayString().length() + 8); // length(.desktop) = 8 if (truncatePos > 0) { encodedFilename.truncate(truncatePos); } const QString decodedFilename = QFile::decodeName(encodedFilename); info.uDest = addPathToUrl(info.uDest, KIO::encodeFileName(decodedFilename) + QLatin1String(".desktop")); } } files.append(info); // Files and any symlinks statNextSrc(); // we could use a loop instead of a recursive call :) return; } // Let's see if we can skip stat'ing, for the case where a directory view has the info already KIO::UDSEntry entry; const KFileItem cachedItem = KCoreDirLister::cachedItemForUrl(m_currentSrcURL); if (!cachedItem.isNull()) { entry = cachedItem.entry(); if (destinationState != DEST_DOESNT_EXIST) { // only resolve src if we could resolve dest (#218719) bool dummyIsLocal; m_currentSrcURL = cachedItem.mostLocalUrl(dummyIsLocal); // #183585 } } if (m_mode == CopyJob::Move && ( // Don't go renaming right away if we need a stat() to find out the destination filename KProtocolManager::fileNameUsedForCopying(m_currentSrcURL) == KProtocolInfo::FromUrl || destinationState != DEST_IS_DIR || m_asMethod) ) { // If moving, before going for the full stat+[list+]copy+del thing, try to rename // The logic is pretty similar to FileCopyJobPrivate::slotStart() if ((m_currentSrcURL.scheme() == m_dest.scheme()) && (m_currentSrcURL.host() == m_dest.host()) && (m_currentSrcURL.port() == m_dest.port()) && (m_currentSrcURL.userName() == m_dest.userName()) && (m_currentSrcURL.password() == m_dest.password())) { startRenameJob(m_currentSrcURL); return; } else if (m_currentSrcURL.isLocalFile() && KProtocolManager::canRenameFromFile(m_dest)) { startRenameJob(m_dest); return; } else if (m_dest.isLocalFile() && KProtocolManager::canRenameToFile(m_currentSrcURL)) { startRenameJob(m_currentSrcURL); return; } } // if the file system doesn't support deleting, we do not even stat if (m_mode == CopyJob::Move && !KProtocolManager::supportsDeleting(m_currentSrcURL)) { QPointer that = q; emit q->warning(q, buildErrorString(ERR_CANNOT_DELETE, m_currentSrcURL.toDisplayString())); if (that) { statNextSrc(); // we could use a loop instead of a recursive call :) } return; } m_bOnlyRenames = false; // Testing for entry.count()>0 here is not good enough; KFileItem inserts // entries for UDS_USER and UDS_GROUP even on initially empty UDSEntries (#192185) if (entry.contains(KIO::UDSEntry::UDS_NAME)) { qCDebug(KIO_COPYJOB_DEBUG) << "fast path! found info about" << m_currentSrcURL << "in KCoreDirLister"; // sourceStated(entry, m_currentSrcURL); // don't recurse, see #319747, use queued invokeMethod instead QMetaObject::invokeMethod(q, "sourceStated", Qt::QueuedConnection, Q_ARG(KIO::UDSEntry, entry), Q_ARG(QUrl, m_currentSrcURL)); return; } // Stat the next src url Job *job = KIO::stat(m_currentSrcURL, StatJob::SourceSide, 2, KIO::HideProgressInfo); qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL; state = STATE_STATING; q->addSubjob(job); m_currentDestURL = m_dest; m_bURLDirty = true; } else { // Finished the stat'ing phase // First make sure that the totals were correctly emitted state = STATE_STATING; m_bURLDirty = true; slotReport(); qCDebug(KIO_COPYJOB_DEBUG)<<"Stating finished. To copy:"< m_freeSpace && m_freeSpace != static_cast(-1)) { q->setError(ERR_DISK_FULL); q->setErrorText(m_currentSrcURL.toDisplayString()); q->emitResult(); return; } if (!dirs.isEmpty()) { emit q->aboutToCreate(q, dirs); } if (!files.isEmpty()) { emit q->aboutToCreate(q, files); } // Check if we are copying a single file m_bSingleFileCopy = (files.count() == 1 && dirs.isEmpty()); // Then start copying things state = STATE_CREATING_DIRS; createNextDir(); } } void CopyJobPrivate::startRenameJob(const QUrl &slave_url) { Q_Q(CopyJob); // Silence KDirWatch notifications, otherwise performance is horrible if (m_currentSrcURL.isLocalFile()) { const QString parentDir = m_currentSrcURL.adjusted(QUrl::RemoveFilename).path(); if (!m_parentDirs.contains(parentDir)) { KDirWatch::self()->stopDirScan(parentDir); m_parentDirs.insert(parentDir); } } QUrl dest = m_dest; // Append filename or dirname to destination URL, if allowed if (destinationState == DEST_IS_DIR && !m_asMethod) { dest = addPathToUrl(dest, m_currentSrcURL.fileName()); } m_currentDestURL = dest; qCDebug(KIO_COPYJOB_DEBUG) << m_currentSrcURL << "->" << dest << "trying direct rename first"; state = STATE_RENAMING; struct CopyInfo info; info.permissions = -1; info.size = (KIO::filesize_t) - 1; info.uSource = m_currentSrcURL; info.uDest = dest; QList files; files.append(info); emit q->aboutToCreate(q, files); KIO_ARGS << m_currentSrcURL << dest << (qint8) false /*no overwrite*/; SimpleJob *newJob = SimpleJobPrivate::newJobNoUi(slave_url, CMD_RENAME, packedArgs); newJob->setParentJob(q); Scheduler::setJobPriority(newJob, 1); q->addSubjob(newJob); if (m_currentSrcURL.adjusted(QUrl::RemoveFilename) != dest.adjusted(QUrl::RemoveFilename)) { // For the user, moving isn't renaming. Only renaming is. m_bOnlyRenames = false; } } void CopyJobPrivate::startListing(const QUrl &src) { Q_Q(CopyJob); state = STATE_LISTING; m_bURLDirty = true; ListJob *newjob = listRecursive(src, KIO::HideProgressInfo); newjob->setUnrestricted(true); q->connect(newjob, &ListJob::entries, q, [this](KIO::Job *job, KIO::UDSEntryList list) { slotEntries(job, list); }); q->connect(newjob, &ListJob::subError, q, [this](KIO::ListJob *job, KIO::ListJob *subJob) { slotSubError(job, subJob); }); q->addSubjob(newjob); } void CopyJobPrivate::skip(const QUrl &sourceUrl, bool isDir) { QUrl dir(sourceUrl); if (!isDir) { // Skipping a file: make sure not to delete the parent dir (#208418) dir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); } while (dirsToRemove.removeAll(dir) > 0) { // Do not rely on rmdir() on the parent directories aborting. // Exclude the parent dirs explicitly. dir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); } } bool CopyJobPrivate::shouldOverwriteDir(const QString &path) const { if (m_bOverwriteAllDirs) { return true; } return m_overwriteList.contains(path); } bool CopyJobPrivate::shouldOverwriteFile(const QString &path) const { if (m_bOverwriteAllFiles) { return true; } return m_overwriteList.contains(path); } bool CopyJobPrivate::shouldSkip(const QString &path) const { Q_FOREACH (const QString &skipPath, m_skipList) { if (path.startsWith(skipPath)) { return true; } } return false; } void CopyJobPrivate::renameDirectory(const QList::iterator &it, const QUrl &newUrl) { Q_Q(CopyJob); emit q->renamed(q, (*it).uDest, newUrl); // for e.g. KPropertiesDialog QString oldPath = (*it).uDest.path(); if (!oldPath.endsWith(QLatin1Char('/'))) { oldPath += QLatin1Char('/'); } // Change the current one and strip the trailing '/' (*it).uDest = newUrl.adjusted(QUrl::StripTrailingSlash); QString newPath = newUrl.path(); // With trailing slash if (!newPath.endsWith(QLatin1Char('/'))) { newPath += QLatin1Char('/'); } QList::Iterator renamedirit = it; ++renamedirit; // Change the name of subdirectories inside the directory for (; renamedirit != dirs.end(); ++renamedirit) { QString path = (*renamedirit).uDest.path(); if (path.startsWith(oldPath)) { QString n = path; n.replace(0, oldPath.length(), newPath); /*qDebug() << "dirs list:" << (*renamedirit).uSource.path() << "was going to be" << path << ", changed into" << n;*/ (*renamedirit).uDest.setPath(n, QUrl::DecodedMode); } } // Change filenames inside the directory QList::Iterator renamefileit = files.begin(); for (; renamefileit != files.end(); ++renamefileit) { QString path = (*renamefileit).uDest.path(QUrl::FullyDecoded); if (path.startsWith(oldPath)) { QString n = path; n.replace(0, oldPath.length(), newPath); /*qDebug() << "files list:" << (*renamefileit).uSource.path() << "was going to be" << path << ", changed into" << n;*/ (*renamefileit).uDest.setPath(n, QUrl::DecodedMode); } } if (!dirs.isEmpty()) { emit q->aboutToCreate(q, dirs); } if (!files.isEmpty()) { emit q->aboutToCreate(q, files); } } void CopyJobPrivate::slotResultCreatingDirs(KJob *job) { Q_Q(CopyJob); // The dir we are trying to create: QList::Iterator it = dirs.begin(); // Was there an error creating a dir ? if (job->error()) { m_conflictError = job->error(); if ((m_conflictError == ERR_DIR_ALREADY_EXIST) || (m_conflictError == ERR_FILE_ALREADY_EXIST)) { // can't happen? QUrl oldURL = ((SimpleJob *)job)->url(); // Should we skip automatically ? if (m_bAutoSkipDirs) { // We don't want to copy files in this directory, so we put it on the skip list QString path = oldURL.path(); if (!path.endsWith(QLatin1Char('/'))) { path += QLatin1Char('/'); } m_skipList.append(path); skip(oldURL, true); dirs.erase(it); // Move on to next dir } else { // Did the user choose to overwrite already? const QString destDir = (*it).uDest.path(); if (shouldOverwriteDir(destDir)) { // overwrite => just skip emit q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */); dirs.erase(it); // Move on to next dir } else { if (m_bAutoRenameDirs) { const QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName()); QUrl newUrl(destDirectory); newUrl.setPath(concatPaths(newUrl.path(), newName)); renameDirectory(it, newUrl); } else { if (!q->uiDelegateExtension()) { q->Job::slotResult(job); // will set the error and emit result(this) return; } Q_ASSERT(((SimpleJob *)job)->url() == (*it).uDest); q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... // We need to stat the existing dir, to get its last-modification time QUrl existingDest((*it).uDest); SimpleJob *newJob = KIO::stat(existingDest, StatJob::DestinationSide, 2, KIO::HideProgressInfo); Scheduler::setJobPriority(newJob, 1); qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingDest; state = STATE_CONFLICT_CREATING_DIRS; q->addSubjob(newJob); return; // Don't move to next dir yet ! } } } } else { // Severe error, abort q->Job::slotResult(job); // will set the error and emit result(this) return; } } else { // no error : remove from list, to move on to next dir //this is required for the undo feature emit q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true, false); m_directoriesCopied.append(*it); dirs.erase(it); } m_processedDirs++; //emit processedAmount( this, KJob::Directories, m_processedDirs ); q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... createNextDir(); } void CopyJobPrivate::slotResultConflictCreatingDirs(KJob *job) { Q_Q(CopyJob); // We come here after a conflict has been detected and we've stated the existing dir // The dir we were trying to create: QList::Iterator it = dirs.begin(); const UDSEntry entry = ((KIO::StatJob *)job)->statResult(); QDateTime destmtime, destctime; const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE); const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... // Always multi and skip (since there are files after that) RenameDialog_Options options(RenameDialog_MultipleItems | RenameDialog_Skip | RenameDialog_IsDirectory); // Overwrite only if the existing thing is a dir (no chance with a file) if (m_conflictError == ERR_DIR_ALREADY_EXIST) { if ((*it).uSource == (*it).uDest || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) { options |= RenameDialog_OverwriteItself; } else { options |= RenameDialog_Overwrite; destmtime = QDateTime::fromMSecsSinceEpoch(1000 * entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1), Qt::UTC); destctime = QDateTime::fromMSecsSinceEpoch(1000 * entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), Qt::UTC); } } const QString existingDest = (*it).uDest.path(); QString newPath; if (m_reportTimer) { m_reportTimer->stop(); } RenameDialog_Result r = q->uiDelegateExtension()->askFileRename(q, i18n("Folder Already Exists"), (*it).uSource, (*it).uDest, options, newPath, (*it).size, destsize, (*it).ctime, destctime, (*it).mtime, destmtime); if (m_reportTimer) { m_reportTimer->start(REPORT_TIMEOUT); } switch (r) { case Result_Cancel: q->setError(ERR_USER_CANCELED); q->emitResult(); return; case Result_AutoRename: m_bAutoRenameDirs = true; // fall through case Result_Rename: { QUrl newUrl((*it).uDest); newUrl.setPath(newPath, QUrl::DecodedMode); renameDirectory(it, newUrl); } break; case Result_AutoSkip: m_bAutoSkipDirs = true; // fall through case Result_Skip: m_skipList.append(existingDest); skip((*it).uSource, true); // Move on to next dir dirs.erase(it); m_processedDirs++; break; case Result_Overwrite: m_overwriteList.insert(existingDest); emit q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */); // Move on to next dir dirs.erase(it); m_processedDirs++; break; case Result_OverwriteAll: m_bOverwriteAllDirs = true; emit q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */); // Move on to next dir dirs.erase(it); m_processedDirs++; break; default: Q_ASSERT(0); } state = STATE_CREATING_DIRS; //emit processedAmount( this, KJob::Directories, m_processedDirs ); createNextDir(); } void CopyJobPrivate::createNextDir() { Q_Q(CopyJob); QUrl udir; if (!dirs.isEmpty()) { // Take first dir to create out of list QList::Iterator it = dirs.begin(); // Is this URL on the skip list or the overwrite list ? while (it != dirs.end() && udir.isEmpty()) { const QString dir = (*it).uDest.path(); if (shouldSkip(dir)) { it = dirs.erase(it); } else { udir = (*it).uDest; } } } if (!udir.isEmpty()) { // any dir to create, finally ? // Create the directory - with default permissions so that we can put files into it // TODO : change permissions once all is finished; but for stuff coming from CDROM it sucks... KIO::SimpleJob *newjob = KIO::mkdir(udir, -1); newjob->setParentJob(q); Scheduler::setJobPriority(newjob, 1); if (shouldOverwriteFile(udir.path())) { // if we are overwriting an existing file or symlink newjob->addMetaData(QStringLiteral("overwrite"), QStringLiteral("true")); } m_currentDestURL = udir; m_bURLDirty = true; q->addSubjob(newjob); return; } else { // we have finished creating dirs q->setProcessedAmount(KJob::Directories, m_processedDirs); // make sure final number appears if (m_mode == CopyJob::Move) { // Now we know which dirs hold the files we're going to delete. // To speed things up and prevent double-notification, we disable KDirWatch // on those dirs temporarily (using KDirWatch::self, that's the instanced // used by e.g. kdirlister). for (QSet::const_iterator it = m_parentDirs.constBegin(); it != m_parentDirs.constEnd(); ++it) { KDirWatch::self()->stopDirScan(*it); } } state = STATE_COPYING_FILES; m_processedFiles++; // Ralf wants it to start at 1, not 0 copyNextFile(); } } void CopyJobPrivate::slotResultCopyingFiles(KJob *job) { Q_Q(CopyJob); // The file we were trying to copy: QList::Iterator it = files.begin(); if (job->error()) { // Should we skip automatically ? if (m_bAutoSkipFiles) { skip((*it).uSource, false); m_fileProcessedSize = (*it).size; files.erase(it); // Move on to next file } else { m_conflictError = job->error(); // save for later // Existing dest ? if ((m_conflictError == ERR_FILE_ALREADY_EXIST) || (m_conflictError == ERR_DIR_ALREADY_EXIST) || (m_conflictError == ERR_IDENTICAL_FILES)) { if (m_bAutoRenameFiles) { QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName()); QUrl newDest(destDirectory); newDest.setPath(concatPaths(newDest.path(), newName)); emit q->renamed(q, (*it).uDest, newDest); // for e.g. kpropsdlg (*it).uDest = newDest; QList files; files.append(*it); emit q->aboutToCreate(q, files); } else { if (!q->uiDelegateExtension()) { q->Job::slotResult(job); // will set the error and emit result(this) return; } q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); // We need to stat the existing file, to get its last-modification time QUrl existingFile((*it).uDest); SimpleJob *newJob = KIO::stat(existingFile, StatJob::DestinationSide, 2, KIO::HideProgressInfo); Scheduler::setJobPriority(newJob, 1); qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingFile; state = STATE_CONFLICT_COPYING_FILES; q->addSubjob(newJob); return; // Don't move to next file yet ! } } else { if (m_bCurrentOperationIsLink && qobject_cast(job)) { // Very special case, see a few lines below // We are deleting the source of a symlink we successfully moved... ignore error m_fileProcessedSize = (*it).size; files.erase(it); } else { if (!q->uiDelegateExtension()) { q->Job::slotResult(job); // will set the error and emit result(this) return; } // Go directly to the conflict resolution, there is nothing to stat slotResultErrorCopyingFiles(job); return; } } } } else { // no error // Special case for moving links. That operation needs two jobs, unlike others. if (m_bCurrentOperationIsLink && m_mode == CopyJob::Move && !qobject_cast(job) // Deleting source not already done ) { q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); // The only problem with this trick is that the error handling for this del operation // is not going to be right... see 'Very special case' above. KIO::Job *newjob = KIO::del((*it).uSource, HideProgressInfo); newjob->setParentJob(q); q->addSubjob(newjob); return; // Don't move to next file yet ! } const QUrl finalUrl = finalDestUrl((*it).uSource, (*it).uDest); if (m_bCurrentOperationIsLink) { QString target = (m_mode == CopyJob::Link ? (*it).uSource.path() : (*it).linkDest); //required for the undo feature emit q->copyingLinkDone(q, (*it).uSource, target, finalUrl); } else { //required for the undo feature emit q->copyingDone(q, (*it).uSource, finalUrl, (*it).mtime, false, false); if (m_mode == CopyJob::Move) { org::kde::KDirNotify::emitFileMoved((*it).uSource, finalUrl); } m_successSrcList.append((*it).uSource); if (m_freeSpace != (KIO::filesize_t) - 1 && (*it).size != (KIO::filesize_t) - 1) { m_freeSpace -= (*it).size; } } // remove from list, to move on to next file files.erase(it); } m_processedFiles++; // clear processed size for last file and add it to overall processed size m_processedSize += m_fileProcessedSize; m_fileProcessedSize = 0; qCDebug(KIO_COPYJOB_DEBUG) << files.count() << "files remaining"; // Merge metadata from subjob KIO::Job *kiojob = qobject_cast(job); Q_ASSERT(kiojob); m_incomingMetaData += kiojob->metaData(); q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... copyNextFile(); } void CopyJobPrivate::slotResultErrorCopyingFiles(KJob *job) { Q_Q(CopyJob); // We come here after a conflict has been detected and we've stated the existing file // The file we were trying to create: QList::Iterator it = files.begin(); RenameDialog_Result res; QString newPath; if (m_reportTimer) { m_reportTimer->stop(); } if ((m_conflictError == ERR_FILE_ALREADY_EXIST) || (m_conflictError == ERR_DIR_ALREADY_EXIST) || (m_conflictError == ERR_IDENTICAL_FILES)) { // Its modification time: const UDSEntry entry = static_cast(job)->statResult(); QDateTime destmtime, destctime; const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE); const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); // Offer overwrite only if the existing thing is a file // If src==dest, use "overwrite-itself" RenameDialog_Options options; bool isDir = true; if (m_conflictError == ERR_DIR_ALREADY_EXIST) { options = RenameDialog_IsDirectory; } else { if ((*it).uSource == (*it).uDest || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) { options = RenameDialog_OverwriteItself; } else { options = RenameDialog_Overwrite; // These timestamps are used only when RenameDialog_Overwrite is set. destmtime = QDateTime::fromMSecsSinceEpoch(1000 * entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1), Qt::UTC); destctime = QDateTime::fromMSecsSinceEpoch(1000 * entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), Qt::UTC); } isDir = false; } if (!m_bSingleFileCopy) { options = RenameDialog_Options(options | RenameDialog_MultipleItems | RenameDialog_Skip); } res = q->uiDelegateExtension()->askFileRename(q, !isDir ? i18n("File Already Exists") : i18n("Already Exists as Folder"), (*it).uSource, (*it).uDest, options, newPath, (*it).size, destsize, (*it).ctime, destctime, (*it).mtime, destmtime); } else { if (job->error() == ERR_USER_CANCELED) { res = Result_Cancel; } else if (!q->uiDelegateExtension()) { q->Job::slotResult(job); // will set the error and emit result(this) return; } else { SkipDialog_Options options; if (files.count() > 1) { options |= SkipDialog_MultipleItems; } res = q->uiDelegateExtension()->askSkip(q, options, job->errorString()); } } if (m_reportTimer) { m_reportTimer->start(REPORT_TIMEOUT); } q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); switch (res) { case Result_Cancel: q->setError(ERR_USER_CANCELED); q->emitResult(); return; case Result_AutoRename: m_bAutoRenameFiles = true; // fall through Q_FALLTHROUGH(); case Result_Rename: { QUrl newUrl((*it).uDest); newUrl.setPath(newPath); emit q->renamed(q, (*it).uDest, newUrl); // for e.g. kpropsdlg (*it).uDest = newUrl; m_bURLDirty = true; QList files; files.append(*it); emit q->aboutToCreate(q, files); } break; case Result_AutoSkip: m_bAutoSkipFiles = true; // fall through Q_FALLTHROUGH(); case Result_Skip: // Move on to next file skip((*it).uSource, false); m_processedSize += (*it).size; files.erase(it); m_processedFiles++; break; case Result_OverwriteAll: m_bOverwriteAllFiles = true; break; case Result_Overwrite: // Add to overwrite list, so that copyNextFile knows to overwrite m_overwriteList.insert((*it).uDest.path()); break; case Result_Retry: // Do nothing, copy file again break; default: Q_ASSERT(0); } state = STATE_COPYING_FILES; copyNextFile(); } KIO::Job *CopyJobPrivate::linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags) { qCDebug(KIO_COPYJOB_DEBUG) << "Linking"; if ( (uSource.scheme() == uDest.scheme()) && (uSource.host() == uDest.host()) && (uSource.port() == uDest.port()) && (uSource.userName() == uDest.userName()) && (uSource.password() == uDest.password())) { // This is the case of creating a real symlink KIO::SimpleJob *newJob = KIO::symlink(uSource.path(), uDest, flags | HideProgressInfo /*no GUI*/); newJob->setParentJob(q_func()); Scheduler::setJobPriority(newJob, 1); qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << uSource.path() << "link=" << uDest; //emit linking( this, uSource.path(), uDest ); m_bCurrentOperationIsLink = true; m_currentSrcURL = uSource; m_currentDestURL = uDest; m_bURLDirty = true; //Observer::self()->slotCopying( this, uSource, uDest ); // should be slotLinking perhaps return newJob; } else { Q_Q(CopyJob); qCDebug(KIO_COPYJOB_DEBUG) << "Linking URL=" << uSource << "link=" << uDest; if (uDest.isLocalFile()) { // if the source is a devices url, handle it a littlebit special QString path = uDest.toLocalFile(); qCDebug(KIO_COPYJOB_DEBUG) << "path=" << path; QFile f(path); if (f.open(QIODevice::ReadWrite)) { f.close(); KDesktopFile desktopFile(path); KConfigGroup config = desktopFile.desktopGroup(); QUrl url = uSource; url.setPassword(QString()); config.writePathEntry("URL", url.toString()); config.writeEntry("Name", url.toString()); config.writeEntry("Type", QStringLiteral("Link")); QString protocol = uSource.scheme(); if (protocol == QLatin1String("ftp")) { config.writeEntry("Icon", QStringLiteral("folder-remote")); } else if (protocol == QLatin1String("http")) { config.writeEntry("Icon", QStringLiteral("text-html")); } else if (protocol == QLatin1String("info")) { config.writeEntry("Icon", QStringLiteral("text-x-texinfo")); } else if (protocol == QLatin1String("mailto")) { // sven: config.writeEntry("Icon", QStringLiteral("internet-mail")); // added mailto: support } else if (protocol == QLatin1String("trash") && url.path().length() <= 1) { // trash:/ link config.writeEntry("Name", i18n("Trash")); config.writeEntry("Icon", QStringLiteral("user-trash-full")); config.writeEntry("EmptyIcon", QStringLiteral("user-trash")); } else { config.writeEntry("Icon", QStringLiteral("unknown")); } config.sync(); files.erase(files.begin()); // done with this one, move on m_processedFiles++; //emit processedAmount( this, KJob::Files, m_processedFiles ); copyNextFile(); return nullptr; } else { qCDebug(KIO_COPYJOB_DEBUG) << "ERR_CANNOT_OPEN_FOR_WRITING"; q->setError(ERR_CANNOT_OPEN_FOR_WRITING); q->setErrorText(uDest.toLocalFile()); q->emitResult(); return nullptr; } } else { // Todo: not show "link" on remote dirs if the src urls are not from the same protocol+host+... q->setError(ERR_CANNOT_SYMLINK); q->setErrorText(uDest.toDisplayString()); q->emitResult(); return nullptr; } } } void CopyJobPrivate::copyNextFile() { Q_Q(CopyJob); bool bCopyFile = false; qCDebug(KIO_COPYJOB_DEBUG); // Take the first file in the list QList::Iterator it = files.begin(); // Is this URL on the skip list ? while (it != files.end() && !bCopyFile) { const QString destFile = (*it).uDest.path(); bCopyFile = !shouldSkip(destFile); if (!bCopyFile) { it = files.erase(it); } if (it != files.end() && (*it).size > ((1ul << 32) - 1)) { // ((1ul << 32) - 1) = 4 GB const auto fileSystem = KFileSystemType::fileSystemType(m_globalDest.toLocalFile()); if (fileSystem == KFileSystemType::Fat) { q->setError(ERR_FILE_TOO_LARGE_FOR_FAT32); q->setErrorText((*it).uDest.toDisplayString()); q->emitResult(); return; } } } if (bCopyFile) { // any file to create, finally ? qCDebug(KIO_COPYJOB_DEBUG)<<"preparing to copy"<<(*it).uSource<<(*it).size<setError(ERR_DISK_FULL); q->emitResult(); return; } } const QUrl &uSource = (*it).uSource; const QUrl &uDest = (*it).uDest; // Do we set overwrite ? bool bOverwrite; const QString destFile = uDest.path(); qCDebug(KIO_COPYJOB_DEBUG) << "copying" << destFile; if (uDest == uSource) { bOverwrite = false; } else { bOverwrite = shouldOverwriteFile(destFile); } // If source isn't local and target is local, we ignore the original permissions // Otherwise, files downloaded from HTTP end up with -r--r--r-- const bool remoteSource = !KProtocolManager::supportsListing(uSource) || uSource.scheme() == QLatin1String("trash"); int permissions = (*it).permissions; if (m_defaultPermissions || (remoteSource && uDest.isLocalFile())) { permissions = -1; } const JobFlags flags = bOverwrite ? Overwrite : DefaultFlags; m_bCurrentOperationIsLink = false; KIO::Job *newjob = nullptr; if (m_mode == CopyJob::Link) { // User requested that a symlink be made newjob = linkNextFile(uSource, uDest, flags); if (!newjob) { return; } } else if (!(*it).linkDest.isEmpty() && (uSource.scheme() == uDest.scheme()) && (uSource.host() == uDest.host()) && (uSource.port() == uDest.port()) && (uSource.userName() == uDest.userName()) && (uSource.password() == uDest.password())) // Copying a symlink - only on the same protocol/host/etc. (#5601, downloading an FTP file through its link), { KIO::SimpleJob *newJob = KIO::symlink((*it).linkDest, uDest, flags | HideProgressInfo /*no GUI*/); newJob->setParentJob(q); Scheduler::setJobPriority(newJob, 1); newjob = newJob; qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << (*it).linkDest << "link=" << uDest; m_currentSrcURL = QUrl::fromUserInput((*it).linkDest); m_currentDestURL = uDest; m_bURLDirty = true; //emit linking( this, (*it).linkDest, uDest ); //Observer::self()->slotCopying( this, m_currentSrcURL, uDest ); // should be slotLinking perhaps m_bCurrentOperationIsLink = true; // NOTE: if we are moving stuff, the deletion of the source will be done in slotResultCopyingFiles } else if (m_mode == CopyJob::Move) { // Moving a file KIO::FileCopyJob *moveJob = KIO::file_move(uSource, uDest, permissions, flags | HideProgressInfo/*no GUI*/); moveJob->setParentJob(q); moveJob->setSourceSize((*it).size); moveJob->setModificationTime((*it).mtime); // #55804 newjob = moveJob; qCDebug(KIO_COPYJOB_DEBUG) << "Moving" << uSource << "to" << uDest; //emit moving( this, uSource, uDest ); m_currentSrcURL = uSource; m_currentDestURL = uDest; m_bURLDirty = true; //Observer::self()->slotMoving( this, uSource, uDest ); } else { // Copying a file KIO::FileCopyJob *copyJob = KIO::file_copy(uSource, uDest, permissions, flags | HideProgressInfo/*no GUI*/); copyJob->setParentJob(q); // in case of rename dialog copyJob->setSourceSize((*it).size); copyJob->setModificationTime((*it).mtime); newjob = copyJob; qCDebug(KIO_COPYJOB_DEBUG) << "Copying" << uSource << "to" << uDest; m_currentSrcURL = uSource; m_currentDestURL = uDest; m_bURLDirty = true; } q->addSubjob(newjob); q->connect(newjob, &Job::processedSize, q, [this](KJob *job, qulonglong processedSize) { slotProcessedSize(job, processedSize); }); q->connect(newjob, &Job::totalSize, q, [this](KJob *job, qulonglong totalSize) { slotTotalSize(job, totalSize); }); } else { // We're done qCDebug(KIO_COPYJOB_DEBUG) << "copyNextFile finished"; --m_processedFiles; // undo the "start at 1" hack slotReport(); // display final numbers, important if progress dialog stays up deleteNextDir(); } } void CopyJobPrivate::deleteNextDir() { Q_Q(CopyJob); if (m_mode == CopyJob::Move && !dirsToRemove.isEmpty()) { // some dirs to delete ? state = STATE_DELETING_DIRS; m_bURLDirty = true; // Take first dir to delete out of list - last ones first ! QList::Iterator it = --dirsToRemove.end(); SimpleJob *job = KIO::rmdir(*it); job->setParentJob(q); Scheduler::setJobPriority(job, 1); dirsToRemove.erase(it); q->addSubjob(job); } else { // This step is done, move on state = STATE_SETTING_DIR_ATTRIBUTES; m_directoriesCopiedIterator = m_directoriesCopied.constBegin(); setNextDirAttribute(); } } void CopyJobPrivate::setNextDirAttribute() { Q_Q(CopyJob); while (m_directoriesCopiedIterator != m_directoriesCopied.constEnd() && !(*m_directoriesCopiedIterator).mtime.isValid()) { ++m_directoriesCopiedIterator; } if (m_directoriesCopiedIterator != m_directoriesCopied.constEnd()) { const QUrl url = (*m_directoriesCopiedIterator).uDest; const QDateTime dt = (*m_directoriesCopiedIterator).mtime; ++m_directoriesCopiedIterator; KIO::SimpleJob *job = KIO::setModificationTime(url, dt); job->setParentJob(q); Scheduler::setJobPriority(job, 1); q->addSubjob(job); #if 0 // ifdef Q_OS_UNIX // TODO: can be removed now. Or reintroduced as a fast path for local files // if launching even more jobs as done above is a performance problem. // QLinkedList::const_iterator it = m_directoriesCopied.constBegin(); for (; it != m_directoriesCopied.constEnd(); ++it) { const QUrl &url = (*it).uDest; if (url.isLocalFile() && (*it).mtime != (time_t) - 1) { QT_STATBUF statbuf; if (QT_LSTAT(url.path(), &statbuf) == 0) { struct utimbuf utbuf; utbuf.actime = statbuf.st_atime; // access time, unchanged utbuf.modtime = (*it).mtime; // modification time utime(path, &utbuf); } } } m_directoriesCopied.clear(); // but then we need to jump to the else part below. Maybe with a recursive call? #endif } else { if (m_reportTimer) { m_reportTimer->stop(); } q->emitResult(); } } void CopyJob::emitResult() { Q_D(CopyJob); // Before we go, tell the world about the changes that were made. // Even if some error made us abort midway, we might still have done // part of the job so we better update the views! (#118583) if (!d->m_bOnlyRenames) { // If only renaming happened, KDirNotify::FileRenamed was emitted by the rename jobs QUrl url(d->m_globalDest); if (d->m_globalDestinationState != DEST_IS_DIR || d->m_asMethod) { url = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); } qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesAdded" << url; org::kde::KDirNotify::emitFilesAdded(url); if (d->m_mode == CopyJob::Move && !d->m_successSrcList.isEmpty()) { qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesRemoved" << d->m_successSrcList; org::kde::KDirNotify::emitFilesRemoved(d->m_successSrcList); } } // Re-enable watching on the dirs that held the deleted/moved files if (d->m_mode == CopyJob::Move) { for (QSet::const_iterator it = d->m_parentDirs.constBegin(); it != d->m_parentDirs.constEnd(); ++it) { KDirWatch::self()->restartDirScan(*it); } } Job::emitResult(); } void CopyJobPrivate::slotProcessedSize(KJob *, qulonglong data_size) { Q_Q(CopyJob); qCDebug(KIO_COPYJOB_DEBUG) << data_size; m_fileProcessedSize = data_size; if (m_processedSize + m_fileProcessedSize > m_totalSize) { // Example: download any attachment from bugs.kde.org m_totalSize = m_processedSize + m_fileProcessedSize; qCDebug(KIO_COPYJOB_DEBUG) << "Adjusting m_totalSize to" << m_totalSize; q->setTotalAmount(KJob::Bytes, m_totalSize); // safety } qCDebug(KIO_COPYJOB_DEBUG) << "emit processedSize" << (unsigned long) (m_processedSize + m_fileProcessedSize); } void CopyJobPrivate::slotTotalSize(KJob *, qulonglong size) { Q_Q(CopyJob); qCDebug(KIO_COPYJOB_DEBUG) << size; // Special case for copying a single file // This is because some protocols don't implement stat properly // (e.g. HTTP), and don't give us a size in some cases (redirection) // so we'd rather rely on the size given for the transfer if (m_bSingleFileCopy && size != m_totalSize) { qCDebug(KIO_COPYJOB_DEBUG) << "slotTotalSize: updating totalsize to" << size; m_totalSize = size; q->setTotalAmount(KJob::Bytes, size); } } void CopyJobPrivate::slotResultDeletingDirs(KJob *job) { Q_Q(CopyJob); if (job->error()) { // Couldn't remove directory. Well, perhaps it's not empty // because the user pressed Skip for a given file in it. // Let's not display "Could not remove dir ..." for each of those dir ! } else { m_successSrcList.append(static_cast(job)->url()); } q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); deleteNextDir(); } void CopyJobPrivate::slotResultSettingDirAttributes(KJob *job) { Q_Q(CopyJob); if (job->error()) { // Couldn't set directory attributes. Ignore the error, it can happen // with inferior file systems like VFAT. // Let's not display warnings for each dir like "cp -a" does. } q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); setNextDirAttribute(); } // We were trying to do a direct renaming, before even stat'ing void CopyJobPrivate::slotResultRenaming(KJob *job) { Q_Q(CopyJob); int err = job->error(); const QString errText = job->errorText(); // Merge metadata from subjob KIO::Job *kiojob = qobject_cast(job); Q_ASSERT(kiojob); m_incomingMetaData += kiojob->metaData(); q->removeSubjob(job); Q_ASSERT(!q->hasSubjobs()); // Determine dest again QUrl dest = m_dest; if (destinationState == DEST_IS_DIR && !m_asMethod) { dest = addPathToUrl(dest, m_currentSrcURL.fileName()); } if (err) { // Direct renaming didn't work. Try renaming to a temp name, // this can help e.g. when renaming 'a' to 'A' on a VFAT partition. // In that case it's the _same_ dir, we don't want to copy+del (data loss!) // TODO: replace all this code with QFile::rename once // https://codereview.qt-project.org/44823 is in if ((err == ERR_FILE_ALREADY_EXIST || err == ERR_DIR_ALREADY_EXIST || err == ERR_IDENTICAL_FILES) && m_currentSrcURL.isLocalFile() && dest.isLocalFile()) { const QString _src(m_currentSrcURL.adjusted(QUrl::StripTrailingSlash).toLocalFile()); const QString _dest(dest.adjusted(QUrl::StripTrailingSlash).toLocalFile()); if (_src != _dest && QString::compare(_src, _dest, Qt::CaseInsensitive) == 0) { qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename directly, dest already exists. Detected special case of lower/uppercase renaming in same dir, try with 2 rename calls"; const QString srcDir = QFileInfo(_src).absolutePath(); QTemporaryFile tmpFile(srcDir + QLatin1String("/kio_XXXXXX")); const bool openOk = tmpFile.open(); if (!openOk) { qCWarning(KIO_CORE) << "Couldn't open temp file in" << srcDir; } else { const QString _tmp(tmpFile.fileName()); tmpFile.close(); tmpFile.remove(); qCDebug(KIO_COPYJOB_DEBUG) << "QTemporaryFile using" << _tmp << "as intermediary"; if (QFile::rename(_src, _tmp)) { qCDebug(KIO_COPYJOB_DEBUG) << "Renaming" << _src << "to" << _tmp << "succeeded"; if (!QFile::exists(_dest) && QFile::rename(_tmp, _dest)) { err = 0; org::kde::KDirNotify::emitFileRenamed(m_currentSrcURL, dest); } else { qCDebug(KIO_COPYJOB_DEBUG) << "Didn't manage to rename" << _tmp << "to" << _dest << ", reverting"; // Revert back to original name! if (!QFile::rename(_tmp, _src)) { qCWarning(KIO_CORE) << "Couldn't rename" << _tmp << "back to" << _src << '!'; // Severe error, abort q->Job::slotResult(job); // will set the error and emit result(this) return; } } } else { qCDebug(KIO_COPYJOB_DEBUG) << "mv" << _src << _tmp << "failed:" << strerror(errno); } } } } } if (err) { // This code is similar to CopyJobPrivate::slotResultErrorCopyingFiles // but here it's about the base src url being moved/renamed // (m_currentSrcURL) and its dest (m_dest), not about a single file. // It also means we already stated the dest, here. // On the other hand we haven't stated the src yet (we skipped doing it // to save time, since it's not necessary to rename directly!)... // Existing dest? if (err == ERR_DIR_ALREADY_EXIST || err == ERR_FILE_ALREADY_EXIST || err == ERR_IDENTICAL_FILES) { // Should we skip automatically ? bool isDir = (err == ERR_DIR_ALREADY_EXIST); // ## technically, isDir means "source is dir", not "dest is dir" ####### if ((isDir && m_bAutoSkipDirs) || (!isDir && m_bAutoSkipFiles)) { // Move on to next source url skipSrc(isDir); return; } else if ((isDir && m_bOverwriteAllDirs) || (!isDir && m_bOverwriteAllFiles)) { ; // nothing to do, stat+copy+del will overwrite } else if ((isDir && m_bAutoRenameDirs) || (!isDir && m_bAutoRenameFiles)) { QUrl destDirectory = m_currentDestURL.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); // m_currendDestURL includes filename const QString newName = KFileUtils::suggestName(destDirectory, m_currentDestURL.fileName()); m_dest = destDirectory; m_dest.setPath(concatPaths(m_dest.path(), newName)); KIO::Job *job = KIO::stat(m_dest, StatJob::DestinationSide, 2, KIO::HideProgressInfo); state = STATE_STATING; destinationState = DEST_NOT_STATED; q->addSubjob(job); return; } else if (q->uiDelegateExtension()) { QString newPath; // we lack mtime info for both the src (not stated) // and the dest (stated but this info wasn't stored) // Let's do it for local files, at least KIO::filesize_t sizeSrc = (KIO::filesize_t) - 1; KIO::filesize_t sizeDest = (KIO::filesize_t) - 1; QDateTime ctimeSrc; QDateTime ctimeDest; QDateTime mtimeSrc; QDateTime mtimeDest; bool destIsDir = err == ERR_DIR_ALREADY_EXIST; // ## TODO we need to stat the source using KIO::stat // so that this code is properly network-transparent. if (m_currentSrcURL.isLocalFile()) { QFileInfo info(m_currentSrcURL.toLocalFile()); if (info.exists()) { sizeSrc = info.size(); - ctimeSrc = info.created(); + ctimeSrc = info.birthTime(); mtimeSrc = info.lastModified(); isDir = info.isDir(); } } if (dest.isLocalFile()) { QFileInfo destInfo(dest.toLocalFile()); if (destInfo.exists()) { sizeDest = destInfo.size(); - ctimeDest = destInfo.created(); + ctimeDest = destInfo.birthTime(); mtimeDest = destInfo.lastModified(); destIsDir = destInfo.isDir(); } } // If src==dest, use "overwrite-itself" RenameDialog_Options options = (m_currentSrcURL == dest) ? RenameDialog_OverwriteItself : RenameDialog_Overwrite; if (!isDir && destIsDir) { // We can't overwrite a dir with a file. options = RenameDialog_Options(); } if (m_srcList.count() > 1) { options |= RenameDialog_Options(RenameDialog_MultipleItems | RenameDialog_Skip); } if (destIsDir) { options |= RenameDialog_IsDirectory; } if (m_reportTimer) { m_reportTimer->stop(); } RenameDialog_Result r = q->uiDelegateExtension()->askFileRename( q, err != ERR_DIR_ALREADY_EXIST ? i18n("File Already Exists") : i18n("Already Exists as Folder"), m_currentSrcURL, dest, options, newPath, sizeSrc, sizeDest, ctimeSrc, ctimeDest, mtimeSrc, mtimeDest); if (m_reportTimer) { m_reportTimer->start(REPORT_TIMEOUT); } switch (r) { case Result_Cancel: { q->setError(ERR_USER_CANCELED); q->emitResult(); return; } case Result_AutoRename: if (isDir) { m_bAutoRenameDirs = true; } else { m_bAutoRenameFiles = true; } // fall through Q_FALLTHROUGH(); case Result_Rename: { // Set m_dest to the chosen destination // This is only for this src url; the next one will revert to m_globalDest m_dest.setPath(newPath); KIO::Job *job = KIO::stat(m_dest, StatJob::DestinationSide, 2, KIO::HideProgressInfo); state = STATE_STATING; destinationState = DEST_NOT_STATED; q->addSubjob(job); return; } case Result_AutoSkip: if (isDir) { m_bAutoSkipDirs = true; } else { m_bAutoSkipFiles = true; } // fall through Q_FALLTHROUGH(); case Result_Skip: // Move on to next url skipSrc(isDir); return; case Result_OverwriteAll: if (destIsDir) { m_bOverwriteAllDirs = true; } else { m_bOverwriteAllFiles = true; } break; case Result_Overwrite: // Add to overwrite list // Note that we add dest, not m_dest. // This ensures that when moving several urls into a dir (m_dest), // we only overwrite for the current one, not for all. // When renaming a single file (m_asMethod), it makes no difference. qCDebug(KIO_COPYJOB_DEBUG) << "adding to overwrite list: " << dest.path(); m_overwriteList.insert(dest.path()); break; default: //Q_ASSERT( 0 ); break; } } else if (err != KIO::ERR_UNSUPPORTED_ACTION) { // Dest already exists, and job is not interactive -> abort with error q->setError(err); q->setErrorText(errText); q->emitResult(); return; } } else if (err != KIO::ERR_UNSUPPORTED_ACTION) { qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", aborting"; q->setError(err); q->setErrorText(errText); q->emitResult(); return; } qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", reverting to normal way, starting with stat"; qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL; KIO::Job *job = KIO::stat(m_currentSrcURL, StatJob::SourceSide, 2, KIO::HideProgressInfo); state = STATE_STATING; q->addSubjob(job); m_bOnlyRenames = false; } else { qCDebug(KIO_COPYJOB_DEBUG) << "Renaming succeeded, move on"; ++m_processedFiles; // Emit copyingDone for FileUndoManager to remember what we did. // Use resolved URL m_currentSrcURL since that's what we just used for renaming. See bug 391606 and kio_desktop's testTrashAndUndo(). emit q->copyingDone(q, m_currentSrcURL, finalDestUrl(m_currentSrcURL, dest), QDateTime() /*mtime unknown, and not needed*/, m_bCurrentSrcIsDir, true); m_successSrcList.append(*m_currentStatSrc); statNextSrc(); } } void CopyJob::slotResult(KJob *job) { Q_D(CopyJob); qCDebug(KIO_COPYJOB_DEBUG) << "d->state=" << (int) d->state; // In each case, what we have to do is : // 1 - check for errors and treat them // 2 - removeSubjob(job); // 3 - decide what to do next switch (d->state) { case STATE_STATING: // We were trying to stat a src url or the dest d->slotResultStating(job); break; case STATE_RENAMING: { // We were trying to do a direct renaming, before even stat'ing d->slotResultRenaming(job); break; } case STATE_LISTING: // recursive listing finished qCDebug(KIO_COPYJOB_DEBUG) << "totalSize:" << (unsigned int) d->m_totalSize << "files:" << d->files.count() << "d->dirs:" << d->dirs.count(); // Was there an error ? if (job->error()) { Job::slotResult(job); // will set the error and emit result(this) return; } removeSubjob(job); Q_ASSERT(!hasSubjobs()); d->statNextSrc(); break; case STATE_CREATING_DIRS: d->slotResultCreatingDirs(job); break; case STATE_CONFLICT_CREATING_DIRS: d->slotResultConflictCreatingDirs(job); break; case STATE_COPYING_FILES: d->slotResultCopyingFiles(job); break; case STATE_CONFLICT_COPYING_FILES: d->slotResultErrorCopyingFiles(job); break; case STATE_DELETING_DIRS: d->slotResultDeletingDirs(job); break; case STATE_SETTING_DIR_ATTRIBUTES: d->slotResultSettingDirAttributes(job); break; default: Q_ASSERT(0); } } void KIO::CopyJob::setDefaultPermissions(bool b) { d_func()->m_defaultPermissions = b; } KIO::CopyJob::CopyMode KIO::CopyJob::operationMode() const { return d_func()->m_mode; } void KIO::CopyJob::setAutoSkip(bool autoSkip) { d_func()->m_bAutoSkipFiles = autoSkip; d_func()->m_bAutoSkipDirs = autoSkip; } void KIO::CopyJob::setAutoRename(bool autoRename) { d_func()->m_bAutoRenameFiles = autoRename; d_func()->m_bAutoRenameDirs = autoRename; } void KIO::CopyJob::setWriteIntoExistingDirectories(bool overwriteAll) // #65926 { d_func()->m_bOverwriteAllDirs = overwriteAll; } CopyJob *KIO::copy(const QUrl &src, const QUrl &dest, JobFlags flags) { qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest; QList srcList; srcList.append(src); return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, false, flags); } CopyJob *KIO::copyAs(const QUrl &src, const QUrl &dest, JobFlags flags) { qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest; QList srcList; srcList.append(src); return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, true, flags); } CopyJob *KIO::copy(const QList &src, const QUrl &dest, JobFlags flags) { qCDebug(KIO_COPYJOB_DEBUG) << src << dest; return CopyJobPrivate::newJob(src, dest, CopyJob::Copy, false, flags); } CopyJob *KIO::move(const QUrl &src, const QUrl &dest, JobFlags flags) { qCDebug(KIO_COPYJOB_DEBUG) << src << dest; QList srcList; srcList.append(src); CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, false, flags); if (job->uiDelegateExtension()) { job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); } return job; } CopyJob *KIO::moveAs(const QUrl &src, const QUrl &dest, JobFlags flags) { qCDebug(KIO_COPYJOB_DEBUG) << src << dest; QList srcList; srcList.append(src); CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, true, flags); if (job->uiDelegateExtension()) { job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); } return job; } CopyJob *KIO::move(const QList &src, const QUrl &dest, JobFlags flags) { qCDebug(KIO_COPYJOB_DEBUG) << src << dest; CopyJob *job = CopyJobPrivate::newJob(src, dest, CopyJob::Move, false, flags); if (job->uiDelegateExtension()) { job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); } return job; } CopyJob *KIO::link(const QUrl &src, const QUrl &destDir, JobFlags flags) { QList srcList; srcList.append(src); return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags); } CopyJob *KIO::link(const QList &srcList, const QUrl &destDir, JobFlags flags) { return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags); } CopyJob *KIO::linkAs(const QUrl &src, const QUrl &destDir, JobFlags flags) { QList srcList; srcList.append(src); return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, true, flags); } CopyJob *KIO::trash(const QUrl &src, JobFlags flags) { QList srcList; srcList.append(src); return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags); } CopyJob *KIO::trash(const QList &srcList, JobFlags flags) { return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags); } #include "moc_copyjob.cpp" diff --git a/src/core/ksslcertificatemanager.cpp b/src/core/ksslcertificatemanager.cpp index f2ee3670..d525998c 100644 --- a/src/core/ksslcertificatemanager.cpp +++ b/src/core/ksslcertificatemanager.cpp @@ -1,498 +1,498 @@ /* This file is part of the KDE project * * Copyright (C) 2007, 2008, 2010 Andreas Hartmetz * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "ksslcertificatemanager.h" #include "ksslcertificatemanager_p.h" #include "ktcpsocket.h" #include "ksslerroruidata_p.h" #include #include #include #include #include #include #include #include #include #include "kssld_interface.h" /* Config file format: [] = #for example #mail.kdab.net = ExpireUTC 2008-08-20T18:22:14, SelfSigned, Expired #very.old.com = ExpireUTC 2008-08-20T18:22:14, TooWeakEncryption <- not actually planned to implement #clueless.admin.com = ExpireUTC 2008-08-20T18:22:14, HostNameMismatch # #Wildcard syntax #* = ExpireUTC 2008-08-20T18:22:14, SelfSigned #*.kdab.net = ExpireUTC 2008-08-20T18:22:14, SelfSigned #mail.kdab.net = ExpireUTC 2008-08-20T18:22:14, All <- not implemented #* = ExpireUTC 9999-12-31T23:59:59, Reject #we know that something is wrong with that certificate CertificatePEM = #host entries are all lowercase, thus no clashes */ // TODO GUI for managing exception rules class KSslCertificateRulePrivate { public: QSslCertificate certificate; QString hostName; bool isRejected; QDateTime expiryDateTime; QList ignoredErrors; }; KSslCertificateRule::KSslCertificateRule(const QSslCertificate &cert, const QString &hostName) : d(new KSslCertificateRulePrivate()) { d->certificate = cert; d->hostName = hostName; d->isRejected = false; } KSslCertificateRule::KSslCertificateRule(const KSslCertificateRule &other) : d(new KSslCertificateRulePrivate()) { *d = *other.d; } KSslCertificateRule::~KSslCertificateRule() { delete d; } KSslCertificateRule &KSslCertificateRule::operator=(const KSslCertificateRule &other) { *d = *other.d; return *this; } QSslCertificate KSslCertificateRule::certificate() const { return d->certificate; } QString KSslCertificateRule::hostName() const { return d->hostName; } void KSslCertificateRule::setExpiryDateTime(const QDateTime &dateTime) { d->expiryDateTime = dateTime; } QDateTime KSslCertificateRule::expiryDateTime() const { return d->expiryDateTime; } void KSslCertificateRule::setRejected(bool rejected) { d->isRejected = rejected; } bool KSslCertificateRule::isRejected() const { return d->isRejected; } bool KSslCertificateRule::isErrorIgnored(KSslError::Error error) const { foreach (KSslError::Error ignoredError, d->ignoredErrors) if (error == ignoredError) { return true; } return false; } void KSslCertificateRule::setIgnoredErrors(const QList &errors) { d->ignoredErrors.clear(); //### Quadratic runtime, woohoo! Use a QSet if that should ever be an issue. for (KSslError::Error e : errors) if (!isErrorIgnored(e)) { d->ignoredErrors.append(e); } } void KSslCertificateRule::setIgnoredErrors(const QList &errors) { QList el; el.reserve(errors.size()); for (const KSslError &e : errors) { el.append(e.error()); } setIgnoredErrors(el); } QList KSslCertificateRule::ignoredErrors() const { return d->ignoredErrors; } QList KSslCertificateRule::filterErrors(const QList &errors) const { QList ret; for (KSslError::Error error : errors) { if (!isErrorIgnored(error)) { ret.append(error); } } return ret; } QList KSslCertificateRule::filterErrors(const QList &errors) const { QList ret; for (const KSslError &error : errors) { if (!isErrorIgnored(error.error())) { ret.append(error); } } return ret; } //////////////////////////////////////////////////////////////////// static QList deduplicate(const QList &certs) { QSet digests; QList ret; for (const QSslCertificate &cert : certs) { QByteArray digest = cert.digest(); if (!digests.contains(digest)) { digests.insert(digest); ret.append(cert); } } return ret; } KSslCertificateManagerPrivate::KSslCertificateManagerPrivate() : config(QStringLiteral("ksslcertificatemanager"), KConfig::SimpleConfig), iface(new org::kde::KSSLDInterface(QStringLiteral("org.kde.kssld5"), QStringLiteral("/modules/kssld"), QDBusConnection::sessionBus())), isCertListLoaded(false), userCertDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kssl/userCaCertificates/")) { } KSslCertificateManagerPrivate::~KSslCertificateManagerPrivate() { delete iface; iface = nullptr; } void KSslCertificateManagerPrivate::loadDefaultCaCertificates() { defaultCaCertificates.clear(); - QList certs = deduplicate(QSslSocket::systemCaCertificates()); + QList certs = deduplicate(QSslConfiguration::systemCaCertificates()); KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig); KConfigGroup group = config.group("Blacklist of CA Certificates"); certs.append(QSslCertificate::fromPath(userCertDir + QLatin1Char('*'), QSsl::Pem, QRegExp::Wildcard)); for (const QSslCertificate &cert : qAsConst(certs)) { const QByteArray digest = cert.digest().toHex(); if (!group.hasKey(digest.constData())) { defaultCaCertificates += cert; } } isCertListLoaded = true; } bool KSslCertificateManagerPrivate::addCertificate(const KSslCaCertificate &in) { //qDebug() << Q_FUNC_INFO; // cannot add a certificate to the system store if (in.store == KSslCaCertificate::SystemStore) { Q_ASSERT(false); return false; } if (knownCerts.contains(in.certHash)) { Q_ASSERT(false); return false; } QString certFilename = userCertDir + QString::fromLatin1(in.certHash); QFile certFile(certFilename); if (!QDir().mkpath(userCertDir) || certFile.open(QIODevice::ReadOnly)) { return false; } if (!certFile.open(QIODevice::WriteOnly)) { return false; } if (certFile.write(in.cert.toPem()) < 1) { return false; } knownCerts.insert(in.certHash); updateCertificateBlacklisted(in); return true; } bool KSslCertificateManagerPrivate::removeCertificate(const KSslCaCertificate &old) { //qDebug() << Q_FUNC_INFO; // cannot remove a certificate from the system store if (old.store == KSslCaCertificate::SystemStore) { Q_ASSERT(false); return false; } if (!QFile::remove(userCertDir + QString::fromLatin1(old.certHash))) { // suppose somebody copied a certificate file into userCertDir without changing the // filename to the digest. // the rest of the code will work fine because it loads all certificate files from // userCertDir without asking for the name, we just can't remove the certificate using // its digest as filename - so search the whole directory. // if the certificate was added with the digest as name *and* with a different name, we // still fail to remove it completely at first try - BAD USER! BAD! bool removed = false; QDir dir(userCertDir); foreach (const QString &certFilename, dir.entryList(QDir::Files)) { const QString certPath = userCertDir + certFilename; QList certs = QSslCertificate::fromPath(certPath); if (!certs.isEmpty() && certs.at(0).digest().toHex() == old.certHash) { if (QFile::remove(certPath)) { removed = true; } else { // maybe the file is readable but not writable return false; } } } if (!removed) { // looks like the file is not there return false; } } // note that knownCerts *should* need no updating due to the way setAllCertificates() works - // it should never call addCertificate and removeCertificate for the same cert in one run // clean up the blacklist setCertificateBlacklisted(old.certHash, false); return true; } static bool certLessThan(const KSslCaCertificate &cacert1, const KSslCaCertificate &cacert2) { if (cacert1.store != cacert2.store) { // SystemStore is numerically smaller so the system certs come first; this is important // so that system certificates come first in case the user added an already-present // certificate as a user certificate. return cacert1.store < cacert2.store; } return cacert1.certHash < cacert2.certHash; } void KSslCertificateManagerPrivate::setAllCertificates(const QList &certsIn) { Q_ASSERT(knownCerts.isEmpty()); QList in = certsIn; QList old = allCertificates(); std::sort(in.begin(), in.end(), certLessThan); std::sort(old.begin(), old.end(), certLessThan); for (int ii = 0, oi = 0; ii < in.size() || oi < old.size(); ++ii, ++oi) { // look at all elements in both lists, even if we reach the end of one early. if (ii >= in.size()) { removeCertificate(old.at(oi)); continue; } else if (oi >= old.size()) { addCertificate(in.at(ii)); continue; } if (certLessThan(old.at(oi), in.at(ii))) { // the certificate in "old" is not in "in". only advance the index of "old". removeCertificate(old.at(oi)); ii--; } else if (certLessThan(in.at(ii), old.at(oi))) { // the certificate in "in" is not in "old". only advance the index of "in". addCertificate(in.at(ii)); oi--; } else { // in.at(ii) "==" old.at(oi) if (in.at(ii).cert != old.at(oi).cert) { // hash collision, be prudent(?) and don't do anything. } else { knownCerts.insert(old.at(oi).certHash); if (in.at(ii).isBlacklisted != old.at(oi).isBlacklisted) { updateCertificateBlacklisted(in.at(ii)); } } } } knownCerts.clear(); QMutexLocker certListLocker(&certListMutex); isCertListLoaded = false; loadDefaultCaCertificates(); } QList KSslCertificateManagerPrivate::allCertificates() const { //qDebug() << Q_FUNC_INFO; QList ret; - foreach (const QSslCertificate &cert, deduplicate(QSslSocket::systemCaCertificates())) { + foreach (const QSslCertificate &cert, deduplicate(QSslConfiguration::systemCaCertificates())) { ret += KSslCaCertificate(cert, KSslCaCertificate::SystemStore, false); } foreach (const QSslCertificate &cert, QSslCertificate::fromPath(userCertDir + QLatin1Char('*'), QSsl::Pem, QRegExp::Wildcard)) { ret += KSslCaCertificate(cert, KSslCaCertificate::UserStore, false); } KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig); KConfigGroup group = config.group("Blacklist of CA Certificates"); for (int i = 0; i < ret.size(); i++) { if (group.hasKey(ret[i].certHash.constData())) { ret[i].isBlacklisted = true; //qDebug() << "is blacklisted"; } } return ret; } bool KSslCertificateManagerPrivate::updateCertificateBlacklisted(const KSslCaCertificate &cert) { return setCertificateBlacklisted(cert.certHash, cert.isBlacklisted); } bool KSslCertificateManagerPrivate::setCertificateBlacklisted(const QByteArray &certHash, bool isBlacklisted) { //qDebug() << Q_FUNC_INFO << isBlacklisted; KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig); KConfigGroup group = config.group("Blacklist of CA Certificates"); if (isBlacklisted) { // TODO check against certificate list ? group.writeEntry(certHash.constData(), QString()); } else { if (!group.hasKey(certHash.constData())) { return false; } group.deleteEntry(certHash.constData()); } return true; } class KSslCertificateManagerContainer { public: KSslCertificateManager sslCertificateManager; }; Q_GLOBAL_STATIC(KSslCertificateManagerContainer, g_instance) KSslCertificateManager::KSslCertificateManager() : d(new KSslCertificateManagerPrivate()) { } KSslCertificateManager::~KSslCertificateManager() { delete d; } //static KSslCertificateManager *KSslCertificateManager::self() { return &g_instance()->sslCertificateManager; } void KSslCertificateManager::setRule(const KSslCertificateRule &rule) { d->iface->setRule(rule); } void KSslCertificateManager::clearRule(const KSslCertificateRule &rule) { d->iface->clearRule(rule); } void KSslCertificateManager::clearRule(const QSslCertificate &cert, const QString &hostName) { d->iface->clearRule(cert, hostName); } KSslCertificateRule KSslCertificateManager::rule(const QSslCertificate &cert, const QString &hostName) const { return d->iface->rule(cert, hostName); } QList KSslCertificateManager::caCertificates() const { QMutexLocker certLocker(&d->certListMutex); if (!d->isCertListLoaded) { d->loadDefaultCaCertificates(); } return d->defaultCaCertificates; } //static QList KSslCertificateManager::nonIgnorableErrors(const QList &/*e*/) { QList ret; // ### add filtering here... return ret; } //static QList KSslCertificateManager::nonIgnorableErrors(const QList &/*e*/) { QList ret; // ### add filtering here... return ret; } QList _allKsslCaCertificates(KSslCertificateManager *cm) { return KSslCertificateManagerPrivate::get(cm)->allCertificates(); } void _setAllKsslCaCertificates(KSslCertificateManager *cm, const QList &certsIn) { KSslCertificateManagerPrivate::get(cm)->setAllCertificates(certsIn); } #include "moc_kssld_interface.cpp" diff --git a/src/filewidgets/kdiroperator.cpp b/src/filewidgets/kdiroperator.cpp index 29eb3589..cedbdac1 100644 --- a/src/filewidgets/kdiroperator.cpp +++ b/src/filewidgets/kdiroperator.cpp @@ -1,2834 +1,2829 @@ /* This file is part of the KDE libraries Copyright (C) 1999,2000 Stephan Kulow 1999,2000,2001,2002,2003 Carsten Pfeiffer 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 "kdiroperator.h" #include #include #include "kdirmodel.h" #include "kdiroperatordetailview_p.h" #include "kdiroperatoriconview_p.h" #include "kdirsortfilterproxymodel.h" #include "kfileitem.h" #include "kfilemetapreview_p.h" #include "kpreviewwidgetbase.h" #include "knewfilemenu.h" #include #include "../pathhelpers_p.h" #include #include // ConfigGroup, DefaultShowHidden, DefaultDirsFirst, DefaultSortReversed #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 template class QHash; // QDir::SortByMask is not only undocumented, it also omits QDir::Type which is another // sorting mode. static const int QDirSortMask = QDir::SortByMask | QDir::Type; void KDirOperator::keyPressEvent(QKeyEvent *e) { if (!(e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter)) { QWidget::keyPressEvent(e); } else { emit keyEnterReturnPressed(); } } class Q_DECL_HIDDEN KDirOperator::Private { public: explicit Private(KDirOperator *parent); ~Private(); enum InlinePreviewState { ForcedToFalse = 0, ForcedToTrue, NotForced }; // private methods bool checkPreviewInternal() const; void checkPath(const QString &txt, bool takeFiles = false); bool openUrl(const QUrl &url, KDirLister::OpenUrlFlags flags = KDirLister::NoFlags); int sortColumn() const; Qt::SortOrder sortOrder() const; void updateSorting(QDir::SortFlags sort); static bool isReadable(const QUrl &url); bool isSchemeSupported(const QString &scheme) const; KFile::FileView allViews(); QMetaObject::Connection m_connection; // A pair to store zoom settings for view kinds struct ZoomSettingsForView { QString name; int defaultValue; }; // private slots void _k_slotDetailedView(); void _k_slotSimpleView(); void _k_slotTreeView(); void _k_slotDetailedTreeView(); void _k_slotIconsView(); void _k_slotCompactView(); void _k_slotDetailsView(); void _k_slotToggleHidden(bool); void _k_slotToggleAllowExpansion(bool); void _k_togglePreview(bool); void _k_toggleInlinePreviews(bool); void _k_slotOpenFileManager(); void _k_slotSortByName(); void _k_slotSortBySize(); void _k_slotSortByDate(); void _k_slotSortByType(); void _k_slotSortReversed(bool doReverse); void _k_slotToggleDirsFirst(); void _k_slotToggleIconsView(); void _k_slotToggleCompactView(); void _k_slotToggleDetailsView(); void _k_slotToggleIgnoreCase(); void _k_slotStarted(); void _k_slotProgress(int); void _k_slotShowProgress(); void _k_slotIOFinished(); void _k_slotCanceled(); void _k_slotRedirected(const QUrl &); void _k_slotProperties(); void _k_slotActivated(const QModelIndex &); void _k_slotSelectionChanged(); void _k_openContextMenu(const QPoint &); void _k_triggerPreview(const QModelIndex &); void _k_showPreview(); void _k_slotSplitterMoved(int, int); void _k_assureVisibleSelection(); void _k_synchronizeSortingState(int, Qt::SortOrder); void _k_slotChangeDecorationPosition(); void _k_slotExpandToUrl(const QModelIndex &); void _k_slotItemsChanged(); void _k_slotDirectoryCreated(const QUrl &); int iconSizeForViewType(QAbstractItemView *itemView) const; void writeIconZoomSettingsIfNeeded(); ZoomSettingsForView zoomSettingsForViewForView() const; // private members KDirOperator * const parent; QStack backStack; ///< Contains all URLs you can reach with the back button. QStack forwardStack; ///< Contains all URLs you can reach with the forward button. QModelIndex lastHoveredIndex; KDirLister *dirLister; QUrl currUrl; KCompletion completion; KCompletion dirCompletion; bool completeListDirty; QDir::SortFlags sorting; QStyleOptionViewItem::Position decorationPosition; QSplitter *splitter; QAbstractItemView *itemView; KDirModel *dirModel; KDirSortFilterProxyModel *proxyModel; KFileItemList pendingMimeTypes; // the enum KFile::FileView as an int int viewKind; int defaultView; KFile::Modes mode; QProgressBar *progressBar; KPreviewWidgetBase *preview; QUrl previewUrl; int previewWidth; bool dirHighlighting; bool onlyDoubleClickSelectsFiles; bool followNewDirectories; bool followSelectedDirectories; QString lastURL; // used for highlighting a directory on cdUp QTimer *progressDelayTimer; int dropOptions; KActionMenu *actionMenu; KActionCollection *actionCollection; KNewFileMenu *newFileMenu; KConfigGroup *configGroup; KFilePreviewGenerator *previewGenerator; bool showPreviews; int iconsZoom; bool isSaving; KActionMenu *decorationMenu; KToggleAction *leftAction; QList itemsToBeSetAsCurrent; bool shouldFetchForItems; InlinePreviewState inlinePreviewState; QStringList supportedSchemes; }; KDirOperator::Private::Private(KDirOperator *_parent) : parent(_parent), dirLister(nullptr), decorationPosition(QStyleOptionViewItem::Left), splitter(nullptr), itemView(nullptr), dirModel(nullptr), proxyModel(nullptr), progressBar(nullptr), preview(nullptr), previewUrl(), previewWidth(0), dirHighlighting(false), onlyDoubleClickSelectsFiles(!qApp->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)), followNewDirectories(true), followSelectedDirectories(true), progressDelayTimer(nullptr), dropOptions(0), actionMenu(nullptr), actionCollection(nullptr), newFileMenu(nullptr), configGroup(nullptr), previewGenerator(nullptr), showPreviews(false), iconsZoom(0), isSaving(false), decorationMenu(nullptr), leftAction(nullptr), shouldFetchForItems(false), inlinePreviewState(NotForced) { } KDirOperator::Private::~Private() { delete itemView; itemView = nullptr; // TODO: // if (configGroup) { // itemView->writeConfig(configGroup); // } qDeleteAll(backStack); qDeleteAll(forwardStack); delete preview; preview = nullptr; delete proxyModel; proxyModel = nullptr; delete dirModel; dirModel = nullptr; dirLister = nullptr; // deleted by KDirModel delete configGroup; configGroup = nullptr; delete progressDelayTimer; progressDelayTimer = nullptr; } KDirOperator::KDirOperator(const QUrl &_url, QWidget *parent) : QWidget(parent), d(new Private(this)) { d->splitter = new QSplitter(this); d->splitter->setChildrenCollapsible(false); connect(d->splitter, SIGNAL(splitterMoved(int,int)), this, SLOT(_k_slotSplitterMoved(int,int))); d->preview = nullptr; d->mode = KFile::File; d->viewKind = KFile::Simple; if (_url.isEmpty()) { // no dir specified -> current dir QString strPath = QDir::currentPath(); strPath.append(QLatin1Char('/')); d->currUrl = QUrl::fromLocalFile(strPath); } else { d->currUrl = _url; if (d->currUrl.scheme().isEmpty()) { d->currUrl.setScheme(QStringLiteral("file")); } QString path = d->currUrl.path(); if (!path.endsWith(QLatin1Char('/'))) { path.append(QLatin1Char('/')); // make sure we have a trailing slash! } d->currUrl.setPath(path); } // We set the direction of this widget to LTR, since even on RTL desktops // viewing directory listings in RTL mode makes people's head explode. // Is this the correct place? Maybe it should be in some lower level widgets...? setLayoutDirection(Qt::LeftToRight); setDirLister(new KDirLister()); connect(&d->completion, &KCompletion::match, this, &KDirOperator::slotCompletionMatch); d->progressBar = new QProgressBar(this); d->progressBar->setObjectName(QStringLiteral("d->progressBar")); d->progressBar->adjustSize(); d->progressBar->move(2, height() - d->progressBar->height() - 2); d->progressDelayTimer = new QTimer(this); d->progressDelayTimer->setObjectName(QStringLiteral("d->progressBar delay timer")); connect(d->progressDelayTimer, SIGNAL(timeout()), SLOT(_k_slotShowProgress())); d->completeListDirty = false; // action stuff setupActions(); setupMenu(); d->sorting = QDir::NoSort; //so updateSorting() doesn't think nothing has changed d->updateSorting(QDir::Name | QDir::DirsFirst); setFocusPolicy(Qt::WheelFocus); setAcceptDrops(true); } KDirOperator::~KDirOperator() { resetCursor(); disconnect(d->dirLister, nullptr, this, nullptr); delete d; } void KDirOperator::setSorting(QDir::SortFlags spec) { d->updateSorting(spec); } QDir::SortFlags KDirOperator::sorting() const { return d->sorting; } bool KDirOperator::isRoot() const { #ifdef Q_OS_WIN if (url().isLocalFile()) { const QString path = url().toLocalFile(); if (path.length() == 3) { return (path[0].isLetter() && path[1] == QLatin1Char(':') && path[2] == QLatin1Char('/')); } return false; } else #endif return url().path() == QLatin1String("/"); } KDirLister *KDirOperator::dirLister() const { return d->dirLister; } void KDirOperator::resetCursor() { if (qApp) { QApplication::restoreOverrideCursor(); } d->progressBar->hide(); } void KDirOperator::sortByName() { d->updateSorting((d->sorting & ~QDirSortMask) | QDir::Name); } void KDirOperator::sortBySize() { d->updateSorting((d->sorting & ~QDirSortMask) | QDir::Size); } void KDirOperator::sortByDate() { d->updateSorting((d->sorting & ~QDirSortMask) | QDir::Time); } void KDirOperator::sortByType() { d->updateSorting((d->sorting & ~QDirSortMask) | QDir::Type); } void KDirOperator::sortReversed() { // toggle it, hence the inversion of current state d->_k_slotSortReversed(!(d->sorting & QDir::Reversed)); } void KDirOperator::toggleDirsFirst() { d->_k_slotToggleDirsFirst(); } void KDirOperator::toggleIgnoreCase() { if (d->proxyModel != nullptr) { Qt::CaseSensitivity cs = d->proxyModel->sortCaseSensitivity(); cs = (cs == Qt::CaseSensitive) ? Qt::CaseInsensitive : Qt::CaseSensitive; d->proxyModel->setSortCaseSensitivity(cs); } } void KDirOperator::updateSelectionDependentActions() { const bool hasSelection = (d->itemView != nullptr) && d->itemView->selectionModel()->hasSelection(); d->actionCollection->action(QStringLiteral("trash"))->setEnabled(hasSelection); d->actionCollection->action(QStringLiteral("delete"))->setEnabled(hasSelection); d->actionCollection->action(QStringLiteral("properties"))->setEnabled(hasSelection); } void KDirOperator::setPreviewWidget(KPreviewWidgetBase *w) { const bool showPreview = (w != nullptr); if (showPreview) { d->viewKind = (d->viewKind | KFile::PreviewContents); } else { d->viewKind = (d->viewKind & ~KFile::PreviewContents); } delete d->preview; d->preview = w; if (w) { d->splitter->addWidget(w); } KToggleAction *previewAction = static_cast(d->actionCollection->action(QStringLiteral("preview"))); previewAction->setEnabled(showPreview); previewAction->setChecked(showPreview); setView(static_cast(d->viewKind)); } KFileItemList KDirOperator::selectedItems() const { KFileItemList itemList; if (d->itemView == nullptr) { return itemList; } const QItemSelection selection = d->proxyModel->mapSelectionToSource(d->itemView->selectionModel()->selection()); const QModelIndexList indexList = selection.indexes(); for (const QModelIndex &index : indexList) { KFileItem item = d->dirModel->itemForIndex(index); if (!item.isNull()) { itemList.append(item); } } return itemList; } bool KDirOperator::isSelected(const KFileItem &item) const { if ((item.isNull()) || (d->itemView == nullptr)) { return false; } const QModelIndex dirIndex = d->dirModel->indexForItem(item); const QModelIndex proxyIndex = d->proxyModel->mapFromSource(dirIndex); return d->itemView->selectionModel()->isSelected(proxyIndex); } int KDirOperator::numDirs() const { return (d->dirLister == nullptr) ? 0 : d->dirLister->directories().count(); } int KDirOperator::numFiles() const { return (d->dirLister == nullptr) ? 0 : d->dirLister->items().count() - numDirs(); } KCompletion *KDirOperator::completionObject() const { return const_cast(&d->completion); } KCompletion *KDirOperator::dirCompletionObject() const { return const_cast(&d->dirCompletion); } KActionCollection *KDirOperator::actionCollection() const { return d->actionCollection; } KFile::FileView KDirOperator::Private::allViews() { return static_cast(KFile::Simple | KFile::Detail | KFile::Tree | KFile::DetailTree); } void KDirOperator::Private::_k_slotDetailedView() { // save old zoom settings writeIconZoomSettingsIfNeeded(); KFile::FileView view = static_cast((viewKind & ~allViews()) | KFile::Detail); parent->setView(view); } void KDirOperator::Private::_k_slotSimpleView() { // save old zoom settings writeIconZoomSettingsIfNeeded(); KFile::FileView view = static_cast((viewKind & ~allViews()) | KFile::Simple); parent->setView(view); } void KDirOperator::Private::_k_slotTreeView() { // save old zoom settings writeIconZoomSettingsIfNeeded(); KFile::FileView view = static_cast((viewKind & ~allViews()) | KFile::Tree); parent->setView(view); } void KDirOperator::Private::_k_slotDetailedTreeView() { // save old zoom settings writeIconZoomSettingsIfNeeded(); KFile::FileView view = static_cast((viewKind & ~allViews()) | KFile::DetailTree); parent->setView(view); } void KDirOperator::Private::_k_slotToggleAllowExpansion(bool allow) { KFile::FileView view = KFile::Detail; if (allow) { view = KFile::DetailTree; } parent->setView(view); } void KDirOperator::Private::_k_slotToggleHidden(bool show) { dirLister->setShowingDotFiles(show); parent->updateDir(); _k_assureVisibleSelection(); } void KDirOperator::Private::_k_togglePreview(bool on) { if (on) { viewKind = viewKind | KFile::PreviewContents; if (preview == nullptr) { preview = new KFileMetaPreview(parent); actionCollection->action(QStringLiteral("preview"))->setChecked(true); splitter->addWidget(preview); } preview->show(); QMetaObject::invokeMethod(parent, "_k_assureVisibleSelection", Qt::QueuedConnection); if (itemView != nullptr) { const QModelIndex index = itemView->selectionModel()->currentIndex(); if (index.isValid()) { _k_triggerPreview(index); } } } else if (preview != nullptr) { viewKind = viewKind & ~KFile::PreviewContents; preview->hide(); } } void KDirOperator::Private::_k_toggleInlinePreviews(bool show) { if (showPreviews == show) { return; } showPreviews = show; if (!previewGenerator) { return; } previewGenerator->setPreviewShown(show); } void KDirOperator::Private::_k_slotOpenFileManager() { const KFileItemList list = parent->selectedItems(); if (list.isEmpty()) { KIO::highlightInFileManager({currUrl.adjusted(QUrl::StripTrailingSlash)}); } else { KIO::highlightInFileManager(list.urlList()); } } void KDirOperator::Private::_k_slotSortByName() { parent->sortByName(); } void KDirOperator::Private::_k_slotSortBySize() { parent->sortBySize(); } void KDirOperator::Private::_k_slotSortByDate() { parent->sortByDate(); } void KDirOperator::Private::_k_slotSortByType() { parent->sortByType(); } void KDirOperator::Private::_k_slotSortReversed(bool doReverse) { QDir::SortFlags s = sorting & ~QDir::Reversed; if (doReverse) { s |= QDir::Reversed; } updateSorting(s); } void KDirOperator::Private::_k_slotToggleDirsFirst() { QDir::SortFlags s = (sorting ^ QDir::DirsFirst); updateSorting(s); } void KDirOperator::Private::_k_slotIconsView() { // save old zoom settings writeIconZoomSettingsIfNeeded(); // Put the icons on top actionCollection->action(QStringLiteral("decorationAtTop"))->setChecked(true); decorationPosition = QStyleOptionViewItem::Top; // Switch to simple view KFile::FileView fileView = static_cast((viewKind & ~allViews()) | KFile::Simple); parent->setView(fileView); } void KDirOperator::Private::_k_slotCompactView() { // save old zoom settings writeIconZoomSettingsIfNeeded(); // Put the icons on the side actionCollection->action(QStringLiteral("decorationAtLeft"))->setChecked(true); decorationPosition = QStyleOptionViewItem::Left; // Switch to simple view KFile::FileView fileView = static_cast((viewKind & ~allViews()) | KFile::Simple); parent->setView(fileView); } void KDirOperator::Private::_k_slotDetailsView() { // save old zoom settings writeIconZoomSettingsIfNeeded(); KFile::FileView view; if (actionCollection->action(QStringLiteral("allow expansion"))->isChecked()) { view = static_cast((viewKind & ~allViews()) | KFile::DetailTree); } else { view = static_cast((viewKind & ~allViews()) | KFile::Detail); } parent->setView(view); } void KDirOperator::Private::_k_slotToggleIgnoreCase() { // TODO: port to Qt4's QAbstractItemView /*if ( !d->fileView ) return; QDir::SortFlags sorting = d->fileView->sorting(); if ( !KFile::isSortCaseInsensitive( sorting ) ) d->fileView->setSorting( sorting | QDir::IgnoreCase ); else d->fileView->setSorting( sorting & ~QDir::IgnoreCase ); d->sorting = d->fileView->sorting();*/ } void KDirOperator::mkdir() { d->newFileMenu->setPopupFiles(QList() << url()); d->newFileMenu->setViewShowsHiddenFiles(showHiddenFiles()); d->newFileMenu->createDirectory(); } bool KDirOperator::mkdir(const QString &directory, bool enterDirectory) { // Creates "directory", relative to the current directory (d->currUrl). // The given path may contain any number directories, existent or not. // They will all be created, if possible. // TODO: very similar to KDirSelectDialog::Private::slotMkdir bool writeOk = false; bool exists = false; QUrl folderurl(d->currUrl); const QStringList dirs = directory.split(QLatin1Char('/'), QString::SkipEmptyParts); QStringList::ConstIterator it = dirs.begin(); for (; it != dirs.end(); ++it) { folderurl.setPath(concatPaths(folderurl.path(), *it)); if (folderurl.isLocalFile()) { exists = QFile::exists(folderurl.toLocalFile()); } else { KIO::StatJob *job = KIO::stat(folderurl); KJobWidgets::setWindow(job, this); job->setDetails(0); //We only want to know if it exists, 0 == that. job->setSide(KIO::StatJob::DestinationSide); exists = job->exec(); } if (!exists) { KIO::Job *job = KIO::mkdir(folderurl); KJobWidgets::setWindow(job, this); writeOk = job->exec(); } } if (exists) { // url was already existent KMessageBox::sorry(d->itemView, i18n("A file or folder named %1 already exists.", folderurl.toDisplayString(QUrl::PreferLocalFile))); } else if (!writeOk) { KMessageBox::sorry(d->itemView, i18n("You do not have permission to " "create that folder.")); } else if (enterDirectory) { setUrl(folderurl, true); } return writeOk; } KIO::DeleteJob *KDirOperator::del(const KFileItemList &items, QWidget *parent, bool ask, bool showProgress) { if (items.isEmpty()) { KMessageBox::information(parent, i18n("You did not select a file to delete."), i18n("Nothing to Delete")); return nullptr; } if (parent == nullptr) { parent = this; } const QList urls = items.urlList(); bool doIt = !ask; if (ask) { KIO::JobUiDelegate uiDelegate; uiDelegate.setWindow(parent); doIt = uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::DefaultConfirmation); } if (doIt) { KIO::JobFlags flags = showProgress ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::DeleteJob *job = KIO::del(urls, flags); KJobWidgets::setWindow(job, this); job->uiDelegate()->setAutoErrorHandlingEnabled(true); return job; } return nullptr; } void KDirOperator::deleteSelected() { const KFileItemList list = selectedItems(); if (!list.isEmpty()) { del(list, this); } } KIO::CopyJob *KDirOperator::trash(const KFileItemList &items, QWidget *parent, bool ask, bool showProgress) { if (items.isEmpty()) { KMessageBox::information(parent, i18n("You did not select a file to trash."), i18n("Nothing to Trash")); return nullptr; } const QList urls = items.urlList(); bool doIt = !ask; if (ask) { KIO::JobUiDelegate uiDelegate; uiDelegate.setWindow(parent); doIt = uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation); } if (doIt) { KIO::JobFlags flags = showProgress ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::CopyJob *job = KIO::trash(urls, flags); KJobWidgets::setWindow(job, this); job->uiDelegate()->setAutoErrorHandlingEnabled(true); return job; } return nullptr; } KFilePreviewGenerator *KDirOperator::previewGenerator() const { return d->previewGenerator; } void KDirOperator::setInlinePreviewShown(bool show) { d->inlinePreviewState = show ? Private::ForcedToTrue : Private::ForcedToFalse; } bool KDirOperator::isInlinePreviewShown() const { return d->showPreviews; } int KDirOperator::iconsZoom() const { return d->iconsZoom; } void KDirOperator::setIsSaving(bool isSaving) { d->isSaving = isSaving; } bool KDirOperator::isSaving() const { return d->isSaving; } void KDirOperator::trashSelected() { if (d->itemView == nullptr) { return; } if (QApplication::keyboardModifiers() & Qt::ShiftModifier) { deleteSelected(); return; } const KFileItemList list = selectedItems(); if (!list.isEmpty()) { trash(list, this); } } void KDirOperator::setIconsZoom(int _value) { if (d->iconsZoom == _value) { return; } int value = _value; value = qMin(100, value); value = qMax(0, value); d->iconsZoom = value; if (!d->previewGenerator) { return; } const int maxSize = KIconLoader::SizeEnormous - KIconLoader::SizeSmall; const int val = (maxSize * value / 100) + KIconLoader::SizeSmall; d->itemView->setIconSize(QSize(val, val)); d->previewGenerator->updatePreviews(); emit currentIconSizeChanged(value); } void KDirOperator::close() { resetCursor(); d->pendingMimeTypes.clear(); d->completion.clear(); d->dirCompletion.clear(); d->completeListDirty = true; d->dirLister->stop(); } void KDirOperator::Private::checkPath(const QString &, bool /*takeFiles*/) // SLOT { #if 0 // copy the argument in a temporary string QString text = _txt; // it's unlikely to happen, that at the beginning are spaces, but // for the end, it happens quite often, I guess. text = text.trimmed(); // if the argument is no URL (the check is quite fragil) and it's // no absolute path, we add the current directory to get a correct url if (text.find(':') < 0 && text[0] != '/') { text.insert(0, d->currUrl); } // in case we have a selection defined and someone patched the file- // name, we check, if the end of the new name is changed. if (!selection.isNull()) { int position = text.lastIndexOf('/'); ASSERT(position >= 0); // we already inserted the current d->dirLister in case QString filename = text.mid(position + 1, text.length()); if (filename != selection) { selection.clear(); } } QUrl u(text); // I have to take care of entered URLs bool filenameEntered = false; if (u.isLocalFile()) { // the empty path is kind of a hack KFileItem i("", u.toLocalFile()); if (i.isDir()) { setUrl(text, true); } else { if (takeFiles) if (acceptOnlyExisting && !i.isFile()) { warning("you entered an invalid URL"); } else { filenameEntered = true; } } } else { setUrl(text, true); } if (filenameEntered) { filename_ = u.url(); emit fileSelected(filename_); QApplication::restoreOverrideCursor(); accept(); } #endif // qDebug() << "TODO KDirOperator::checkPath()"; } void KDirOperator::setUrl(const QUrl &_newurl, bool clearforward) { QUrl newurl; if (!_newurl.isValid()) { newurl = QUrl::fromLocalFile(QDir::homePath()); } else { newurl = _newurl.adjusted(QUrl::NormalizePathSegments); } if (!newurl.path().isEmpty() && !newurl.path().endsWith(QLatin1Char('/'))) { newurl.setPath(newurl.path() + QLatin1Char('/')); } // already set if (newurl.matches(d->currUrl, QUrl::StripTrailingSlash)) { return; } if (!d->isSchemeSupported(newurl.scheme())) return; if (!Private::isReadable(newurl)) { // maybe newurl is a file? check its parent directory newurl = newurl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); if (newurl.matches(d->currUrl, QUrl::StripTrailingSlash)) { return; // parent is current dir, nothing to do (fixes #173454, too) } KIO::StatJob *job = KIO::stat(newurl); KJobWidgets::setWindow(job, this); bool res = job->exec(); KIO::UDSEntry entry = job->statResult(); KFileItem i(entry, newurl); if ((!res || !Private::isReadable(newurl)) && i.isDir()) { resetCursor(); KMessageBox::error(d->itemView, i18n("The specified folder does not exist " "or was not readable.")); return; } else if (!i.isDir()) { return; } } if (clearforward) { // autodelete should remove this one d->backStack.push(new QUrl(d->currUrl)); qDeleteAll(d->forwardStack); d->forwardStack.clear(); } d->lastURL = d->currUrl.toString(QUrl::StripTrailingSlash); d->currUrl = newurl; pathChanged(); emit urlEntered(newurl); // enable/disable actions QAction *forwardAction = d->actionCollection->action(QStringLiteral("forward")); forwardAction->setEnabled(!d->forwardStack.isEmpty()); QAction *backAction = d->actionCollection->action(QStringLiteral("back")); backAction->setEnabled(!d->backStack.isEmpty()); QAction *upAction = d->actionCollection->action(QStringLiteral("up")); upAction->setEnabled(!isRoot()); d->openUrl(newurl); } void KDirOperator::updateDir() { QApplication::setOverrideCursor(Qt::WaitCursor); d->dirLister->emitChanges(); QApplication::restoreOverrideCursor(); } void KDirOperator::rereadDir() { pathChanged(); d->openUrl(d->currUrl, KDirLister::Reload); } bool KDirOperator::Private::isSchemeSupported(const QString &scheme) const { return supportedSchemes.isEmpty() || supportedSchemes.contains(scheme); } bool KDirOperator::Private::openUrl(const QUrl &url, KDirLister::OpenUrlFlags flags) { const bool result = KProtocolManager::supportsListing(url) && isSchemeSupported(url.scheme()) && dirLister->openUrl(url, flags); if (!result) { // in that case, neither completed() nor canceled() will be emitted by KDL _k_slotCanceled(); } return result; } int KDirOperator::Private::sortColumn() const { int column = KDirModel::Name; if (KFile::isSortByDate(sorting)) { column = KDirModel::ModifiedTime; } else if (KFile::isSortBySize(sorting)) { column = KDirModel::Size; } else if (KFile::isSortByType(sorting)) { column = KDirModel::Type; } else { Q_ASSERT(KFile::isSortByName(sorting)); } return column; } Qt::SortOrder KDirOperator::Private::sortOrder() const { return (sorting & QDir::Reversed) ? Qt::DescendingOrder : Qt::AscendingOrder; } void KDirOperator::Private::updateSorting(QDir::SortFlags sort) { // qDebug() << "changing sort flags from" << sorting << "to" << sort; if (sort == sorting) { return; } if ((sorting ^ sort) & QDir::DirsFirst) { // The "Folders First" setting has been changed. // We need to make sure that the files and folders are really re-sorted. // Without the following intermediate "fake resorting", // QSortFilterProxyModel::sort(int column, Qt::SortOrder order) // would do nothing because neither the column nor the sort order have been changed. Qt::SortOrder tmpSortOrder = (sortOrder() == Qt::AscendingOrder ? Qt::DescendingOrder : Qt::AscendingOrder); proxyModel->sort(sortOrder(), tmpSortOrder); proxyModel->setSortFoldersFirst(sort & QDir::DirsFirst); } sorting = sort; parent->updateSortActions(); proxyModel->sort(sortColumn(), sortOrder()); // TODO: The headers from QTreeView don't take care about a sorting // change of the proxy model hence they must be updated the manually. // This is done here by a qobject_cast, but it would be nicer to: // - provide a signal 'sortingChanged()' // - connect KDirOperatorDetailView() with this signal and update the // header internally QTreeView *treeView = qobject_cast(itemView); if (treeView != nullptr) { QHeaderView *headerView = treeView->header(); headerView->blockSignals(true); headerView->setSortIndicator(sortColumn(), sortOrder()); headerView->blockSignals(false); } _k_assureVisibleSelection(); } // Protected void KDirOperator::pathChanged() { if (d->itemView == nullptr) { return; } d->pendingMimeTypes.clear(); //d->fileView->clear(); TODO d->completion.clear(); d->dirCompletion.clear(); // it may be, that we weren't ready at this time QApplication::restoreOverrideCursor(); // when KIO::Job emits finished, the slot will restore the cursor QApplication::setOverrideCursor(Qt::WaitCursor); if (!Private::isReadable(d->currUrl)) { KMessageBox::error(d->itemView, i18n("The specified folder does not exist " "or was not readable.")); if (d->backStack.isEmpty()) { home(); } else { back(); } } } void KDirOperator::Private::_k_slotRedirected(const QUrl &newURL) { currUrl = newURL; pendingMimeTypes.clear(); completion.clear(); dirCompletion.clear(); completeListDirty = true; emit parent->urlEntered(newURL); } // Code pinched from kfm then hacked void KDirOperator::back() { if (d->backStack.isEmpty()) { return; } d->forwardStack.push(new QUrl(d->currUrl)); QUrl *s = d->backStack.pop(); setUrl(*s, false); delete s; } // Code pinched from kfm then hacked void KDirOperator::forward() { if (d->forwardStack.isEmpty()) { return; } d->backStack.push(new QUrl(d->currUrl)); QUrl *s = d->forwardStack.pop(); setUrl(*s, false); delete s; } QUrl KDirOperator::url() const { return d->currUrl; } void KDirOperator::cdUp() { // Allow /d/c// to go up to /d/ instead of /d/c/ QUrl tmp(d->currUrl.adjusted(QUrl::NormalizePathSegments)); setUrl(tmp.resolved(QUrl(QStringLiteral(".."))), true); } void KDirOperator::home() { setUrl(QUrl::fromLocalFile(QDir::homePath()), true); } void KDirOperator::clearFilter() { d->dirLister->setNameFilter(QString()); d->dirLister->clearMimeFilter(); checkPreviewSupport(); } void KDirOperator::setNameFilter(const QString &filter) { d->dirLister->setNameFilter(filter); checkPreviewSupport(); } QString KDirOperator::nameFilter() const { return d->dirLister->nameFilter(); } void KDirOperator::setMimeFilter(const QStringList &mimetypes) { d->dirLister->setMimeFilter(mimetypes); checkPreviewSupport(); } QStringList KDirOperator::mimeFilter() const { return d->dirLister->mimeFilters(); } void KDirOperator::setNewFileMenuSupportedMimeTypes(const QStringList &mimeTypes) { d->newFileMenu->setSupportedMimeTypes(mimeTypes); } QStringList KDirOperator::newFileMenuSupportedMimeTypes() const { return d->newFileMenu->supportedMimeTypes(); } bool KDirOperator::checkPreviewSupport() { KToggleAction *previewAction = static_cast(d->actionCollection->action(QStringLiteral("preview"))); bool hasPreviewSupport = false; KConfigGroup cg(KSharedConfig::openConfig(), ConfigGroup); if (cg.readEntry("Show Default Preview", true)) { hasPreviewSupport = d->checkPreviewInternal(); } previewAction->setEnabled(hasPreviewSupport); return hasPreviewSupport; } void KDirOperator::activatedMenu(const KFileItem &item, const QPoint &pos) { updateSelectionDependentActions(); d->newFileMenu->setPopupFiles(QList() << item.url()); d->newFileMenu->setViewShowsHiddenFiles(showHiddenFiles()); d->newFileMenu->checkUpToDate(); d->actionCollection->action(QStringLiteral("new"))->setEnabled(item.isDir()); emit contextMenuAboutToShow(item, d->actionMenu->menu()); d->actionMenu->menu()->exec(pos); } void KDirOperator::changeEvent(QEvent *event) { QWidget::changeEvent(event); } bool KDirOperator::eventFilter(QObject *watched, QEvent *event) { Q_UNUSED(watched); // If we are not hovering any items, check if there is a current index // set. In that case, we show the preview of that item. switch (event->type()) { case QEvent::MouseMove: { if (d->preview && !d->preview->isHidden()) { const QModelIndex hoveredIndex = d->itemView->indexAt(d->itemView->viewport()->mapFromGlobal(QCursor::pos())); if (d->lastHoveredIndex == hoveredIndex) { return QWidget::eventFilter(watched, event); } d->lastHoveredIndex = hoveredIndex; const QModelIndex focusedIndex = d->itemView->selectionModel() ? d->itemView->selectionModel()->currentIndex() : QModelIndex(); if (!hoveredIndex.isValid() && focusedIndex.isValid() && d->itemView->selectionModel()->isSelected(focusedIndex) && (d->lastHoveredIndex != focusedIndex)) { const QModelIndex sourceFocusedIndex = d->proxyModel->mapToSource(focusedIndex); const KFileItem item = d->dirModel->itemForIndex(sourceFocusedIndex); if (!item.isNull()) { d->preview->showPreview(item.url()); } } } } break; case QEvent::MouseButtonRelease: { if (d->preview != nullptr && !d->preview->isHidden()) { const QModelIndex hoveredIndex = d->itemView->indexAt(d->itemView->viewport()->mapFromGlobal(QCursor::pos())); const QModelIndex focusedIndex = d->itemView->selectionModel() ? d->itemView->selectionModel()->currentIndex() : QModelIndex(); if (((!focusedIndex.isValid()) || !d->itemView->selectionModel()->isSelected(focusedIndex)) && (!hoveredIndex.isValid())) { d->preview->clearPreview(); } } QMouseEvent *mouseEvent = static_cast(event); if (mouseEvent) { switch (mouseEvent->button()) { case Qt::BackButton: back(); break; case Qt::ForwardButton: forward(); break; default: break; } } } break; case QEvent::Wheel: { QWheelEvent *evt = static_cast(event); if (evt->modifiers() & Qt::ControlModifier) { if (evt->angleDelta().y() > 0) { setIconsZoom(d->iconsZoom + 10); } else { setIconsZoom(d->iconsZoom - 10); } return true; } } break; case QEvent::DragEnter: { // Accepts drops of one file or folder only QDragEnterEvent *evt = static_cast(event); const QList urls = KUrlMimeData::urlsFromMimeData(evt->mimeData(), KUrlMimeData::DecodeOptions::PreferLocalUrls); // only one file/folder can be dropped at the moment if (urls.size() != 1) { evt->ignore(); } else { // mimetype filtering bool mimeFilterPass = true; const QStringList mimeFilters = d->dirLister->mimeFilters(); if (mimeFilters.size() > 1) { mimeFilterPass = false; const QUrl url = urls.constFirst(); QMimeDatabase mimeDataBase; QMimeType fileMimeType = mimeDataBase.mimeTypeForUrl(url); QRegularExpression regex; for (const auto& mimeFilter : mimeFilters) { regex.setPattern(mimeFilter); if (regex.match(fileMimeType.name()).hasMatch()) { // matches! mimeFilterPass = true; break; } } } event->setAccepted(mimeFilterPass); } return true; } case QEvent::Drop: { QDropEvent *evt = static_cast(event); const QList urls = KUrlMimeData::urlsFromMimeData(evt->mimeData(), KUrlMimeData::DecodeOptions::PreferLocalUrls); const QUrl url = urls.constFirst(); // stat the url to get details KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo); job->exec(); setFocus(); KIO::UDSEntry entry = job->statResult(); if (entry.isDir()) { // if this was a directory setUrl(url, false); } else { // if the current url is not known if (d->dirLister->findByUrl(url).isNull()) { setUrl(url.adjusted(QUrl::RemoveFilename), false); // Will set the current item once loading has finished auto urlSetterClosure = [this, url](){ setCurrentItem(url); QObject::disconnect(d->m_connection); }; d->m_connection = connect(this, &KDirOperator::finishedLoading, this, urlSetterClosure); } else { setCurrentItem(url); } } evt->accept(); return true; } default: break; } return QWidget::eventFilter(watched, event); } bool KDirOperator::Private::checkPreviewInternal() const { const QStringList supported = KIO::PreviewJob::supportedMimeTypes(); // no preview support for directories? if (parent->dirOnlyMode() && supported.indexOf(QLatin1String("inode/directory")) == -1) { return false; } QStringList mimeTypes = dirLister->mimeFilters(); const QStringList nameFilter = dirLister->nameFilter().split(QLatin1Char(' '), QString::SkipEmptyParts); QMimeDatabase db; if (mimeTypes.isEmpty() && nameFilter.isEmpty() && !supported.isEmpty()) { return true; } else { QRegExp r; r.setPatternSyntax(QRegExp::Wildcard); // the "mimetype" can be "image/*" if (!mimeTypes.isEmpty()) { QStringList::ConstIterator it = supported.begin(); for (; it != supported.end(); ++it) { r.setPattern(*it); QStringList result = mimeTypes.filter(r); if (!result.isEmpty()) { // matches! -> we want previews return true; } } } if (!nameFilter.isEmpty()) { // find the mimetypes of all the filter-patterns QStringList::const_iterator it1 = nameFilter.begin(); for (; it1 != nameFilter.end(); ++it1) { if ((*it1) == QLatin1String("*")) { return true; } QMimeType mt = db.mimeTypeForFile(*it1, QMimeDatabase::MatchExtension /*fast mode, no file contents exist*/); if (!mt.isValid()) { continue; } QString mime = mt.name(); // the "mimetypes" we get from the PreviewJob can be "image/*" // so we need to check in wildcard mode QStringList::ConstIterator it2 = supported.begin(); for (; it2 != supported.end(); ++it2) { r.setPattern(*it2); if (r.indexIn(mime) != -1) { return true; } } } } } return false; } QAbstractItemView *KDirOperator::createView(QWidget *parent, KFile::FileView viewKind) { QAbstractItemView *itemView = nullptr; if (KFile::isDetailView(viewKind) || KFile::isTreeView(viewKind) || KFile::isDetailTreeView(viewKind)) { KDirOperatorDetailView *detailView = new KDirOperatorDetailView(parent); detailView->setViewMode(viewKind); itemView = detailView; } else { itemView = new KDirOperatorIconView(parent, decorationPosition()); } return itemView; } void KDirOperator::setAcceptDrops(bool acceptsDrops) { QWidget::setAcceptDrops(acceptsDrops); if (view()) { view()->setAcceptDrops(acceptsDrops); if (acceptsDrops) { view()->installEventFilter(this); } else { view()->removeEventFilter(this); } } } void KDirOperator::setDropOptions(int options) { d->dropOptions = options; // TODO: //if (d->fileView) // d->fileView->setDropOptions(options); } void KDirOperator::setView(KFile::FileView viewKind) { bool preview = (KFile::isPreviewInfo(viewKind) || KFile::isPreviewContents(viewKind)); if (viewKind == KFile::Default) { if (KFile::isDetailView((KFile::FileView)d->defaultView)) { viewKind = KFile::Detail; } else if (KFile::isTreeView((KFile::FileView)d->defaultView)) { viewKind = KFile::Tree; } else if (KFile::isDetailTreeView((KFile::FileView)d->defaultView)) { viewKind = KFile::DetailTree; } else { viewKind = KFile::Simple; } const KFile::FileView defaultViewKind = static_cast(d->defaultView); preview = (KFile::isPreviewInfo(defaultViewKind) || KFile::isPreviewContents(defaultViewKind)) && d->actionCollection->action(QStringLiteral("preview"))->isEnabled(); } d->viewKind = static_cast(viewKind); viewKind = static_cast(d->viewKind); QAbstractItemView *newView = createView(this, viewKind); setView(newView); if (acceptDrops()) { newView->setAcceptDrops(true); newView->installEventFilter(this); } d->_k_togglePreview(preview); } KFile::FileView KDirOperator::viewMode() const { return static_cast(d->viewKind); } QAbstractItemView *KDirOperator::view() const { return d->itemView; } KFile::Modes KDirOperator::mode() const { return d->mode; } void KDirOperator::setMode(KFile::Modes mode) { if (d->mode == mode) { return; } d->mode = mode; d->dirLister->setDirOnlyMode(dirOnlyMode()); // reset the view with the different mode if (d->itemView != nullptr) { setView(static_cast(d->viewKind)); } } void KDirOperator::setView(QAbstractItemView *view) { if (view == d->itemView) { return; } // TODO: do a real timer and restart it after that d->pendingMimeTypes.clear(); const bool listDir = (d->itemView == nullptr); if (d->mode & KFile::Files) { view->setSelectionMode(QAbstractItemView::ExtendedSelection); } else { view->setSelectionMode(QAbstractItemView::SingleSelection); } QItemSelectionModel *selectionModel = nullptr; if ((d->itemView != nullptr) && d->itemView->selectionModel()->hasSelection()) { // remember the selection of the current item view and apply this selection // to the new view later const QItemSelection selection = d->itemView->selectionModel()->selection(); selectionModel = new QItemSelectionModel(d->proxyModel, this); selectionModel->select(selection, QItemSelectionModel::Select); } setFocusProxy(nullptr); delete d->itemView; d->itemView = view; d->itemView->setModel(d->proxyModel); setFocusProxy(d->itemView); view->viewport()->installEventFilter(this); KFileItemDelegate *delegate = new KFileItemDelegate(d->itemView); d->itemView->setItemDelegate(delegate); d->itemView->viewport()->setAttribute(Qt::WA_Hover); d->itemView->setContextMenuPolicy(Qt::CustomContextMenu); d->itemView->setMouseTracking(true); //d->itemView->setDropOptions(d->dropOptions); // first push our settings to the view, then listen for changes from the view QTreeView *treeView = qobject_cast(d->itemView); if (treeView) { QHeaderView *headerView = treeView->header(); headerView->setSortIndicator(d->sortColumn(), d->sortOrder()); connect(headerView, SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), this, SLOT(_k_synchronizeSortingState(int,Qt::SortOrder))); } connect(d->itemView, SIGNAL(activated(QModelIndex)), this, SLOT(_k_slotActivated(QModelIndex))); connect(d->itemView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(_k_openContextMenu(QPoint))); connect(d->itemView, SIGNAL(entered(QModelIndex)), this, SLOT(_k_triggerPreview(QModelIndex))); d->splitter->insertWidget(0, d->itemView); d->splitter->resize(size()); d->itemView->show(); if (listDir) { QApplication::setOverrideCursor(Qt::WaitCursor); d->openUrl(d->currUrl); } if (selectionModel != nullptr) { d->itemView->setSelectionModel(selectionModel); QMetaObject::invokeMethod(this, "_k_assureVisibleSelection", Qt::QueuedConnection); } connect(d->itemView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(_k_triggerPreview(QModelIndex))); connect(d->itemView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(_k_slotSelectionChanged())); // if we cannot cast it to a QListView, disable the "Icon Position" menu. Note that this check // needs to be done here, and not in createView, since we can be set an external view d->decorationMenu->setEnabled(qobject_cast(d->itemView)); d->shouldFetchForItems = qobject_cast(view); if (d->shouldFetchForItems) { connect(d->dirModel, SIGNAL(expand(QModelIndex)), this, SLOT(_k_slotExpandToUrl(QModelIndex))); } else { d->itemsToBeSetAsCurrent.clear(); } const bool previewForcedToTrue = d->inlinePreviewState == Private::ForcedToTrue; const bool previewShown = d->inlinePreviewState == Private::NotForced ? d->showPreviews : previewForcedToTrue; d->previewGenerator = new KFilePreviewGenerator(d->itemView); const int maxSize = KIconLoader::SizeEnormous - KIconLoader::SizeSmall; const int val = (maxSize * d->iconsZoom / 100) + KIconLoader::SizeSmall; d->itemView->setIconSize(previewForcedToTrue ? QSize(KIconLoader::SizeHuge, KIconLoader::SizeHuge) : QSize(val, val)); d->previewGenerator->setPreviewShown(previewShown); d->actionCollection->action(QStringLiteral("inline preview"))->setChecked(previewShown); // ensure we change everything needed d->_k_slotChangeDecorationPosition(); updateViewActions(); emit viewChanged(view); const int zoom = previewForcedToTrue ? (KIconLoader::SizeHuge - KIconLoader::SizeSmall + 1) * 100 / maxSize : d->iconSizeForViewType(view); // this will make d->iconsZoom be updated, since setIconsZoom slot will be called emit currentIconSizeChanged(zoom); } void KDirOperator::setDirLister(KDirLister *lister) { if (lister == d->dirLister) { // sanity check return; } delete d->dirModel; d->dirModel = nullptr; delete d->proxyModel; d->proxyModel = nullptr; //delete d->dirLister; // deleted by KDirModel already, which took ownership d->dirLister = lister; d->dirModel = new KDirModel(); d->dirModel->setDirLister(d->dirLister); d->dirModel->setDropsAllowed(KDirModel::DropOnDirectory); d->shouldFetchForItems = qobject_cast(d->itemView); if (d->shouldFetchForItems) { connect(d->dirModel, SIGNAL(expand(QModelIndex)), this, SLOT(_k_slotExpandToUrl(QModelIndex))); } else { d->itemsToBeSetAsCurrent.clear(); } d->proxyModel = new KDirSortFilterProxyModel(this); d->proxyModel->setSourceModel(d->dirModel); d->dirLister->setDelayedMimeTypes(true); QWidget *mainWidget = topLevelWidget(); d->dirLister->setMainWindow(mainWidget); // qDebug() << "mainWidget=" << mainWidget; connect(d->dirLister, SIGNAL(percent(int)), SLOT(_k_slotProgress(int))); connect(d->dirLister, SIGNAL(started(QUrl)), SLOT(_k_slotStarted())); connect(d->dirLister, SIGNAL(completed()), SLOT(_k_slotIOFinished())); connect(d->dirLister, SIGNAL(canceled()), SLOT(_k_slotCanceled())); connect(d->dirLister, SIGNAL(redirection(QUrl)), SLOT(_k_slotRedirected(QUrl))); connect(d->dirLister, SIGNAL(newItems(KFileItemList)), SLOT(_k_slotItemsChanged())); connect(d->dirLister, SIGNAL(itemsDeleted(KFileItemList)), SLOT(_k_slotItemsChanged())); connect(d->dirLister, SIGNAL(itemsFilteredByMime(KFileItemList)), SLOT(_k_slotItemsChanged())); connect(d->dirLister, SIGNAL(clear()), SLOT(_k_slotItemsChanged())); } void KDirOperator::selectDir(const KFileItem &item) { setUrl(item.targetUrl(), true); } void KDirOperator::selectFile(const KFileItem &item) { QApplication::restoreOverrideCursor(); emit fileSelected(item); } void KDirOperator::highlightFile(const KFileItem &item) { if ((d->preview != nullptr && !d->preview->isHidden()) && !item.isNull()) { d->preview->showPreview(item.url()); } emit fileHighlighted(item); } void KDirOperator::setCurrentItem(const QUrl &url) { // qDebug(); KFileItem item = d->dirLister->findByUrl(url); if (d->shouldFetchForItems && item.isNull()) { d->itemsToBeSetAsCurrent << url; d->dirModel->expandToUrl(url); return; } setCurrentItem(item); } void KDirOperator::setCurrentItem(const KFileItem &item) { // qDebug(); if (!d->itemView) { return; } QItemSelectionModel *selModel = d->itemView->selectionModel(); if (selModel) { selModel->clear(); if (!item.isNull()) { const QModelIndex dirIndex = d->dirModel->indexForItem(item); const QModelIndex proxyIndex = d->proxyModel->mapFromSource(dirIndex); selModel->setCurrentIndex(proxyIndex, QItemSelectionModel::Select); } } } void KDirOperator::setCurrentItems(const QList &urls) { // qDebug(); if (!d->itemView) { return; } KFileItemList itemList; for (const QUrl &url : urls) { KFileItem item = d->dirLister->findByUrl(url); if (d->shouldFetchForItems && item.isNull()) { d->itemsToBeSetAsCurrent << url; d->dirModel->expandToUrl(url); continue; } itemList << item; } setCurrentItems(itemList); } void KDirOperator::setCurrentItems(const KFileItemList &items) { // qDebug(); if (d->itemView == nullptr) { return; } QItemSelectionModel *selModel = d->itemView->selectionModel(); if (selModel) { selModel->clear(); QModelIndex proxyIndex; for (const KFileItem &item : items) { if (!item.isNull()) { const QModelIndex dirIndex = d->dirModel->indexForItem(item); proxyIndex = d->proxyModel->mapFromSource(dirIndex); selModel->select(proxyIndex, QItemSelectionModel::Select); } } if (proxyIndex.isValid()) { selModel->setCurrentIndex(proxyIndex, QItemSelectionModel::NoUpdate); } } } QString KDirOperator::makeCompletion(const QString &string) { if (string.isEmpty()) { d->itemView->selectionModel()->clear(); return QString(); } prepareCompletionObjects(); return d->completion.makeCompletion(string); } QString KDirOperator::makeDirCompletion(const QString &string) { if (string.isEmpty()) { d->itemView->selectionModel()->clear(); return QString(); } prepareCompletionObjects(); return d->dirCompletion.makeCompletion(string); } void KDirOperator::prepareCompletionObjects() { if (d->itemView == nullptr) { return; } if (d->completeListDirty) { // create the list of all possible completions const KFileItemList itemList = d->dirLister->items(); for (const KFileItem &item : itemList) { d->completion.addItem(item.name()); if (item.isDir()) { d->dirCompletion.addItem(item.name()); } } d->completeListDirty = false; } } void KDirOperator::slotCompletionMatch(const QString &match) { QUrl url(match); if (url.isRelative()) { url = d->currUrl.resolved(url); } setCurrentItem(url); emit completion(match); } void KDirOperator::setupActions() { d->actionCollection = new KActionCollection(this); d->actionCollection->setObjectName(QStringLiteral("KDirOperator::actionCollection")); d->actionMenu = new KActionMenu(i18n("Menu"), this); d->actionCollection->addAction(QStringLiteral("popupMenu"), d->actionMenu); QAction *upAction = d->actionCollection->addAction(KStandardAction::Up, QStringLiteral("up"), this, SLOT(cdUp())); upAction->setText(i18n("Parent Folder")); d->actionCollection->addAction(KStandardAction::Back, QStringLiteral("back"), this, SLOT(back())); d->actionCollection->addAction(KStandardAction::Forward, QStringLiteral("forward"), this, SLOT(forward())); QAction *homeAction = d->actionCollection->addAction(KStandardAction::Home, QStringLiteral("home"), this, SLOT(home())); homeAction->setText(i18n("Home Folder")); QAction *reloadAction = d->actionCollection->addAction(KStandardAction::Redisplay, QStringLiteral("reload"), this, SLOT(rereadDir())); reloadAction->setText(i18n("Reload")); reloadAction->setShortcuts(KStandardShortcut::shortcut(KStandardShortcut::Reload)); QAction *mkdirAction = new QAction(i18n("New Folder..."), this); d->actionCollection->addAction(QStringLiteral("mkdir"), mkdirAction); mkdirAction->setIcon(QIcon::fromTheme(QStringLiteral("folder-new"))); connect(mkdirAction, SIGNAL(triggered(bool)), this, SLOT(mkdir())); QAction *trash = new QAction(i18n("Move to Trash"), this); d->actionCollection->addAction(QStringLiteral("trash"), trash); trash->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); trash->setShortcut(Qt::Key_Delete); connect(trash, &QAction::triggered, this, &KDirOperator::trashSelected); QAction *action = new QAction(i18n("Delete"), this); d->actionCollection->addAction(QStringLiteral("delete"), action); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); action->setShortcut(Qt::SHIFT + Qt::Key_Delete); connect(action, &QAction::triggered, this, &KDirOperator::deleteSelected); // the sort menu actions KActionMenu *sortMenu = new KActionMenu(i18n("Sorting"), this); sortMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-sort"))); sortMenu->setDelayed(false); d->actionCollection->addAction(QStringLiteral("sorting menu"), sortMenu); KToggleAction *byNameAction = new KToggleAction(i18n("Sort by Name"), this); d->actionCollection->addAction(QStringLiteral("by name"), byNameAction); connect(byNameAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotSortByName())); KToggleAction *bySizeAction = new KToggleAction(i18n("Sort by Size"), this); d->actionCollection->addAction(QStringLiteral("by size"), bySizeAction); connect(bySizeAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotSortBySize())); KToggleAction *byDateAction = new KToggleAction(i18n("Sort by Date"), this); d->actionCollection->addAction(QStringLiteral("by date"), byDateAction); connect(byDateAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotSortByDate())); KToggleAction *byTypeAction = new KToggleAction(i18n("Sort by Type"), this); d->actionCollection->addAction(QStringLiteral("by type"), byTypeAction); connect(byTypeAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotSortByType())); QActionGroup *sortOrderGroup = new QActionGroup(this); sortOrderGroup->setExclusive(true); KToggleAction *ascendingAction = new KToggleAction(i18n("Ascending"), this); d->actionCollection->addAction(QStringLiteral("ascending"), ascendingAction); ascendingAction->setActionGroup(sortOrderGroup); connect(ascendingAction, &QAction::triggered, this, [this]() { this->d->_k_slotSortReversed(false); }); KToggleAction *descendingAction = new KToggleAction(i18n("Descending"), this); d->actionCollection->addAction(QStringLiteral("descending"), descendingAction); descendingAction->setActionGroup(sortOrderGroup); connect(descendingAction, &QAction::triggered, this, [this]() { this->d->_k_slotSortReversed(true); }); KToggleAction *dirsFirstAction = new KToggleAction(i18n("Folders First"), this); d->actionCollection->addAction(QStringLiteral("dirs first"), dirsFirstAction); connect(dirsFirstAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotToggleDirsFirst())); // View modes that match those of Dolphin KToggleAction *iconsViewAction = new KToggleAction(i18n("Icons View"), this); iconsViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-list-icons"))); d->actionCollection->addAction(QStringLiteral("icons view"), iconsViewAction); connect(iconsViewAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotIconsView())); KToggleAction *compactViewAction = new KToggleAction(i18n("Compact View"), this); compactViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-list-details"))); d->actionCollection->addAction(QStringLiteral("compact view"), compactViewAction); connect(compactViewAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotCompactView())); KToggleAction *detailsViewAction = new KToggleAction(i18n("Details View"), this); detailsViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-list-tree"))); d->actionCollection->addAction(QStringLiteral("details view"), detailsViewAction); connect(detailsViewAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotDetailsView())); QActionGroup *viewModeGroup = new QActionGroup(this); viewModeGroup->setExclusive(true); iconsViewAction->setActionGroup(viewModeGroup); compactViewAction->setActionGroup(viewModeGroup); detailsViewAction->setActionGroup(viewModeGroup); QActionGroup *sortGroup = new QActionGroup(this); byNameAction->setActionGroup(sortGroup); bySizeAction->setActionGroup(sortGroup); byDateAction->setActionGroup(sortGroup); byTypeAction->setActionGroup(sortGroup); d->decorationMenu = new KActionMenu(i18n("Icon Position"), this); d->actionCollection->addAction(QStringLiteral("decoration menu"), d->decorationMenu); d->leftAction = new KToggleAction(i18n("Next to File Name"), this); d->actionCollection->addAction(QStringLiteral("decorationAtLeft"), d->leftAction); connect(d->leftAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotChangeDecorationPosition())); KToggleAction *topAction = new KToggleAction(i18n("Above File Name"), this); d->actionCollection->addAction(QStringLiteral("decorationAtTop"), topAction); connect(topAction, SIGNAL(triggered(bool)), this, SLOT(_k_slotChangeDecorationPosition())); d->decorationMenu->addAction(d->leftAction); d->decorationMenu->addAction(topAction); QActionGroup *decorationGroup = new QActionGroup(this); decorationGroup->setExclusive(true); d->leftAction->setActionGroup(decorationGroup); topAction->setActionGroup(decorationGroup); KToggleAction *shortAction = new KToggleAction(i18n("Short View"), this); d->actionCollection->addAction(QStringLiteral("short view"), shortAction); shortAction->setIcon(QIcon::fromTheme(QStringLiteral("view-list-icons"))); connect(shortAction, SIGNAL(triggered()), SLOT(_k_slotSimpleView())); KToggleAction *detailedAction = new KToggleAction(i18n("Detailed View"), this); d->actionCollection->addAction(QStringLiteral("detailed view"), detailedAction); detailedAction->setIcon(QIcon::fromTheme(QStringLiteral("view-list-details"))); connect(detailedAction, SIGNAL(triggered()), SLOT(_k_slotDetailedView())); KToggleAction *treeAction = new KToggleAction(i18n("Tree View"), this); d->actionCollection->addAction(QStringLiteral("tree view"), treeAction); treeAction->setIcon(QIcon::fromTheme(QStringLiteral("view-list-tree"))); connect(treeAction, SIGNAL(triggered()), SLOT(_k_slotTreeView())); KToggleAction *detailedTreeAction = new KToggleAction(i18n("Detailed Tree View"), this); d->actionCollection->addAction(QStringLiteral("detailed tree view"), detailedTreeAction); detailedTreeAction->setIcon(QIcon::fromTheme(QStringLiteral("view-list-tree"))); connect(detailedTreeAction, SIGNAL(triggered()), SLOT(_k_slotDetailedTreeView())); QActionGroup *viewGroup = new QActionGroup(this); shortAction->setActionGroup(viewGroup); detailedAction->setActionGroup(viewGroup); treeAction->setActionGroup(viewGroup); detailedTreeAction->setActionGroup(viewGroup); KToggleAction *allowExpansionAction = new KToggleAction(i18n("Allow Expansion in Details View"), this); d->actionCollection->addAction(QStringLiteral("allow expansion"), allowExpansionAction); connect(allowExpansionAction, SIGNAL(toggled(bool)), SLOT(_k_slotToggleAllowExpansion(bool))); KToggleAction *showHiddenAction = new KToggleAction(i18n("Show Hidden Files"), this); d->actionCollection->addAction(QStringLiteral("show hidden"), showHiddenAction); showHiddenAction->setShortcuts({Qt::ALT + Qt::Key_Period, Qt::CTRL + Qt::Key_H, Qt::Key_F8}); connect(showHiddenAction, SIGNAL(toggled(bool)), SLOT(_k_slotToggleHidden(bool))); KToggleAction *previewAction = new KToggleAction(i18n("Show Preview Panel"), this); d->actionCollection->addAction(QStringLiteral("preview"), previewAction); previewAction->setShortcut(Qt::Key_F11); connect(previewAction, SIGNAL(toggled(bool)), SLOT(_k_togglePreview(bool))); KToggleAction *inlinePreview = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-preview")), i18n("Show Preview"), this); d->actionCollection->addAction(QStringLiteral("inline preview"), inlinePreview); inlinePreview->setShortcut(Qt::Key_F12); connect(inlinePreview, SIGNAL(toggled(bool)), SLOT(_k_toggleInlinePreviews(bool))); QAction *fileManager = new QAction(i18n("Open Containing Folder"), this); d->actionCollection->addAction(QStringLiteral("file manager"), fileManager); fileManager->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); connect(fileManager, SIGNAL(triggered()), SLOT(_k_slotOpenFileManager())); action = new QAction(i18n("Properties"), this); d->actionCollection->addAction(QStringLiteral("properties"), action); action->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); action->setShortcut(Qt::ALT + Qt::Key_Return); connect(action, SIGNAL(triggered(bool)), this, SLOT(_k_slotProperties())); // the view menu actions KActionMenu *viewMenu = new KActionMenu(i18n("&View"), this); d->actionCollection->addAction(QStringLiteral("view menu"), viewMenu); viewMenu->addAction(shortAction); viewMenu->addAction(detailedAction); // Comment following lines to hide the extra two modes viewMenu->addAction(treeAction); viewMenu->addAction(detailedTreeAction); // TODO: QAbstractItemView does not offer an action collection. Provide // an interface to add a custom action collection. d->newFileMenu = new KNewFileMenu(d->actionCollection, QStringLiteral("new"), this); connect(d->newFileMenu, SIGNAL(directoryCreated(QUrl)), this, SLOT(_k_slotDirectoryCreated(QUrl))); d->actionCollection->addAssociatedWidget(this); foreach (QAction *action, d->actionCollection->actions()) { action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } } void KDirOperator::setupMenu() { setupMenu(SortActions | ViewActions | FileActions); } void KDirOperator::setupMenu(int whichActions) { // first fill the submenus (sort and view) KActionMenu *sortMenu = static_cast(d->actionCollection->action(QStringLiteral("sorting menu"))); sortMenu->menu()->clear(); sortMenu->addAction(d->actionCollection->action(QStringLiteral("by name"))); sortMenu->addAction(d->actionCollection->action(QStringLiteral("by size"))); sortMenu->addAction(d->actionCollection->action(QStringLiteral("by date"))); sortMenu->addAction(d->actionCollection->action(QStringLiteral("by type"))); sortMenu->addSeparator(); sortMenu->addAction(d->actionCollection->action(QStringLiteral("ascending"))); sortMenu->addAction(d->actionCollection->action(QStringLiteral("descending"))); sortMenu->addSeparator(); sortMenu->addAction(d->actionCollection->action(QStringLiteral("dirs first"))); // now plug everything into the popupmenu d->actionMenu->menu()->clear(); if (whichActions & NavActions) { d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("up"))); d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("back"))); d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("forward"))); d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("home"))); d->actionMenu->addSeparator(); } if (whichActions & FileActions) { d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("new"))); if (d->currUrl.isLocalFile() && !(QApplication::keyboardModifiers() & Qt::ShiftModifier)) { d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("trash"))); } KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KDE")); const bool del = !d->currUrl.isLocalFile() || (QApplication::keyboardModifiers() & Qt::ShiftModifier) || cg.readEntry("ShowDeleteCommand", false); if (del) { d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("delete"))); } d->actionMenu->addSeparator(); } if (whichActions & SortActions) { d->actionMenu->addAction(sortMenu); if (!(whichActions & ViewActions)) { d->actionMenu->addSeparator(); } } if (whichActions & ViewActions) { d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("view menu"))); d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("reload"))); d->actionMenu->addSeparator(); } if (whichActions & FileActions) { d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("file manager"))); d->actionMenu->addAction(d->actionCollection->action(QStringLiteral("properties"))); } } void KDirOperator::updateSortActions() { QAction *ascending = d->actionCollection->action(QStringLiteral("ascending")); QAction *descending = d->actionCollection->action(QStringLiteral("descending")); if (KFile::isSortByName(d->sorting)) { d->actionCollection->action(QStringLiteral("by name"))->setChecked(true); descending->setText(i18nc("Sort descending", "Z-A")); ascending->setText(i18nc("Sort ascending", "A-Z")); } else if (KFile::isSortByDate(d->sorting)) { d->actionCollection->action(QStringLiteral("by date"))->setChecked(true); descending->setText(i18nc("Sort descending", "Newest First")); ascending->setText(i18nc("Sort ascending", "Oldest First")); } else if (KFile::isSortBySize(d->sorting)) { d->actionCollection->action(QStringLiteral("by size"))->setChecked(true); descending->setText(i18nc("Sort descending", "Largest First")); ascending->setText(i18nc("Sort ascending", "Smallest First")); } else if (KFile::isSortByType(d->sorting)) { d->actionCollection->action(QStringLiteral("by type"))->setChecked(true); descending->setText(i18nc("Sort descending", "Z-A")); ascending->setText(i18nc("Sort ascending", "A-Z")); } ascending->setChecked(!(d->sorting & QDir::Reversed)); descending->setChecked(d->sorting & QDir::Reversed); d->actionCollection->action(QStringLiteral("dirs first"))->setChecked(d->sorting & QDir::DirsFirst); } void KDirOperator::updateViewActions() { KFile::FileView fv = static_cast(d->viewKind); //QAction *separateDirs = d->actionCollection->action("separate dirs"); //separateDirs->setChecked(KFile::isSeparateDirs(fv) && // separateDirs->isEnabled()); d->actionCollection->action(QStringLiteral("short view"))->setChecked(KFile::isSimpleView(fv)); d->actionCollection->action(QStringLiteral("detailed view"))->setChecked(KFile::isDetailView(fv)); d->actionCollection->action(QStringLiteral("tree view"))->setChecked(KFile::isTreeView(fv)); d->actionCollection->action(QStringLiteral("detailed tree view"))->setChecked(KFile::isDetailTreeView(fv)); // dolphin style views d->actionCollection->action(QStringLiteral("icons view"))->setChecked(KFile::isSimpleView(fv) && d->decorationPosition == QStyleOptionViewItem::Top); d->actionCollection->action(QStringLiteral("compact view"))->setChecked(KFile::isSimpleView(fv) && d->decorationPosition == QStyleOptionViewItem::Left); d->actionCollection->action(QStringLiteral("details view"))->setChecked(KFile::isDetailTreeView(fv) || KFile::isDetailView(fv)); } void KDirOperator::readConfig(const KConfigGroup &configGroup) { d->defaultView = 0; QString viewStyle = configGroup.readEntry("View Style", "DetailTree"); if (viewStyle == QLatin1String("Detail")) { d->defaultView |= KFile::Detail; } else if (viewStyle == QLatin1String("Tree")) { d->defaultView |= KFile::Tree; } else if (viewStyle == QLatin1String("DetailTree")) { d->defaultView |= KFile::DetailTree; } else { d->defaultView |= KFile::Simple; } //if (configGroup.readEntry(QLatin1String("Separate Directories"), // DefaultMixDirsAndFiles)) { // d->defaultView |= KFile::SeparateDirs; //} if (configGroup.readEntry(QStringLiteral("Show Preview"), false)) { d->defaultView |= KFile::PreviewContents; } d->previewWidth = configGroup.readEntry(QStringLiteral("Preview Width"), 100); if (configGroup.readEntry(QStringLiteral("Show hidden files"), DefaultShowHidden)) { d->actionCollection->action(QStringLiteral("show hidden"))->setChecked(true); d->dirLister->setShowingDotFiles(true); } if (configGroup.readEntry(QStringLiteral("Allow Expansion"), DefaultShowHidden)) { d->actionCollection->action(QStringLiteral("allow expansion"))->setChecked(true); } QDir::SortFlags sorting = QDir::Name; if (configGroup.readEntry(QStringLiteral("Sort directories first"), DefaultDirsFirst)) { sorting |= QDir::DirsFirst; } QString name = QStringLiteral("Name"); QString sortBy = configGroup.readEntry(QStringLiteral("Sort by"), name); if (sortBy == name) { sorting |= QDir::Name; } else if (sortBy == QLatin1String("Size")) { sorting |= QDir::Size; } else if (sortBy == QLatin1String("Date")) { sorting |= QDir::Time; } else if (sortBy == QLatin1String("Type")) { sorting |= QDir::Type; } if (configGroup.readEntry(QStringLiteral("Sort reversed"), DefaultSortReversed)) { sorting |= QDir::Reversed; } d->updateSorting(sorting); if (d->inlinePreviewState == Private::NotForced) { d->showPreviews = configGroup.readEntry(QStringLiteral("Show Inline Previews"), true); } QStyleOptionViewItem::Position pos = (QStyleOptionViewItem::Position) configGroup.readEntry(QStringLiteral("Decoration position"), (int) QStyleOptionViewItem::Top); setDecorationPosition(pos); } void KDirOperator::writeConfig(KConfigGroup &configGroup) { QString sortBy = QStringLiteral("Name"); if (KFile::isSortBySize(d->sorting)) { sortBy = QStringLiteral("Size"); } else if (KFile::isSortByDate(d->sorting)) { sortBy = QStringLiteral("Date"); } else if (KFile::isSortByType(d->sorting)) { sortBy = QStringLiteral("Type"); } configGroup.writeEntry(QStringLiteral("Sort by"), sortBy); configGroup.writeEntry(QStringLiteral("Sort reversed"), d->actionCollection->action(QStringLiteral("descending"))->isChecked()); configGroup.writeEntry(QStringLiteral("Sort directories first"), d->actionCollection->action(QStringLiteral("dirs first"))->isChecked()); // don't save the preview when an application specific preview is in use. bool appSpecificPreview = false; if (d->preview) { KFileMetaPreview *tmp = dynamic_cast(d->preview); appSpecificPreview = (tmp == nullptr); } if (!appSpecificPreview) { KToggleAction *previewAction = static_cast(d->actionCollection->action(QStringLiteral("preview"))); if (previewAction->isEnabled()) { bool hasPreview = previewAction->isChecked(); configGroup.writeEntry(QStringLiteral("Show Preview"), hasPreview); if (hasPreview) { // remember the width of the preview widget QList sizes = d->splitter->sizes(); Q_ASSERT(sizes.count() == 2); configGroup.writeEntry(QStringLiteral("Preview Width"), sizes[1]); } } } configGroup.writeEntry(QStringLiteral("Show hidden files"), d->actionCollection->action(QStringLiteral("show hidden"))->isChecked()); configGroup.writeEntry(QStringLiteral("Allow Expansion"), d->actionCollection->action(QStringLiteral("allow expansion"))->isChecked()); KFile::FileView fv = static_cast(d->viewKind); QString style; if (KFile::isDetailView(fv)) { style = QStringLiteral("Detail"); } else if (KFile::isSimpleView(fv)) { style = QStringLiteral("Simple"); } else if (KFile::isTreeView(fv)) { style = QStringLiteral("Tree"); } else if (KFile::isDetailTreeView(fv)) { style = QStringLiteral("DetailTree"); } configGroup.writeEntry(QStringLiteral("View Style"), style); if (d->inlinePreviewState == Private::NotForced) { configGroup.writeEntry(QStringLiteral("Show Inline Previews"), d->showPreviews); d->writeIconZoomSettingsIfNeeded(); } configGroup.writeEntry(QStringLiteral("Decoration position"), (int) d->decorationPosition); } void KDirOperator::Private::writeIconZoomSettingsIfNeeded() { // must match behavior of iconSizeForViewType if (configGroup && itemView) { ZoomSettingsForView zoomSettings = zoomSettingsForViewForView(); configGroup->writeEntry(zoomSettings.name, iconsZoom); } } void KDirOperator::resizeEvent(QResizeEvent *) { // resize the splitter and assure that the width of // the preview widget is restored QList sizes = d->splitter->sizes(); const bool hasPreview = (sizes.count() == 2); d->splitter->resize(size()); sizes = d->splitter->sizes(); const bool restorePreviewWidth = hasPreview && (d->previewWidth != sizes[1]); if (restorePreviewWidth) { const int availableWidth = sizes[0] + sizes[1]; sizes[0] = availableWidth - d->previewWidth; sizes[1] = d->previewWidth; d->splitter->setSizes(sizes); } if (hasPreview) { d->previewWidth = sizes[1]; } if (d->progressBar->parent() == this) { // might be reparented into a statusbar d->progressBar->move(2, height() - d->progressBar->height() - 2); } } void KDirOperator::setOnlyDoubleClickSelectsFiles(bool enable) { d->onlyDoubleClickSelectsFiles = enable; // TODO: port to QAbstractItemModel //if (d->itemView != 0) { // d->itemView->setOnlyDoubleClickSelectsFiles(enable); //} } bool KDirOperator::onlyDoubleClickSelectsFiles() const { return d->onlyDoubleClickSelectsFiles; } void KDirOperator::setFollowNewDirectories(bool enable) { d->followNewDirectories = enable; } bool KDirOperator::followNewDirectories() const { return d->followNewDirectories; } void KDirOperator::setFollowSelectedDirectories(bool enable) { d->followSelectedDirectories = enable; } bool KDirOperator::followSelectedDirectories() const { return d->followSelectedDirectories; } void KDirOperator::Private::_k_slotStarted() { progressBar->setValue(0); // delay showing the progressbar for one second progressDelayTimer->setSingleShot(true); progressDelayTimer->start(1000); } void KDirOperator::Private::_k_slotShowProgress() { progressBar->raise(); progressBar->show(); - QApplication::flush(); } void KDirOperator::Private::_k_slotProgress(int percent) { progressBar->setValue(percent); - // we have to redraw this as fast as possible - if (progressBar->isVisible()) { - QApplication::flush(); - } } void KDirOperator::Private::_k_slotIOFinished() { progressDelayTimer->stop(); _k_slotProgress(100); progressBar->hide(); emit parent->finishedLoading(); parent->resetCursor(); if (preview) { preview->clearPreview(); } } void KDirOperator::Private::_k_slotCanceled() { emit parent->finishedLoading(); parent->resetCursor(); } QProgressBar *KDirOperator::progressBar() const { return d->progressBar; } void KDirOperator::clearHistory() { qDeleteAll(d->backStack); d->backStack.clear(); d->actionCollection->action(QStringLiteral("back"))->setEnabled(false); qDeleteAll(d->forwardStack); d->forwardStack.clear(); d->actionCollection->action(QStringLiteral("forward"))->setEnabled(false); } void KDirOperator::setEnableDirHighlighting(bool enable) { d->dirHighlighting = enable; } bool KDirOperator::dirHighlighting() const { return d->dirHighlighting; } bool KDirOperator::dirOnlyMode() const { return dirOnlyMode(d->mode); } bool KDirOperator::dirOnlyMode(uint mode) { return ((mode & KFile::Directory) && (mode & (KFile::File | KFile::Files)) == 0); } void KDirOperator::Private::_k_slotProperties() { if (itemView == nullptr) { return; } const KFileItemList list = parent->selectedItems(); if (!list.isEmpty()) { KPropertiesDialog dialog(list, parent); dialog.exec(); } } void KDirOperator::Private::_k_slotActivated(const QModelIndex &index) { const QModelIndex dirIndex = proxyModel->mapToSource(index); KFileItem item = dirModel->itemForIndex(dirIndex); const Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); if (item.isNull() || (modifiers & Qt::ShiftModifier) || (modifiers & Qt::ControlModifier)) { return; } if (item.isDir()) { // Only allow disabling following selected directories on Tree and // DetailTree views as selected directories in these views still expand // when selected. For other views, disabling following selected // directories would make selecting a directory a noop which is // unintuitive. if (followSelectedDirectories || (viewKind != KFile::Tree && viewKind != KFile::DetailTree)) { parent->selectDir(item); } } else { parent->selectFile(item); } } void KDirOperator::Private::_k_slotSelectionChanged() { if (itemView == nullptr) { return; } // In the multiselection mode each selection change is indicated by // emitting a null item. Also when the selection has been cleared, a // null item must be emitted. const bool multiSelectionMode = (itemView->selectionMode() == QAbstractItemView::ExtendedSelection); const bool hasSelection = itemView->selectionModel()->hasSelection(); if (multiSelectionMode || !hasSelection) { KFileItem nullItem; parent->highlightFile(nullItem); } else { const KFileItem selectedItem = parent->selectedItems().constFirst(); parent->highlightFile(selectedItem); } } void KDirOperator::Private::_k_openContextMenu(const QPoint &pos) { const QModelIndex proxyIndex = itemView->indexAt(pos); const QModelIndex dirIndex = proxyModel->mapToSource(proxyIndex); KFileItem item = dirModel->itemForIndex(dirIndex); if (item.isNull()) { return; } parent->activatedMenu(item, QCursor::pos()); } void KDirOperator::Private::_k_triggerPreview(const QModelIndex &index) { if ((preview != nullptr && !preview->isHidden()) && index.isValid() && (index.column() == KDirModel::Name)) { const QModelIndex dirIndex = proxyModel->mapToSource(index); const KFileItem item = dirModel->itemForIndex(dirIndex); if (item.isNull()) { return; } if (!item.isDir()) { previewUrl = item.url(); _k_showPreview(); } else { preview->clearPreview(); } } } void KDirOperator::Private::_k_showPreview() { if (preview != nullptr) { preview->showPreview(previewUrl); } } void KDirOperator::Private::_k_slotSplitterMoved(int, int) { const QList sizes = splitter->sizes(); if (sizes.count() == 2) { // remember the width of the preview widget (see KDirOperator::resizeEvent()) previewWidth = sizes[1]; } } void KDirOperator::Private::_k_assureVisibleSelection() { if (itemView == nullptr) { return; } QItemSelectionModel *selModel = itemView->selectionModel(); if (selModel->hasSelection()) { const QModelIndex index = selModel->currentIndex(); itemView->scrollTo(index, QAbstractItemView::EnsureVisible); _k_triggerPreview(index); } } void KDirOperator::Private::_k_synchronizeSortingState(int logicalIndex, Qt::SortOrder order) { QDir::SortFlags newSort = sorting & ~(QDirSortMask | QDir::Reversed); switch (logicalIndex) { case KDirModel::Name: newSort |= QDir::Name; break; case KDirModel::Size: newSort |= QDir::Size; break; case KDirModel::ModifiedTime: newSort |= QDir::Time; break; case KDirModel::Type: newSort |= QDir::Type; break; default: Q_ASSERT(false); } if (order == Qt::DescendingOrder) { newSort |= QDir::Reversed; } updateSorting(newSort); QMetaObject::invokeMethod(parent, "_k_assureVisibleSelection", Qt::QueuedConnection); } void KDirOperator::Private::_k_slotChangeDecorationPosition() { if (!itemView) { return; } KDirOperatorIconView *view = qobject_cast(itemView); if (!view) { return; } const bool leftChecked = actionCollection->action(QStringLiteral("decorationAtLeft"))->isChecked(); if (leftChecked) { view->setDecorationPosition(QStyleOptionViewItem::Left); } else { view->setDecorationPosition(QStyleOptionViewItem::Top); } itemView->update(); } void KDirOperator::Private::_k_slotExpandToUrl(const QModelIndex &index) { QTreeView *treeView = qobject_cast(itemView); if (!treeView) { return; } const KFileItem item = dirModel->itemForIndex(index); if (item.isNull()) { return; } if (!item.isDir()) { const QModelIndex proxyIndex = proxyModel->mapFromSource(index); QList::Iterator it = itemsToBeSetAsCurrent.begin(); while (it != itemsToBeSetAsCurrent.end()) { const QUrl url = *it; if (url.matches(item.url(), QUrl::StripTrailingSlash) || url.isParentOf(item.url())) { const KFileItem _item = dirLister->findByUrl(url); if (!_item.isNull() && _item.isDir()) { const QModelIndex _index = dirModel->indexForItem(_item); const QModelIndex _proxyIndex = proxyModel->mapFromSource(_index); treeView->expand(_proxyIndex); // if we have expanded the last parent of this item, select it if (item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash) == url.adjusted(QUrl::StripTrailingSlash)) { treeView->selectionModel()->select(proxyIndex, QItemSelectionModel::Select); } } it = itemsToBeSetAsCurrent.erase(it); } else { ++it; } } } else if (!itemsToBeSetAsCurrent.contains(item.url())) { itemsToBeSetAsCurrent << item.url(); } } void KDirOperator::Private::_k_slotItemsChanged() { completeListDirty = true; } int KDirOperator::Private::iconSizeForViewType(QAbstractItemView *itemView) const { // must match behavior of writeIconZoomSettingsIfNeeded if (!itemView || !configGroup) { return 0; } ZoomSettingsForView ZoomSettingsForView = zoomSettingsForViewForView(); return configGroup->readEntry(ZoomSettingsForView.name, ZoomSettingsForView.defaultValue); } KDirOperator::Private::ZoomSettingsForView KDirOperator::Private::zoomSettingsForViewForView() const { KFile::FileView fv = static_cast(viewKind); if (KFile::isSimpleView(fv)) { if (decorationPosition == QStyleOptionViewItem::Top){ // Simple view decoration above, aka Icons View // default to 43% aka 64px return {QStringLiteral("iconViewIconSize"), 43}; } else { // Simple view decoration left, aka compact view // default to 15% aka 32px return {QStringLiteral("listViewIconSize"), 15}; } } else { if (KFile::isTreeView(fv)) { return {QStringLiteral("treeViewIconSize"), 0}; } else { // DetailView and DetailTreeView return {QStringLiteral("detailViewIconSize"), 0}; } } } void KDirOperator::setViewConfig(KConfigGroup &configGroup) { delete d->configGroup; d->configGroup = new KConfigGroup(configGroup); } KConfigGroup *KDirOperator::viewConfigGroup() const { return d->configGroup; } void KDirOperator::setShowHiddenFiles(bool s) { d->actionCollection->action(QStringLiteral("show hidden"))->setChecked(s); } bool KDirOperator::showHiddenFiles() const { return d->actionCollection->action(QStringLiteral("show hidden"))->isChecked(); } QStyleOptionViewItem::Position KDirOperator::decorationPosition() const { return d->decorationPosition; } void KDirOperator::setDecorationPosition(QStyleOptionViewItem::Position position) { d->decorationPosition = position; const bool decorationAtLeft = d->decorationPosition == QStyleOptionViewItem::Left; d->actionCollection->action(QStringLiteral("decorationAtLeft"))->setChecked(decorationAtLeft); d->actionCollection->action(QStringLiteral("decorationAtTop"))->setChecked(!decorationAtLeft); } bool KDirOperator::Private::isReadable(const QUrl &url) { if (!url.isLocalFile()) { return true; // what else can we say? } return QDir(url.toLocalFile()).isReadable(); } void KDirOperator::Private::_k_slotDirectoryCreated(const QUrl &url) { if (followNewDirectories) { parent->setUrl(url, true); } } void KDirOperator::setSupportedSchemes(const QStringList &schemes) { d->supportedSchemes = schemes; rereadDir(); } QStringList KDirOperator::supportedSchemes() const { return d->supportedSchemes; } #include "moc_kdiroperator.cpp" diff --git a/src/filewidgets/kdiroperatoriconview.cpp b/src/filewidgets/kdiroperatoriconview.cpp index f55d1881..e9ad0760 100644 --- a/src/filewidgets/kdiroperatoriconview.cpp +++ b/src/filewidgets/kdiroperatoriconview.cpp @@ -1,150 +1,162 @@ /***************************************************************************** * Copyright (C) 2007 by Peter Penz * * Copyright (C) 2019 by Méven Car * * 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 "kdiroperatoriconview_p.h" #include #include #include #include #include #include KDirOperatorIconView::KDirOperatorIconView(QWidget *parent, QStyleOptionViewItem::Position aDecorationPosition) : QListView(parent) { setViewMode(QListView::IconMode); setResizeMode(QListView::Adjust); setSpacing(0); setMovement(QListView::Static); setDragDropMode(QListView::DragOnly); setVerticalScrollMode(QListView::ScrollPerPixel); setHorizontalScrollMode(QListView::ScrollPerPixel); setEditTriggers(QAbstractItemView::NoEditTriggers); setWordWrap(true); setIconSize(QSize(KIconLoader::SizeSmall, KIconLoader::SizeSmall)); decorationPosition = aDecorationPosition; updateLayout(); connect(this, &QListView::iconSizeChanged, this, &KDirOperatorIconView::updateLayout); } KDirOperatorIconView::~KDirOperatorIconView() { } void KDirOperatorIconView::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); updateLayout(); } QStyleOptionViewItem KDirOperatorIconView::viewOptions() const { QStyleOptionViewItem viewOptions = QListView::viewOptions(); viewOptions.showDecorationSelected = true; viewOptions.textElideMode = Qt::ElideMiddle; viewOptions.decorationPosition = decorationPosition; if (viewOptions.decorationPosition == QStyleOptionViewItem::Left) { viewOptions.displayAlignment = Qt::AlignLeft | Qt::AlignVCenter; } else { viewOptions.displayAlignment = Qt::AlignCenter; } return viewOptions; } void KDirOperatorIconView::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls()) { event->acceptProposedAction(); } } void KDirOperatorIconView::mousePressEvent(QMouseEvent *event) { if (!indexAt(event->pos()).isValid()) { const Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); if (!(modifiers & Qt::ShiftModifier) && !(modifiers & Qt::ControlModifier)) { clearSelection(); } } QListView::mousePressEvent(event); } void KDirOperatorIconView::wheelEvent(QWheelEvent *event) { QListView::wheelEvent(event); // apply the vertical wheel event to the horizontal scrollbar, as // the items are aligned from left to right - if (event->orientation() == Qt::Vertical) { + if (event->angleDelta().y() != 0) { +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) QWheelEvent horizEvent(event->pos(), event->angleDelta().y(), event->buttons(), event->modifiers(), Qt::Horizontal); +#else + QWheelEvent horizEvent(event->position(), + event->globalPosition(), + QPoint(event->pixelDelta().y(), 0), + QPoint(event->angleDelta().y(), 0), + event->buttons(), + event->modifiers(), + event->phase(), + event->inverted(), + event->source()); +#endif QApplication::sendEvent(horizontalScrollBar(), &horizEvent); } } void KDirOperatorIconView::updateLayout() { if (decorationPosition == QStyleOptionViewItem::Position::Top) { // Icons view setFlow(QListView::LeftToRight); const QFontMetrics metrics(viewport()->font()); const int height = iconSize().height() + metrics.height() * 2.5; const int minWidth = qMax(height, metrics.height() * 5); const int scrollBarWidth = verticalScrollBar()->sizeHint().width(); // Subtract 1 px to prevent flickering when resizing the window // For Oxygen a column is missing after showing the dialog without resizing it, // therefore subtract 4 more (scaled) pixels const int viewPortWidth = contentsRect().width() - scrollBarWidth - 1 - 4 * devicePixelRatioF(); const int itemsInRow = qMax(1, viewPortWidth / minWidth); const int remainingWidth = viewPortWidth - (minWidth * itemsInRow); const int width = minWidth + (remainingWidth / itemsInRow); const QSize itemSize(width, height); setGridSize(itemSize); KFileItemDelegate *delegate = qobject_cast(itemDelegate()); if (delegate) { delegate->setMaximumSize(itemSize); } } else { // compact view setFlow(QListView::TopToBottom); setGridSize(QSize()); KFileItemDelegate *delegate = qobject_cast(itemDelegate()); if (delegate) { delegate->setMaximumSize(QSize()); } } } void KDirOperatorIconView::setDecorationPosition(QStyleOptionViewItem::Position newDecorationPosition) { decorationPosition = newDecorationPosition; updateLayout(); } diff --git a/src/filewidgets/kfileplacesview.cpp b/src/filewidgets/kfileplacesview.cpp index de758836..86e0d3dd 100644 --- a/src/filewidgets/kfileplacesview.cpp +++ b/src/filewidgets/kfileplacesview.cpp @@ -1,1518 +1,1518 @@ /* This file is part of the KDE project Copyright (C) 2007 Kevin Ottens Copyright (C) 2008 Rafael Fernández López This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License 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 "kfileplacesview.h" #include "kfileplacesview_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kfileplaceeditdialog.h" #include "kfileplacesmodel.h" #define LATERAL_MARGIN 4 #define CAPACITYBAR_HEIGHT 6 struct PlaceFreeSpaceInfo { QDateTime lastUpdated; KIO::filesize_t used = 0; KIO::filesize_t size = 0; QPointer job; }; class KFilePlacesViewDelegate : public QAbstractItemDelegate { Q_OBJECT public: explicit KFilePlacesViewDelegate(KFilePlacesView *parent); ~KFilePlacesViewDelegate() override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; int iconSize() const; void setIconSize(int newSize); void addAppearingItem(const QModelIndex &index); void setAppearingItemProgress(qreal value); void addDisappearingItem(const QModelIndex &index); void addDisappearingItemGroup(const QModelIndex &index); void setDisappearingItemProgress(qreal value); void setShowHoverIndication(bool show); void addFadeAnimation(const QModelIndex &index, QTimeLine *timeLine); void removeFadeAnimation(const QModelIndex &index); QModelIndex indexForFadeAnimation(QTimeLine *timeLine) const; QTimeLine *fadeAnimationForIndex(const QModelIndex &index) const; qreal contentsOpacity(const QModelIndex &index) const; bool pointIsHeaderArea(const QPoint &pos); void startDrag(); int sectionHeaderHeight() const; void clearFreeSpaceInfo(); private: QString groupNameFromIndex(const QModelIndex &index) const; QModelIndex previousVisibleIndex(const QModelIndex &index) const; bool indexIsSectionHeader(const QModelIndex &index) const; void drawSectionHeader(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; QColor textColor(const QStyleOption &option) const; QColor baseColor(const QStyleOption &option) const; QColor mixedColor(const QColor &c1, const QColor &c2, int c1Percent) const; KFilePlacesView *m_view; int m_iconSize; QList m_appearingItems; int m_appearingIconSize; qreal m_appearingOpacity; QList m_disappearingItems; int m_disappearingIconSize; qreal m_disappearingOpacity; bool m_showHoverIndication; mutable bool m_dragStarted; QMap m_timeLineMap; QMap m_timeLineInverseMap; mutable QMap m_freeSpaceInfo; }; KFilePlacesViewDelegate::KFilePlacesViewDelegate(KFilePlacesView *parent) : QAbstractItemDelegate(parent), m_view(parent), m_iconSize(48), m_appearingIconSize(0), m_appearingOpacity(0.0), m_disappearingIconSize(0), m_disappearingOpacity(0.0), m_showHoverIndication(true), m_dragStarted(false) { } KFilePlacesViewDelegate::~KFilePlacesViewDelegate() { } QSize KFilePlacesViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { int iconSize = m_iconSize; if (m_appearingItems.contains(index)) { iconSize = m_appearingIconSize; } else if (m_disappearingItems.contains(index)) { iconSize = m_disappearingIconSize; } int height = option.fontMetrics.height() / 2 + qMax(iconSize, option.fontMetrics.height()); if (indexIsSectionHeader(index)) { height += sectionHeaderHeight(); } return QSize(option.rect.width(), height); } void KFilePlacesViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); QStyleOptionViewItem opt = option; // draw header when necessary if (indexIsSectionHeader(index)) { // If we are drawing the floating element used by drag/drop, do not draw the header if (!m_dragStarted) { drawSectionHeader(painter, opt, index); } // Move the target rect to the actual item rect const int headerHeight = sectionHeaderHeight(); opt.rect.translate(0, headerHeight); opt.rect.setHeight(opt.rect.height() - headerHeight); } m_dragStarted = false; // draw item if (m_appearingItems.contains(index)) { painter->setOpacity(m_appearingOpacity); } else if (m_disappearingItems.contains(index)) { painter->setOpacity(m_disappearingOpacity); } if (!m_showHoverIndication) { opt.state &= ~QStyle::State_MouseOver; } QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter); const KFilePlacesModel *placesModel = static_cast(index.model()); bool isLTR = opt.direction == Qt::LeftToRight; QIcon icon = index.model()->data(index, Qt::DecorationRole).value(); QPixmap pm = icon.pixmap(m_iconSize, m_iconSize, (opt.state & QStyle::State_Selected) && (opt.state & QStyle::State_Active) ? QIcon::Selected : QIcon::Normal); QPoint point(isLTR ? opt.rect.left() + LATERAL_MARGIN : opt.rect.right() - LATERAL_MARGIN - m_iconSize, opt.rect.top() + (opt.rect.height() - m_iconSize) / 2); painter->drawPixmap(point, pm); if (opt.state & QStyle::State_Selected) { QPalette::ColorGroup cg = QPalette::Active; if (!(opt.state & QStyle::State_Enabled)) { cg = QPalette::Disabled; } else if (!(opt.state & QStyle::State_Active)) { cg = QPalette::Inactive; } painter->setPen(opt.palette.color(cg, QPalette::HighlightedText)); } QRect rectText; bool drawCapacityBar = false; if (placesModel->data(index, KFilePlacesModel::CapacityBarRecommendedRole).toBool()) { const QUrl url = placesModel->url(index); if (contentsOpacity(index) > 0) { QPersistentModelIndex persistentIndex(index); PlaceFreeSpaceInfo &info = m_freeSpaceInfo[persistentIndex]; drawCapacityBar = info.size > 0; if (drawCapacityBar) { painter->save(); painter->setOpacity(painter->opacity() * contentsOpacity(index)); int height = opt.fontMetrics.height() + CAPACITYBAR_HEIGHT; rectText = QRect(isLTR ? m_iconSize + LATERAL_MARGIN * 2 + opt.rect.left() : 0, opt.rect.top() + (opt.rect.height() / 2 - height / 2), opt.rect.width() - m_iconSize - LATERAL_MARGIN * 2, opt.fontMetrics.height()); painter->drawText(rectText, Qt::AlignLeft | Qt::AlignTop, opt.fontMetrics.elidedText(index.model()->data(index).toString(), Qt::ElideRight, rectText.width())); QRect capacityRect(isLTR ? rectText.x() : LATERAL_MARGIN, rectText.bottom() - 1, rectText.width() - LATERAL_MARGIN, CAPACITYBAR_HEIGHT); KCapacityBar capacityBar(KCapacityBar::DrawTextInline); capacityBar.setValue((info.used * 100) / info.size); capacityBar.drawCapacityBar(painter, capacityRect); painter->restore(); painter->save(); painter->setOpacity(painter->opacity() * (1 - contentsOpacity(index))); } if (!info.job && (!info.lastUpdated.isValid() || info.lastUpdated.secsTo(QDateTime::currentDateTimeUtc()) > 60)) { info.job = KIO::fileSystemFreeSpace(url); connect(info.job, &KIO::FileSystemFreeSpaceJob::result, this, [this, persistentIndex](KIO::Job *job, KIO::filesize_t size, KIO::filesize_t available) { PlaceFreeSpaceInfo &info = m_freeSpaceInfo[persistentIndex]; // even if we receive an error we want to refresh lastUpdated to avoid repeatedly querying in this case info.lastUpdated = QDateTime::currentDateTimeUtc(); if (job->error()) { return; } info.size = size; info.used = size - available; // FIXME scheduleDelayedItemsLayout but we're in the delegate here, not the view }); } } } rectText = QRect(isLTR ? m_iconSize + LATERAL_MARGIN * 2 + opt.rect.left() : 0, opt.rect.top(), opt.rect.width() - m_iconSize - LATERAL_MARGIN * 2, opt.rect.height()); painter->drawText(rectText, Qt::AlignLeft | Qt::AlignVCenter, opt.fontMetrics.elidedText(index.model()->data(index).toString(), Qt::ElideRight, rectText.width())); if (drawCapacityBar) { painter->restore(); } painter->restore(); } int KFilePlacesViewDelegate::iconSize() const { return m_iconSize; } void KFilePlacesViewDelegate::setIconSize(int newSize) { m_iconSize = newSize; } void KFilePlacesViewDelegate::addAppearingItem(const QModelIndex &index) { m_appearingItems << index; } void KFilePlacesViewDelegate::setAppearingItemProgress(qreal value) { if (value <= 0.25) { m_appearingOpacity = 0.0; m_appearingIconSize = iconSize() * value * 4; if (m_appearingIconSize >= m_iconSize) { m_appearingIconSize = m_iconSize; } } else { m_appearingIconSize = m_iconSize; m_appearingOpacity = (value - 0.25) * 4 / 3; if (value >= 1.0) { m_appearingItems.clear(); } } } void KFilePlacesViewDelegate::addDisappearingItem(const QModelIndex &index) { m_disappearingItems << index; } void KFilePlacesViewDelegate::addDisappearingItemGroup(const QModelIndex &index) { const KFilePlacesModel *placesModel = static_cast(index.model()); const QModelIndexList indexesGroup = placesModel->groupIndexes(placesModel->groupType(index)); m_disappearingItems.reserve(m_disappearingItems.count() + indexesGroup.count()); std::transform(indexesGroup.begin(), indexesGroup.end(), std::back_inserter(m_disappearingItems), [](const QModelIndex &idx){ return QPersistentModelIndex(idx); }); } void KFilePlacesViewDelegate::setDisappearingItemProgress(qreal value) { value = 1.0 - value; if (value <= 0.25) { m_disappearingOpacity = 0.0; m_disappearingIconSize = iconSize() * value * 4; if (m_disappearingIconSize >= m_iconSize) { m_disappearingIconSize = m_iconSize; } if (value <= 0.0) { m_disappearingItems.clear(); } } else { m_disappearingIconSize = m_iconSize; m_disappearingOpacity = (value - 0.25) * 4 / 3; } } void KFilePlacesViewDelegate::setShowHoverIndication(bool show) { m_showHoverIndication = show; } void KFilePlacesViewDelegate::addFadeAnimation(const QModelIndex &index, QTimeLine *timeLine) { m_timeLineMap.insert(index, timeLine); m_timeLineInverseMap.insert(timeLine, index); } void KFilePlacesViewDelegate::removeFadeAnimation(const QModelIndex &index) { QTimeLine *timeLine = m_timeLineMap.value(index, nullptr); m_timeLineMap.remove(index); m_timeLineInverseMap.remove(timeLine); } QModelIndex KFilePlacesViewDelegate::indexForFadeAnimation(QTimeLine *timeLine) const { return m_timeLineInverseMap.value(timeLine, QModelIndex()); } QTimeLine *KFilePlacesViewDelegate::fadeAnimationForIndex(const QModelIndex &index) const { return m_timeLineMap.value(index, nullptr); } qreal KFilePlacesViewDelegate::contentsOpacity(const QModelIndex &index) const { QTimeLine *timeLine = fadeAnimationForIndex(index); if (timeLine) { return timeLine->currentValue(); } return 0; } bool KFilePlacesViewDelegate::pointIsHeaderArea(const QPoint &pos) { // we only accept drag events starting from item body, ignore drag request from header QModelIndex index = m_view->indexAt(pos); if (!index.isValid()) { return false; } if (indexIsSectionHeader(index)) { const QRect vRect = m_view->visualRect(index); const int delegateY = pos.y() - vRect.y(); if (delegateY <= sectionHeaderHeight()) { return true; } } return false; } void KFilePlacesViewDelegate::startDrag() { m_dragStarted = true; } void KFilePlacesViewDelegate::clearFreeSpaceInfo() { m_freeSpaceInfo.clear(); } QString KFilePlacesViewDelegate::groupNameFromIndex(const QModelIndex &index) const { if (index.isValid()) { return index.data(KFilePlacesModel::GroupRole).toString(); } else { return QString(); } } QModelIndex KFilePlacesViewDelegate::previousVisibleIndex(const QModelIndex &index) const { if (index.row() == 0) { return QModelIndex(); } const QAbstractItemModel *model = index.model(); QModelIndex prevIndex = model->index(index.row() - 1, index.column(), index.parent()); while (m_view->isRowHidden(prevIndex.row())) { if (prevIndex.row() == 0) { return QModelIndex(); } prevIndex = model->index(prevIndex.row() - 1, index.column(), index.parent()); } return prevIndex; } bool KFilePlacesViewDelegate::indexIsSectionHeader(const QModelIndex &index) const { if (m_view->isRowHidden(index.row())) { return false; } if (index.row() == 0) { return true; } const auto groupName = groupNameFromIndex(index); const auto previousGroupName = groupNameFromIndex(previousVisibleIndex(index)); return groupName != previousGroupName; } void KFilePlacesViewDelegate::drawSectionHeader(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { const KFilePlacesModel *placesModel = static_cast(index.model()); const QString groupLabel = index.data(KFilePlacesModel::GroupRole).toString(); const QString category = placesModel->isGroupHidden(index) ? i18n("%1 (hidden)", groupLabel) : groupLabel; QRect textRect(option.rect); textRect.setLeft(textRect.left() + 3); /* Take spacing into account: The spacing to the previous section compensates for the spacing to the first item.*/ textRect.setY(textRect.y() /* + qMax(2, m_view->spacing()) - qMax(2, m_view->spacing())*/); textRect.setHeight(sectionHeaderHeight()); painter->save(); // based on dolphin colors const QColor c1 = textColor(option); const QColor c2 = baseColor(option); QColor penColor = mixedColor(c1, c2, 60); painter->setPen(penColor); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignBottom, category); painter->restore(); } QColor KFilePlacesViewDelegate::textColor(const QStyleOption &option) const { const QPalette::ColorGroup group = m_view->isActiveWindow() ? QPalette::Active : QPalette::Inactive; return option.palette.color(group, QPalette::WindowText); } QColor KFilePlacesViewDelegate::baseColor(const QStyleOption &option) const { const QPalette::ColorGroup group = m_view->isActiveWindow() ? QPalette::Active : QPalette::Inactive; return option.palette.color(group, QPalette::Window); } QColor KFilePlacesViewDelegate::mixedColor(const QColor& c1, const QColor& c2, int c1Percent) const { Q_ASSERT(c1Percent >= 0 && c1Percent <= 100); const int c2Percent = 100 - c1Percent; return QColor((c1.red() * c1Percent + c2.red() * c2Percent) / 100, (c1.green() * c1Percent + c2.green() * c2Percent) / 100, (c1.blue() * c1Percent + c2.blue() * c2Percent) / 100); } int KFilePlacesViewDelegate::sectionHeaderHeight() const { // Account for the spacing between header and item return QApplication::fontMetrics().height() + qMax(2, m_view->spacing()); } class Q_DECL_HIDDEN KFilePlacesView::Private { public: explicit Private(KFilePlacesView *parent) : q(parent) , watcher(new KFilePlacesEventWatcher(q)) {} enum FadeType { FadeIn = 0, FadeOut }; KFilePlacesView *const q; QUrl currentUrl; bool autoResizeItems; bool showAll; bool smoothItemResizing; bool dropOnPlace; bool dragging; Solid::StorageAccess *lastClickedStorage = nullptr; QPersistentModelIndex lastClickedIndex; QRect dropRect; void setCurrentIndex(const QModelIndex &index); void adaptItemSize(); void updateHiddenRows(); void clearFreeSpaceInfos(); bool insertAbove(const QRect &itemRect, const QPoint &pos) const; bool insertBelow(const QRect &itemRect, const QPoint &pos) const; int insertIndicatorHeight(int itemHeight) const; void fadeCapacityBar(const QModelIndex &index, FadeType fadeType); int sectionsCount() const; void addDisappearingItem(KFilePlacesViewDelegate *delegate, const QModelIndex &index); void triggerItemAppearingAnimation(); void triggerItemDisappearingAnimation(); void _k_placeClicked(const QModelIndex &index); void _k_placeEntered(const QModelIndex &index); void _k_placeLeft(const QModelIndex &index); void _k_storageSetupDone(const QModelIndex &index, bool success); void _k_adaptItemsUpdate(qreal value); void _k_itemAppearUpdate(qreal value); void _k_itemDisappearUpdate(qreal value); void _k_enableSmoothItemResizing(); void _k_capacityBarFadeValueChanged(); void _k_triggerDevicePolling(); QTimeLine adaptItemsTimeline; int oldSize, endSize; QTimeLine itemAppearTimeline; QTimeLine itemDisappearTimeline; KFilePlacesEventWatcher *const watcher; KFilePlacesViewDelegate *delegate = nullptr; QTimer pollDevices; int pollingRequestCount; }; KFilePlacesView::KFilePlacesView(QWidget *parent) : QListView(parent), d(new Private(this)) { d->showAll = false; d->smoothItemResizing = false; d->dropOnPlace = false; d->autoResizeItems = true; d->dragging = false; d->lastClickedStorage = nullptr; d->pollingRequestCount = 0; d->delegate = new KFilePlacesViewDelegate(this); setSelectionRectVisible(false); setSelectionMode(SingleSelection); setDragEnabled(true); setAcceptDrops(true); setMouseTracking(true); setDropIndicatorShown(false); setFrameStyle(QFrame::NoFrame); setResizeMode(Adjust); setItemDelegate(d->delegate); QPalette palette = viewport()->palette(); palette.setColor(viewport()->backgroundRole(), Qt::transparent); palette.setColor(viewport()->foregroundRole(), palette.color(QPalette::WindowText)); viewport()->setPalette(palette); connect(this, SIGNAL(clicked(QModelIndex)), this, SLOT(_k_placeClicked(QModelIndex))); // Note: Don't connect to the activated() signal, as the behavior when it is // committed depends on the used widget style. The click behavior of // KFilePlacesView should be style independent. connect(&d->adaptItemsTimeline, SIGNAL(valueChanged(qreal)), this, SLOT(_k_adaptItemsUpdate(qreal))); d->adaptItemsTimeline.setDuration(500); d->adaptItemsTimeline.setUpdateInterval(5); d->adaptItemsTimeline.setCurveShape(QTimeLine::EaseInOutCurve); connect(&d->itemAppearTimeline, SIGNAL(valueChanged(qreal)), this, SLOT(_k_itemAppearUpdate(qreal))); d->itemAppearTimeline.setDuration(500); d->itemAppearTimeline.setUpdateInterval(5); d->itemAppearTimeline.setCurveShape(QTimeLine::EaseInOutCurve); connect(&d->itemDisappearTimeline, SIGNAL(valueChanged(qreal)), this, SLOT(_k_itemDisappearUpdate(qreal))); d->itemDisappearTimeline.setDuration(500); d->itemDisappearTimeline.setUpdateInterval(5); d->itemDisappearTimeline.setCurveShape(QTimeLine::EaseInOutCurve); viewport()->installEventFilter(d->watcher); connect(d->watcher, SIGNAL(entryEntered(QModelIndex)), this, SLOT(_k_placeEntered(QModelIndex))); connect(d->watcher, SIGNAL(entryLeft(QModelIndex)), this, SLOT(_k_placeLeft(QModelIndex))); d->pollDevices.setInterval(5000); connect(&d->pollDevices, SIGNAL(timeout()), this, SLOT(_k_triggerDevicePolling())); // FIXME: this is necessary to avoid flashes of black with some widget styles. // could be a bug in Qt (e.g. QAbstractScrollArea) or KFilePlacesView, but has not // yet been tracked down yet. until then, this works and is harmlessly enough. // in fact, some QStyle (Oxygen, Skulpture, others?) do this already internally. // See br #242358 for more information verticalScrollBar()->setAttribute(Qt::WA_OpaquePaintEvent, false); } KFilePlacesView::~KFilePlacesView() { delete d; } void KFilePlacesView::setDropOnPlaceEnabled(bool enabled) { d->dropOnPlace = enabled; } bool KFilePlacesView::isDropOnPlaceEnabled() const { return d->dropOnPlace; } void KFilePlacesView::setAutoResizeItemsEnabled(bool enabled) { d->autoResizeItems = enabled; } bool KFilePlacesView::isAutoResizeItemsEnabled() const { return d->autoResizeItems; } void KFilePlacesView::setUrl(const QUrl &url) { KFilePlacesModel *placesModel = qobject_cast(model()); if (placesModel == nullptr) { return; } QModelIndex index = placesModel->closestItem(url); QModelIndex current = selectionModel()->currentIndex(); if (index.isValid()) { if (current != index && placesModel->isHidden(current) && !d->showAll) { KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); d->addDisappearingItem(delegate, current); } if (current != index && placesModel->isHidden(index) && !d->showAll) { KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); delegate->addAppearingItem(index); d->triggerItemAppearingAnimation(); setRowHidden(index.row(), false); } d->currentUrl = url; selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); } else { d->currentUrl = QUrl(); selectionModel()->clear(); } if (!current.isValid()) { d->updateHiddenRows(); } } void KFilePlacesView::setShowAll(bool showAll) { KFilePlacesModel *placesModel = qobject_cast(model()); if (placesModel == nullptr) { return; } d->showAll = showAll; KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); int rowCount = placesModel->rowCount(); QModelIndex current = placesModel->closestItem(d->currentUrl); if (showAll) { d->updateHiddenRows(); for (int i = 0; i < rowCount; ++i) { QModelIndex index = placesModel->index(i, 0); if (index != current && placesModel->isHidden(index)) { delegate->addAppearingItem(index); } } d->triggerItemAppearingAnimation(); } else { for (int i = 0; i < rowCount; ++i) { QModelIndex index = placesModel->index(i, 0); if (index != current && placesModel->isHidden(index)) { delegate->addDisappearingItem(index); } } d->triggerItemDisappearingAnimation(); } } void KFilePlacesView::keyPressEvent(QKeyEvent *event) { QListView::keyPressEvent(event); if ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Enter)) { d->_k_placeClicked(currentIndex()); } } void KFilePlacesView::contextMenuEvent(QContextMenuEvent *event) { KFilePlacesModel *placesModel = qobject_cast(model()); if (!placesModel) { return; } KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); QModelIndex index = indexAt(event->pos()); const QString label = placesModel->text(index).replace(QLatin1Char('&'), QLatin1String("&&")); const QUrl placeUrl = placesModel->url(index); QMenu menu; QAction *edit = nullptr; QAction *hide = nullptr; QAction *emptyTrash = nullptr; QAction *eject = nullptr; QAction *teardown = nullptr; QAction *add = nullptr; QAction *mainSeparator = nullptr; QAction *hideSection = nullptr; QAction *properties = nullptr; QAction *mount = nullptr; const bool clickOverHeader = delegate->pointIsHeaderArea(event->pos()); if (clickOverHeader) { const KFilePlacesModel::GroupType type = placesModel->groupType(index); hideSection = menu.addAction(QIcon::fromTheme(QStringLiteral("hint")), i18n("Hide Section")); hideSection->setCheckable(true); hideSection->setChecked(placesModel->isGroupHidden(type)); } else if (index.isValid()) { if (!placesModel->isDevice(index)) { if (placeUrl.toString() == QLatin1String("trash:/")) { emptyTrash = menu.addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash")); KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig); emptyTrash->setEnabled(!trashConfig.group("Status").readEntry("Empty", true)); menu.addSeparator(); } add = menu.addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add Entry...")); mainSeparator = menu.addSeparator(); } else { eject = placesModel->ejectActionForIndex(index); if (eject != nullptr) { eject->setParent(&menu); menu.addAction(eject); } teardown = placesModel->teardownActionForIndex(index); if (teardown != nullptr) { // Disable teardown option for root and home partitions bool teardownEnabled = placeUrl != QUrl::fromLocalFile(QDir::rootPath()); if (teardownEnabled) { KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByPath(QDir::homePath()); if (mountPoint && placeUrl == QUrl::fromLocalFile(mountPoint->mountPoint())) { teardownEnabled = false; } } teardown->setEnabled(teardownEnabled); teardown->setParent(&menu); menu.addAction(teardown); } if (placesModel->setupNeeded(index)) { mount = menu.addAction(QIcon::fromTheme(QStringLiteral("media-mount")), i18nc("@action:inmenu", "Mount")); } if (teardown != nullptr || eject != nullptr || mount != nullptr) { mainSeparator = menu.addSeparator(); } } if (add == nullptr) { add = menu.addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add Entry...")); } if (placeUrl.isLocalFile()) { properties = menu.addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("Properties")); } if (!placesModel->isDevice(index)) { edit = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-entry")), i18n("&Edit Entry '%1'...", label)); } hide = menu.addAction(QIcon::fromTheme(QStringLiteral("hint")), i18n("&Hide Entry '%1'", label)); hide->setCheckable(true); hide->setChecked(placesModel->isHidden(index)); // if a parent is hidden no interaction should be possible with children, show it first to do so hide->setEnabled(!placesModel->isGroupHidden(placesModel->groupType(index))); } else { add = menu.addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add Entry...")); } QAction *showAll = nullptr; if (placesModel->hiddenCount() > 0) { showAll = new QAction(QIcon::fromTheme(QStringLiteral("visibility")), i18n("&Show All Entries"), &menu); showAll->setCheckable(true); showAll->setChecked(d->showAll); if (mainSeparator == nullptr) { mainSeparator = menu.addSeparator(); } menu.insertAction(mainSeparator, showAll); } QAction *remove = nullptr; if (!clickOverHeader && index.isValid() && !placesModel->isDevice(index)) { remove = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("&Remove Entry '%1'", label)); } menu.addActions(actions()); if (menu.isEmpty()) { return; } QAction *result = menu.exec(event->globalPos()); if (emptyTrash && (result == emptyTrash)) { KIO::JobUiDelegate uiDelegate; uiDelegate.setWindow(window()); if (uiDelegate.askDeleteConfirmation(QList(), KIO::JobUiDelegate::EmptyTrash, KIO::JobUiDelegate::DefaultConfirmation)) { KIO::Job* job = KIO::emptyTrash(); KJobWidgets::setWindow(job, window()); job->uiDelegate()->setAutoErrorHandlingEnabled(true); } } else if (properties && (result == properties)) { KPropertiesDialog::showDialog(placeUrl, this); } else if (edit && (result == edit)) { KBookmark bookmark = placesModel->bookmarkForIndex(index); QUrl url = bookmark.url(); QString label = bookmark.text(); QString iconName = bookmark.icon(); bool appLocal = !bookmark.metaDataItem(QStringLiteral("OnlyInApp")).isEmpty(); if (KFilePlaceEditDialog::getInformation(true, url, label, iconName, false, appLocal, 64, this)) { QString appName; if (appLocal) { appName = QCoreApplication::instance()->applicationName(); } placesModel->editPlace(index, label, url, iconName, appName); } } else if (remove && (result == remove)) { placesModel->removePlace(index); } else if (hideSection && (result == hideSection)) { const KFilePlacesModel::GroupType type = placesModel->groupType(index); placesModel->setGroupHidden(type, hideSection->isChecked()); if (!d->showAll && hideSection->isChecked()) { delegate->addDisappearingItemGroup(index); d->triggerItemDisappearingAnimation(); } } else if (hide && (result == hide)) { placesModel->setPlaceHidden(index, hide->isChecked()); QModelIndex current = placesModel->closestItem(d->currentUrl); if (index != current && !d->showAll && hide->isChecked()) { delegate->addDisappearingItem(index); d->triggerItemDisappearingAnimation(); } } else if (showAll && (result == showAll)) { setShowAll(showAll->isChecked()); } else if (teardown && (result == teardown)) { placesModel->requestTeardown(index); } else if (eject && (result == eject)) { placesModel->requestEject(index); } else if (add && (result == add)) { QUrl url = d->currentUrl; QString label; QString iconName = QStringLiteral("folder"); bool appLocal = true; if (KFilePlaceEditDialog::getInformation(true, url, label, iconName, true, appLocal, 64, this)) { QString appName; if (appLocal) { appName = QCoreApplication::instance()->applicationName(); } placesModel->addPlace(label, url, iconName, appName, index); } } else if (mount && (result == mount)) { placesModel->requestSetup(index); } index = placesModel->closestItem(d->currentUrl); selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); } void KFilePlacesView::resizeEvent(QResizeEvent *event) { QListView::resizeEvent(event); d->adaptItemSize(); } void KFilePlacesView::showEvent(QShowEvent *event) { QListView::showEvent(event); QTimer::singleShot(100, this, SLOT(_k_enableSmoothItemResizing())); } void KFilePlacesView::hideEvent(QHideEvent *event) { QListView::hideEvent(event); d->smoothItemResizing = false; } void KFilePlacesView::dragEnterEvent(QDragEnterEvent *event) { QListView::dragEnterEvent(event); d->dragging = true; KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); delegate->setShowHoverIndication(false); d->dropRect = QRect(); } void KFilePlacesView::dragLeaveEvent(QDragLeaveEvent *event) { QListView::dragLeaveEvent(event); d->dragging = false; KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); delegate->setShowHoverIndication(true); setDirtyRegion(d->dropRect); } void KFilePlacesView::dragMoveEvent(QDragMoveEvent *event) { QListView::dragMoveEvent(event); // update the drop indicator const QPoint pos = event->pos(); const QModelIndex index = indexAt(pos); setDirtyRegion(d->dropRect); if (index.isValid()) { const QRect rect = visualRect(index); const int gap = d->insertIndicatorHeight(rect.height()); if (d->insertAbove(rect, pos)) { // indicate that the item will be inserted above the current place d->dropRect = QRect(rect.left(), rect.top() - gap / 2, rect.width(), gap); } else if (d->insertBelow(rect, pos)) { // indicate that the item will be inserted below the current place d->dropRect = QRect(rect.left(), rect.bottom() + 1 - gap / 2, rect.width(), gap); } else { // indicate that the item be dropped above the current place d->dropRect = rect; } } setDirtyRegion(d->dropRect); } void KFilePlacesView::dropEvent(QDropEvent *event) { const QPoint pos = event->pos(); const QModelIndex index = indexAt(pos); if (index.isValid()) { const QRect rect = visualRect(index); if (!d->insertAbove(rect, pos) && !d->insertBelow(rect, pos)) { KFilePlacesModel *placesModel = qobject_cast(model()); Q_ASSERT(placesModel != nullptr); emit urlsDropped(placesModel->url(index), event, this); event->acceptProposedAction(); } } QListView::dropEvent(event); d->dragging = false; KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); delegate->setShowHoverIndication(true); } void KFilePlacesView::paintEvent(QPaintEvent *event) { QListView::paintEvent(event); if (d->dragging && !d->dropRect.isEmpty()) { // draw drop indicator QPainter painter(viewport()); const QModelIndex index = indexAt(d->dropRect.topLeft()); const QRect itemRect = visualRect(index); const bool drawInsertIndicator = !d->dropOnPlace || d->dropRect.height() <= d->insertIndicatorHeight(itemRect.height()); if (drawInsertIndicator) { // draw indicator for inserting items QBrush blendedBrush = viewOptions().palette.brush(QPalette::Normal, QPalette::Highlight); QColor color = blendedBrush.color(); const int y = (d->dropRect.top() + d->dropRect.bottom()) / 2; const int thickness = d->dropRect.height() / 2; Q_ASSERT(thickness >= 1); int alpha = 255; const int alphaDec = alpha / (thickness + 1); for (int i = 0; i < thickness; i++) { color.setAlpha(alpha); alpha -= alphaDec; painter.setPen(color); painter.drawLine(d->dropRect.left(), y - i, d->dropRect.right(), y - i); painter.drawLine(d->dropRect.left(), y + i, d->dropRect.right(), y + i); } } else { // draw indicator for copying/moving/linking to items QStyleOptionViewItem opt; opt.initFrom(this); opt.rect = itemRect; opt.state = QStyle::State_Enabled | QStyle::State_MouseOver; style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, &painter, this); } } } void KFilePlacesView::startDrag(Qt::DropActions supportedActions) { KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); delegate->startDrag(); QListView::startDrag(supportedActions); } void KFilePlacesView::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); // does not accept drags from section header area if (delegate->pointIsHeaderArea(event->pos())) { return; } } QListView::mousePressEvent(event); } void KFilePlacesView::setModel(QAbstractItemModel *model) { QListView::setModel(model); d->updateHiddenRows(); // Uses Qt::QueuedConnection to delay the time when the slot will be // called. In case of an item move the remove+add will be done before // we adapt the item size (otherwise we'd get it wrong as we'd execute // it after the remove only). connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(adaptItemSize()), Qt::QueuedConnection); connect(selectionModel(), &QItemSelectionModel::currentChanged, d->watcher, &KFilePlacesEventWatcher::currentIndexChanged); static_cast(itemDelegate())->clearFreeSpaceInfo(); } void KFilePlacesView::rowsInserted(const QModelIndex &parent, int start, int end) { QListView::rowsInserted(parent, start, end); setUrl(d->currentUrl); KFilePlacesViewDelegate *delegate = static_cast(itemDelegate()); KFilePlacesModel *placesModel = static_cast(model()); for (int i = start; i <= end; ++i) { QModelIndex index = placesModel->index(i, 0, parent); if (d->showAll || !placesModel->isHidden(index)) { delegate->addAppearingItem(index); d->triggerItemAppearingAnimation(); } else { setRowHidden(i, true); } } d->triggerItemAppearingAnimation(); d->adaptItemSize(); } QSize KFilePlacesView::sizeHint() const { KFilePlacesModel *placesModel = qobject_cast(model()); if (!placesModel) { return QListView::sizeHint(); } const int height = QListView::sizeHint().height(); QFontMetrics fm = d->q->fontMetrics(); int textWidth = 0; for (int i = 0; i < placesModel->rowCount(); ++i) { QModelIndex index = placesModel->index(i, 0); if (!placesModel->isHidden(index)) { - textWidth = qMax(textWidth, fm.width(index.data(Qt::DisplayRole).toString())); + textWidth = qMax(textWidth, fm.boundingRect(index.data(Qt::DisplayRole).toString()).width()); } } const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Small) + 3 * LATERAL_MARGIN; return QSize(iconSize + textWidth + fm.height() / 2, height); } void KFilePlacesView::Private::addDisappearingItem(KFilePlacesViewDelegate *delegate, const QModelIndex &index) { delegate->addDisappearingItem(index); if (itemDisappearTimeline.state() != QTimeLine::Running) { delegate->setDisappearingItemProgress(0.0); itemDisappearTimeline.start(); } } void KFilePlacesView::Private::setCurrentIndex(const QModelIndex &index) { KFilePlacesModel *placesModel = qobject_cast(q->model()); if (placesModel == nullptr) { return; } QUrl url = placesModel->url(index); if (url.isValid()) { currentUrl = url; updateHiddenRows(); emit q->urlChanged(KFilePlacesModel::convertedUrl(url)); if (showAll) { q->setShowAll(false); } } else { q->setUrl(currentUrl); } } void KFilePlacesView::Private::adaptItemSize() { KFilePlacesViewDelegate *delegate = static_cast(q->itemDelegate()); if (!autoResizeItems) { const int size = q->iconSize().width(); // Assume width == height delegate->setIconSize(size); q->scheduleDelayedItemsLayout(); return; } KFilePlacesModel *placesModel = qobject_cast(q->model()); if (placesModel == nullptr) { return; } int rowCount = placesModel->rowCount(); if (!showAll) { rowCount -= placesModel->hiddenCount(); QModelIndex current = placesModel->closestItem(currentUrl); if (placesModel->isHidden(current)) { rowCount++; } } if (rowCount == 0) { return; // We've nothing to display anyway } const int minSize = IconSize(KIconLoader::Small); const int maxSize = 64; int textWidth = 0; QFontMetrics fm = q->fontMetrics(); for (int i = 0; i < placesModel->rowCount(); ++i) { QModelIndex index = placesModel->index(i, 0); if (!placesModel->isHidden(index)) { - textWidth = qMax(textWidth, fm.width(index.data(Qt::DisplayRole).toString())); + textWidth = qMax(textWidth, fm.boundingRect(index.data(Qt::DisplayRole).toString()).width()); } } const int margin = q->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, q) + 1; const int maxWidth = q->viewport()->width() - textWidth - 4 * margin - 1; const int totalItemsHeight = (fm.height() / 2) * rowCount; const int totalSectionsHeight = delegate->sectionHeaderHeight() * sectionsCount(); const int maxHeight = ((q->height() - totalSectionsHeight - totalItemsHeight) / rowCount) - 1; int size = qMin(maxHeight, maxWidth); if (size < minSize) { size = minSize; } else if (size > maxSize) { size = maxSize; } else { // Make it a multiple of 16 size &= ~0xf; } if (size == delegate->iconSize()) { return; } if (smoothItemResizing) { oldSize = delegate->iconSize(); endSize = size; if (adaptItemsTimeline.state() != QTimeLine::Running) { adaptItemsTimeline.start(); } } else { delegate->setIconSize(size); q->scheduleDelayedItemsLayout(); } } void KFilePlacesView::Private::updateHiddenRows() { KFilePlacesModel *placesModel = qobject_cast(q->model()); if (placesModel == nullptr) { return; } int rowCount = placesModel->rowCount(); QModelIndex current = placesModel->closestItem(currentUrl); for (int i = 0; i < rowCount; ++i) { QModelIndex index = placesModel->index(i, 0); if (index != current && placesModel->isHidden(index) && !showAll) { q->setRowHidden(i, true); } else { q->setRowHidden(i, false); } } adaptItemSize(); } bool KFilePlacesView::Private::insertAbove(const QRect &itemRect, const QPoint &pos) const { if (dropOnPlace) { return pos.y() < itemRect.top() + insertIndicatorHeight(itemRect.height()) / 2; } return pos.y() < itemRect.top() + (itemRect.height() / 2); } bool KFilePlacesView::Private::insertBelow(const QRect &itemRect, const QPoint &pos) const { if (dropOnPlace) { return pos.y() > itemRect.bottom() - insertIndicatorHeight(itemRect.height()) / 2; } return pos.y() >= itemRect.top() + (itemRect.height() / 2); } int KFilePlacesView::Private::insertIndicatorHeight(int itemHeight) const { const int min = 4; const int max = 12; int height = itemHeight / 4; if (height < min) { height = min; } else if (height > max) { height = max; } return height; } void KFilePlacesView::Private::fadeCapacityBar(const QModelIndex &index, FadeType fadeType) { QTimeLine *timeLine = delegate->fadeAnimationForIndex(index); delete timeLine; delegate->removeFadeAnimation(index); timeLine = new QTimeLine(250, q); connect(timeLine, SIGNAL(valueChanged(qreal)), q, SLOT(_k_capacityBarFadeValueChanged())); if (fadeType == FadeIn) { timeLine->setDirection(QTimeLine::Forward); timeLine->setCurrentTime(0); } else { timeLine->setDirection(QTimeLine::Backward); timeLine->setCurrentTime(250); } delegate->addFadeAnimation(index, timeLine); timeLine->start(); } int KFilePlacesView::Private::sectionsCount() const { int count = 0; QString prevSection; const int rowCount = q->model()->rowCount(); for(int i = 0; i < rowCount; i++) { if (!q->isRowHidden(i)) { const QModelIndex index = q->model()->index(i, 0); const QString sectionName = index.data(KFilePlacesModel::GroupRole).toString(); if (prevSection != sectionName) { prevSection = sectionName; count++; } } } return count; } void KFilePlacesView::Private::triggerItemAppearingAnimation() { if (itemAppearTimeline.state() != QTimeLine::Running) { delegate->setAppearingItemProgress(0.0); itemAppearTimeline.start(); } } void KFilePlacesView::Private::triggerItemDisappearingAnimation() { if (itemDisappearTimeline.state() != QTimeLine::Running) { delegate->setDisappearingItemProgress(0.0); itemDisappearTimeline.start(); } } void KFilePlacesView::Private::_k_placeClicked(const QModelIndex &index) { KFilePlacesModel *placesModel = qobject_cast(q->model()); if (placesModel == nullptr) { return; } lastClickedIndex = QPersistentModelIndex(); if (placesModel->setupNeeded(index)) { QObject::connect(placesModel, SIGNAL(setupDone(QModelIndex,bool)), q, SLOT(_k_storageSetupDone(QModelIndex,bool))); lastClickedIndex = index; placesModel->requestSetup(index); return; } setCurrentIndex(index); } void KFilePlacesView::Private::_k_placeEntered(const QModelIndex &index) { fadeCapacityBar(index, FadeIn); pollingRequestCount++; if (pollingRequestCount == 1) { pollDevices.start(); } } void KFilePlacesView::Private::_k_placeLeft(const QModelIndex &index) { fadeCapacityBar(index, FadeOut); pollingRequestCount--; if (!pollingRequestCount) { pollDevices.stop(); } } void KFilePlacesView::Private::_k_storageSetupDone(const QModelIndex &index, bool success) { if (index != lastClickedIndex) { return; } KFilePlacesModel *placesModel = qobject_cast(q->model()); if (placesModel) { QObject::disconnect(placesModel, SIGNAL(setupDone(QModelIndex,bool)), q, SLOT(_k_storageSetupDone(QModelIndex,bool))); } if (success) { setCurrentIndex(lastClickedIndex); } else { q->setUrl(currentUrl); } lastClickedIndex = QPersistentModelIndex(); } void KFilePlacesView::Private::_k_adaptItemsUpdate(qreal value) { int add = (endSize - oldSize) * value; int size = oldSize + add; KFilePlacesViewDelegate *delegate = static_cast(q->itemDelegate()); delegate->setIconSize(size); q->scheduleDelayedItemsLayout(); } void KFilePlacesView::Private::_k_itemAppearUpdate(qreal value) { KFilePlacesViewDelegate *delegate = static_cast(q->itemDelegate()); delegate->setAppearingItemProgress(value); q->scheduleDelayedItemsLayout(); } void KFilePlacesView::Private::_k_itemDisappearUpdate(qreal value) { KFilePlacesViewDelegate *delegate = static_cast(q->itemDelegate()); delegate->setDisappearingItemProgress(value); if (value >= 1.0) { updateHiddenRows(); } q->scheduleDelayedItemsLayout(); } void KFilePlacesView::Private::_k_enableSmoothItemResizing() { smoothItemResizing = true; } void KFilePlacesView::Private::_k_capacityBarFadeValueChanged() { const QModelIndex index = delegate->indexForFadeAnimation(static_cast(q->sender())); if (!index.isValid()) { return; } q->update(index); } void KFilePlacesView::Private::_k_triggerDevicePolling() { const QModelIndex hoveredIndex = watcher->hoveredIndex(); if (hoveredIndex.isValid()) { const KFilePlacesModel *placesModel = static_cast(hoveredIndex.model()); if (placesModel->isDevice(hoveredIndex)) { q->update(hoveredIndex); } } const QModelIndex focusedIndex = watcher->focusedIndex(); if (focusedIndex.isValid() && focusedIndex != hoveredIndex) { const KFilePlacesModel *placesModel = static_cast(focusedIndex.model()); if (placesModel->isDevice(focusedIndex)) { q->update(focusedIndex); } } } void KFilePlacesView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { QListView::dataChanged(topLeft, bottomRight, roles); d->adaptItemSize(); } #include "moc_kfileplacesview.cpp" #include "moc_kfileplacesview_p.cpp" #include "kfileplacesview.moc" diff --git a/src/filewidgets/kstatusbarofflineindicator.cpp b/src/filewidgets/kstatusbarofflineindicator.cpp index 94a53506..d2723653 100644 --- a/src/filewidgets/kstatusbarofflineindicator.cpp +++ b/src/filewidgets/kstatusbarofflineindicator.cpp @@ -1,81 +1,81 @@ /* This file is part of the KDE project Copyright (C) 2007 Will Stephenson 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. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. As a special exception, permission is given to link this library with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "kstatusbarofflineindicator.h" #include #include #include #include #include class KStatusBarOfflineIndicatorPrivate { public: explicit KStatusBarOfflineIndicatorPrivate(KStatusBarOfflineIndicator *parent) : q(parent) , networkConfiguration(new QNetworkConfigurationManager(parent)) { } void initialize(); void _k_networkStatusChanged(bool isOnline); KStatusBarOfflineIndicator * const q; QNetworkConfigurationManager *networkConfiguration; }; KStatusBarOfflineIndicator::KStatusBarOfflineIndicator(QWidget *parent) : QWidget(parent), d(new KStatusBarOfflineIndicatorPrivate(this)) { QVBoxLayout *layout = new QVBoxLayout(this); - layout->setMargin(2); + layout->setContentsMargins(2, 2, 2, 2); QLabel *label = new QLabel(this); label->setPixmap(SmallIcon(QStringLiteral("network-disconnect"))); label->setToolTip(i18n("The desktop is offline")); layout->addWidget(label); d->initialize(); connect(d->networkConfiguration, SIGNAL(onlineStateChanged(bool)), SLOT(_k_networkStatusChanged(bool))); } KStatusBarOfflineIndicator::~KStatusBarOfflineIndicator() { delete d; } void KStatusBarOfflineIndicatorPrivate::initialize() { _k_networkStatusChanged(networkConfiguration->isOnline()); } void KStatusBarOfflineIndicatorPrivate::_k_networkStatusChanged(bool isOnline) { if (isOnline) { q->hide(); } else { q->show(); } } #include "moc_kstatusbarofflineindicator.cpp" diff --git a/src/filewidgets/kurlnavigatorbutton.cpp b/src/filewidgets/kurlnavigatorbutton.cpp index 5f303204..822d8121 100644 --- a/src/filewidgets/kurlnavigatorbutton.cpp +++ b/src/filewidgets/kurlnavigatorbutton.cpp @@ -1,702 +1,702 @@ /***************************************************************************** * Copyright (C) 2006 by Peter Penz * * Copyright (C) 2006 by Aaron J. Seigo * * * * 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 "kurlnavigatorbutton_p.h" #include "kurlnavigator.h" #include "kurlnavigatormenu_p.h" #include "kdirsortfilterproxymodel.h" #include "../pathhelpers_p.h" #include #include #include #include #include #include #include #include #include namespace KDEPrivate { QPointer KUrlNavigatorButton::m_subDirsMenu; KUrlNavigatorButton::KUrlNavigatorButton(const QUrl &url, QWidget *parent) : KUrlNavigatorButtonBase(parent), m_hoverArrow(false), m_pendingTextChange(false), m_replaceButton(false), m_showMnemonic(false), m_wheelSteps(0), m_url(url), m_subDir(), m_openSubDirsTimer(nullptr), m_subDirsJob(nullptr) { setAcceptDrops(true); setUrl(url); setMouseTracking(true); m_openSubDirsTimer = new QTimer(this); m_openSubDirsTimer->setSingleShot(true); m_openSubDirsTimer->setInterval(300); connect(m_openSubDirsTimer, &QTimer::timeout, this, &KUrlNavigatorButton::startSubDirsJob); connect(this, &QAbstractButton::pressed, this, &KUrlNavigatorButton::requestSubDirs); } KUrlNavigatorButton::~KUrlNavigatorButton() { } void KUrlNavigatorButton::setUrl(const QUrl &url) { m_url = url; // Doing a text-resolving with KIO::stat() for all non-local // URLs leads to problems for protocols where a limit is given for // the number of parallel connections. A black-list // is given where KIO::stat() should not be used: static const QSet protocolBlacklist = QSet{ QStringLiteral("nfs"), QStringLiteral("fish"), QStringLiteral("ftp"), QStringLiteral("sftp"), QStringLiteral("smb"), QStringLiteral("webdav"), QStringLiteral("mtp"), }; const bool startTextResolving = m_url.isValid() && !m_url.isLocalFile() && !protocolBlacklist.contains(m_url.scheme()); if (startTextResolving) { m_pendingTextChange = true; KIO::StatJob *job = KIO::stat(m_url, KIO::HideProgressInfo); connect(job, &KJob::result, this, &KUrlNavigatorButton::statFinished); emit startedTextResolving(); } else { setText(m_url.fileName().replace(QLatin1Char('&'), QLatin1String("&&"))); } } QUrl KUrlNavigatorButton::url() const { return m_url; } void KUrlNavigatorButton::setText(const QString &text) { QString adjustedText = text; if (adjustedText.isEmpty()) { adjustedText = m_url.scheme(); } // Assure that the button always consists of one line adjustedText.remove(QLatin1Char('\n')); KUrlNavigatorButtonBase::setText(adjustedText); updateMinimumWidth(); // Assure that statFinished() does not overwrite a text that has been // set by a client of the URL navigator button m_pendingTextChange = false; } void KUrlNavigatorButton::setActiveSubDirectory(const QString &subDir) { m_subDir = subDir; // We use a different (bold) font on active, so the size hint changes updateGeometry(); update(); } QString KUrlNavigatorButton::activeSubDirectory() const { return m_subDir; } QSize KUrlNavigatorButton::sizeHint() const { QFont adjustedFont(font()); adjustedFont.setBold(m_subDir.isEmpty()); // the minimum size is textWidth + arrowWidth() + 2 * BorderWidth; for the // preferred size we add the BorderWidth 2 times again for having an uncluttered look - const int width = QFontMetrics(adjustedFont).width(plainText()) + arrowWidth() + 4 * BorderWidth; + const int width = QFontMetrics(adjustedFont).boundingRect(plainText()).width() + arrowWidth() + 4 * BorderWidth; return QSize(width, KUrlNavigatorButtonBase::sizeHint().height()); } void KUrlNavigatorButton::setShowMnemonic(bool show) { if (m_showMnemonic != show) { m_showMnemonic = show; update(); } } bool KUrlNavigatorButton::showMnemonic() const { return m_showMnemonic; } void KUrlNavigatorButton::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); QFont adjustedFont(font()); adjustedFont.setBold(m_subDir.isEmpty()); painter.setFont(adjustedFont); int buttonWidth = width(); int preferredWidth = sizeHint().width(); if (preferredWidth < minimumWidth()) { preferredWidth = minimumWidth(); } if (buttonWidth > preferredWidth) { buttonWidth = preferredWidth; } const int buttonHeight = height(); const QColor fgColor = foregroundColor(); drawHoverBackground(&painter); int textLeft = 0; int textWidth = buttonWidth; const bool leftToRight = (layoutDirection() == Qt::LeftToRight); if (!m_subDir.isEmpty()) { // draw arrow const int arrowSize = arrowWidth(); const int arrowX = leftToRight ? (buttonWidth - arrowSize) - BorderWidth : BorderWidth; const int arrowY = (buttonHeight - arrowSize) / 2; QStyleOption option; option.initFrom(this); option.rect = QRect(arrowX, arrowY, arrowSize, arrowSize); option.palette = palette(); option.palette.setColor(QPalette::Text, fgColor); option.palette.setColor(QPalette::WindowText, fgColor); option.palette.setColor(QPalette::ButtonText, fgColor); if (m_hoverArrow) { // highlight the background of the arrow to indicate that the directories // popup can be opened by a mouse click QColor hoverColor = palette().color(QPalette::HighlightedText); hoverColor.setAlpha(96); painter.setPen(Qt::NoPen); painter.setBrush(hoverColor); int hoverX = arrowX; if (!leftToRight) { hoverX -= BorderWidth; } painter.drawRect(QRect(hoverX, 0, arrowSize + BorderWidth, buttonHeight)); } if (leftToRight) { style()->drawPrimitive(QStyle::PE_IndicatorArrowRight, &option, &painter, this); } else { style()->drawPrimitive(QStyle::PE_IndicatorArrowLeft, &option, &painter, this); textLeft += arrowSize + 2 * BorderWidth; } textWidth -= arrowSize + 2 * BorderWidth; } painter.setPen(fgColor); const bool clipped = isTextClipped(); const QRect textRect(textLeft, 0, textWidth, buttonHeight); if (clipped) { QColor bgColor = fgColor; bgColor.setAlpha(0); QLinearGradient gradient(textRect.topLeft(), textRect.topRight()); if (leftToRight) { gradient.setColorAt(0.8, fgColor); gradient.setColorAt(1.0, bgColor); } else { gradient.setColorAt(0.0, bgColor); gradient.setColorAt(0.2, fgColor); } QPen pen; pen.setBrush(QBrush(gradient)); painter.setPen(pen); } int textFlags = clipped ? Qt::AlignVCenter : Qt::AlignCenter; if (m_showMnemonic) { textFlags |= Qt::TextShowMnemonic; painter.drawText(textRect, textFlags, text()); } else { painter.drawText(textRect, textFlags, plainText()); } } void KUrlNavigatorButton::enterEvent(QEvent *event) { KUrlNavigatorButtonBase::enterEvent(event); // if the text is clipped due to a small window width, the text should // be shown as tooltip if (isTextClipped()) { setToolTip(plainText()); } } void KUrlNavigatorButton::leaveEvent(QEvent *event) { KUrlNavigatorButtonBase::leaveEvent(event); setToolTip(QString()); if (m_hoverArrow) { m_hoverArrow = false; update(); } } void KUrlNavigatorButton::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Enter: case Qt::Key_Return: emit clicked(m_url, Qt::LeftButton, event->modifiers()); break; case Qt::Key_Down: case Qt::Key_Space: startSubDirsJob(); break; default: KUrlNavigatorButtonBase::keyPressEvent(event); } } void KUrlNavigatorButton::dropEvent(QDropEvent *event) { if (event->mimeData()->hasUrls()) { setDisplayHintEnabled(DraggedHint, true); emit urlsDropped(m_url, event); setDisplayHintEnabled(DraggedHint, false); update(); } } void KUrlNavigatorButton::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls()) { setDisplayHintEnabled(DraggedHint, true); event->acceptProposedAction(); update(); } } void KUrlNavigatorButton::dragMoveEvent(QDragMoveEvent *event) { QRect rect = event->answerRect(); if (isAboveArrow(rect.center().x())) { m_hoverArrow = true; update(); if (m_subDirsMenu == nullptr) { requestSubDirs(); } else if (m_subDirsMenu->parent() != this) { m_subDirsMenu->close(); m_subDirsMenu->deleteLater(); m_subDirsMenu = nullptr; requestSubDirs(); } } else { if (m_openSubDirsTimer->isActive()) { cancelSubDirsRequest(); } delete m_subDirsMenu; m_subDirsMenu = nullptr; m_hoverArrow = false; update(); } } void KUrlNavigatorButton::dragLeaveEvent(QDragLeaveEvent *event) { KUrlNavigatorButtonBase::dragLeaveEvent(event); m_hoverArrow = false; setDisplayHintEnabled(DraggedHint, false); update(); } void KUrlNavigatorButton::mousePressEvent(QMouseEvent *event) { if (isAboveArrow(event->x()) && (event->button() == Qt::LeftButton)) { // the mouse is pressed above the [>] button startSubDirsJob(); } KUrlNavigatorButtonBase::mousePressEvent(event); } void KUrlNavigatorButton::mouseReleaseEvent(QMouseEvent *event) { if (!isAboveArrow(event->x()) || (event->button() != Qt::LeftButton)) { // the mouse has been released above the text area and not // above the [>] button emit clicked(m_url, event->button(), event->modifiers()); cancelSubDirsRequest(); } KUrlNavigatorButtonBase::mouseReleaseEvent(event); } void KUrlNavigatorButton::mouseMoveEvent(QMouseEvent *event) { KUrlNavigatorButtonBase::mouseMoveEvent(event); const bool hoverArrow = isAboveArrow(event->x()); if (hoverArrow != m_hoverArrow) { m_hoverArrow = hoverArrow; update(); } } void KUrlNavigatorButton::wheelEvent(QWheelEvent *event) { - if (event->orientation() == Qt::Vertical) { + if (event->angleDelta().y() != 0) { m_wheelSteps = event->angleDelta().y() / 120; m_replaceButton = true; startSubDirsJob(); } KUrlNavigatorButtonBase::wheelEvent(event); } void KUrlNavigatorButton::requestSubDirs() { if (!m_openSubDirsTimer->isActive() && (m_subDirsJob == nullptr)) { m_openSubDirsTimer->start(); } } void KUrlNavigatorButton::startSubDirsJob() { if (m_subDirsJob != nullptr) { return; } const QUrl url = m_replaceButton ? KIO::upUrl(m_url) : m_url; m_subDirsJob = KIO::listDir(url, KIO::HideProgressInfo, false /*no hidden files*/); m_subDirs.clear(); // just to be ++safe connect(m_subDirsJob, &KIO::ListJob::entries, this, &KUrlNavigatorButton::addEntriesToSubDirs); if (m_replaceButton) { connect(m_subDirsJob, &KJob::result, this, &KUrlNavigatorButton::replaceButton); } else { connect(m_subDirsJob, &KJob::result, this, &KUrlNavigatorButton::openSubDirsMenu); } } void KUrlNavigatorButton::addEntriesToSubDirs(KIO::Job *job, const KIO::UDSEntryList &entries) { Q_ASSERT(job == m_subDirsJob); Q_UNUSED(job); for (const KIO::UDSEntry &entry : entries) { if (entry.isDir()) { const QString name = entry.stringValue(KIO::UDSEntry::UDS_NAME); QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); if (displayName.isEmpty()) { displayName = name; } if ((name != QLatin1String(".")) && (name != QLatin1String(".."))) { m_subDirs.append(qMakePair(name, displayName)); } } } } void KUrlNavigatorButton::urlsDropped(QAction *action, QDropEvent *event) { const int result = action->data().toInt(); QUrl url(m_url); url.setPath(concatPaths(url.path(), m_subDirs.at(result).first)); emit urlsDropped(url, event); } void KUrlNavigatorButton::slotMenuActionClicked(QAction *action, Qt::MouseButton button) { const int result = action->data().toInt(); QUrl url(m_url); url.setPath(concatPaths(url.path(), m_subDirs.at(result).first)); emit clicked(url, button, Qt::NoModifier); } void KUrlNavigatorButton::statFinished(KJob *job) { if (m_pendingTextChange) { m_pendingTextChange = false; const KIO::UDSEntry entry = static_cast(job)->statResult(); QString name = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); if (name.isEmpty()) { name = m_url.fileName(); } setText(name); emit finishedTextResolving(); } } /** * Helper class for openSubDirsMenu */ class NaturalLessThan { public: NaturalLessThan() { m_collator.setCaseSensitivity(Qt::CaseInsensitive); m_collator.setNumericMode(true); } bool operator()(const QPair &s1, const QPair &s2) { return m_collator.compare(s1.first, s2.first) < 0; } private: QCollator m_collator; }; void KUrlNavigatorButton::openSubDirsMenu(KJob *job) { Q_ASSERT(job == m_subDirsJob); m_subDirsJob = nullptr; if (job->error() || m_subDirs.isEmpty()) { // clear listing return; } NaturalLessThan nlt; std::sort(m_subDirs.begin(), m_subDirs.end(), nlt); setDisplayHintEnabled(PopupActiveHint, true); update(); // ensure the button is drawn highlighted if (m_subDirsMenu != nullptr) { m_subDirsMenu->close(); m_subDirsMenu->deleteLater(); m_subDirsMenu = nullptr; } m_subDirsMenu = new KUrlNavigatorMenu(this); initMenu(m_subDirsMenu, 0); const bool leftToRight = (layoutDirection() == Qt::LeftToRight); const int popupX = leftToRight ? width() - arrowWidth() - BorderWidth : 0; const QPoint popupPos = parentWidget()->mapToGlobal(geometry().bottomLeft() + QPoint(popupX, 0)); QPointer guard(this); m_subDirsMenu->exec(popupPos); // If 'this' has been deleted in the menu's nested event loop, we have to return // immediately because any access to a member variable might cause a crash. if (!guard) { return; } m_subDirs.clear(); delete m_subDirsMenu; m_subDirsMenu = nullptr; setDisplayHintEnabled(PopupActiveHint, false); } void KUrlNavigatorButton::replaceButton(KJob *job) { Q_ASSERT(job == m_subDirsJob); m_subDirsJob = nullptr; m_replaceButton = false; if (job->error() || m_subDirs.isEmpty()) { return; } NaturalLessThan nlt; std::sort(m_subDirs.begin(), m_subDirs.end(), nlt); // Get index of the directory that is shown currently in the button const QString currentDir = m_url.fileName(); int currentIndex = 0; const int subDirsCount = m_subDirs.count(); while (currentIndex < subDirsCount) { if (m_subDirs[currentIndex].first == currentDir) { break; } ++currentIndex; } // Adjust the index by respecting the wheel steps and // trigger a replacing of the button content int targetIndex = currentIndex - m_wheelSteps; if (targetIndex < 0) { targetIndex = 0; } else if (targetIndex >= subDirsCount) { targetIndex = subDirsCount - 1; } QUrl url(KIO::upUrl(m_url)); url.setPath(concatPaths(url.path(), m_subDirs[targetIndex].first)); emit clicked(url, Qt::LeftButton, Qt::NoModifier); m_subDirs.clear(); } void KUrlNavigatorButton::cancelSubDirsRequest() { m_openSubDirsTimer->stop(); if (m_subDirsJob != nullptr) { m_subDirsJob->kill(); m_subDirsJob = nullptr; } } QString KUrlNavigatorButton::plainText() const { // Replace all "&&" by '&' and remove all single // '&' characters const QString source = text(); const int sourceLength = source.length(); QString dest; dest.reserve(sourceLength); int sourceIndex = 0; int destIndex = 0; while (sourceIndex < sourceLength) { if (source.at(sourceIndex) == QLatin1Char('&')) { ++sourceIndex; if (sourceIndex >= sourceLength) { break; } } dest[destIndex] = source.at(sourceIndex); ++sourceIndex; ++destIndex; } return dest; } int KUrlNavigatorButton::arrowWidth() const { // if there isn't arrow then return 0 int width = 0; if (!m_subDir.isEmpty()) { width = height() / 2; if (width < 4) { width = 4; } } return width; } bool KUrlNavigatorButton::isAboveArrow(int x) const { const bool leftToRight = (layoutDirection() == Qt::LeftToRight); return leftToRight ? (x >= width() - arrowWidth()) : (x < arrowWidth()); } bool KUrlNavigatorButton::isTextClipped() const { int availableWidth = width() - 2 * BorderWidth; if (!m_subDir.isEmpty()) { availableWidth -= arrowWidth() - BorderWidth; } QFont adjustedFont(font()); adjustedFont.setBold(m_subDir.isEmpty()); - return QFontMetrics(adjustedFont).width(plainText()) >= availableWidth; + return QFontMetrics(adjustedFont).boundingRect(plainText()).width() >= availableWidth; } void KUrlNavigatorButton::updateMinimumWidth() { const int oldMinWidth = minimumWidth(); int minWidth = sizeHint().width(); if (minWidth < 40) { minWidth = 40; } else if (minWidth > 150) { // don't let an overlong path name waste all the URL navigator space minWidth = 150; } if (oldMinWidth != minWidth) { setMinimumWidth(minWidth); } } void KUrlNavigatorButton::initMenu(KUrlNavigatorMenu *menu, int startIndex) { connect(menu, &KUrlNavigatorMenu::mouseButtonClicked, this, &KUrlNavigatorButton::slotMenuActionClicked); connect(menu, &KUrlNavigatorMenu::urlsDropped, this, QOverload::of(&KUrlNavigatorButton::urlsDropped)); menu->setLayoutDirection(Qt::LeftToRight); const int maxIndex = startIndex + 30; // Don't show more than 30 items in a menu const int lastIndex = qMin(m_subDirs.count() - 1, maxIndex); for (int i = startIndex; i <= lastIndex; ++i) { const QString subDirName = m_subDirs[i].first; const QString subDirDisplayName = m_subDirs[i].second; QString text = KStringHandler::csqueeze(subDirDisplayName, 60); text.replace(QLatin1Char('&'), QLatin1String("&&")); QAction *action = new QAction(text, this); if (m_subDir == subDirName) { QFont font(action->font()); font.setBold(true); action->setFont(font); } action->setData(i); menu->addAction(action); } if (m_subDirs.count() > maxIndex) { // If too much items are shown, move them into a sub menu menu->addSeparator(); KUrlNavigatorMenu *subDirsMenu = new KUrlNavigatorMenu(menu); subDirsMenu->setTitle(i18nc("@action:inmenu", "More")); initMenu(subDirsMenu, maxIndex); menu->addMenu(subDirsMenu); } } } // namespace KDEPrivate #include "moc_kurlnavigatorbutton_p.cpp" diff --git a/src/filewidgets/kurlnavigatorprotocolcombo.cpp b/src/filewidgets/kurlnavigatorprotocolcombo.cpp index 258b2f8f..f66345e1 100644 --- a/src/filewidgets/kurlnavigatorprotocolcombo.cpp +++ b/src/filewidgets/kurlnavigatorprotocolcombo.cpp @@ -1,234 +1,233 @@ /*************************************************************************** * Copyright (C) 2006 by Aaron J. Seigo () * * Copyright (C) 2009 by Peter Penz () * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with this library; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kurlnavigatorprotocolcombo_p.h" #include #include #include #include #include #include #include #include #include namespace { const int ArrowSize = 10; } namespace KDEPrivate { KUrlNavigatorProtocolCombo::KUrlNavigatorProtocolCombo(const QString &protocol, QWidget *parent) : KUrlNavigatorButtonBase(parent), m_menu(nullptr), m_protocols(), m_categories() { m_menu = new QMenu(this); connect(m_menu, &QMenu::triggered, this, QOverload::of(&KUrlNavigatorProtocolCombo::setProtocol)); setText(protocol); setMenu(m_menu); } void KUrlNavigatorProtocolCombo::setCustomProtocols(const QStringList &protocols) { m_protocols = protocols; m_menu->clear(); for (const QString &protocol : protocols) { QAction *action = m_menu->addAction(protocol); action->setData(protocol); } } QSize KUrlNavigatorProtocolCombo::sizeHint() const { const QSize size = KUrlNavigatorButtonBase::sizeHint(); - QFontMetrics fontMetrics(font()); - int width = fontMetrics.width(KLocalizedString::removeAcceleratorMarker(text())); + int width = fontMetrics().boundingRect(KLocalizedString::removeAcceleratorMarker(text())).width(); width += (3 * BorderWidth) + ArrowSize; return QSize(width, size.height()); } void KUrlNavigatorProtocolCombo::setProtocol(const QString &protocol) { setText(protocol); } QString KUrlNavigatorProtocolCombo::currentProtocol() const { return text(); } void KUrlNavigatorProtocolCombo::showEvent(QShowEvent *event) { KUrlNavigatorButtonBase::showEvent(event); if (!event->spontaneous() && m_protocols.isEmpty()) { m_protocols = KProtocolInfo::protocols(); std::sort(m_protocols.begin(), m_protocols.end()); QStringList::iterator it = m_protocols.begin(); while (it != m_protocols.end()) { QUrl url; url.setScheme(*it); if (!KProtocolManager::supportsListing(url)) { it = m_protocols.erase(it); } else { ++it; } } updateMenu(); } } void KUrlNavigatorProtocolCombo::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); const int buttonWidth = width(); const int buttonHeight = height(); drawHoverBackground(&painter); const QColor fgColor = foregroundColor(); painter.setPen(fgColor); // draw arrow const int arrowX = buttonWidth - ArrowSize - BorderWidth; const int arrowY = (buttonHeight - ArrowSize) / 2; QStyleOption option; option.rect = QRect(arrowX, arrowY, ArrowSize, ArrowSize); option.palette = palette(); option.palette.setColor(QPalette::Text, fgColor); option.palette.setColor(QPalette::WindowText, fgColor); option.palette.setColor(QPalette::ButtonText, fgColor); style()->drawPrimitive(QStyle::PE_IndicatorArrowDown, &option, &painter, this); // draw text const int textWidth = arrowX - (2 * BorderWidth); int alignment = Qt::AlignCenter | Qt::TextShowMnemonic; if (!style()->styleHint(QStyle::SH_UnderlineShortcut, &option, this)) { alignment |= Qt::TextHideMnemonic; } style()->drawItemText(&painter, QRect(BorderWidth, 0, textWidth, buttonHeight), alignment, option.palette, isEnabled(), text()); } void KUrlNavigatorProtocolCombo::setProtocol(QAction *action) { const QString protocol = action->data().toString(); setText(protocol); emit activated(protocol); } void KUrlNavigatorProtocolCombo::updateMenu() { initializeCategories(); std::sort(m_protocols.begin(), m_protocols.end()); // move all protocols into the corresponding category of 'items' QList items[CategoryCount]; foreach (const QString &protocol, m_protocols) { if (m_categories.contains(protocol)) { const ProtocolCategory category = m_categories.value(protocol); items[category].append(protocol); } else { items[OtherCategory].append(protocol); } } // Create the menu that includes all entries from 'items'. The categories // CoreCategory and PlacesCategory are placed at the top level, the remaining // categories are placed in sub menus. QMenu *menu = m_menu; for (int category = 0; category < CategoryCount; ++category) { if (!items[category].isEmpty()) { switch (category) { case DevicesCategory: menu = m_menu->addMenu(i18nc("@item:inmenu", "Devices")); break; case SubversionCategory: menu = m_menu->addMenu(i18nc("@item:inmenu", "Subversion")); break; case OtherCategory: menu = m_menu->addMenu(i18nc("@item:inmenu", "Other")); break; case CoreCategory: case PlacesCategory: default: break; } foreach (const QString &protocol, items[category]) { QAction *action = menu->addAction(protocol); action->setData(protocol); } if (menu == m_menu) { menu->addSeparator(); } } } } void KUrlNavigatorProtocolCombo::initializeCategories() { if (m_categories.isEmpty()) { m_categories.insert(QStringLiteral("file"), CoreCategory); m_categories.insert(QStringLiteral("ftp"), CoreCategory); m_categories.insert(QStringLiteral("fish"), CoreCategory); m_categories.insert(QStringLiteral("nfs"), CoreCategory); m_categories.insert(QStringLiteral("sftp"), CoreCategory); m_categories.insert(QStringLiteral("smb"), CoreCategory); m_categories.insert(QStringLiteral("webdav"), CoreCategory); m_categories.insert(QStringLiteral("desktop"), PlacesCategory); m_categories.insert(QStringLiteral("fonts"), PlacesCategory); m_categories.insert(QStringLiteral("programs"), PlacesCategory); m_categories.insert(QStringLiteral("settings"), PlacesCategory); m_categories.insert(QStringLiteral("trash"), PlacesCategory); m_categories.insert(QStringLiteral("floppy"), DevicesCategory); m_categories.insert(QStringLiteral("camera"), DevicesCategory); m_categories.insert(QStringLiteral("remote"), DevicesCategory); m_categories.insert(QStringLiteral("svn"), SubversionCategory); m_categories.insert(QStringLiteral("svn+file"), SubversionCategory); m_categories.insert(QStringLiteral("svn+http"), SubversionCategory); m_categories.insert(QStringLiteral("svn+https"), SubversionCategory); m_categories.insert(QStringLiteral("svn+ssh"), SubversionCategory); } } } // namespace KDEPrivate #include "moc_kurlnavigatorprotocolcombo_p.cpp" diff --git a/src/ioslaves/ftp/ftp.cpp b/src/ioslaves/ftp/ftp.cpp index 2c07ceb9..d0ef85b6 100644 --- a/src/ioslaves/ftp/ftp.cpp +++ b/src/ioslaves/ftp/ftp.cpp @@ -1,2691 +1,2691 @@ /* 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 = QStringLiteral("/"); } 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, &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 != QLatin1String(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 " + user.toLatin1(); if (m_proxyURL.isValid()) { tempbuf += '@' + m_host.toLatin1(); if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { tempbuf += ':' + QByteArray::number(m_port); } } 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 = QString::fromUtf8(ftpResponse(0)); qCDebug(KIO_FTP) << "Login failed: " << lastServerResponse; ++failedAuth; continue; // Well we failed, prompt the user please!! } if (needPass) { tempbuf = "PASS " + 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 = QString::fromUtf8(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)); const int iBeg = sTmp.indexOf(QLatin1Char('"')); const int iEnd = sTmp.lastIndexOf(QLatin1Char('"')); if (iBeg > 0 && iBeg < iEnd) { m_initialPath = sTmp.mid(iBeg + 1, iEnd - iBeg - 1); if (m_initialPath[0] != QLatin1Char('/')) { m_initialPath.prepend(QLatin1Char('/')); } 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(QLatin1Char('\n'), QString::SkipEmptyParts); for (QStringList::const_iterator it = list.begin(); it != list.end(); ++it) { if ((*it).startsWith(QLatin1String("init"))) { const QStringList list2 = macro.split(QLatin1Char('\\'), 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... const QByteArray buf = cmd + "\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]); + command = QStringLiteral("PORT %1,%2,%3,%4,%5,%6").arg(pData[3]).arg(pData[2]).arg(pData[1]).arg(pData[0]).arg(pData[5]).arg(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 += ' ' + 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 code here depends on the command errormessage = _path + i18n("\nThe server said: \"%1\"", QString::fromUtf8(ftpResponse(0)).trimmed()); } 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(QLatin1Char('/')); if (pos >= 0) { if (!ftpFolder(src.left(pos + 1), false)) { return false; } } const QByteArray from_cmd = "RNFR " + remoteEncoding()->encode(src.mid(pos + 1)); if (!ftpSendCmd(from_cmd) || (m_iRespType != 3)) { error(ERR_CANNOT_RENAME, src); return false; } const QByteArray to_cmd = "RNTO " + 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 } const QByteArray cmd = (isfile ? "DELE " : "RMD ") + 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) const QByteArray cmd = "SITE CHMOD " + QByteArray::number(permissions & 0777/*octal*/, 8 /*octal*/) + ' ' + remoteEncoding()->encode(path); ftpSendCmd(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.toSecsSinceEpoch()); 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(QLatin1String("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 = QStringLiteral("/"); } 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', KJob::NoError)) { // Servers running with Turkish locale having problems converting 'i' letter to upper case. // So we send correct upper case command as last resort. if (!ftpOpenCommand("LIST -la", 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(); const QString dest_part = dest_orig + QLatin1String(".part"); if (ftpSize(dest_orig, 'I')) { if (m_size == 0) { // delete files with zero size const QByteArray cmd = "DELE " + 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 const QByteArray cmd = "DELE " + 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))) { const QByteArray cmd = "DELE " + 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; } const QByteArray buf = "SIZE " + 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) { const QByteArray buf = "SIZE " + 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; } const QByteArray buf = QByteArrayLiteral("TYPE ") + 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] == QLatin1Char('/')) { newPath.chop(1); } qCDebug(KIO_FTP) << "want" << newPath << "has" << m_currentPath; if (m_currentPath == newPath) { return true; } const QByteArray tmp = "cwd " + 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); } 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); } 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 (cs == statusServerError && 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; info.refresh(); utbuf.actime = info.lastRead().toSecsSinceEpoch(); // access time, unchanged utbuf.modtime = dt.toSecsSinceEpoch(); // 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, &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, &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 45d93754..24f94139 100644 --- a/src/ioslaves/http/http.cpp +++ b/src/ioslaves/http/http.cpp @@ -1,5626 +1,5626 @@ /* Copyright (C) 2000-2003 Waldo Bastian Copyright (C) 2000-2002 George Staikos Copyright (C) 2000-2002 Dawit Alemayehu Copyright (C) 2001,2002 Hamish Rodda Copyright (C) 2007 Nick Shaforostoff Copyright (C) 2007-2018 Daniel Nicoletti Copyright (C) 2008,2009 Andreas Hartmetz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // TODO delete / do not save very big files; "very big" to be defined #include "http.h" #include #include // must be explicitly included for MacOSX #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "httpauthentication.h" #include "kioglobal_p.h" #include Q_DECLARE_LOGGING_CATEGORY(KIO_HTTP) Q_LOGGING_CATEGORY(KIO_HTTP, "kf5.kio.kio_http", QtWarningMsg) // disable debug by default // HeaderTokenizer declarations #include "parsinghelpers.h" //string parsing helpers and HeaderTokenizer implementation #include "parsinghelpers.cpp" // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.http" FILE "http.json") }; static bool supportedProxyScheme(const QString &scheme) { // scheme is supposed to be lowercase return (scheme.startsWith(QLatin1String("http")) || scheme == QLatin1String("socks")); } // see filenameFromUrl(): a sha1 hash is 160 bits static const int s_hashedUrlBits = 160; // this number should always be divisible by eight static const int s_hashedUrlNibbles = s_hashedUrlBits / 4; static const int s_MaxInMemPostBufSize = 256 * 1024; // Write anything over 256 KB to file... using namespace KIO; extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) { QCoreApplication app(argc, argv); // needed for QSocketNotifier app.setApplicationName(QStringLiteral("kio_http")); if (argc != 4) { fprintf(stderr, "Usage: kio_http protocol domain-socket1 domain-socket2\n"); exit(-1); } HTTPProtocol slave(argv[1], argv[2], argv[3]); slave.dispatchLoop(); return 0; } /*********************************** Generic utility functions ********************/ static QString toQString(const QByteArray &value) { return QString::fromLatin1(value.constData(), value.size()); } static bool isCrossDomainRequest(const QString &fqdn, const QString &originURL) { //TODO read the RFC if (originURL == QLatin1String("true")) { // Backwards compatibility return true; } QUrl url(originURL); // Document Origin domain QString a = url.host(); // Current request domain QString b = fqdn; if (a == b) { return false; } QStringList la = a.split(QLatin1Char('.'), QString::SkipEmptyParts); QStringList lb = b.split(QLatin1Char('.'), QString::SkipEmptyParts); if (qMin(la.count(), lb.count()) < 2) { return true; // better safe than sorry... } while (la.count() > 2) { la.pop_front(); } while (lb.count() > 2) { lb.pop_front(); } return la != lb; } /* Eliminates any custom header that could potentially alter the request */ static QString sanitizeCustomHTTPHeader(const QString &_header) { QString sanitizedHeaders; const QStringList headers = _header.split(QRegExp(QStringLiteral("[\r\n]"))); for (QStringList::ConstIterator it = headers.begin(); it != headers.end(); ++it) { // Do not allow Request line to be specified and ignore // the other HTTP headers. if (!(*it).contains(QLatin1Char(':')) || (*it).startsWith(QLatin1String("host"), Qt::CaseInsensitive) || (*it).startsWith(QLatin1String("proxy-authorization"), Qt::CaseInsensitive) || (*it).startsWith(QLatin1String("via"), Qt::CaseInsensitive)) { continue; } sanitizedHeaders += (*it) + QLatin1String("\r\n"); } sanitizedHeaders.chop(2); return sanitizedHeaders; } static bool isPotentialSpoofingAttack(const HTTPProtocol::HTTPRequest &request, const KConfigGroup *config) { qCDebug(KIO_HTTP) << request.url << "response code: " << request.responseCode << "previous response code:" << request.prevResponseCode; if (config->readEntry("no-spoof-check", false)) { return false; } if (request.url.userName().isEmpty()) { return false; } // We already have cached authentication. if (config->readEntry(QStringLiteral("cached-www-auth"), false)) { return false; } const QString userName = config->readEntry(QStringLiteral("LastSpoofedUserName"), QString()); return ((userName.isEmpty() || userName != request.url.userName()) && request.responseCode != 401 && request.prevResponseCode != 401); } // for a given response code, conclude if the response is going to/likely to have a response body static bool canHaveResponseBody(int responseCode, KIO::HTTP_METHOD method) { /* RFC 2616 says... 1xx: false 200: method HEAD: false, otherwise:true 201: true 202: true 203: see 200 204: false 205: false 206: true 300: see 200 301: see 200 302: see 200 303: see 200 304: false 305: probably like 300, RFC seems to expect disconnection afterwards... 306: (reserved), for simplicity do it just like 200 307: see 200 4xx: see 200 5xx :see 200 */ if (responseCode >= 100 && responseCode < 200) { return false; } switch (responseCode) { case 201: case 202: case 206: // RFC 2616 does not mention HEAD in the description of the above. if the assert turns out // to be a problem the response code should probably be treated just like 200 and friends. Q_ASSERT(method != HTTP_HEAD); return true; case 204: case 205: case 304: return false; default: break; } // safe (and for most remaining response codes exactly correct) default return method != HTTP_HEAD; } static bool isEncryptedHttpVariety(const QByteArray &p) { return p == "https" || p == "webdavs"; } static bool isValidProxy(const QUrl &u) { return u.isValid() && !u.host().isEmpty(); } static bool isHttpProxy(const QUrl &u) { return isValidProxy(u) && u.scheme() == QLatin1String("http"); } static QIODevice *createPostBufferDeviceFor(KIO::filesize_t size) { QIODevice *device; if (size > static_cast(s_MaxInMemPostBufSize)) { device = new QTemporaryFile; } else { device = new QBuffer; } if (!device->open(QIODevice::ReadWrite)) { return nullptr; } return device; } QByteArray HTTPProtocol::HTTPRequest::methodString() const { if (!methodStringOverride.isEmpty()) { return (methodStringOverride).toLatin1(); } switch (method) { case HTTP_GET: return "GET"; case HTTP_PUT: return "PUT"; case HTTP_POST: return "POST"; case HTTP_HEAD: return "HEAD"; case HTTP_DELETE: return "DELETE"; case HTTP_OPTIONS: return "OPTIONS"; case DAV_PROPFIND: return "PROPFIND"; case DAV_PROPPATCH: return "PROPPATCH"; case DAV_MKCOL: return "MKCOL"; case DAV_COPY: return "COPY"; case DAV_MOVE: return "MOVE"; case DAV_LOCK: return "LOCK"; case DAV_UNLOCK: return "UNLOCK"; case DAV_SEARCH: return "SEARCH"; case DAV_SUBSCRIBE: return "SUBSCRIBE"; case DAV_UNSUBSCRIBE: return "UNSUBSCRIBE"; case DAV_POLL: return "POLL"; case DAV_NOTIFY: return "NOTIFY"; case DAV_REPORT: return "REPORT"; default: Q_ASSERT(false); return QByteArray(); } } static QString formatHttpDate(const QDateTime &date) { return QLocale::c().toString(date, QStringLiteral("ddd, dd MMM yyyy hh:mm:ss 'GMT'")); } static bool isAuthenticationRequired(int responseCode) { return (responseCode == 401) || (responseCode == 407); } static void changeProtocolToHttp(QUrl* url) { const QString protocol(url->scheme()); if (protocol == QLatin1String("webdavs")) { url->setScheme(QStringLiteral("https")); } else if (protocol == QLatin1String("webdav")) { url->setScheme(QStringLiteral("http")); } } #define NO_SIZE ((KIO::filesize_t) -1) #if HAVE_STRTOLL #define STRTOLL strtoll #else #define STRTOLL strtol #endif /************************************** HTTPProtocol **********************************************/ HTTPProtocol::HTTPProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app) : TCPSlaveBase(protocol, pool, app, isEncryptedHttpVariety(protocol)) , m_iSize(NO_SIZE) , m_iPostDataSize(NO_SIZE) , m_isBusy(false) , m_POSTbuf(nullptr) , m_maxCacheAge(DEFAULT_MAX_CACHE_AGE) , m_maxCacheSize(DEFAULT_MAX_CACHE_SIZE) , m_protocol(protocol) , m_wwwAuth(nullptr) , m_triedWwwCredentials(NoCredentials) , m_proxyAuth(nullptr) , m_triedProxyCredentials(NoCredentials) , m_socketProxyAuth(nullptr) , m_networkConfig(nullptr) , m_kioError(0) , m_isLoadingErrorPage(false) , m_remoteRespTimeout(DEFAULT_RESPONSE_TIMEOUT) , m_iEOFRetryCount(0) { reparseConfiguration(); setBlocking(true); connect(socket(), SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)), this, SLOT(proxyAuthenticationForSocket(QNetworkProxy,QAuthenticator*))); } HTTPProtocol::~HTTPProtocol() { httpClose(false); } void HTTPProtocol::reparseConfiguration() { qCDebug(KIO_HTTP); delete m_proxyAuth; delete m_wwwAuth; m_proxyAuth = nullptr; m_wwwAuth = nullptr; m_request.proxyUrl.clear(); //TODO revisit m_request.proxyUrls.clear(); TCPSlaveBase::reparseConfiguration(); } void HTTPProtocol::resetConnectionSettings() { m_isEOF = false; m_kioError = 0; m_isLoadingErrorPage = false; } quint16 HTTPProtocol::defaultPort() const { return isEncryptedHttpVariety(m_protocol) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT; } void HTTPProtocol::resetResponseParsing() { m_isRedirection = false; m_isChunked = false; m_iSize = NO_SIZE; clearUnreadBuffer(); m_responseHeaders.clear(); m_contentEncodings.clear(); m_transferEncodings.clear(); m_contentMD5.clear(); m_mimeType.clear(); setMetaData(QStringLiteral("request-id"), m_request.id); } void HTTPProtocol::resetSessionSettings() { // Follow HTTP/1.1 spec and enable keep-alive by default // unless the remote side tells us otherwise or we determine // the persistent link has been terminated by the remote end. m_request.isKeepAlive = true; m_request.keepAliveTimeout = 0; m_request.redirectUrl = QUrl(); m_request.useCookieJar = config()->readEntry("Cookies", false); m_request.cacheTag.useCache = config()->readEntry("UseCache", true); m_request.preferErrorPage = config()->readEntry("errorPage", true); const bool noAuth = config()->readEntry("no-auth", false); m_request.doNotWWWAuthenticate = config()->readEntry("no-www-auth", noAuth); m_request.doNotProxyAuthenticate = config()->readEntry("no-proxy-auth", noAuth); m_strCacheDir = config()->readPathEntry("CacheDir", QString()); m_maxCacheAge = config()->readEntry("MaxCacheAge", DEFAULT_MAX_CACHE_AGE); m_request.windowId = config()->readEntry("window-id"); m_request.methodStringOverride = metaData(QStringLiteral("CustomHTTPMethod")); m_request.sentMethodString.clear(); qCDebug(KIO_HTTP) << "Window Id =" << m_request.windowId; qCDebug(KIO_HTTP) << "ssl_was_in_use =" << metaData(QStringLiteral("ssl_was_in_use")); m_request.referrer.clear(); // RFC 2616: do not send the referrer if the referrer page was served using SSL and // the current page does not use SSL. if (config()->readEntry("SendReferrer", true) && (isEncryptedHttpVariety(m_protocol) || metaData(QStringLiteral("ssl_was_in_use")) != QLatin1String("TRUE"))) { QUrl refUrl(metaData(QStringLiteral("referrer"))); if (refUrl.isValid()) { // Sanitize QString protocol = refUrl.scheme(); if (protocol.startsWith(QLatin1String("webdav"))) { protocol.replace(0, 6, QStringLiteral("http")); refUrl.setScheme(protocol); } if (protocol.startsWith(QLatin1String("http"))) { m_request.referrer = toQString(refUrl.toEncoded(QUrl::RemoveUserInfo | QUrl::RemoveFragment)); } } } if (config()->readEntry("SendLanguageSettings", true)) { m_request.charsets = config()->readEntry("Charsets", DEFAULT_PARTIAL_CHARSET_HEADER); if (!m_request.charsets.contains(QLatin1String("*;"), Qt::CaseInsensitive)) { m_request.charsets += QLatin1String(",*;q=0.5"); } m_request.languages = config()->readEntry("Languages", DEFAULT_LANGUAGE_HEADER); } else { m_request.charsets.clear(); m_request.languages.clear(); } // Adjust the offset value based on the "range-start" meta-data. QString resumeOffset = metaData(QStringLiteral("range-start")); if (resumeOffset.isEmpty()) { resumeOffset = metaData(QStringLiteral("resume")); // old name } if (!resumeOffset.isEmpty()) { m_request.offset = resumeOffset.toULongLong(); } else { m_request.offset = 0; } // Same procedure for endoffset. QString resumeEndOffset = metaData(QStringLiteral("range-end")); if (resumeEndOffset.isEmpty()) { resumeEndOffset = metaData(QStringLiteral("resume_until")); // old name } if (!resumeEndOffset.isEmpty()) { m_request.endoffset = resumeEndOffset.toULongLong(); } else { m_request.endoffset = 0; } m_request.disablePassDialog = config()->readEntry("DisablePassDlg", false); m_request.allowTransferCompression = config()->readEntry("AllowCompressedPage", true); m_request.id = metaData(QStringLiteral("request-id")); // Store user agent for this host. if (config()->readEntry("SendUserAgent", true)) { m_request.userAgent = metaData(QStringLiteral("UserAgent")); } else { m_request.userAgent.clear(); } m_request.cacheTag.etag.clear(); m_request.cacheTag.servedDate = QDateTime(); m_request.cacheTag.lastModifiedDate = QDateTime(); m_request.cacheTag.expireDate = QDateTime(); m_request.responseCode = 0; m_request.prevResponseCode = 0; delete m_wwwAuth; m_wwwAuth = nullptr; delete m_socketProxyAuth; m_socketProxyAuth = nullptr; m_blacklistedWwwAuthMethods.clear(); m_triedWwwCredentials = NoCredentials; m_blacklistedProxyAuthMethods.clear(); m_triedProxyCredentials = NoCredentials; // Obtain timeout values m_remoteRespTimeout = responseTimeout(); // Bounce back the actual referrer sent setMetaData(QStringLiteral("referrer"), m_request.referrer); // Reset the post data size m_iPostDataSize = NO_SIZE; // Reset the EOF retry counter m_iEOFRetryCount = 0; } void HTTPProtocol::setHost(const QString &host, quint16 port, const QString &user, const QString &pass) { // Reset the webdav-capable flags for this host if (m_request.url.host() != host) { m_davHostOk = m_davHostUnsupported = false; } m_request.url.setHost(host); // is it an IPv6 address? if (host.indexOf(QLatin1Char(':')) == -1) { m_request.encoded_hostname = toQString(QUrl::toAce(host)); } else { int pos = host.indexOf(QLatin1Char('%')); if (pos == -1) { m_request.encoded_hostname = QLatin1Char('[') + host + QLatin1Char(']'); } else // don't send the scope-id in IPv6 addresses to the server { m_request.encoded_hostname = QLatin1Char('[') + host.leftRef(pos) + QLatin1Char(']'); } } m_request.url.setPort((port > 0 && port != defaultPort()) ? port : -1); m_request.url.setUserName(user); m_request.url.setPassword(pass); // On new connection always clear previous proxy information... m_request.proxyUrl.clear(); m_request.proxyUrls.clear(); qCDebug(KIO_HTTP) << "Hostname is now:" << m_request.url.host() << "(" << m_request.encoded_hostname << ")"; } bool HTTPProtocol::maybeSetRequestUrl(const QUrl &u) { qCDebug(KIO_HTTP) << u; m_request.url = u; m_request.url.setPort(u.port(defaultPort()) != defaultPort() ? u.port() : -1); if (u.host().isEmpty()) { error(KIO::ERR_UNKNOWN_HOST, i18n("No host specified.")); return false; } if (u.path().isEmpty()) { QUrl newUrl(u); newUrl.setPath(QStringLiteral("/")); redirection(newUrl); finished(); return false; } return true; } void HTTPProtocol::proceedUntilResponseContent(bool dataInternal /* = false */) { qCDebug(KIO_HTTP); const bool status = proceedUntilResponseHeader() && readBody(dataInternal || m_kioError); // If not an error condition or internal request, close // the connection based on the keep alive settings... if (!m_kioError && !dataInternal) { httpClose(m_request.isKeepAlive); } // if data is required internally or we got error, don't finish, // it is processed before we finish() if (dataInternal || !status) { return; } if (!sendHttpError()) { finished(); } } bool HTTPProtocol::proceedUntilResponseHeader() { qCDebug(KIO_HTTP); // Retry the request until it succeeds or an unrecoverable error occurs. // Recoverable errors are, for example: // - Proxy or server authentication required: Ask for credentials and try again, // this time with an authorization header in the request. // - Server-initiated timeout on keep-alive connection: Reconnect and try again while (true) { if (!sendQuery()) { return false; } if (readResponseHeader()) { // Success, finish the request. break; } // If not loading error page and the response code requires us to resend the query, // then throw away any error message that might have been sent by the server. if (!m_isLoadingErrorPage && isAuthenticationRequired(m_request.responseCode)) { // This gets rid of any error page sent with 401 or 407 authentication required response... readBody(true); } // no success, close the cache file so the cache state is reset - that way most other code // doesn't have to deal with the cache being in various states. cacheFileClose(); if (m_kioError || m_isLoadingErrorPage) { // Unrecoverable error, abort everything. // Also, if we've just loaded an error page there is nothing more to do. // In that case we abort to avoid loops; some webservers manage to send 401 and // no authentication request. Or an auth request we don't understand. setMetaData(QStringLiteral("responsecode"), QString::number(m_request.responseCode)); return false; } if (!m_request.isKeepAlive) { httpCloseConnection(); m_request.isKeepAlive = true; m_request.keepAliveTimeout = 0; } } // Do not save authorization if the current response code is // 4xx (client error) or 5xx (server error). qCDebug(KIO_HTTP) << "Previous Response:" << m_request.prevResponseCode; qCDebug(KIO_HTTP) << "Current Response:" << m_request.responseCode; setMetaData(QStringLiteral("responsecode"), QString::number(m_request.responseCode)); setMetaData(QStringLiteral("content-type"), m_mimeType); // At this point sendBody() should have delivered any POST data. clearPostDataBuffer(); return true; } void HTTPProtocol::stat(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); if (m_protocol != "webdav" && m_protocol != "webdavs") { QString statSide = metaData(QStringLiteral("statSide")); if (statSide != QLatin1String("source")) { // When uploading we assume the file doesn't exit error(ERR_DOES_NOT_EXIST, url.toDisplayString()); return; } // When downloading we assume it exists UDSEntry entry; entry.fastInsert(KIO::UDSEntry::UDS_NAME, url.fileName()); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); // a file entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH); // readable by everybody statEntry(entry); finished(); return; } davStatList(url); } void HTTPProtocol::listDir(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); davStatList(url, false); } void HTTPProtocol::davSetRequest(const QByteArray &requestXML) { // insert the document into the POST buffer, kill trailing zero byte cachePostData(requestXML); } void HTTPProtocol::davStatList(const QUrl &url, bool stat) { UDSEntry entry; // check to make sure this host supports WebDAV if (!davHostOk()) { return; } QMimeDatabase db; // Maybe it's a disguised SEARCH... QString query = metaData(QStringLiteral("davSearchQuery")); if (!query.isEmpty()) { const QByteArray request = "\r\n" "\r\n" + query.toUtf8() + "\r\n"; davSetRequest(request); } else { // We are only after certain features... QByteArray request = QByteArrayLiteral("" ""); // insert additional XML request from the davRequestResponse metadata if (hasMetaData(QStringLiteral("davRequestResponse"))) { request += metaData(QStringLiteral("davRequestResponse")).toUtf8(); } else { // No special request, ask for default properties request += "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } request += ""; davSetRequest(request); } // WebDAV Stat or List... m_request.method = query.isEmpty() ? DAV_PROPFIND : DAV_SEARCH; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_request.davData.depth = stat ? 0 : 1; if (!stat) { if (!m_request.url.path().endsWith(QLatin1Char('/'))) { m_request.url.setPath(m_request.url.path() + QLatin1Char('/')); } } proceedUntilResponseContent(true); infoMessage(QLatin1String("")); // Has a redirection already been called? If so, we're done. if (m_isRedirection || m_kioError) { if (m_isRedirection) { davFinished(); } return; } QDomDocument multiResponse; multiResponse.setContent(m_webDavDataBuf, true); bool hasResponse = false; qCDebug(KIO_HTTP) << endl << multiResponse.toString(2); for (QDomNode n = multiResponse.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement thisResponse = n.toElement(); if (thisResponse.isNull()) { continue; } hasResponse = true; QDomElement href = thisResponse.namedItem(QStringLiteral("href")).toElement(); if (!href.isNull()) { entry.clear(); const QUrl thisURL(href.text()); // href.text() is a percent-encoded url. if (thisURL.isValid()) { const QUrl adjustedThisURL = thisURL.adjusted(QUrl::StripTrailingSlash); const QUrl adjustedUrl = url.adjusted(QUrl::StripTrailingSlash); // base dir of a listDir(): name should be "." QString name; if (!stat && adjustedThisURL.path() == adjustedUrl.path()) { name = QLatin1Char('.'); } else { name = adjustedThisURL.fileName(); } entry.fastInsert(KIO::UDSEntry::UDS_NAME, name.isEmpty() ? href.text() : name); } QDomNodeList propstats = thisResponse.elementsByTagName(QStringLiteral("propstat")); davParsePropstats(propstats, entry); // Since a lot of webdav servers seem not to send the content-type information // for the requested directory listings, we attempt to guess the mime-type from // the resource name so long as the resource is not a directory. if (entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE).isEmpty() && entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE) != S_IFDIR) { QMimeType mime = db.mimeTypeForFile(thisURL.path(), QMimeDatabase::MatchExtension); if (mime.isValid() && !mime.isDefault()) { qCDebug(KIO_HTTP) << "Setting" << mime.name() << "as guessed mime type for" << thisURL.path(); entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, mime.name()); } } if (stat) { // return an item statEntry(entry); davFinished(); return; } listEntry(entry); } else { qCDebug(KIO_HTTP) << "Error: no URL contained in response to PROPFIND on" << url; } } if (stat || !hasResponse) { error(ERR_DOES_NOT_EXIST, url.toDisplayString()); return; } davFinished(); } void HTTPProtocol::davGeneric(const QUrl &url, KIO::HTTP_METHOD method, qint64 size) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // WebDAV method m_request.method = method; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_iPostDataSize = (size > -1 ? static_cast(size) : NO_SIZE); proceedUntilResponseContent(); } int HTTPProtocol::codeFromResponse(const QString &response) { const int firstSpace = response.indexOf(QLatin1Char(' ')); const int secondSpace = response.indexOf(QLatin1Char(' '), firstSpace + 1); return response.midRef(firstSpace + 1, secondSpace - firstSpace - 1).toInt(); } void HTTPProtocol::davParsePropstats(const QDomNodeList &propstats, UDSEntry &entry) { QString mimeType; bool foundExecutable = false; bool isDirectory = false; uint lockCount = 0; uint supportedLockCount = 0; qlonglong quotaUsed = -1; qlonglong quotaAvailable = -1; for (int i = 0; i < propstats.count(); i++) { QDomElement propstat = propstats.item(i).toElement(); QDomElement status = propstat.namedItem(QStringLiteral("status")).toElement(); if (status.isNull()) { // error, no status code in this propstat qCDebug(KIO_HTTP) << "Error, no status code in this propstat"; return; } int code = codeFromResponse(status.text()); if (code != 200) { qCDebug(KIO_HTTP) << "Got status code" << code << "(this may mean that some properties are unavailable)"; continue; } QDomElement prop = propstat.namedItem(QStringLiteral("prop")).toElement(); if (prop.isNull()) { qCDebug(KIO_HTTP) << "Error: no prop segment in this propstat."; return; } if (hasMetaData(QStringLiteral("davRequestResponse"))) { QDomDocument doc; doc.appendChild(prop); entry.replace(KIO::UDSEntry::UDS_XML_PROPERTIES, doc.toString()); } for (QDomNode n = prop.firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement property = n.toElement(); if (property.isNull()) { continue; } if (property.namespaceURI() != QLatin1String("DAV:")) { // break out - we're only interested in properties from the DAV namespace continue; } if (property.tagName() == QLatin1String("creationdate")) { // Resource creation date. Should be is ISO 8601 format. entry.replace(KIO::UDSEntry::UDS_CREATION_TIME, parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toSecsSinceEpoch()); } else if (property.tagName() == QLatin1String("getcontentlength")) { // Content length (file size) entry.replace(KIO::UDSEntry::UDS_SIZE, property.text().toULong()); } else if (property.tagName() == QLatin1String("displayname")) { // Name suitable for presentation to the user setMetaData(QStringLiteral("davDisplayName"), property.text()); } else if (property.tagName() == QLatin1String("source")) { // Source template location QDomElement source = property.namedItem(QStringLiteral("link")).toElement() .namedItem(QStringLiteral("dst")).toElement(); if (!source.isNull()) { setMetaData(QStringLiteral("davSource"), source.text()); } } else if (property.tagName() == QLatin1String("getcontentlanguage")) { // equiv. to Content-Language header on a GET setMetaData(QStringLiteral("davContentLanguage"), property.text()); } else if (property.tagName() == QLatin1String("getcontenttype")) { // Content type (mime type) // This may require adjustments for other server-side webdav implementations // (tested with Apache + mod_dav 1.0.3) if (property.text() == QLatin1String("httpd/unix-directory")) { isDirectory = true; } else { mimeType = property.text(); } } else if (property.tagName() == QLatin1String("executable")) { // File executable status if (property.text() == QLatin1Char('T')) { foundExecutable = true; } } else if (property.tagName() == QLatin1String("getlastmodified")) { // Last modification date entry.replace(KIO::UDSEntry::UDS_MODIFICATION_TIME, parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toSecsSinceEpoch()); } else if (property.tagName() == QLatin1String("getetag")) { // Entity tag setMetaData(QStringLiteral("davEntityTag"), property.text()); } else if (property.tagName() == QLatin1String("supportedlock")) { // Supported locking specifications for (QDomNode n2 = property.firstChild(); !n2.isNull(); n2 = n2.nextSibling()) { QDomElement lockEntry = n2.toElement(); if (lockEntry.tagName() == QLatin1String("lockentry")) { QDomElement lockScope = lockEntry.namedItem(QStringLiteral("lockscope")).toElement(); QDomElement lockType = lockEntry.namedItem(QStringLiteral("locktype")).toElement(); if (!lockScope.isNull() && !lockType.isNull()) { // Lock type was properly specified supportedLockCount++; const QString lockCountStr = QString::number(supportedLockCount); const QString scope = lockScope.firstChild().toElement().tagName(); const QString type = lockType.firstChild().toElement().tagName(); setMetaData(QLatin1String("davSupportedLockScope") + lockCountStr, scope); setMetaData(QLatin1String("davSupportedLockType") + lockCountStr, type); } } } } else if (property.tagName() == QLatin1String("lockdiscovery")) { // Lists the available locks davParseActiveLocks(property.elementsByTagName(QStringLiteral("activelock")), lockCount); } else if (property.tagName() == QLatin1String("resourcetype")) { // Resource type. "Specifies the nature of the resource." if (!property.namedItem(QStringLiteral("collection")).toElement().isNull()) { // This is a collection (directory) isDirectory = true; } } else if (property.tagName() == QLatin1String("quota-used-bytes")) { // Quota-used-bytes. "Contains the amount of storage already in use." quotaUsed = property.text().toLongLong(); } else if (property.tagName() == QLatin1String("quota-available-bytes")) { // Quota-available-bytes. "Indicates the maximum amount of additional storage available." quotaAvailable = property.text().toLongLong(); } else { qCDebug(KIO_HTTP) << "Found unknown webdav property:" << property.tagName(); } } } setMetaData(QStringLiteral("davLockCount"), QString::number(lockCount)); setMetaData(QStringLiteral("davSupportedLockCount"), QString::number(supportedLockCount)); entry.replace(KIO::UDSEntry::UDS_FILE_TYPE, isDirectory ? S_IFDIR : S_IFREG); if (foundExecutable || isDirectory) { // File was executable, or is a directory. entry.replace(KIO::UDSEntry::UDS_ACCESS, 0700); } else { entry.replace(KIO::UDSEntry::UDS_ACCESS, 0600); } if (!isDirectory && !mimeType.isEmpty()) { entry.replace(KIO::UDSEntry::UDS_MIME_TYPE, mimeType); } if (quotaUsed >= 0 && quotaAvailable >= 0) { // Only used and available storage properties exist, the total storage size has to be calculated. setMetaData(QStringLiteral("total"), QString::number(quotaUsed + quotaAvailable)); setMetaData(QStringLiteral("available"), QString::number(quotaAvailable)); } } void HTTPProtocol::davParseActiveLocks(const QDomNodeList &activeLocks, uint &lockCount) { for (int i = 0; i < activeLocks.count(); i++) { const QDomElement activeLock = activeLocks.item(i).toElement(); lockCount++; // required const QDomElement lockScope = activeLock.namedItem(QStringLiteral("lockscope")).toElement(); const QDomElement lockType = activeLock.namedItem(QStringLiteral("locktype")).toElement(); const QDomElement lockDepth = activeLock.namedItem(QStringLiteral("depth")).toElement(); // optional const QDomElement lockOwner = activeLock.namedItem(QStringLiteral("owner")).toElement(); const QDomElement lockTimeout = activeLock.namedItem(QStringLiteral("timeout")).toElement(); const QDomElement lockToken = activeLock.namedItem(QStringLiteral("locktoken")).toElement(); if (!lockScope.isNull() && !lockType.isNull() && !lockDepth.isNull()) { // lock was properly specified lockCount++; const QString lockCountStr = QString::number(lockCount); const QString scope = lockScope.firstChild().toElement().tagName(); const QString type = lockType.firstChild().toElement().tagName(); const QString depth = lockDepth.text(); setMetaData(QLatin1String("davLockScope") + lockCountStr, scope); setMetaData(QLatin1String("davLockType") + lockCountStr, type); setMetaData(QLatin1String("davLockDepth") + lockCountStr, depth); if (!lockOwner.isNull()) { setMetaData(QLatin1String("davLockOwner") + lockCountStr, lockOwner.text()); } if (!lockTimeout.isNull()) { setMetaData(QLatin1String("davLockTimeout") + lockCountStr, lockTimeout.text()); } if (!lockToken.isNull()) { QDomElement tokenVal = lockScope.namedItem(QStringLiteral("href")).toElement(); if (!tokenVal.isNull()) { setMetaData(QLatin1String("davLockToken") + lockCountStr, tokenVal.text()); } } } } } QDateTime HTTPProtocol::parseDateTime(const QString &input, const QString &type) { if (type == QLatin1String("dateTime.tz")) { return QDateTime::fromString(input, Qt::ISODate); } else if (type == QLatin1String("dateTime.rfc1123")) { return QDateTime::fromString(input, Qt::RFC2822Date); } // format not advertised... try to parse anyway QDateTime time = QDateTime::fromString(input, Qt::RFC2822Date); if (time.isValid()) { return time; } return QDateTime::fromString(input, Qt::ISODate); } QString HTTPProtocol::davProcessLocks() { if (hasMetaData(QStringLiteral("davLockCount"))) { QString response = QStringLiteral("If:"); int numLocks = metaData(QStringLiteral("davLockCount")).toInt(); bool bracketsOpen = false; for (int i = 0; i < numLocks; i++) { const QString countStr = QString::number(i); if (hasMetaData(QLatin1String("davLockToken") + countStr)) { if (hasMetaData(QLatin1String("davLockURL") + countStr)) { if (bracketsOpen) { response += QLatin1Char(')'); bracketsOpen = false; } response += QLatin1String(" <") + metaData(QLatin1String("davLockURL") + countStr) + QLatin1Char('>'); } if (!bracketsOpen) { response += QLatin1String(" ("); bracketsOpen = true; } else { response += QLatin1Char(' '); } if (hasMetaData(QLatin1String("davLockNot") + countStr)) { response += QLatin1String("Not "); } response += QLatin1Char('<') + metaData(QLatin1String("davLockToken") + countStr) + QLatin1Char('>'); } } if (bracketsOpen) { response += QLatin1Char(')'); } response += QLatin1String("\r\n"); return response; } return QString(); } bool HTTPProtocol::davHostOk() { // FIXME needs to be reworked. Switched off for now. return true; // cached? if (m_davHostOk) { qCDebug(KIO_HTTP) << "true"; return true; } else if (m_davHostUnsupported) { qCDebug(KIO_HTTP) << " false"; davError(-2); return false; } m_request.method = HTTP_OPTIONS; // query the server's capabilities generally, not for a specific URL m_request.url.setPath(QStringLiteral("*")); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; // clear davVersions variable, which holds the response to the DAV: header m_davCapabilities.clear(); proceedUntilResponseHeader(); if (!m_davCapabilities.isEmpty()) { for (int i = 0; i < m_davCapabilities.count(); i++) { bool ok; uint verNo = m_davCapabilities[i].toUInt(&ok); if (ok && verNo > 0 && verNo < 3) { m_davHostOk = true; qCDebug(KIO_HTTP) << "Server supports DAV version" << verNo; } } if (m_davHostOk) { return true; } } m_davHostUnsupported = true; davError(-2); return false; } // This function is for closing proceedUntilResponseHeader(); requests // Required because there may or may not be further info expected void HTTPProtocol::davFinished() { // TODO: Check with the DAV extension developers httpClose(m_request.isKeepAlive); finished(); } void HTTPProtocol::mkdir(const QUrl &url, int) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = DAV_MKCOL; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(true); if (m_request.responseCode == 201) { davFinished(); } else { davError(); } } void HTTPProtocol::get(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_GET; QString tmp(metaData(QStringLiteral("cache"))); if (!tmp.isEmpty()) { m_request.cacheTag.policy = parseCacheControl(tmp); } else { m_request.cacheTag.policy = DEFAULT_CACHE_CONTROL; } proceedUntilResponseContent(); } void HTTPProtocol::put(const QUrl &url, int, KIO::JobFlags flags) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); // Webdav hosts are capable of observing overwrite == false if (m_protocol.startsWith("webdav")) { // krazy:exclude=strings if (!(flags & KIO::Overwrite)) { // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // Checks if the destination exists and return an error if it does. if (!davStatDestination()) { error(ERR_FILE_ALREADY_EXIST, QString()); return; } // force re-authentication... delete m_wwwAuth; m_wwwAuth = nullptr; } } m_request.method = HTTP_PUT; m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(); } void HTTPProtocol::copy(const QUrl &src, const QUrl &dest, int, KIO::JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; const bool isSourceLocal = src.isLocalFile(); const bool isDestinationLocal = dest.isLocalFile(); if (isSourceLocal && !isDestinationLocal) { copyPut(src, dest, flags); } else { if (!maybeSetRequestUrl(dest) || !maybeSetRequestUrl(src)) { return; } resetSessionSettings(); // destination has to be "http(s)://..." QUrl newDest (dest); changeProtocolToHttp(&newDest); m_request.method = DAV_COPY; m_request.davData.desturl = newDest.toString(QUrl::FullyEncoded); m_request.davData.overwrite = (flags & KIO::Overwrite); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseHeader(); // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion if (m_request.responseCode == 201 || m_request.responseCode == 204) { davFinished(); } else { davError(); } } } void HTTPProtocol::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; if (!maybeSetRequestUrl(dest) || !maybeSetRequestUrl(src)) { return; } resetSessionSettings(); // destination has to be "http://..." QUrl newDest(dest); changeProtocolToHttp(&newDest); m_request.method = DAV_MOVE; m_request.davData.desturl = newDest.toString(QUrl::FullyEncoded); m_request.davData.overwrite = (flags & KIO::Overwrite); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseHeader(); // Work around strict Apache-2 WebDAV implementation which refuses to cooperate // with webdav://host/directory, instead requiring webdav://host/directory/ // (strangely enough it accepts Destination: without a trailing slash) // See BR# 209508 and BR#187970 if (m_request.responseCode == 301) { QUrl redir = m_request.redirectUrl; resetSessionSettings(); m_request.url = redir; m_request.method = DAV_MOVE; m_request.davData.desturl = newDest.toString(); m_request.davData.overwrite = (flags & KIO::Overwrite); m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseHeader(); } if (m_request.responseCode == 201) { davFinished(); } else { davError(); } } void HTTPProtocol::del(const QUrl &url, bool) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_DELETE; m_request.cacheTag.policy = CC_Reload; if (m_protocol.startsWith("webdav")) { //krazy:exclude=strings due to QByteArray m_request.url.setQuery(QString()); if (!proceedUntilResponseHeader()) { return; } // The server returns a HTTP/1.1 200 Ok or HTTP/1.1 204 No Content // on successful completion. if (m_request.responseCode == 200 || m_request.responseCode == 204 || m_isRedirection) { davFinished(); } else { davError(); } return; } proceedUntilResponseContent(); } void HTTPProtocol::post(const QUrl &url, qint64 size) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_POST; m_request.cacheTag.policy = CC_Reload; m_iPostDataSize = (size > -1 ? static_cast(size) : NO_SIZE); proceedUntilResponseContent(); } void HTTPProtocol::davLock(const QUrl &url, const QString &scope, const QString &type, const QString &owner) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = DAV_LOCK; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; /* Create appropriate lock XML request. */ QDomDocument lockReq; QDomElement lockInfo = lockReq.createElementNS(QStringLiteral("DAV:"), QStringLiteral("lockinfo")); lockReq.appendChild(lockInfo); QDomElement lockScope = lockReq.createElement(QStringLiteral("lockscope")); lockInfo.appendChild(lockScope); lockScope.appendChild(lockReq.createElement(scope)); QDomElement lockType = lockReq.createElement(QStringLiteral("locktype")); lockInfo.appendChild(lockType); lockType.appendChild(lockReq.createElement(type)); if (!owner.isNull()) { QDomElement ownerElement = lockReq.createElement(QStringLiteral("owner")); lockReq.appendChild(ownerElement); QDomElement ownerHref = lockReq.createElement(QStringLiteral("href")); ownerElement.appendChild(ownerHref); ownerHref.appendChild(lockReq.createTextNode(owner)); } // insert the document into the POST buffer cachePostData(lockReq.toByteArray()); proceedUntilResponseContent(true); if (m_request.responseCode == 200) { // success QDomDocument multiResponse; multiResponse.setContent(m_webDavDataBuf, true); QDomElement prop = multiResponse.documentElement().namedItem(QStringLiteral("prop")).toElement(); QDomElement lockdiscovery = prop.namedItem(QStringLiteral("lockdiscovery")).toElement(); uint lockCount = 0; davParseActiveLocks(lockdiscovery.elementsByTagName(QStringLiteral("activelock")), lockCount); setMetaData(QStringLiteral("davLockCount"), QString::number(lockCount)); finished(); } else { davError(); } } void HTTPProtocol::davUnlock(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = DAV_UNLOCK; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(true); if (m_request.responseCode == 200) { finished(); } else { davError(); } } QString HTTPProtocol::davError(int code /* = -1 */, const QString &_url) { bool callError = false; if (code == -1) { code = m_request.responseCode; callError = true; } if (code == -2) { callError = true; } QString url = _url; if (!url.isNull()) { url = m_request.url.toDisplayString(); } QString action, errorString; int errorCode = ERR_SLAVE_DEFINED; // for 412 Precondition Failed QString ow = i18n("Otherwise, the request would have succeeded."); switch (m_request.method) { case DAV_PROPFIND: action = i18nc("request type", "retrieve property values"); break; case DAV_PROPPATCH: action = i18nc("request type", "set property values"); break; case DAV_MKCOL: action = i18nc("request type", "create the requested folder"); break; case DAV_COPY: action = i18nc("request type", "copy the specified file or folder"); break; case DAV_MOVE: action = i18nc("request type", "move the specified file or folder"); break; case DAV_SEARCH: action = i18nc("request type", "search in the specified folder"); break; case DAV_LOCK: action = i18nc("request type", "lock the specified file or folder"); break; case DAV_UNLOCK: action = i18nc("request type", "unlock the specified file or folder"); break; case HTTP_DELETE: action = i18nc("request type", "delete the specified file or folder"); break; case HTTP_OPTIONS: action = i18nc("request type", "query the server's capabilities"); break; case HTTP_GET: action = i18nc("request type", "retrieve the contents of the specified file or folder"); break; case DAV_REPORT: action = i18nc("request type", "run a report in the specified folder"); break; case HTTP_PUT: case HTTP_POST: case HTTP_HEAD: default: // this should not happen, this function is for webdav errors only Q_ASSERT(0); } // default error message if the following code fails errorString = i18nc("%1: code, %2: request type", "An unexpected error (%1) occurred " "while attempting to %2.", code, action); switch (code) { case -2: // internal error: OPTIONS request did not specify DAV compliance // ERR_UNSUPPORTED_PROTOCOL errorString = i18n("The server does not support the WebDAV protocol."); break; case 207: // 207 Multi-status { // our error info is in the returned XML document. // retrieve the XML document // there was an error retrieving the XML document. if (!readBody(true) && m_kioError) { return QString(); } QStringList errors; QDomDocument multiResponse; multiResponse.setContent(m_webDavDataBuf, true); QDomElement multistatus = multiResponse.documentElement().namedItem(QStringLiteral("multistatus")).toElement(); QDomNodeList responses = multistatus.elementsByTagName(QStringLiteral("response")); for (int i = 0; i < responses.count(); i++) { int errCode; QString errUrl; QDomElement response = responses.item(i).toElement(); QDomElement code = response.namedItem(QStringLiteral("status")).toElement(); if (!code.isNull()) { errCode = codeFromResponse(code.text()); QDomElement href = response.namedItem(QStringLiteral("href")).toElement(); if (!href.isNull()) { errUrl = href.text(); } errors << davError(errCode, errUrl); } } //kError = ERR_SLAVE_DEFINED; errorString = i18nc("%1: request type, %2: url", "An error occurred while attempting to %1, %2. A " "summary of the reasons is below.", action, url); errorString += QLatin1String("
    "); Q_FOREACH (const QString &error, errors) { errorString += QLatin1String("
  • ") + error + QLatin1String("
  • "); } errorString += QLatin1String("
"); } break; case 403: case 500: // hack: Apache mod_dav returns this instead of 403 (!) // 403 Forbidden // ERR_ACCESS_DENIED errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action); break; case 405: // 405 Method Not Allowed if (m_request.method == DAV_MKCOL) { // ERR_DIR_ALREADY_EXIST errorString = url; errorCode = ERR_DIR_ALREADY_EXIST; } break; case 409: // 409 Conflict // ERR_ACCESS_DENIED errorString = i18n("A resource cannot be created at the destination " "until one or more intermediate collections (folders) " "have been created."); break; case 412: // 412 Precondition failed if (m_request.method == DAV_COPY || m_request.method == DAV_MOVE) { // ERR_ACCESS_DENIED errorString = i18n("The server was unable to maintain the liveness of " "the properties listed in the propertybehavior XML " "element\n or you attempted to overwrite a file while " "requesting that files are not overwritten.\n %1", ow); } else if (m_request.method == DAV_LOCK) { // ERR_ACCESS_DENIED errorString = i18n("The requested lock could not be granted. %1", ow); } break; case 415: // 415 Unsupported Media Type // ERR_ACCESS_DENIED errorString = i18n("The server does not support the request type of the body."); break; case 423: // 423 Locked // ERR_ACCESS_DENIED errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action); break; case 425: // 424 Failed Dependency errorString = i18n("This action was prevented by another error."); break; case 502: // 502 Bad Gateway if (m_request.method == DAV_COPY || m_request.method == DAV_MOVE) { // ERR_WRITE_ACCESS_DENIED errorString = i18nc("%1: request type", "Unable to %1 because the destination server refuses " "to accept the file or folder.", action); } break; case 507: // 507 Insufficient Storage // ERR_DISK_FULL errorString = i18n("The destination resource does not have sufficient space " "to record the state of the resource after the execution " "of this method."); break; default: break; } // if ( kError != ERR_SLAVE_DEFINED ) //errorString += " (" + url + ')'; if (callError) { error(errorCode, errorString); } return errorString; } // HTTP generic error static int httpGenericError(const HTTPProtocol::HTTPRequest &request, QString *errorString) { Q_ASSERT(errorString); int errorCode = 0; errorString->clear(); if (request.responseCode == 204) { errorCode = ERR_NO_CONTENT; } return errorCode; } // HTTP DELETE specific errors static int httpDelError(const HTTPProtocol::HTTPRequest &request, QString *errorString) { Q_ASSERT(errorString); int errorCode = 0; const int responseCode = request.responseCode; errorString->clear(); switch (responseCode) { case 204: errorCode = ERR_NO_CONTENT; break; default: break; } if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) { errorCode = ERR_SLAVE_DEFINED; *errorString = i18n("The resource cannot be deleted."); } return errorCode; } // HTTP PUT specific errors static int httpPutError(const HTTPProtocol::HTTPRequest &request, QString *errorString) { Q_ASSERT(errorString); int errorCode = 0; const int responseCode = request.responseCode; const QString action(i18nc("request type", "upload %1", request.url.toDisplayString())); switch (responseCode) { case 403: case 405: case 500: // hack: Apache mod_dav returns this instead of 403 (!) // 403 Forbidden // 405 Method Not Allowed // ERR_ACCESS_DENIED *errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action); errorCode = ERR_SLAVE_DEFINED; break; case 409: // 409 Conflict // ERR_ACCESS_DENIED *errorString = i18n("A resource cannot be created at the destination " "until one or more intermediate collections (folders) " "have been created."); errorCode = ERR_SLAVE_DEFINED; break; case 423: // 423 Locked // ERR_ACCESS_DENIED *errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action); errorCode = ERR_SLAVE_DEFINED; break; case 502: // 502 Bad Gateway // ERR_WRITE_ACCESS_DENIED; *errorString = i18nc("%1: request type", "Unable to %1 because the destination server refuses " "to accept the file or folder.", action); errorCode = ERR_SLAVE_DEFINED; break; case 507: // 507 Insufficient Storage // ERR_DISK_FULL *errorString = i18n("The destination resource does not have sufficient space " "to record the state of the resource after the execution " "of this method."); errorCode = ERR_SLAVE_DEFINED; break; default: break; } if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) { errorCode = ERR_SLAVE_DEFINED; *errorString = i18nc("%1: response code, %2: request type", "An unexpected error (%1) occurred while attempting to %2.", responseCode, action); } return errorCode; } bool HTTPProtocol::sendHttpError() { QString errorString; int errorCode = 0; switch (m_request.method) { case HTTP_GET: case HTTP_POST: errorCode = httpGenericError(m_request, &errorString); break; case HTTP_PUT: errorCode = httpPutError(m_request, &errorString); break; case HTTP_DELETE: errorCode = httpDelError(m_request, &errorString); break; default: break; } // Force any message previously shown by the client to be cleared. infoMessage(QLatin1String("")); if (errorCode) { error(errorCode, errorString); return true; } return false; } bool HTTPProtocol::sendErrorPageNotification() { if (!m_request.preferErrorPage) { return false; } if (m_isLoadingErrorPage) { qCWarning(KIO_HTTP) << "called twice during one request, something is probably wrong."; } m_isLoadingErrorPage = true; SlaveBase::errorPage(); return true; } bool HTTPProtocol::isOffline() { if (!m_networkConfig) { m_networkConfig = new QNetworkConfigurationManager(this); } return !m_networkConfig->isOnline(); } void HTTPProtocol::multiGet(const QByteArray &data) { QDataStream stream(data); quint32 n; stream >> n; qCDebug(KIO_HTTP) << n; HTTPRequest saveRequest; if (m_isBusy) { saveRequest = m_request; } resetSessionSettings(); for (unsigned i = 0; i < n; ++i) { QUrl url; stream >> url >> mIncomingMetaData; if (!maybeSetRequestUrl(url)) { continue; } //### should maybe call resetSessionSettings() if the server/domain is // different from the last request! qCDebug(KIO_HTTP) << url; m_request.method = HTTP_GET; m_request.isKeepAlive = true; //readResponseHeader clears it if necessary QString tmp = metaData(QStringLiteral("cache")); if (!tmp.isEmpty()) { m_request.cacheTag.policy = parseCacheControl(tmp); } else { m_request.cacheTag.policy = DEFAULT_CACHE_CONTROL; } m_requestQueue.append(m_request); } if (m_isBusy) { m_request = saveRequest; } #if 0 if (!m_isBusy) { m_isBusy = true; QMutableListIterator it(m_requestQueue); while (it.hasNext()) { m_request = it.next(); it.remove(); proceedUntilResponseContent(); } m_isBusy = false; } #endif if (!m_isBusy) { m_isBusy = true; QMutableListIterator it(m_requestQueue); // send the requests while (it.hasNext()) { m_request = it.next(); sendQuery(); // save the request state so we can pick it up again in the collection phase it.setValue(m_request); qCDebug(KIO_HTTP) << "check one: isKeepAlive =" << m_request.isKeepAlive; if (m_request.cacheTag.ioMode != ReadFromCache) { m_server.initFrom(m_request); } } // collect the responses //### for the moment we use a hack: instead of saving and restoring request-id // we just count up like ParallelGetJobs does. int requestId = 0; Q_FOREACH (const HTTPRequest &r, m_requestQueue) { m_request = r; qCDebug(KIO_HTTP) << "check two: isKeepAlive =" << m_request.isKeepAlive; setMetaData(QStringLiteral("request-id"), QString::number(requestId++)); sendAndKeepMetaData(); if (!(readResponseHeader() && readBody())) { return; } // the "next job" signal for ParallelGetJob is data of size zero which // readBody() sends without our intervention. qCDebug(KIO_HTTP) << "check three: isKeepAlive =" << m_request.isKeepAlive; httpClose(m_request.isKeepAlive); //actually keep-alive is mandatory for pipelining } finished(); m_requestQueue.clear(); m_isBusy = false; } } ssize_t HTTPProtocol::write(const void *_buf, size_t nbytes) { size_t sent = 0; const char *buf = static_cast(_buf); while (sent < nbytes) { int n = TCPSlaveBase::write(buf + sent, nbytes - sent); if (n < 0) { // some error occurred return -1; } sent += n; } return sent; } void HTTPProtocol::clearUnreadBuffer() { m_unreadBuf.clear(); } // Note: the implementation of unread/readBuffered assumes that unread will only // be used when there is extra data we don't want to handle, and not to wait for more data. void HTTPProtocol::unread(char *buf, size_t size) { // implement LIFO (stack) semantics const int newSize = m_unreadBuf.size() + size; m_unreadBuf.resize(newSize); for (size_t i = 0; i < size; i++) { m_unreadBuf.data()[newSize - i - 1] = buf[i]; } if (size) { //hey, we still have data, closed connection or not! m_isEOF = false; } } size_t HTTPProtocol::readBuffered(char *buf, size_t size, bool unlimited) { size_t bytesRead = 0; if (!m_unreadBuf.isEmpty()) { const int bufSize = m_unreadBuf.size(); bytesRead = qMin((int)size, bufSize); for (size_t i = 0; i < bytesRead; i++) { buf[i] = m_unreadBuf.constData()[bufSize - i - 1]; } m_unreadBuf.chop(bytesRead); // If we have an unread buffer and the size of the content returned by the // server is unknown, e.g. chuncked transfer, return the bytes read here since // we may already have enough data to complete the response and don't want to // wait for more. See BR# 180631. if (unlimited) { return bytesRead; } } if (bytesRead < size) { int rawRead = TCPSlaveBase::read(buf + bytesRead, size - bytesRead); if (rawRead < 1) { m_isEOF = true; return bytesRead; } bytesRead += rawRead; } return bytesRead; } //### this method will detect an n*(\r\n) sequence if it crosses invocations. // it will look (n*2 - 1) bytes before start at most and never before buf, naturally. // supported number of newlines are one and two, in line with HTTP syntax. // return true if numNewlines newlines were found. bool HTTPProtocol::readDelimitedText(char *buf, int *idx, int end, int numNewlines) { Q_ASSERT(numNewlines >= 1 && numNewlines <= 2); char mybuf[64]; //somewhere close to the usual line length to avoid unread()ing too much int pos = *idx; while (pos < end && !m_isEOF) { int step = qMin((int)sizeof(mybuf), end - pos); if (m_isChunked) { //we might be reading the end of the very last chunk after which there is no data. //don't try to read any more bytes than there are because it causes stalls //(yes, it shouldn't stall but it does) step = 1; } size_t bufferFill = readBuffered(mybuf, step); for (size_t i = 0; i < bufferFill; ++i, ++pos) { // we copy the data from mybuf to buf immediately and look for the newlines in buf. // that way we don't miss newlines split over several invocations of this method. buf[pos] = mybuf[i]; // did we just copy one or two times the (usually) \r\n delimiter? // until we find even more broken webservers in the wild let's assume that they either // send \r\n (RFC compliant) or \n (broken) as delimiter... if (buf[pos] == '\n') { bool found = numNewlines == 1; if (!found) { // looking for two newlines // Detect \n\n and \n\r\n. The other cases (\r\n\n, \r\n\r\n) are covered by the first two. found = ((pos >= 1 && buf[pos - 1] == '\n') || (pos >= 2 && buf[pos - 2] == '\n' && buf[pos - 1] == '\r')); } if (found) { i++; // unread bytes *after* CRLF unread(&mybuf[i], bufferFill - i); *idx = pos + 1; return true; } } } } *idx = pos; return false; } static bool isCompatibleNextUrl(const QUrl &previous, const QUrl &now) { if (previous.host() != now.host() || previous.port() != now.port()) { return false; } if (previous.userName().isEmpty() && previous.password().isEmpty()) { return true; } return previous.userName() == now.userName() && previous.password() == now.password(); } bool HTTPProtocol::httpShouldCloseConnection() { qCDebug(KIO_HTTP); if (!isConnected()) { return false; } if (!m_request.proxyUrls.isEmpty() && !isAutoSsl()) { Q_FOREACH (const QString &url, m_request.proxyUrls) { if (url != QLatin1String("DIRECT")) { if (isCompatibleNextUrl(m_server.proxyUrl, QUrl(url))) { return false; } } } return true; } return !isCompatibleNextUrl(m_server.url, m_request.url); } bool HTTPProtocol::httpOpenConnection() { qCDebug(KIO_HTTP); m_server.clear(); // Only save proxy auth information after proxy authentication has // actually taken place, which will set up exactly this connection. disconnect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); clearUnreadBuffer(); int connectError = 0; QString errorString; // Get proxy information... if (m_request.proxyUrls.isEmpty()) { m_request.proxyUrls = config()->readEntry("ProxyUrls", QStringList()); qCDebug(KIO_HTTP) << "Proxy URLs:" << m_request.proxyUrls; } if (m_request.proxyUrls.isEmpty()) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); } else { QList badProxyUrls; Q_FOREACH (const QString &proxyUrl, m_request.proxyUrls) { if (proxyUrl == QLatin1String("DIRECT")) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); if (connectError == 0) { //qDebug() << "Connected DIRECT: host=" << m_request.url.host() << "port=" << m_request.url.port(defaultPort()); break; } else { continue; } } const QUrl url(proxyUrl); const QString proxyScheme(url.scheme()); if (!supportedProxyScheme(proxyScheme)) { connectError = ERR_CANNOT_CONNECT; errorString = url.toDisplayString(); badProxyUrls << url; continue; } QNetworkProxy::ProxyType proxyType = QNetworkProxy::NoProxy; if (proxyScheme == QLatin1String("socks")) { proxyType = QNetworkProxy::Socks5Proxy; } else if (isAutoSsl()) { proxyType = QNetworkProxy::HttpProxy; } qCDebug(KIO_HTTP) << "Connecting to proxy: address=" << proxyUrl << "type=" << proxyType; if (proxyType == QNetworkProxy::NoProxy) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); connectError = connectToHost(url.host(), url.port(), &errorString); if (connectError == 0) { m_request.proxyUrl = url; //qDebug() << "Connected to proxy: host=" << url.host() << "port=" << url.port(); break; } else { if (connectError == ERR_UNKNOWN_HOST) { connectError = ERR_UNKNOWN_PROXY_HOST; } //qDebug() << "Failed to connect to proxy:" << proxyUrl; badProxyUrls << url; } } else { QNetworkProxy proxy(proxyType, url.host(), url.port(), url.userName(), url.password()); QNetworkProxy::setApplicationProxy(proxy); connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); if (connectError == 0) { qCDebug(KIO_HTTP) << "Tunneling thru proxy: host=" << url.host() << "port=" << url.port(); break; } else { if (connectError == ERR_UNKNOWN_HOST) { connectError = ERR_UNKNOWN_PROXY_HOST; } qCDebug(KIO_HTTP) << "Failed to connect to proxy:" << proxyUrl; badProxyUrls << url; QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); } } } if (!badProxyUrls.isEmpty()) { //TODO: Notify the client of BAD proxy addresses (needed for PAC setups). } } if (connectError != 0) { error(connectError, errorString); return false; } // Disable Nagle's algorithm, i.e turn on TCP_NODELAY. KTcpSocket *sock = qobject_cast(socket()); if (sock) { qCDebug(KIO_HTTP) << "TCP_NODELAY:" << sock->socketOption(QAbstractSocket::LowDelayOption); sock->setSocketOption(QAbstractSocket::LowDelayOption, 1); } m_server.initFrom(m_request); connected(); return true; } bool HTTPProtocol::satisfyRequestFromCache(bool *cacheHasPage) { qCDebug(KIO_HTTP); if (m_request.cacheTag.useCache) { const bool offline = isOffline(); if (offline && m_request.cacheTag.policy != KIO::CC_Reload) { m_request.cacheTag.policy = KIO::CC_CacheOnly; } const bool isCacheOnly = m_request.cacheTag.policy == KIO::CC_CacheOnly; const CacheTag::CachePlan plan = m_request.cacheTag.plan(m_maxCacheAge); bool openForReading = false; if (plan == CacheTag::UseCached || plan == CacheTag::ValidateCached) { openForReading = cacheFileOpenRead(); if (!openForReading && (isCacheOnly || offline)) { // cache-only or offline -> we give a definite answer and it is "no" *cacheHasPage = false; if (isCacheOnly) { error(ERR_DOES_NOT_EXIST, m_request.url.toDisplayString()); } else if (offline) { error(ERR_CANNOT_CONNECT, m_request.url.toDisplayString()); } return true; } } if (openForReading) { m_request.cacheTag.ioMode = ReadFromCache; *cacheHasPage = true; // return false if validation is required, so a network request will be sent return m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached; } } *cacheHasPage = false; return false; } QString HTTPProtocol::formatRequestUri() const { // Only specify protocol, host and port when they are not already clear, i.e. when // we handle HTTP proxying ourself and the proxy server needs to know them. // Sending protocol/host/port in other cases confuses some servers, and it's not their fault. if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { QUrl u; QString protocol = m_request.url.scheme(); if (protocol.startsWith(QLatin1String("webdav"))) { protocol.replace(0, qstrlen("webdav"), QStringLiteral("http")); } u.setScheme(protocol); u.setHost(m_request.url.host()); // if the URL contained the default port it should have been stripped earlier Q_ASSERT(m_request.url.port() != defaultPort()); u.setPort(m_request.url.port()); u.setPath(m_request.url.path(QUrl::FullyEncoded)); u.setQuery(m_request.url.query(QUrl::FullyEncoded)); return u.toString(QUrl::FullyEncoded); } else { QString result = m_request.url.path(QUrl::FullyEncoded); if (m_request.url.hasQuery()) { result += QLatin1Char('?') + m_request.url.query(QUrl::FullyEncoded); } return result; } } /** * This function is responsible for opening up the connection to the remote * HTTP server and sending the header. If this requires special * authentication or other such fun stuff, then it will handle it. This * function will NOT receive anything from the server, however. This is in * contrast to previous incarnations of 'httpOpen' as this method used to be * called. * * The basic process now is this: * * 1) Open up the socket and port * 2) Format our request/header * 3) Send the header to the remote server * 4) Call sendBody() if the HTTP method requires sending body data */ bool HTTPProtocol::sendQuery() { qCDebug(KIO_HTTP); // Cannot have an https request without autoSsl! This can // only happen if the current installation does not support SSL... if (isEncryptedHttpVariety(m_protocol) && !isAutoSsl()) { error(ERR_UNSUPPORTED_PROTOCOL, toQString(m_protocol)); return false; } // Check the reusability of the current connection. if (httpShouldCloseConnection()) { httpCloseConnection(); } // Create a new connection to the remote machine if we do // not already have one... // NB: the !m_socketProxyAuth condition is a workaround for a proxied Qt socket sometimes // looking disconnected after receiving the initial 407 response. // I guess the Qt socket fails to hide the effect of proxy-connection: close after receiving // the 407 header. if ((!isConnected() && !m_socketProxyAuth)) { if (!httpOpenConnection()) { qCDebug(KIO_HTTP) << "Couldn't connect, oopsie!"; return false; } } m_request.cacheTag.ioMode = NoCache; m_request.cacheTag.servedDate = QDateTime(); m_request.cacheTag.lastModifiedDate = QDateTime(); m_request.cacheTag.expireDate = QDateTime(); QString header; bool hasBodyData = false; bool hasDavData = false; { m_request.sentMethodString = m_request.methodString(); header = toQString(m_request.sentMethodString) + QLatin1Char(' '); QString davHeader; // Fill in some values depending on the HTTP method to guide further processing switch (m_request.method) { case HTTP_GET: { bool cacheHasPage = false; if (satisfyRequestFromCache(&cacheHasPage)) { qCDebug(KIO_HTTP) << "cacheHasPage =" << cacheHasPage; return cacheHasPage; } if (!cacheHasPage) { // start a new cache file later if appropriate m_request.cacheTag.ioMode = WriteToCache; } break; } case HTTP_HEAD: break; case HTTP_PUT: case HTTP_POST: hasBodyData = true; break; case HTTP_DELETE: case HTTP_OPTIONS: break; case DAV_PROPFIND: hasDavData = true; davHeader = QStringLiteral("Depth: "); if (hasMetaData(QStringLiteral("davDepth"))) { qCDebug(KIO_HTTP) << "Reading DAV depth from metadata:" << metaData( QStringLiteral("davDepth") ); davHeader += metaData(QStringLiteral("davDepth")); } else { if (m_request.davData.depth == 2) { davHeader += QLatin1String("infinity"); } else { davHeader += QString::number(m_request.davData.depth); } } davHeader += QLatin1String("\r\n"); break; case DAV_PROPPATCH: hasDavData = true; break; case DAV_MKCOL: break; case DAV_COPY: case DAV_MOVE: davHeader = QLatin1String("Destination: ") + m_request.davData.desturl + // infinity depth means copy recursively // (optional for copy -> but is the desired action) QLatin1String("\r\nDepth: infinity\r\nOverwrite: ") + QLatin1Char(m_request.davData.overwrite ? 'T' : 'F') + QLatin1String("\r\n"); break; case DAV_LOCK: davHeader = QStringLiteral("Timeout: "); { uint timeout = 0; if (hasMetaData(QStringLiteral("davTimeout"))) { timeout = metaData(QStringLiteral("davTimeout")).toUInt(); } if (timeout == 0) { davHeader += QLatin1String("Infinite"); } else { davHeader += QLatin1String("Seconds-") + QString::number(timeout); } } davHeader += QLatin1String("\r\n"); hasDavData = true; break; case DAV_UNLOCK: davHeader = QLatin1String("Lock-token: ") + metaData(QStringLiteral("davLockToken")) + QLatin1String("\r\n"); break; case DAV_SEARCH: case DAV_REPORT: hasDavData = true; /* fall through */ case DAV_SUBSCRIBE: case DAV_UNSUBSCRIBE: case DAV_POLL: break; default: error(ERR_UNSUPPORTED_ACTION, QString()); return false; } // DAV_POLL; DAV_NOTIFY header += formatRequestUri() + QLatin1String(" HTTP/1.1\r\n"); /* start header */ /* support for virtual hosts and required by HTTP 1.1 */ header += QLatin1String("Host: ") + m_request.encoded_hostname; if (m_request.url.port(defaultPort()) != defaultPort()) { header += QLatin1Char(':') + QString::number(m_request.url.port()); } header += QLatin1String("\r\n"); // Support old HTTP/1.0 style keep-alive header for compatibility // purposes as well as performance improvements while giving end // users the ability to disable this feature for proxy servers that // don't support it, e.g. junkbuster proxy server. if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { header += QLatin1String("Proxy-Connection: "); } else { header += QLatin1String("Connection: "); } if (m_request.isKeepAlive) { header += QLatin1String("keep-alive\r\n"); } else { header += QLatin1String("close\r\n"); } if (!m_request.userAgent.isEmpty()) { header += QLatin1String("User-Agent: ") + m_request.userAgent + QLatin1String("\r\n"); } if (!m_request.referrer.isEmpty()) { // Don't try to correct spelling! header += QLatin1String("Referer: ") + m_request.referrer + QLatin1String("\r\n"); } if (m_request.endoffset > m_request.offset) { header += QLatin1String("Range: bytes=") + KIO::number(m_request.offset) + QLatin1Char('-') + KIO::number(m_request.endoffset) + QLatin1String("\r\n"); qCDebug(KIO_HTTP) << "kio_http : Range =" << KIO::number(m_request.offset) << "-" << KIO::number(m_request.endoffset); } else if (m_request.offset > 0 && m_request.endoffset == 0) { header += QLatin1String("Range: bytes=") + KIO::number(m_request.offset) + QLatin1String("-\r\n"); qCDebug(KIO_HTTP) << "kio_http: Range =" << KIO::number(m_request.offset); } if (!m_request.cacheTag.useCache || m_request.cacheTag.policy == CC_Reload) { /* No caching for reload */ header += QLatin1String("Pragma: no-cache\r\n"); /* for HTTP/1.0 caches */ header += QLatin1String("Cache-control: no-cache\r\n"); /* for HTTP >=1.1 caches */ } else if (m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::ValidateCached) { qCDebug(KIO_HTTP) << "needs validation, performing conditional get."; /* conditional get */ if (!m_request.cacheTag.etag.isEmpty()) { header += QLatin1String("If-None-Match: ") + m_request.cacheTag.etag + QLatin1String("\r\n"); } if (m_request.cacheTag.lastModifiedDate.isValid()) { const QString httpDate = formatHttpDate(m_request.cacheTag.lastModifiedDate); header += QLatin1String("If-Modified-Since: ") + httpDate + QLatin1String("\r\n"); setMetaData(QStringLiteral("modified"), httpDate); } } header += QLatin1String("Accept: "); const QString acceptHeader = metaData(QStringLiteral("accept")); if (!acceptHeader.isEmpty()) { header += acceptHeader; } else { header += QLatin1String(DEFAULT_ACCEPT_HEADER); } header += QLatin1String("\r\n"); if (m_request.allowTransferCompression) { header += QLatin1String("Accept-Encoding: gzip, deflate, x-gzip, x-deflate\r\n"); } if (!m_request.charsets.isEmpty()) { header += QLatin1String("Accept-Charset: ") + m_request.charsets + QLatin1String("\r\n"); } if (!m_request.languages.isEmpty()) { header += QLatin1String("Accept-Language: ") + m_request.languages + QLatin1String("\r\n"); } QString cookieStr; const QString cookieMode = metaData(QStringLiteral("cookies")).toLower(); if (cookieMode == QLatin1String("none")) { m_request.cookieMode = HTTPRequest::CookiesNone; } else if (cookieMode == QLatin1String("manual")) { m_request.cookieMode = HTTPRequest::CookiesManual; cookieStr = metaData(QStringLiteral("setcookies")); } else { m_request.cookieMode = HTTPRequest::CookiesAuto; if (m_request.useCookieJar) { cookieStr = findCookies(m_request.url.toString()); } } if (!cookieStr.isEmpty()) { header += cookieStr + QLatin1String("\r\n"); } const QString customHeader = metaData(QStringLiteral("customHTTPHeader")); if (!customHeader.isEmpty()) { header += sanitizeCustomHTTPHeader(customHeader) + QLatin1String("\r\n"); } const QString contentType = metaData(QStringLiteral("content-type")); if (!contentType.isEmpty()) { if (!contentType.startsWith(QLatin1String("content-type"), Qt::CaseInsensitive)) { header += QLatin1String("Content-Type: "); } header += contentType + QLatin1String("\r\n"); } // DoNotTrack feature... if (config()->readEntry("DoNotTrack", false)) { header += QLatin1String("DNT: 1\r\n"); } // Remember that at least one failed (with 401 or 407) request/response // roundtrip is necessary for the server to tell us that it requires // authentication. However, we proactively add authentication headers if when // we have cached credentials to avoid the extra roundtrip where possible. header += authenticationHeader(); if (m_protocol == "webdav" || m_protocol == "webdavs") { header += davProcessLocks(); // add extra webdav headers, if supplied davHeader += metaData(QStringLiteral("davHeader")); // Set content type of webdav data if (hasDavData) { davHeader += QStringLiteral("Content-Type: text/xml; charset=utf-8\r\n"); } // add extra header elements for WebDAV header += davHeader; } } qCDebug(KIO_HTTP) << "============ Sending Header:"; Q_FOREACH (const QString &s, header.split(QStringLiteral("\r\n"), QString::SkipEmptyParts)) { qCDebug(KIO_HTTP) << s; } // End the header iff there is no payload data. If we do have payload data // sendBody() will add another field to the header, Content-Length. if (!hasBodyData && !hasDavData) { header += QStringLiteral("\r\n"); } // Now that we have our formatted header, let's send it! // Clear out per-connection settings... resetConnectionSettings(); // Send the data to the remote machine... const QByteArray headerBytes = header.toLatin1(); ssize_t written = write(headerBytes.constData(), headerBytes.length()); bool sendOk = (written == (ssize_t) headerBytes.length()); if (!sendOk) { qCDebug(KIO_HTTP) << "Connection broken! (" << m_request.url.host() << ")" << " -- intended to write" << headerBytes.length() << "bytes but wrote" << (int)written << "."; // The server might have closed the connection due to a timeout, or maybe // some transport problem arose while the connection was idle. if (m_request.isKeepAlive) { httpCloseConnection(); return true; // Try again } qCDebug(KIO_HTTP) << "sendOk == false. Connection broken !" << " -- intended to write" << headerBytes.length() << "bytes but wrote" << (int)written << "."; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } else { qCDebug(KIO_HTTP) << "sent it!"; } bool res = true; if (hasBodyData || hasDavData) { res = sendBody(); } infoMessage(i18n("%1 contacted. Waiting for reply...", m_request.url.host())); return res; } void HTTPProtocol::forwardHttpResponseHeader(bool forwardImmediately) { // Send the response header if it was requested... if (!config()->readEntry("PropagateHttpHeader", false)) { return; } setMetaData(QStringLiteral("HTTP-Headers"), m_responseHeaders.join(QLatin1Char('\n'))); if (forwardImmediately) { sendMetaData(); } } bool HTTPProtocol::parseHeaderFromCache() { qCDebug(KIO_HTTP); if (!cacheFileReadTextHeader2()) { return false; } Q_FOREACH (const QString &str, m_responseHeaders) { const QString header = str.trimmed(); if (header.startsWith(QLatin1String("content-type:"), Qt::CaseInsensitive)) { int pos = header.indexOf(QLatin1String("charset="), Qt::CaseInsensitive); if (pos != -1) { const QString charset = header.mid(pos + 8).toLower(); m_request.cacheTag.charset = charset; setMetaData(QStringLiteral("charset"), charset); } } else if (header.startsWith(QLatin1String("content-language:"), Qt::CaseInsensitive)) { const QString language = header.mid(17).trimmed().toLower(); setMetaData(QStringLiteral("content-language"), language); } else if (header.startsWith(QLatin1String("content-disposition:"), Qt::CaseInsensitive)) { parseContentDisposition(header.mid(20).toLower()); } } if (m_request.cacheTag.lastModifiedDate.isValid()) { setMetaData(QStringLiteral("modified"), formatHttpDate(m_request.cacheTag.lastModifiedDate)); } // this header comes from the cache, so the response must have been cacheable :) setCacheabilityMetadata(true); qCDebug(KIO_HTTP) << "Emitting mimeType" << m_mimeType; forwardHttpResponseHeader(false); mimeType(m_mimeType); // IMPORTANT: Do not remove the call below or the http response headers will // not be available to the application if this slave is put on hold. forwardHttpResponseHeader(); return true; } void HTTPProtocol::fixupResponseMimetype() { if (m_mimeType.isEmpty()) { return; } qCDebug(KIO_HTTP) << "before fixup" << m_mimeType; // Convert some common mimetypes to standard mimetypes if (m_mimeType == QLatin1String("application/x-targz")) { m_mimeType = QStringLiteral("application/x-compressed-tar"); } else if (m_mimeType == QLatin1String("image/x-png")) { m_mimeType = QStringLiteral("image/png"); } else if (m_mimeType == QLatin1String("audio/x-mp3") || m_mimeType == QLatin1String("audio/x-mpeg") || m_mimeType == QLatin1String("audio/mp3")) { m_mimeType = QStringLiteral("audio/mpeg"); } else if (m_mimeType == QLatin1String("audio/microsoft-wave")) { m_mimeType = QStringLiteral("audio/x-wav"); } else if (m_mimeType == QLatin1String("image/x-ms-bmp")) { m_mimeType = QStringLiteral("image/bmp"); } // Crypto ones.... else if (m_mimeType == QLatin1String("application/pkix-cert") || m_mimeType == QLatin1String("application/binary-certificate")) { m_mimeType = QStringLiteral("application/x-x509-ca-cert"); } // Prefer application/x-compressed-tar or x-gzpostscript over application/x-gzip. else if (m_mimeType == QLatin1String("application/x-gzip")) { if ((m_request.url.path().endsWith(QLatin1String(".tar.gz"))) || (m_request.url.path().endsWith(QLatin1String(".tar")))) { m_mimeType = QStringLiteral("application/x-compressed-tar"); } if ((m_request.url.path().endsWith(QLatin1String(".ps.gz")))) { m_mimeType = QStringLiteral("application/x-gzpostscript"); } } // Prefer application/x-xz-compressed-tar over application/x-xz for LMZA compressed // tar files. Arch Linux AUR servers notoriously send the wrong mimetype for this. else if (m_mimeType == QLatin1String("application/x-xz")) { if (m_request.url.path().endsWith(QLatin1String(".tar.xz")) || m_request.url.path().endsWith(QLatin1String(".txz"))) { m_mimeType = QStringLiteral("application/x-xz-compressed-tar"); } } // Some webservers say "text/plain" when they mean "application/x-bzip" else if ((m_mimeType == QLatin1String("text/plain")) || (m_mimeType == QLatin1String("application/octet-stream"))) { const QString ext = QFileInfo(m_request.url.path()).suffix().toUpper(); if (ext == QLatin1String("BZ2")) { m_mimeType = QStringLiteral("application/x-bzip"); } else if (ext == QLatin1String("PEM")) { m_mimeType = QStringLiteral("application/x-x509-ca-cert"); } else if (ext == QLatin1String("SWF")) { m_mimeType = QStringLiteral("application/x-shockwave-flash"); } else if (ext == QLatin1String("PLS")) { m_mimeType = QStringLiteral("audio/x-scpls"); } else if (ext == QLatin1String("WMV")) { m_mimeType = QStringLiteral("video/x-ms-wmv"); } else if (ext == QLatin1String("WEBM")) { m_mimeType = QStringLiteral("video/webm"); } else if (ext == QLatin1String("DEB")) { m_mimeType = QStringLiteral("application/x-deb"); } } qCDebug(KIO_HTTP) << "after fixup" << m_mimeType; } void HTTPProtocol::fixupResponseContentEncoding() { // WABA: Correct for tgz files with a gzip-encoding. // They really shouldn't put gzip in the Content-Encoding field! // Web-servers really shouldn't do this: They let Content-Size refer // to the size of the tgz file, not to the size of the tar file, // while the Content-Type refers to "tar" instead of "tgz". if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("gzip")) { if (m_mimeType == QLatin1String("application/x-tar")) { m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-compressed-tar"); } else if (m_mimeType == QLatin1String("application/postscript")) { // LEONB: Adding another exception for psgz files. // Could we use the mimelnk files instead of hardcoding all this? m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-gzpostscript"); } else if ((m_request.allowTransferCompression && m_mimeType == QLatin1String("text/html")) || (m_request.allowTransferCompression && m_mimeType != QLatin1String("application/x-compressed-tar") && m_mimeType != QLatin1String("application/x-tgz") && // deprecated name m_mimeType != QLatin1String("application/x-targz") && // deprecated name m_mimeType != QLatin1String("application/x-gzip"))) { // Unzip! } else { m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-gzip"); } } // We can't handle "bzip2" encoding (yet). So if we get something with // bzip2 encoding, we change the mimetype to "application/x-bzip". // Note for future changes: some web-servers send both "bzip2" as // encoding and "application/x-bzip[2]" as mimetype. That is wrong. // currently that doesn't bother us, because we remove the encoding // and set the mimetype to x-bzip anyway. if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("bzip2")) { m_contentEncodings.removeLast(); m_mimeType = QStringLiteral("application/x-bzip"); } } #ifdef Q_CC_MSVC // strncasecmp does not exist on windows, have to use _strnicmp static inline int strncasecmp(const char *c1, const char* c2, size_t max) { return _strnicmp(c1, c2, max); } #endif //Return true if the term was found, false otherwise. Advance *pos. //If (*pos + strlen(term) >= end) just advance *pos to end and return false. //This means that users should always search for the shortest terms first. static bool consume(const char input[], int *pos, int end, const char *term) { // note: gcc/g++ is quite good at optimizing away redundant strlen()s int idx = *pos; if (idx + (int)strlen(term) >= end) { *pos = end; return false; } if (strncasecmp(&input[idx], term, strlen(term)) == 0) { *pos = idx + strlen(term); return true; } return false; } /** * This function will read in the return header from the server. It will * not read in the body of the return message. It will also not transmit * the header to our client as the client doesn't need to know the gory * details of HTTP headers. */ bool HTTPProtocol::readResponseHeader() { resetResponseParsing(); if (m_request.cacheTag.ioMode == ReadFromCache && m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached) { // parseHeaderFromCache replaces this method in case of cached content return parseHeaderFromCache(); } try_again: qCDebug(KIO_HTTP); bool upgradeRequired = false; // Server demands that we upgrade to something // This is also true if we ask to upgrade and // the server accepts, since we are now // committed to doing so bool noHeadersFound = false; m_request.cacheTag.charset.clear(); m_responseHeaders.clear(); static const int maxHeaderSize = 128 * 1024; char buffer[maxHeaderSize]; bool cont = false; bool bCanResume = false; if (!isConnected()) { qCDebug(KIO_HTTP) << "No connection."; return false; // Reestablish connection and try again } #if 0 // NOTE: This is unnecessary since TCPSlaveBase::read does the same exact // thing. Plus, if we are unable to read from the socket we need to resend // the request as done below, not error out! Do not assume remote server // will honor persistent connections!! if (!waitForResponse(m_remoteRespTimeout)) { qCDebug(KIO_HTTP) << "Got socket error:" << socket()->errorString(); // No response error error(ERR_SERVER_TIMEOUT, m_request.url.host()); return false; } #endif int bufPos = 0; bool foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 1); if (!foundDelimiter && bufPos < maxHeaderSize) { qCDebug(KIO_HTTP) << "EOF while waiting for header start."; if (m_request.isKeepAlive && m_iEOFRetryCount < 2) { m_iEOFRetryCount++; httpCloseConnection(); // Try to reestablish connection. return false; // Reestablish connection and try again. } if (m_request.method == HTTP_HEAD) { // HACK // Some web-servers fail to respond properly to a HEAD request. // We compensate for their failure to properly implement the HTTP standard // by assuming that they will be sending html. qCDebug(KIO_HTTP) << "HEAD -> returned mimetype:" << DEFAULT_MIME_TYPE; mimeType(QStringLiteral(DEFAULT_MIME_TYPE)); return true; } qCDebug(KIO_HTTP) << "Connection broken !"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } if (!foundDelimiter) { //### buffer too small for first line of header(!) Q_ASSERT(0); } qCDebug(KIO_HTTP) << "============ Received Status Response:"; qCDebug(KIO_HTTP) << QByteArray(buffer, bufPos).trimmed(); HTTP_REV httpRev = HTTP_None; int idx = 0; if (idx != bufPos && buffer[idx] == '<') { qCDebug(KIO_HTTP) << "No valid HTTP header found! Document starts with XML/HTML tag"; // document starts with a tag, assume HTML instead of text/plain m_mimeType = QStringLiteral("text/html"); m_request.responseCode = 200; // Fake it httpRev = HTTP_Unknown; m_request.isKeepAlive = false; noHeadersFound = true; // put string back unread(buffer, bufPos); goto endParsing; } // "HTTP/1.1" or similar if (consume(buffer, &idx, bufPos, "ICY ")) { httpRev = SHOUTCAST; m_request.isKeepAlive = false; } else if (consume(buffer, &idx, bufPos, "HTTP/")) { if (consume(buffer, &idx, bufPos, "1.0")) { httpRev = HTTP_10; m_request.isKeepAlive = false; } else if (consume(buffer, &idx, bufPos, "1.1")) { httpRev = HTTP_11; } } if (httpRev == HTTP_None && bufPos != 0) { // Remote server does not seem to speak HTTP at all // Put the crap back into the buffer and hope for the best qCDebug(KIO_HTTP) << "DO NOT WANT." << bufPos; unread(buffer, bufPos); if (m_request.responseCode) { m_request.prevResponseCode = m_request.responseCode; } m_request.responseCode = 200; // Fake it httpRev = HTTP_Unknown; m_request.isKeepAlive = false; noHeadersFound = true; goto endParsing; } // response code //### maybe wrong if we need several iterations for this response... //### also, do multiple iterations (cf. try_again) to parse one header work w/ pipelining? if (m_request.responseCode) { m_request.prevResponseCode = m_request.responseCode; } skipSpace(buffer, &idx, bufPos); //TODO saner handling of invalid response code strings if (idx != bufPos) { m_request.responseCode = atoi(&buffer[idx]); } else { m_request.responseCode = 200; } // move idx to start of (yet to be fetched) next line, skipping the "OK" idx = bufPos; // (don't bother parsing the "OK", what do we do if it isn't there anyway?) // immediately act on most response codes... // Protect users against bogus username intended to fool them into visiting // sites they had no intention of visiting. if (isPotentialSpoofingAttack(m_request, config())) { qCDebug(KIO_HTTP) << "**** POTENTIAL ADDRESS SPOOFING:" << m_request.url; const int result = messageBox(WarningYesNo, i18nc("@info Security check on url being accessed", "

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

" "

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

", m_request.url.host(), m_request.url.userName()), i18nc("@title:window", "Confirm Website Access")); if (result == SlaveBase::No) { error(ERR_USER_CANCELED, m_request.url.toDisplayString()); return false; } setMetaData(QStringLiteral("{internal~currenthost}LastSpoofedUserName"), m_request.url.userName()); } if (m_request.responseCode != 200 && m_request.responseCode != 304) { m_request.cacheTag.ioMode = NoCache; if (m_request.responseCode >= 500 && m_request.responseCode <= 599) { // Server side errors if (m_request.method == HTTP_HEAD) { ; // Ignore error } else { if (!sendErrorPageNotification()) { error(ERR_INTERNAL_SERVER, m_request.url.toDisplayString()); return false; } } } else if (m_request.responseCode == 416) { // Range not supported m_request.offset = 0; return false; // Try again. } else if (m_request.responseCode == 426) { // Upgrade Required upgradeRequired = true; } else if (m_request.responseCode >= 400 && m_request.responseCode <= 499 && !isAuthenticationRequired(m_request.responseCode)) { // Any other client errors // Tell that we will only get an error page here. if (!sendErrorPageNotification()) { if (m_request.responseCode == 403) { error(ERR_ACCESS_DENIED, m_request.url.toDisplayString()); } else { error(ERR_DOES_NOT_EXIST, m_request.url.toDisplayString()); } } } else if (m_request.responseCode >= 301 && m_request.responseCode <= 308) { // NOTE: According to RFC 2616 (section 10.3.[2-4,8]), 301 and 302 // redirects for a POST operation should not be converted to a GET // request. That should only be done for a 303 response. However, // because almost all other client implementations do exactly that // in violation of the spec, many servers have simply adapted to // this way of doing things! Thus, we are forced to do the same // thing here. Otherwise, we loose compatibility and might not be // able to correctly retrieve sites that redirect. switch (m_request.responseCode) { case 301: // Moved Permanently setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true")); // fall through case 302: // Found if (m_request.sentMethodString == "POST") { m_request.method = HTTP_GET; // FORCE a GET setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true")); } break; case 303: // See Other if (m_request.method != HTTP_HEAD) { m_request.method = HTTP_GET; // FORCE a GET setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true")); } break; case 308: // Permanent Redirect setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true")); break; default: break; } } else if (m_request.responseCode == 204) { // No content // error(ERR_NO_CONTENT, i18n("Data have been successfully sent.")); // Short circuit and do nothing! // The original handling here was wrong, this is not an error: eg. in the // example of a 204 No Content response to a PUT completing. // return false; } else if (m_request.responseCode == 206) { if (m_request.offset) { bCanResume = true; } } else if (m_request.responseCode == 102) { // Processing (for WebDAV) /*** * This status code is given when the server expects the * command to take significant time to complete. So, inform * the user. */ infoMessage(i18n("Server processing request, please wait...")); cont = true; } else if (m_request.responseCode == 100) { // We got 'Continue' - ignore it cont = true; } } // (m_request.responseCode != 200 && m_request.responseCode != 304) endParsing: bool authRequiresAnotherRoundtrip = false; // Skip the whole header parsing if we got no HTTP headers at all if (!noHeadersFound) { // Auth handling const bool wasAuthError = isAuthenticationRequired(m_request.prevResponseCode); const bool isAuthError = isAuthenticationRequired(m_request.responseCode); const bool sameAuthError = (m_request.responseCode == m_request.prevResponseCode); qCDebug(KIO_HTTP) << "wasAuthError=" << wasAuthError << "isAuthError=" << isAuthError << "sameAuthError=" << sameAuthError; // Not the same authorization error as before and no generic error? // -> save the successful credentials. if (wasAuthError && (m_request.responseCode < 400 || (isAuthError && !sameAuthError))) { saveAuthenticationData(); } // done with the first line; now tokenize the other lines // TODO review use of STRTOLL vs. QByteArray::toInt() foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 2); qCDebug(KIO_HTTP) << " -- full response:" << endl << QByteArray(buffer, bufPos).trimmed(); // Use this to see newlines: //qCDebug(KIO_HTTP) << " -- full response:" << endl << QByteArray(buffer, bufPos).replace("\r", "\\r").replace("\n", "\\n\n"); Q_ASSERT(foundDelimiter); //NOTE because tokenizer will overwrite newlines in case of line continuations in the header // unread(buffer, bufSize) will not generally work anymore. we don't need it either. // either we have a http response line -> try to parse the header, fail if it doesn't work // or we have garbage -> fail. HeaderTokenizer tokenizer(buffer); tokenizer.tokenize(idx, sizeof(buffer)); // Note that not receiving "accept-ranges" means that all bets are off // wrt the server supporting ranges. TokenIterator tIt = tokenizer.iterator("accept-ranges"); if (tIt.hasNext() && tIt.next().toLower().startsWith("none")) { // krazy:exclude=strings bCanResume = false; } tIt = tokenizer.iterator("keep-alive"); while (tIt.hasNext()) { QByteArray ka = tIt.next().trimmed().toLower(); if (ka.startsWith("timeout=")) { // krazy:exclude=strings int ka_timeout = ka.mid(qstrlen("timeout=")).trimmed().toInt(); if (ka_timeout > 0) { m_request.keepAliveTimeout = ka_timeout; } if (httpRev == HTTP_10) { m_request.isKeepAlive = true; } break; // we want to fetch ka timeout only } } // get the size of our data tIt = tokenizer.iterator("content-length"); if (tIt.hasNext()) { m_iSize = STRTOLL(tIt.next().constData(), nullptr, 10); } tIt = tokenizer.iterator("content-location"); if (tIt.hasNext()) { setMetaData(QStringLiteral("content-location"), toQString(tIt.next().trimmed())); } // which type of data do we have? QString mediaValue; QString mediaAttribute; tIt = tokenizer.iterator("content-type"); if (tIt.hasNext()) { QList l = tIt.next().split(';'); if (!l.isEmpty()) { // Assign the mime-type. m_mimeType = toQString(l.first().trimmed().toLower()); if (m_mimeType.startsWith(QLatin1Char('"'))) { m_mimeType.remove(0, 1); } if (m_mimeType.endsWith(QLatin1Char('"'))) { m_mimeType.chop(1); } qCDebug(KIO_HTTP) << "Content-type:" << m_mimeType; l.removeFirst(); } // If we still have text, then it means we have a mime-type with a // parameter (eg: charset=iso-8851) ; so let's get that... Q_FOREACH (const QByteArray &statement, l) { const int index = statement.indexOf('='); if (index <= 0) { mediaAttribute = toQString(statement.mid(0, index)); } else { mediaAttribute = toQString(statement.mid(0, index)); mediaValue = toQString(statement.mid(index + 1)); } mediaAttribute = mediaAttribute.trimmed(); mediaValue = mediaValue.trimmed(); bool quoted = false; if (mediaValue.startsWith(QLatin1Char('"'))) { quoted = true; mediaValue.remove(0, 1); } if (mediaValue.endsWith(QLatin1Char('"'))) { mediaValue.chop(1); } qCDebug(KIO_HTTP) << "Encoding-type:" << mediaAttribute << "=" << mediaValue; if (mediaAttribute == QLatin1String("charset")) { mediaValue = mediaValue.toLower(); m_request.cacheTag.charset = mediaValue; setMetaData(QStringLiteral("charset"), mediaValue); } else { setMetaData(QLatin1String("media-") + mediaAttribute, mediaValue); if (quoted) { setMetaData(QLatin1String("media-") + mediaAttribute + QLatin1String("-kio-quoted"), QStringLiteral("true")); } } } } // content? tIt = tokenizer.iterator("content-encoding"); while (tIt.hasNext()) { // This is so wrong !! No wonder kio_http is stripping the // gzip encoding from downloaded files. This solves multiple // bug reports and caitoo's problem with downloads when such a // header is encountered... // A quote from RFC 2616: // " When present, its (Content-Encoding) value indicates what additional // content have been applied to the entity body, and thus what decoding // mechanism must be applied to obtain the media-type referenced by the // Content-Type header field. Content-Encoding is primarily used to allow // a document to be compressed without loosing the identity of its underlying // media type. Simply put if it is specified, this is the actual mime-type // we should use when we pull the resource !!! addEncoding(toQString(tIt.next()), m_contentEncodings); } // Refer to RFC 2616 sec 15.5/19.5.1 and RFC 2183 tIt = tokenizer.iterator("content-disposition"); if (tIt.hasNext()) { parseContentDisposition(toQString(tIt.next())); } tIt = tokenizer.iterator("content-language"); if (tIt.hasNext()) { QString language = toQString(tIt.next().trimmed()); if (!language.isEmpty()) { setMetaData(QStringLiteral("content-language"), language); } } tIt = tokenizer.iterator("proxy-connection"); if (tIt.hasNext() && isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { QByteArray pc = tIt.next().toLower(); if (pc.startsWith("close")) { // krazy:exclude=strings m_request.isKeepAlive = false; } else if (pc.startsWith("keep-alive")) { // krazy:exclude=strings m_request.isKeepAlive = true; } } tIt = tokenizer.iterator("link"); if (tIt.hasNext()) { // We only support Link: ; rel="type" so far QStringList link = toQString(tIt.next()).split(QLatin1Char(';'), QString::SkipEmptyParts); if (link.count() == 2) { QString rel = link[1].trimmed(); if (rel.startsWith(QLatin1String("rel=\""))) { rel = rel.mid(5, rel.length() - 6); if (rel.toLower() == QLatin1String("pageservices")) { //### the remove() part looks fishy! QString url = link[0].remove(QRegExp(QStringLiteral("[<>]"))).trimmed(); setMetaData(QStringLiteral("PageServices"), url); } } } } tIt = tokenizer.iterator("p3p"); if (tIt.hasNext()) { // P3P privacy policy information QStringList policyrefs, compact; while (tIt.hasNext()) { QStringList policy = toQString(tIt.next().simplified()) .split(QLatin1Char('='), QString::SkipEmptyParts); if (policy.count() == 2) { if (policy[0].toLower() == QLatin1String("policyref")) { policyrefs << policy[1].remove(QRegExp(QStringLiteral("[\")\']"))).trimmed(); } else if (policy[0].toLower() == QLatin1String("cp")) { // We convert to cp\ncp\ncp\n[...]\ncp to be consistent with // other metadata sent in strings. This could be a bit more // efficient but I'm going for correctness right now. const QString s = policy[1].remove(QRegExp(QStringLiteral("[\")\']"))); const QStringList cps = s.split(QLatin1Char(' '), QString::SkipEmptyParts); compact << cps; } } } if (!policyrefs.isEmpty()) { setMetaData(QStringLiteral("PrivacyPolicy"), policyrefs.join(QLatin1Char('\n'))); } if (!compact.isEmpty()) { setMetaData(QStringLiteral("PrivacyCompactPolicy"), compact.join(QLatin1Char('\n'))); } } // continue only if we know that we're at least HTTP/1.0 if (httpRev == HTTP_11 || httpRev == HTTP_10) { // let them tell us if we should stay alive or not tIt = tokenizer.iterator("connection"); while (tIt.hasNext()) { QByteArray connection = tIt.next().toLower(); if (!(isHttpProxy(m_request.proxyUrl) && !isAutoSsl())) { if (connection.startsWith("close")) { // krazy:exclude=strings m_request.isKeepAlive = false; } else if (connection.startsWith("keep-alive")) { // krazy:exclude=strings m_request.isKeepAlive = true; } } if (connection.startsWith("upgrade")) { // krazy:exclude=strings if (m_request.responseCode == 101) { // Ok, an upgrade was accepted, now we must do it upgradeRequired = true; } else if (upgradeRequired) { // 426 // Nothing to do since we did it above already } } } // what kind of encoding do we have? transfer? tIt = tokenizer.iterator("transfer-encoding"); while (tIt.hasNext()) { // If multiple encodings have been applied to an entity, the // transfer-codings MUST be listed in the order in which they // were applied. addEncoding(toQString(tIt.next().trimmed()), m_transferEncodings); } // md5 signature tIt = tokenizer.iterator("content-md5"); if (tIt.hasNext()) { m_contentMD5 = toQString(tIt.next().trimmed()); } // *** Responses to the HTTP OPTIONS method follow // WebDAV capabilities tIt = tokenizer.iterator("dav"); while (tIt.hasNext()) { m_davCapabilities << toQString(tIt.next()); } // *** Responses to the HTTP OPTIONS method finished } // Now process the HTTP/1.1 upgrade QStringList upgradeOffers; tIt = tokenizer.iterator("upgrade"); if (tIt.hasNext()) { // Now we have to check to see what is offered for the upgrade QString offered = toQString(tIt.next()); upgradeOffers = offered.split(QRegExp(QStringLiteral("[ \n,\r\t]")), QString::SkipEmptyParts); } Q_FOREACH (const QString &opt, upgradeOffers) { if (opt == QLatin1String("TLS/1.0")) { if (!startSsl() && upgradeRequired) { error(ERR_UPGRADE_REQUIRED, opt); return false; } } else if (opt == QLatin1String("HTTP/1.1")) { httpRev = HTTP_11; } else if (upgradeRequired) { // we are told to do an upgrade we don't understand error(ERR_UPGRADE_REQUIRED, opt); return false; } } // Harvest cookies (mmm, cookie fields!) QByteArray cookieStr; // In case we get a cookie. tIt = tokenizer.iterator("set-cookie"); while (tIt.hasNext()) { cookieStr += "Set-Cookie: " + tIt.next() + '\n'; } if (!cookieStr.isEmpty()) { if ((m_request.cookieMode == HTTPRequest::CookiesAuto) && m_request.useCookieJar) { // Give cookies to the cookiejar. const QString domain = config()->readEntry("cross-domain"); if (!domain.isEmpty() && isCrossDomainRequest(m_request.url.host(), domain)) { cookieStr = "Cross-Domain\n" + cookieStr; } addCookies(m_request.url.toString(), cookieStr); } else if (m_request.cookieMode == HTTPRequest::CookiesManual) { // Pass cookie to application setMetaData(QStringLiteral("setcookies"), QString::fromUtf8(cookieStr)); // ## is encoding ok? } } // We need to reread the header if we got a '100 Continue' or '102 Processing' // This may be a non keepalive connection so we handle this kind of loop internally if (cont) { qCDebug(KIO_HTTP) << "cont; returning to mark try_again"; goto try_again; } if (!m_isChunked && (m_iSize == NO_SIZE) && m_request.isKeepAlive && canHaveResponseBody(m_request.responseCode, m_request.method)) { qCDebug(KIO_HTTP) << "Ignoring keep-alive: otherwise unable to determine response body length."; m_request.isKeepAlive = false; } // TODO cache the proxy auth data (not doing this means a small performance regression for now) // we may need to send (Proxy or WWW) authorization data if ((!m_request.doNotWWWAuthenticate && m_request.responseCode == 401) || (!m_request.doNotProxyAuthenticate && m_request.responseCode == 407)) { authRequiresAnotherRoundtrip = handleAuthenticationHeader(&tokenizer); if (m_kioError) { // If error is set, then handleAuthenticationHeader failed. return false; } } else { authRequiresAnotherRoundtrip = false; } QString locationStr; // In fact we should do redirection only if we have a redirection response code (300 range) tIt = tokenizer.iterator("location"); if (tIt.hasNext() && m_request.responseCode > 299 && m_request.responseCode < 400) { locationStr = QString::fromUtf8(tIt.next().trimmed()); } // We need to do a redirect if (!locationStr.isEmpty()) { QUrl u = m_request.url.resolved(QUrl(locationStr)); if (!u.isValid()) { error(ERR_MALFORMED_URL, u.toDisplayString()); return false; } // preserve #ref: (bug 124654) // if we were at http://host/resource1#ref, we sent a GET for "/resource1" // if we got redirected to http://host/resource2, then we have to re-add // the fragment: // http to https redirection included if (m_request.url.hasFragment() && !u.hasFragment() && (m_request.url.host() == u.host()) && (m_request.url.scheme() == u.scheme() || (m_request.url.scheme() == QLatin1String("http") && u.scheme() == QLatin1String("https")))) { u.setFragment(m_request.url.fragment()); } m_isRedirection = true; if (!m_request.id.isEmpty()) { sendMetaData(); } // If we're redirected to a http:// url, remember that we're doing webdav... if (m_protocol == "webdav" || m_protocol == "webdavs") { if (u.scheme() == QLatin1String("http")) { u.setScheme(QStringLiteral("webdav")); } else if (u.scheme() == QLatin1String("https")) { u.setScheme(QStringLiteral("webdavs")); } m_request.redirectUrl = u; } qCDebug(KIO_HTTP) << "Re-directing from" << m_request.url << "to" << u; redirection(u); // It would be hard to cache the redirection response correctly. The possible benefit // is small (if at all, assuming fast disk and slow network), so don't do it. cacheFileClose(); setCacheabilityMetadata(false); } // Inform the job that we can indeed resume... if (bCanResume && m_request.offset) { //TODO turn off caching??? canResume(); } else { m_request.offset = 0; } // Correct a few common wrong content encodings fixupResponseContentEncoding(); // Correct some common incorrect pseudo-mimetypes fixupResponseMimetype(); // parse everything related to expire and other dates, and cache directives; also switch // between cache reading and writing depending on cache validation result. cacheParseResponseHeader(tokenizer); } if (m_request.cacheTag.ioMode == ReadFromCache) { if (m_request.cacheTag.policy == CC_Verify && m_request.cacheTag.plan(m_maxCacheAge) != CacheTag::UseCached) { qCDebug(KIO_HTTP) << "Reading resource from cache even though the cache plan is not " "UseCached; the server is probably sending wrong expiry information."; } // parseHeaderFromCache replaces this method in case of cached content return parseHeaderFromCache(); } if (config()->readEntry("PropagateHttpHeader", false) || m_request.cacheTag.ioMode == WriteToCache) { // store header lines if they will be used; note that the tokenizer removing // line continuation special cases is probably more good than bad. int nextLinePos = 0; int prevLinePos = 0; bool haveMore = true; while (haveMore) { haveMore = nextLine(buffer, &nextLinePos, bufPos); int prevLineEnd = nextLinePos; while (buffer[prevLineEnd - 1] == '\r' || buffer[prevLineEnd - 1] == '\n') { prevLineEnd--; } m_responseHeaders.append(QString::fromLatin1(&buffer[prevLinePos], prevLineEnd - prevLinePos)); prevLinePos = nextLinePos; } // IMPORTANT: Do not remove this line because forwardHttpResponseHeader // is called below. This line is here to ensure the response headers are // available to the client before it receives mimetype information. // The support for putting ioslaves on hold in the KIO-QNAM integration // will break if this line is removed. setMetaData(QStringLiteral("HTTP-Headers"), m_responseHeaders.join(QLatin1Char('\n'))); } // Let the app know about the mime-type iff this is not a redirection and // the mime-type string is not empty. if (!m_isRedirection && m_request.responseCode != 204 && (!m_mimeType.isEmpty() || m_request.method == HTTP_HEAD) && !m_kioError && (m_isLoadingErrorPage || !authRequiresAnotherRoundtrip)) { qCDebug(KIO_HTTP) << "Emitting mimetype " << m_mimeType; mimeType(m_mimeType); } // IMPORTANT: Do not move the function call below before doing any // redirection. Otherwise it might mess up some sites, see BR# 150904. forwardHttpResponseHeader(); if (m_request.method == HTTP_HEAD) { return true; } return !authRequiresAnotherRoundtrip; // return true if no more credentials need to be sent } void HTTPProtocol::parseContentDisposition(const QString &disposition) { const QMap parameters = contentDispositionParser(disposition); QMap::const_iterator i = parameters.constBegin(); while (i != parameters.constEnd()) { setMetaData(QLatin1String("content-disposition-") + i.key(), i.value()); qCDebug(KIO_HTTP) << "Content-Disposition:" << i.key() << "=" << i.value(); ++i; } } void HTTPProtocol::addEncoding(const QString &_encoding, QStringList &encs) { QString encoding = _encoding.trimmed().toLower(); // Identity is the same as no encoding if (encoding == QLatin1String("identity")) { return; } else if (encoding == QLatin1String("8bit")) { // Strange encoding returned by http://linac.ikp.physik.tu-darmstadt.de return; } else if (encoding == QLatin1String("chunked")) { m_isChunked = true; // Anyone know of a better way to handle unknown sizes possibly/ideally with unsigned ints? //if ( m_cmd != CMD_COPY ) m_iSize = NO_SIZE; } else if ((encoding == QLatin1String("x-gzip")) || (encoding == QLatin1String("gzip"))) { encs.append(QStringLiteral("gzip")); } else if ((encoding == QLatin1String("x-bzip2")) || (encoding == QLatin1String("bzip2"))) { encs.append(QStringLiteral("bzip2")); // Not yet supported! } else if ((encoding == QLatin1String("x-deflate")) || (encoding == QLatin1String("deflate"))) { encs.append(QStringLiteral("deflate")); } else { qCDebug(KIO_HTTP) << "Unknown encoding encountered. " << "Please write code. Encoding =" << encoding; } } void HTTPProtocol::cacheParseResponseHeader(const HeaderTokenizer &tokenizer) { if (!m_request.cacheTag.useCache) { return; } // might have to add more response codes if (m_request.responseCode != 200 && m_request.responseCode != 304) { return; } m_request.cacheTag.servedDate = QDateTime(); m_request.cacheTag.lastModifiedDate = QDateTime(); m_request.cacheTag.expireDate = QDateTime(); const QDateTime currentDate = QDateTime::currentDateTime(); bool mayCache = m_request.cacheTag.ioMode != NoCache; TokenIterator tIt = tokenizer.iterator("last-modified"); if (tIt.hasNext()) { m_request.cacheTag.lastModifiedDate = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); //### might be good to canonicalize the date by using QDateTime::toString() if (m_request.cacheTag.lastModifiedDate.isValid()) { setMetaData(QStringLiteral("modified"), toQString(tIt.current())); } } // determine from available information when the response was served by the origin server { QDateTime dateHeader; tIt = tokenizer.iterator("date"); if (tIt.hasNext()) { dateHeader = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); // -1 on error } qint64 ageHeader = 0; tIt = tokenizer.iterator("age"); if (tIt.hasNext()) { ageHeader = tIt.next().toLongLong(); // 0 on error } if (dateHeader.isValid()) { m_request.cacheTag.servedDate = dateHeader; } else if (ageHeader) { m_request.cacheTag.servedDate = currentDate.addSecs(-ageHeader); } else { m_request.cacheTag.servedDate = currentDate; } } bool hasCacheDirective = false; // determine when the response "expires", i.e. becomes stale and needs revalidation { // (we also parse other cache directives here) qint64 maxAgeHeader = 0; tIt = tokenizer.iterator("cache-control"); while (tIt.hasNext()) { QByteArray cacheStr = tIt.next().toLower(); if (cacheStr.startsWith("no-cache") || cacheStr.startsWith("no-store")) { // krazy:exclude=strings // Don't put in cache mayCache = false; hasCacheDirective = true; } else if (cacheStr.startsWith("max-age=")) { // krazy:exclude=strings QByteArray ba = cacheStr.mid(qstrlen("max-age=")).trimmed(); bool ok = false; maxAgeHeader = ba.toLongLong(&ok); if (ok) { hasCacheDirective = true; } } } QDateTime expiresHeader; tIt = tokenizer.iterator("expires"); if (tIt.hasNext()) { expiresHeader = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); qCDebug(KIO_HTTP) << "parsed expire date from 'expires' header:" << tIt.current(); } if (maxAgeHeader) { m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(maxAgeHeader); } else if (expiresHeader.isValid()) { m_request.cacheTag.expireDate = expiresHeader; } else { // heuristic expiration date if (m_request.cacheTag.lastModifiedDate.isValid()) { // expAge is following the RFC 2616 suggestion for heuristic expiration qint64 expAge = (m_request.cacheTag.lastModifiedDate.secsTo(m_request.cacheTag.servedDate)) / 10; // not in the RFC: make sure not to have a huge heuristic cache lifetime expAge = qMin(expAge, qint64(3600 * 24)); m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(expAge); } else { m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(DEFAULT_CACHE_EXPIRE); } } // make sure that no future clock monkey business causes the cache entry to un-expire if (m_request.cacheTag.expireDate < currentDate) { m_request.cacheTag.expireDate.setMSecsSinceEpoch(0); // January 1, 1970 :) } } tIt = tokenizer.iterator("etag"); if (tIt.hasNext()) { QString prevEtag = m_request.cacheTag.etag; m_request.cacheTag.etag = toQString(tIt.next()); if (m_request.cacheTag.etag != prevEtag && m_request.responseCode == 304) { qCDebug(KIO_HTTP) << "304 Not Modified but new entity tag - I don't think this is legal HTTP."; } } // whoops.. we received a warning tIt = tokenizer.iterator("warning"); if (tIt.hasNext()) { //Don't use warning() here, no need to bother the user. //Those warnings are mostly about caches. infoMessage(toQString(tIt.next())); } // Cache management (HTTP 1.0) tIt = tokenizer.iterator("pragma"); while (tIt.hasNext()) { if (tIt.next().toLower().startsWith("no-cache")) { // krazy:exclude=strings mayCache = false; hasCacheDirective = true; } } // The deprecated Refresh Response tIt = tokenizer.iterator("refresh"); if (tIt.hasNext()) { mayCache = false; setMetaData(QStringLiteral("http-refresh"), toQString(tIt.next().trimmed())); } // We don't cache certain text objects if (m_mimeType.startsWith(QLatin1String("text/")) && (m_mimeType != QLatin1String("text/css")) && (m_mimeType != QLatin1String("text/x-javascript")) && !hasCacheDirective) { // Do not cache secure pages or pages // originating from password protected sites // unless the webserver explicitly allows it. if (isUsingSsl() || m_wwwAuth) { mayCache = false; } } // note that we've updated cacheTag, so the plan() is with current data if (m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::ValidateCached) { qCDebug(KIO_HTTP) << "Cache needs validation"; if (m_request.responseCode == 304) { qCDebug(KIO_HTTP) << "...was revalidated by response code but not by updated expire times. " "We're going to set the expire date to 60 seconds in the future..."; m_request.cacheTag.expireDate = currentDate.addSecs(60); if (m_request.cacheTag.policy == CC_Verify && m_request.cacheTag.plan(m_maxCacheAge) != CacheTag::UseCached) { // "apparently" because we /could/ have made an error ourselves, but the errors I // witnessed were all the server's fault. qCDebug(KIO_HTTP) << "this proxy or server apparently sends bogus expiry information."; } } } // validation handling if (mayCache && m_request.responseCode == 200 && !m_mimeType.isEmpty()) { qCDebug(KIO_HTTP) << "Cache, adding" << m_request.url; // ioMode can still be ReadFromCache here if we're performing a conditional get // aka validation m_request.cacheTag.ioMode = WriteToCache; if (!cacheFileOpenWrite()) { qCDebug(KIO_HTTP) << "Error creating cache entry for" << m_request.url << "!"; } m_maxCacheSize = config()->readEntry("MaxCacheSize", DEFAULT_MAX_CACHE_SIZE); } else if (m_request.responseCode == 304 && m_request.cacheTag.file) { if (!mayCache) { qCDebug(KIO_HTTP) << "This webserver is confused about the cacheability of the data it sends."; } // the cache file should still be open for reading, see satisfyRequestFromCache(). Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly); Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache); } else { cacheFileClose(); } setCacheabilityMetadata(mayCache); } void HTTPProtocol::setCacheabilityMetadata(bool cachingAllowed) { if (!cachingAllowed) { setMetaData(QStringLiteral("no-cache"), QStringLiteral("true")); setMetaData(QStringLiteral("expire-date"), QStringLiteral("1")); // Expired } else { QString tmp; tmp.setNum(m_request.cacheTag.expireDate.toSecsSinceEpoch()); setMetaData(QStringLiteral("expire-date"), tmp); // slightly changed semantics from old creationDate, probably more correct now tmp.setNum(m_request.cacheTag.servedDate.toSecsSinceEpoch()); setMetaData(QStringLiteral("cache-creation-date"), tmp); } } bool HTTPProtocol::sendCachedBody() { infoMessage(i18n("Sending data to %1", m_request.url.host())); const qint64 size = m_POSTbuf->size(); const QByteArray cLength = "Content-Length: " + QByteArray::number(size) + "\r\n\r\n"; //qDebug() << "sending cached data (size=" << size << ")"; // Send the content length... bool sendOk = (write(cLength.data(), cLength.size()) == (ssize_t) cLength.size()); if (!sendOk) { qCDebug(KIO_HTTP) << "Connection broken when sending " << "content length: (" << m_request.url.host() << ")"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } totalSize(size); // Make sure the read head is at the beginning... m_POSTbuf->reset(); KIO::filesize_t totalBytesSent = 0; // Send the data... while (!m_POSTbuf->atEnd()) { const QByteArray buffer = m_POSTbuf->read(65536); const ssize_t bytesSent = write(buffer.data(), buffer.size()); if (bytesSent != static_cast(buffer.size())) { qCDebug(KIO_HTTP) << "Connection broken when sending message body: (" << m_request.url.host() << ")"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } totalBytesSent += bytesSent; processedSize(totalBytesSent); } return true; } bool HTTPProtocol::sendBody() { // If we have cached data, the it is either a repost or a DAV request so send // the cached data... if (m_POSTbuf) { return sendCachedBody(); } if (m_iPostDataSize == NO_SIZE) { // Try the old approach of retrieving content data from the job // before giving up. if (retrieveAllData()) { return sendCachedBody(); } error(ERR_POST_NO_SIZE, m_request.url.host()); return false; } qCDebug(KIO_HTTP) << "sending data (size=" << m_iPostDataSize << ")"; infoMessage(i18n("Sending data to %1", m_request.url.host())); const QByteArray cLength = "Content-Length: " + QByteArray::number(m_iPostDataSize) + "\r\n\r\n"; qCDebug(KIO_HTTP) << cLength.trimmed(); // Send the content length... bool sendOk = (write(cLength.data(), cLength.size()) == (ssize_t) cLength.size()); if (!sendOk) { // The server might have closed the connection due to a timeout, or maybe // some transport problem arose while the connection was idle. if (m_request.isKeepAlive) { httpCloseConnection(); return true; // Try again } qCDebug(KIO_HTTP) << "Connection broken while sending POST content size to" << m_request.url.host(); error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } // Send the amount totalSize(m_iPostDataSize); // If content-length is 0, then do nothing but simply return true. if (m_iPostDataSize == 0) { return true; } sendOk = true; KIO::filesize_t bytesSent = 0; while (true) { dataReq(); QByteArray buffer; const int bytesRead = readData(buffer); // On done... if (bytesRead == 0) { sendOk = (bytesSent == m_iPostDataSize); break; } // On error return false... if (bytesRead < 0) { error(ERR_ABORTED, m_request.url.host()); sendOk = false; break; } // Cache the POST data in case of a repost request. cachePostData(buffer); // This will only happen if transmitting the data fails, so we will simply // cache the content locally for the potential re-transmit... if (!sendOk) { continue; } if (write(buffer.data(), bytesRead) == static_cast(bytesRead)) { bytesSent += bytesRead; processedSize(bytesSent); // Send update status... continue; } qCDebug(KIO_HTTP) << "Connection broken while sending POST content to" << m_request.url.host(); error(ERR_CONNECTION_BROKEN, m_request.url.host()); sendOk = false; } return sendOk; } void HTTPProtocol::httpClose(bool keepAlive) { qCDebug(KIO_HTTP) << "keepAlive =" << keepAlive; cacheFileClose(); // Only allow persistent connections for GET requests. // NOTE: we might even want to narrow this down to non-form // based submit requests which will require a meta-data from // khtml. if (keepAlive) { if (!m_request.keepAliveTimeout) { m_request.keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT; } else if (m_request.keepAliveTimeout > 2 * DEFAULT_KEEP_ALIVE_TIMEOUT) { m_request.keepAliveTimeout = 2 * DEFAULT_KEEP_ALIVE_TIMEOUT; } qCDebug(KIO_HTTP) << "keep alive (" << m_request.keepAliveTimeout << ")"; QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); stream << int(99); // special: Close connection setTimeoutSpecialCommand(m_request.keepAliveTimeout, data); return; } httpCloseConnection(); } void HTTPProtocol::closeConnection() { qCDebug(KIO_HTTP); httpCloseConnection(); } void HTTPProtocol::httpCloseConnection() { qCDebug(KIO_HTTP); m_server.clear(); disconnectFromHost(); clearUnreadBuffer(); setTimeoutSpecialCommand(-1); // Cancel any connection timeout } void HTTPProtocol::slave_status() { qCDebug(KIO_HTTP); if (!isConnected()) { httpCloseConnection(); } slaveStatus(m_server.url.host(), isConnected()); } void HTTPProtocol::mimetype(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); m_request.method = HTTP_HEAD; m_request.cacheTag.policy = CC_Cache; if (proceedUntilResponseHeader()) { httpClose(m_request.isKeepAlive); finished(); } qCDebug(KIO_HTTP) << m_mimeType; } void HTTPProtocol::special(const QByteArray &data) { qCDebug(KIO_HTTP); int tmp; QDataStream stream(data); stream >> tmp; switch (tmp) { case 1: { // HTTP POST QUrl url; qint64 size; stream >> url >> size; post(url, size); break; } case 2: { // cache_update QUrl url; bool no_cache; qint64 expireDate; stream >> url >> no_cache >> expireDate; if (no_cache) { QString filename = cacheFilePathFromUrl(url); // there is a tiny risk of deleting the wrong file due to hash collisions here. // this is an unimportant performance issue. // FIXME on Windows we may be unable to delete the file if open QFile::remove(filename); finished(); break; } // let's be paranoid and inefficient here... HTTPRequest savedRequest = m_request; m_request.url = url; if (cacheFileOpenRead()) { - m_request.cacheTag.expireDate.setTime_t(expireDate); + m_request.cacheTag.expireDate.setSecsSinceEpoch(expireDate); cacheFileClose(); // this sends an update command to the cache cleaner process } m_request = savedRequest; finished(); break; } case 5: { // WebDAV lock QUrl url; QString scope, type, owner; stream >> url >> scope >> type >> owner; davLock(url, scope, type, owner); break; } case 6: { // WebDAV unlock QUrl url; stream >> url; davUnlock(url); break; } case 7: { // Generic WebDAV QUrl url; int method; qint64 size; stream >> url >> method >> size; davGeneric(url, (KIO::HTTP_METHOD) method, size); break; } case 99: { // Close Connection httpCloseConnection(); break; } default: // Some command we don't understand. // Just ignore it, it may come from some future version of KDE. break; } } /** * Read a chunk from the data stream. */ int HTTPProtocol::readChunked() { if ((m_iBytesLeft == 0) || (m_iBytesLeft == NO_SIZE)) { // discard CRLF from previous chunk, if any, and read size of next chunk int bufPos = 0; m_receiveBuf.resize(4096); bool foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); if (foundCrLf && bufPos == 2) { // The previous read gave us the CRLF from the previous chunk. As bufPos includes // the trailing CRLF it has to be > 2 to possibly include the next chunksize. bufPos = 0; foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); } if (!foundCrLf) { qCDebug(KIO_HTTP) << "Failed to read chunk header."; return -1; } Q_ASSERT(bufPos > 2); long long nextChunkSize = STRTOLL(m_receiveBuf.data(), nullptr, 16); if (nextChunkSize < 0) { qCDebug(KIO_HTTP) << "Negative chunk size"; return -1; } m_iBytesLeft = nextChunkSize; qCDebug(KIO_HTTP) << "Chunk size =" << m_iBytesLeft << "bytes"; if (m_iBytesLeft == 0) { // Last chunk; read and discard chunk trailer. // The last trailer line ends with CRLF and is followed by another CRLF // so we have CRLFCRLF like at the end of a standard HTTP header. // Do not miss a CRLFCRLF spread over two of our 4K blocks: keep three previous bytes. //NOTE the CRLF after the chunksize also counts if there is no trailer. Copy it over. char trash[4096]; trash[0] = m_receiveBuf.constData()[bufPos - 2]; trash[1] = m_receiveBuf.constData()[bufPos - 1]; int trashBufPos = 2; bool done = false; while (!done && !m_isEOF) { if (trashBufPos > 3) { // shift everything but the last three bytes out of the buffer for (int i = 0; i < 3; i++) { trash[i] = trash[trashBufPos - 3 + i]; } trashBufPos = 3; } done = readDelimitedText(trash, &trashBufPos, 4096, 2); } if (m_isEOF && !done) { qCDebug(KIO_HTTP) << "Failed to read chunk trailer."; return -1; } return 0; } } int bytesReceived = readLimited(); if (!m_iBytesLeft) { m_iBytesLeft = NO_SIZE; // Don't stop, continue with next chunk } return bytesReceived; } int HTTPProtocol::readLimited() { if (!m_iBytesLeft) { return 0; } m_receiveBuf.resize(4096); int bytesToReceive; if (m_iBytesLeft > KIO::filesize_t(m_receiveBuf.size())) { bytesToReceive = m_receiveBuf.size(); } else { bytesToReceive = m_iBytesLeft; } const int bytesReceived = readBuffered(m_receiveBuf.data(), bytesToReceive, false); if (bytesReceived <= 0) { return -1; // Error: connection lost } m_iBytesLeft -= bytesReceived; return bytesReceived; } int HTTPProtocol::readUnlimited() { if (m_request.isKeepAlive) { qCDebug(KIO_HTTP) << "Unbounded datastream on a Keep-alive connection!"; m_request.isKeepAlive = false; } m_receiveBuf.resize(4096); int result = readBuffered(m_receiveBuf.data(), m_receiveBuf.size()); if (result > 0) { return result; } m_isEOF = true; m_iBytesLeft = 0; return 0; } void HTTPProtocol::slotData(const QByteArray &_d) { if (!_d.size()) { m_isEOD = true; return; } if (m_iContentLeft != NO_SIZE) { if (m_iContentLeft >= KIO::filesize_t(_d.size())) { m_iContentLeft -= _d.size(); } else { m_iContentLeft = NO_SIZE; } } QByteArray d = _d; if (!m_dataInternal) { // If a broken server does not send the mime-type, // we try to id it from the content before dealing // with the content itself. if (m_mimeType.isEmpty() && !m_isRedirection && !(m_request.responseCode >= 300 && m_request.responseCode <= 399)) { qCDebug(KIO_HTTP) << "Determining mime-type from content..."; int old_size = m_mimeTypeBuffer.size(); m_mimeTypeBuffer.resize(old_size + d.size()); memcpy(m_mimeTypeBuffer.data() + old_size, d.data(), d.size()); if ((m_iBytesLeft != NO_SIZE) && (m_iBytesLeft > 0) && (m_mimeTypeBuffer.size() < 1024)) { m_cpMimeBuffer = true; return; // Do not send up the data since we do not yet know its mimetype! } qCDebug(KIO_HTTP) << "Mimetype buffer size:" << m_mimeTypeBuffer.size(); QMimeDatabase db; QMimeType mime = db.mimeTypeForFileNameAndData(m_request.url.adjusted(QUrl::StripTrailingSlash).path(), m_mimeTypeBuffer); if (mime.isValid() && !mime.isDefault()) { m_mimeType = mime.name(); qCDebug(KIO_HTTP) << "Mimetype from content:" << m_mimeType; } if (m_mimeType.isEmpty()) { m_mimeType = QStringLiteral(DEFAULT_MIME_TYPE); qCDebug(KIO_HTTP) << "Using default mimetype:" << m_mimeType; } //### we could also open the cache file here if (m_cpMimeBuffer) { d.resize(0); d.resize(m_mimeTypeBuffer.size()); memcpy(d.data(), m_mimeTypeBuffer.data(), d.size()); } mimeType(m_mimeType); m_mimeTypeBuffer.resize(0); } //qDebug() << "Sending data of size" << d.size(); data(d); if (m_request.cacheTag.ioMode == WriteToCache) { cacheFileWritePayload(d); } } else { uint old_size = m_webDavDataBuf.size(); m_webDavDataBuf.resize(old_size + d.size()); memcpy(m_webDavDataBuf.data() + old_size, d.data(), d.size()); } } /** * This function is our "receive" function. It is responsible for * downloading the message (not the header) from the HTTP server. It * is called either as a response to a client's KIOJob::dataEnd() * (meaning that the client is done sending data) or by 'sendQuery()' * (if we are in the process of a PUT/POST request). It can also be * called by a webDAV function, to receive stat/list/property/etc. * data; in this case the data is stored in m_webDavDataBuf. */ bool HTTPProtocol::readBody(bool dataInternal /* = false */) { // special case for reading cached body since we also do it in this function. oh well. if (!canHaveResponseBody(m_request.responseCode, m_request.method) && !(m_request.cacheTag.ioMode == ReadFromCache && m_request.responseCode == 304 && m_request.method != HTTP_HEAD)) { return true; } m_isEOD = false; // Note that when dataInternal is true, we are going to: // 1) save the body data to a member variable, m_webDavDataBuf // 2) _not_ advertise the data, speed, size, etc., through the // corresponding functions. // This is used for returning data to WebDAV. m_dataInternal = dataInternal; if (dataInternal) { m_webDavDataBuf.clear(); } // Check if we need to decode the data. // If we are in copy mode, then use only transfer decoding. bool useMD5 = !m_contentMD5.isEmpty(); // Deal with the size of the file. KIO::filesize_t sz = m_request.offset; if (sz) { m_iSize += sz; } if (!m_isRedirection) { // Update the application with total size except when // it is compressed, or when the data is to be handled // internally (webDAV). If compressed we have to wait // until we uncompress to find out the actual data size if (!dataInternal) { if ((m_iSize > 0) && (m_iSize != NO_SIZE)) { totalSize(m_iSize); infoMessage(i18n("Retrieving %1 from %2...", KIO::convertSize(m_iSize), m_request.url.host())); } else { totalSize(0); } } if (m_request.cacheTag.ioMode == ReadFromCache) { qCDebug(KIO_HTTP) << "reading data from cache..."; m_iContentLeft = NO_SIZE; QByteArray d; while (true) { d = cacheFileReadPayload(MAX_IPC_SIZE); if (d.isEmpty()) { break; } slotData(d); sz += d.size(); if (!dataInternal) { processedSize(sz); } } m_receiveBuf.resize(0); if (!dataInternal) { data(QByteArray()); } return true; } } if (m_iSize != NO_SIZE) { m_iBytesLeft = m_iSize - sz; } else { m_iBytesLeft = NO_SIZE; } m_iContentLeft = m_iBytesLeft; if (m_isChunked) { m_iBytesLeft = NO_SIZE; } qCDebug(KIO_HTTP) << KIO::number(m_iBytesLeft) << "bytes left."; // Main incoming loop... Gather everything while we can... m_cpMimeBuffer = false; m_mimeTypeBuffer.resize(0); HTTPFilterChain chain; // redirection ignores the body if (!m_isRedirection) { QObject::connect(&chain, &HTTPFilterBase::output, this, &HTTPProtocol::slotData); } QObject::connect(&chain, &HTTPFilterBase::error, this, &HTTPProtocol::slotFilterError); // decode all of the transfer encodings while (!m_transferEncodings.isEmpty()) { QString enc = m_transferEncodings.takeLast(); if (enc == QLatin1String("gzip")) { chain.addFilter(new HTTPFilterGZip); } else if (enc == QLatin1String("deflate")) { chain.addFilter(new HTTPFilterDeflate); } } // From HTTP 1.1 Draft 6: // The MD5 digest is computed based on the content of the entity-body, // including any content-coding that has been applied, but not including // any transfer-encoding applied to the message-body. If the message is // received with a transfer-encoding, that encoding MUST be removed // prior to checking the Content-MD5 value against the received entity. HTTPFilterMD5 *md5Filter = nullptr; if (useMD5) { md5Filter = new HTTPFilterMD5; chain.addFilter(md5Filter); } // now decode all of the content encodings // -- Why ?? We are not // -- a proxy server, be a client side implementation!! The applications // -- are capable of determining how to extract the encoded implementation. // WB: That's a misunderstanding. We are free to remove the encoding. // WB: Some braindead www-servers however, give .tgz files an encoding // WB: of "gzip" (or even "x-gzip") and a content-type of "applications/tar" // WB: They shouldn't do that. We can work around that though... while (!m_contentEncodings.isEmpty()) { QString enc = m_contentEncodings.takeLast(); if (enc == QLatin1String("gzip")) { chain.addFilter(new HTTPFilterGZip); } else if (enc == QLatin1String("deflate")) { chain.addFilter(new HTTPFilterDeflate); } } while (!m_isEOF) { int bytesReceived; if (m_isChunked) { bytesReceived = readChunked(); } else if (m_iSize != NO_SIZE) { bytesReceived = readLimited(); } else { bytesReceived = readUnlimited(); } // make sure that this wasn't an error, first qCDebug(KIO_HTTP) << "bytesReceived:" << bytesReceived << " m_iSize:" << (int)m_iSize << " Chunked:" << m_isChunked << " BytesLeft:"<< (int)m_iBytesLeft; if (bytesReceived == -1) { if (m_iContentLeft == 0) { // gzip'ed data sometimes reports a too long content-length. // (The length of the unzipped data) m_iBytesLeft = 0; break; } // Oh well... log an error and bug out qCDebug(KIO_HTTP) << "bytesReceived==-1 sz=" << (int)sz << " Connection broken !"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } // I guess that nbytes == 0 isn't an error.. but we certainly // won't work with it! if (bytesReceived > 0) { // Important: truncate the buffer to the actual size received! // Otherwise garbage will be passed to the app m_receiveBuf.truncate(bytesReceived); chain.slotInput(m_receiveBuf); if (m_kioError) { return false; } sz += bytesReceived; if (!dataInternal) { processedSize(sz); } } m_receiveBuf.resize(0); // res if (m_iBytesLeft && m_isEOD && !m_isChunked) { // gzip'ed data sometimes reports a too long content-length. // (The length of the unzipped data) m_iBytesLeft = 0; } if (m_iBytesLeft == 0) { qCDebug(KIO_HTTP) << "EOD received! Left ="<< KIO::number(m_iBytesLeft); break; } } chain.slotInput(QByteArray()); // Flush chain. if (useMD5) { QString calculatedMD5 = md5Filter->md5(); if (m_contentMD5 != calculatedMD5) qCWarning(KIO_HTTP) << "MD5 checksum MISMATCH! Expected:" << calculatedMD5 << ", Got:" << m_contentMD5; } // Close cache entry if (m_iBytesLeft == 0) { cacheFileClose(); // no-op if not necessary } if (!dataInternal && sz <= 1) { if (m_request.responseCode >= 500 && m_request.responseCode <= 599) { error(ERR_INTERNAL_SERVER, m_request.url.host()); return false; } else if (m_request.responseCode >= 400 && m_request.responseCode <= 499 && !isAuthenticationRequired(m_request.responseCode)) { error(ERR_DOES_NOT_EXIST, m_request.url.host()); return false; } } if (!dataInternal && !m_isRedirection) { data(QByteArray()); } return true; } void HTTPProtocol::slotFilterError(const QString &text) { error(KIO::ERR_SLAVE_DEFINED, text); } void HTTPProtocol::error(int _err, const QString &_text) { // Close the connection only on connection errors. Otherwise, honor the // keep alive flag. if (_err == ERR_CONNECTION_BROKEN || _err == ERR_CANNOT_CONNECT) { httpClose(false); } else { httpClose(m_request.isKeepAlive); } if (!m_request.id.isEmpty()) { forwardHttpResponseHeader(); sendMetaData(); } // It's over, we don't need it anymore clearPostDataBuffer(); SlaveBase::error(_err, _text); m_kioError = _err; } void HTTPProtocol::addCookies(const QString &url, const QByteArray &cookieHeader) { qlonglong windowId = m_request.windowId.toLongLong(); QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); (void)kcookiejar.call(QDBus::NoBlock, QStringLiteral("addCookies"), url, cookieHeader, windowId); } QString HTTPProtocol::findCookies(const QString &url) { qlonglong windowId = m_request.windowId.toLongLong(); QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); QDBusReply reply = kcookiejar.call(QStringLiteral("findCookies"), url, windowId); if (!reply.isValid()) { qCWarning(KIO_HTTP) << "Can't communicate with kded_kcookiejar!"; return QString(); } return reply; } /******************************* CACHING CODE ****************************/ HTTPProtocol::CacheTag::CachePlan HTTPProtocol::CacheTag::plan(int maxCacheAge) const { //notable omission: we're not checking cache file presence or integrity switch (policy) { case KIO::CC_Refresh: // Conditional GET requires the presence of either an ETag or // last modified date. if (lastModifiedDate.isValid() || !etag.isEmpty()) { return ValidateCached; } break; case KIO::CC_Reload: return IgnoreCached; case KIO::CC_CacheOnly: case KIO::CC_Cache: return UseCached; default: break; } Q_ASSERT((policy == CC_Verify || policy == CC_Refresh)); QDateTime currentDate = QDateTime::currentDateTime(); if ((servedDate.isValid() && (currentDate > servedDate.addSecs(maxCacheAge))) || (expireDate.isValid() && (currentDate > expireDate))) { return ValidateCached; } return UseCached; } // !START SYNC! // The following code should be kept in sync // with the code in http_cache_cleaner.cpp // we use QDataStream; this is just an illustration struct BinaryCacheFileHeader { quint8 version[2]; quint8 compression; // for now fixed to 0 quint8 reserved; // for now; also alignment qint32 useCount; qint64 servedDate; qint64 lastModifiedDate; qint64 expireDate; qint32 bytesCached; // packed size should be 36 bytes; we explicitly set it here to make sure that no compiler // padding ruins it. We write the fields to disk without any padding. static const int size = 36; }; enum CacheCleanerCommandCode { InvalidCommand = 0, CreateFileNotificationCommand, UpdateFileCommand }; // illustration for cache cleaner update "commands" struct CacheCleanerCommand { BinaryCacheFileHeader header; quint32 commandCode; // filename in ASCII, binary isn't worth the coding and decoding quint8 filename[s_hashedUrlNibbles]; }; QByteArray HTTPProtocol::CacheTag::serialize() const { QByteArray ret; QDataStream stream(&ret, QIODevice::WriteOnly); stream << quint8('A'); stream << quint8('\n'); stream << quint8(0); stream << quint8(0); stream << fileUseCount; stream << servedDate.toMSecsSinceEpoch() / 1000; stream << lastModifiedDate.toMSecsSinceEpoch() / 1000; stream << expireDate.toMSecsSinceEpoch() / 1000; stream << bytesCached; Q_ASSERT(ret.size() == BinaryCacheFileHeader::size); return ret; } static bool compareByte(QDataStream *stream, quint8 value) { quint8 byte; *stream >> byte; return byte == value; } // If starting a new file cacheFileWriteVariableSizeHeader() must have been called *before* // calling this! This is to fill in the headerEnd field. // If the file is not new headerEnd has already been read from the file and in fact the variable // size header *may* not be rewritten because a size change would mess up the file layout. bool HTTPProtocol::CacheTag::deserialize(const QByteArray &d) { if (d.size() != BinaryCacheFileHeader::size) { return false; } QDataStream stream(d); stream.setVersion(QDataStream::Qt_4_5); bool ok = true; ok = ok && compareByte(&stream, 'A'); ok = ok && compareByte(&stream, '\n'); ok = ok && compareByte(&stream, 0); ok = ok && compareByte(&stream, 0); if (!ok) { return false; } stream >> fileUseCount; qint64 servedDateMs; stream >> servedDateMs; servedDate = QDateTime::fromMSecsSinceEpoch(servedDateMs * 1000); qint64 lastModifiedDateMs; stream >> lastModifiedDateMs; lastModifiedDate = QDateTime::fromMSecsSinceEpoch(lastModifiedDateMs * 1000); qint64 expireDateMs; stream >> expireDateMs; expireDate = QDateTime::fromMSecsSinceEpoch(expireDateMs * 1000); stream >> bytesCached; return true; } /* Text part of the header, directly following the binary first part: URL\n etag\n mimetype\n header line\n header line\n ... \n */ static QUrl storableUrl(const QUrl &url) { QUrl ret(url); ret.setPassword(QString()); ret.setFragment(QString()); return ret; } static void writeLine(QIODevice *dev, const QByteArray &line) { static const char linefeed = '\n'; dev->write(line); dev->write(&linefeed, 1); } void HTTPProtocol::cacheFileWriteTextHeader() { QFile *&file = m_request.cacheTag.file; Q_ASSERT(file); Q_ASSERT(file->openMode() & QIODevice::WriteOnly); file->seek(BinaryCacheFileHeader::size); writeLine(file, storableUrl(m_request.url).toEncoded()); writeLine(file, m_request.cacheTag.etag.toLatin1()); writeLine(file, m_mimeType.toLatin1()); writeLine(file, m_responseHeaders.join(QLatin1Char('\n')).toLatin1()); // join("\n") adds no \n to the end, but writeLine() does. // Add another newline to mark the end of text. writeLine(file, QByteArray()); } static bool readLineChecked(QIODevice *dev, QByteArray *line) { *line = dev->readLine(MAX_IPC_SIZE); // if nothing read or the line didn't fit into 8192 bytes(!) if (line->isEmpty() || !line->endsWith('\n')) { return false; } // we don't actually want the newline! line->chop(1); return true; } bool HTTPProtocol::cacheFileReadTextHeader1(const QUrl &desiredUrl) { QFile *&file = m_request.cacheTag.file; Q_ASSERT(file); Q_ASSERT(file->openMode() == QIODevice::ReadOnly); QByteArray readBuf; bool ok = readLineChecked(file, &readBuf); if (storableUrl(desiredUrl).toEncoded() != readBuf) { qCDebug(KIO_HTTP) << "You have witnessed a very improbable hash collision!"; return false; } ok = ok && readLineChecked(file, &readBuf); m_request.cacheTag.etag = toQString(readBuf); return ok; } bool HTTPProtocol::cacheFileReadTextHeader2() { QFile *&file = m_request.cacheTag.file; Q_ASSERT(file); Q_ASSERT(file->openMode() == QIODevice::ReadOnly); bool ok = true; QByteArray readBuf; #ifndef NDEBUG // we assume that the URL and etag have already been read qint64 oldPos = file->pos(); file->seek(BinaryCacheFileHeader::size); ok = ok && readLineChecked(file, &readBuf); ok = ok && readLineChecked(file, &readBuf); Q_ASSERT(file->pos() == oldPos); #endif ok = ok && readLineChecked(file, &readBuf); m_mimeType = toQString(readBuf); m_responseHeaders.clear(); // read as long as no error and no empty line found while (true) { ok = ok && readLineChecked(file, &readBuf); if (ok && !readBuf.isEmpty()) { m_responseHeaders.append(toQString(readBuf)); } else { break; } } return ok; // it may still be false ;) } static QString filenameFromUrl(const QUrl &url) { QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(storableUrl(url).toEncoded()); return toQString(hash.result().toHex()); } QString HTTPProtocol::cacheFilePathFromUrl(const QUrl &url) const { QString filePath = m_strCacheDir; if (!filePath.endsWith(QLatin1Char('/'))) { filePath.append(QLatin1Char('/')); } filePath.append(filenameFromUrl(url)); return filePath; } bool HTTPProtocol::cacheFileOpenRead() { qCDebug(KIO_HTTP); QString filename = cacheFilePathFromUrl(m_request.url); QFile *&file = m_request.cacheTag.file; if (file) { qCDebug(KIO_HTTP) << "File unexpectedly open; old file is" << file->fileName() << "new name is" << filename; Q_ASSERT(file->fileName() == filename); } Q_ASSERT(!file); file = new QFile(filename); if (file->open(QIODevice::ReadOnly)) { QByteArray header = file->read(BinaryCacheFileHeader::size); if (!m_request.cacheTag.deserialize(header)) { qCDebug(KIO_HTTP) << "Cache file header is invalid."; file->close(); } } if (file->isOpen() && !cacheFileReadTextHeader1(m_request.url)) { file->close(); } if (!file->isOpen()) { cacheFileClose(); return false; } return true; } bool HTTPProtocol::cacheFileOpenWrite() { qCDebug(KIO_HTTP); QString filename = cacheFilePathFromUrl(m_request.url); // if we open a cache file for writing while we have a file open for reading we must have // found out that the old cached content is obsolete, so delete the file. QFile *&file = m_request.cacheTag.file; if (file) { // ensure that the file is in a known state - either open for reading or null Q_ASSERT(!qobject_cast(file)); Q_ASSERT((file->openMode() & QIODevice::WriteOnly) == 0); Q_ASSERT(file->fileName() == filename); qCDebug(KIO_HTTP) << "deleting expired cache entry and recreating."; file->remove(); delete file; file = nullptr; } // note that QTemporaryFile will automatically append random chars to filename file = new QTemporaryFile(filename); file->open(QIODevice::WriteOnly); // if we have started a new file we have not initialized some variables from disk data. m_request.cacheTag.fileUseCount = 0; // the file has not been *read* yet m_request.cacheTag.bytesCached = 0; if ((file->openMode() & QIODevice::WriteOnly) == 0) { qCDebug(KIO_HTTP) << "Could not open file for writing: QTemporaryFile(" << filename << ")" << "due to error" << file->error(); cacheFileClose(); return false; } return true; } static QByteArray makeCacheCleanerCommand(const HTTPProtocol::CacheTag &cacheTag, CacheCleanerCommandCode cmd) { QByteArray ret = cacheTag.serialize(); QDataStream stream(&ret, QIODevice::ReadWrite); stream.setVersion(QDataStream::Qt_4_5); stream.skipRawData(BinaryCacheFileHeader::size); // append the command code stream << quint32(cmd); // append the filename QString fileName = cacheTag.file->fileName(); int basenameStart = fileName.lastIndexOf(QLatin1Char('/')) + 1; const QByteArray baseName = fileName.midRef(basenameStart, s_hashedUrlNibbles).toLatin1(); stream.writeRawData(baseName.constData(), baseName.size()); Q_ASSERT(ret.size() == BinaryCacheFileHeader::size + sizeof(quint32) + s_hashedUrlNibbles); return ret; } //### not yet 100% sure when and when not to call this void HTTPProtocol::cacheFileClose() { qCDebug(KIO_HTTP); QFile *&file = m_request.cacheTag.file; if (!file) { return; } m_request.cacheTag.ioMode = NoCache; QByteArray ccCommand; QTemporaryFile *tempFile = qobject_cast(file); if (file->openMode() & QIODevice::WriteOnly) { Q_ASSERT(tempFile); if (m_request.cacheTag.bytesCached && !m_kioError) { QByteArray header = m_request.cacheTag.serialize(); tempFile->seek(0); tempFile->write(header); ccCommand = makeCacheCleanerCommand(m_request.cacheTag, CreateFileNotificationCommand); QString oldName = tempFile->fileName(); QString newName = oldName; int basenameStart = newName.lastIndexOf(QLatin1Char('/')) + 1; // remove the randomized name part added by QTemporaryFile newName.chop(newName.length() - basenameStart - s_hashedUrlNibbles); qCDebug(KIO_HTTP) << "Renaming temporary file" << oldName << "to" << newName; // on windows open files can't be renamed tempFile->setAutoRemove(false); delete tempFile; file = nullptr; if (!QFile::rename(oldName, newName)) { // ### currently this hides a minor bug when force-reloading a resource. We // should not even open a new file for writing in that case. qCDebug(KIO_HTTP) << "Renaming temporary file failed, deleting it instead."; QFile::remove(oldName); ccCommand.clear(); // we have nothing of value to tell the cache cleaner } } else { // oh, we've never written payload data to the cache file. // the temporary file is closed and removed and no proper cache entry is created. } } else if (file->openMode() == QIODevice::ReadOnly) { Q_ASSERT(!tempFile); ccCommand = makeCacheCleanerCommand(m_request.cacheTag, UpdateFileCommand); } delete file; file = nullptr; if (!ccCommand.isEmpty()) { sendCacheCleanerCommand(ccCommand); } } void HTTPProtocol::sendCacheCleanerCommand(const QByteArray &command) { qCDebug(KIO_HTTP); if (!qEnvironmentVariableIsEmpty("KIO_DISABLE_CACHE_CLEANER")) // for autotests return; Q_ASSERT(command.size() == BinaryCacheFileHeader::size + s_hashedUrlNibbles + sizeof(quint32)); if (m_cacheCleanerConnection.state() != QLocalSocket::ConnectedState) { QString socketFileName = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QLatin1Char('/') + QLatin1String("kio_http_cache_cleaner"); m_cacheCleanerConnection.connectToServer(socketFileName, QIODevice::WriteOnly); if (m_cacheCleanerConnection.state() == QLocalSocket::UnconnectedState) { // An error happened. // Most likely the cache cleaner is not running, let's start it. // search paths const QStringList searchPaths = QStringList() << QCoreApplication::applicationDirPath() // then look where our application binary is located << QLibraryInfo::location(QLibraryInfo::LibraryExecutablesPath) // look where libexec path is (can be set in qt.conf) << QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5); // look at our installation location const QString exe = QStandardPaths::findExecutable(QStringLiteral("kio_http_cache_cleaner"), searchPaths); if (exe.isEmpty()) { qCWarning(KIO_HTTP) << "kio_http_cache_cleaner not found in" << searchPaths; return; } qCDebug(KIO_HTTP) << "starting" << exe; QProcess::startDetached(exe, QStringList()); for (int i = 0 ; i < 30 && m_cacheCleanerConnection.state() == QLocalSocket::UnconnectedState; ++i) { // Server is not listening yet; let's hope it does so under 3 seconds QThread::msleep(100); m_cacheCleanerConnection.connectToServer(socketFileName, QIODevice::WriteOnly); if (m_cacheCleanerConnection.state() != QLocalSocket::UnconnectedState) { break; // connecting or connected, sounds good } } } if (!m_cacheCleanerConnection.waitForConnected(1500)) { // updating the stats is not vital, so we just give up. qCDebug(KIO_HTTP) << "Could not connect to cache cleaner, not updating stats of this cache file."; return; } qCDebug(KIO_HTTP) << "Successfully connected to cache cleaner."; } Q_ASSERT(m_cacheCleanerConnection.state() == QLocalSocket::ConnectedState); m_cacheCleanerConnection.write(command); m_cacheCleanerConnection.flush(); } QByteArray HTTPProtocol::cacheFileReadPayload(int maxLength) { Q_ASSERT(m_request.cacheTag.file); Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache); Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly); QByteArray ret = m_request.cacheTag.file->read(maxLength); if (ret.isEmpty()) { cacheFileClose(); } return ret; } void HTTPProtocol::cacheFileWritePayload(const QByteArray &d) { if (!m_request.cacheTag.file) { return; } // If the file being downloaded is so big that it exceeds the max cache size, // do not cache it! See BR# 244215. NOTE: this can be improved upon in the // future... if (m_iSize >= KIO::filesize_t(m_maxCacheSize * 1024)) { qCDebug(KIO_HTTP) << "Caching disabled because content size is too big."; cacheFileClose(); return; } Q_ASSERT(m_request.cacheTag.ioMode == WriteToCache); Q_ASSERT(m_request.cacheTag.file->openMode() & QIODevice::WriteOnly); if (d.isEmpty()) { cacheFileClose(); } //TODO: abort if file grows too big! // write the variable length text header as soon as we start writing to the file if (!m_request.cacheTag.bytesCached) { cacheFileWriteTextHeader(); } m_request.cacheTag.bytesCached += d.size(); m_request.cacheTag.file->write(d); } void HTTPProtocol::cachePostData(const QByteArray &data) { if (!m_POSTbuf) { m_POSTbuf = createPostBufferDeviceFor(qMax(m_iPostDataSize, static_cast(data.size()))); if (!m_POSTbuf) { return; } } m_POSTbuf->write(data.constData(), data.size()); } void HTTPProtocol::clearPostDataBuffer() { if (!m_POSTbuf) { return; } delete m_POSTbuf; m_POSTbuf = nullptr; } bool HTTPProtocol::retrieveAllData() { if (!m_POSTbuf) { m_POSTbuf = createPostBufferDeviceFor(s_MaxInMemPostBufSize + 1); } if (!m_POSTbuf) { error(ERR_OUT_OF_MEMORY, m_request.url.host()); return false; } while (true) { dataReq(); QByteArray buffer; const int bytesRead = readData(buffer); if (bytesRead < 0) { error(ERR_ABORTED, m_request.url.host()); return false; } if (bytesRead == 0) { break; } m_POSTbuf->write(buffer.constData(), buffer.size()); } return true; } // The above code should be kept in sync // with the code in http_cache_cleaner.cpp // !END SYNC! //************************** AUTHENTICATION CODE ********************/ QString HTTPProtocol::authenticationHeader() { QByteArray ret; // If the internal meta-data "cached-www-auth" is set, then check for cached // authentication data and preemptively send the authentication header if a // matching one is found. if (!m_wwwAuth && config()->readEntry("cached-www-auth", false)) { KIO::AuthInfo authinfo; authinfo.url = m_request.url; authinfo.realmValue = config()->readEntry("www-auth-realm", QString()); // If no realm metadata, then make sure path matching is turned on. authinfo.verifyPath = (authinfo.realmValue.isEmpty()); const bool useCachedAuth = (m_request.responseCode == 401 || !config()->readEntry("no-preemptive-auth-reuse", false)); if (useCachedAuth && checkCachedAuthentication(authinfo)) { const QByteArray cachedChallenge = config()->readEntry("www-auth-challenge", QByteArray()); if (!cachedChallenge.isEmpty()) { m_wwwAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config()); if (m_wwwAuth) { qCDebug(KIO_HTTP) << "creating www authentication header from cached info"; m_wwwAuth->setChallenge(cachedChallenge, m_request.url, m_request.sentMethodString); m_wwwAuth->generateResponse(authinfo.username, authinfo.password); } } } } // If the internal meta-data "cached-proxy-auth" is set, then check for cached // authentication data and preemptively send the authentication header if a // matching one is found. if (!m_proxyAuth && config()->readEntry("cached-proxy-auth", false)) { KIO::AuthInfo authinfo; authinfo.url = m_request.proxyUrl; authinfo.realmValue = config()->readEntry("proxy-auth-realm", QString()); // If no realm metadata, then make sure path matching is turned on. authinfo.verifyPath = (authinfo.realmValue.isEmpty()); if (checkCachedAuthentication(authinfo)) { const QByteArray cachedChallenge = config()->readEntry("proxy-auth-challenge", QByteArray()); if (!cachedChallenge.isEmpty()) { m_proxyAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config()); if (m_proxyAuth) { qCDebug(KIO_HTTP) << "creating proxy authentication header from cached info"; m_proxyAuth->setChallenge(cachedChallenge, m_request.proxyUrl, m_request.sentMethodString); m_proxyAuth->generateResponse(authinfo.username, authinfo.password); } } } } // the authentication classes don't know if they are for proxy or webserver authentication... if (m_wwwAuth && !m_wwwAuth->isError()) { ret += "Authorization: " + m_wwwAuth->headerFragment(); } if (m_proxyAuth && !m_proxyAuth->isError()) { ret += "Proxy-Authorization: " + m_proxyAuth->headerFragment(); } return toQString(ret); // ## encoding ok? } static QString protocolForProxyType(QNetworkProxy::ProxyType type) { switch (type) { case QNetworkProxy::DefaultProxy: break; case QNetworkProxy::Socks5Proxy: return QStringLiteral("socks"); case QNetworkProxy::NoProxy: break; case QNetworkProxy::HttpProxy: case QNetworkProxy::HttpCachingProxy: case QNetworkProxy::FtpCachingProxy: break; } return QStringLiteral("http"); } void HTTPProtocol::proxyAuthenticationForSocket(const QNetworkProxy &proxy, QAuthenticator *authenticator) { qCDebug(KIO_HTTP) << "realm:" << authenticator->realm() << "user:" << authenticator->user(); // Set the proxy URL... m_request.proxyUrl.setScheme(protocolForProxyType(proxy.type())); m_request.proxyUrl.setUserName(proxy.user()); m_request.proxyUrl.setHost(proxy.hostName()); m_request.proxyUrl.setPort(proxy.port()); AuthInfo info; info.url = m_request.proxyUrl; info.realmValue = authenticator->realm(); info.username = authenticator->user(); info.verifyPath = info.realmValue.isEmpty(); const bool haveCachedCredentials = checkCachedAuthentication(info); const bool retryAuth = (m_socketProxyAuth != nullptr); // if m_socketProxyAuth is a valid pointer then authentication has been attempted before, // and it was not successful. see below and saveProxyAuthenticationForSocket(). if (!haveCachedCredentials || retryAuth) { // Save authentication info if the connection succeeds. We need to disconnect // this after saving the auth data (or an error) so we won't save garbage afterwards! connect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); //### fillPromptInfo(&info); info.prompt = i18n("You need to supply a username and a password for " "the proxy server listed below before you are allowed " "to access any sites."); info.keepPassword = true; info.commentLabel = i18n("Proxy:"); info.comment = i18n("%1 at %2", info.realmValue.toHtmlEscaped(), m_request.proxyUrl.host()); const QString errMsg((retryAuth ? i18n("Proxy Authentication Failed.") : QString())); const int errorCode = openPasswordDialogV2(info, errMsg); if (errorCode) { qCDebug(KIO_HTTP) << "proxy auth cancelled by user, or communication error"; error(errorCode, QString()); delete m_proxyAuth; m_proxyAuth = nullptr; return; } } authenticator->setUser(info.username); authenticator->setPassword(info.password); authenticator->setOption(QStringLiteral("keepalive"), info.keepPassword); if (m_socketProxyAuth) { *m_socketProxyAuth = *authenticator; } else { m_socketProxyAuth = new QAuthenticator(*authenticator); } if (!m_request.proxyUrl.userName().isEmpty()) { m_request.proxyUrl.setUserName(info.username); } } void HTTPProtocol::saveProxyAuthenticationForSocket() { qCDebug(KIO_HTTP) << "Saving authenticator"; disconnect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); Q_ASSERT(m_socketProxyAuth); if (m_socketProxyAuth) { qCDebug(KIO_HTTP) << "realm:" << m_socketProxyAuth->realm() << "user:" << m_socketProxyAuth->user(); KIO::AuthInfo a; a.verifyPath = true; a.url = m_request.proxyUrl; a.realmValue = m_socketProxyAuth->realm(); a.username = m_socketProxyAuth->user(); a.password = m_socketProxyAuth->password(); a.keepPassword = m_socketProxyAuth->option(QStringLiteral("keepalive")).toBool(); cacheAuthentication(a); } delete m_socketProxyAuth; m_socketProxyAuth = nullptr; } void HTTPProtocol::saveAuthenticationData() { KIO::AuthInfo authinfo; bool alreadyCached = false; KAbstractHttpAuthentication *auth = nullptr; switch (m_request.prevResponseCode) { case 401: auth = m_wwwAuth; alreadyCached = config()->readEntry("cached-www-auth", false); break; case 407: auth = m_proxyAuth; alreadyCached = config()->readEntry("cached-proxy-auth", false); break; default: Q_ASSERT(false); // should never happen! } // Prevent recaching of the same credentials over and over again. if (auth && (!auth->realm().isEmpty() || !alreadyCached)) { auth->fillKioAuthInfo(&authinfo); if (auth == m_wwwAuth) { setMetaData(QStringLiteral("{internal~currenthost}cached-www-auth"), QStringLiteral("true")); if (!authinfo.realmValue.isEmpty()) { setMetaData(QStringLiteral("{internal~currenthost}www-auth-realm"), authinfo.realmValue); } if (!authinfo.digestInfo.isEmpty()) { setMetaData(QStringLiteral("{internal~currenthost}www-auth-challenge"), authinfo.digestInfo); } } else { setMetaData(QStringLiteral("{internal~allhosts}cached-proxy-auth"), QStringLiteral("true")); if (!authinfo.realmValue.isEmpty()) { setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-realm"), authinfo.realmValue); } if (!authinfo.digestInfo.isEmpty()) { setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-challenge"), authinfo.digestInfo); } } qCDebug(KIO_HTTP) << "Cache authentication info ?" << authinfo.keepPassword; if (authinfo.keepPassword) { cacheAuthentication(authinfo); qCDebug(KIO_HTTP) << "Cached authentication for" << m_request.url; } } // Update our server connection state which includes www and proxy username and password. m_server.updateCredentials(m_request); } bool HTTPProtocol::handleAuthenticationHeader(const HeaderTokenizer *tokenizer) { KIO::AuthInfo authinfo; QList authTokens; KAbstractHttpAuthentication **auth; QList *blacklistedAuthTokens; TriedCredentials *triedCredentials; if (m_request.responseCode == 401) { auth = &m_wwwAuth; blacklistedAuthTokens = &m_blacklistedWwwAuthMethods; triedCredentials = &m_triedWwwCredentials; authTokens = tokenizer->iterator("www-authenticate").all(); authinfo.url = m_request.url; authinfo.username = m_server.url.userName(); authinfo.prompt = i18n("You need to supply a username and a " "password to access this site."); authinfo.commentLabel = i18n("Site:"); } else { // make sure that the 407 header hasn't escaped a lower layer when it shouldn't. // this may break proxy chains which were never tested anyway, and AFAIK they are // rare to nonexistent in the wild. Q_ASSERT(QNetworkProxy::applicationProxy().type() == QNetworkProxy::NoProxy); auth = &m_proxyAuth; blacklistedAuthTokens = &m_blacklistedProxyAuthMethods; triedCredentials = &m_triedProxyCredentials; authTokens = tokenizer->iterator("proxy-authenticate").all(); authinfo.url = m_request.proxyUrl; authinfo.username = m_request.proxyUrl.userName(); authinfo.prompt = i18n("You need to supply a username and a password for " "the proxy server listed below before you are allowed " "to access any sites."); authinfo.commentLabel = i18n("Proxy:"); } bool authRequiresAnotherRoundtrip = false; // Workaround brain dead server responses that violate the spec and // incorrectly return a 401/407 without the required WWW/Proxy-Authenticate // header fields. See bug 215736... if (!authTokens.isEmpty()) { QString errorMsg; authRequiresAnotherRoundtrip = true; if (m_request.responseCode == m_request.prevResponseCode && *auth) { // Authentication attempt failed. Retry... if ((*auth)->wasFinalStage()) { errorMsg = (m_request.responseCode == 401 ? i18n("Authentication Failed.") : i18n("Proxy Authentication Failed.")); // The authentication failed in its final stage. If the chosen method didn't use a password or // if it failed with both the supplied and prompted password then blacklist this method and try // again with another one if possible. if (!(*auth)->needCredentials() || *triedCredentials > JobCredentials) { QByteArray scheme((*auth)->scheme().trimmed()); qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; blacklistedAuthTokens->append(scheme); } delete *auth; *auth = nullptr; } else { // Create authentication header // WORKAROUND: The following piece of code prevents brain dead IIS // servers that send back multiple "WWW-Authenticate" headers from // screwing up our authentication logic during the challenge // phase (Type 2) of NTLM authentication. QMutableListIterator it(authTokens); const QByteArray authScheme((*auth)->scheme().trimmed()); while (it.hasNext()) { if (qstrnicmp(authScheme.constData(), it.next().constData(), authScheme.length()) != 0) { it.remove(); } } } } QList::iterator it = authTokens.begin(); while (it != authTokens.end()) { QByteArray scheme = *it; // Separate the method name from any additional parameters (for ex. nonce or realm). int index = it->indexOf(' '); if (index > 0) { scheme.truncate(index); } if (blacklistedAuthTokens->contains(scheme)) { it = authTokens.erase(it); } else { it++; } } try_next_auth_scheme: QByteArray bestOffer = KAbstractHttpAuthentication::bestOffer(authTokens); if (*auth) { const QByteArray authScheme((*auth)->scheme().trimmed()); if (qstrnicmp(authScheme.constData(), bestOffer.constData(), authScheme.length()) != 0) { // huh, the strongest authentication scheme offered has changed. delete *auth; *auth = nullptr; } } if (!(*auth)) { *auth = KAbstractHttpAuthentication::newAuth(bestOffer, config()); } if (*auth) { qCDebug(KIO_HTTP) << "Trying authentication scheme:" << (*auth)->scheme(); // remove trailing space from the method string, or digest auth will fail (*auth)->setChallenge(bestOffer, authinfo.url, m_request.sentMethodString); QString username, password; bool generateAuthHeader = true; if ((*auth)->needCredentials()) { // use credentials supplied by the application if available if (!m_request.url.userName().isEmpty() && !m_request.url.password().isEmpty() && *triedCredentials == NoCredentials) { username = m_request.url.userName(); password = m_request.url.password(); // don't try this password any more *triedCredentials = JobCredentials; } else { // try to get credentials from kpasswdserver's cache, then try asking the user. authinfo.verifyPath = false; // we have realm, no path based checking please! authinfo.realmValue = (*auth)->realm(); if (authinfo.realmValue.isEmpty() && !(*auth)->supportsPathMatching()) { authinfo.realmValue = QLatin1String((*auth)->scheme()); } // Save the current authinfo url because it can be modified by the call to // checkCachedAuthentication. That way we can restore it if the call // modified it. const QUrl reqUrl = authinfo.url; if (!errorMsg.isEmpty() || !checkCachedAuthentication(authinfo)) { // Reset url to the saved url... authinfo.url = reqUrl; authinfo.keepPassword = true; authinfo.comment = i18n("%1 at %2", authinfo.realmValue.toHtmlEscaped(), authinfo.url.host()); const int errorCode = openPasswordDialogV2(authinfo, errorMsg); if (errorCode) { generateAuthHeader = false; authRequiresAnotherRoundtrip = false; if (!sendErrorPageNotification()) { error(ERR_ACCESS_DENIED, reqUrl.host()); } qCDebug(KIO_HTTP) << "looks like the user canceled the authentication dialog"; delete *auth; *auth = nullptr; } *triedCredentials = UserInputCredentials; } else { *triedCredentials = CachedCredentials; } username = authinfo.username; password = authinfo.password; } } if (generateAuthHeader) { (*auth)->generateResponse(username, password); (*auth)->setCachePasswordEnabled(authinfo.keepPassword); qCDebug(KIO_HTTP) << "isError=" << (*auth)->isError() << "needCredentials=" << (*auth)->needCredentials() << "forceKeepAlive=" << (*auth)->forceKeepAlive() << "forceDisconnect=" << (*auth)->forceDisconnect(); if ((*auth)->isError()) { QByteArray scheme((*auth)->scheme().trimmed()); qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; authTokens.removeOne(scheme); blacklistedAuthTokens->append(scheme); if (!authTokens.isEmpty()) { goto try_next_auth_scheme; } else { if (!sendErrorPageNotification()) { error(ERR_UNSUPPORTED_ACTION, i18n("Authorization failed.")); } authRequiresAnotherRoundtrip = false; } //### return false; ? } else if ((*auth)->forceKeepAlive()) { //### think this through for proxied / not proxied m_request.isKeepAlive = true; } else if ((*auth)->forceDisconnect()) { //### think this through for proxied / not proxied m_request.isKeepAlive = false; httpCloseConnection(); } } } else { authRequiresAnotherRoundtrip = false; if (!sendErrorPageNotification()) { error(ERR_UNSUPPORTED_ACTION, i18n("Unknown Authorization method.")); } } } return authRequiresAnotherRoundtrip; } void HTTPProtocol::copyPut(const QUrl& src, const QUrl& dest, JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; if (!maybeSetRequestUrl(dest)) { return; } resetSessionSettings(); if (!(flags & KIO::Overwrite)) { // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // Checks if the destination exists and return an error if it does. if (!davStatDestination()) { return; } } m_POSTbuf = new QFile (src.toLocalFile()); if (!m_POSTbuf->open(QFile::ReadOnly)) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, QString()); return; } m_request.method = HTTP_PUT; m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(); } bool HTTPProtocol::davStatDestination() { const QByteArray request ("" "" "" "" "" "" ""); davSetRequest(request); // WebDAV Stat or List... m_request.method = DAV_PROPFIND; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_request.davData.depth = 0; proceedUntilResponseContent(true); if (!m_request.isKeepAlive) { httpCloseConnection(); // close connection if server requested it. m_request.isKeepAlive = true; // reset the keep alive flag. } if (m_request.responseCode == 207) { error(ERR_FILE_ALREADY_EXIST, QString()); return false; } // force re-authentication... delete m_wwwAuth; m_wwwAuth = nullptr; return true; } void HTTPProtocol::fileSystemFreeSpace(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); davStatList(url); } void HTTPProtocol::virtual_hook(int id, void *data) { switch(id) { case SlaveBase::GetFileSystemFreeSpace: { QUrl *url = static_cast(data); fileSystemFreeSpace(*url); } break; default: SlaveBase::virtual_hook(id, data); } } // needed for JSON file embedding #include "http.moc" diff --git a/src/ioslaves/http/http_cache_cleaner.cpp b/src/ioslaves/http/http_cache_cleaner.cpp index f72363c9..c5475815 100644 --- a/src/ioslaves/http/http_cache_cleaner.cpp +++ b/src/ioslaves/http/http_cache_cleaner.cpp @@ -1,850 +1,850 @@ /* This file is part of KDE Copyright (C) 1999-2000 Waldo Bastian (bastian@kde.org) Copyright (C) 2009 Andreas Hartmetz (ahartmetz@gmail.com) 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 HTTP Cache cleanup tool #include #include #include #include #include -#include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include QDateTime g_currentDate; int g_maxCacheAge; qint64 g_maxCacheSize; static const char appFullName[] = "org.kio5.kio_http_cache_cleaner"; static const char appName[] = "kio_http_cache_cleaner"; // !START OF SYNC! // Keep the following in sync with the cache code in http.cpp 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_hashedUrlBytes = s_hashedUrlBits / 8; static const char version[] = "A\n"; // never instantiated, on-disk / wire format only struct SerializedCacheFileInfo { // from http.cpp quint8 version[2]; quint8 compression; // for now fixed to 0 quint8 reserved; // for now; also alignment static const int useCountOffset = 4; qint32 useCount; qint64 servedDate; qint64 lastModifiedDate; qint64 expireDate; qint32 bytesCached; static const int size = 36; QString url; QString etag; QString mimeType; QStringList responseHeaders; // including status response like "HTTP 200 OK" }; struct MiniCacheFileInfo { // data from cache entry file, or from scoreboard file qint32 useCount; // from filesystem QDateTime lastUsedDate; qint64 sizeOnDisk; // we want to delete the least "useful" files and we'll have to sort a list for that... bool operator<(const MiniCacheFileInfo &other) const; void debugPrint() const { // qDebug() << "useCount:" << useCount // << "\nlastUsedDate:" << lastUsedDate.toString(Qt::ISODate) // << "\nsizeOnDisk:" << sizeOnDisk << '\n'; } }; struct CacheFileInfo : MiniCacheFileInfo { quint8 version[2]; quint8 compression; // for now fixed to 0 quint8 reserved; // for now; also alignment QDateTime servedDate; QDateTime lastModifiedDate; QDateTime expireDate; qint32 bytesCached; QString baseName; QString url; QString etag; QString mimeType; QStringList responseHeaders; // including status response like "HTTP 200 OK" void prettyPrint() const { QTextStream out(stdout, QIODevice::WriteOnly); out << "File " << baseName << " version " << version[0] << version[1]; out << "\n cached bytes " << bytesCached << " useCount " << useCount; out << "\n servedDate " << servedDate.toString(Qt::ISODate); out << "\n lastModifiedDate " << lastModifiedDate.toString(Qt::ISODate); out << "\n expireDate " << expireDate.toString(Qt::ISODate); out << "\n entity tag " << etag; out << "\n encoded URL " << url; out << "\n mimetype " << mimeType; out << "\nResponse headers follow...\n"; Q_FOREACH (const QString &h, responseHeaders) { out << h << '\n'; } } }; bool MiniCacheFileInfo::operator<(const MiniCacheFileInfo &other) const { const int thisUseful = useCount / qMax(lastUsedDate.secsTo(g_currentDate), qint64(1)); const int otherUseful = other.useCount / qMax(other.lastUsedDate.secsTo(g_currentDate), qint64(1)); return thisUseful < otherUseful; } bool CacheFileInfoPtrLessThan(const CacheFileInfo *cf1, const CacheFileInfo *cf2) { return *cf1 < *cf2; } enum OperationMode { CleanCache = 0, DeleteCache, FileInfo }; static bool readBinaryHeader(const QByteArray &d, CacheFileInfo *fi) { if (d.size() < SerializedCacheFileInfo::size) { // qDebug() << "readBinaryHeader(): file too small?"; return false; } QDataStream stream(d); stream.setVersion(QDataStream::Qt_4_5); stream >> fi->version[0]; stream >> fi->version[1]; if (fi->version[0] != version[0] || fi->version[1] != version[1]) { // qDebug() << "readBinaryHeader(): wrong magic bytes"; return false; } stream >> fi->compression; stream >> fi->reserved; stream >> fi->useCount; SerializedCacheFileInfo serialized; stream >> serialized.servedDate; fi->servedDate.setSecsSinceEpoch(serialized.servedDate); stream >> serialized.lastModifiedDate; fi->lastModifiedDate.setSecsSinceEpoch(serialized.lastModifiedDate); stream >> serialized.expireDate; fi->expireDate.setSecsSinceEpoch(serialized.expireDate); stream >> fi->bytesCached; return true; } static QString filenameFromUrl(const QByteArray &url) { QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(url); return QString::fromLatin1(hash.result().toHex()); } static QString cacheDir() { return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_http"); } static QString filePath(const QString &baseName) { QString cacheDirName = cacheDir(); if (!cacheDirName.endsWith(QLatin1Char('/'))) { cacheDirName.append(QLatin1Char('/')); } return cacheDirName + baseName; } static bool readLineChecked(QIODevice *dev, QByteArray *line) { *line = dev->readLine(8192); // 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; } static bool readTextHeader(QFile *file, CacheFileInfo *fi, OperationMode mode) { bool ok = true; QByteArray readBuf; ok = ok && readLineChecked(file, &readBuf); fi->url = QString::fromLatin1(readBuf); if (filenameFromUrl(readBuf) != QFileInfo(*file).baseName()) { // qDebug() << "You have witnessed a very improbable hash collision!"; return false; } // only read the necessary info for cache cleaning. Saves time and (more importantly) memory. if (mode != FileInfo) { return true; } ok = ok && readLineChecked(file, &readBuf); fi->etag = QString::fromLatin1(readBuf); ok = ok && readLineChecked(file, &readBuf); fi->mimeType = QString::fromLatin1(readBuf); // read as long as no error and no empty line found while (true) { ok = ok && readLineChecked(file, &readBuf); if (ok && !readBuf.isEmpty()) { fi->responseHeaders.append(QString::fromLatin1(readBuf)); } else { break; } } return ok; // it may still be false ;) } // TODO common include file with http.cpp? enum CacheCleanerCommand { InvalidCommand = 0, CreateFileNotificationCommand, UpdateFileCommand }; static bool readCacheFile(const QString &baseName, CacheFileInfo *fi, OperationMode mode) { QFile file(filePath(baseName)); if (!file.open(QIODevice::ReadOnly)) { return false; } fi->baseName = baseName; QByteArray header = file.read(SerializedCacheFileInfo::size); // do *not* modify/delete the file if we're in file info mode. if (!(readBinaryHeader(header, fi) && readTextHeader(&file, fi, mode)) && mode != FileInfo) { // qDebug() << "read(Text|Binary)Header() returned false, deleting file" << baseName; file.remove(); return false; } // get meta-information from the filesystem QFileInfo fileInfo(file); fi->lastUsedDate = fileInfo.lastModified(); fi->sizeOnDisk = fileInfo.size(); return true; } class Scoreboard; class CacheIndex { public: explicit CacheIndex(const QString &baseName) { QByteArray ba = baseName.toLatin1(); const int sz = ba.size(); const char *input = ba.constData(); Q_ASSERT(sz == s_hashedUrlNibbles); int translated = 0; for (int i = 0; i < sz; i++) { int c = input[i]; if (c >= '0' && c <= '9') { translated |= c - '0'; } else if (c >= 'a' && c <= 'f') { translated |= c - 'a' + 10; } else { Q_ASSERT(false); } if (i & 1) { // odd index m_index[i >> 1] = translated; translated = 0; } else { translated = translated << 4; } } computeHash(); } bool operator==(const CacheIndex &other) const { const bool isEqual = memcmp(m_index, other.m_index, s_hashedUrlBytes) == 0; if (isEqual) { Q_ASSERT(m_hash == other.m_hash); } return isEqual; } private: explicit CacheIndex(const QByteArray &index) { Q_ASSERT(index.length() >= s_hashedUrlBytes); memcpy(m_index, index.constData(), s_hashedUrlBytes); computeHash(); } void computeHash() { uint hash = 0; const int ints = s_hashedUrlBytes / sizeof(uint); for (int i = 0; i < ints; i++) { hash ^= reinterpret_cast(&m_index[0])[i]; } if (const int bytesLeft = s_hashedUrlBytes % sizeof(uint)) { // dead code until a new url hash algorithm or architecture with sizeof(uint) != 4 appears. // we have the luxury of ignoring endianness because the hash is never written to disk. // just merge the bits into the hash in some way. const int offset = ints * sizeof(uint); for (int i = 0; i < bytesLeft; i++) { hash ^= static_cast(m_index[offset + i]) << (i * 8); } } m_hash = hash; } friend uint qHash(const CacheIndex &); friend class Scoreboard; quint8 m_index[s_hashedUrlBytes]; // packed binary version of the hexadecimal name uint m_hash; }; uint qHash(const CacheIndex &ci) { return ci.m_hash; } static CacheCleanerCommand readCommand(const QByteArray &cmd, CacheFileInfo *fi) { readBinaryHeader(cmd, fi); QDataStream stream(cmd); stream.skipRawData(SerializedCacheFileInfo::size); quint32 ret; stream >> ret; QByteArray baseName; baseName.resize(s_hashedUrlNibbles); stream.readRawData(baseName.data(), s_hashedUrlNibbles); Q_ASSERT(stream.atEnd()); fi->baseName = QString::fromLatin1(baseName); Q_ASSERT(ret == CreateFileNotificationCommand || ret == UpdateFileCommand); return static_cast(ret); } // never instantiated, on-disk format only struct ScoreboardEntry { // from scoreboard file quint8 index[s_hashedUrlBytes]; static const int indexSize = s_hashedUrlBytes; qint32 useCount; // from scoreboard file, but compared with filesystem to see if scoreboard has current data qint64 lastUsedDate; qint32 sizeOnDisk; static const int size = 36; // we want to delete the least "useful" files and we'll have to sort a list for that... bool operator<(const MiniCacheFileInfo &other) const; }; class Scoreboard { public: Scoreboard() { // read in the scoreboard... QFile sboard(filePath(QStringLiteral("scoreboard"))); if (sboard.open(QIODevice::ReadOnly)) { while (true) { QByteArray baIndex = sboard.read(ScoreboardEntry::indexSize); QByteArray baRest = sboard.read(ScoreboardEntry::size - ScoreboardEntry::indexSize); if (baIndex.size() + baRest.size() != ScoreboardEntry::size) { break; } const QString entryBasename = QString::fromLatin1(baIndex.toHex()); MiniCacheFileInfo mcfi; if (readAndValidateMcfi(baRest, entryBasename, &mcfi)) { m_scoreboard.insert(CacheIndex(baIndex), mcfi); } } } } void writeOut() { // write out the scoreboard QFile sboard(filePath(QStringLiteral("scoreboard"))); if (!sboard.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return; } QDataStream stream(&sboard); QHash::ConstIterator it = m_scoreboard.constBegin(); for (; it != m_scoreboard.constEnd(); ++it) { const char *indexData = reinterpret_cast(it.key().m_index); stream.writeRawData(indexData, s_hashedUrlBytes); stream << it.value().useCount; stream << it.value().lastUsedDate.toSecsSinceEpoch(); stream << qint32(it.value().sizeOnDisk); } } bool fillInfo(const QString &baseName, MiniCacheFileInfo *mcfi) { QHash::ConstIterator it = m_scoreboard.constFind(CacheIndex(baseName)); if (it == m_scoreboard.constEnd()) { return false; } *mcfi = it.value(); return true; } qint64 runCommand(const QByteArray &cmd) { // execute the command; return number of bytes if a new file was created, zero otherwise. Q_ASSERT(cmd.size() == 80); CacheFileInfo fi; const CacheCleanerCommand ccc = readCommand(cmd, &fi); QString fileName = filePath(fi.baseName); switch (ccc) { case CreateFileNotificationCommand: // qDebug() << "CreateNotificationCommand for" << fi.baseName; if (!readBinaryHeader(cmd, &fi)) { return 0; } break; case UpdateFileCommand: { // qDebug() << "UpdateFileCommand for" << fi.baseName; QFile file(fileName); file.open(QIODevice::ReadWrite); CacheFileInfo fiFromDisk; QByteArray header = file.read(SerializedCacheFileInfo::size); if (!readBinaryHeader(header, &fiFromDisk) || fiFromDisk.bytesCached != fi.bytesCached) { return 0; } // adjust the use count, to make sure that we actually count up. (slaves read the file // asynchronously...) const quint32 newUseCount = fiFromDisk.useCount + 1; QByteArray newHeader = cmd.mid(0, SerializedCacheFileInfo::size); { QDataStream stream(&newHeader, QIODevice::WriteOnly); stream.skipRawData(SerializedCacheFileInfo::useCountOffset); stream << newUseCount; } file.seek(0); file.write(newHeader); file.close(); if (!readBinaryHeader(newHeader, &fi)) { return 0; } break; } default: // qDebug() << "received invalid command"; return 0; } QFileInfo fileInfo(fileName); fi.lastUsedDate = fileInfo.lastModified(); fi.sizeOnDisk = fileInfo.size(); fi.debugPrint(); // a CacheFileInfo is-a MiniCacheFileInfo which enables the following assignment... add(fi); // finally, return cache dir growth (only relevant if a file was actually created!) return ccc == CreateFileNotificationCommand ? fi.sizeOnDisk : 0; } void add(const CacheFileInfo &fi) { m_scoreboard[CacheIndex(fi.baseName)] = fi; } void remove(const QString &basename) { m_scoreboard.remove(CacheIndex(basename)); } // keep memory usage reasonably low - otherwise entries of nonexistent files don't hurt. void maybeRemoveStaleEntries(const QList &fiList) { // don't bother when there are a few bogus entries if (m_scoreboard.count() < fiList.count() + 100) { return; } // qDebug() << "we have too many fake/stale entries, cleaning up..."; QSet realFiles; for (CacheFileInfo *fi : fiList) { realFiles.insert(CacheIndex(fi->baseName)); } QHash::Iterator it = m_scoreboard.begin(); while (it != m_scoreboard.end()) { if (realFiles.contains(it.key())) { ++it; } else { it = m_scoreboard.erase(it); } } } private: bool readAndValidateMcfi(const QByteArray &rawData, const QString &basename, MiniCacheFileInfo *mcfi) { QDataStream stream(rawData); stream >> mcfi->useCount; // check those against filesystem qint64 lastUsedDate; stream >> lastUsedDate; mcfi->lastUsedDate.setSecsSinceEpoch(lastUsedDate); qint32 sizeOnDisk; stream >> sizeOnDisk; mcfi->sizeOnDisk = sizeOnDisk; //qDebug() << basename << "sizeOnDisk" << mcfi->sizeOnDisk; QFileInfo fileInfo(filePath(basename)); if (!fileInfo.exists()) { return false; } bool ok = true; ok = ok && fileInfo.lastModified() == mcfi->lastUsedDate; ok = ok && fileInfo.size() == mcfi->sizeOnDisk; if (!ok) { // size or last-modified date not consistent with entry file; reload useCount // note that avoiding to open the file is the whole purpose of the scoreboard - we only // open the file if we really have to. QFile entryFile(fileInfo.absoluteFilePath()); if (!entryFile.open(QIODevice::ReadOnly)) { return false; } if (entryFile.size() < SerializedCacheFileInfo::size) { return false; } QDataStream stream(&entryFile); stream.skipRawData(SerializedCacheFileInfo::useCountOffset); stream >> mcfi->useCount; mcfi->lastUsedDate = fileInfo.lastModified(); mcfi->sizeOnDisk = fileInfo.size(); ok = true; } return ok; } QHash m_scoreboard; }; // Keep the above in sync with the cache code in http.cpp // !END OF SYNC! // remove files and directories used by earlier versions of the HTTP cache. static void removeOldFiles() { const char *oldDirs = "0abcdefghijklmnopqrstuvwxyz"; const int n = strlen(oldDirs); QDir cacheRootDir(filePath(QString())); for (int i = 0; i < n; i++) { QString dirName = QString::fromLatin1(&oldDirs[i], 1); // delete files in directory... Q_FOREACH (const QString &baseName, QDir(filePath(dirName)).entryList()) { QFile::remove(filePath(dirName + QLatin1Char('/') + baseName)); } // delete the (now hopefully empty!) directory itself cacheRootDir.rmdir(dirName); } QFile::remove(filePath(QStringLiteral("cleaned"))); } class CacheCleaner { public: CacheCleaner(const QDir &cacheDir) : m_totalSizeOnDisk(0) { // qDebug(); m_fileNameList = cacheDir.entryList(QDir::Files); } // Delete some of the files that need to be deleted. Return true when done, false otherwise. // This makes interleaved cleaning / serving ioslaves possible. bool processSlice(Scoreboard *scoreboard = nullptr) { - QTime t; + QElapsedTimer t; t.start(); // phase one: gather information about cache files if (!m_fileNameList.isEmpty()) { while (t.elapsed() < 100 && !m_fileNameList.isEmpty()) { QString baseName = m_fileNameList.takeFirst(); // check if the filename is of the $s_hashedUrlNibbles letters, 0...f type if (baseName.length() < s_hashedUrlNibbles) { continue; } bool nameOk = true; for (int i = 0; i < s_hashedUrlNibbles && nameOk; i++) { QChar c = baseName[i]; nameOk = (c >= QLatin1Char('0') && c <= QLatin1Char('9')) || (c >= QLatin1Char('a') && c <= QLatin1Char('f')); } if (!nameOk) { continue; } if (baseName.length() > s_hashedUrlNibbles) { if (QFileInfo(filePath(baseName)).lastModified().secsTo(g_currentDate) > 15 * 60) { // it looks like a temporary file that hasn't been touched in > 15 minutes... QFile::remove(filePath(baseName)); } // the temporary file might still be written to, leave it alone continue; } CacheFileInfo *fi = new CacheFileInfo(); fi->baseName = baseName; bool gotInfo = false; if (scoreboard) { gotInfo = scoreboard->fillInfo(baseName, fi); } if (!gotInfo) { gotInfo = readCacheFile(baseName, fi, CleanCache); if (gotInfo && scoreboard) { scoreboard->add(*fi); } } if (gotInfo) { m_fiList.append(fi); m_totalSizeOnDisk += fi->sizeOnDisk; } else { delete fi; } } // qDebug() << "total size of cache files is" << m_totalSizeOnDisk; if (m_fileNameList.isEmpty()) { // final step of phase one std::sort(m_fiList.begin(), m_fiList.end(), CacheFileInfoPtrLessThan); } return false; } // phase two: delete files until cache is under maximum allowed size // TODO: delete files larger than allowed for a single file while (t.elapsed() < 100) { if (m_totalSizeOnDisk <= g_maxCacheSize || m_fiList.isEmpty()) { // qDebug() << "total size of cache files after cleaning is" << m_totalSizeOnDisk; if (scoreboard) { scoreboard->maybeRemoveStaleEntries(m_fiList); scoreboard->writeOut(); } qDeleteAll(m_fiList); m_fiList.clear(); return true; } CacheFileInfo *fi = m_fiList.takeFirst(); QString filename = filePath(fi->baseName); if (QFile::remove(filename)) { m_totalSizeOnDisk -= fi->sizeOnDisk; if (scoreboard) { scoreboard->remove(fi->baseName); } } delete fi; } return false; } private: QStringList m_fileNameList; QList m_fiList; qint64 m_totalSizeOnDisk; }; int main(int argc, char **argv) { QCoreApplication app(argc, argv); app.setApplicationVersion(QStringLiteral("5.0")); KLocalizedString::setApplicationDomain("kio5"); QCommandLineParser parser; parser.addVersionOption(); parser.setApplicationDescription(QCoreApplication::translate("main", "KDE HTTP cache maintenance tool")); parser.addHelpOption(); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("clear-all"), QCoreApplication::translate("main", "Empty the cache"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("file-info"), QCoreApplication::translate("main", "Display information about cache file"), QStringLiteral("filename"))); parser.process(app); OperationMode mode = CleanCache; if (parser.isSet(QStringLiteral("clear-all"))) { mode = DeleteCache; } else if (parser.isSet(QStringLiteral("file-info"))) { mode = FileInfo; } // file info mode: no scanning of directories, just output info and exit. if (mode == FileInfo) { CacheFileInfo fi; if (!readCacheFile(parser.value(QStringLiteral("file-info")), &fi, mode)) { return 1; } fi.prettyPrint(); return 0; } // make sure we're the only running instance of the cleaner service if (mode == CleanCache) { if (!QDBusConnection::sessionBus().isConnected()) { QDBusError error(QDBusConnection::sessionBus().lastError()); fprintf(stderr, "%s: Could not connect to D-Bus! (%s: %s)\n", appName, qPrintable(error.name()), qPrintable(error.message())); return 1; } if (!QDBusConnection::sessionBus().registerService(QString::fromLatin1(appFullName))) { fprintf(stderr, "%s: Already running!\n", appName); return 0; } } g_currentDate = QDateTime::currentDateTime(); g_maxCacheAge = KProtocolManager::maxCacheAge(); g_maxCacheSize = mode == DeleteCache ? -1 : KProtocolManager::maxCacheSize() * 1024; QString cacheDirName = cacheDir(); QDir().mkpath(cacheDirName); QDir cacheDir(cacheDirName); if (!cacheDir.exists()) { fprintf(stderr, "%s: '%s' does not exist.\n", appName, qPrintable(cacheDirName)); return 0; } removeOldFiles(); if (mode == DeleteCache) { - QTime t; + QElapsedTimer t; t.start(); cacheDir.refresh(); //qDebug() << "time to refresh the cacheDir QDir:" << t.elapsed(); CacheCleaner cleaner(cacheDir); while (!cleaner.processSlice()) { } QFile::remove(filePath(QStringLiteral("scoreboard"))); return 0; } QLocalServer lServer; const QString socketFileName = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QLatin1String("/kio_http_cache_cleaner"); // we need to create the file by opening the socket, otherwise it won't work QFile::remove(socketFileName); if (!lServer.listen(socketFileName)) { qWarning() << "Error listening on" << socketFileName; } QList sockets; qint64 newBytesCounter = LLONG_MAX; // force cleaner run on startup Scoreboard scoreboard; CacheCleaner *cleaner = nullptr; while (QDBusConnection::sessionBus().isConnected()) { g_currentDate = QDateTime::currentDateTime(); if (!lServer.isListening()) { return 1; } lServer.waitForNewConnection(100); while (QLocalSocket *sock = lServer.nextPendingConnection()) { sock->waitForConnected(); sockets.append(sock); } for (int i = 0; i < sockets.size(); i++) { QLocalSocket *sock = sockets[i]; if (sock->state() != QLocalSocket::ConnectedState) { if (sock->state() != QLocalSocket::UnconnectedState) { sock->waitForDisconnected(); } delete sock; sockets.removeAll(sock); i--; continue; } sock->waitForReadyRead(0); while (true) { QByteArray recv = sock->read(80); if (recv.isEmpty()) { break; } Q_ASSERT(recv.size() == 80); newBytesCounter += scoreboard.runCommand(recv); } } // interleave cleaning with serving ioslaves to reduce "garbage collection pauses" if (cleaner) { if (cleaner->processSlice(&scoreboard)) { // that was the last slice, done delete cleaner; cleaner = nullptr; } } else if (newBytesCounter > (g_maxCacheSize / 8)) { cacheDir.refresh(); cleaner = new CacheCleaner(cacheDir); newBytesCounter = 0; } } return 0; } diff --git a/src/ioslaves/http/kcookiejar/kcookiejar.cpp b/src/ioslaves/http/kcookiejar/kcookiejar.cpp index 7b60a6b6..08f500eb 100644 --- a/src/ioslaves/http/kcookiejar/kcookiejar.cpp +++ b/src/ioslaves/http/kcookiejar/kcookiejar.cpp @@ -1,1611 +1,1611 @@ /* This file is part of the KDE File Manager Copyright (C) 1998-2000 Waldo Bastian (bastian@kde.org) Copyright (C) 2000,2001 Dawit Alemayehu (adawit@kde.org) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ //---------------------------------------------------------------------------- // // KDE File Manager -- HTTP Cookies // // The cookie protocol is a mess. RFC2109 is a joke since nobody seems to // use it. Apart from that it is badly written. // We try to implement Netscape Cookies and try to behave us according to // RFC2109 as much as we can. // // We assume cookies do not contain any spaces (Netscape spec.) // According to RFC2109 this is allowed though. // #include "kcookiejar.h" #include #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(KIO_COOKIEJAR, "kf5.kio.cookiejar") // BR87227 // Waba: Should the number of cookies be limited? // I am not convinced of the need of such limit // Mozilla seems to limit to 20 cookies / domain // but it is unclear which policy it uses to expire // cookies when it exceeds that amount #undef MAX_COOKIE_LIMIT #define MAX_COOKIES_PER_HOST 25 #define READ_BUFFER_SIZE 8192 #define IP_ADDRESS_EXPRESSION "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" // Note with respect to QLatin1String( ).... // Cookies are stored as 8 bit data and passed to kio_http as Latin1 // regardless of their actual encoding. #define QL1S(x) QLatin1String(x) #define QL1C(x) QLatin1Char(x) static QString removeWeekday(const QString &value) { const int index = value.indexOf(QL1C(' ')); if (index > -1) { int pos = 0; const QStringRef weekday = value.leftRef(index); const QLocale cLocale = QLocale::c(); for (int i = 1; i < 8; ++i) { // No need to check for long names since the short names are // prefixes of the long names if (weekday.startsWith(cLocale.dayName(i, QLocale::ShortFormat), Qt::CaseInsensitive)) { pos = index + 1; break; } } if (pos > 0) { return value.mid(pos); } } return value; } static QDateTime parseDate(const QString &_value) { // Handle sites sending invalid weekday as part of the date. #298660 const QString value(removeWeekday(_value)); // Check if expiration date matches RFC dates as specified under // RFC 2616 sec 3.3.1 & RFC 6265 sec 4.1.1 QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date); if (!dt.isValid()) { static const char *const date_formats[] = { // Other formats documented in RFC 2616 sec 3.3.1 // Note: the RFC says timezone information MUST be "GMT", hence the hardcoded timezone string "MMM dd HH:mm:ss yyyy", /* ANSI C's asctime() format (#145244): Jan 01 00:00:00 1970 GMT */ "dd-MMM-yy HH:mm:ss 'GMT'", /* RFC 850 date: 06-Dec-39 00:30:42 GMT */ // Non-standard formats "MMM dd yyyy HH:mm:ss", /* A variation on ANSI C format seen @ amazon.com: Jan 01 1970 00:00:00 GMT */ "dd-MMM-yyyy HH:mm:ss 'GMT'", /* cookies.test: Y2K38 problem: 06-Dec-2039 00:30:42 GMT */ "MMM dd HH:mm:ss yyyy 'GMT'", /* cookies.test: Non-standard expiration dates: Sep 12 07:00:00 2020 GMT */ "MMM dd yyyy HH:mm:ss 'GMT'", /* cookies.test: Non-standard expiration dates: Sep 12 2020 07:00:00 GMT */ nullptr }; // Only English month names are allowed, thus use the C locale. const QLocale cLocale = QLocale::c(); for (int i = 0; date_formats[i]; ++i) { dt = cLocale.toDateTime(value, QL1S(date_formats[i])); if (dt.isValid()) { dt.setTimeSpec(Qt::UTC); break; } } } return dt.toUTC(); // Per RFC 2616 sec 3.3.1 always convert to UTC. } static qint64 toEpochSecs(const QDateTime &dt) { return (dt.toMSecsSinceEpoch() / 1000); // convert to seconds... } static qint64 epoch() { return toEpochSecs(QDateTime::currentDateTimeUtc()); } QString KCookieJar::adviceToStr(KCookieAdvice _advice) { switch (_advice) { case KCookieAccept: return QStringLiteral("Accept"); case KCookieAcceptForSession: return QStringLiteral("AcceptForSession"); case KCookieReject: return QStringLiteral("Reject"); case KCookieAsk: return QStringLiteral("Ask"); default: return QStringLiteral("Dunno"); } } KCookieAdvice KCookieJar::strToAdvice(const QString &_str) { if (_str.isEmpty()) { return KCookieDunno; } QString advice = _str.toLower(); if (advice == QL1S("accept")) { return KCookieAccept; } else if (advice == QL1S("acceptforsession")) { return KCookieAcceptForSession; } else if (advice == QL1S("reject")) { return KCookieReject; } else if (advice == QL1S("ask")) { return KCookieAsk; } return KCookieDunno; } // KHttpCookie /////////////////////////////////////////////////////////////////////////// // // Cookie constructor // KHttpCookie::KHttpCookie(const QString &_host, const QString &_domain, const QString &_path, const QString &_name, const QString &_value, qint64 _expireDate, int _protocolVersion, bool _secure, bool _httpOnly, bool _explicitPath) : mHost(_host), mDomain(_domain), mPath(_path.isEmpty() ? QString() : _path), mName(_name), mValue(_value), mExpireDate(_expireDate), mProtocolVersion(_protocolVersion), mSecure(_secure), mCrossDomain(false), mHttpOnly(_httpOnly), mExplicitPath(_explicitPath), mUserSelectedAdvice(KCookieDunno) { } // // Checks if a cookie has been expired // bool KHttpCookie::isExpired(qint64 currentDate) const { if (currentDate == -1) { currentDate = epoch(); } return (mExpireDate != 0) && (mExpireDate < currentDate); } // // Returns a string for a HTTP-header // QString KHttpCookie::cookieStr(bool useDOMFormat) const { QString result; if (useDOMFormat || (mProtocolVersion == 0)) { if (mName.isEmpty()) { result = mValue; } else { result = mName + QL1C('=') + mValue; } } else { result = mName + QL1C('=') + mValue; if (mExplicitPath) { result += QL1S("; $Path=\"") + mPath + QL1C('"'); } if (!mDomain.isEmpty()) { result += QL1S("; $Domain=\"") + mDomain + QL1C('"'); } if (!mPorts.isEmpty()) { if (mPorts.length() == 2 && mPorts.at(0) == -1) { result += QL1S("; $Port"); } else { QString portNums; for (int port : qAsConst(mPorts)) { portNums += QString::number(port) + QL1C(' '); } result += QL1S("; $Port=\"") + portNums.trimmed() + QL1C('"'); } } } return result; } // // Returns whether this cookie should be send to this location. bool KHttpCookie::match(const QString &fqdn, const QStringList &domains, const QString &path, int port) const { // Cookie domain match check if (mDomain.isEmpty()) { if (fqdn != mHost) { return false; } } else if (!domains.contains(mDomain)) { if (mDomain[0] == QL1C('.')) { return false; } // Maybe the domain needs an extra dot. const QString domain = QL1C('.') + mDomain; if (!domains.contains(domain)) if (fqdn != mDomain) { return false; } } else if (mProtocolVersion != 0 && port != -1 && !mPorts.isEmpty() && !mPorts.contains(port)) { return false; } // Cookie path match check if (mPath.isEmpty()) { return true; } // According to the netscape spec http://www.acme.com/foobar, // http://www.acme.com/foo.bar and http://www.acme.com/foo/bar // should all match http://www.acme.com/foo... // We only match http://www.acme.com/foo/bar if (path.startsWith(mPath) && ( (path.length() == mPath.length()) || // Paths are exact match mPath.endsWith(QL1C('/')) || // mPath ended with a slash (path[mPath.length()] == QL1C('/')) // A slash follows )) { return true; // Path of URL starts with cookie-path } return false; } // KCookieJar /////////////////////////////////////////////////////////////////////////// // // Constructs a new cookie jar // // One jar should be enough for all cookies. // KCookieJar::KCookieJar() { m_globalAdvice = KCookieDunno; m_configChanged = false; m_cookiesChanged = false; KConfig cfg(QStringLiteral("kf5/kcookiejar/domain_info"), KConfig::NoGlobals, QStandardPaths::GenericDataLocation); KConfigGroup group(&cfg, QString()); m_gTLDs = QSet::fromList(group.readEntry("gTLDs", QStringList())); m_twoLevelTLD = QSet::fromList(group.readEntry("twoLevelTLD", QStringList())); } // // Destructs the cookie jar // // Poor little cookies, they will all be eaten by the cookie monster! // KCookieJar::~KCookieJar() { qDeleteAll(m_cookieDomains); // Not much to do here } // cookiePtr is modified: the window ids of the existing cookie in the list are added to it static void removeDuplicateFromList(KHttpCookieList *list, KHttpCookie &cookiePtr, bool nameMatchOnly = false, bool updateWindowId = false) { QString domain1 = cookiePtr.domain(); if (domain1.isEmpty()) { domain1 = cookiePtr.host(); } QMutableListIterator cookieIterator(*list); while (cookieIterator.hasNext()) { const KHttpCookie &cookie = cookieIterator.next(); QString domain2 = cookie.domain(); if (domain2.isEmpty()) { domain2 = cookie.host(); } if (cookiePtr.name() == cookie.name() && (nameMatchOnly || (domain1 == domain2 && cookiePtr.path() == cookie.path()))) { if (updateWindowId) { Q_FOREACH (WId windowId, cookie.windowIds()) { if (windowId && (!cookiePtr.windowIds().contains(windowId))) { cookiePtr.windowIds().append(windowId); } } } cookieIterator.remove(); break; } } } // // Looks for cookies in the cookie jar which are appropriate for _url. // Returned is a string containing all appropriate cookies in a format // which can be added to a HTTP-header without any additional processing. // QString KCookieJar::findCookies(const QString &_url, bool useDOMFormat, WId windowId, KHttpCookieList *pendingCookies) { QString cookieStr, fqdn, path; QStringList domains; int port = -1; if (!parseUrl(_url, fqdn, path, &port)) { return cookieStr; } const bool secureRequest = (_url.startsWith(QL1S("https://"), Qt::CaseInsensitive) || _url.startsWith(QL1S("webdavs://"), Qt::CaseInsensitive)); if (port == -1) { port = (secureRequest ? 443 : 80); } extractDomains(fqdn, domains); KHttpCookieList allCookies; for (QStringList::ConstIterator it = domains.constBegin(), itEnd = domains.constEnd();; ++it) { KHttpCookieList *cookieList = nullptr; if (it == itEnd) { cookieList = pendingCookies; // Add pending cookies pendingCookies = nullptr; if (!cookieList) { break; } } else { if ((*it).isNull()) { cookieList = m_cookieDomains.value(QL1S("")); } else { cookieList = m_cookieDomains.value(*it); } if (!cookieList) { continue; // No cookies for this domain } } QMutableListIterator cookieIt(*cookieList); while (cookieIt.hasNext()) { KHttpCookie &cookie = cookieIt.next(); if (cookieAdvice(cookie) == KCookieReject) { continue; } if (!cookie.match(fqdn, domains, path, port)) { continue; } if (cookie.isSecure() && !secureRequest) { continue; } if (cookie.isHttpOnly() && useDOMFormat) { continue; } // Do not send expired cookies. if (cookie.isExpired()) { // NOTE: there is no need to delete the cookie here because the // cookieserver will invoke its saveCookieJar function as a result // of the state change below. This will then result in the cookie // being deleting at that point. m_cookiesChanged = true; continue; } if (windowId && (cookie.windowIds().indexOf(windowId) == -1)) { cookie.windowIds().append(windowId); } if (it == itEnd) { // Only needed when processing pending cookies removeDuplicateFromList(&allCookies, cookie); } allCookies.append(cookie); } if (it == itEnd) { break; // Finished. } } int protVersion = 0; for (const KHttpCookie &cookie : qAsConst(allCookies)) { if (cookie.protocolVersion() > protVersion) { protVersion = cookie.protocolVersion(); } } if (!allCookies.isEmpty()) { if (!useDOMFormat) { cookieStr = QStringLiteral("Cookie: "); } if (protVersion > 0) { cookieStr = cookieStr + QLatin1String("$Version=") + QString::number(protVersion) + QLatin1String("; "); } for (const KHttpCookie &cookie : qAsConst(allCookies)) { cookieStr = cookieStr + cookie.cookieStr(useDOMFormat) + QStringLiteral("; "); } cookieStr.chop(2); // Remove the trailing '; ' } return cookieStr; } // // This function parses a string like 'my_name="my_value";' and returns // 'my_name' in Name and 'my_value' in Value. // // A pointer to the end of the parsed part is returned. // This pointer points either to: // '\0' - The end of the string has reached. // ';' - Another my_name="my_value" pair follows // ',' - Another cookie follows // '\n' - Another header follows static const char *parseNameValue(const char *header, QString &Name, QString &Value, bool keepQuotes = false, bool rfcQuotes = false) { const char *s = header; // Parse 'my_name' part for (; (*s != '='); s++) { if ((*s == '\0') || (*s == ';') || (*s == '\n')) { // No '=' sign -> use string as the value, name is empty // (behavior found in Mozilla and IE) Name = QL1S(""); Value = QL1S(header); Value.truncate(s - header); Value = Value.trimmed(); return s; } } Name = QL1S(header); Name.truncate(s - header); Name = Name.trimmed(); // *s == '=' s++; // Skip any whitespace for (; (*s == ' ') || (*s == '\t'); s++) { if ((*s == '\0') || (*s == ';') || (*s == '\n')) { // End of Name Value = QLatin1String(""); return s; } } if ((rfcQuotes || !keepQuotes) && (*s == '\"')) { // Parse '"my_value"' part (quoted value) if (keepQuotes) { header = s++; } else { header = ++s; // skip " } for (; (*s != '\"'); s++) { if ((*s == '\0') || (*s == '\n')) { // End of Name Value = QL1S(header); Value.truncate(s - header); return s; } } Value = QL1S(header); // *s == '\"'; if (keepQuotes) { Value.truncate(++s - header); } else { Value.truncate(s++ - header); } // Skip any remaining garbage for (;; s++) { if ((*s == '\0') || (*s == ';') || (*s == '\n')) { break; } } } else { // Parse 'my_value' part (unquoted value) header = s; while ((*s != '\0') && (*s != ';') && (*s != '\n')) { s++; } // End of Name Value = QL1S(header); Value.truncate(s - header); Value = Value.trimmed(); } return s; } void KCookieJar::stripDomain(const QString &_fqdn, QString &_domain) const { QStringList domains; extractDomains(_fqdn, domains); if (domains.count() > 3) { _domain = domains[3]; } else if (!domains.isEmpty()) { _domain = domains[0]; } else { _domain = QL1S(""); } } QString KCookieJar::stripDomain(const KHttpCookie &cookie) const { QString domain; // We file the cookie under this domain. if (cookie.domain().isEmpty()) { stripDomain(cookie.host(), domain); } else { domain = cookie.domain(); } return domain; } bool KCookieJar::parseUrl(const QString &_url, QString &_fqdn, QString &_path, int *port) { QUrl kurl(_url); if (!kurl.isValid() || kurl.scheme().isEmpty()) { return false; } _fqdn = kurl.host().toLower(); // Cookie spoofing protection. Since there is no way a path separator, // a space or the escape encoding character is allowed in the hostname // according to RFC 2396, reject attempts to include such things there! if (_fqdn.contains(QL1C('/')) || _fqdn.contains(QL1C('%'))) { return false; // deny everything!! } // Set the port number from the protocol when one is found... if (port) { *port = kurl.port(); } _path = kurl.path(); if (_path.isEmpty()) { _path = QStringLiteral("/"); } return true; } // not static because it uses m_twoLevelTLD void KCookieJar::extractDomains(const QString &_fqdn, QStringList &_domains) const { if (_fqdn.isEmpty()) { _domains.append(QStringLiteral("localhost")); return; } // Return numeric IPv6 addresses as is... if (_fqdn[0] == QL1C('[')) { _domains.append(_fqdn); return; } // Return numeric IPv4 addresses as is... if (_fqdn[0] >= QL1C('0') && _fqdn[0] <= QL1C('9') && _fqdn.indexOf(QRegExp(QStringLiteral(IP_ADDRESS_EXPRESSION))) > -1) { _domains.append(_fqdn); return; } // Always add the FQDN at the start of the list for // hostname == cookie-domainname checks! _domains.append(_fqdn); _domains.append(QL1C('.') + _fqdn); QStringList partList = _fqdn.split(QL1C('.'), QString::SkipEmptyParts); if (!partList.isEmpty()) { partList.erase(partList.begin()); // Remove hostname } while (partList.count()) { if (partList.count() == 1) { break; // We only have a TLD left. } if ((partList.count() == 2) && m_twoLevelTLD.contains(partList[1].toLower())) { // This domain uses two-level TLDs in the form xxxx.yy break; } if ((partList.count() == 2) && (partList[1].length() == 2)) { // If this is a TLD, we should stop. (e.g. co.uk) // We assume this is a TLD if it ends with .xx.yy or .x.yy if (partList[0].length() <= 2) { break; // This is a TLD. } // Catch some TLDs that we miss with the previous check // e.g. com.au, org.uk, mil.co if (m_gTLDs.contains(partList[0].toLower())) { break; } } QString domain = partList.join(QLatin1Char('.')); _domains.append(domain); _domains.append(QL1C('.') + domain); partList.erase(partList.begin()); // Remove part } } // // This function parses cookie_headers and returns a linked list of // KHttpCookie objects for all cookies found in cookie_headers. // If no cookies could be found 0 is returned. // // cookie_headers should be a concatenation of all lines of a HTTP-header // which start with "Set-Cookie". The lines should be separated by '\n's. // KHttpCookieList KCookieJar::makeCookies(const QString &_url, const QByteArray &cookie_headers, WId windowId) { QString fqdn, path; if (!parseUrl(_url, fqdn, path)) { return KHttpCookieList(); // Error parsing _url } QString Name, Value; KHttpCookieList cookieList, cookieList2; bool isRFC2965 = false; bool crossDomain = false; const char *cookieStr = cookie_headers.constData(); QString defaultPath; const int index = path.lastIndexOf(QL1C('/')); if (index > 0) { defaultPath = path.left(index); } // Check for cross-domain flag from kio_http if (qstrncmp(cookieStr, "Cross-Domain\n", 13) == 0) { cookieStr += 13; crossDomain = true; } // The hard stuff :) for (;;) { // check for "Set-Cookie" if (qstrnicmp(cookieStr, "Set-Cookie:", 11) == 0) { cookieStr = parseNameValue(cookieStr + 11, Name, Value, true); // Host = FQDN // Default domain = "" // Default path according to rfc2109 KHttpCookie cookie(fqdn, QL1S(""), defaultPath, Name, Value); if (windowId) { cookie.mWindowIds.append(windowId); } cookie.mCrossDomain = crossDomain; // Insert cookie in chain cookieList.append(cookie); } else if (qstrnicmp(cookieStr, "Set-Cookie2:", 12) == 0) { // Attempt to follow rfc2965 isRFC2965 = true; cookieStr = parseNameValue(cookieStr + 12, Name, Value, true, true); // Host = FQDN // Default domain = "" // Default path according to rfc2965 KHttpCookie cookie(fqdn, QL1S(""), defaultPath, Name, Value); if (windowId) { cookie.mWindowIds.append(windowId); } cookie.mCrossDomain = crossDomain; // Insert cookie in chain cookieList2.append(cookie); } else { // This is not the start of a cookie header, skip till next line. while (*cookieStr && *cookieStr != '\n') { cookieStr++; } if (*cookieStr == '\n') { cookieStr++; } if (!*cookieStr) { break; // End of cookie_headers } else { continue; // end of this header, continue with next. } } while ((*cookieStr == ';') || (*cookieStr == ' ')) { cookieStr++; // Name-Value pair follows cookieStr = parseNameValue(cookieStr, Name, Value); KHttpCookie &lastCookie = (isRFC2965 ? cookieList2.last() : cookieList.last()); if (Name.compare(QL1S("domain"), Qt::CaseInsensitive) == 0) { QString dom = Value.toLower(); // RFC2965 3.2.2: If an explicitly specified value does not // start with a dot, the user agent supplies a leading dot if (dom.length() > 0 && dom[0] != QL1C('.')) { dom.prepend(QL1C('.')); } // remove a trailing dot if (dom.length() > 2 && dom[dom.length() - 1] == QL1C('.')) { dom.chop(1); } if (dom.count(QL1C('.')) > 1 || dom == QLatin1String(".local")) { lastCookie.mDomain = dom; } } else if (Name.compare(QL1S("max-age"), Qt::CaseInsensitive) == 0) { int max_age = Value.toInt(); if (max_age == 0) { lastCookie.mExpireDate = 1; } else { lastCookie.mExpireDate = toEpochSecs(QDateTime::currentDateTimeUtc().addSecs(max_age)); } } else if (Name.compare(QL1S("expires"), Qt::CaseInsensitive) == 0) { const QDateTime dt = parseDate(Value); if (dt.isValid()) { lastCookie.mExpireDate = toEpochSecs(dt); if (lastCookie.mExpireDate == 0) { lastCookie.mExpireDate = 1; } } } else if (Name.compare(QL1S("path"), Qt::CaseInsensitive) == 0) { if (Value.isEmpty()) { lastCookie.mPath.clear(); // Catch "" <> QString() } else { lastCookie.mPath = QUrl::fromPercentEncoding(Value.toLatin1()); } lastCookie.mExplicitPath = true; } else if (Name.compare(QL1S("version"), Qt::CaseInsensitive) == 0) { lastCookie.mProtocolVersion = Value.toInt(); } else if (Name.compare(QL1S("secure"), Qt::CaseInsensitive) == 0 || (Name.isEmpty() && Value.compare(QL1S("secure"), Qt::CaseInsensitive) == 0)) { lastCookie.mSecure = true; } else if (Name.compare(QL1S("httponly"), Qt::CaseInsensitive) == 0 || (Name.isEmpty() && Value.compare(QL1S("httponly"), Qt::CaseInsensitive) == 0)) { lastCookie.mHttpOnly = true; } else if (isRFC2965 && (Name.compare(QL1S("port"), Qt::CaseInsensitive) == 0 || (Name.isEmpty() && Value.compare(QL1S("port"), Qt::CaseInsensitive) == 0))) { // Based on the port selection rule of RFC 2965 section 3.3.4... if (Name.isEmpty()) { // We intentionally append a -1 first in order to distinguish // between only a 'Port' vs a 'Port="80 443"' in the sent cookie. lastCookie.mPorts.append(-1); const bool secureRequest = (_url.startsWith(QL1S("https://"), Qt::CaseInsensitive) || _url.startsWith(QL1S("webdavs://"), Qt::CaseInsensitive)); if (secureRequest) { lastCookie.mPorts.append(443); } else { lastCookie.mPorts.append(80); } } else { bool ok; const QStringList portNums = Value.split(QL1C(' '), QString::SkipEmptyParts); for (const QString &portNum : portNums) { const int port = portNum.toInt(&ok); if (ok) { lastCookie.mPorts.append(port); } } } } } if (*cookieStr == '\0') { break; // End of header } // Skip ';' or '\n' cookieStr++; } // RFC2965 cookies come last so that they override netscape cookies. while (!cookieList2.isEmpty()) { KHttpCookie &lastCookie = cookieList2.first(); removeDuplicateFromList(&cookieList, lastCookie, true); cookieList.append(lastCookie); cookieList2.removeFirst(); } return cookieList; } /** * Parses cookie_domstr and returns a linked list of KHttpCookie objects. * cookie_domstr should be a semicolon-delimited list of "name=value" * pairs. Any whitespace before "name" or around '=' is discarded. * If no cookies are found, 0 is returned. */ KHttpCookieList KCookieJar::makeDOMCookies(const QString &_url, const QByteArray &cookie_domstring, WId windowId) { // A lot copied from above KHttpCookieList cookieList; const char *cookieStr = cookie_domstring.data(); QString fqdn; QString path; if (!parseUrl(_url, fqdn, path)) { // Error parsing _url return KHttpCookieList(); } QString Name; QString Value; // This time it's easy while (*cookieStr) { cookieStr = parseNameValue(cookieStr, Name, Value); // Host = FQDN // Default domain = "" // Default path = "" KHttpCookie cookie(fqdn, QString(), QString(), Name, Value); if (windowId) { cookie.mWindowIds.append(windowId); } cookieList.append(cookie); if (*cookieStr != '\0') { cookieStr++; // Skip ';' or '\n' } } return cookieList; } // KHttpCookieList sorting /////////////////////////////////////////////////////////////////////////// // We want the longest path first static bool compareCookies(const KHttpCookie &item1, const KHttpCookie &item2) { return item1.path().length() > item2.path().length(); } #ifdef MAX_COOKIE_LIMIT static void makeRoom(KHttpCookieList *cookieList, KHttpCookiePtr &cookiePtr) { // Too many cookies: throw one away, try to be somewhat clever KHttpCookiePtr lastCookie = 0; for (KHttpCookiePtr cookie = cookieList->first(); cookie; cookie = cookieList->next()) { if (compareCookies(cookie, cookiePtr)) { break; } lastCookie = cookie; } if (!lastCookie) { lastCookie = cookieList->first(); } cookieList->removeRef(lastCookie); } #endif // // This function hands a KHttpCookie object over to the cookie jar. // void KCookieJar::addCookie(KHttpCookie &cookie) { QStringList domains; // We always need to do this to make sure that the // that cookies of type hostname == cookie-domainname // are properly removed and/or updated as necessary! extractDomains(cookie.host(), domains); // If the cookie specifies a domain, check whether it is valid. Otherwise, // accept the cookie anyways but removes the domain="" value to prevent // cross-site cookie injection. if (!cookie.domain().isEmpty()) { if (!domains.contains(cookie.domain()) && !cookie.domain().endsWith(QL1C('.') + cookie.host())) { cookie.fixDomain(QString()); } } QStringListIterator it(domains); while (it.hasNext()) { const QString &key = it.next(); KHttpCookieList *list; if (key.isNull()) { list = m_cookieDomains.value(QL1S("")); } else { list = m_cookieDomains.value(key); } if (list) { removeDuplicateFromList(list, cookie, false, true); } } const QString domain = stripDomain(cookie); KHttpCookieList *cookieList; if (domain.isNull()) { cookieList = m_cookieDomains.value(QL1S("")); } else { cookieList = m_cookieDomains.value(domain); } if (!cookieList) { // Make a new cookie list cookieList = new KHttpCookieList(); // All cookies whose domain is not already // known to us should be added with KCookieDunno. // KCookieDunno means that we use the global policy. cookieList->setAdvice(KCookieDunno); m_cookieDomains.insert(domain, cookieList); // Update the list of domains m_domainList.append(domain); } // Add the cookie to the cookie list // The cookie list is sorted 'longest path first' if (!cookie.isExpired()) { #ifdef MAX_COOKIE_LIMIT if (cookieList->count() >= MAX_COOKIES_PER_HOST) { makeRoom(cookieList, cookie); // Delete a cookie } #endif cookieList->push_back(cookie); // Use a stable sort so that unit tests are reliable. // In practice it doesn't matter though. std::stable_sort(cookieList->begin(), cookieList->end(), compareCookies); m_cookiesChanged = true; } } // // This function advises whether a single KHttpCookie object should // be added to the cookie jar. // KCookieAdvice KCookieJar::cookieAdvice(const KHttpCookie &cookie) const { if (m_rejectCrossDomainCookies && cookie.isCrossDomain()) { return KCookieReject; } if (cookie.getUserSelectedAdvice() != KCookieDunno) { return cookie.getUserSelectedAdvice(); } if (m_autoAcceptSessionCookies && cookie.expireDate() == 0) { return KCookieAccept; } QStringList domains; extractDomains(cookie.host(), domains); KCookieAdvice advice = KCookieDunno; QStringListIterator it(domains); while (advice == KCookieDunno && it.hasNext()) { const QString &domain = it.next(); if (domain.startsWith(QL1C('.')) || cookie.host() == domain) { KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (cookieList) { advice = cookieList->getAdvice(); } } } if (advice == KCookieDunno) { advice = m_globalAdvice; } return advice; } // // This function tells whether a single KHttpCookie object should // be considered persistent. Persistent cookies do not get deleted // at the end of the session and are saved on disk. // bool KCookieJar::cookieIsPersistent(const KHttpCookie &cookie) const { if (cookie.expireDate() == 0) { return false; } KCookieAdvice advice = cookieAdvice(cookie); if (advice == KCookieReject || advice == KCookieAcceptForSession) { return false; } return true; } // // This function gets the advice for all cookies originating from // _domain. // KCookieAdvice KCookieJar::getDomainAdvice(const QString &_domain) const { KHttpCookieList *cookieList = m_cookieDomains.value(_domain); KCookieAdvice advice; if (cookieList) { advice = cookieList->getAdvice(); } else { advice = KCookieDunno; } return advice; } // // This function sets the advice for all cookies originating from // _domain. // void KCookieJar::setDomainAdvice(const QString &_domain, KCookieAdvice _advice) { QString domain(_domain); KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (cookieList) { if (cookieList->getAdvice() != _advice) { m_configChanged = true; // domain is already known cookieList->setAdvice(_advice); } if ((cookieList->isEmpty()) && (_advice == KCookieDunno)) { // This deletes cookieList! delete m_cookieDomains.take(domain); m_domainList.removeAll(domain); } } else { // domain is not yet known if (_advice != KCookieDunno) { // We should create a domain entry m_configChanged = true; // Make a new cookie list cookieList = new KHttpCookieList(); cookieList->setAdvice(_advice); m_cookieDomains.insert(domain, cookieList); // Update the list of domains m_domainList.append(domain); } } } // // This function sets the advice for all cookies originating from // the same domain as _cookie // void KCookieJar::setDomainAdvice(const KHttpCookie &cookie, KCookieAdvice _advice) { QString domain; stripDomain(cookie.host(), domain); // We file the cookie under this domain. setDomainAdvice(domain, _advice); } // // This function sets the global advice for cookies // void KCookieJar::setGlobalAdvice(KCookieAdvice _advice) { if (m_globalAdvice != _advice) { m_configChanged = true; } m_globalAdvice = _advice; } // // Get a list of all domains known to the cookie jar. // const QStringList &KCookieJar::getDomainList() { return m_domainList; } // // Get a list of all cookies in the cookie jar originating from _domain. // KHttpCookieList *KCookieJar::getCookieList(const QString &_domain, const QString &_fqdn) { QString domain; if (_domain.isEmpty()) { stripDomain(_fqdn, domain); } else { domain = _domain; } return m_cookieDomains.value(domain); } // // Eat a cookie out of the jar. // cookieIterator should be one of the cookies returned by getCookieList() // void KCookieJar::eatCookie(const KHttpCookieList::iterator &cookieIterator) { const KHttpCookie &cookie = *cookieIterator; const QString domain = stripDomain(cookie); // We file the cookie under this domain. KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (cookieList) { // This deletes cookie! cookieList->erase(cookieIterator); if ((cookieList->isEmpty()) && (cookieList->getAdvice() == KCookieDunno)) { // This deletes cookieList! delete m_cookieDomains.take(domain); m_domainList.removeAll(domain); } } } void KCookieJar::eatCookiesForDomain(const QString &domain) { KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (!cookieList || cookieList->isEmpty()) { return; } cookieList->clear(); if (cookieList->getAdvice() == KCookieDunno) { // This deletes cookieList! delete m_cookieDomains.take(domain); m_domainList.removeAll(domain); } m_cookiesChanged = true; } void KCookieJar::eatSessionCookies(long windowId) { if (!windowId) { return; } Q_FOREACH (const QString &domain, m_domainList) { eatSessionCookies(domain, windowId, false); } } void KCookieJar::eatAllCookies() { Q_FOREACH (const QString &domain, m_domainList) { eatCookiesForDomain(domain); // This might remove domain from m_domainList! } } void KCookieJar::eatSessionCookies(const QString &fqdn, WId windowId, bool isFQDN) { KHttpCookieList *cookieList; if (!isFQDN) { cookieList = m_cookieDomains.value(fqdn); } else { QString domain; stripDomain(fqdn, domain); cookieList = m_cookieDomains.value(domain); } if (cookieList) { QMutableListIterator cookieIterator(*cookieList); while (cookieIterator.hasNext()) { KHttpCookie &cookie = cookieIterator.next(); if (cookieIsPersistent(cookie)) { continue; } QList &ids = cookie.windowIds(); #ifndef NDEBUG if (ids.contains(windowId)) { if (ids.count() > 1) { qCDebug(KIO_COOKIEJAR) << "removing window id" << windowId << "from session cookie"; } else { qCDebug(KIO_COOKIEJAR) << "deleting session cookie"; } } #endif if (!ids.removeAll(windowId) || !ids.isEmpty()) { continue; } cookieIterator.remove(); } } } static QString hostWithPort(const KHttpCookie *cookie) { const QList &ports = cookie->ports(); if (ports.isEmpty()) { return cookie->host(); } QStringList portList; for (int port : ports) { portList << QString::number(port); } return (cookie->host() + QL1C(':') + portList.join(QLatin1Char(','))); } // // Saves all cookies to the file '_filename'. // On success 'true' is returned. // On failure 'false' is returned. bool KCookieJar::saveCookies(const QString &_filename) { QSaveFile cookieFile(_filename); if (!cookieFile.open(QIODevice::WriteOnly)) { return false; } QTextStream ts(&cookieFile); ts << "# KDE Cookie File v2\n#\n"; - QString s; - s.sprintf("%-20s %-20s %-12s %-10s %-4s %-20s %-4s %s\n", + const QString str = + QString::asprintf("%-20s %-20s %-12s %-10s %-4s %-20s %-4s %s\n", "# Host", "Domain", "Path", "Exp.date", "Prot", "Name", "Sec", "Value"); - ts << s.toLatin1().constData(); + ts << str; QStringListIterator it(m_domainList); while (it.hasNext()) { const QString &domain = it.next(); bool domainPrinted = false; KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (!cookieList) { continue; } QMutableListIterator cookieIterator(*cookieList); while (cookieIterator.hasNext()) { const KHttpCookie &cookie = cookieIterator.next(); if (cookie.isExpired()) { // Delete expired cookies cookieIterator.remove(); continue; } if (cookieIsPersistent(cookie)) { // Only save cookies that are not "session-only cookies" if (!domainPrinted) { domainPrinted = true; ts << '[' << domain.toLocal8Bit().data() << "]\n"; } // Store persistent cookies const QString path = QL1C('"') + cookie.path() + QL1C('"'); const QString domain = QL1C('"') + cookie.domain() + QL1C('"'); const QString host = hostWithPort(&cookie); // TODO: replace with direct QTextStream output ? - s.sprintf("%-20s %-20s %-12s %10lld %3d %-20s %-4i %s\n", + const QString str = QString::asprintf("%-20s %-20s %-12s %10lld %3d %-20s %-4i %s\n", host.toLatin1().constData(), domain.toLatin1().constData(), path.toLatin1().constData(), cookie.expireDate(), cookie.protocolVersion(), cookie.name().isEmpty() ? cookie.value().toLatin1().constData() : cookie.name().toLatin1().constData(), (cookie.isSecure() ? 1 : 0) + (cookie.isHttpOnly() ? 2 : 0) + (cookie.hasExplicitPath() ? 4 : 0) + (cookie.name().isEmpty() ? 8 : 0), cookie.value().toLatin1().constData()); - ts << s.toLatin1().constData(); + ts << str.toLatin1(); } } } if (cookieFile.commit()) { QFile::setPermissions(_filename, QFile::ReadUser | QFile::WriteUser); return true; } return false; } static const char *parseField(char *&buffer, bool keepQuotes = false) { char *result; if (!keepQuotes && (*buffer == '\"')) { // Find terminating " buffer++; result = buffer; while ((*buffer != '\"') && (*buffer)) { buffer++; } } else { // Find first white space result = buffer; while ((*buffer != ' ') && (*buffer != '\t') && (*buffer != '\n') && (*buffer)) { buffer++; } } if (!*buffer) { return result; // } *buffer++ = '\0'; // Skip white-space while ((*buffer == ' ') || (*buffer == '\t') || (*buffer == '\n')) { buffer++; } return result; } static QString extractHostAndPorts(const QString &str, QList *ports = nullptr) { if (str.isEmpty()) { return str; } const int index = str.indexOf(QL1C(':')); if (index == -1) { return str; } const QString host = str.left(index); if (ports) { bool ok; QStringList portList = str.mid(index + 1).split(QL1C(',')); Q_FOREACH (const QString &portStr, portList) { const int portNum = portStr.toInt(&ok); if (ok) { ports->append(portNum); } } } return host; } // // Reloads all cookies from the file '_filename'. // On success 'true' is returned. // On failure 'false' is returned. bool KCookieJar::loadCookies(const QString &_filename) { QFile cookieFile(_filename); if (!cookieFile.open(QIODevice::ReadOnly)) { return false; } int version = 1; bool success = false; char *buffer = new char[READ_BUFFER_SIZE]; qint64 len = cookieFile.readLine(buffer, READ_BUFFER_SIZE - 1); if (len != -1) { if (qstrcmp(buffer, "# KDE Cookie File\n") == 0) { success = true; } else if (qstrcmp(buffer, "# KDE Cookie File v") > 0) { bool ok = false; const int verNum = QByteArray(buffer + 19, len - 19).trimmed().toInt(&ok); if (ok) { version = verNum; success = true; } } } if (success) { const qint64 currentTime = epoch(); QList ports; while (cookieFile.readLine(buffer, READ_BUFFER_SIZE - 1) != -1) { char *line = buffer; // Skip lines which begin with '#' or '[' if ((line[0] == '#') || (line[0] == '[')) { continue; } const QString host = extractHostAndPorts(QL1S(parseField(line)), &ports); const QString domain = QL1S(parseField(line)); if (host.isEmpty() && domain.isEmpty()) { continue; } const QString path = QL1S(parseField(line)); const QString expStr = QL1S(parseField(line)); if (expStr.isEmpty()) { continue; } const qint64 expDate = expStr.toLongLong(); const QString verStr = QL1S(parseField(line)); if (verStr.isEmpty()) { continue; } int protVer = verStr.toInt(); QString name = QL1S(parseField(line)); bool keepQuotes = false; bool secure = false; bool httpOnly = false; bool explicitPath = false; const char *value = nullptr; if ((version == 2) || (protVer >= 200)) { if (protVer >= 200) { protVer -= 200; } int i = atoi(parseField(line)); secure = i & 1; httpOnly = i & 2; explicitPath = i & 4; if (i & 8) { name = QLatin1String(""); } line[strlen(line) - 1] = '\0'; // Strip LF. value = line; } else { if (protVer >= 100) { protVer -= 100; keepQuotes = true; } value = parseField(line, keepQuotes); secure = QByteArray(parseField(line)).toShort(); } // Expired or parse error if (!value || expDate == 0 || expDate < currentTime) { continue; } KHttpCookie cookie(host, domain, path, name, QString::fromUtf8(value), expDate, protVer, secure, httpOnly, explicitPath); if (!ports.isEmpty()) { cookie.mPorts = ports; } addCookie(cookie); } } delete [] buffer; m_cookiesChanged = false; return success; } // // Save the cookie configuration // void KCookieJar::saveConfig(KConfig *_config) { if (!m_configChanged) { return; } KConfigGroup dlgGroup(_config, "Cookie Dialog"); dlgGroup.writeEntry("PreferredPolicy", static_cast(m_preferredPolicy)); dlgGroup.writeEntry("ShowCookieDetails", m_showCookieDetails); KConfigGroup policyGroup(_config, "Cookie Policy"); policyGroup.writeEntry("CookieGlobalAdvice", adviceToStr(m_globalAdvice)); QStringList domainSettings; QStringListIterator it(m_domainList); while (it.hasNext()) { const QString &domain = it.next(); KCookieAdvice advice = getDomainAdvice(domain); if (advice != KCookieDunno) { const QString value = domain + QL1C(':') + adviceToStr(advice); domainSettings.append(value); } } policyGroup.writeEntry("CookieDomainAdvice", domainSettings); _config->sync(); m_configChanged = false; } // // Load the cookie configuration // void KCookieJar::loadConfig(KConfig *_config, bool reparse) { if (reparse) { _config->reparseConfiguration(); } KConfigGroup dlgGroup(_config, "Cookie Dialog"); m_showCookieDetails = dlgGroup.readEntry("ShowCookieDetails", false); m_preferredPolicy = static_cast(dlgGroup.readEntry("PreferredPolicy", 0)); KConfigGroup policyGroup(_config, "Cookie Policy"); const QStringList domainSettings = policyGroup.readEntry("CookieDomainAdvice", QStringList()); // Warning: those default values are duplicated in the kcm (kio/kcookiespolicies.cpp) m_rejectCrossDomainCookies = policyGroup.readEntry("RejectCrossDomainCookies", true); m_autoAcceptSessionCookies = policyGroup.readEntry("AcceptSessionCookies", true); m_globalAdvice = strToAdvice(policyGroup.readEntry("CookieGlobalAdvice", QStringLiteral("Accept"))); // Reset current domain settings first. Q_FOREACH (const QString &domain, m_domainList) { setDomainAdvice(domain, KCookieDunno); } // Now apply the domain settings read from config file... for (QStringList::ConstIterator it = domainSettings.constBegin(), itEnd = domainSettings.constEnd(); it != itEnd; ++it) { const QString &value = *it; const int sepPos = value.lastIndexOf(QL1C(':')); if (sepPos <= 0) { continue; } const QString domain(value.left(sepPos)); KCookieAdvice advice = strToAdvice(value.mid(sepPos + 1)); setDomainAdvice(domain, advice); } } QDebug operator<<(QDebug dbg, const KHttpCookie &cookie) { dbg.nospace() << cookie.cookieStr(false); return dbg.space(); } QDebug operator<<(QDebug dbg, const KHttpCookieList &list) { for (const KHttpCookie &cookie : list) { dbg << cookie; } return dbg; } diff --git a/src/ioslaves/http/kcookiejar/kcookiewin.cpp b/src/ioslaves/http/kcookiejar/kcookiewin.cpp index 95297371..10bbda26 100644 --- a/src/ioslaves/http/kcookiejar/kcookiewin.cpp +++ b/src/ioslaves/http/kcookiejar/kcookiewin.cpp @@ -1,388 +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().isEmpty()) { setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(windowHandle(), 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()); const int pos = host.indexOf(QLatin1Char(':')); if (pos > 0) { QString portNum = host.left(pos); host.remove(0, pos + 1); host += QLatin1Char(':'); 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") + QLatin1String(" >>")); 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, &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, &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, &QAbstractButton::clicked, this, &QDialog::accept); but = buttonBox->button(QDialogButtonBox::No); but->setText(i18n("&Reject")); connect(but, &QAbstractButton::clicked, this, &QDialog::reject); topLayout->addWidget(buttonBox); QVBoxLayout *vBox1Layout = new QVBoxLayout(vBox1); vBox1Layout->setSpacing(-1); vBox1Layout->setContentsMargins(0, 0, 0, 0); // Cookie image and message to user QFrame *hBox = new QFrame(vBox1); vBox1Layout->addWidget(hBox); QHBoxLayout *hBoxLayout = new QHBoxLayout(hBox); hBoxLayout->setSpacing(0); hBoxLayout->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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, &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()); + QDateTime cookiedate = QDateTime::fromSecsSinceEpoch(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 + QLatin1String(" >>")); m_detailView->hide(); } else { m_detailsButton->setText(baseText + QLatin1String(" <<")); m_detailView->show(); } } diff --git a/src/kcms/kio/kcookiesmanagement.cpp b/src/kcms/kio/kcookiesmanagement.cpp index 332b516a..221926da 100644 --- a/src/kcms/kio/kcookiesmanagement.cpp +++ b/src/kcms/kio/kcookiesmanagement.cpp @@ -1,423 +1,422 @@ /** * kcookiesmanagement.cpp - Cookies manager * * Copyright 2000-2001 Marco Pinelli * Copyright 2000-2001 Dawit Alemayehu * * 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. */ // Own #include "kcookiesmanagement.h" // Qt #include #include #include #include #include // KDE #include #include #include #include #include // Local #include "kcookiesmain.h" #include "kcookiespolicies.h" QString tolerantFromAce(const QByteArray& _domain); struct CookieProp { QString host; QString name; QString value; QString domain; QString path; QString expireDate; QString secure; bool allLoaded; }; CookieListViewItem::CookieListViewItem(QTreeWidget *parent, const QString &dom) :QTreeWidgetItem(parent) { init( nullptr, dom ); } CookieListViewItem::CookieListViewItem(QTreeWidgetItem *parent, CookieProp *cookie) :QTreeWidgetItem(parent) { init( cookie ); } CookieListViewItem::~CookieListViewItem() { delete mCookie; } void CookieListViewItem::init( CookieProp* cookie, const QString &domain, bool cookieLoaded ) { mCookie = cookie; mDomain = domain; mCookiesLoaded = cookieLoaded; if (mCookie) { if (mDomain.isEmpty()) setText(0, tolerantFromAce(mCookie->host.toLatin1())); else setText(0, tolerantFromAce(mDomain.toLatin1())); setText(1, mCookie->name); } else { QString siteName; if (mDomain.startsWith(QLatin1Char('.'))) siteName = mDomain.mid(1); else siteName = mDomain; setText(0, tolerantFromAce(siteName.toLatin1())); } } CookieProp* CookieListViewItem::leaveCookie() { CookieProp *ret = mCookie; mCookie = nullptr; return ret; } KCookiesManagement::KCookiesManagement(QWidget *parent) : KCModule(parent), mDeleteAllFlag(false), mMainWidget(parent) { mUi.setupUi(this); mUi.searchLineEdit->setTreeWidget(mUi.cookiesTreeWidget); mUi.cookiesTreeWidget->setColumnWidth(0, 150); connect(mUi.cookiesTreeWidget, &QTreeWidget::itemDoubleClicked, this, &KCookiesManagement::on_configPolicyButton_clicked); } KCookiesManagement::~KCookiesManagement() { } void KCookiesManagement::load() { defaults(); } void KCookiesManagement::save() { // If delete all cookies was requested! if(mDeleteAllFlag) { QDBusInterface kded(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"), QDBusConnection::sessionBus()); QDBusReply reply = kded.call( QStringLiteral("deleteAllCookies") ); if (!reply.isValid()) { const QString caption = i18n ("D-Bus Communication Error"); const QString message = i18n ("Unable to delete all the cookies as requested."); KMessageBox::sorry (this, message, caption); return; } mDeleteAllFlag = false; // deleted[Cookies|Domains] have been cleared yet } // Certain groups of cookies were deleted... QMutableStringListIterator it (mDeletedDomains); while (it.hasNext()) { QDBusInterface kded(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"), QDBusConnection::sessionBus()); QDBusReply reply = kded.call( QStringLiteral("deleteCookiesFromDomain"),( it.next() ) ); if (!reply.isValid()) { const QString caption = i18n ("D-Bus Communication Error"); const QString message = i18n ("Unable to delete cookies as requested."); KMessageBox::sorry (this, message, caption); return; } it.remove(); } // Individual cookies were deleted... bool success = true; // Maybe we can go on... QMutableHashIterator cookiesDom(mDeletedCookies); while(cookiesDom.hasNext()) { cookiesDom.next(); CookiePropList list = cookiesDom.value(); foreach(CookieProp *cookie, list) { QDBusInterface kded(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"), QDBusConnection::sessionBus()); QDBusReply reply = kded.call( QStringLiteral("deleteCookie"), cookie->domain, cookie->host, cookie->path, cookie->name ); if (!reply.isValid()) { success = false; break; } list.removeOne(cookie); } if (!success) break; mDeletedCookies.remove(cookiesDom.key()); } emit changed( false ); } void KCookiesManagement::defaults() { reset(); on_reloadButton_clicked(); } void KCookiesManagement::reset(bool deleteAll) { if (!deleteAll) mDeleteAllFlag = false; clearCookieDetails(); mDeletedDomains.clear(); mDeletedCookies.clear(); mUi.cookiesTreeWidget->clear(); mUi.deleteButton->setEnabled(false); mUi.deleteAllButton->setEnabled(false); mUi.configPolicyButton->setEnabled(false); } void KCookiesManagement::clearCookieDetails() { mUi.nameLineEdit->clear(); mUi.valueLineEdit->clear(); mUi.domainLineEdit->clear(); mUi.pathLineEdit->clear(); mUi.expiresLineEdit->clear(); mUi.secureLineEdit->clear(); } QString KCookiesManagement::quickHelp() const { return i18n("

Cookie Management Quick Help

" ); } void KCookiesManagement::on_reloadButton_clicked() { QDBusInterface kded(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"), QDBusConnection::sessionBus()); QDBusReply reply = kded.call( QStringLiteral("findDomains") ); if (!reply.isValid()) { const QString caption = i18n ("Information Lookup Failure"); const QString message = i18n ("Unable to retrieve information about the " "cookies stored on your computer."); KMessageBox::sorry (this, message, caption); return; } if (mUi.cookiesTreeWidget->topLevelItemCount() > 0) reset(); CookieListViewItem *dom; const QStringList domains (reply.value()); for (const QString& domain : domains) { const QString siteName = (domain.startsWith(QLatin1Char('.')) ? domain.mid(1) : domain); if (mUi.cookiesTreeWidget->findItems(siteName, Qt::MatchFixedString).isEmpty()) { dom = new CookieListViewItem(mUi.cookiesTreeWidget, domain); dom->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); } } // are there any cookies? mUi.deleteAllButton->setEnabled(mUi.cookiesTreeWidget->topLevelItemCount() > 0); mUi.cookiesTreeWidget->sortItems(0, Qt::AscendingOrder); emit changed(false); } Q_DECLARE_METATYPE( QList ) void KCookiesManagement::on_cookiesTreeWidget_itemExpanded(QTreeWidgetItem *item) { CookieListViewItem* cookieDom = static_cast(item); if (!cookieDom || cookieDom->cookiesLoaded()) return; QStringList cookies; const QList fields { 0, 1, 2, 3 }; // Always check for cookies in both "foo.bar" and ".foo.bar" domains... const QString domain = cookieDom->domain() + QLatin1String(" .") + cookieDom->domain(); QDBusInterface kded(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"), QDBusConnection::sessionBus()); QDBusReply reply = kded.call(QStringLiteral("findCookies"), QVariant::fromValue(fields), domain, QString(), QString(), QString()); if (reply.isValid()) cookies.append(reply.value()); QStringListIterator it(cookies); while (it.hasNext()) { CookieProp *details = new CookieProp; details->domain = it.next(); details->path = it.next(); details->name = it.next(); details->host = it.next(); details->allLoaded = false; new CookieListViewItem(item, details); } if (!cookies.isEmpty()) { static_cast(item)->setCookiesLoaded(); mUi.searchLineEdit->updateSearch(); } } bool KCookiesManagement::cookieDetails(CookieProp *cookie) { const QList fields{ 4, 5, 7 }; QDBusInterface kded(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"), QDBusConnection::sessionBus()); QDBusReply reply = kded.call( QStringLiteral("findCookies"), QVariant::fromValue( fields ), cookie->domain, cookie->host, cookie->path, cookie->name); if (!reply.isValid()) return false; const QStringList fieldVal = reply.value(); QStringList::const_iterator c = fieldVal.begin(); if (c == fieldVal.end()) // empty list, do not crash return false; bool ok; cookie->value = *c++; qint64 tmp = (*c++).toLongLong(&ok); if (!ok || tmp == 0) cookie->expireDate = i18n("End of session"); else { - QDateTime expDate; - expDate.setTime_t(tmp); + QDateTime expDate = QDateTime::fromSecsSinceEpoch(tmp); cookie->expireDate = QLocale().toString((expDate), QLocale::ShortFormat); } tmp = (*c).toUInt(&ok); cookie->secure = i18n((ok && tmp) ? "Yes" : "No"); cookie->allLoaded = true; return true; } void KCookiesManagement::on_cookiesTreeWidget_currentItemChanged(QTreeWidgetItem* item) { if (item) { CookieListViewItem* cookieItem = static_cast(item); CookieProp *cookie = cookieItem->cookie(); if (cookie) { if (cookie->allLoaded || cookieDetails(cookie)) { mUi.nameLineEdit->setText(cookie->name); mUi.valueLineEdit->setText(cookie->value); mUi.domainLineEdit->setText(cookie->domain); mUi.pathLineEdit->setText(cookie->path); mUi.expiresLineEdit->setText(cookie->expireDate); mUi.secureLineEdit->setText(cookie->secure); } mUi.configPolicyButton->setEnabled(false); } else { clearCookieDetails(); mUi.configPolicyButton->setEnabled(true); } } else { mUi.configPolicyButton->setEnabled(false); } mUi.deleteButton->setEnabled(item != nullptr); } void KCookiesManagement::on_configPolicyButton_clicked() { // Get current item CookieListViewItem *item = static_cast(mUi.cookiesTreeWidget->currentItem()); Q_ASSERT(item); // the button is disabled otherwise if (item) { KCookiesMain* mainDlg = qobject_cast(mMainWidget); // must be present or something is really wrong. Q_ASSERT(mainDlg); KCookiesPolicies* policyDlg = mainDlg->policyDlg(); // must be present unless someone rewrote the widget in which case // this needs to be re-written as well. Q_ASSERT(policyDlg); policyDlg->setPolicy(item->domain()); } } void KCookiesManagement::on_deleteButton_clicked() { QTreeWidgetItem* currentItem = mUi.cookiesTreeWidget->currentItem(); Q_ASSERT(currentItem); // the button is disabled otherwise CookieListViewItem *item = static_cast( currentItem ); if (item->cookie()) { CookieListViewItem *parent = static_cast(item->parent()); CookiePropList list = mDeletedCookies.value(parent->domain()); list.append(item->leaveCookie()); mDeletedCookies.insert(parent->domain(), list); delete item; if (parent->childCount() == 0) delete parent; } else { mDeletedDomains.append(item->domain()); delete item; } currentItem = mUi.cookiesTreeWidget->currentItem(); if (currentItem) { mUi.cookiesTreeWidget->setCurrentItem( currentItem ); } else clearCookieDetails(); mUi.deleteAllButton->setEnabled(mUi.cookiesTreeWidget->topLevelItemCount() > 0); emit changed( true ); } void KCookiesManagement::on_deleteAllButton_clicked() { mDeleteAllFlag = true; reset(true); emit changed(true); } diff --git a/src/kcms/kio/useragentselectordlg.cpp b/src/kcms/kio/useragentselectordlg.cpp index 9c094656..7864fae3 100644 --- a/src/kcms/kio/useragentselectordlg.cpp +++ b/src/kcms/kio/useragentselectordlg.cpp @@ -1,159 +1,164 @@ /** * Copyright (c) 2001 Dawit Alemayehu * * 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. */ // Own #include "useragentselectordlg.h" // Local #include "useragentinfo.h" // Qt #include #include #include #include // KDE #include #include #include #include class UserAgentSiteNameValidator : public QValidator { Q_OBJECT public: UserAgentSiteNameValidator (QObject* parent) : QValidator (parent) { setObjectName (QStringLiteral ("UserAgentSiteNameValidator")); } State validate (QString& input, int&) const override { if (input.isEmpty()) return Intermediate; if (input.startsWith(QLatin1Char('.'))) return Invalid; const int length = input.length(); for (int i = 0 ; i < length; i++) { if (!input[i].isLetterOrNumber() && input[i] != QLatin1Char('.') && input[i] != QLatin1Char('-')) return Invalid; } return Acceptable; } }; UserAgentSelectorDlg::UserAgentSelectorDlg (UserAgentInfo* info, QWidget* parent, Qt::WindowFlags f) : QDialog (parent, f), mUserAgentInfo (info), mButtonBox(nullptr) { QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->addWidget(mainWidget); mUi.setupUi (mainWidget); mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); mainLayout->addWidget(mButtonBox); connect(mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); if (!mUserAgentInfo) { setEnabled (false); return; } mUi.aliasComboBox->clear(); mUi.aliasComboBox->addItems (mUserAgentInfo->userAgentAliasList()); mUi.aliasComboBox->insertItem (0, QString()); mUi.aliasComboBox->model()->sort (0); mUi.aliasComboBox->setCurrentIndex (0); UserAgentSiteNameValidator* validator = new UserAgentSiteNameValidator (this); mUi.siteLineEdit->setValidator (validator); mUi.siteLineEdit->setFocus(); connect (mUi.siteLineEdit, &QLineEdit::textEdited, this, &UserAgentSelectorDlg::onHostNameChanged); +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) connect (mUi.aliasComboBox, QOverload::of(&QComboBox::activated), this, &UserAgentSelectorDlg::onAliasChanged); +#else + connect (mUi.aliasComboBox, &QComboBox::textActivated, + this, &UserAgentSelectorDlg::onAliasChanged); +#endif mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); } UserAgentSelectorDlg::~UserAgentSelectorDlg() { } void UserAgentSelectorDlg::onAliasChanged (const QString& text) { if (text.isEmpty()) mUi.identityLineEdit->setText (QString()); else mUi.identityLineEdit->setText (mUserAgentInfo->agentStr (text)); const bool enable = (!mUi.siteLineEdit->text().isEmpty() && !text.isEmpty()); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (enable); } void UserAgentSelectorDlg::onHostNameChanged (const QString& text) { const bool enable = (!text.isEmpty() && !mUi.aliasComboBox->currentText().isEmpty()); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (enable); } void UserAgentSelectorDlg::setSiteName (const QString& text) { mUi.siteLineEdit->setText (text); } void UserAgentSelectorDlg::setIdentity (const QString& text) { const int id = mUi.aliasComboBox->findText (text); if (id != -1) mUi.aliasComboBox->setCurrentIndex (id); mUi.identityLineEdit->setText (mUserAgentInfo->agentStr (mUi.aliasComboBox->currentText())); if (!mUi.siteLineEdit->isEnabled()) mUi.aliasComboBox->setFocus(); } QString UserAgentSelectorDlg::siteName() { return mUi.siteLineEdit->text().toLower(); } QString UserAgentSelectorDlg::identity() { return mUi.aliasComboBox->currentText(); } QString UserAgentSelectorDlg::alias() { return mUi.identityLineEdit->text(); } #include "useragentselectordlg.moc" diff --git a/src/widgets/delegateanimationhandler_p.h b/src/widgets/delegateanimationhandler_p.h index 41512554..184dfb5c 100644 --- a/src/widgets/delegateanimationhandler_p.h +++ b/src/widgets/delegateanimationhandler_p.h @@ -1,176 +1,177 @@ /* This file is part of the KDE project Copyright © 2007 Fredrik Höglund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef DELEGATEANIMATIONHANDLER_P_H #define DELEGATEANIMATIONHANDLER_P_H #include +#include #include #include #include #include #include #include #include class QAbstractItemView; namespace KIO { class CachedRendering : public QObject { Q_OBJECT public: CachedRendering(QStyle::State state, const QSize &size, const QModelIndex &validityIndex, qreal devicePixelRatio = 1.0); bool checkValidity(QStyle::State current) const { return state == current && valid; } QStyle::State state; QPixmap regular; QPixmap hover; bool valid; QPersistentModelIndex validityIndex; private Q_SLOTS: void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void modelReset(); }; class AnimationState { public: ~AnimationState(); AnimationState(const AnimationState &) = delete; AnimationState &operator=(const AnimationState &) = delete; //Progress of the mouse hovering animation qreal hoverProgress() const; //Progress of the icon fading animation qreal fadeProgress() const; //Angle of the painter, to paint the animation for a file job on an item qreal jobAnimationAngle() const; void setJobAnimation(bool value); bool hasJobAnimation() const; CachedRendering *cachedRendering() const { return renderCache; } //The previous render-cache is deleted, if there was one void setCachedRendering(CachedRendering *rendering) { delete renderCache; renderCache = rendering; } //Returns current cached rendering, and removes it from this state. //The caller has the ownership. CachedRendering *takeCachedRendering() { CachedRendering *ret = renderCache; renderCache = nullptr; return ret; } CachedRendering *cachedRenderingFadeFrom() const { return fadeFromRenderCache; } //The previous render-cache is deleted, if there was one void setCachedRenderingFadeFrom(CachedRendering *rendering) { delete fadeFromRenderCache; fadeFromRenderCache = rendering; if (rendering) { m_fadeProgress = 0; } else { m_fadeProgress = 1; } } private: explicit AnimationState(const QModelIndex &index); bool update(); QPersistentModelIndex index; QTimeLine::Direction direction; bool animating; bool jobAnimation; qreal progress; qreal m_fadeProgress; qreal m_jobAnimationAngle; - QTime time; - QTime creationTime; + QElapsedTimer time; + QElapsedTimer creationTime; CachedRendering *renderCache; CachedRendering *fadeFromRenderCache; friend class DelegateAnimationHandler; }; class DelegateAnimationHandler : public QObject { Q_OBJECT typedef QLinkedList AnimationList; typedef QMapIterator AnimationListsIterator; typedef QMutableMapIterator MutableAnimationListsIterator; public: explicit DelegateAnimationHandler(QObject *parent = nullptr); ~DelegateAnimationHandler(); AnimationState *animationState(const QStyleOption &option, const QModelIndex &index, const QAbstractItemView *view); void restartAnimation(AnimationState *state); void gotNewIcon(const QModelIndex &index); private Q_SLOTS: void viewDeleted(QObject *view); void sequenceTimerTimeout(); private: void eventuallyStartIteration(const QModelIndex &index); AnimationState *findAnimationState(const QAbstractItemView *view, const QModelIndex &index) const; void addAnimationState(AnimationState *state, const QAbstractItemView *view); void startAnimation(AnimationState *state); int runAnimations(AnimationList *list, const QAbstractItemView *view); void timerEvent(QTimerEvent *event) override; void setSequenceIndex(int arg1); private: QMap animationLists; - QTime fadeInAddTime; + QElapsedTimer fadeInAddTime; QBasicTimer timer; //Icon sequence handling: QPersistentModelIndex sequenceModelIndex; QTimer iconSequenceTimer; int currentSequenceIndex; }; } #endif diff --git a/src/widgets/kbuildsycocaprogressdialog.cpp b/src/widgets/kbuildsycocaprogressdialog.cpp index eb3ac939..e4d64c79 100644 --- a/src/widgets/kbuildsycocaprogressdialog.cpp +++ b/src/widgets/kbuildsycocaprogressdialog.cpp @@ -1,80 +1,80 @@ /* This file is part of the KDE project Copyright (C) 2003 Waldo Bastian This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License 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 "kbuildsycocaprogressdialog.h" #include #include #include #include #include #include #include class KBuildSycocaProgressDialogPrivate { public: explicit KBuildSycocaProgressDialogPrivate(KBuildSycocaProgressDialog *parent) : m_parent(parent) { } KBuildSycocaProgressDialog * const m_parent; }; void KBuildSycocaProgressDialog::rebuildKSycoca(QWidget *parent) { KBuildSycocaProgressDialog dlg(parent, i18n("Updating System Configuration"), i18n("Updating system configuration.")); // FIXME HACK: kdelibs 4 doesn't evaluate mimeapps.list at query time; refresh // its cache as well. QDBusInterface kbuildsycoca4(QStringLiteral("org.kde.kded"), QStringLiteral("/kbuildsycoca"), QStringLiteral("org.kde.kbuildsycoca")); if (kbuildsycoca4.isValid()) { kbuildsycoca4.call(QDBus::NoBlock, QStringLiteral("recreate")); } else { QProcess::startDetached(QStringLiteral("kbuildsycoca4"), QStringList()); } QProcess *proc = new QProcess(&dlg); proc->start(QStringLiteral(KBUILDSYCOCA_EXENAME), QStringList()); - QObject::connect(proc, QOverload::of(&QProcess::finished), + QObject::connect(proc, QOverload::of(&QProcess::finished), &dlg, &QWidget::close); dlg.exec(); } KBuildSycocaProgressDialog::KBuildSycocaProgressDialog(QWidget *_parent, const QString &_caption, const QString &text) : QProgressDialog(_parent) , d(new KBuildSycocaProgressDialogPrivate(this)) { setWindowTitle(_caption); setModal(true); setLabelText(text); setRange(0, 0); setAutoClose(false); QDialogButtonBox* dialogButtonBox = new QDialogButtonBox(QDialogButtonBox::Cancel, this); setCancelButton(dialogButtonBox->button(QDialogButtonBox::Cancel)); } KBuildSycocaProgressDialog::~KBuildSycocaProgressDialog() { delete d; } #include "moc_kbuildsycocaprogressdialog.cpp" diff --git a/src/widgets/kopenwithdialog.cpp b/src/widgets/kopenwithdialog.cpp index 85a70e88..e41f2c9c 100644 --- a/src/widgets/kopenwithdialog.cpp +++ b/src/widgets/kopenwithdialog.cpp @@ -1,1158 +1,1163 @@ /* This file is part of the KDE libraries Copyright (C) 1997 Torben Weis Copyright (C) 1999 Dirk Mueller Portions copyright (C) 1999 Preston Brown Copyright (C) 2007 Pino Toscano This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kopenwithdialog.h" #include "kopenwithdialog_p.h" #include "kio_widgets_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include inline void writeEntry(KConfigGroup &group, const char *key, const KCompletion::CompletionMode &aValue, KConfigBase::WriteConfigFlags flags = KConfigBase::Normal) { group.writeEntry(key, int(aValue), flags); } namespace KDEPrivate { class AppNode { public: AppNode() : isDir(false), parent(nullptr), fetched(false) { } ~AppNode() { qDeleteAll(children); } AppNode(const AppNode &) = delete; AppNode &operator=(const AppNode &) = delete; QString icon; QString text; QString entryPath; QString exec; bool isDir; AppNode *parent; bool fetched; QList children; }; static bool AppNodeLessThan(KDEPrivate::AppNode *n1, KDEPrivate::AppNode *n2) { if (n1->isDir) { if (n2->isDir) { return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0; } else { return true; } } else { if (n2->isDir) { return false; } else { return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0; } } } } class KApplicationModelPrivate { public: explicit KApplicationModelPrivate(KApplicationModel *qq) : q(qq), root(new KDEPrivate::AppNode()) { } ~KApplicationModelPrivate() { delete root; } void fillNode(const QString &entryPath, KDEPrivate::AppNode *node); KApplicationModel * const q; KDEPrivate::AppNode *root; }; void KApplicationModelPrivate::fillNode(const QString &_entryPath, KDEPrivate::AppNode *node) { KServiceGroup::Ptr root = KServiceGroup::group(_entryPath); if (!root || !root->isValid()) { return; } const KServiceGroup::List list = root->entries(); for (KServiceGroup::List::ConstIterator it = list.begin(); it != list.end(); ++it) { QString icon; QString text; QString entryPath; QString exec; bool isDir = false; const KSycocaEntry::Ptr p = (*it); if (p->isType(KST_KService)) { const KService::Ptr service(static_cast(p.data())); if (service->noDisplay()) { continue; } icon = service->icon(); text = service->name(); exec = service->exec(); entryPath = service->entryPath(); } else if (p->isType(KST_KServiceGroup)) { const KServiceGroup::Ptr serviceGroup(static_cast(p.data())); if (serviceGroup->noDisplay() || serviceGroup->childCount() == 0) { continue; } icon = serviceGroup->icon(); text = serviceGroup->caption(); entryPath = serviceGroup->entryPath(); isDir = true; } else { qCWarning(KIO_WIDGETS) << "KServiceGroup: Unexpected object in list!"; continue; } KDEPrivate::AppNode *newnode = new KDEPrivate::AppNode(); newnode->icon = icon; newnode->text = text; newnode->entryPath = entryPath; newnode->exec = exec; newnode->isDir = isDir; newnode->parent = node; node->children.append(newnode); } std::stable_sort(node->children.begin(), node->children.end(), KDEPrivate::AppNodeLessThan); } KApplicationModel::KApplicationModel(QObject *parent) : QAbstractItemModel(parent), d(new KApplicationModelPrivate(this)) { d->fillNode(QString(), d->root); const int nRows = rowCount(); for (int i = 0; i < nRows; i++) { fetchAll(index(i, 0)); } } KApplicationModel::~KApplicationModel() { delete d; } bool KApplicationModel::canFetchMore(const QModelIndex &parent) const { if (!parent.isValid()) { return false; } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); return node->isDir && !node->fetched; } int KApplicationModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return 1; } QVariant KApplicationModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: return node->text; case Qt::DecorationRole: if (!node->icon.isEmpty()) { return QIcon::fromTheme(node->icon); } break; default: ; } return QVariant(); } void KApplicationModel::fetchMore(const QModelIndex &parent) { if (!parent.isValid()) { return; } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); if (!node->isDir) { return; } emit layoutAboutToBeChanged(); d->fillNode(node->entryPath, node); node->fetched = true; emit layoutChanged(); } void KApplicationModel::fetchAll(const QModelIndex &parent) { if (!parent.isValid() || !canFetchMore(parent)) { return; } fetchMore(parent); int childCount = rowCount(parent); for (int i = 0; i < childCount; i++) { const QModelIndex &child = index(i, 0, parent); // Recursively call the function for each child node. fetchAll(child); } } bool KApplicationModel::hasChildren(const QModelIndex &parent) const { if (!parent.isValid()) { return true; } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); return node->isDir; } QVariant KApplicationModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || section != 0) { return QVariant(); } switch (role) { case Qt::DisplayRole: return i18n("Known Applications"); default: return QVariant(); } } QModelIndex KApplicationModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) { return QModelIndex(); } KDEPrivate::AppNode *node = d->root; if (parent.isValid()) { node = static_cast(parent.internalPointer()); } if (row >= node->children.count()) { return QModelIndex(); } else { return createIndex(row, 0, node->children.at(row)); } } QModelIndex KApplicationModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); if (node->parent->parent) { int id = node->parent->parent->children.indexOf(node->parent); if (id >= 0 && id < node->parent->parent->children.count()) { return createIndex(id, 0, node->parent); } else { return QModelIndex(); } } else { return QModelIndex(); } } int KApplicationModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return d->root->children.count(); } KDEPrivate::AppNode *node = static_cast(parent.internalPointer()); return node->children.count(); } QString KApplicationModel::entryPathFor(const QModelIndex &index) const { if (!index.isValid()) { return QString(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); return node->entryPath; } QString KApplicationModel::execFor(const QModelIndex &index) const { if (!index.isValid()) { return QString(); } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); return node->exec; } bool KApplicationModel::isDirectory(const QModelIndex &index) const { if (!index.isValid()) { return false; } KDEPrivate::AppNode *node = static_cast(index.internalPointer()); return node->isDir; } QTreeViewProxyFilter::QTreeViewProxyFilter(QObject *parent) : QSortFilterProxyModel(parent) { } bool QTreeViewProxyFilter::filterAcceptsRow(int sourceRow, const QModelIndex &parent) const { QModelIndex index = sourceModel()->index(sourceRow, 0, parent); if (!index.isValid()) { return false; } // Match the regexp only on leaf nodes if (!sourceModel()->hasChildren(index) && index.data().toString().contains(filterRegExp())) { return true; } return false; } class KApplicationViewPrivate { public: KApplicationViewPrivate() : appModel(nullptr), m_proxyModel(nullptr) { } KApplicationModel *appModel; QSortFilterProxyModel *m_proxyModel; }; KApplicationView::KApplicationView(QWidget *parent) : QTreeView(parent), d(new KApplicationViewPrivate) { setHeaderHidden(true); } KApplicationView::~KApplicationView() { delete d; } void KApplicationView::setModels(KApplicationModel *model, QSortFilterProxyModel *proxyModel) { if (d->appModel) { disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &KApplicationView::slotSelectionChanged); } QTreeView::setModel(proxyModel); // Here we set the proxy model d->m_proxyModel = proxyModel; // Also store it in a member property to avoid many casts later d->appModel = model; if (d->appModel) { connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &KApplicationView::slotSelectionChanged); } } QSortFilterProxyModel* KApplicationView::proxyModel() { return d->m_proxyModel; } bool KApplicationView::isDirSel() const { if (d->appModel) { QModelIndex index = selectionModel()->currentIndex(); index = d->m_proxyModel->mapToSource(index); return d->appModel->isDirectory(index); } return false; } void KApplicationView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { QTreeView::currentChanged(current, previous); if (d->appModel) { QModelIndex sourceCurrent = d->m_proxyModel->mapToSource(current); if(!d->appModel->isDirectory(sourceCurrent)) { QString exec = d->appModel->execFor(sourceCurrent); if (!exec.isEmpty()) { emit highlighted(d->appModel->entryPathFor(sourceCurrent), exec); } } } } void KApplicationView::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(deselected) QItemSelection sourceSelected = d->m_proxyModel->mapSelectionToSource(selected); const QModelIndexList indexes = sourceSelected.indexes(); if (indexes.count() == 1) { QString exec = d->appModel->execFor(indexes.at(0)); emit this->selected(d->appModel->entryPathFor(indexes.at(0)), exec); } } /*************************************************************** * * KOpenWithDialog * ***************************************************************/ class KOpenWithDialogPrivate { public: explicit KOpenWithDialogPrivate(KOpenWithDialog *qq) : q(qq), saveNewApps(false) { } KOpenWithDialog * const q; /** * Determine mime type from URLs */ void setMimeType(const QList &_urls); void addToMimeAppsList(const QString &serviceId); /** * Create a dialog that asks for a application to open a given * URL(s) with. * * @param text appears as a label on top of the entry box. * @param value is the initial value of the line */ void init(const QString &text, const QString &value); /** * Called by checkAccept() in order to save the history of the combobox */ void saveComboboxHistory(); /** * Process the choices made by the user, and return true if everything is OK. * Called by KOpenWithDialog::accept(), i.e. when clicking on OK or typing Return. */ bool checkAccept(); // slots void _k_slotDbClick(); void _k_slotFileSelected(); bool saveNewApps; bool m_terminaldirty; KService::Ptr curService; KApplicationView *view; KUrlRequester *edit; QString m_command; QLabel *label; QString qMimeType; QString qMimeTypeComment; QCheckBox *terminal; QCheckBox *remember; QCheckBox *nocloseonexit; KService::Ptr m_pService; QDialogButtonBox *buttonBox; }; KOpenWithDialog::KOpenWithDialog(const QList &_urls, QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); setWindowTitle(i18n("Open With")); QString text; if (_urls.count() == 1) { text = i18n("Select the program that should be used to open %1. " "If the program is not listed, enter the name or click " "the browse button.", _urls.first().fileName().toHtmlEscaped()); } else // Should never happen ?? { text = i18n("Choose the name of the program with which to open the selected files."); } d->setMimeType(_urls); d->init(text, QString()); } KOpenWithDialog::KOpenWithDialog(const QList &_urls, const QString &_text, const QString &_value, QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); QString text = _text; if (text.isEmpty() && !_urls.isEmpty()) { if (_urls.count() == 1) { const QString fileName = KStringHandler::csqueeze(_urls.first().fileName()); text = i18n("Select the program you want to use to open the file
%1
", fileName.toHtmlEscaped()); } else { text = i18np("Select the program you want to use to open the file.", "Select the program you want to use to open the %1 files.", _urls.count()); } } setWindowTitle(i18n("Choose Application")); d->setMimeType(_urls); d->init(text, _value); } KOpenWithDialog::KOpenWithDialog(const QString &mimeType, const QString &value, QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); setWindowTitle(i18n("Choose Application for %1", mimeType)); QString text = i18n("Select the program for the file type: %1. " "If the program is not listed, enter the name or click " "the browse button.", mimeType); d->qMimeType = mimeType; QMimeDatabase db; d->qMimeTypeComment = db.mimeTypeForName(mimeType).comment(); d->init(text, value); if (d->remember) { d->remember->hide(); } } KOpenWithDialog::KOpenWithDialog(QWidget *parent) : QDialog(parent), d(new KOpenWithDialogPrivate(this)) { setObjectName(QStringLiteral("openwith")); setModal(true); setWindowTitle(i18n("Choose Application")); QString text = i18n("Select a program. " "If the program is not listed, enter the name or click " "the browse button."); d->qMimeType.clear(); d->init(text, QString()); } void KOpenWithDialogPrivate::setMimeType(const QList &_urls) { if (_urls.count() == 1) { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(_urls.first()); qMimeType = mime.name(); if (mime.isDefault()) { qMimeType.clear(); } else { qMimeTypeComment = mime.comment(); } } else { qMimeType.clear(); } } void KOpenWithDialogPrivate::init(const QString &_text, const QString &_value) { bool bReadOnly = !KAuthorized::authorize(QStringLiteral("shell_access")); m_terminaldirty = false; view = nullptr; m_pService = nullptr; curService = nullptr; QBoxLayout *topLayout = new QVBoxLayout; q->setLayout(topLayout); label = new QLabel(_text, q); label->setWordWrap(true); topLayout->addWidget(label); if (!bReadOnly) { // init the history combo and insert it into the URL-Requester KHistoryComboBox *combo = new KHistoryComboBox(); combo->setToolTip(i18n("Type to filter the applications below, or specify the name of a command.\nPress down arrow to navigate the results.")); KLineEdit *lineEdit = new KLineEdit(q); lineEdit->setClearButtonEnabled(true); combo->setLineEdit(lineEdit); combo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); combo->setDuplicatesEnabled(false); KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("Open-with settings")); int max = cg.readEntry("Maximum history", 15); combo->setMaxCount(max); int mode = cg.readEntry("CompletionMode", int(KCompletion::CompletionNone)); combo->setCompletionMode(static_cast(mode)); const QStringList list = cg.readEntry("History", QStringList()); combo->setHistoryItems(list, true); edit = new KUrlRequester(combo, q); edit->installEventFilter(q); } else { edit = new KUrlRequester(q); edit->lineEdit()->setReadOnly(true); edit->button()->hide(); } edit->setText(_value); edit->setWhatsThis(i18n( "Following the command, you can have several place holders which will be replaced " "with the actual values when the actual program is run:\n" "%f - a single file name\n" "%F - a list of files; use for applications that can open several local files at once\n" "%u - a single URL\n" "%U - a list of URLs\n" "%d - the directory of the file to open\n" "%D - a list of directories\n" "%i - the icon\n" "%m - the mini-icon\n" "%c - the comment")); topLayout->addWidget(edit); if (edit->comboBox()) { KUrlCompletion *comp = new KUrlCompletion(KUrlCompletion::ExeCompletion); edit->comboBox()->setCompletionObject(comp); edit->comboBox()->setAutoDeleteCompletionObject(true); } QObject::connect(edit, &KUrlRequester::textChanged, q, &KOpenWithDialog::slotTextChanged); QObject::connect(edit, SIGNAL(urlSelected(QUrl)), q, SLOT(_k_slotFileSelected())); QTreeViewProxyFilter *proxyModel = new QTreeViewProxyFilter(view); KApplicationModel *appModel = new KApplicationModel(proxyModel); proxyModel->setSourceModel(appModel); proxyModel->setFilterKeyColumn(0); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); proxyModel->setRecursiveFilteringEnabled(true); view = new KApplicationView(q); view->setModels(appModel, proxyModel); topLayout->addWidget(view); topLayout->setStretchFactor(view, 1); QObject::connect(view, &KApplicationView::selected, q, &KOpenWithDialog::slotSelected); QObject::connect(view, &KApplicationView::highlighted, q, &KOpenWithDialog::slotHighlighted); QObject::connect(view, SIGNAL(doubleClicked(QModelIndex)), q, SLOT(_k_slotDbClick())); if (!qMimeType.isNull()) { remember = new QCheckBox(i18n("&Remember application association for all files of type\n\"%1\" (%2)", qMimeTypeComment, qMimeType)); // remember->setChecked(true); topLayout->addWidget(remember); } else { remember = nullptr; } //Advanced options KCollapsibleGroupBox *dialogExtension = new KCollapsibleGroupBox(q); dialogExtension->setTitle(i18n("Terminal options")); QVBoxLayout *dialogExtensionLayout = new QVBoxLayout; dialogExtensionLayout->setContentsMargins(0, 0, 0, 0); terminal = new QCheckBox(i18n("Run in &terminal"), q); if (bReadOnly) { terminal->hide(); } QObject::connect(terminal, &QAbstractButton::toggled, q, &KOpenWithDialog::slotTerminalToggled); dialogExtensionLayout->addWidget(terminal); QStyleOptionButton checkBoxOption; checkBoxOption.initFrom(terminal); int checkBoxIndentation = terminal->style()->pixelMetric(QStyle::PM_IndicatorWidth, &checkBoxOption, terminal); checkBoxIndentation += terminal->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &checkBoxOption, terminal); QBoxLayout *nocloseonexitLayout = new QHBoxLayout(); nocloseonexitLayout->setContentsMargins(0, 0, 0, 0); QSpacerItem *spacer = new QSpacerItem(checkBoxIndentation, 0, QSizePolicy::Fixed, QSizePolicy::Minimum); nocloseonexitLayout->addItem(spacer); nocloseonexit = new QCheckBox(i18n("&Do not close when command exits"), q); nocloseonexit->setChecked(false); nocloseonexit->setDisabled(true); // check to see if we use konsole if not disable the nocloseonexit // because we don't know how to do this on other terminal applications KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); if (bReadOnly || preferredTerminal != QLatin1String("konsole")) { nocloseonexit->hide(); } nocloseonexitLayout->addWidget(nocloseonexit); dialogExtensionLayout->addLayout(nocloseonexitLayout); dialogExtension->setLayout(dialogExtensionLayout); topLayout->addWidget(dialogExtension); buttonBox = new QDialogButtonBox(q); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); q->connect(buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept); q->connect(buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); topLayout->addWidget(buttonBox); q->setMinimumSize(q->minimumSizeHint()); //edit->setText( _value ); // The resize is what caused "can't click on items before clicking on Name header" in previous versions. // Probably due to the resizeEvent handler using width(). +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) q->resize( q->minimumWidth(), 0.6*QApplication::desktop()->availableGeometry().height()); +#else + q->resize( q->minimumWidth(), 0.6 * q->screen()->availableGeometry().height()); +#endif edit->setFocus(); q->slotTextChanged(); } // ---------------------------------------------------------------------- KOpenWithDialog::~KOpenWithDialog() { delete d; } // ---------------------------------------------------------------------- void KOpenWithDialog::slotSelected(const QString & /*_name*/, const QString &_exec) { d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!_exec.isEmpty()); } // ---------------------------------------------------------------------- void KOpenWithDialog::slotHighlighted(const QString &entryPath, const QString &) { d->curService = KService::serviceByDesktopPath(entryPath); if (d->curService && !d->m_terminaldirty) { // ### indicate that default value was restored d->terminal->setChecked(d->curService->terminal()); QString terminalOptions = d->curService->terminalOptions(); d->nocloseonexit->setChecked((terminalOptions.contains(QLatin1String("--noclose")))); d->m_terminaldirty = false; // slotTerminalToggled changed it } } // ---------------------------------------------------------------------- void KOpenWithDialog::slotTextChanged() { // Forget about the service only when the selection is empty // otherwise changing text but hitting the same result clears curService bool selectionEmpty = !d->view->currentIndex().isValid(); if (d->curService && selectionEmpty) { d->curService = nullptr; } d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!d->edit->text().isEmpty() || d->curService); //Update the filter regexp with the new text in the lineedit d->view->proxyModel()->setFilterFixedString(d->edit->text()); //Expand all the nodes when the search string is 3 characters long //If the search string doesn't match anything there will be no nodes to expand if (d->edit->text().size() > 2) { d->view->expandAll(); QAbstractItemModel *model = d->view->model(); //Automatically select the first result (first leaf node) when the filter has match QModelIndex leafNodeIdx = model->index(0, 0); while (model->hasChildren(leafNodeIdx)) { leafNodeIdx = model->index(0, 0, leafNodeIdx); } d->view->setCurrentIndex(leafNodeIdx); } else { d->view->collapseAll(); d->view->setCurrentIndex(d->view->rootIndex()); // Unset and deselect all the elements d->curService = nullptr; } } // ---------------------------------------------------------------------- void KOpenWithDialog::slotTerminalToggled(bool) { // ### indicate that default value was overridden d->m_terminaldirty = true; d->nocloseonexit->setDisabled(!d->terminal->isChecked()); } // ---------------------------------------------------------------------- void KOpenWithDialogPrivate::_k_slotDbClick() { // check if a directory is selected if (view->isDirSel()) { return; } q->accept(); } void KOpenWithDialogPrivate::_k_slotFileSelected() { // quote the path to avoid unescaped whitespace, backslashes, etc. edit->setText(KShell::quoteArg(edit->text())); } void KOpenWithDialog::setSaveNewApplications(bool b) { d->saveNewApps = b; } static QString simplifiedExecLineFromService(const QString &fullExec) { QString exec = fullExec; exec.remove(QStringLiteral("%u"), Qt::CaseInsensitive); exec.remove(QStringLiteral("%f"), Qt::CaseInsensitive); exec.remove(QStringLiteral("-caption %c")); exec.remove(QStringLiteral("-caption \"%c\"")); exec.remove(QStringLiteral("%i")); exec.remove(QStringLiteral("%m")); return exec.simplified(); } void KOpenWithDialogPrivate::addToMimeAppsList(const QString &serviceId /*menu id or storage id*/) { KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); // Save the default application according to mime-apps-spec 1.0 KConfigGroup defaultApp(profile, "Default Applications"); defaultApp.writeXdgListEntry(qMimeType, QStringList(serviceId)); KConfigGroup addedApps(profile, "Added Associations"); QStringList apps = addedApps.readXdgListEntry(qMimeType); apps.removeAll(serviceId); apps.prepend(serviceId); // make it the preferred app addedApps.writeXdgListEntry(qMimeType, apps); profile->sync(); // Also make sure the "auto embed" setting for this mimetype is off KSharedConfig::Ptr fileTypesConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals); fileTypesConfig->group("EmbedSettings").writeEntry(QStringLiteral("embed-") + qMimeType, false); fileTypesConfig->sync(); // qDebug() << "rebuilding ksycoca..."; // kbuildsycoca is the one reading mimeapps.list, so we need to run it now KBuildSycocaProgressDialog::rebuildKSycoca(q); // could be nullptr if the user canceled the dialog... m_pService = KService::serviceByStorageId(serviceId); } bool KOpenWithDialogPrivate::checkAccept() { const QString typedExec(edit->text()); QString fullExec(typedExec); QString serviceName; QString initialServiceName; QString preferredTerminal; QString configPath; QString serviceExec; m_pService = curService; if (!m_pService) { // No service selected - check the command line // Find out the name of the service from the command line, removing args and paths serviceName = KIO::DesktopExecParser::executableName(typedExec); if (serviceName.isEmpty()) { KMessageBox::error(q, i18n("Could not extract executable name from '%1', please type a valid program name.", serviceName)); return false; } initialServiceName = serviceName; // Also remember the executableName with a path, if any, for the // check that the executable exists. // qDebug() << "initialServiceName=" << initialServiceName; int i = 1; // We have app, app-2, app-3... Looks better for the user. bool ok = false; // Check if there's already a service by that name, with the same Exec line do { // qDebug() << "looking for service" << serviceName; KService::Ptr serv = KService::serviceByDesktopName(serviceName); ok = !serv; // ok if no such service yet // also ok if we find the exact same service (well, "kwrite" == "kwrite %U") if (serv && !serv->noDisplay() /* #297720 */) { if (serv->isApplication()) { /*// qDebug() << "typedExec=" << typedExec << "serv->exec=" << serv->exec() << "simplifiedExecLineFromService=" << simplifiedExecLineFromService(fullExec);*/ serviceExec = simplifiedExecLineFromService(serv->exec()); if (typedExec == serviceExec) { ok = true; m_pService = serv; // qDebug() << "OK, found identical service: " << serv->entryPath(); } else { // qDebug() << "Exec line differs, service says:" << serviceExec; configPath = serv->entryPath(); serviceExec = serv->exec(); } } else { // qDebug() << "Found, but not an application:" << serv->entryPath(); } } if (!ok) { // service was found, but it was different -> keep looking ++i; serviceName = initialServiceName + QLatin1Char('-') + QString::number(i); } } while (!ok); } if (m_pService) { // Existing service selected serviceName = m_pService->name(); initialServiceName = serviceName; fullExec = m_pService->exec(); } else { const QString binaryName = KIO::DesktopExecParser::executablePath(typedExec); // qDebug() << "binaryName=" << binaryName; // Ensure that the typed binary name actually exists (#81190) if (QStandardPaths::findExecutable(binaryName).isEmpty()) { KMessageBox::error(q, i18n("'%1' not found, please type a valid program name.", binaryName)); return false; } } if (terminal->isChecked()) { KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); m_command = preferredTerminal; // only add --noclose when we are sure it is konsole we're using if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { m_command += QStringLiteral(" --noclose"); } m_command += QLatin1String(" -e ") + edit->text(); // qDebug() << "Setting m_command to" << m_command; } if (m_pService && terminal->isChecked() != m_pService->terminal()) { m_pService = nullptr; // It's not exactly this service we're running } const bool bRemember = remember && remember->isChecked(); // qDebug() << "bRemember=" << bRemember << "service found=" << m_pService; if (m_pService) { if (bRemember) { // Associate this app with qMimeType in mimeapps.list Q_ASSERT(!qMimeType.isEmpty()); // we don't show the remember checkbox otherwise addToMimeAppsList(m_pService->storageId()); } } else { const bool createDesktopFile = bRemember || saveNewApps; if (!createDesktopFile) { // Create temp service if (configPath.isEmpty()) { m_pService = new KService(initialServiceName, fullExec, QString()); } else { if (!typedExec.contains(QLatin1String("%u"), Qt::CaseInsensitive) && !typedExec.contains(QLatin1String("%f"), Qt::CaseInsensitive)) { int index = serviceExec.indexOf(QLatin1String("%u"), 0, Qt::CaseInsensitive); if (index == -1) { index = serviceExec.indexOf(QLatin1String("%f"), 0, Qt::CaseInsensitive); } if (index > -1) { fullExec += QLatin1Char(' ') + serviceExec.midRef(index, 2); } } // qDebug() << "Creating service with Exec=" << fullExec; m_pService = new KService(configPath); m_pService->setExec(fullExec); } if (terminal->isChecked()) { m_pService->setTerminal(true); // only add --noclose when we are sure it is konsole we're using if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { m_pService->setTerminalOptions(QStringLiteral("--noclose")); } } } else { // If we got here, we can't seem to find a service for what they wanted. Create one. QString menuId; #ifdef Q_OS_WIN32 // on windows, do not use the complete path, but only the default name. serviceName = QFileInfo(serviceName).fileName(); #endif QString newPath = KService::newServicePath(false /* ignored argument */, serviceName, &menuId); // qDebug() << "Creating new service" << serviceName << "(" << newPath << ")" << "menuId=" << menuId; KDesktopFile desktopFile(newPath); KConfigGroup cg = desktopFile.desktopGroup(); cg.writeEntry("Type", "Application"); cg.writeEntry("Name", initialServiceName); cg.writeEntry("Exec", fullExec); cg.writeEntry("NoDisplay", true); // don't make it appear in the K menu if (terminal->isChecked()) { cg.writeEntry("Terminal", true); // only add --noclose when we are sure it is konsole we're using if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { cg.writeEntry("TerminalOptions", "--noclose"); } } if (!qMimeType.isEmpty()) { cg.writeXdgListEntry("MimeType", QStringList() << qMimeType); } cg.sync(); if (!qMimeType.isEmpty()) { addToMimeAppsList(menuId); } else { m_pService = new KService(newPath); } } } saveComboboxHistory(); return true; } bool KOpenWithDialog::eventFilter(QObject *object, QEvent *event) { // Detect DownArrow to navigate the results in the QTreeView if (object == d->edit && event->type() == QEvent::ShortcutOverride) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Down) { KHistoryComboBox *combo = static_cast(d->edit->comboBox()); // FIXME: Disable arrow down in CompletionPopup and CompletionPopupAuto only when the dropdown list is shown. // When popup completion mode is used the down arrow is used to navigate the dropdown list of results if (combo->completionMode() != KCompletion::CompletionPopup && combo->completionMode() != KCompletion::CompletionPopupAuto) { QModelIndex leafNodeIdx = d->view->model()->index(0, 0); // Check if we have at least one result or the focus is passed to the empty QTreeView if (d->view->model()->hasChildren(leafNodeIdx)) { d->view->setFocus(Qt::OtherFocusReason); QApplication::sendEvent(d->view, keyEvent); return true; } } } } return QDialog::eventFilter(object, event); } void KOpenWithDialog::accept() { if (d->checkAccept()) { QDialog::accept(); } } QString KOpenWithDialog::text() const { if (!d->m_command.isEmpty()) { return d->m_command; } else { return d->edit->text(); } } void KOpenWithDialog::hideNoCloseOnExit() { // uncheck the checkbox because the value could be used when "Run in Terminal" is selected d->nocloseonexit->setChecked(false); d->nocloseonexit->hide(); } void KOpenWithDialog::hideRunInTerminal() { d->terminal->hide(); hideNoCloseOnExit(); } KService::Ptr KOpenWithDialog::service() const { return d->m_pService; } void KOpenWithDialogPrivate::saveComboboxHistory() { KHistoryComboBox *combo = static_cast(edit->comboBox()); if (combo) { combo->addToHistory(edit->text()); KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("Open-with settings")); cg.writeEntry("History", combo->historyItems()); writeEntry(cg, "CompletionMode", combo->completionMode()); // don't store the completion-list, as it contains all of KUrlCompletion's // executables cg.sync(); } } #include "moc_kopenwithdialog.cpp" #include "moc_kopenwithdialog_p.cpp" diff --git a/src/widgets/krun.cpp b/src/widgets/krun.cpp index a37fbda6..73bc41ba 100644 --- a/src/widgets/krun.cpp +++ b/src/widgets/krun.cpp @@ -1,1751 +1,1752 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Torben Weis Copyright (C) 2006 David Faure Copyright (C) 2009 Michael Pyne This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "krun.h" #include "krun_p.h" #include // HAVE_X11 #include "kio_widgets_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include "kio/job.h" #include "kio/global.h" #include "kio/scheduler.h" #include "kopenwithdialog.h" #include "krecentdocument.h" #include "kdesktopfileactions.h" #include "executablefileopendialog_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 #include #endif #include KRun::KRunPrivate::KRunPrivate(KRun *parent) : q(parent), m_showingDialog(false) { } void KRun::KRunPrivate::startTimer() { m_timer->start(0); } // --------------------------------------------------------------------------- static QString schemeHandler(const QString &protocol) { // We have up to two sources of data, for protocols not handled by kioslaves (so called "helper") : // 1) the exec line of the .protocol file, if there's one // 2) the application associated with x-scheme-handler/ if there's one // If both exist, then: // A) if the .protocol file says "launch an application", then the new-style handler-app has priority // B) but if the .protocol file is for a kioslave (e.g. kio_http) then this has priority over // firefox or chromium saying x-scheme-handler/http. Gnome people want to send all HTTP urls // to a webbrowser, but we want mimetype-determination-in-calling-application by default // (the user can configure a BrowserApplication though) const KService::Ptr service = KMimeTypeTrader::self()->preferredService(QLatin1String("x-scheme-handler/") + protocol); if (service) { return service->exec(); // for helper protocols, the handler app has priority over the hardcoded one (see A above) } Q_ASSERT(KProtocolInfo::isHelperProtocol(protocol)); return KProtocolInfo::exec(protocol); } static bool checkNeedPortalSupport() { return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, QLatin1String("flatpak-info")).isEmpty() || qEnvironmentVariableIsSet("SNAP"); } static qint64 runProcessRunner(KProcess *p, const QString &executable, const KStartupInfoId &id, QWidget *widget) { auto *processRunner = new KProcessRunner(p, executable, id); QObject *receiver = widget ? static_cast(widget) : static_cast(qApp); QObject::connect(processRunner, &KProcessRunner::error, receiver, [widget](const QString &errorString) { QEventLoopLocker locker; KMessageBox::sorry(widget, errorString); }); return processRunner->pid(); } // --------------------------------------------------------------------------- // Helper function that returns whether a file has the execute bit set or not. static bool hasExecuteBit(const QString &fileName) { QFileInfo file(fileName); return file.isExecutable(); } bool KRun::isExecutableFile(const QUrl &url, const QString &mimetype) { if (!url.isLocalFile()) { return false; } // While isExecutable performs similar check to this one, some users depend on // this method not returning true for application/x-desktop QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName(mimetype); if (!mimeType.inherits(QStringLiteral("application/x-executable")) #ifdef Q_OS_WIN && !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable")) #endif && !mimeType.inherits(QStringLiteral("application/x-executable-script")) && !mimeType.inherits(QStringLiteral("application/x-sharedlib"))) { return false; } if (!hasExecuteBit(url.toLocalFile())) { return false; } return true; } void KRun::handleInitError(int kioErrorCode, const QString &errorMsg) { Q_UNUSED(kioErrorCode); d->m_showingDialog = true; KMessageBox::error(d->m_window, errorMsg); d->m_showingDialog = false; } void KRun::handleError(KJob *job) { Q_ASSERT(job); if (job) { d->m_showingDialog = true; job->uiDelegate()->showErrorMessage(); d->m_showingDialog = false; } } // Simple QDialog that resizes the given text edit after being shown to more // or less fit the enclosed text. class SecureMessageDialog : public QDialog { Q_OBJECT public: SecureMessageDialog(QWidget *parent) : QDialog(parent), m_textEdit(nullptr) { } void setTextEdit(QPlainTextEdit *textEdit) { m_textEdit = textEdit; } protected: void showEvent(QShowEvent *e) override { if (e->spontaneous()) { return; } // Now that we're shown, use our width to calculate a good // bounding box for the text, and resize m_textEdit appropriately. QDialog::showEvent(e); if (!m_textEdit) { return; } QSize fudge(20, 24); // About what it sounds like :-/ // Form rect with a lot of height for bounding. Use no more than // 5 lines. QRect curRect(m_textEdit->rect()); QFontMetrics metrics(fontMetrics()); curRect.setHeight(5 * metrics.lineSpacing()); curRect.setWidth(qMax(curRect.width(), 300)); // At least 300 pixels ok? QString text(m_textEdit->toPlainText()); curRect = metrics.boundingRect(curRect, Qt::TextWordWrap | Qt::TextSingleLine, text); // Scroll bars interfere. If we don't think there's enough room, enable // the vertical scrollbar however. m_textEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); if (curRect.height() < m_textEdit->height()) { // then we've got room m_textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_textEdit->setMaximumHeight(curRect.height() + fudge.height()); } m_textEdit->setMinimumSize(curRect.size() + fudge); m_textEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); } private: QPlainTextEdit *m_textEdit; }; // Shows confirmation dialog whether an untrusted program should be run // or not, since it may be potentially dangerous. static int showUntrustedProgramWarning(const QString &programName, QWidget *window) { SecureMessageDialog *baseDialog = new SecureMessageDialog(window); baseDialog->setWindowTitle(i18nc("Warning about executing unknown program", "Warning")); QVBoxLayout *topLayout = new QVBoxLayout; baseDialog->setLayout(topLayout); // Dialog will have explanatory text with a disabled lineedit with the // Exec= to make it visually distinct. QWidget *baseWidget = new QWidget(baseDialog); QHBoxLayout *mainLayout = new QHBoxLayout(baseWidget); QLabel *iconLabel = new QLabel(baseWidget); QPixmap warningIcon(KIconLoader::global()->loadIcon(QStringLiteral("dialog-warning"), KIconLoader::NoGroup, KIconLoader::SizeHuge)); mainLayout->addWidget(iconLabel); iconLabel->setPixmap(warningIcon); QVBoxLayout *contentLayout = new QVBoxLayout; QString warningMessage = i18nc("program name follows in a line edit below", "This will start the program:"); QLabel *message = new QLabel(warningMessage, baseWidget); contentLayout->addWidget(message); QPlainTextEdit *textEdit = new QPlainTextEdit(baseWidget); textEdit->setPlainText(programName); textEdit->setReadOnly(true); contentLayout->addWidget(textEdit); QLabel *footerLabel = new QLabel(i18n("If you do not trust this program, click Cancel")); contentLayout->addWidget(footerLabel); contentLayout->addStretch(0); // Don't allow the text edit to expand mainLayout->addLayout(contentLayout); topLayout->addWidget(baseWidget); baseDialog->setTextEdit(textEdit); QDialogButtonBox *buttonBox = new QDialogButtonBox(baseDialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::cont()); buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true); buttonBox->button(QDialogButtonBox::Cancel)->setFocus(); QObject::connect(buttonBox, &QDialogButtonBox::accepted, baseDialog, &QDialog::accept); QObject::connect(buttonBox, &QDialogButtonBox::rejected, baseDialog, &QDialog::reject); topLayout->addWidget(buttonBox); // Constrain maximum size. Minimum size set in // the dialog's show event. QSize screenSize = QApplication::desktop()->screen()->size(); baseDialog->resize(screenSize.width() / 4, 50); baseDialog->setMaximumHeight(screenSize.height() / 3); baseDialog->setMaximumWidth(screenSize.width() / 10 * 8); return baseDialog->exec(); } // Helper function that attempts to set execute bit for given file. static bool setExecuteBit(const QString &fileName, QString &errorString) { QFile file(fileName); // corresponds to owner on unix, which will have to do since if the user // isn't the owner we can't change perms anyways. if (!file.setPermissions(QFile::ExeUser | file.permissions())) { errorString = file.errorString(); qCWarning(KIO_WIDGETS) << "Unable to change permissions for" << fileName << errorString; return false; } return true; } #ifndef KIOWIDGETS_NO_DEPRECATED bool KRun::runUrl(const QUrl &url, const QString &mimetype, QWidget *window, bool tempFile, bool runExecutables, const QString &suggestedFileName, const QByteArray &asn) { RunFlags flags = tempFile ? KRun::DeleteTemporaryFiles : RunFlags(); if (runExecutables) { flags |= KRun::RunExecutables; } return runUrl(url, mimetype, window, flags, suggestedFileName, asn); } #endif // This is called by foundMimeType, since it knows the mimetype of the URL bool KRun::runUrl(const QUrl &u, const QString &_mimetype, QWidget *window, RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { const QMimeDatabase db; const bool runExecutables = flags.testFlag(KRun::RunExecutables); const bool tempFile = flags.testFlag(KRun::DeleteTemporaryFiles); bool noRun = false; bool noAuth = false; if (_mimetype == QLatin1String("inode/directory-locked")) { KMessageBox::error(window, i18n("Unable to enter %1.\nYou do not have access rights to this location.", u.toDisplayString().toHtmlEscaped())); return false; } else if (_mimetype == QLatin1String("application/x-desktop")) { if (u.isLocalFile() && runExecutables) { return KDesktopFileActions::runWithStartup(u, true, asn); } } else if (isExecutable(_mimetype)) { // Check whether file is executable script const QMimeType mime = db.mimeTypeForName(_mimetype); bool isTextFile = mime.inherits(QStringLiteral("text/plain")); // Only run local files if (u.isLocalFile() && runExecutables) { if (KAuthorized::authorize(QStringLiteral("shell_access"))) { bool canRun = true; bool isFileExecutable = hasExecuteBit(u.toLocalFile()); // For executables that aren't scripts and without execute bit, // show prompt asking user if he wants to run the program. if (!isFileExecutable && !isTextFile) { canRun = false; int result = showUntrustedProgramWarning(u.fileName(), window); if (result == QDialog::Accepted) { QString errorString; if (!setExecuteBit(u.toLocalFile(), errorString)) { KMessageBox::sorry( window, i18n("Unable to make file %1 executable.\n%2.", u.toLocalFile(), errorString) ); } else { canRun = true; } } } else if (!isFileExecutable && isTextFile) { // Don't try to run scripts without execute bit, instead // open them with default application canRun = false; } if (canRun) { return (KRun::runCommand(KShell::quoteArg(u.toLocalFile()), QString(), QString(), window, asn, u.adjusted(QUrl::RemoveFilename).toLocalFile())); // just execute the url as a command // ## TODO implement deleting the file if tempFile==true } } else { // Show no permission warning noAuth = true; } } else if (!isTextFile) { // Show warning for executables that aren't scripts noRun = true; } } if (noRun) { KMessageBox::sorry(window, i18n("The file %1 is an executable program. " "For safety it will not be started.", u.toDisplayString().toHtmlEscaped())); return false; } if (noAuth) { KMessageBox::error(window, i18n("You do not have permission to run %1.", u.toDisplayString().toHtmlEscaped())); return false; } QList lst; lst.append(u); KService::Ptr offer = KMimeTypeTrader::self()->preferredService(_mimetype); if (!offer) { #ifdef Q_OS_WIN // As KDE on windows doesn't know about the windows default applications offers will be empty in nearly all cases. // So we use QDesktopServices::openUrl to let windows decide how to open the file return QDesktopServices::openUrl(u); #else // Open-with dialog // TODO : pass the mimetype as a parameter, to show it (comment field) in the dialog ! // Hmm, in fact KOpenWithDialog::setServiceType already guesses the mimetype from the first URL of the list... return displayOpenWithDialog(lst, window, tempFile, suggestedFileName, asn); #endif } return KRun::runApplication(*offer, lst, window, flags, suggestedFileName, asn); } bool KRun::displayOpenWithDialog(const QList &lst, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) { KMessageBox::sorry(window, i18n("You are not authorized to select an application to open this file.")); return false; } #ifdef Q_OS_WIN KConfigGroup cfgGroup(KSharedConfig::openConfig(), QStringLiteral("KOpenWithDialog Settings")); if (cfgGroup.readEntry("Native", true)) { return KRun::KRunPrivate::displayNativeOpenWithDialog(lst, window, tempFiles, suggestedFileName, asn); } #endif KOpenWithDialog dialog(lst, QString(), QString(), window); dialog.setWindowModality(Qt::WindowModal); if (dialog.exec()) { KService::Ptr service = dialog.service(); if (!service) { //qDebug() << "No service set, running " << dialog.text(); service = KService::Ptr(new KService(QString() /*name*/, dialog.text(), QString() /*icon*/)); } const RunFlags flags = tempFiles ? KRun::DeleteTemporaryFiles : RunFlags(); return KRun::runApplication(*service, lst, window, flags, suggestedFileName, asn); } return false; } #ifndef KIOWIDGETS_NO_DEPRECATED void KRun::shellQuote(QString &_str) { // Credits to Walter, says Bernd G. :) if (_str.isEmpty()) { // Don't create an explicit empty parameter return; } const QChar q = QLatin1Char('\''); _str.replace(q, QLatin1String("'\\''")).prepend(q).append(q); } #endif QStringList KRun::processDesktopExec(const KService &_service, const QList &_urls, bool tempFiles, const QString &suggestedFileName) { KIO::DesktopExecParser parser(_service, _urls); parser.setUrlsAreTempFiles(tempFiles); parser.setSuggestedFileName(suggestedFileName); return parser.resultingArguments(); } #ifndef KIOWIDGETS_NO_DEPRECATED QString KRun::binaryName(const QString &execLine, bool removePath) { return removePath ? KIO::DesktopExecParser::executableName(execLine) : KIO::DesktopExecParser::executablePath(execLine); } #endif static qint64 runCommandInternal(KProcess *proc, const KService *service, const QString &executable, const QString &userVisibleName, const QString &iconName, QWidget *window, const QByteArray &asn) { if (window) { window = window->topLevelWidget(); } if (service && !service->entryPath().isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(service->entryPath())) { qCWarning(KIO_WIDGETS) << "No authorization to execute " << service->entryPath(); KMessageBox::sorry(window, i18n("You are not authorized to execute this file.")); delete proc; return 0; } QString bin = KIO::DesktopExecParser::executableName(executable); #if HAVE_X11 static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb"); if (isX11) { bool silent; QByteArray wmclass; KStartupInfoId id; bool startup_notify = (asn != "0" && KRun::checkStartupNotify(QString() /*unused*/, service, &silent, &wmclass)); if (startup_notify) { id.initId(asn); id.setupStartupEnv(); KStartupInfoData data; data.setHostname(); data.setBin(bin); if (!userVisibleName.isEmpty()) { data.setName(userVisibleName); } else if (service && !service->name().isEmpty()) { data.setName(service->name()); } data.setDescription(i18n("Launching %1", data.name())); if (!iconName.isEmpty()) { data.setIcon(iconName); } else if (service && !service->icon().isEmpty()) { data.setIcon(service->icon()); } if (!wmclass.isEmpty()) { data.setWMClass(wmclass); } if (silent) { data.setSilent(KStartupInfoData::Yes); } data.setDesktop(KWindowSystem::currentDesktop()); // QTBUG-59017 Calling winId() on an embedded widget will break interaction // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using // its parent window instead if (window && window->window()) { data.setLaunchedBy(window->window()->winId()); } if (service && !service->entryPath().isEmpty()) { data.setApplicationId(service->entryPath()); } KStartupInfo::sendStartup(id, data); } const qint64 pid = runProcessRunner(proc, executable, id, window); if (startup_notify && pid) { KStartupInfoData data; data.addPid(pid); KStartupInfo::sendChange(id, data); KStartupInfo::resetStartupEnv(); } return pid; } #else Q_UNUSED(userVisibleName); Q_UNUSED(iconName); #endif return runProcessRunner(proc, bin, KStartupInfoId(), window); } // This code is also used in klauncher. bool KRun::checkStartupNotify(const QString & /*binName*/, const KService *service, bool *silent_arg, QByteArray *wmclass_arg) { bool silent = false; QByteArray wmclass; if (service && service->property(QStringLiteral("StartupNotify")).isValid()) { silent = !service->property(QStringLiteral("StartupNotify")).toBool(); wmclass = service->property(QStringLiteral("StartupWMClass")).toString().toLatin1(); } else if (service && service->property(QStringLiteral("X-KDE-StartupNotify")).isValid()) { silent = !service->property(QStringLiteral("X-KDE-StartupNotify")).toBool(); wmclass = service->property(QStringLiteral("X-KDE-WMClass")).toString().toLatin1(); } else { // non-compliant app if (service) { if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant wmclass = "0"; // krazy:exclude=doublequote_chars } else { return false; // no startup notification at all } } else { #if 0 // Create startup notification even for apps for which there shouldn't be any, // just without any visual feedback. This will ensure they'll be positioned on the proper // virtual desktop, and will get user timestamp from the ASN ID. wmclass = '0'; silent = true; #else // That unfortunately doesn't work, when the launched non-compliant application // launches another one that is compliant and there is any delay inbetween (bnc:#343359) return false; #endif } } if (silent_arg) { *silent_arg = silent; } if (wmclass_arg) { *wmclass_arg = wmclass; } return true; } static qint64 runApplicationImpl(const KService &_service, const QList &_urls, QWidget *window, KRun::RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { QList urlsToRun = _urls; if ((_urls.count() > 1) && !_service.allowMultipleFiles()) { // We need to launch the application N times. That sucks. // We ignore the result for application 2 to N. // For the first file we launch the application in the // usual way. The reported result is based on this // application. QList::ConstIterator it = _urls.begin(); while (++it != _urls.end()) { QList singleUrl; singleUrl.append(*it); runApplicationImpl(_service, singleUrl, window, flags, suggestedFileName, QByteArray()); } urlsToRun.clear(); urlsToRun.append(_urls.first()); } KIO::DesktopExecParser execParser(_service, urlsToRun); execParser.setUrlsAreTempFiles(flags & KRun::DeleteTemporaryFiles); execParser.setSuggestedFileName(suggestedFileName); const QStringList args = execParser.resultingArguments(); if (args.isEmpty()) { KMessageBox::sorry(window, i18n("Error processing Exec field in %1", _service.entryPath())); return 0; } //qDebug() << "runTempService: KProcess args=" << args; KProcess * proc = new KProcess; *proc << args; enum DiscreteGpuCheck { NotChecked, Present, Absent }; static DiscreteGpuCheck s_gpuCheck = NotChecked; if (_service.runOnDiscreteGpu() && s_gpuCheck == NotChecked) { // Check whether we have a discrete gpu bool hasDiscreteGpu = false; QDBusInterface iface(QStringLiteral("org.kde.Solid.PowerManagement"), QStringLiteral("/org/kde/Solid/PowerManagement"), QStringLiteral("org.kde.Solid.PowerManagement"), QDBusConnection::sessionBus()); if (iface.isValid()) { QDBusReply reply = iface.call(QStringLiteral("hasDualGpu")); if (reply.isValid()) { hasDiscreteGpu = reply.value(); } } s_gpuCheck = hasDiscreteGpu ? Present : Absent; } if (_service.runOnDiscreteGpu() && s_gpuCheck == Present) { proc->setEnv(QStringLiteral("DRI_PRIME"), QStringLiteral("1")); } QString path(_service.path()); if (path.isEmpty() && !_urls.isEmpty() && _urls.first().isLocalFile()) { path = _urls.first().adjusted(QUrl::RemoveFilename).toLocalFile(); } proc->setWorkingDirectory(path); return runCommandInternal(proc, &_service, KIO::DesktopExecParser::executablePath(_service.exec()), _service.name(), _service.icon(), window, asn); } // WARNING: don't call this from DesktopExecParser, since klauncher uses that too... // TODO: make this async, see the job->exec() in there... static QList resolveURLs(const QList &_urls, const KService &_service) { // Check which protocols the application supports. // This can be a list of actual protocol names, or just KIO for KDE apps. QStringList appSupportedProtocols = KIO::DesktopExecParser::supportedProtocols(_service); QList urls(_urls); if (!appSupportedProtocols.contains(QLatin1String("KIO"))) { for (QList::Iterator it = urls.begin(); it != urls.end(); ++it) { const QUrl url = *it; bool supported = KIO::DesktopExecParser::isProtocolInSupportedList(url, appSupportedProtocols); //qDebug() << "Looking at url=" << url << " supported=" << supported; if (!supported && KProtocolInfo::protocolClass(url.scheme()) == QLatin1String(":local")) { // Maybe we can resolve to a local URL? KIO::StatJob *job = KIO::mostLocalUrl(url); if (job->exec()) { // ## nasty nested event loop! const QUrl localURL = job->mostLocalUrl(); if (localURL != url) { *it = localURL; //qDebug() << "Changed to" << localURL; } } } } } return urls; } // Helper function to make the given .desktop file executable by ensuring // that a #!/usr/bin/env xdg-open line is added if necessary and the file has // the +x bit set for the user. Returns false if either fails. static bool makeServiceFileExecutable(const QString &fileName, QString &errorString) { // Open the file and read the first two characters, check if it's // #!. If not, create a new file, prepend appropriate lines, and copy // over. QFile desktopFile(fileName); if (!desktopFile.open(QFile::ReadOnly)) { errorString = desktopFile.errorString(); qCWarning(KIO_WIDGETS) << "Error opening service" << fileName << errorString; return false; } QByteArray header = desktopFile.peek(2); // First two chars of file if (header.size() == 0) { errorString = desktopFile.errorString(); qCWarning(KIO_WIDGETS) << "Error inspecting service" << fileName << errorString; return false; // Some kind of error } if (header != "#!") { // Add header QSaveFile saveFile; saveFile.setFileName(fileName); if (!saveFile.open(QIODevice::WriteOnly)) { errorString = saveFile.errorString(); qCWarning(KIO_WIDGETS) << "Unable to open replacement file for" << fileName << errorString; return false; } QByteArray shebang("#!/usr/bin/env xdg-open\n"); if (saveFile.write(shebang) != shebang.size()) { errorString = saveFile.errorString(); qCWarning(KIO_WIDGETS) << "Error occurred adding header for" << fileName << errorString; saveFile.cancelWriting(); return false; } // Now copy the one into the other and then close and reopen desktopFile QByteArray desktopData(desktopFile.readAll()); if (desktopData.isEmpty()) { errorString = desktopFile.errorString(); qCWarning(KIO_WIDGETS) << "Unable to read service" << fileName << errorString; saveFile.cancelWriting(); return false; } if (saveFile.write(desktopData) != desktopData.size()) { errorString = saveFile.errorString(); qCWarning(KIO_WIDGETS) << "Error copying service" << fileName << errorString; saveFile.cancelWriting(); return false; } desktopFile.close(); if (!saveFile.commit()) { // Figures.... errorString = saveFile.errorString(); qCWarning(KIO_WIDGETS) << "Error committing changes to service" << fileName << errorString; return false; } if (!desktopFile.open(QFile::ReadOnly)) { errorString = desktopFile.errorString(); qCWarning(KIO_WIDGETS) << "Error re-opening service" << fileName << errorString; return false; } } // Add header return setExecuteBit(fileName, errorString); } // Helper function to make a .desktop file executable if prompted by the user. // returns true if KRun::run() should continue with execution, false if user declined // to make the file executable or we failed to make it executable. static bool makeServiceExecutable(const KService &service, QWidget *window) { if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) { qCWarning(KIO_WIDGETS) << "No authorization to execute " << service.entryPath(); KMessageBox::sorry(window, i18n("You are not authorized to execute this service.")); return false; // Don't circumvent the Kiosk } // We can use KStandardDirs::findExe to resolve relative pathnames // but that gets rid of the command line arguments. QString program = QFileInfo(service.exec()).canonicalFilePath(); if (program.isEmpty()) { // e.g. due to command line arguments program = service.exec(); } int result = showUntrustedProgramWarning(program, window); if (result != QDialog::Accepted) { return false; } // Assume that service is an absolute path since we're being called (relative paths // would have been allowed unless Kiosk said no, therefore we already know where the // .desktop file is. Now add a header to it if it doesn't already have one // and add the +x bit. QString errorString; if (!::makeServiceFileExecutable(service.entryPath(), errorString)) { QString serviceName = service.name(); if (serviceName.isEmpty()) { serviceName = service.genericName(); } KMessageBox::sorry( window, i18n("Unable to make the service %1 executable, aborting execution.\n%2.", serviceName, errorString) ); return false; } return true; } #ifndef KIOWIDGETS_NO_DEPRECATED bool KRun::run(const KService &_service, const QList &_urls, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { const RunFlags flags = tempFiles ? KRun::DeleteTemporaryFiles : RunFlags(); return runApplication(_service, _urls, window, flags, suggestedFileName, asn) != 0; } #endif qint64 KRun::runApplication(const KService &service, const QList &urls, QWidget *window, RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { if (!service.entryPath().isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(service.entryPath()) && !::makeServiceExecutable(service, window)) { return 0; } if ((flags & DeleteTemporaryFiles) == 0) { // Remember we opened those urls, for the "recent documents" menu in kicker for (const QUrl &url : urls) { KRecentDocument::add(url, service.desktopEntryName()); } } return runApplicationImpl(service, urls, window, flags, suggestedFileName, asn); } qint64 KRun::runService(const KService &_service, const QList &_urls, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { if (!_service.entryPath().isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(_service.entryPath()) && !::makeServiceExecutable(_service, window)) { return 0; } if (!tempFiles) { // Remember we opened those urls, for the "recent documents" menu in kicker for (const QUrl &url : _urls) { KRecentDocument::add(url, _service.desktopEntryName()); } } bool useKToolInvocation = !(tempFiles || _service.entryPath().isEmpty() || !suggestedFileName.isEmpty()); if (useKToolInvocation) { // Is klauncher installed? Let's try to start it, if it fails, then we won't use it. static int klauncherAvailable = -1; if (klauncherAvailable == -1) { KToolInvocation::ensureKdeinitRunning(); QDBusConnectionInterface *dbusDaemon = QDBusConnection::sessionBus().interface(); klauncherAvailable = dbusDaemon->isServiceRegistered(QStringLiteral("org.kde.klauncher5")); } if (klauncherAvailable == 0) { useKToolInvocation = false; } } if (!useKToolInvocation) { return runApplicationImpl(_service, _urls, window, tempFiles ? RunFlags(DeleteTemporaryFiles) : RunFlags(), suggestedFileName, asn); } // Resolve urls if needed, depending on what the app supports const QList urls = resolveURLs(_urls, _service); //qDebug() << "Running" << _service.entryPath() << _urls << "using klauncher"; QString error; int pid = 0; //TODO KF6: change KToolInvokation to take a qint64* QByteArray myasn = asn; // startServiceByDesktopPath() doesn't take QWidget*, add it to the startup info now if (window) { if (myasn.isEmpty()) { myasn = KStartupInfo::createNewStartupId(); } if (myasn != "0") { KStartupInfoId id; id.initId(myasn); KStartupInfoData data; // QTBUG-59017 Calling winId() on an embedded widget will break interaction // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using // its parent window instead if (window->window()) { data.setLaunchedBy(window->window()->winId()); } KStartupInfo::sendChange(id, data); } } int i = KToolInvocation::startServiceByDesktopPath( _service.entryPath(), QUrl::toStringList(urls), &error, nullptr, &pid, myasn ); if (i != 0) { //qDebug() << error; KMessageBox::sorry(window, error); return 0; } //qDebug() << "startServiceByDesktopPath worked fine"; return pid; } bool KRun::run(const QString &_exec, const QList &_urls, QWidget *window, const QString &_name, const QString &_icon, const QByteArray &asn) { KService::Ptr service(new KService(_name, _exec, _icon)); return runApplication(*service, _urls, window, RunFlags{}, QString(), asn); } bool KRun::runCommand(const QString &cmd, QWidget *window, const QString &workingDirectory) { if (cmd.isEmpty()) { qCWarning(KIO_WIDGETS) << "Command was empty, nothing to run"; return false; } const QStringList args = KShell::splitArgs(cmd); if (args.isEmpty()) { qCWarning(KIO_WIDGETS) << "Command could not be parsed."; return false; } const QString bin = args.first(); return KRun::runCommand(cmd, bin, bin /*iconName*/, window, QByteArray(), workingDirectory); } bool KRun::runCommand(const QString &cmd, const QString &execName, const QString &iconName, QWidget *window, const QByteArray &asn) { return runCommand(cmd, execName, iconName, window, asn, QString()); } bool KRun::runCommand(const QString &cmd, const QString &execName, const QString &iconName, QWidget *window, const QByteArray &asn, const QString &workingDirectory) { //qDebug() << "runCommand " << cmd << "," << execName; KProcess *proc = new KProcess; proc->setShellCommand(cmd); if (!workingDirectory.isEmpty()) { proc->setWorkingDirectory(workingDirectory); } QString bin = KIO::DesktopExecParser::executableName(execName); KService::Ptr service = KService::serviceByDesktopName(bin); return runCommandInternal(proc, service.data(), execName /*executable to check for in slotProcessExited*/, execName /*user-visible name*/, iconName, window, asn) != 0; } KRun::KRun(const QUrl &url, QWidget *window, bool showProgressInfo, const QByteArray &asn) : d(new KRunPrivate(this)) { d->m_timer = new QTimer(this); d->m_timer->setObjectName(QStringLiteral("KRun::timer")); d->m_timer->setSingleShot(true); d->init(url, window, showProgressInfo, asn); } void KRun::KRunPrivate::init(const QUrl &url, QWidget *window, bool showProgressInfo, const QByteArray &asn) { m_bFault = false; m_bAutoDelete = true; m_bProgressInfo = showProgressInfo; m_bFinished = false; m_job = nullptr; m_strURL = url; m_bScanFile = false; m_bIsDirectory = false; m_runExecutables = true; m_followRedirections = true; m_window = window; m_asn = asn; q->setEnableExternalBrowser(true); // Start the timer. This means we will return to the event // loop and do initialization afterwards. // Reason: We must complete the constructor before we do anything else. m_bCheckPrompt = false; m_bInit = true; q->connect(m_timer, &QTimer::timeout, q, &KRun::slotTimeout); startTimer(); //qDebug() << "new KRun" << q << url << "timer=" << m_timer; } void KRun::init() { //qDebug() << "INIT called"; if (!d->m_strURL.isValid() || d->m_strURL.scheme().isEmpty()) { const QString error = !d->m_strURL.isValid() ? d->m_strURL.errorString() : d->m_strURL.toString(); handleInitError(KIO::ERR_MALFORMED_URL, i18n("Malformed URL\n%1", error)); qCWarning(KIO_WIDGETS) << "Malformed URL:" << error; d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), d->m_strURL)) { QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, d->m_strURL.toDisplayString()); handleInitError(KIO::ERR_ACCESS_DENIED, msg); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } if (d->m_externalBrowserEnabled && checkNeedPortalSupport()) { // use the function from QDesktopServices as it handles portals correctly d->m_bFault = !QDesktopServices::openUrl(d->m_strURL); d->m_bFinished = true; d->startTimer(); return; } if (!d->m_externalBrowser.isEmpty() && d->m_strURL.scheme().startsWith(QLatin1String("http"))) { if (d->runExecutable(d->m_externalBrowser)) { return; } } else if (d->m_strURL.isLocalFile() && (d->m_strURL.host().isEmpty() || (d->m_strURL.host() == QLatin1String("localhost")) || (d->m_strURL.host().compare(QHostInfo::localHostName(), Qt::CaseInsensitive) == 0))) { const QString localPath = d->m_strURL.toLocalFile(); if (!QFile::exists(localPath)) { handleInitError(KIO::ERR_DOES_NOT_EXIST, i18n("Unable to run the command specified. " "The file or folder %1 does not exist.", localPath.toHtmlEscaped())); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(d->m_strURL); //qDebug() << "MIME TYPE is " << mime.name(); if (!d->m_externalBrowser.isEmpty() && ( mime.inherits(QStringLiteral("text/html")) || mime.inherits(QStringLiteral("application/xhtml+xml")))) { if (d->runExecutable(d->m_externalBrowser)) { return; } } else if (mime.isDefault() && !QFileInfo(localPath).isReadable()) { // Unknown mimetype because the file is unreadable, no point in showing an open-with dialog (#261002) const QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, localPath); handleInitError(KIO::ERR_ACCESS_DENIED, msg); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } else { mimeTypeDetermined(mime.name()); return; } } else if (KIO::DesktopExecParser::hasSchemeHandler(d->m_strURL)) { //qDebug() << "Using scheme handler"; const QString exec = schemeHandler(d->m_strURL.scheme()); if (exec.isEmpty()) { mimeTypeDetermined(KProtocolManager::defaultMimetype(d->m_strURL)); return; } else { if (run(exec, QList() << d->m_strURL, d->m_window, QString(), QString(), d->m_asn)) { d->m_bFinished = true; d->startTimer(); return; } } } #if 0 // removed for KF5 (for portability). Reintroduce a bool or flag if useful. // Did we already get the information that it is a directory ? if ((d->m_mode & QT_STAT_MASK) == QT_STAT_DIR) { mimeTypeDetermined("inode/directory"); return; } #endif // Let's see whether it is a directory if (!KProtocolManager::supportsListing(d->m_strURL)) { // No support for listing => it can't be a directory (example: http) if (!KProtocolManager::supportsReading(d->m_strURL)) { // No support for reading files either => we can't do anything (example: mailto URL, with no associated app) handleInitError(KIO::ERR_UNSUPPORTED_ACTION, i18n("Could not find any application or handler for %1", d->m_strURL.toDisplayString())); d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } scanFile(); return; } //qDebug() << "Testing directory (stating)"; // It may be a directory or a file, let's stat KIO::JobFlags flags = d->m_bProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::StatJob *job = KIO::stat(d->m_strURL, KIO::StatJob::SourceSide, 0 /* no details */, flags); KJobWidgets::setWindow(job, d->m_window); connect(job, &KJob::result, this, &KRun::slotStatResult); d->m_job = job; //qDebug() << "Job" << job << "is about stating" << d->m_strURL; } KRun::~KRun() { //qDebug() << this; d->m_timer->stop(); killJob(); //qDebug() << this << "done"; delete d; } bool KRun::KRunPrivate::runExecutable(const QString &_exec) { QList urls; urls.append(m_strURL); if (_exec.startsWith(QLatin1Char('!'))) { // Literal command const QString exec = _exec.midRef(1) + QLatin1String(" %u"); if (q->run(exec, urls, m_window, QString(), QString(), m_asn)) { m_bFinished = true; startTimer(); return true; } } else { KService::Ptr service = KService::serviceByStorageId(_exec); if (service && q->runApplication(*service, urls, m_window, RunFlags{}, QString(), m_asn)) { m_bFinished = true; startTimer(); return true; } } return false; } void KRun::KRunPrivate::showPrompt() { ExecutableFileOpenDialog *dialog = new ExecutableFileOpenDialog(q->window()); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &ExecutableFileOpenDialog::finished, q, [this, dialog](int result){ onDialogFinished(result, dialog->isDontAskAgainChecked()); }); dialog->show(); } bool KRun::KRunPrivate::isPromptNeeded() { if (m_strURL == QUrl(QStringLiteral("remote:/x-wizard_service.desktop"))) { return false; } const QMimeDatabase db; const QMimeType mime = db.mimeTypeForUrl(m_strURL); const bool isFileExecutable = (isExecutableFile(m_strURL, mime.name()) || mime.inherits(QStringLiteral("application/x-desktop"))); const bool isTextFile = mime.inherits(QStringLiteral("text/plain")); if (isFileExecutable && isTextFile) { KConfigGroup cfgGroup(KSharedConfig::openConfig(QStringLiteral("kiorc")), "Executable scripts"); const QString value = cfgGroup.readEntry("behaviourOnLaunch", "alwaysAsk"); if (value == QLatin1String("alwaysAsk")) { return true; } else { q->setRunExecutables(value == QLatin1String("execute")); } } return false; } void KRun::KRunPrivate::onDialogFinished(int result, bool isDontAskAgainSet) { if (result == ExecutableFileOpenDialog::Rejected) { m_bFinished = true; m_bInit = false; startTimer(); return; } q->setRunExecutables(result == ExecutableFileOpenDialog::ExecuteFile); if (isDontAskAgainSet) { QString output = result == ExecutableFileOpenDialog::OpenFile ? QStringLiteral("open") : QStringLiteral("execute"); KConfigGroup cfgGroup(KSharedConfig::openConfig(QStringLiteral("kiorc")), "Executable scripts"); cfgGroup.writeEntry("behaviourOnLaunch", output); } startTimer(); } void KRun::scanFile() { //qDebug() << d->m_strURL; // First, let's check for well-known extensions // Not when there is a query in the URL, in any case. if (!d->m_strURL.hasQuery()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(d->m_strURL); if (!mime.isDefault() || d->m_strURL.isLocalFile()) { //qDebug() << "Scanfile: MIME TYPE is " << mime.name(); mimeTypeDetermined(mime.name()); return; } } // No mimetype found, and the URL is not local (or fast mode not allowed). // We need to apply the 'KIO' method, i.e. either asking the server or // getting some data out of the file, to know what mimetype it is. if (!KProtocolManager::supportsReading(d->m_strURL)) { qCWarning(KIO_WIDGETS) << "#### NO SUPPORT FOR READING!"; d->m_bFault = true; d->m_bFinished = true; d->startTimer(); return; } //qDebug() << this << "Scanning file" << d->m_strURL; KIO::JobFlags flags = d->m_bProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo; KIO::TransferJob *job = KIO::get(d->m_strURL, KIO::NoReload /*reload*/, flags); KJobWidgets::setWindow(job, d->m_window); connect(job, &KJob::result, this, &KRun::slotScanFinished); connect(job, QOverload::of(&KIO::TransferJob::mimetype), this, &KRun::slotScanMimeType); d->m_job = job; //qDebug() << "Job" << job << "is about getting from" << d->m_strURL; } // When arriving in that method there are 6 possible states: // must_show_prompt, must_init, must_scan_file, found_dir, done+error or done+success. void KRun::slotTimeout() { if (d->m_bCheckPrompt) { d->m_bCheckPrompt = false; if (d->isPromptNeeded()) { d->showPrompt(); return; } } if (d->m_bInit) { d->m_bInit = false; init(); return; } if (d->m_bFault) { emit error(); } if (d->m_bFinished) { emit finished(); } else { if (d->m_bScanFile) { d->m_bScanFile = false; scanFile(); return; } else if (d->m_bIsDirectory) { d->m_bIsDirectory = false; mimeTypeDetermined(QStringLiteral("inode/directory")); return; } } if (d->m_bAutoDelete) { deleteLater(); return; } } void KRun::slotStatResult(KJob *job) { d->m_job = nullptr; const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { qCWarning(KIO_WIDGETS) << this << "ERROR" << job->error() << job->errorString(); handleError(job); //qDebug() << this << " KRun returning from showErrorDialog, starting timer to delete us"; d->m_bFault = true; } d->m_bFinished = true; // will emit the error and autodelete this d->startTimer(); } else { //qDebug() << "Finished"; KIO::StatJob *statJob = qobject_cast(job); if (!statJob) { qFatal("Fatal Error: job is a %s, should be a StatJob", typeid(*job).name()); } // Update our URL in case of a redirection setUrl(statJob->url()); const KIO::UDSEntry entry = statJob->statResult(); const mode_t mode = entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE); if ((mode & QT_STAT_MASK) == QT_STAT_DIR) { d->m_bIsDirectory = true; // it's a dir } else { d->m_bScanFile = true; // it's a file } d->m_localPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); // mimetype already known? (e.g. print:/manager) const QString knownMimeType = entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE); if (!knownMimeType.isEmpty()) { mimeTypeDetermined(knownMimeType); d->m_bFinished = true; } // We should have found something assert(d->m_bScanFile || d->m_bIsDirectory); // Start the timer. Once we get the timer event this // protocol server is back in the pool and we can reuse it. // This gives better performance than starting a new slave d->startTimer(); } } void KRun::slotScanMimeType(KIO::Job *, const QString &mimetype) { if (mimetype.isEmpty()) { qCWarning(KIO_WIDGETS) << "get() didn't emit a mimetype! Probably a kioslave bug, please check the implementation of" << url().scheme(); } mimeTypeDetermined(mimetype); d->m_job = nullptr; } void KRun::slotScanFinished(KJob *job) { d->m_job = nullptr; const int errCode = job->error(); if (errCode) { // ERR_NO_CONTENT is not an error, but an indication no further // actions needs to be taken. if (errCode != KIO::ERR_NO_CONTENT) { qCWarning(KIO_WIDGETS) << this << "ERROR (stat):" << job->error() << ' ' << job->errorString(); handleError(job); d->m_bFault = true; } d->m_bFinished = true; // will emit the error and autodelete this d->startTimer(); } } void KRun::mimeTypeDetermined(const QString &mimeType) { // foundMimeType reimplementations might show a dialog box; // make sure some timer doesn't kill us meanwhile (#137678, #156447) Q_ASSERT(!d->m_showingDialog); d->m_showingDialog = true; foundMimeType(mimeType); d->m_showingDialog = false; // We cannot assume that we're finished here. Some reimplementations // start a KIO job and call setFinished only later. } void KRun::foundMimeType(const QString &type) { //qDebug() << "Resulting mime type is " << type; QMimeDatabase db; KIO::TransferJob *job = qobject_cast(d->m_job); if (job) { // Update our URL in case of a redirection if (d->m_followRedirections) { setUrl(job->url()); } job->putOnHold(); KIO::Scheduler::publishSlaveOnHold(); d->m_job = nullptr; } Q_ASSERT(!d->m_bFinished); // Support for preferred service setting, see setPreferredService if (!d->m_preferredService.isEmpty()) { //qDebug() << "Attempting to open with preferred service: " << d->m_preferredService; KService::Ptr serv = KService::serviceByDesktopName(d->m_preferredService); if (serv && serv->hasMimeType(type)) { QList lst; lst.append(d->m_strURL); if (KRun::runApplication(*serv, lst, d->m_window, RunFlags{}, QString(), d->m_asn)) { setFinished(true); return; } /// Note: if that service failed, we'll go to runUrl below to /// maybe find another service, even though an error dialog box was /// already displayed. That's good if runUrl tries another service, /// but it's not good if it tries the same one :} } } // Resolve .desktop files from media:/, remote:/, applications:/ etc. QMimeType mime = db.mimeTypeForName(type); if (!mime.isValid()) { qCWarning(KIO_WIDGETS) << "Unknown mimetype " << type; } else if (mime.inherits(QStringLiteral("application/x-desktop")) && !d->m_localPath.isEmpty()) { d->m_strURL = QUrl::fromLocalFile(d->m_localPath); } KRun::RunFlags runFlags; if (d->m_runExecutables) { runFlags |= KRun::RunExecutables; } if (!KRun::runUrl(d->m_strURL, type, d->m_window, runFlags, d->m_suggestedFileName, d->m_asn)) { d->m_bFault = true; } setFinished(true); } void KRun::killJob() { if (d->m_job) { //qDebug() << this << "m_job=" << d->m_job; d->m_job->kill(); d->m_job = nullptr; } } void KRun::abort() { if (d->m_bFinished) { return; } //qDebug() << this << "m_showingDialog=" << d->m_showingDialog; killJob(); // If we're showing an error message box, the rest will be done // after closing the msgbox -> don't autodelete nor emit signals now. if (d->m_showingDialog) { return; } d->m_bFault = true; d->m_bFinished = true; d->m_bInit = false; d->m_bScanFile = false; // will emit the error and autodelete this d->startTimer(); } QWidget *KRun::window() const { return d->m_window; } bool KRun::hasError() const { return d->m_bFault; } bool KRun::hasFinished() const { return d->m_bFinished; } bool KRun::autoDelete() const { return d->m_bAutoDelete; } void KRun::setAutoDelete(bool b) { d->m_bAutoDelete = b; } void KRun::setEnableExternalBrowser(bool b) { d->m_externalBrowserEnabled = b; if (d->m_externalBrowserEnabled) { d->m_externalBrowser = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("BrowserApplication"); // If a default browser isn't set in kdeglobals, fall back to mimeapps.list if (!d->m_externalBrowser.isEmpty()) { return; } KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); KConfigGroup defaultApps(profile, "Default Applications"); d->m_externalBrowser = defaultApps.readEntry("x-scheme-handler/https"); if (d->m_externalBrowser.isEmpty()) { d->m_externalBrowser = defaultApps.readEntry("x-scheme-handler/http"); } } else { d->m_externalBrowser.clear(); } } void KRun::setPreferredService(const QString &desktopEntryName) { d->m_preferredService = desktopEntryName; } void KRun::setRunExecutables(bool b) { d->m_runExecutables = b; } void KRun::setSuggestedFileName(const QString &fileName) { d->m_suggestedFileName = fileName; } void KRun::setShowScriptExecutionPrompt(bool showPrompt) { d->m_bCheckPrompt = showPrompt; } void KRun::setFollowRedirections(bool followRedirections) { d->m_followRedirections = followRedirections; } QString KRun::suggestedFileName() const { return d->m_suggestedFileName; } bool KRun::isExecutable(const QString &mimeTypeName) { QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName(mimeTypeName); return (mimeType.inherits(QLatin1String("application/x-desktop")) || mimeType.inherits(QLatin1String("application/x-executable")) || /* See https://bugs.freedesktop.org/show_bug.cgi?id=97226 */ mimeType.inherits(QLatin1String("application/x-sharedlib")) || mimeType.inherits(QLatin1String("application/x-ms-dos-executable")) || mimeType.inherits(QLatin1String("application/x-shellscript"))); } void KRun::setUrl(const QUrl &url) { d->m_strURL = url; } QUrl KRun::url() const { return d->m_strURL; } void KRun::setError(bool error) { d->m_bFault = error; } void KRun::setProgressInfo(bool progressInfo) { d->m_bProgressInfo = progressInfo; } bool KRun::progressInfo() const { return d->m_bProgressInfo; } void KRun::setFinished(bool finished) { d->m_bFinished = finished; if (finished) { d->startTimer(); } } void KRun::setJob(KIO::Job *job) { d->m_job = job; } KIO::Job *KRun::job() { return d->m_job; } #ifndef KIOWIDGETS_NO_DEPRECATED QTimer &KRun::timer() { return *d->m_timer; } #endif #ifndef KIOWIDGETS_NO_DEPRECATED void KRun::setDoScanFile(bool scanFile) { d->m_bScanFile = scanFile; } #endif #ifndef KIOWIDGETS_NO_DEPRECATED bool KRun::doScanFile() const { return d->m_bScanFile; } #endif #ifndef KIOWIDGETS_NO_DEPRECATED void KRun::setIsDirecory(bool isDirectory) { d->m_bIsDirectory = isDirectory; } #endif bool KRun::isDirectory() const { return d->m_bIsDirectory; } #ifndef KIOWIDGETS_NO_DEPRECATED void KRun::setInitializeNextAction(bool initialize) { d->m_bInit = initialize; } #endif #ifndef KIOWIDGETS_NO_DEPRECATED bool KRun::initializeNextAction() const { return d->m_bInit; } #endif bool KRun::isLocalFile() const { return d->m_strURL.isLocalFile(); } /****************/ KProcessRunner::KProcessRunner(KProcess *p, const QString &executable, const KStartupInfoId &id) : id(id) { m_pid = 0; process = p; m_executable = executable; connect(process, QOverload::of(&QProcess::finished), this, &KProcessRunner::slotProcessExited); process->start(); if (!process->waitForStarted()) { //qDebug() << "wait for started failed, exitCode=" << process->exitCode() // << "exitStatus=" << process->exitStatus(); // Note that exitCode is 255 here (the first time), and 0 later on (bug?). // Use delayed invocation so the caller has time to connect to the signal QMetaObject::invokeMethod(this, [this]() { slotProcessExited(255, process->exitStatus()); }, Qt::QueuedConnection); } else { m_pid = process->processId(); } } KProcessRunner::~KProcessRunner() { delete process; } qint64 KProcessRunner::pid() const { return m_pid; } void KProcessRunner::terminateStartupNotification() { #if HAVE_X11 if (!id.isNull()) { KStartupInfoData data; data.addPid(m_pid); // announce this pid for the startup notification has finished data.setHostname(); KStartupInfo::sendFinish(id, data); } #endif } void KProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus) { //qDebug() << m_executable << "exitCode=" << exitCode << "exitStatus=" << exitStatus; Q_UNUSED(exitStatus) terminateStartupNotification(); // do this before the messagebox if (exitCode != 0 && !m_executable.isEmpty()) { // Let's see if the error is because the exe doesn't exist. // When this happens, waitForStarted returns false, but not if kioexec // was involved, then we come here, that's why the code is here. // // We'll try to find the executable relatively to current directory, // (or with a full path, if m_executable is absolute), and then in the PATH. if (!QFile(m_executable).exists() && QStandardPaths::findExecutable(m_executable).isEmpty()) { const QString &errorString = i18n("Could not find the program '%1'", m_executable); qWarning() << errorString; emit error(errorString); } else { //qDebug() << process->readAllStandardError(); } } deleteLater(); } #include "moc_krun.cpp" #include "moc_krun_p.cpp" #include "krun.moc"