diff --git a/autotests/dataprotocoltest.h b/autotests/dataprotocoltest.h index 17d71915..001de790 100644 --- a/autotests/dataprotocoltest.h +++ b/autotests/dataprotocoltest.h @@ -1,21 +1,20 @@ /* * Copyright (C) 2012 Rolf Eike Beer */ #ifndef DATAPROTOCOLTEST_H #define DATAPROTOCOLTEST_H #include #include -#include class DataProtocolTest : public QObject { Q_OBJECT private Q_SLOTS: void runAllTests(); void runAllTests_data(); }; #endif /* DATAPROTOCOLTEST_H */ diff --git a/autotests/favicontest.cpp b/autotests/favicontest.cpp index bc77c433..282f012a 100644 --- a/autotests/favicontest.cpp +++ b/autotests/favicontest.cpp @@ -1,304 +1,301 @@ /* This file is part of KDE Copyright (c) 2006-2016 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 -#include #include #include #include #include static const char s_hostUrl[] = "http://www.google.com/index.html"; static const char s_pageUrl[] = "http://www.google.com/somepage.html"; static const char s_iconUrl[] = "http://www.google.com/favicon.ico"; static const char s_altIconUrl[] = "http://www.ibm.com/favicon.ico"; static const char s_thirdIconUrl[] = "http://www.google.fr/favicon.ico"; static const char s_iconUrlForThreadTest[] = "http://www.google.de/favicon.ico"; static enum NetworkAccess { Unknown, Yes, No } s_networkAccess = Unknown; static bool checkNetworkAccess() { if (s_networkAccess == Unknown) { QElapsedTimer tm; tm.start(); KIO::Job *job = KIO::get(QUrl(s_iconUrl), KIO::NoReload, KIO::HideProgressInfo); if (job->exec()) { s_networkAccess = Yes; qDebug() << "Network access OK. Download time" << tm.elapsed() << "ms"; } else { qWarning() << job->errorString(); s_networkAccess = No; } } return s_networkAccess == Yes; } class FavIconTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void favIconForUrlShouldBeEmptyInitially(); void hostJobShouldDownloadIconThenUseCache(); void iconUrlJobShouldDownloadIconThenUseCache(); void reloadShouldReload(); void failedDownloadShouldBeRemembered(); void tooBigFaviconShouldAbort(); void simultaneousRequestsShouldWork(); void concurrentRequestsShouldWork(); private: }; void FavIconTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); // To let ctest exit, we shouldn't start kio_http_cache_cleaner qputenv("KIO_DISABLE_CACHE_CLEANER", "yes"); // To get KJob::errorString() in English qputenv("LC_ALL", "en_US.UTF-8"); if (!checkNetworkAccess()) { QSKIP("no network access", SkipAll); } // Ensure we start with no cache on disk const QString favIconCacheDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/favicons"); QDir(favIconCacheDir).removeRecursively(); QVERIFY(!QFileInfo::exists(favIconCacheDir)); // Enable debug output QLoggingCategory::setFilterRules(QStringLiteral("kde.kio.favicons.debug=true")); } void FavIconTest::favIconForUrlShouldBeEmptyInitially() { QCOMPARE(KIO::favIconForUrl(QUrl(s_hostUrl)), QString()); } // Waits for start() and checks whether a transfer job was created. static bool willDownload(KIO::FavIconRequestJob *job) { qApp->sendPostedEvents(job, QEvent::MetaCall); // start() is delayed return job->findChild(); } void FavIconTest::hostJobShouldDownloadIconThenUseCache() { const QUrl url(s_hostUrl); KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); QVERIFY(willDownload(job)); QVERIFY(job->exec()); const QString iconFile = job->iconFile(); QVERIFY(iconFile.endsWith(QLatin1String("favicons/www.google.com.png"))); QVERIFY2(QFile::exists(iconFile), qPrintable(iconFile)); QVERIFY(!QIcon(iconFile).isNull()); // pass full path to QIcon // This requires https://codereview.qt-project.org/148444 //QVERIFY(!QIcon::fromTheme(iconFile).isNull()); // old code ported from kdelibs4 might do that, should work too // Lookup should give the same result QCOMPARE(KIO::favIconForUrl(url), iconFile); // Second job should use the cache KIO::FavIconRequestJob *secondJob = new KIO::FavIconRequestJob(url); QVERIFY(!willDownload(secondJob)); QVERIFY(secondJob->exec()); QCOMPARE(secondJob->iconFile(), iconFile); // The code from the class docu QString goticonFile; { KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); connect(job, &KIO::FavIconRequestJob::result, this, [job, &goticonFile](KJob *){ if (!job->error()) { goticonFile = job->iconFile(); } }); QVERIFY(job->exec()); } QCOMPARE(goticonFile, iconFile); } void FavIconTest::iconUrlJobShouldDownloadIconThenUseCache() { const QUrl url(s_pageUrl); // Set icon URL to "ibm" KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); job->setIconUrl(QUrl(s_altIconUrl)); QVERIFY(willDownload(job)); QVERIFY(job->exec()); const QString iconFile = job->iconFile(); QVERIFY(iconFile.endsWith(QLatin1String("favicons/www.ibm.com.png"))); QVERIFY2(QFile::exists(iconFile), qPrintable(iconFile)); QVERIFY(!QPixmap(iconFile).isNull()); // pass full path to QPixmap (to test this too) // Lookup should give the same result QCOMPARE(KIO::favIconForUrl(url), iconFile); // Second job should use the cache. It doesn't even need the icon url again. KIO::FavIconRequestJob *secondJob = new KIO::FavIconRequestJob(url); QVERIFY(!willDownload(secondJob)); QVERIFY(secondJob->exec()); QCOMPARE(secondJob->iconFile(), iconFile); // Set icon URL to "www.google.fr/favicon.ico" KIO::FavIconRequestJob *thirdJob = new KIO::FavIconRequestJob(url); thirdJob->setIconUrl(QUrl(s_thirdIconUrl)); QVERIFY(willDownload(thirdJob)); QVERIFY(thirdJob->exec()); const QString newiconFile = thirdJob->iconFile(); QVERIFY(newiconFile.endsWith(QLatin1String("favicons/www.google.fr.png"))); // Lookup should give the same result QCOMPARE(KIO::favIconForUrl(url), newiconFile); } void FavIconTest::reloadShouldReload() { const QUrl url(s_hostUrl); // First job, to make sure it's in the cache (if the other tests didn't run first) KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); QVERIFY(job->exec()); const QString iconFile = job->iconFile(); // Second job should use the cache KIO::FavIconRequestJob *secondJob = new KIO::FavIconRequestJob(url); QVERIFY(!willDownload(secondJob)); QVERIFY(secondJob->exec()); QCOMPARE(secondJob->iconFile(), iconFile); // job with Reload should not use the cache KIO::FavIconRequestJob *jobWithReload = new KIO::FavIconRequestJob(url, KIO::Reload); QVERIFY(willDownload(jobWithReload)); QVERIFY(jobWithReload->exec()); QCOMPARE(jobWithReload->iconFile(), iconFile); } void FavIconTest::failedDownloadShouldBeRemembered() { const QUrl url(s_pageUrl); // Set icon URL to a non-existing favicon KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); job->setIconUrl(QUrl("https://kde.org/doesnotexist/favicon.ico")); QVERIFY(willDownload(job)); QVERIFY(!job->exec()); QVERIFY(job->iconFile().isEmpty()); qDebug() << job->errorString(); QCOMPARE(job->error(), int(KIO::ERR_DOES_NOT_EXIST)); QCOMPARE(job->errorString(), QStringLiteral("The file or folder https://kde.org/doesnotexist/favicon.ico does not exist.")); // Second job should use the cache and not do anything KIO::FavIconRequestJob *secondJob = new KIO::FavIconRequestJob(url); QVERIFY(!willDownload(secondJob)); QVERIFY(!secondJob->exec()); QVERIFY(secondJob->iconFile().isEmpty()); QCOMPARE(job->error(), int(KIO::ERR_DOES_NOT_EXIST)); QCOMPARE(job->errorString(), QStringLiteral("The file or folder https://kde.org/doesnotexist/favicon.ico does not exist.")); } void FavIconTest::tooBigFaviconShouldAbort() { const QUrl url(s_pageUrl); // Set icon URL to a >65KB file KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); job->setIconUrl(QUrl("http://download.kde.org/Attic/4.13.2/src/kcalc-4.13.2.tar.xz")); QVERIFY(willDownload(job)); QVERIFY(!job->exec()); QCOMPARE(job->error(), int(KIO::ERR_SLAVE_DEFINED)); QCOMPARE(job->errorString(), QStringLiteral("Icon file too big, download aborted")); } void FavIconTest::simultaneousRequestsShouldWork() { const QUrl url(s_hostUrl); // First job, to find out the iconFile and delete it QString iconFile; { KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); QVERIFY(job->exec()); iconFile = job->iconFile(); QFile::remove(iconFile); } // This is a case we could maybe optimize: not downloading twice in parallel KIO::FavIconRequestJob *job1 = new KIO::FavIconRequestJob(url); job1->setAutoDelete(false); KIO::FavIconRequestJob *job2 = new KIO::FavIconRequestJob(url); job2->setAutoDelete(false); QVERIFY(willDownload(job1)); QVERIFY(willDownload(job2)); QVERIFY(job1->exec()); QCOMPARE(job1->iconFile(), iconFile); QVERIFY(job2->exec()); QCOMPARE(job2->iconFile(), iconFile); delete job1; delete job2; } static QString getAltIconUrl() { const QUrl url(s_pageUrl); // Set icon URL to one that we haven't downloaded yet KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); job->setIconUrl(QUrl(s_iconUrlForThreadTest)); job->exec(); return job->iconFile(); } void FavIconTest::concurrentRequestsShouldWork() { const int numThreads = 3; QThreadPool tp; tp.setMaxThreadCount(numThreads); QVector> futures(numThreads); for (int i = 0; i < numThreads; ++i) { futures[i] = QtConcurrent::run(&tp, getAltIconUrl); } QVERIFY(tp.waitForDone(60000)); const QString firstResult = futures.at(0).result(); for (int i = 1; i < numThreads; ++i) { QCOMPARE(futures.at(i).result(), firstResult); } QVERIFY(!QPixmap(firstResult).isNull()); } QTEST_MAIN(FavIconTest) #include "favicontest.moc" diff --git a/autotests/fileundomanagertest.h b/autotests/fileundomanagertest.h index 57cfba44..7a61729d 100644 --- a/autotests/fileundomanagertest.h +++ b/autotests/fileundomanagertest.h @@ -1,62 +1,61 @@ /* This file is part of KDE Copyright (c) 2006, 2008 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. */ #ifndef FILEUNDOMANAGERTEST_H #define FILEUNDOMANAGERTEST_H #include -#include class TestUiInterface; class FileUndoManagerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testCopyFiles(); void testMoveFiles(); void testCopyDirectory(); void testMoveDirectory(); void testRenameFile(); void testRenameDir(); void testTrashFiles(); void testRestoreTrashedFiles(); void testModifyFileBeforeUndo(); // #20532 void testCreateSymlink(); void testCreateDir(); void testMkpath(); void testPasteClipboardUndo(); // #318757 void testBatchRename(); void testUndoCopyOfDeletedFile(); // TODO find tests that would lead to kio job errors // TODO test renaming during a CopyJob. // Doesn't seem possible though, requires user interaction... // TODO: add test for undoing after a partial move (http://bugs.kde.org/show_bug.cgi?id=91579) // Difficult too. private: void doUndo(); TestUiInterface *m_uiInterface; }; #endif diff --git a/autotests/globaltest.cpp b/autotests/globaltest.cpp index 5ea34954..4ccd2d4f 100644 --- a/autotests/globaltest.cpp +++ b/autotests/globaltest.cpp @@ -1,76 +1,75 @@ /* This file is part of the KDE project Copyright (C) 2013 Dawit Alemayehu This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "globaltest.h" #include #include "global.h" #include "kioglobal_p.h" #include -#include #include QTEST_MAIN(GlobalTest) void GlobalTest::testUserPermissionConversion() { const int permissions = S_IRUSR | S_IWUSR | S_IXUSR; QFile::Permissions qPermissions = KIO::convertPermissions(permissions); QFile::Permissions perms = (QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); QCOMPARE(qPermissions & perms, perms); perms = (QFile::ReadGroup | QFile::WriteGroup | QFile::ExeGroup); QCOMPARE(qPermissions & perms, 0); perms = (QFile::ReadOther | QFile::WriteOther | QFile::ExeOther); QCOMPARE(qPermissions & perms, 0); } void GlobalTest::testGroupPermissionConversion() { const int permissions = S_IRGRP | S_IWGRP | S_IXGRP; QFile::Permissions qPermissions = KIO::convertPermissions(permissions); QFile::Permissions perms = (QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); QCOMPARE(qPermissions & perms, 0); perms = (QFile::ReadGroup | QFile::WriteGroup | QFile::ExeGroup); QCOMPARE(qPermissions & perms, perms); perms = (QFile::ReadOther | QFile::WriteOther | QFile::ExeOther); QCOMPARE(qPermissions & perms, 0); } void GlobalTest::testOtherPermissionConversion() { const int permissions = S_IROTH | S_IWOTH | S_IXOTH; QFile::Permissions qPermissions = KIO::convertPermissions(permissions); QFile::Permissions perms = (QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); QCOMPARE(qPermissions & perms, 0); perms = (QFile::ReadGroup | QFile::WriteGroup | QFile::ExeGroup); QCOMPARE(qPermissions & perms, 0); perms = (QFile::ReadOther | QFile::WriteOther | QFile::ExeOther); QCOMPARE(qPermissions & perms, perms); } diff --git a/autotests/http/httpauthenticationtest.cpp b/autotests/http/httpauthenticationtest.cpp index 982ff72a..3ead2041 100644 --- a/autotests/http/httpauthenticationtest.cpp +++ b/autotests/http/httpauthenticationtest.cpp @@ -1,344 +1,344 @@ /* This file is part of the KDE libraries Copyright (C) 2011 Dawit Alemayehu This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "httpauthenticationtest.h" #include #include #include -#include #include + #include #define ENABLE_HTTP_AUTH_NONCE_SETTER #include "httpauthentication.cpp" // QT5 TODO QTEST_GUILESS_MAIN(HTTPAuthenticationTest) QTEST_MAIN(HTTPAuthenticationTest) static void parseAuthHeader(const QByteArray &header, QByteArray *bestOffer, QByteArray *scheme, QList *result) { const QList authHeaders = KAbstractHttpAuthentication::splitOffers(QList() << header); QByteArray chosenHeader = KAbstractHttpAuthentication::bestOffer(authHeaders); if (bestOffer) { *bestOffer = chosenHeader; } if (!scheme && !result) { return; } QByteArray authScheme; const QList parseResult = parseChallenge(chosenHeader, &authScheme); if (scheme) { *scheme = authScheme; } if (result) { *result = parseResult; } } static QByteArray hmacMD5(const QByteArray &data, const QByteArray &key) { QByteArray ret; QByteArray ipad(64, 0x36); QByteArray opad(64, 0x5c); Q_ASSERT(key.size() <= 64); for (int i = qMin(key.size(), 64) - 1; i >= 0; i--) { ipad.data()[i] ^= key[i]; opad.data()[i] ^= key[i]; } QByteArray content(ipad + data); QCryptographicHash md5(QCryptographicHash::Md5); md5.addData(content); content = opad + md5.result(); md5.reset(); md5.addData(content); return md5.result(); } static QByteArray QString2UnicodeLE(const QString &target) { QByteArray unicode(target.length() * 2, 0); for (int i = 0; i < target.length(); i++) { ((quint16 *) unicode.data()) [ i ] = qToLittleEndian(target[i].unicode()); } return unicode; } void HTTPAuthenticationTest::testHeaderParsing_data() { QTest::addColumn("header"); QTest::addColumn("resultScheme"); QTest::addColumn("resultValues"); // Tests cases from http://greenbytes.de/tech/tc/httpauth/ QTest::newRow("greenbytes-simplebasic") << QByteArray("Basic realm=\"foo\"") << QByteArray("Basic") << QByteArray("realm,foo"); QTest::newRow("greenbytes-simplebasictok") << QByteArray("Basic realm=foo") << QByteArray("Basic") << QByteArray("realm,foo"); QTest::newRow("greenbytes-simplebasiccomma") << QByteArray("Basic , realm=\"foo\"") << QByteArray("Basic") << QByteArray("realm,foo"); // there must be a space after the scheme QTest::newRow("greenbytes-simplebasiccomma2") << QByteArray("Basic, realm=\"foo\"") << QByteArray() << QByteArray(); // we accept scheme without any parameters to maintain compatibility with too simple minded servers out there QTest::newRow("greenbytes-simplebasicnorealm") << QByteArray("Basic") << QByteArray("Basic") << QByteArray(); QTest::newRow("greenbytes-simplebasicwsrealm") << QByteArray("Basic realm = \"foo\"") << QByteArray("Basic") << QByteArray("realm,foo"); QTest::newRow("greenbytes-simplebasicrealmsqc") << QByteArray("Basic realm=\"\\f\\o\\o\"") << QByteArray("Basic") << QByteArray("realm,foo"); QTest::newRow("greenbytes-simplebasicrealmsqc2") << QByteArray("Basic realm=\"\\\"foo\\\"\"") << QByteArray("Basic") << QByteArray("realm,\"foo\""); QTest::newRow("greenbytes-simplebasicnewparam1") << QByteArray("Basic realm=\"foo\", bar=\"xyz\"") << QByteArray("Basic") << QByteArray("realm,foo,bar,xyz"); QTest::newRow("greenbytes-simplebasicnewparam2") << QByteArray("Basic bar=\"xyz\", realm=\"foo\"") << QByteArray("Basic") << QByteArray("bar,xyz,realm,foo"); // a Basic challenge following an empty one QTest::newRow("greenbytes-multibasicempty") << QByteArray(",Basic realm=\"foo\"") << QByteArray("Basic") << QByteArray("realm,foo"); QTest::newRow("greenbytes-multibasicunknown") << QByteArray("Basic realm=\"basic\", Newauth realm=\"newauth\"") << QByteArray("Basic") << QByteArray("realm,basic"); QTest::newRow("greenbytes-multibasicunknown2") << QByteArray("Newauth realm=\"newauth\", Basic realm=\"basic\"") << QByteArray("Basic") << QByteArray("realm,basic"); QTest::newRow("greenbytes-unknown") << QByteArray("Newauth realm=\"newauth\"") << QByteArray() << QByteArray(); // Misc. test cases QTest::newRow("ntlm") << QByteArray("NTLM ") << QByteArray("NTLM") << QByteArray(); QTest::newRow("unterminated-quoted-value") << QByteArray("Basic realm=\"") << QByteArray("Basic") << QByteArray(); QTest::newRow("spacing-and-tabs") << QByteArray("bAsic bar\t =\t\"baz\", realm =\t\"foo\"") << QByteArray("bAsic") << QByteArray("bar,baz,realm,foo"); QTest::newRow("empty-fields") << QByteArray("Basic realm=foo , , , ,, bar=\"baz\"\t,") << QByteArray("Basic") << QByteArray("realm,foo,bar,baz"); QTest::newRow("spacing") << QByteArray("Basic realm=foo, bar = baz") << QByteArray("Basic") << QByteArray("realm,foo,bar,baz"); QTest::newRow("missing-comma-between-fields") << QByteArray("Basic realm=foo bar = baz") << QByteArray("Basic") << QByteArray("realm,foo"); // quotes around text, every character needlessly quoted QTest::newRow("quote-excess") << QByteArray("Basic realm=\"\\\"\\f\\o\\o\\\"\"") << QByteArray("Basic") << QByteArray("realm,\"foo\""); // quotes around text, quoted backslashes QTest::newRow("quoted-backslash") << QByteArray("Basic realm=\"\\\"foo\\\\\\\\\"") << QByteArray("Basic") << QByteArray("realm,\"foo\\\\"); // quotes around text, quoted backslashes, quote hidden behind them QTest::newRow("quoted-backslash-and-quote") << QByteArray("Basic realm=\"\\\"foo\\\\\\\"\"") << QByteArray("Basic") << QByteArray("realm,\"foo\\\""); // invalid quoted text QTest::newRow("invalid-quoted") << QByteArray("Basic realm=\"\\\"foo\\\\\\\"") << QByteArray("Basic") << QByteArray(); // ends in backslash without quoted value QTest::newRow("invalid-quote") << QByteArray("Basic realm=\"\\\"foo\\\\\\") << QByteArray("Basic") << QByteArray(); } QByteArray joinQByteArray(const QList &list) { QByteArray data; const int count = list.count(); for (int i = 0; i < count; ++i) { if (i > 0) { data += ','; } data += list.at(i); } return data; } void HTTPAuthenticationTest::testHeaderParsing() { QFETCH(QByteArray, header); QFETCH(QByteArray, resultScheme); QFETCH(QByteArray, resultValues); QByteArray chosenHeader, chosenScheme; QList parsingResult, expectedResult; parseAuthHeader(header, &chosenHeader, &chosenScheme, &parsingResult); QCOMPARE(chosenScheme, resultScheme); QCOMPARE(joinQByteArray(parsingResult), resultValues); } void HTTPAuthenticationTest::testAuthenticationSelection_data() { QTest::addColumn("input"); QTest::addColumn("expectedScheme"); QTest::addColumn("expectedOffer"); #if HAVE_LIBGSSAPI QTest::newRow("all-with-negotiate") << QByteArray("Negotiate , Digest , NTLM , Basic") << QByteArray("Negotiate") << QByteArray("Negotiate"); #endif QTest::newRow("all-without-negotiate") << QByteArray("Digest , NTLM , Basic , NewAuth") << QByteArray("Digest") << QByteArray("Digest"); QTest::newRow("ntlm-basic-unknown") << QByteArray("NTLM , Basic , NewAuth") << QByteArray("NTLM") << QByteArray("NTLM"); QTest::newRow("basic-unknown") << QByteArray("Basic , NewAuth") << QByteArray("Basic") << QByteArray("Basic"); QTest::newRow("ntlm-basic+param-ntlm") << QByteArray("NTLM , Basic realm=foo, bar = baz, NTLM") << QByteArray("NTLM") << QByteArray("NTLM"); QTest::newRow("ntlm-with-type{2|3}") << QByteArray("NTLM VFlQRV8yX09SXzNfTUVTU0FHRQo=") << QByteArray("NTLM") << QByteArray("NTLM VFlQRV8yX09SXzNfTUVTU0FHRQo="); // Unknown schemes always return blank, i.e. auth request should be ignored QTest::newRow("unknown-param") << QByteArray("Newauth realm=\"newauth\"") << QByteArray() << QByteArray(); QTest::newRow("unknown-unknown") << QByteArray("NewAuth , NewAuth2") << QByteArray() << QByteArray(); } void HTTPAuthenticationTest::testAuthenticationSelection() { QFETCH(QByteArray, input); QFETCH(QByteArray, expectedScheme); QFETCH(QByteArray, expectedOffer); QByteArray scheme, offer; parseAuthHeader(input, &offer, &scheme, nullptr); QCOMPARE(scheme, expectedScheme); QCOMPARE(offer, expectedOffer); } void HTTPAuthenticationTest::testAuthentication_data() { QTest::addColumn("input"); QTest::addColumn("expectedResponse"); QTest::addColumn("user"); QTest::addColumn("pass"); QTest::addColumn("url"); QTest::addColumn("cnonce"); // Test cases from RFC 2617... QTest::newRow("rfc-2617-basic-example") << QByteArray("Basic realm=\"WallyWorld\"") << QByteArray("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==") << QByteArray("Aladdin") << QByteArray("open sesame") << QByteArray() << QByteArray(); QTest::newRow("rfc-2617-digest-example") << QByteArray("Digest realm=\"testrealm@host.com\", qop=\"auth,auth-int\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"") << QByteArray("Digest username=\"Mufasa\", realm=\"testrealm@host.com\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", uri=\"/dir/index.html\", algorithm=MD5, qop=auth, cnonce=\"0a4f113b\", nc=00000001, response=\"6629fae49393a05397450978507c4ef1\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"") << QByteArray("Mufasa") << QByteArray("Circle Of Life") << QByteArray("http://www.nowhere.org/dir/index.html") << QByteArray("0a4f113b"); QTest::newRow("ntlm-negotiate-type1") << QByteArray("NTLM") << QByteArray("NTLM TlRMTVNTUAABAAAABQIAAAAAAAAAAAAAAAAAAAAAAAA=") << QByteArray() << QByteArray() << QByteArray() << QByteArray(); QTest::newRow("ntlm-challenge-type2") << QByteArray("NTLM TlRMTVNTUAACAAAAFAAUACgAAAABggAAU3J2Tm9uY2UAAAAAAAAAAFUAcgBzAGEALQBNAGEAagBvAHIA") << QByteArray("NTLM TlRMTVNTUAADAAAAGAAYAFgAAAAYABgAQAAAABQAFABwAAAADAAMAIQAAAAWABYAkAAAAAAAAAAAAAAAAYIAAODgDeMQShvyBT8Hx92oLTxImumJ4bAA062Hym3v40aFucQ8R3qMQtYAZn1okufol1UAcgBzAGEALQBNAGkAbgBvAHIAWgBhAHAAaABvAGQAVwBPAFIASwBTAFQAQQBUAEkATwBOAA==") << QByteArray("Ursa-Minor\\Zaphod") << QByteArray("Beeblebrox") << QByteArray() << QByteArray(); QTest::newRow("ntlm-challenge-type2-no-domain") << QByteArray("NTLM TlRMTVNTUAACAAAAFAAUACgAAAABggAAU3J2Tm9uY2UAAAAAAAAAAFUAcgBzAGEALQBNAGEAagBvAHIA") << QByteArray("NTLM TlRMTVNTUAADAAAAGAAYAFgAAAAYABgAQAAAABQAFABwAAAADAAMAIQAAAAWABYAkAAAAAAAAAAAAAAAAYIAAODgDeMQShvyBT8Hx92oLTxImumJ4bAA062Hym3v40aFucQ8R3qMQtYAZn1okufol1UAcgBzAGEALQBNAGEAagBvAHIAWgBhAHAAaABvAGQAVwBPAFIASwBTAFQAQQBUAEkATwBOAA==") << QByteArray("Zaphod") << QByteArray("Beeblebrox") << QByteArray() << QByteArray(); QTest::newRow("ntlm-challenge-type2-empty-domain") << QByteArray("NTLM TlRMTVNTUAACAAAAFAAUACgAAAABggAAU3J2Tm9uY2UAAAAAAAAAAFUAcgBzAGEALQBNAGEAagBvAHIA") << QByteArray("NTLM TlRMTVNTUAADAAAAGAAYAFgAAAAYABgAQAAAAAAAAAAAAAAADAAMAHAAAAAWABYAfAAAAAAAAAAAAAAAAYIAAODgDeMQShvyBT8Hx92oLTxImumJ4bAA062Hym3v40aFucQ8R3qMQtYAZn1okufol1oAYQBwAGgAbwBkAFcATwBSAEsAUwBUAEEAVABJAE8ATgA=") << QByteArray("\\Zaphod") << QByteArray("Beeblebrox") << QByteArray() << QByteArray(); } void HTTPAuthenticationTest::testAuthentication() { QFETCH(QByteArray, input); QFETCH(QByteArray, expectedResponse); QFETCH(QByteArray, user); QFETCH(QByteArray, pass); QFETCH(QByteArray, url); QFETCH(QByteArray, cnonce); QByteArray bestOffer; parseAuthHeader(input, &bestOffer, nullptr, nullptr); KAbstractHttpAuthentication *authObj = KAbstractHttpAuthentication::newAuth(bestOffer); QVERIFY(authObj); if (!cnonce.isEmpty()) { authObj->setDigestNonceValue(cnonce); } authObj->setChallenge(bestOffer, QUrl(url), "GET"); authObj->generateResponse(QString(user), QString(pass)); QCOMPARE(authObj->headerFragment().trimmed().constData(), expectedResponse.constData()); delete authObj; } void HTTPAuthenticationTest::testAuthenticationNTLMv2() { QByteArray input("NTLM TlRMTVNTUAACAAAABgAGADgAAAAFAokCT0wyUnb4OSQAAAAAAAAAAMYAxgA+AAAABgGxHQAAAA9UAFMAVAACAAYAVABTAFQAAQASAEQAVgBHAFIASwBWAFEAUABEAAQAKgB0AHMAdAAuAGQAagBrAGgAcQBjAGkAaABtAGMAbwBmAGoALgBvAHIAZwADAD4ARABWAEcAUgBLAFYAUQBQAEQALgB0AHMAdAAuAGQAagBrAGgAcQBjAGkAaABtAGMAbwBmAGoALgBvAHIAZwAFACIAZABqAGsAaABxAGMAaQBoAG0AYwBvAGYAagAuAG8AcgBnAAcACABvb9jXZl7RAQAAAAA="); QByteArray expectedResponse("TlRMTVNTUAADAAAAGAAYADYBAAD2APYAQAAAAAYABgBOAQAABgAGAFQBAAAWABYAWgEAAAAAAAAAAAAABQKJArXyhsxZPveKcfcV21viIsUBAQAAAAAAAAC8GQxfX9EBTHOi1kJbHbQAAAAAAgAGAFQAUwBUAAEAEgBEAFYARwBSAEsAVgBRAFAARAAEACoAdABzAHQALgBkAGoAawBoAHEAYwBpAGgAbQBjAG8AZgBqAC4AbwByAGcAAwA+AEQAVgBHAFIASwBWAFEAUABEAC4AdABzAHQALgBkAGoAawBoAHEAYwBpAGgAbQBjAG8AZgBqAC4AbwByAGcABQAiAGQAagBrAGgAcQBjAGkAaABtAGMAbwBmAGoALgBvAHIAZwAHAAgAb2/Y12Ze0QEAAAAAAAAAAOInN0N/15GHBtz3WXvvV159KG/2MbYk0FQAUwBUAGIAbwBiAFcATwBSAEsAUwBUAEEAVABJAE8ATgA="); QString user("TST\\bob"); QString pass("cacamas"); QString target("TST"); QByteArray bestOffer; parseAuthHeader(input, &bestOffer, nullptr, nullptr); KConfig conf; KConfigGroup confGroup = conf.group("test"); confGroup.writeEntry("EnableNTLMv2Auth", true); KAbstractHttpAuthentication *authObj = KAbstractHttpAuthentication::newAuth(bestOffer, &confGroup); QVERIFY(authObj); authObj->setChallenge(bestOffer, QUrl(), "GET"); authObj->generateResponse(QString(user), QString(pass)); QByteArray resp(QByteArray::fromBase64(authObj->headerFragment().trimmed().mid(5))); QByteArray expResp(QByteArray::fromBase64(expectedResponse)); /* Prepare responses stripped from any data that is variable. */ QByteArray strippedResp(resp); memset(strippedResp.data() + 0x40, 0, 0x10); // NTLMv2 MAC memset(strippedResp.data() + 0x58, 0, 0x10); // timestamp + client nonce memset(strippedResp.data() + 0x136, 0, 0x18); // LMv2 MAC QByteArray strippedExpResp(expResp); memset(strippedExpResp.data() + 0x40, 0, 0x10); // NTLMv2 MAC memset(strippedExpResp.data() + 0x58, 0, 0x10); // timestamp + client nonce memset(strippedExpResp.data() + 0x136, 0, 0x18); // LMv2 MAC /* Compare the stripped responses. */ QCOMPARE(strippedResp.toBase64(), strippedExpResp.toBase64()); /* Verify the NTLMv2 response MAC. */ QByteArray challenge(QByteArray::fromBase64(input.mid(5))); QByteArray serverNonce(challenge.mid(0x18, 8)); QByteArray uniPass(QString2UnicodeLE(pass)); QByteArray ntlmHash(QCryptographicHash::hash(uniPass, QCryptographicHash::Md4)); int i = user.indexOf('\\'); QString username; if (i >= 0) { username = user.mid(i + 1); } else { username = user; } QByteArray userTarget(QString2UnicodeLE(username.toUpper() + target)); QByteArray ntlm2Hash(hmacMD5(userTarget, ntlmHash)); QByteArray hashData(serverNonce + resp.mid(0x50, 230)); QByteArray mac(hmacMD5(hashData, ntlm2Hash)); QCOMPARE(mac.toHex(), resp.mid(0x40, 16).toHex()); /* Verify the LMv2 response MAC. */ QByteArray lmHashData(serverNonce + resp.mid(0x146, 8)); QByteArray lmHash(hmacMD5(lmHashData, ntlm2Hash)); QCOMPARE(lmHash.toHex(), resp.mid(0x136, 16).toHex()); delete authObj; } diff --git a/autotests/http/httpheaderdispositiontest.cpp b/autotests/http/httpheaderdispositiontest.cpp index 065dcc64..00cc19ff 100644 --- a/autotests/http/httpheaderdispositiontest.cpp +++ b/autotests/http/httpheaderdispositiontest.cpp @@ -1,382 +1,381 @@ /* This file is part of the KDE libraries Copyright (C) 2010,2011 Rolf Eike Beer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "httpheaderdispositiontest.h" #include #include -#include #include #include // QT5 TODO QTEST_GUILESS_MAIN(HeaderDispositionTest) QTEST_MAIN(HeaderDispositionTest) static void runTest(const QString &header, const QByteArray &result) { QMap parameters = contentDispositionParser(header); QList results = result.split('\n'); if (result.isEmpty()) { results.clear(); } for (const QByteArray &ba : qAsConst(results)) { QList values = ba.split('\t'); const QString key(QString::fromLatin1(values.takeFirst())); QVERIFY(parameters.contains(key)); const QByteArray val = values.takeFirst(); QVERIFY(values.isEmpty()); QCOMPARE(parameters[key], QString::fromUtf8(val.constData(), val.length())); } QCOMPARE(parameters.count(), results.count()); } void HeaderDispositionTest::runAllTests_data() { QTest::addColumn("header"); QTest::addColumn("result"); // http://greenbytes.de/tech/tc2231/ QTest::newRow("greenbytes-inlonly") << "inline" << QByteArray("type\tinline"); QTest::newRow("greenbytes-inlonlyquoted") << "\"inline\"" << QByteArray(); QTest::newRow("greenbytes-inlwithasciifilename") << "inline; filename=\"foo.html\"" << QByteArray("type\tinline\n" "filename\tfoo.html"); QTest::newRow("greenbytes-inlwithfnattach") << "inline; filename=\"Not an attachment!\"" << QByteArray("type\tinline\n" "filename\tNot an attachment!"); QTest::newRow("greenbytes-inlwithasciifilenamepdf") << "inline; filename=\"foo.pdf\"" << QByteArray("type\tinline\n" "filename\tfoo.pdf"); QTest::newRow("greenbytes-attonly") << "attachment" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attonlyquoted") << "\"attachment\"" << QByteArray(); QTest::newRow("greenbytes-attonlyucase") << "ATTACHMENT" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attwithasciifilename") << "attachment; filename=\"foo.html\"" << QByteArray("type\tattachment\n" "filename\tfoo.html"); QTest::newRow("greenbytes-attwithasciifnescapedchar") << "attachment; filename=\"f\\oo.html\"" << QByteArray("type\tattachment\n" "filename\tfoo.html"); QTest::newRow("greenbytes-attwithasciifnescapedquote") << "attachment; filename=\"\\\"quoting\\\" tested.html\"" << QByteArray("type\tattachment\n" "filename\t\"quoting\" tested.html"); QTest::newRow("greenbytes-attwithquotedsemicolon") << "attachment; filename=\"Here's a semicolon;.html\"" << QByteArray("type\tattachment\n" "filename\tHere's a semicolon;.html"); QTest::newRow("greenbytes-attwithfilenameandextparam") << "attachment; foo=\"bar\"; filename=\"foo.html\"" << QByteArray("type\tattachment\n" "foo\tbar\n" "filename\tfoo.html"); QTest::newRow("greenbytes-attwithfilenameandextparamescaped") << "attachment; foo=\"\\\"\\\\\";filename=\"foo.html\"" << QByteArray("type\tattachment\n" "foo\t\"\\\n" "filename\tfoo.html"); QTest::newRow("greenbytes-attwithasciifilenameucase") << "attachment; FILENAME=\"foo.html\"" << QByteArray("type\tattachment\n" "filename\tfoo.html"); // specification bug in RfC 2616, legal through RfC 2183 and 6266 QTest::newRow("greenbytes-attwithasciifilenamenq") << "attachment; filename=foo.html" << QByteArray("type\tattachment\n" "filename\tfoo.html"); QTest::newRow("greenbytes-attwithasciifilenamenqws") << "attachment; filename=foo bar.html" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attwithfntokensq") << "attachment; filename='foo.bar'" << QByteArray("type\tattachment\n" "filename\t'foo.bar'"); QTest::newRow("greenbytes-attwithisofnplain-x") << QStringLiteral("attachment; filename=\"foo-\xe4.html\"") << QByteArray("type\tattachment\n" "filename\tfoo-ä.html"); QTest::newRow("greenbytes-attwithisofnplain") << QString::fromLatin1("attachment; filename=\"foo-ä.html\"") << QByteArray("type\tattachment\n" "filename\tfoo-ä.html"); QTest::newRow("greenbytes-attwithfnrawpctenca") << "attachment; filename=\"foo-%41.html\"" << QByteArray("type\tattachment\n" "filename\tfoo-%41.html"); QTest::newRow("greenbytes-attwithfnusingpct") << "attachment; filename=\"50%.html\"" << QByteArray("type\tattachment\n" "filename\t50%.html"); QTest::newRow("greenbytes-attwithfnrawpctencaq") << "attachment; filename=\"foo-%\\41.html\"" << QByteArray("type\tattachment\n" "filename\tfoo-%41.html"); QTest::newRow("greenbytes-attwithnamepct") << "attachment; name=\"foo-%41.html\"" << QByteArray("type\tattachment\n" "name\tfoo-%41.html"); QTest::newRow("greenbytes-attwithfilenamepctandiso") << QStringLiteral("attachment; filename=\"\xe4-%41.html\"") << QByteArray("type\tattachment\n" "filename\tä-%41.html"); QTest::newRow("greenbytes-attwithfnrawpctenclong") << "attachment; filename=\"foo-%c3%a4-%e2%82%ac.html\"" << QByteArray("type\tattachment\n" "filename\tfoo-%c3%a4-%e2%82%ac.html"); QTest::newRow("greenbytes-attwithasciifilenamews1") << "attachment; filename =\"foo.html\"" << QByteArray("type\tattachment\n" "filename\tfoo.html"); QTest::newRow("greenbytes-attwith2filenames") << "attachment; filename=\"foo.html\"; filename=\"bar.html\"" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attfnbrokentoken") << "attachment; filename=foo[1](2).html" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attmissingdisposition") << "filename=foo.html" << QByteArray(); QTest::newRow("greenbytes-attmissingdisposition2") << "x=y; filename=foo.html" << QByteArray(); QTest::newRow("greenbytes-attmissingdisposition3") << "\"foo; filename=bar;baz\"; filename=qux" << QByteArray(); QTest::newRow("greenbytes-attmissingdisposition4") << "filename=foo.html, filename=bar.html" << QByteArray(); QTest::newRow("greenbytes-emptydisposition") << "; filename=foo.html" << QByteArray(); QTest::newRow("greenbytes-attbrokenquotedfn") << "attachment; filename=\"foo.html\".txt" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attbrokenquotedfn2") << "attachment; filename=\"bar" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attbrokenquotedfn3") << "attachment; filename=foo\"bar;baz\"qux" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attreversed") << "filename=foo.html; attachment" << QByteArray(); QTest::newRow("greenbytes-attconfusedparam") << "attachment; xfilename=foo.html" << QByteArray("type\tattachment\n" "xfilename\tfoo.html"); QTest::newRow("greenbytes-attabspath") << "attachment; filename=\"/foo.html\"" << QByteArray("type\tattachment\n" "filename\tfoo.html"); #ifdef Q_OS_WIN QTest::newRow("greenbytes-attabspath") << "attachment; filename=\"\\\\foo.html\"" << QByteArray("type\tattachment\n" "filename\tfoo.html"); #else // Q_OS_WIN QTest::newRow("greenbytes-attabspath") << "attachment; filename=\"\\\\foo.html\"" << QByteArray("type\tattachment\n" "filename\t\\foo.html"); #endif // Q_OS_WIN QTest::newRow("greenbytes-") << "attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"" << QByteArray("type\tattachment\n" "creation-date\tWed, 12 Feb 1997 16:29:51 -0500"); QTest::newRow("greenbytes-") << "attachment; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"" << QByteArray("type\tattachment\n" "modification-date\tWed, 12 Feb 1997 16:29:51 -0500"); QTest::newRow("greenbytes-dispext") << "foobar" << QByteArray("type\tfoobar"); QTest::newRow("greenbytes-dispextbadfn") << "attachment; example=\"filename=example.txt\"" << QByteArray("type\tattachment\n" "example\tfilename=example.txt"); QTest::newRow("greenbytes-attwithisofn2231iso") << "attachment; filename*=iso-8859-1''foo-%E4.html" << QByteArray("type\tattachment\n" "filename\tfoo-ä.html"); QTest::newRow("greenbytes-attwithfn2231utf8") << "attachment; filename*=UTF-8''foo-%c3%a4-%e2%82%ac.html" << QByteArray("type\tattachment\n" "filename\tfoo-ä-€.html"); QTest::newRow("greenbytes-attwithfn2231noc") << "attachment; filename*=''foo-%c3%a4-%e2%82%ac.html" << QByteArray("type\tattachment"); // it's not filename, but "filename " QTest::newRow("greenbytes-attwithfn2231ws1") << "attachment; filename *=UTF-8''foo-%c3%a4.html" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attwithfn2231ws2") << "attachment; filename*= UTF-8''foo-%c3%a4.html" << QByteArray("type\tattachment\n" "filename\tfoo-ä.html"); QTest::newRow("greenbytes-attwithfn2231ws3") << "attachment; filename* =UTF-8''foo-%c3%a4.html" << QByteArray("type\tattachment\n" "filename\tfoo-ä.html"); // argument must not be enclosed in double quotes QTest::newRow("greenbytes-attwithfn2231quot") << "attachment; filename*=\"UTF-8''foo-%c3%a4.html\"" << QByteArray("type\tattachment"); QTest::newRow("greenbytes-attwithfn2231dpct") << "attachment; filename*=UTF-8''A-%2541.html" << QByteArray("type\tattachment\n" "filename\tA-%41.html"); #ifdef Q_OS_WIN QTest::newRow("greenbytes-attwithfn2231abspathdisguised") << "attachment; filename*=UTF-8''%5cfoo.html" << QByteArray("type\tattachment\n" "filename\tfoo.html"); #else // Q_OS_WIN QTest::newRow("greenbytes-attwithfn2231abspathdisguised") << "attachment; filename*=UTF-8''%5cfoo.html" << QByteArray("type\tattachment\n" "filename\t\\foo.html"); #endif // Q_OS_WIN QTest::newRow("greenbytes-attfncont") << "attachment; filename*0=\"foo.\"; filename*1=\"html\"" << QByteArray("type\tattachment\n" "filename\tfoo.html"); QTest::newRow("greenbytes-attfncontenc") << "attachment; filename*0*=UTF-8''foo-%c3%a4; filename*1=\".html\"" << QByteArray("type\tattachment\n" "filename\tfoo-ä.html"); // no leading zeros QTest::newRow("greenbytes-attfncontlz") << "attachment; filename*0=\"foo\"; filename*01=\"bar\"" << QByteArray("type\tattachment\n" "filename\tfoo"); QTest::newRow("greenbytes-attfncontnc") << "attachment; filename*0=\"foo\"; filename*2=\"bar\"" << QByteArray("type\tattachment\n" "filename\tfoo"); // first element must have number 0 QTest::newRow("greenbytes-attfnconts1") << "attachment; filename*1=\"foo.\"; filename*2=\"html\"" << QByteArray("type\tattachment"); // we must not rely on element ordering QTest::newRow("greenbytes-attfncontord") << "attachment; filename*1=\"bar\"; filename*0=\"foo\"" << QByteArray("type\tattachment\n" "filename\tfoobar"); // specifying both param and param* is allowed, param* should be taken QTest::newRow("greenbytes-attfnboth") << "attachment; filename=\"foo-ae.html\"; filename*=UTF-8''foo-%c3%a4.html" << QByteArray("type\tattachment\n" "filename\tfoo-ä.html"); // specifying both param and param* is allowed, param* should be taken QTest::newRow("greenbytes-attfnboth2") << "attachment; filename*=UTF-8''foo-%c3%a4.html; filename=\"foo-ae.html\"" << QByteArray("type\tattachment\n" "filename\tfoo-ä.html"); QTest::newRow("greenbytes-attnewandfn") << "attachment; foobar=x; filename=\"foo.html\"" << QByteArray("type\tattachment\n" "filename\tfoo.html\n" "foobar\tx"); // invalid argument, should be ignored QTest::newRow("greenbytes-attrfc2047token") << "attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=" << QByteArray("type\tattachment"); QTest::newRow("space_before_value") << "attachment; filename= \"foo.html\"" << QByteArray("type\tattachment\n" "filename\tfoo.html"); // no character set given but 8 bit characters QTest::newRow("8bit_in_ascii") << "attachment; filename*=''foo-%c3%a4.html" << QByteArray("type\tattachment"); // there may not be gaps in numbering QTest::newRow("continuation013") << "attachment; filename*0=\"foo.\"; filename*1=\"html\"; filename*3=\"bar\"" << QByteArray("type\tattachment\n" "filename\tfoo.html"); // "wrong" element ordering and encoding QTest::newRow("reversed_continuation+encoding") << "attachment; filename*1=\"html\"; filename*0*=us-ascii''foo." << QByteArray("type\tattachment\n" "filename\tfoo.html"); // unknown charset QTest::newRow("unknown_charset") << "attachment; filename*=unknown''foo" << QByteArray("type\tattachment"); // no apostrophs QTest::newRow("encoding-no-apostrophs") << "attachment; filename*=foo" << QByteArray("type\tattachment"); // only one apostroph QTest::newRow("encoding-one-apostroph") << "attachment; filename*=us-ascii'foo" << QByteArray("type\tattachment"); // duplicate filename, both should be ignored and parsing should stop QTest::newRow("duplicate-filename") << "attachment; filename=foo; filename=bar; foo=bar" << QByteArray("type\tattachment"); // garbage after closing quote, parsing should stop there QTest::newRow("garbage_after_closing_quote") << "attachment; filename*=''foo; bar=\"f\"oo; baz=foo" << QByteArray("type\tattachment\n" "filename\tfoo"); // trailing whitespace should be ignored QTest::newRow("whitespace_after_value") << "attachment; filename=\"foo\" ; bar=baz" << QByteArray("type\tattachment\n" "filename\tfoo\n" "bar\tbaz"); // invalid syntax for type QTest::newRow("invalid_type1") << "filename=foo.html" << QByteArray(); // invalid syntax for type QTest::newRow("invalid_type2") << "inline{; filename=\"foo\"" << QByteArray(); QTest::newRow("invalid_type3") << "foo bar; filename=\"foo\"" << QByteArray(); QTest::newRow("invalid_type4") << "foo\tbar; filename=\"foo\"" << QByteArray(); // missing closing quote, so parameter is broken QTest::newRow("no_closing_quote") << "attachment; filename=\"bar" << QByteArray("type\tattachment"); // we ignore any path given in the header and use only the filename QTest::newRow("full_path_given") << "attachment; filename=\"/etc/shadow\"" << QByteArray("type\tattachment\n" "filename\tshadow"); // we ignore any path given in the header and use only the filename even if there is an error later QTest::newRow("full_path_and_parse_error") << "attachment; filename=\"/etc/shadow\"; foo=\"baz\"; foo=\"bar\"" << QByteArray("type\tattachment\n" "filename\tshadow"); // control characters are forbidden in the quoted string QTest::newRow("control_character_in_value") << "attachment; filename=\"foo\003\"" << QByteArray("type\tattachment"); // duplicate keys must be ignored QTest::newRow("duplicate_with_continuation") << "attachment; filename=\"bar\"; filename*0=\"foo.\"; filename*1=\"html\"" << QByteArray("type\tattachment"); // percent encoding, invalid first character QTest::newRow("percent-first-char-invalid") << "attachment; filename*=UTF-8''foo-%o5.html" << QByteArray("type\tattachment"); // percent encoding, invalid second character QTest::newRow("percent-second-char-invalid") << "attachment; filename*=UTF-8''foo-%5o.html" << QByteArray("type\tattachment"); // percent encoding, both characters invalid QTest::newRow("greenbytes-attwithfn2231nbadpct2") << "attachment; filename*=UTF-8''foo-%oo.html" << QByteArray("type\tattachment"); // percent encoding, invalid second character QTest::newRow("percent-second-char-missing") << "attachment; filename*=UTF-8''foo-%f.html" << QByteArray("type\tattachment"); // percent encoding, too short value QTest::newRow("percent-short-encoding-at-end") << "attachment; filename*=UTF-8''foo-%f" << QByteArray("type\tattachment"); } #if 0 // currently unclear if our behaviour is only accidentally correct // invalid syntax { "inline; attachment; filename=foo.html", "type\tinline" }, // invalid syntax { "attachment; inline; filename=foo.html", "type\tattachment" }, // deactivated for now: failing due to missing implementation { "attachment; filename=\"foo-ä.html\"", "type\tattachment\n" "filename\tfoo-ä.html" }, // deactivated for now: not the same utf, no idea if the expected value is actually correct { "attachment; filename*=UTF-8''foo-a%cc%88.html", "type\tattachment\n" "filename\tfoo-ä.html" } // deactivated for now: only working to missing implementation // string is not valid iso-8859-1 so filename should be ignored //"attachment; filename*=iso-8859-1''foo-%c3%a4-%e2%82%ac.html", //"type\tattachment", // deactivated for now: only working to missing implementation // should not be decoded //"attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"", //"type\tattachment\n" //"filename\t=?ISO-8859-1?Q?foo-=E4.html?=", }; #endif void HeaderDispositionTest::runAllTests() { QFETCH(QString, header); QFETCH(QByteArray, result); runTest(header, result); } diff --git a/autotests/http_jobtest.cpp b/autotests/http_jobtest.cpp index 4911855c..89c89474 100644 --- a/autotests/http_jobtest.cpp +++ b/autotests/http_jobtest.cpp @@ -1,111 +1,110 @@ /* This file is part of the KDE libraries Copyright (c) 2016 David Faure 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 ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser 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 "httpserver_p.h" class HTTPJobTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testBasicGet(); void testErrorPage(); void testMimeTypeDetermination(); }; void HTTPJobTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); // To let ctest exit, we shouldn't start kio_http_cache_cleaner qputenv("KIO_DISABLE_CACHE_CLEANER", "yes"); } void HTTPJobTest::testBasicGet() { static const char response[] = "Hello world"; HttpServerThread server(response, HttpServerThread::Public); KIO::StoredTransferJob *job = KIO::storedGet(QUrl(server.endPoint())); job->setUiDelegate(nullptr); QVERIFY(job->exec()); QCOMPARE(QString::fromLatin1(job->data()), QString::fromLatin1(response)); } void HTTPJobTest::testErrorPage() { static const char response[] = "This is a response\nFile not found"; HttpServerThread server(response, HttpServerThread::Error404); server.setContentType("text/html"); // First we get an error page KIO::StoredTransferJob *job = KIO::storedGet(QUrl(server.endPoint())); job->setUiDelegate(nullptr); QVERIFY(job->exec()); QCOMPARE(QString::fromLatin1(job->data()), QString::fromLatin1(response)); QVERIFY(job->isErrorPage()); QCOMPARE(job->error(), 0); // Second we disable error page, and get the actual job error job = KIO::storedGet(QUrl(server.endPoint())); job->setUiDelegate(nullptr); job->addMetaData(QStringLiteral("errorPage"), QStringLiteral("false")); // maybe this should be a proper setter... QVERIFY(!job->exec()); QVERIFY(!job->isErrorPage()); QCOMPARE(job->error(), int(KIO::ERR_DOES_NOT_EXIST)); // To check that kio_http did read and discard the body correctly, do another working download. server.setResponseData("Some HTML page here"); server.setFeatures(HttpServerThread::Public); server.setContentType(""); job = KIO::storedGet(QUrl(server.endPoint())); job->setUiDelegate(nullptr); QSignalSpy mimeTypeSpy(job, SIGNAL(mimetype(KIO::Job*,QString))); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); QCOMPARE(mimeTypeSpy.count(), 1); QCOMPARE(mimeTypeSpy.at(0).at(1).toString(), QStringLiteral("text/html")); } void HTTPJobTest::testMimeTypeDetermination() { static const char response[] = "Some HTML page here"; HttpServerThread server(response, HttpServerThread::Public); // Add a trailing slash to ensure kio_http doesn't confuse QMimeDatabase with it. KIO::StoredTransferJob *job = KIO::storedGet(QUrl(server.endPoint() + '/')); job->setUiDelegate(nullptr); QSignalSpy mimeTypeSpy(job, SIGNAL(mimetype(KIO::Job*,QString))); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); QCOMPARE(mimeTypeSpy.count(), 1); QCOMPARE(mimeTypeSpy.at(0).at(1).toString(), QStringLiteral("text/html")); } QTEST_MAIN(HTTPJobTest) #include "http_jobtest.moc" diff --git a/autotests/httpserver_p.cpp b/autotests/httpserver_p.cpp index 6bb3d77f..38126169 100644 --- a/autotests/httpserver_p.cpp +++ b/autotests/httpserver_p.cpp @@ -1,287 +1,285 @@ /**************************************************************************** ** Copyright (C) 2010-2016 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com. ** Author: David Faure ** All rights reserved. ** ** This file initially comes from the KD Soap library. ** ** This file may be distributed and/or modified under the terms of the ** GNU Lesser General Public License version 2.1 and version 3 as published by the ** Free Software Foundation and appearing in the file COPYING.LIB included. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ** Contact info@kdab.com if any conditions of this licensing are not ** clear to you. ** **********************************************************************/ #include "httpserver_p.h" #include #include -#include -#include #include static bool splitHeadersAndData(const QByteArray &request, QByteArray &header, QByteArray &data) { const int sep = request.indexOf("\r\n\r\n"); if (sep <= 0) { return false; } header = request.left(sep); data = request.mid(sep + 4); return true; } typedef QMap HeadersMap; static HeadersMap parseHeaders(const QByteArray &headerData) { HeadersMap headersMap; QBuffer sourceBuffer; sourceBuffer.setData(headerData); sourceBuffer.open(QIODevice::ReadOnly); // The first line is special, it's the GET or POST line const QList firstLine = sourceBuffer.readLine().split(' '); if (firstLine.count() < 3) { qDebug() << "Malformed HTTP request:" << firstLine; return headersMap; } const QByteArray request = firstLine[0]; const QByteArray path = firstLine[1]; const QByteArray httpVersion = firstLine[2]; if (request != "GET" && request != "POST") { qDebug() << "Unknown HTTP request:" << firstLine; return headersMap; } headersMap.insert("_path", path); headersMap.insert("_httpVersion", httpVersion); while (!sourceBuffer.atEnd()) { const QByteArray line = sourceBuffer.readLine(); const int pos = line.indexOf(':'); if (pos == -1) { qDebug() << "Malformed HTTP header:" << line; } const QByteArray header = line.left(pos); const QByteArray value = line.mid(pos + 1).trimmed(); // remove space before and \r\n after //qDebug() << "HEADER" << header << "VALUE" << value; headersMap.insert(header, value); } return headersMap; } enum Method { None, Basic, Plain, Login, Ntlm, CramMd5, DigestMd5 }; static void parseAuthLine(const QString &str, Method *method, QString *headerVal) { *method = None; // The code below (from QAuthenticatorPrivate::parseHttpResponse) // is supposed to be run in a loop, apparently // (multiple WWW-Authenticate lines? multiple values in the line?) //qDebug() << "parseAuthLine() " << str; if (*method < Basic && str.startsWith(QLatin1String("Basic"), Qt::CaseInsensitive)) { *method = Basic; *headerVal = str.mid(6); } else if (*method < Ntlm && str.startsWith(QLatin1String("NTLM"), Qt::CaseInsensitive)) { *method = Ntlm; *headerVal = str.mid(5); } else if (*method < DigestMd5 && str.startsWith(QLatin1String("Digest"), Qt::CaseInsensitive)) { *method = DigestMd5; *headerVal = str.mid(7); } } QByteArray HttpServerThread::makeHttpResponse(const QByteArray &responseData) const { QByteArray httpResponse; if (m_features & Error404) { httpResponse += "HTTP/1.1 404 Not Found\r\n"; } else { httpResponse += "HTTP/1.1 200 OK\r\n"; } if (!m_contentType.isEmpty()) { httpResponse += "Content-Type: " + m_contentType + "\r\n"; } httpResponse += "Mozilla/5.0 (X11; Linux x86_64) KHTML/5.20.0 (like Gecko) Konqueror/5.20\r\n"; httpResponse += "Content-Length: "; httpResponse += QByteArray::number(responseData.size()); httpResponse += "\r\n"; // We don't support multiple connections so let's ask the client // to close the connection every time. httpResponse += "Connection: close\r\n"; httpResponse += "\r\n"; httpResponse += responseData; return httpResponse; } void HttpServerThread::disableSsl() { m_server->disableSsl(); } void HttpServerThread::finish() { KIO::Job *job = KIO::get(QUrl(endPoint() + QLatin1String("/terminateThread"))); job->exec(); } void HttpServerThread::run() { m_server = new BlockingHttpServer(m_features & Ssl); m_server->listen(); QMutexLocker lock(&m_mutex); m_port = m_server->serverPort(); lock.unlock(); m_ready.release(); const bool doDebug = qEnvironmentVariableIsSet("HTTP_TEST_DEBUG"); if (doDebug) { qDebug() << "HttpServerThread listening on port" << m_port; } // Wait for first connection (we'll wait for further ones inside the loop) QTcpSocket *clientSocket = m_server->waitForNextConnectionSocket(); Q_ASSERT(clientSocket); Q_FOREVER { // get the "request" packet if (doDebug) { qDebug() << "HttpServerThread: waiting for read"; } if (clientSocket->state() == QAbstractSocket::UnconnectedState || !clientSocket->waitForReadyRead(2000)) { if (clientSocket->state() == QAbstractSocket::UnconnectedState) { delete clientSocket; if (doDebug) { qDebug() << "Waiting for next connection..."; } clientSocket = m_server->waitForNextConnectionSocket(); Q_ASSERT(clientSocket); continue; // go to "waitForReadyRead" } else { qDebug() << "HttpServerThread:" << clientSocket->error() << "waiting for \"request\" packet"; break; } } const QByteArray request = m_partialRequest + clientSocket->readAll(); if (doDebug) { qDebug() << "HttpServerThread: request:" << request; } // Split headers and request xml lock.relock(); const bool splitOK = splitHeadersAndData(request, m_receivedHeaders, m_receivedData); if (!splitOK) { //if (doDebug) // qDebug() << "Storing partial request" << request; m_partialRequest = request; continue; } m_headers = parseHeaders(m_receivedHeaders); if (m_headers.value("Content-Length").toInt() > m_receivedData.size()) { //if (doDebug) // qDebug() << "Storing partial request" << request; m_partialRequest = request; continue; } m_partialRequest.clear(); if (m_headers.value("_path").endsWith("terminateThread")) { // we're asked to exit break; // normal exit } lock.unlock(); //qDebug() << "headers received:" << m_receivedHeaders; //qDebug() << headers; //qDebug() << "data received:" << m_receivedData; if (m_features & BasicAuth) { QByteArray authValue = m_headers.value("Authorization"); if (authValue.isEmpty()) { authValue = m_headers.value("authorization"); // as sent by Qt-4.5 } bool authOk = false; if (!authValue.isEmpty()) { //qDebug() << "got authValue=" << authValue; // looks like "Basic " Method method; QString headerVal; parseAuthLine(QString::fromLatin1(authValue.data(), authValue.size()), &method, &headerVal); //qDebug() << "method=" << method << "headerVal=" << headerVal; switch (method) { case None: // we want auth, so reject "None" break; case Basic: { const QByteArray userPass = QByteArray::fromBase64(headerVal.toLatin1()); //qDebug() << userPass; // TODO if (validateAuth(userPass)) { if (userPass == ("kdab:testpass")) { authOk = true; } break; } default: qWarning("Unsupported authentication mechanism %s", authValue.constData()); } } if (!authOk) { // send auth request (Qt supports basic, ntlm and digest) const QByteArray unauthorized = "HTTP/1.1 401 Authorization Required\r\nWWW-Authenticate: Basic realm=\"example\"\r\nContent-Length: 0\r\n\r\n"; clientSocket->write(unauthorized); if (!clientSocket->waitForBytesWritten(2000)) { qDebug() << "HttpServerThread:" << clientSocket->error() << "writing auth request"; break; } continue; } } // send response const QByteArray response = makeHttpResponse(m_dataToSend); if (doDebug) { qDebug() << "HttpServerThread: writing" << response; } clientSocket->write(response); clientSocket->flush(); } // all done... delete clientSocket; delete m_server; if (doDebug) { qDebug() << "HttpServerThread terminated"; } } void BlockingHttpServer::incomingConnection(qintptr socketDescriptor) { if (doSsl) { QSslSocket *serverSocket = new QSslSocket; serverSocket->setParent(this); serverSocket->setSocketDescriptor(socketDescriptor); connect(serverSocket, SIGNAL(sslErrors(QList)), this, SLOT(slotSslErrors(QList))); // TODO setupSslServer(serverSocket); //qDebug() << "Created QSslSocket, starting server encryption"; serverSocket->startServerEncryption(); sslSocket = serverSocket; // If startServerEncryption fails internally [and waitForEncrypted hangs], // then this is how to debug it. // A way to catch such errors is really missing in Qt.. //qDebug() << "startServerEncryption said:" << sslSocket->errorString(); bool ok = serverSocket->waitForEncrypted(); Q_ASSERT(ok); Q_UNUSED(ok); } else { QTcpServer::incomingConnection(socketDescriptor); } } diff --git a/autotests/httpserver_p.h b/autotests/httpserver_p.h index f86fea97..1bd91fd4 100644 --- a/autotests/httpserver_p.h +++ b/autotests/httpserver_p.h @@ -1,179 +1,177 @@ /**************************************************************************** ** Copyright (C) 2010-2016 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com. ** Author: David Faure ** All rights reserved. ** ** This file initially comes from the KD Soap library. ** ** This file may be distributed and/or modified under the terms of the ** GNU Lesser General Public License version 2.1 and version 3 as published by the ** Free Software Foundation and appearing in the file COPYING.LIB included. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ** Contact info@kdab.com if any conditions of this licensing are not ** clear to you. ** **********************************************************************/ #ifndef HTTPSERVER_P_H #define HTTPSERVER_P_H #include #include #include #include #include -#include -#include #include class BlockingHttpServer; class HttpServerThread : public QThread { Q_OBJECT public: enum Feature { Public = 0, // HTTP with no ssl and no authentication needed Ssl = 1, // HTTPS BasicAuth = 2, // Requires authentication Error404 = 4 // Return "404 not found" // bitfield, next item is 8 }; Q_DECLARE_FLAGS(Features, Feature) HttpServerThread(const QByteArray &dataToSend, Features features) : m_dataToSend(dataToSend), m_features(features) { start(); m_ready.acquire(); } ~HttpServerThread() { finish(); wait(); } void setContentType(const QByteArray &mime) { QMutexLocker lock(&m_mutex); m_contentType = mime; } void setResponseData(const QByteArray &data) { QMutexLocker lock(&m_mutex); m_dataToSend = data; } void setFeatures(Features features) { QMutexLocker lock(&m_mutex); m_features = features; } void disableSsl(); inline int serverPort() const { QMutexLocker lock(&m_mutex); return m_port; } QString endPoint() const { return QString::fromLatin1("%1://127.0.0.1:%2/path") .arg(QString::fromLatin1((m_features & Ssl) ? "https" : "http")) .arg(serverPort()); } void finish(); QByteArray receivedData() const { QMutexLocker lock(&m_mutex); return m_receivedData; } QByteArray receivedHeaders() const { QMutexLocker lock(&m_mutex); return m_receivedHeaders; } void resetReceivedBuffers() { QMutexLocker lock(&m_mutex); m_receivedData.clear(); m_receivedHeaders.clear(); } QByteArray header(const QByteArray &value) const { QMutexLocker lock(&m_mutex); return m_headers.value(value); } protected: /* \reimp */ void run() override; private: QByteArray makeHttpResponse(const QByteArray &responseData) const; private: QByteArray m_partialRequest; QSemaphore m_ready; QByteArray m_dataToSend; QByteArray m_contentType; mutable QMutex m_mutex; // protects the 4 vars below QByteArray m_receivedData; QByteArray m_receivedHeaders; QMap m_headers; int m_port; Features m_features; BlockingHttpServer *m_server; }; Q_DECLARE_OPERATORS_FOR_FLAGS(HttpServerThread::Features) // A blocking http server (must be used in a thread) which supports SSL. class BlockingHttpServer : public QTcpServer { Q_OBJECT public: BlockingHttpServer(bool ssl) : doSsl(ssl), sslSocket(nullptr) {} ~BlockingHttpServer() {} QTcpSocket *waitForNextConnectionSocket() { if (!waitForNewConnection(20000)) { // 2000 would be enough, except in valgrind return nullptr; } if (doSsl) { Q_ASSERT(sslSocket); return sslSocket; } else { //qDebug() << "returning nextPendingConnection"; return nextPendingConnection(); } } void incomingConnection(qintptr socketDescriptor) override; void disableSsl() { doSsl = false; } private Q_SLOTS: void slotSslErrors(const QList &errors) { qDebug() << "server-side: slotSslErrors" << sslSocket->errorString() << errors; } private: bool doSsl; QTcpSocket *sslSocket; }; #endif /* HTTPSERVER_P_H */ diff --git a/autotests/jobremotetest.cpp b/autotests/jobremotetest.cpp index 1982e0c8..767096d2 100644 --- a/autotests/jobremotetest.cpp +++ b/autotests/jobremotetest.cpp @@ -1,389 +1,387 @@ /* This file is part of the KDE project Copyright (C) 2004-2006 David Faure Copyright (C) 2008 Norbert Frese 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 "jobremotetest.h" #include #include #include -#include #include #include -#include #include #include #include #include #include #include #include #include //#include "kiotesthelper.h" // createTestFile etc. QTEST_MAIN(JobRemoteTest) QDateTime s_referenceTimeStamp; // The code comes partly from jobtest.cpp static QUrl remoteTmpUrl() { QString customDir(qgetenv("KIO_JOBREMOTETEST_REMOTETMP")); if (customDir.isEmpty()) { return QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + '/'); } else { // Could be a path or a URL return QUrl::fromUserInput(customDir + '/'); } } static QString localTmpDir() { #ifdef Q_OS_WIN return QDir::tempPath() + "/jobremotetest/"; #else // This one needs to be on another partition return QStringLiteral("/tmp/jobremotetest/"); #endif } static bool myExists(const QUrl &url) { KIO::Job *job = KIO::stat(url, KIO::StatJob::DestinationSide, 0, KIO::HideProgressInfo); job->setUiDelegate(nullptr); return job->exec(); } static bool myMkdir(const QUrl &url) { KIO::Job *job = KIO::mkdir(url, -1); job->setUiDelegate(nullptr); return job->exec(); } void JobRemoteTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); // 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(); QUrl url = remoteTmpUrl(); if (!myExists(url)) { const bool ok = url.isLocalFile() ? QDir().mkpath(url.toLocalFile()) : myMkdir(url); if (!ok) { qFatal("couldn't create %s", qPrintable(url.toString())); } } const bool ok = QDir().mkpath(localTmpDir()); if (!ok) { qFatal("couldn't create %s", qPrintable(localTmpDir())); } } static void delDir(const QUrl &pathOrUrl) { KIO::Job *job = KIO::del(pathOrUrl, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->exec(); } void JobRemoteTest::cleanupTestCase() { delDir(remoteTmpUrl()); delDir(QUrl::fromLocalFile(localTmpDir())); } void JobRemoteTest::enterLoop() { QEventLoop eventLoop; connect(this, &JobRemoteTest::exitLoop, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } ///// void JobRemoteTest::putAndGet() { QUrl u(remoteTmpUrl()); u.setPath(u.path() + "putAndGetFile"); 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, &JobRemoteTest::slotResult); connect(job, &KIO::TransferJob::dataReq, this, &JobRemoteTest::slotDataReq); m_result = -1; m_dataReqCount = 0; enterLoop(); QVERIFY(m_result == 0); // no error m_result = -1; KIO::StoredTransferJob *getJob = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); getJob->setUiDelegate(nullptr); connect(getJob, &KJob::result, this, &JobRemoteTest::slotGetResult); enterLoop(); QCOMPARE(m_result, 0); // no error QCOMPARE(m_data, QByteArray("This is a test for KIO::put()\n")); //QCOMPARE( m_data.size(), 11 ); } void JobRemoteTest::slotGetResult(KJob *job) { m_result = job->error(); m_data = static_cast(job)->data(); emit exitLoop(); } void JobRemoteTest::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 JobRemoteTest::slotResult(KJob *job) { m_result = job->error(); emit exitLoop(); } //// void JobRemoteTest::openFileWriting() { m_rwCount = 0; QUrl u(remoteTmpUrl()); u.setPath(u.path() + "openFileWriting"); fileJob = KIO::open(u, QIODevice::WriteOnly); fileJob->setUiDelegate(nullptr); connect(fileJob, &KJob::result, this, &JobRemoteTest::slotResult); connect(fileJob, &KIO::FileJob::data, this, &JobRemoteTest::slotFileJobData); connect(fileJob, &KIO::FileJob::open, this, &JobRemoteTest::slotFileJobOpen); connect(fileJob, &KIO::FileJob::written, this, &JobRemoteTest::slotFileJobWritten); connect(fileJob, &KIO::FileJob::position, this, &JobRemoteTest::slotFileJobPosition); connect(fileJob, SIGNAL(close(KIO::Job*)), this, SLOT(slotFileJobClose(KIO::Job*))); m_result = -1; enterLoop(); QEXPECT_FAIL("", "Needs fixing in kio_file", Abort); QVERIFY(m_result == 0); // no error KIO::StoredTransferJob *getJob = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); getJob->setUiDelegate(nullptr); connect(getJob, &KJob::result, this, &JobRemoteTest::slotGetResult); enterLoop(); QCOMPARE(m_result, 0); // no error qDebug() << "m_data: " << m_data; QCOMPARE(m_data, QByteArray("test....test....test....test....test....test....end")); } void JobRemoteTest::slotFileJobData(KIO::Job *job, const QByteArray &data) { Q_UNUSED(job); Q_UNUSED(data); } void JobRemoteTest::slotFileJobRedirection(KIO::Job *job, const QUrl &url) { Q_UNUSED(job); Q_UNUSED(url); } void JobRemoteTest::slotFileJobMimetype(KIO::Job *job, const QString &type) { Q_UNUSED(job); Q_UNUSED(type); } void JobRemoteTest::slotFileJobOpen(KIO::Job *job) { Q_UNUSED(job); fileJob->seek(0); } void JobRemoteTest::slotFileJobWritten(KIO::Job *job, KIO::filesize_t written) { Q_UNUSED(job); Q_UNUSED(written); if (m_rwCount > 5) { fileJob->close(); } else { fileJob->seek(m_rwCount * 8); m_rwCount++; } } void JobRemoteTest::slotFileJobPosition(KIO::Job *job, KIO::filesize_t offset) { Q_UNUSED(job); Q_UNUSED(offset); const QByteArray data("test....end"); fileJob->write(data); } void JobRemoteTest::slotFileJobClose(KIO::Job *job) { Q_UNUSED(job); qDebug() << "+++++++++ closed"; } //// void JobRemoteTest::openFileReading() { QUrl u(remoteTmpUrl()); u.setPath(u.path() + "openFileReading"); const QByteArray putData("test1test2test3test4test5"); KIO::StoredTransferJob *putJob = KIO::storedPut(putData, 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 putJob->setModificationTime(mtime); putJob->setUiDelegate(nullptr); connect(putJob, &KJob::result, this, &JobRemoteTest::slotResult); m_result = -1; enterLoop(); QVERIFY(m_result == 0); // no error m_rwCount = 4; m_data = QByteArray(); fileJob = KIO::open(u, QIODevice::ReadOnly); fileJob->setUiDelegate(nullptr); connect(fileJob, &KJob::result, this, &JobRemoteTest::slotResult); connect(fileJob, &KIO::FileJob::data, this, &JobRemoteTest::slotFileJob2Data); connect(fileJob, &KIO::FileJob::open, this, &JobRemoteTest::slotFileJob2Open); connect(fileJob, &KIO::FileJob::written, this, &JobRemoteTest::slotFileJob2Written); connect(fileJob, &KIO::FileJob::position, this, &JobRemoteTest::slotFileJob2Position); connect(fileJob, SIGNAL(close(KIO::Job*)), this, SLOT(slotFileJob2Close(KIO::Job*))); m_result = -1; enterLoop(); QVERIFY(m_result == 0); // no error qDebug() << "resulting m_data: " << QString(m_data); QCOMPARE(m_data, QByteArray("test5test4test3test2test1")); } void JobRemoteTest::slotFileJob2Data(KIO::Job *job, const QByteArray &data) { Q_UNUSED(job); qDebug() << "m_rwCount = " << m_rwCount << " data: " << data; m_data.append(data); if (m_rwCount < 0) { fileJob->close(); } else { fileJob->seek(m_rwCount-- * 5); } } void JobRemoteTest::slotFileJob2Redirection(KIO::Job *job, const QUrl &url) { Q_UNUSED(job); Q_UNUSED(url); } void JobRemoteTest::slotFileJob2Mimetype(KIO::Job *job, const QString &type) { Q_UNUSED(job); qDebug() << "mimetype: " << type; } void JobRemoteTest::slotFileJob2Open(KIO::Job *job) { Q_UNUSED(job); fileJob->seek(m_rwCount-- * 5); } void JobRemoteTest::slotFileJob2Written(KIO::Job *job, KIO::filesize_t written) { Q_UNUSED(job); Q_UNUSED(written); } void JobRemoteTest::slotFileJob2Position(KIO::Job *job, KIO::filesize_t offset) { Q_UNUSED(job); qDebug() << "position : " << offset << " -> read (5)"; fileJob->read(5); } void JobRemoteTest::slotFileJob2Close(KIO::Job *job) { Q_UNUSED(job); qDebug() << "+++++++++ job2 closed"; } //// void JobRemoteTest::slotMimetype(KIO::Job *job, const QString &type) { QVERIFY(job != nullptr); m_mimetype = type; } diff --git a/autotests/jobtest.cpp b/autotests/jobtest.cpp index 3e2fad97..8e315a2e 100644 --- a/autotests/jobtest.cpp +++ b/autotests/jobtest.cpp @@ -1,2120 +1,2121 @@ /* 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 "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); QVERIFY(job->exec()); 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); QVERIFY(job->exec()); 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(); QVERIFY(job->exec()); 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(); }); QVERIFY(job->exec()); 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(); }); QVERIFY(job->exec()); 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); bool ok = job->exec(); QVERIFY(ok); 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); ok = job->exec(); QVERIFY(ok); 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); QVERIFY(job->exec()); 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); QVERIFY(job->exec()); 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); bool ok = job->exec(); QVERIFY(ok); 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); ok = job->exec(); QVERIFY(ok); // Do it again, without Overwrite (should fail). job = KIO::copyAs(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); ok = job->exec(); QVERIFY(!ok); } #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 QVERIFY(job->exec()); 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); QVERIFY(job->exec()); // 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()); QVERIFY(job->exec()); 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()); QVERIFY(job->exec()); 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); bool ok = job->exec(); QVERIFY(ok); 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); ok = job->exec(); QVERIFY(ok); 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); bool ok = job->exec(); if (!ok) { qWarning() << job->error(); } QVERIFY(ok); 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); ok = job->exec(); QVERIFY(ok); 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); bool ok = job->exec(); QVERIFY2(ok, 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); bool ok = job->exec(); QVERIFY(ok); 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(QStringLiteral(",")); 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); bool ok = job->exec(); QVERIFY(ok); 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); bool ok = job->exec(); QVERIFY(!ok); 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))); bool ok = job->exec(); QVERIFY(ok); 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); ok = job->exec(); QVERIFY(ok); 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) bool ok = job->exec(); QVERIFY(!ok); // 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); bool ok = job->exec(); QVERIFY(ok); 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); bool ok = job->exec(); QVERIFY(ok); 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); bool ok = job->exec(); QVERIFY(ok); 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; dt.start(); KIO::Job *job = KIO::del(dirs, KIO::HideProgressInfo); job->setUiDelegate(nullptr); bool ok = job->exec(); QVERIFY(ok); 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; 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); bool ok = job->exec(); QVERIFY(ok); 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; 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); bool ok = job->exec(); QVERIFY(ok); 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)); QVERIFY(job->exec()); 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); bool ok = job->exec(); QVERIFY(ok); // 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); bool ok = job->exec(); QVERIFY(ok); // 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); bool ok = job->exec(); QVERIFY(ok); // 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); bool ok = job->exec(); QVERIFY(ok); 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); QVERIFY(job->exec()); 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); QVERIFY(job->exec()); 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); QVERIFY(job->exec()); 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))); bool ok = job->exec(); QVERIFY(ok); 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))); bool ok = job->exec(); QVERIFY(ok); 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*))); bool ok = job->exec(); QVERIFY(!ok); 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); bool ok = job->exec(); QVERIFY(ok); 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); QVERIFY(job->exec()); 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(); bool ok = job->exec(); qDebug() << QDir(destDir).entryList(); QVERIFY(ok); 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::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)); }); QVERIFY(job->exec()); 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); bool ok = job->exec(); QVERIFY(ok); 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); ok = job->exec(); QVERIFY(ok); QVERIFY(!QFile::exists(sourceFile)); // it was moved // Now same thing when the target is a symlink to another file createTestFile(sourceFile); createTestFile(sourceFile + "2"); createTestSymlink(existingDest, QFile::encodeName(sourceFile + "2")); QVERIFY(QFile::exists(existingDest)); job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); ok = job->exec(); QVERIFY(ok); 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); ok = job->exec(); QVERIFY(ok); 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); bool ok = job->exec(); QVERIFY(!ok); 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); QVERIFY(job->exec()); 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); QVERIFY(symlinkJob->exec()); 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); QVERIFY(job->exec()); 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); QVERIFY(job->exec()); 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); QVERIFY(job->exec()); QVERIFY(QFileInfo(dest).isSymLink()); // Changing the link target, with overwrite, works job = KIO::linkAs(QUrl::fromLocalFile(sourceFile + "2"), QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo); QVERIFY(job->exec()); QVERIFY(QFileInfo(dest).isSymLink()); QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + "2")); // Changing the link target using KIO::symlink, with overwrite, works KIO::Job *symlinkJob = KIO::symlink(sourceFile + "3", QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo); QVERIFY(symlinkJob->exec()); QVERIFY(QFileInfo(dest).isSymLink()); QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + "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); QVERIFY(job->exec()); 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";}); bool ok = job->exec(); QVERIFY(ok); 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/kacltest.cpp b/autotests/kacltest.cpp index 4fab4724..032c6e0d 100644 --- a/autotests/kacltest.cpp +++ b/autotests/kacltest.cpp @@ -1,221 +1,219 @@ /* This file is part of the KDE project Copyright (C) 2005 Till Adam 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 "kacltest.h" #include #include #include -#include -#include // The code comes partly from kdebase/kioslave/trash/testtrash.cpp QTEST_MAIN(KACLTest) static const QString s_testACL(QStringLiteral("user::rw-\nuser:bin:rwx\ngroup::rw-\nmask::rwx\nother::r--\n")); static const QString s_testACL2(QStringLiteral("user::rwx\nuser:bin:rwx\ngroup::rw-\ngroup:users:r--\ngroup:audio:--x\nmask::r-x\nother::r--\n")); static const QString s_testACLEffective(QStringLiteral("user::rwx\nuser:bin:rwx #effective:r-x\ngroup::rw- #effective:r--\ngroup:audio:--x\ngroup:users:r--\nmask::r-x\nother::r--\n")); KACLTest::KACLTest() : m_acl(s_testACL) { } void KACLTest::initTestCase() { #if !HAVE_POSIX_ACL QSKIP("ACL support not compiled"); #endif m_acl2.setACL(s_testACL2); } void KACLTest::testAsString() { QCOMPARE(m_acl.asString(), s_testACL); } void KACLTest::testSetACL() { QCOMPARE(m_acl2.asString().simplified(), s_testACLEffective.simplified()); } void KACLTest::testGetOwnerPermissions() { QCOMPARE(int(m_acl.ownerPermissions()), 6); } void KACLTest::testGetOwningGroupPermissions() { QCOMPARE(int(m_acl.owningGroupPermissions()), 6); } void KACLTest::testGetOthersPermissions() { QCOMPARE(int(m_acl.othersPermissions()), 4); } void KACLTest::testGetMaskPermissions() { bool exists = false; int mask = m_acl.maskPermissions(exists); QVERIFY(exists); QCOMPARE(mask, 7); } void KACLTest::testGetAllUserPermissions() { ACLUserPermissionsList list = m_acl.allUserPermissions(); ACLUserPermissionsConstIterator it = list.constBegin(); QString name; int permissions = 0; int count = 0; while (it != list.constEnd()) { name = (*it).first; permissions = (*it).second; ++it; ++count; } QCOMPARE(count, 1); QCOMPARE(name, QStringLiteral("bin")); QCOMPARE(permissions, 7); } void KACLTest::testGetAllGroupsPermissions() { ACLGroupPermissionsList list = m_acl2.allGroupPermissions(); ACLGroupPermissionsConstIterator it = list.constBegin(); QString name; int permissions; int count = 0; while (it != list.constEnd()) { name = (*it).first; permissions = (*it).second; // setACL sorts them alphabetically ... if (count == 0) { QCOMPARE(name, QStringLiteral("audio")); QCOMPARE(permissions, 1); } else if (count == 1) { QCOMPARE(name, QStringLiteral("users")); QCOMPARE(permissions, 4); } ++it; ++count; } QCOMPARE(count, 2); } void KACLTest::testIsExtended() { KACL dukeOfMonmoth(s_testACL); QVERIFY(dukeOfMonmoth.isExtended()); KACL earlOfUpnor(QStringLiteral("user::r--\ngroup::r--\nother::r--\n")); QVERIFY(!earlOfUpnor.isExtended()); } void KACLTest::testOperators() { KACL dukeOfMonmoth(s_testACL); KACL JamesScott(s_testACL); KACL earlOfUpnor(s_testACL2); QVERIFY(!(dukeOfMonmoth == earlOfUpnor)); QVERIFY(dukeOfMonmoth != earlOfUpnor); QVERIFY(dukeOfMonmoth != earlOfUpnor); QVERIFY(!(dukeOfMonmoth != JamesScott)); } void KACLTest::testSettingBasic() { KACL CharlesII(s_testACL); CharlesII.setOwnerPermissions(7); // clearly CharlesII.setOwningGroupPermissions(0); CharlesII.setOthersPermissions(0); QCOMPARE(int(CharlesII.ownerPermissions()), 7); QCOMPARE(int(CharlesII.owningGroupPermissions()), 0); QCOMPARE(int(CharlesII.othersPermissions()), 0); } void KACLTest::testSettingExtended() { KACL CharlesII(s_testACL); CharlesII.setMaskPermissions(7); // clearly bool dummy = false; QCOMPARE(int(CharlesII.maskPermissions(dummy)), 7); const QString expected(QStringLiteral("user::rw-\nuser:root:rwx\nuser:bin:r--\ngroup::rw-\nmask::rwx\nother::r--\n")); ACLUserPermissionsList users; ACLUserPermissions user = qMakePair(QStringLiteral("root"), (unsigned short)7); users.append(user); user = qMakePair(QStringLiteral("bin"), (unsigned short)4); users.append(user); CharlesII.setAllUserPermissions(users); QCOMPARE(CharlesII.asString(), expected); CharlesII.setACL(s_testACL); // reset // it already has an entry for bin, let's change it CharlesII.setNamedUserPermissions(QStringLiteral("bin"), 4); CharlesII.setNamedUserPermissions(QStringLiteral("root"), 7); QCOMPARE(CharlesII.asString(), expected); // groups, all and named const QString expected2(QStringLiteral("user::rw-\nuser:bin:rwx\ngroup::rw-\ngroup:audio:-wx\ngroup:users:r--\nmask::rwx\nother::r--\n")); CharlesII.setACL(s_testACL); // reset ACLGroupPermissionsList groups; ACLGroupPermissions group = qMakePair(QStringLiteral("audio"), (unsigned short)3); groups.append(group); group = qMakePair(QStringLiteral("users"), (unsigned short)4); groups.append(group); CharlesII.setAllGroupPermissions(groups); QCOMPARE(CharlesII.asString(), expected2); CharlesII.setACL(s_testACL); // reset CharlesII.setNamedGroupPermissions(QStringLiteral("audio"), 3); CharlesII.setNamedGroupPermissions(QStringLiteral("users"), 4); QCOMPARE(CharlesII.asString(), expected2); } void KACLTest::testSettingErrorHandling() { KACL foo(s_testACL); bool v = foo.setNamedGroupPermissions(QStringLiteral("audio"), 7); // existing group QVERIFY(v); v = foo.setNamedGroupPermissions(QStringLiteral("jongel"), 7); // non-existing group QVERIFY(!v); v = foo.setNamedUserPermissions(QStringLiteral("bin"), 7); // existing user QVERIFY(v); v = foo.setNamedUserPermissions(QStringLiteral("jongel"), 7); // non-existing user QVERIFY(!v); } void KACLTest::testNewMask() { KACL CharlesII(QStringLiteral("user::rw-\ngroup::rw-\nother::rw\n")); bool dummy = false; CharlesII.maskPermissions(dummy); QVERIFY(!dummy); CharlesII.setMaskPermissions(6); QCOMPARE(int(CharlesII.maskPermissions(dummy)), 6); QVERIFY(dummy); // mask exists now } diff --git a/autotests/kcoredirlister_benchmark.cpp b/autotests/kcoredirlister_benchmark.cpp index f73d054f..858a1613 100644 --- a/autotests/kcoredirlister_benchmark.cpp +++ b/autotests/kcoredirlister_benchmark.cpp @@ -1,552 +1,551 @@ /* This file is part of the KDE project Copyright (C) 2018 Jaime Torres 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 #include // BEGIN Global variables const QString fileNameArg = QLatin1String("/home/user/Folder1/SubFolder2/a%1.txt"); // to check with 10, 100, 1000, ... KFileItem const int maxPowerOfTen=4; // To use the same random list of names and url for all the tests QVector randInt[maxPowerOfTen]; // The same list of random integers for all the tests std::default_random_engine generator; // END Global variables /* This is to compare the old list API vs QMap API vs QHash API vs sorted list API in terms of performance for KcoreDirLister list of items. This benchmark assumes that KFileItem has the < operators. */ class kcoreDirListerEntryBenchmark : public QObject { Q_OBJECT public: kcoreDirListerEntryBenchmark() { // Fill randInt[i] with random numbers from 0 to (10^(i+1))-1 for (int i=0; i < maxPowerOfTen; ++i) { std::uniform_int_distribution distribution(0,pow(10,i+1)-1); // Fill the vector with consecutive numbers randInt[i].reserve(pow(10,i+1)); for (int j=0; j < pow(10,i+1); ++j) { randInt[i].append(j); } // And now scramble them a little bit for (int j=0; j < pow(10,i+1); ++j) { int rd1 = distribution(generator); int rd2 = distribution(generator); int swap = randInt[i].at(rd1); randInt[i].replace(rd1, randInt[i].at(rd2)); randInt[i].replace(rd2, swap); } // qDebug() << randInt[i]; } } private Q_SLOTS: void testCreateFiles_List_data(); void testCreateFiles_List(); void testFindByNameFiles_List_data(); void testFindByNameFiles_List(); void testFindByUrlFiles_List_data(); void testFindByUrlFiles_List(); void testFindByUrlAllFiles_List_data(); void testFindByUrlAllFiles_List(); void testCreateFiles_Map_data(); void testCreateFiles_Map(); void testFindByNameFiles_Map_data(); void testFindByNameFiles_Map(); void testFindByUrlFiles_Map_data(); void testFindByUrlFiles_Map(); void testFindByUrlAllFiles_Map_data(); void testFindByUrlAllFiles_Map(); void testCreateFiles_Hash_data(); void testCreateFiles_Hash(); void testFindByNameFiles_Hash_data(); void testFindByNameFiles_Hash(); void testFindByUrlFiles_Hash_data(); void testFindByUrlFiles_Hash(); void testFindByUrlAllFiles_Hash_data(); void testFindByUrlAllFiles_Hash(); void testCreateFiles_Binary_data(); void testCreateFiles_Binary(); void testFindByNameFiles_Binary_data(); void testFindByNameFiles_Binary(); void testFindByUrlFiles_Binary_data(); void testFindByUrlFiles_Binary(); void testFindByUrlAllFiles_Binary_data(); void testFindByUrlAllFiles_Binary(); }; //BEGIN Implementations //BEGIN List // List Implementation (without binary search) class ListImplementation { public: QList lstItems; public: void reserve(int size) { lstItems.reserve(size); } // This search must be fast also KFileItem findByName(const QString &fileName) const { const auto end = lstItems.cend(); for (auto it = lstItems.cbegin() ; it != end; ++it) { if ((*it).name() == fileName) { return *it; } } return KFileItem(); } // simulation of the search by Url in an existing lister (the slowest path) KFileItem findByUrl(const QUrl &_u) const { QUrl url(_u); url = url.adjusted(QUrl::StripTrailingSlash); const auto end = lstItems.cend(); for (auto it = lstItems.cbegin(); it != end; ++it) { if ((*it).url() == url) { return *it; } } return KFileItem(); } void clear() { lstItems.clear(); } void insert(int powerOfTen) { for (int x = 0; x < pow(10, powerOfTen+1); ++x) { QUrl u = QUrl::fromLocalFile(fileNameArg.arg(randInt[powerOfTen].at(x) )).adjusted(QUrl::StripTrailingSlash); KFileItem kfi (u, QStringLiteral("text/text")); lstItems.append(kfi); } } }; //END List //BEGIN QMap // Proposed Implementation using QMap class QMapImplementation { public: void reserve(int size) { Q_UNUSED(size); } KFileItem findByName(const QString &fileName) const { const auto itend = lstItems.cend(); for (auto it = lstItems.cbegin(); it != itend; ++it) { if ((*it).name() == fileName) { return *it; } } return KFileItem(); } // simulation of the search by Url in an existing lister (the slowest path) KFileItem findByUrl(const QUrl &_u) const { QUrl url(_u); url = url.adjusted(QUrl::StripTrailingSlash); auto it = lstItems.find(url); if (it != lstItems.end()) { return *it; } return KFileItem(); } void clear() { lstItems.clear(); } void insert(int powerOfTen) { for (int x = 0; x < pow(10, powerOfTen+1); ++x) { QUrl u = QUrl::fromLocalFile(fileNameArg.arg(randInt[powerOfTen].at(x) )).adjusted(QUrl::StripTrailingSlash); KFileItem kfi(u, QStringLiteral("text/text")); lstItems.insert(u, kfi); } } public: QMap lstItems; }; //END QMap //BEGIN QHash // Proposed Implementation using QHash class QHashImplementation { public: void reserve(int size) { lstItems.reserve(size); } KFileItem findByName(const QString &fileName) const { const auto itend = lstItems.cend(); for (auto it = lstItems.cbegin(); it != itend; ++it) { if ((*it).name() == fileName) { return *it; } } return KFileItem(); } // simulation of the search by Url in an existing lister (the slowest path) KFileItem findByUrl(const QUrl &_u) const { QUrl url(_u); url = url.adjusted(QUrl::StripTrailingSlash); auto it = lstItems.find(url); if (it != lstItems.end()) { return *it; } return KFileItem(); } void clear() { lstItems.clear(); } void insert(int powerOfTen) { for (int x = 0; x < pow(10, powerOfTen+1); ++x) { QUrl u = QUrl::fromLocalFile(fileNameArg.arg(randInt[powerOfTen].at(x) )).adjusted(QUrl::StripTrailingSlash); KFileItem kfi(u, QStringLiteral("text/text")); lstItems.insert(u, kfi); } } public: QHash lstItems; }; //END QHash //BEGIN BinaryList // Proposed Implementation using QList with ordered insert and binary search class BinaryListImplementation { public: QList lstItems; public: void reserve(int size) { lstItems.reserve(size); } KFileItem findByName(const QString &fileName) const { const auto itend = lstItems.cend(); for (auto it = lstItems.cbegin(); it != itend; ++it) { if ((*it).name() == fileName) { return *it; } } return KFileItem(); } // simulation of the search by Url in an existing lister (the slowest path) KFileItem findByUrl(const QUrl &_u) const { QUrl url(_u); url = url.adjusted(QUrl::StripTrailingSlash); auto it = std::lower_bound(lstItems.cbegin(), lstItems.cend(), url); if (it != lstItems.cend() && (*it).url() == url) { return *it; } return KFileItem(); } void clear() { lstItems.clear(); } // Add files in random order from the randInt vector void insert(int powerOfTen) { for (int x = 0; x < pow(10, powerOfTen+1); ++x) { QUrl u = QUrl::fromLocalFile(fileNameArg.arg(randInt[powerOfTen].at(x) )).adjusted(QUrl::StripTrailingSlash); KFileItem kfi(u, QStringLiteral("text/text")); auto it = std::lower_bound(lstItems.begin(), lstItems.end(), u); lstItems.insert(it, kfi); } } }; //END BinaryList //END Implementations //BEGIN templates template void fillNumberOfFiles() { QTest::addColumn("numberOfFiles"); for (int i=0; i < maxPowerOfTen; ++i) { // it shows numberOfFiles: 10, 100 or 1000 but the data is the power of ten QTest::newRow( QStringLiteral("%1").arg(pow(10, i+1)).toLatin1() ) << i; } } template void createFiles(int powerOfTen) { T data; const int numberOfFiles = pow(10, powerOfTen+1); data.reserve(numberOfFiles); QBENCHMARK { data.clear(); data.insert(powerOfTen); } QCOMPARE(data.lstItems.size(), numberOfFiles); } template void findByName(int powerOfTen) { T data; data.clear(); data.reserve(pow(10, powerOfTen+1)); data.insert(powerOfTen); QBENCHMARK { for (int i=0; i void findByUrl(int powerOfTen) { T data; data.clear(); data.reserve(pow(10, powerOfTen+1)); data.insert(powerOfTen); QBENCHMARK { for (int i=0; i void findByUrlAll(int powerOfTen) { T data; data.clear(); data.reserve(pow(10, powerOfTen+1)); data.insert(powerOfTen); QBENCHMARK { for (int i=0; i(); } void kcoreDirListerEntryBenchmark::testCreateFiles_List() { QFETCH(int, numberOfFiles); createFiles(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByNameFiles_List_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByNameFiles_List() { QFETCH(int, numberOfFiles); findByName(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByUrlFiles_List_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByUrlFiles_List() { QFETCH(int, numberOfFiles); findByUrl(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByUrlAllFiles_List_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByUrlAllFiles_List() { QFETCH(int, numberOfFiles); findByUrlAll(numberOfFiles); } void kcoreDirListerEntryBenchmark::testCreateFiles_Map_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testCreateFiles_Map() { QFETCH(int, numberOfFiles); createFiles(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByNameFiles_Map_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByNameFiles_Map() { QFETCH(int, numberOfFiles); findByName(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByUrlFiles_Map_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByUrlFiles_Map() { QFETCH(int, numberOfFiles); findByUrl(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByUrlAllFiles_Map_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByUrlAllFiles_Map() { QFETCH(int, numberOfFiles); findByUrlAll(numberOfFiles); } void kcoreDirListerEntryBenchmark::testCreateFiles_Hash_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testCreateFiles_Hash() { QFETCH(int, numberOfFiles); createFiles(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByNameFiles_Hash_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByNameFiles_Hash() { QFETCH(int, numberOfFiles); findByName(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByUrlFiles_Hash_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByUrlFiles_Hash() { QFETCH(int, numberOfFiles); findByUrl(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByUrlAllFiles_Hash_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByUrlAllFiles_Hash() { QFETCH(int, numberOfFiles); findByUrlAll(numberOfFiles); } void kcoreDirListerEntryBenchmark::testCreateFiles_Binary_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testCreateFiles_Binary() { QFETCH(int, numberOfFiles); createFiles(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByNameFiles_Binary_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByNameFiles_Binary() { QFETCH(int, numberOfFiles); findByName(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByUrlFiles_Binary_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByUrlFiles_Binary() { QFETCH(int, numberOfFiles); findByUrl(numberOfFiles); } void kcoreDirListerEntryBenchmark::testFindByUrlAllFiles_Binary_data() { fillNumberOfFiles(); } void kcoreDirListerEntryBenchmark::testFindByUrlAllFiles_Binary() { QFETCH(int, numberOfFiles); findByUrlAll(numberOfFiles); } //END tests QTEST_MAIN(kcoreDirListerEntryBenchmark) #include "kcoredirlister_benchmark.moc" diff --git a/autotests/kdirmodeltest.h b/autotests/kdirmodeltest.h index bbc8c534..3735c170 100644 --- a/autotests/kdirmodeltest.h +++ b/autotests/kdirmodeltest.h @@ -1,117 +1,116 @@ /* This file is part of the KDE project Copyright (C) 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. */ #ifndef KDIRMODELTEST_H #define KDIRMODELTEST_H #include #include -#include #include #include #include // If you disable this, you need to change all exitLoop into quit in connect() statements... #define USE_QTESTEVENTLOOP class KDirModelTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void cleanup(); void testRowCount(); void testIndex(); void testNames(); void testItemForIndex(); void testIndexForItem(); void testData(); void testReload(); void testModifyFile(); void testRenameFile(); void testMoveDirectory(); void testRenameDirectory(); void testRenameDirectoryInCache(); void testChmodDirectory(); void testExpandToUrl_data(); void testExpandToUrl(); void testFilter(); void testMimeFilter(); void testShowHiddenFiles(); void testMultipleSlashes(); void testUrlWithRef(); void testRemoteUrlWithHost(); void testZipFile(); void testSmb(); void testBug196695(); void testMimeData(); void testDotHiddenFile_data(); void testDotHiddenFile(); // These tests must be done last void testDeleteFile(); void testDeleteFileWhileListing(); void testOverwriteFileWithDir(); void testDeleteFiles(); void testRenameFileToHidden(); void testDeleteDirectory(); void testDeleteCurrentDirectory(); // Somewhat unrelated void testQUrlHash(); protected Q_SLOTS: // 'more private than private slots' - i.e. not seen by qtestlib void slotListingCompleted(); void slotExpand(const QModelIndex &index); void slotRowsInserted(const QModelIndex &index, int, int); private: void recreateTestData(); void enterLoop(); void fillModel(bool reload, bool expectAllIndexes = true); void collectKnownIndexes(); void testMoveDirectory(const QString &srcdir); void testUpdateParentAfterExpand(); private: #ifdef USE_QTESTEVENTLOOP QTestEventLoop m_eventLoop; #else QEventLoop m_eventLoop; #endif QTemporaryDir *m_tempDir; KDirModel *m_dirModel; QModelIndex m_fileIndex; QModelIndex m_specialFileIndex; QModelIndex m_secondFileIndex; QModelIndex m_dirIndex; QModelIndex m_fileInDirIndex; QModelIndex m_fileInSubdirIndex; QStringList m_topLevelFileNames; // files only // for slotExpand QStringList m_expectedExpandSignals; int m_nextExpectedExpandSignals; // index into m_expectedExpandSignals KDirModel *m_dirModelForExpand; QUrl m_urlToExpandTo; bool m_rowsInsertedEmitted; bool m_expectRowsInserted; }; #endif diff --git a/autotests/kfilecopytomenutest.cpp b/autotests/kfilecopytomenutest.cpp index 7d557c63..625ba5e5 100644 --- a/autotests/kfilecopytomenutest.cpp +++ b/autotests/kfilecopytomenutest.cpp @@ -1,183 +1,182 @@ /* This file is part of the KDE project Copyright (C) 2014 David Faure 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 ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser 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 #include #include "kiotesthelper.h" #include "jobuidelegatefactory.h" #include class KFileCopyToMenuTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { QStandardPaths::setTestModeEnabled(true); qputenv("KIOSLAVE_ENABLE_TESTMODE", "1"); // ensure the ioslaves call QStandardPaths::setTestModeEnabled too qputenv("KDE_FORK_SLAVES", "yes"); // to avoid a runtime dependency on klauncher QVERIFY(m_tempDir.isValid()); QVERIFY(m_tempDestDir.isValid()); QVERIFY(m_nonWritableTempDir.isValid()); QVERIFY(QFile(m_nonWritableTempDir.path()).setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::ExeOwner | QFile::ExeUser)); m_srcDir = m_tempDir.path(); m_destDir = m_tempDestDir.path(); m_srcFile = m_srcDir + QStringLiteral("/srcfile"); KIO::setDefaultJobUiDelegateExtension(nullptr); // no "skip" dialogs // Set a recent dir KConfigGroup recentDirsGroup(KSharedConfig::openConfig(), "kuick-copy"); m_recentDirs << m_destDir + QStringLiteral("/nonexistentsubdir") // will be action number count-3 << m_nonWritableTempDir.path() // will be action number count-2 << m_destDir; // will be action number count-1 recentDirsGroup.writeEntry("Paths", m_recentDirs); m_lastActionCount = 0; } void cleanupTestCase() { QVERIFY(QFile(m_nonWritableTempDir.path()).setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::WriteOwner | QFile::WriteUser | QFile::ExeOwner | QFile::ExeUser)); } // Before every test method, ensure the test file m_srcFile exists void init() { if (QFile::exists(m_srcFile)) { QVERIFY(QFileInfo(m_srcFile).isWritable()); } else { QFile srcFile(m_srcFile); QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString())); srcFile.write("Hello world\n"); } QVERIFY(QFileInfo(m_srcFile).isWritable()); } void shouldHaveParentWidget() { KFileCopyToMenu generator(&m_parentWidget); QCOMPARE(generator.parent(), &m_parentWidget); } void shouldAddActions() { KFileCopyToMenu generator(&m_parentWidget); QMenu menu; generator.addActionsTo(&menu); QList urls; urls << QUrl::fromLocalFile(m_srcFile); generator.setUrls(urls); QCOMPARE(extractActionNames(menu), QStringList() << QStringLiteral("copyTo_submenu") << QStringLiteral("moveTo_submenu")); //menu.popup(QPoint(-50, -50)); QMenu *copyMenu = menu.actions().at(0)->menu(); // "copy" submenu QVERIFY(copyMenu); // When copyMenu->popup(QPoint(-100, -100)); // Then const QStringList actionNames = extractActionNames(*copyMenu); QCOMPARE(actionNames.first(), QStringLiteral("home")); QVERIFY(actionNames.contains(QStringLiteral("browse"))); QCOMPARE(actionNames.at(actionNames.count() - 2), m_nonWritableTempDir.path()); QCOMPARE(actionNames.last(), m_destDir); } void shouldTryCopyingToRecentPath_data() { QTest::addColumn("actionNumber"); // from the bottom of the menu, starting at 1; see the recentDirs list in initTestCase QTest::addColumn("expectedErrorCode"); QTest::newRow("working") << 1 << 0; // no error QTest::newRow("non_writable") << 2 << int(KIO::ERR_WRITE_ACCESS_DENIED); QTest::newRow("non_existing") << 3 << int(KIO::ERR_CANNOT_OPEN_FOR_WRITING); } void shouldTryCopyingToRecentPath() { QFETCH(int, actionNumber); QFETCH(int, expectedErrorCode); KFileCopyToMenu generator(&m_parentWidget); QMenu menu; QList urls; urls << QUrl::fromLocalFile(m_srcFile); generator.setUrls(urls); generator.addActionsTo(&menu); QMenu *copyMenu = menu.actions().at(0)->menu(); copyMenu->popup(QPoint(-100, -100)); const QList actions = copyMenu->actions(); if (m_lastActionCount == 0) { m_lastActionCount = actions.count(); } else { QCOMPARE(actions.count(), m_lastActionCount); // should be stable, i.e. selecting a recent dir shouldn't duplicate it } QAction *copyAction = actions.at(actions.count() - actionNumber); QSignalSpy spy(&generator, SIGNAL(error(int,QString))); // When copyAction->trigger(); // Then QTRY_COMPARE(spy.count(), expectedErrorCode ? 1 : 0); if (expectedErrorCode) { QCOMPARE(spy.at(0).at(0).toInt(), expectedErrorCode); } else { QTRY_VERIFY(QFile::exists(m_destDir + QStringLiteral("/srcfile"))); } } private: static QStringList extractActionNames(const QMenu &menu) { QStringList ret; foreach (const QAction *action, menu.actions()) { ret.append(action->objectName()); } return ret; } QTemporaryDir m_tempDir; QString m_srcDir; QString m_srcFile; QTemporaryDir m_tempDestDir; QString m_destDir; QTemporaryDir m_nonWritableTempDir; QWidget m_parentWidget; QStringList m_recentDirs; int m_lastActionCount; }; QTEST_MAIN(KFileCopyToMenuTest) #include "kfilecopytomenutest.moc" diff --git a/autotests/kfilecustomdialogtest.cpp b/autotests/kfilecustomdialogtest.cpp index eb6277c4..3a6ebcb3 100644 --- a/autotests/kfilecustomdialogtest.cpp +++ b/autotests/kfilecustomdialogtest.cpp @@ -1,42 +1,41 @@ /* This file is part of the KDE libraries Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com. Work sponsored by the LiMux project of the city of Munich 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 "kfilecustomdialogtest.h" #include "kfilecustomdialog.h" #include -#include #include #include QTEST_MAIN(KFileCustomDialogTest) void KFileCustomDialogTest::shouldHaveDefaultValue() { KFileCustomDialog dlg; dlg.show(); QVERIFY(QTest::qWaitForWindowExposed(&dlg)); QVBoxLayout *mainLayout = dlg.findChild(); QVERIFY(mainLayout); KFileWidget *mFileWidget = dlg.findChild(QStringLiteral("filewidget")); QVERIFY(mFileWidget); QCOMPARE(dlg.fileWidget(), mFileWidget); } diff --git a/autotests/kfileitemactionstest.cpp b/autotests/kfileitemactionstest.cpp index c2d8343a..f5984c62 100644 --- a/autotests/kfileitemactionstest.cpp +++ b/autotests/kfileitemactionstest.cpp @@ -1,64 +1,63 @@ /* This file is part of the KDE project Copyright (C) 2014 Frank Reininghaus This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfileitemactionstest.h" #include #include #include #include -#include #include /** * In KDE 4.x, calling KFileItemActions::setParentWidget(QWidget *widget) would * result in 'widget' not only being the parent of any dialogs created by, * KFileItemActions, but also of the actions. Nevertheless, the destructor of * KFileItemActions deleted all actions it created. This could lead to the deletion * of dangling pointers, and thus, a crash, if 'widget' was destroyed before the * KFileItemActions instance. */ void KFileItemActionsTest::testSetParentWidget() { KFileItemActions fileItemActions; // Create a widget and make it the parent for any dialogs created by fileItemActions. QWidget *widget = new QWidget(); fileItemActions.setParentWidget(widget); // Initialize fileItemActions with a KFileItemList that contains only the home URL. KFileItemList items; const QUrl homeUrl = QUrl::fromLocalFile(QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first()); const KFileItem item(homeUrl, QStringLiteral("inode/directory")); items << item; const KFileItemListProperties properties(items); fileItemActions.setItemListProperties(properties); // Create the "Open With" actions and add them to a menu. QMenu menu; fileItemActions.addOpenWithActionsTo(&menu); // Delete the widget. In KDE 4.x, this would also delete the "Open With" actions // because they were children of the widget. We would then get a crash in the // destructor of fileItemActions because it tried to delete dangling pointers. delete widget; } QTEST_MAIN(KFileItemActionsTest) diff --git a/autotests/kfileplacesviewtest.cpp b/autotests/kfileplacesviewtest.cpp index e69e11df..ae1487a6 100644 --- a/autotests/kfileplacesviewtest.cpp +++ b/autotests/kfileplacesviewtest.cpp @@ -1,117 +1,116 @@ /* This file is part of the KDE project Copyright (C) 2017 Renato Araujo Oliveira Filho This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include -#include #include #include #include #include #include #include #include #include #include static QString bookmarksFile() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/user-places.xbel"; } class KFilePlacesViewTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testUrlChanged_data(); void testUrlChanged(); private: QTemporaryDir m_tmpHome; }; void KFilePlacesViewTest::initTestCase() { QVERIFY(m_tmpHome.isValid()); qputenv("HOME", m_tmpHome.path().toUtf8()); qputenv("KDE_FORK_SLAVES", "yes"); // to avoid a runtime dependency on klauncher QStandardPaths::setTestModeEnabled(true); cleanupTestCase(); KConfig config(QStringLiteral("baloofilerc")); KConfigGroup basicSettings = config.group("Basic Settings"); basicSettings.writeEntry("Indexing-Enabled", true); config.sync(); qRegisterMetaType(); } void KFilePlacesViewTest::cleanupTestCase() { QFile::remove(bookmarksFile()); } void KFilePlacesViewTest::testUrlChanged_data() { QTest::addColumn("row"); QTest::addColumn("expectedUrl"); const QDate currentDate = QDate::currentDate(); const QDate yesterdayDate = currentDate.addDays(-1); QTest::newRow("Today") << 3 << QStringLiteral("timeline:/today"); QTest::newRow("Yesterday") << 4 << QString("timeline:/%1-%2/%1-%2-%3") .arg(yesterdayDate.year()) .arg(yesterdayDate.month(), 2, 10, QChar('0')) .arg(yesterdayDate.day(), 2, 10, QChar('0')); // search QTest::newRow("Documents") << 5 << QStringLiteral("baloosearch:/documents"); QTest::newRow("Images") << 6 << QStringLiteral("baloosearch:/images"); QTest::newRow("Audio Files") << 7 << QStringLiteral("baloosearch:/audio"); QTest::newRow("Videos") << 8 << QStringLiteral("baloosearch:/videos"); } void KFilePlacesViewTest::testUrlChanged() { QFETCH(int, row); QFETCH(QString, expectedUrl); KFilePlacesView pv; pv.show(); pv.activateWindow(); pv.setModel(new KFilePlacesModel()); QVERIFY(QTest::qWaitForWindowActive(&pv)); QSignalSpy urlChangedSpy(&pv, &KFilePlacesView::urlChanged); const QModelIndex targetIndex = pv.model()->index(row, 0); pv.scrollTo(targetIndex); pv.clicked(targetIndex); QTRY_COMPARE(urlChangedSpy.count(), 1); const QList args = urlChangedSpy.takeFirst(); QCOMPARE(args.at(0).toUrl().toString(), expectedUrl); } QTEST_MAIN(KFilePlacesViewTest) #include "kfileplacesviewtest.moc" diff --git a/autotests/ktcpsockettest.cpp b/autotests/ktcpsockettest.cpp index d191f90f..34c05268 100644 --- a/autotests/ktcpsockettest.cpp +++ b/autotests/ktcpsockettest.cpp @@ -1,384 +1,383 @@ /* * This file is part of the KDE libraries * Copyright (C) 2007 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 "ktcpsockettest.h" #include -#include #include #include "ktcpsocket.h" /* TODO items: - test errors including error strings - test overriding errors - test the most important SSL operations (full coverage is very hard) - test readLine() - test nonblocking, signal based usage - test that waitForDisconnected() writes out all buffered data - (test local and peer address and port getters) - test isValid(). Its documentation is less than clear :( */ static const quint16 testPort = 22342; KTcpSocketTest::KTcpSocketTest() { server = nullptr; } KTcpSocketTest::~KTcpSocketTest() { } void KTcpSocketTest::invokeOnServer(const char *method) { QMetaObject::invokeMethod(server, method, Qt::QueuedConnection); QTest::qWait(1); //Enter the event loop } Server::Server(quint16 _port) : listener(new QTcpServer(this)), socket(nullptr), port(_port) { listener->listen(QHostAddress(QStringLiteral("127.0.0.1")), testPort); } Server::~Server() { } void Server::cleanupSocket() { Q_ASSERT(socket); socket->close(); socket->deleteLater(); socket = nullptr; } void KTcpSocketTest::initTestCase() { m_thread = new QThread(); server = new Server(testPort); server->moveToThread(m_thread); connect(m_thread, &QThread::finished, server, &QObject::deleteLater); m_thread->start(); } void KTcpSocketTest::cleanupTestCase() { m_thread->quit(); m_thread->wait(); delete m_thread; } void KTcpSocketTest::connectDisconnect() { invokeOnServer("connectDisconnect"); KTcpSocket *s = new KTcpSocket(this); QCOMPARE(s->openMode(), QIODevice::NotOpen); QCOMPARE(s->error(), KTcpSocket::UnknownError); s->connectToHost(QStringLiteral("127.0.0.1"), testPort); QCOMPARE(s->state(), KTcpSocket::ConnectingState); QVERIFY(s->openMode() & QIODevice::ReadWrite); const bool connected = s->waitForConnected(150); QVERIFY(connected); QCOMPARE(s->state(), KTcpSocket::ConnectedState); s->waitForDisconnected(150); //ClosingState occurs only when there is buffered data QCOMPARE(s->state(), KTcpSocket::UnconnectedState); s->deleteLater(); } void Server::connectDisconnect() { listener->waitForNewConnection(10000, nullptr); socket = listener->nextPendingConnection(); cleanupSocket(); } #define TESTDATA QByteArray("things and stuff and a bag of chips") void KTcpSocketTest::read() { invokeOnServer("read"); KTcpSocket *s = new KTcpSocket(this); s->connectToHost(QStringLiteral("127.0.0.1"), testPort); s->waitForConnected(40); s->waitForReadyRead(40); QCOMPARE((int)s->bytesAvailable(), TESTDATA.size()); QCOMPARE(s->readAll(), TESTDATA); s->deleteLater(); } void Server::read() { listener->waitForNewConnection(10000, nullptr); socket = listener->nextPendingConnection(); socket->write(TESTDATA); socket->waitForBytesWritten(150); cleanupSocket(); } void KTcpSocketTest::write() { invokeOnServer("write"); KTcpSocket *s = new KTcpSocket(this); s->connectToHost(QStringLiteral("127.0.0.1"), testPort); s->waitForConnected(40); s->write(TESTDATA); QCOMPARE((int)s->bytesToWrite(), TESTDATA.size()); s->waitForReadyRead(150); QCOMPARE((int)s->bytesAvailable(), TESTDATA.size()); QCOMPARE(s->readAll(), TESTDATA); s->write(TESTDATA); QCOMPARE((int)s->bytesToWrite(), TESTDATA.size()); s->disconnectFromHost(); //Test closing with pending data to transmit (pending rx data comes later) QCOMPARE(s->state(), KTcpSocket::ClosingState); s->waitForDisconnected(150); QCOMPARE(s->state(), KTcpSocket::UnconnectedState); s->deleteLater(); } void Server::write() { listener->waitForNewConnection(10000, nullptr); socket = listener->nextPendingConnection(); socket->waitForReadyRead(40); socket->write(socket->readAll()); //echo socket->waitForBytesWritten(150); socket->waitForReadyRead(40); socket->write(socket->readAll()); cleanupSocket(); } static QString stateToString(KTcpSocket::State state) { switch (state) { case KTcpSocket::UnconnectedState: return QStringLiteral("UnconnectedState"); case KTcpSocket::HostLookupState: return QStringLiteral("HostLookupState"); case KTcpSocket::ConnectingState: return QStringLiteral("ConnectingState"); case KTcpSocket::ConnectedState: return QStringLiteral("ConnectedState"); case KTcpSocket::BoundState: return QStringLiteral("BoundState"); case KTcpSocket::ListeningState: return QStringLiteral("ListeningState"); case KTcpSocket::ClosingState: return QStringLiteral("ClosingState"); } return QStringLiteral("ERROR"); } #define HTTPREQUEST QByteArray("GET / HTTP/1.1\nHost: www.example.com\n\n") // I assume that example.com, hosted by the IANA, will exist indefinitely. // It is a nice test site because it serves a very small HTML page that should // fit into a TCP packet or two. void KTcpSocketTest::statesIana() { QSKIP("Too unreliable"); //A connection to a real internet host KTcpSocket *s = new KTcpSocket(this); connect(s, &KTcpSocket::hostFound, this, &KTcpSocketTest::states_hostFound); QCOMPARE(s->state(), KTcpSocket::UnconnectedState); s->connectToHost(QStringLiteral("www.iana.org"), 80); QCOMPARE(s->state(), KTcpSocket::HostLookupState); s->write(HTTPREQUEST); QCOMPARE(s->state(), KTcpSocket::HostLookupState); s->waitForBytesWritten(2500); QCOMPARE(s->state(), KTcpSocket::ConnectedState); // Try to ensure that inbound data in the next part of the test is really from the second request; // it is not *guaranteed* that this reads all data, e.g. if the connection is very slow (so too many // of the waitForReadyRead() time out), or if the reply packets are extremely fragmented (so 50 reads // are not enough to receive all of them). I don't know the details of fragmentation so the latter // problem could be nonexistent. QByteArray received; for (int i = 0; i < 50; i++) { s->waitForReadyRead(50); received.append(s->readAll()); } QVERIFY(received.size() > 200); // Here, the connection should neither have data in its write buffer nor inbound packets in flight // Now reuse the connection for another request / reply pair s->write(HTTPREQUEST); s->waitForReadyRead(); // After waitForReadyRead(), the write buffer should be empty because the server has to wait for the // end of the request before sending a reply. // The socket can then shut down without having to wait for draining the write buffer. // Incoming data cannot delay the transition to UnconnectedState, as documented in // QAbstractSocket::disconnectFromHost(). close() just wraps disconnectFromHost(). s->close(); QCOMPARE((int)s->state(), (int)KTcpSocket::UnconnectedState); delete s; } void KTcpSocketTest::statesLocalHost() { //Now again an internal connection invokeOnServer("states"); KTcpSocket *s = new KTcpSocket(this); connect(s, &KTcpSocket::hostFound, this, &KTcpSocketTest::states_hostFound); s->connectToHost(QStringLiteral("127.0.0.1"), testPort); QCOMPARE(s->state(), KTcpSocket::ConnectingState); s->waitForConnected(40); QCOMPARE(s->state(), KTcpSocket::ConnectedState); s->write(HTTPREQUEST); s->waitForReadyRead(); QCOMPARE((int)s->bytesAvailable(), HTTPREQUEST.size()); //for good measure... QCOMPARE(s->state(), KTcpSocket::ConnectedState); s->waitForDisconnected(40); QCOMPARE(s->state(), KTcpSocket::UnconnectedState); disconnect(s, SIGNAL(hostFound())); delete s; } void KTcpSocketTest::statesManyHosts() { KTcpSocket *s = new KTcpSocket(this); QByteArray requestProlog("GET / HTTP/1.1\r\n" //exact copy of a real HTTP query "Connection: Keep-Alive\r\n" //not really... "User-Agent: Mozilla/5.0 (compatible; Konqueror/3.96; Linux) " "KHTML/3.96.0 (like Gecko)\r\n" "Pragma: no-cache\r\n" "Cache-control: no-cache\r\n" "Accept: text/html, image/jpeg, image/png, text/*, image/*, */*\r\n" "Accept-Encoding: x-gzip, x-deflate, gzip, deflate\r\n" "Accept-Charset: utf-8, utf-8;q=0.5, *;q=0.5\r\n" "Accept-Language: en-US, en\r\n" "Host: "); QByteArray requestEpilog("\r\n\r\n"); //Test rapid connection and disconnection to different hosts static const char *hosts[] = {"www.google.de", "www.spiegel.de", "www.stern.de", "www.google.com"}; static const int numHosts = 4; for (int i = 0; i < numHosts * 5; i++) { qDebug("\nNow trying %s...", hosts[i % numHosts]); QCOMPARE(s->state(), KTcpSocket::UnconnectedState); s->connectToHost(hosts[i % numHosts], 80); bool skip = false; KTcpSocket::State expectedState = KTcpSocket::ConnectingState; if (i < numHosts) { expectedState = KTcpSocket::HostLookupState; } else { expectedState = KTcpSocket::ConnectingState; } if (!skip) { QCOMPARE(stateToString(s->state()), stateToString(expectedState)); } else { // let's make sure it's at least one of the two expected states QVERIFY(stateToString(s->state()) == stateToString(KTcpSocket::HostLookupState) || stateToString(s->state()) == stateToString(KTcpSocket::ConnectingState)); } //weave the host address into the HTTP request QByteArray request(requestProlog); request.append(hosts[i % numHosts]); request.append(requestEpilog); s->write(request); if (!skip) { QCOMPARE(stateToString(s->state()), stateToString(expectedState)); } s->waitForBytesWritten(-1); QCOMPARE(s->state(), KTcpSocket::ConnectedState); int tries = 0; while (s->bytesAvailable() <= 100 && ++tries < 10) { s->waitForReadyRead(-1); } QVERIFY(s->bytesAvailable() > 100); if (i % (numHosts + 1)) { s->readAll(); QVERIFY(s->bytesAvailable() == 0); } else { char dummy[4]; s->read(dummy, 1); QVERIFY(s->bytesAvailable() > 100 - 1); } s->disconnectFromHost(); if (s->state() != KTcpSocket::UnconnectedState) { s->waitForDisconnected(-1); } if (i % 2) { s->close(); //close() is not very well defined for sockets so just check that it //does no harm } } s->deleteLater(); } void KTcpSocketTest::states_hostFound() { QCOMPARE(static_cast(sender())->state(), KTcpSocket::ConnectingState); } void Server::states() { listener->waitForNewConnection(10000, nullptr); socket = listener->nextPendingConnection(); socket->waitForReadyRead(40); socket->write(socket->readAll()); //echo socket->waitForBytesWritten(150); cleanupSocket(); } void KTcpSocketTest::errors() { //invokeOnServer("errors"); } void Server::errors() { listener->waitForNewConnection(10000, nullptr); socket = listener->nextPendingConnection(); cleanupSocket(); } QTEST_MAIN(KTcpSocketTest) diff --git a/autotests/kurlrequestertest.cpp b/autotests/kurlrequestertest.cpp index e2699918..cecb996d 100644 --- a/autotests/kurlrequestertest.cpp +++ b/autotests/kurlrequestertest.cpp @@ -1,205 +1,203 @@ /* This file is part of the KDE Frameworks Copyright (c) 2008, 2016 David Faure 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 ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser 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 #include #include /* IMPORTANT: Because this unittest interacts with the file dialog, remember to run it both with plugins/platformthemes/KDEPlasmaPlatformTheme.so (to use KFileWidget) and without it (to use the builtin QFileDialog code) */ class KUrlRequesterTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testUrlRequester(); void testComboRequester(); void testComboRequester_data(); private: bool createTestFile(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } file.write("Hello world\n"); return true; } }; // Same as in kfiledialog_unittest.cpp static KFileWidget *findFileWidget() { QList widgets; foreach (QWidget *widget, QApplication::topLevelWidgets()) { KFileWidget *fw = widget->findChild(); if (fw) { widgets.append(fw); } } return (widgets.count() == 1) ? widgets.first() : nullptr; } void KUrlRequesterTest::initTestCase() { qputenv("KDE_FORK_SLAVES", "yes"); } void KUrlRequesterTest::testUrlRequester() { KUrlRequester req; req.setFileDialogModality(Qt::NonModal); const QString fileName = QStringLiteral("some_test_file"); QVERIFY(createTestFile(fileName)); QTemporaryFile tempFile; QVERIFY(tempFile.open()); const QString filePath2 = tempFile.fileName(); QVERIFY(QFile::exists(filePath2)); // Set start dir const QUrl dirUrl = QUrl::fromLocalFile(QDir::currentPath()); req.setStartDir(dirUrl); QCOMPARE(req.startDir().toString(), dirUrl.toString()); // Click the button req.button()->click(); QFileDialog *fileDialog = req.findChild(); QVERIFY(fileDialog); // Find out if we're using KFileDialog or QFileDialog KFileWidget *fw = findFileWidget(); // Wait for directory listing if (fw) { QSignalSpy spy(fw->dirOperator(), &KDirOperator::finishedLoading); QVERIFY(spy.wait()); } // Select file const QString filePath = dirUrl.toLocalFile() + '/' + fileName; fileDialog->selectFile(fileName); // Click OK, check URLRequester shows and returns selected file QKeyEvent keyPressEv(QKeyEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); qApp->sendEvent(fw ? static_cast(fw) : static_cast(fileDialog), &keyPressEv); QCOMPARE(fileDialog->result(), static_cast(QDialog::Accepted)); QCOMPARE(fileDialog->selectedFiles(), QStringList() << filePath); QCOMPARE(req.url().toLocalFile(), filePath); // Check there is no longer any file dialog visible QVERIFY(fileDialog->isHidden()); // Click KUrlRequester button again. This time the filedialog is initialized with a file URL req.button()->click(); fileDialog = req.findChild(); QVERIFY(fileDialog); fw = findFileWidget(); if (fw) { // no need to wait for dir listing again, but we need it to be visible at least (for Key_Return to accept) //QVERIFY(QTest::qWaitForWindowExposed(fw->window())); // doesn't seem to be enough QTRY_VERIFY(fw->isVisible()); } // Select file 2 fileDialog->selectFile(filePath2); // Click OK, check URLRequester shows and returns selected file qApp->sendEvent(fw ? static_cast(fw) : static_cast(fileDialog), &keyPressEv); QCOMPARE(fileDialog->result(), static_cast(QDialog::Accepted)); QCOMPARE(fileDialog->selectedFiles(), QStringList() << filePath2); QCOMPARE(req.url().toLocalFile(), filePath2); } void KUrlRequesterTest::testComboRequester() { QFETCH(bool, editable); KUrlComboRequester req; req.show(); QList lineEdits = req.findChildren(); QVERIFY(lineEdits.isEmpty()); // no lineedits, only a readonly combo QSignalSpy textSpy(&req, &KUrlComboRequester::textChanged); QSignalSpy editSpy(&req, &KUrlComboRequester::textEdited); QSignalSpy returnSpy(&req, QOverload<>::of(&KUrlComboRequester::returnPressed)); QSignalSpy returnWithTextSpy(&req, QOverload::of(&KUrlComboRequester::returnPressed)); QVERIFY(!req.comboBox()->isEditable()); if (editable) { req.comboBox()->setEditable(true); const auto text = QStringLiteral("foobar"); QTest::keyClicks(req.comboBox(), text, Qt::NoModifier); QCOMPARE(textSpy.size(), text.size()); QCOMPARE(editSpy.size(), text.size()); QCOMPARE(textSpy.last().first().toString(), text); QCOMPARE(editSpy.last().first().toString(), text); QCOMPARE(returnSpy.size(), 0); QCOMPARE(returnWithTextSpy.size(), 0); QTest::keyEvent(QTest::Click, req.comboBox(), Qt::Key_Return); QCOMPARE(returnSpy.size(), 1); QCOMPARE(returnWithTextSpy.size(), 1); QCOMPARE(returnWithTextSpy.last().first().toString(), text); } else { const auto url1 = QUrl("file:///foo/bar/1"); const auto url2 = QUrl("file:///foo/bar/2"); req.comboBox()->addUrl(url1); QCOMPARE(textSpy.size(), 1); QCOMPARE(textSpy.last().first().toUrl(), url1); req.comboBox()->addUrl(url2); QCOMPARE(textSpy.size(), 1); QTest::keyEvent(QTest::Click, req.comboBox(), Qt::Key_Down); QCOMPARE(textSpy.size(), 2); QCOMPARE(textSpy.last().first().toUrl(), url2); // only editable combo boxes get the edit and return signals emitted QCOMPARE(editSpy.size(), 0); QCOMPARE(returnSpy.size(), 0); QCOMPARE(returnWithTextSpy.size(), 0); } } void KUrlRequesterTest::testComboRequester_data() { QTest::addColumn("editable"); QTest::newRow("read-only") << false; QTest::newRow("editable") << true; } QTEST_MAIN(KUrlRequesterTest) #include "kurlrequestertest.moc" diff --git a/autotests/listdirtest.cpp b/autotests/listdirtest.cpp index 6b62a519..6420fb21 100644 --- a/autotests/listdirtest.cpp +++ b/autotests/listdirtest.cpp @@ -1,80 +1,79 @@ /* * Copyright (C) 2013 Mark Gaiser * * 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 "listdirtest.h" #include -#include #include #include #include #include QTEST_MAIN(ListDirTest) void ListDirTest::initTestCase() { // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); } void ListDirTest::numFilesTestCase_data() { QTest::addColumn("numOfFiles"); QTest::newRow("10 files") << 10; QTest::newRow("100 files") << 100; QTest::newRow("1000 files") << 1000; } void ListDirTest::numFilesTestCase() { QFETCH(int, numOfFiles); QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); createEmptyTestFiles(numOfFiles, tempDir.path()); /*QBENCHMARK*/ { m_receivedEntryCount = -2; // We start at -2 for . and .. slotResult will just increment this value KIO::ListJob *job = KIO::listDir(QUrl::fromLocalFile(tempDir.path()), KIO::HideProgressInfo); job->setUiDelegate(nullptr); connect(job, &KIO::ListJob::entries, this, &ListDirTest::slotEntries); QSignalSpy spy(job, SIGNAL(result(KJob*))); QVERIFY(spy.wait(100000)); QCOMPARE(job->error(), 0); // no error } QCOMPARE(m_receivedEntryCount, numOfFiles); } void ListDirTest::slotEntries(KIO::Job *, const KIO::UDSEntryList &entries) { m_receivedEntryCount += entries.count(); } void ListDirTest::createEmptyTestFiles(int numOfFilesToCreate, const QString &path) { for (int i = 0; i < numOfFilesToCreate; i++) { const QString filename = path + QDir::separator() + QString::number(i) + ".txt"; QFile file(filename); QVERIFY(file.open(QIODevice::WriteOnly)); } QCOMPARE(QDir(path).entryList(QDir::Files).count(), numOfFilesToCreate); } diff --git a/src/core/authinfo.cpp b/src/core/authinfo.cpp index 3567f1b2..50b87b02 100644 --- a/src/core/authinfo.cpp +++ b/src/core/authinfo.cpp @@ -1,482 +1,481 @@ /* * This file is part of the KDE libraries * Copyright (C) 2000-2001 Dawit Alemayehu * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "authinfo.h" -#include #include #include #include #include #include #include #include using namespace KIO; ////// class ExtraField { public: ExtraField() : flags(AuthInfo::ExtraFieldNoFlags) { } ExtraField(const ExtraField &other) : customTitle(other.customTitle), flags(other.flags), value(other.value) { } ExtraField &operator=(const ExtraField &other) { customTitle = other.customTitle; flags = other.flags; value = other.value; return *this; } QString customTitle; // reserved for future use AuthInfo::FieldFlags flags; QVariant value; }; Q_DECLARE_METATYPE(ExtraField) static QDataStream &operator<< (QDataStream &s, const ExtraField &extraField) { s << extraField.customTitle; s << static_cast(extraField.flags); s << extraField.value; return s; } static QDataStream &operator>> (QDataStream &s, ExtraField &extraField) { s >> extraField.customTitle; int i; s >> i; extraField.flags = AuthInfo::FieldFlags(i); s >> extraField.value; return s; } static QDBusArgument &operator<<(QDBusArgument &argument, const ExtraField &extraField) { argument.beginStructure(); argument << extraField.customTitle << static_cast(extraField.flags) << QDBusVariant(extraField.value); argument.endStructure(); return argument; } static const QDBusArgument &operator>>(const QDBusArgument &argument, ExtraField &extraField) { QDBusVariant value; int flag; argument.beginStructure(); argument >> extraField.customTitle >> flag >> value; argument.endStructure(); extraField.value = value.variant(); extraField.flags = KIO::AuthInfo::FieldFlags(flag); return argument; } class KIO::AuthInfoPrivate { public: QMap extraFields; }; ////// AuthInfo::AuthInfo() : d(new AuthInfoPrivate()) { modified = false; readOnly = false; verifyPath = false; keepPassword = false; AuthInfo::registerMetaTypes(); } AuthInfo::AuthInfo(const AuthInfo &info) : d(new AuthInfoPrivate()) { (*this) = info; AuthInfo::registerMetaTypes(); } AuthInfo::~AuthInfo() { delete d; } AuthInfo &AuthInfo::operator= (const AuthInfo &info) { url = info.url; username = info.username; password = info.password; prompt = info.prompt; caption = info.caption; comment = info.comment; commentLabel = info.commentLabel; realmValue = info.realmValue; digestInfo = info.digestInfo; verifyPath = info.verifyPath; readOnly = info.readOnly; keepPassword = info.keepPassword; modified = info.modified; d->extraFields = info.d->extraFields; return *this; } bool AuthInfo::isModified() const { return modified; } void AuthInfo::setModified(bool flag) { modified = flag; } ///// void AuthInfo::setExtraField(const QString &fieldName, const QVariant &value) { d->extraFields[fieldName].value = value; } void AuthInfo::setExtraFieldFlags(const QString &fieldName, const FieldFlags flags) { d->extraFields[fieldName].flags = flags; } QVariant AuthInfo::getExtraField(const QString &fieldName) const { const auto it = d->extraFields.constFind(fieldName); if (it == d->extraFields.constEnd()) { return QVariant(); } return it->value; } AuthInfo::FieldFlags AuthInfo::getExtraFieldFlags(const QString &fieldName) const { const auto it = d->extraFields.constFind(fieldName); if (it == d->extraFields.constEnd()) { return AuthInfo::ExtraFieldNoFlags; } return it->flags; } void AuthInfo::registerMetaTypes() { qRegisterMetaType(); qRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); } ///// QDataStream &KIO::operator<< (QDataStream &s, const AuthInfo &a) { s << quint8(1) << a.url << a.username << a.password << a.prompt << a.caption << a.comment << a.commentLabel << a.realmValue << a.digestInfo << a.verifyPath << a.readOnly << a.keepPassword << a.modified << a.d->extraFields; return s; } QDataStream &KIO::operator>> (QDataStream &s, AuthInfo &a) { quint8 version; s >> version >> a.url >> a.username >> a.password >> a.prompt >> a.caption >> a.comment >> a.commentLabel >> a.realmValue >> a.digestInfo >> a.verifyPath >> a.readOnly >> a.keepPassword >> a.modified >> a.d->extraFields; return s; } QDBusArgument &KIO::operator<<(QDBusArgument &argument, const AuthInfo &a) { argument.beginStructure(); argument << quint8(1) << a.url.toString() << a.username << a.password << a.prompt << a.caption << a.comment << a.commentLabel << a.realmValue << a.digestInfo << a.verifyPath << a.readOnly << a.keepPassword << a.modified << a.d->extraFields; argument.endStructure(); return argument; } const QDBusArgument &KIO::operator>>(const QDBusArgument &argument, AuthInfo &a) { QString url; quint8 version; argument.beginStructure(); argument >> version >> url >> a.username >> a.password >> a.prompt >> a.caption >> a.comment >> a.commentLabel >> a.realmValue >> a.digestInfo >> a.verifyPath >> a.readOnly >> a.keepPassword >> a.modified >> a.d->extraFields; argument.endStructure(); a.url = QUrl(url); return argument; } typedef QList LoginList; typedef QMap LoginMap; class Q_DECL_HIDDEN NetRC::NetRCPrivate { public: NetRCPrivate() : isDirty(false), index(-1) {} QString extract(const QString &buf, const QString &key); void getMachinePart(const QString &line); void getMacdefPart(const QString &line); bool isDirty; LoginMap loginMap; QTextStream fstream; QString type; int index; }; NetRC *NetRC::instance = nullptr; NetRC::NetRC() : d(new NetRCPrivate) { } NetRC::~NetRC() { delete instance; instance = nullptr; delete d; } NetRC *NetRC::self() { if (!instance) { instance = new NetRC; } return instance; } bool NetRC::lookup(const QUrl &url, AutoLogin &login, bool userealnetrc, const QString &_type, LookUpMode mode) { //qDebug() << "AutoLogin lookup for: " << url.host(); if (!url.isValid()) { return false; } QString type = _type; if (type.isEmpty()) { type = url.scheme(); } if (d->loginMap.isEmpty() || d->isDirty) { d->loginMap.clear(); QString filename = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/kionetrc"); bool kionetrcStatus = parse(filename); bool netrcStatus = false; if (userealnetrc) { filename = QDir::homePath() + QLatin1String("/.netrc"); netrcStatus = parse(filename); } if (!(kionetrcStatus || netrcStatus)) { return false; } } const auto loginIt = d->loginMap.constFind(type); if (loginIt == d->loginMap.constEnd()) { return false; } const LoginList &l = *loginIt; if (l.isEmpty()) { return false; } for (LoginList::ConstIterator it = l.begin(); it != l.end(); ++it) { const AutoLogin &log = *it; if ((mode & defaultOnly) == defaultOnly && log.machine == QLatin1String("default") && (login.login.isEmpty() || login.login == log.login)) { login.type = log.type; login.machine = log.machine; login.login = log.login; login.password = log.password; login.macdef = log.macdef; } if ((mode & presetOnly) == presetOnly && log.machine == QLatin1String("preset") && (login.login.isEmpty() || login.login == log.login)) { login.type = log.type; login.machine = log.machine; login.login = log.login; login.password = log.password; login.macdef = log.macdef; } if ((mode & exactOnly) == exactOnly && log.machine == url.host() && (login.login.isEmpty() || login.login == log.login)) { login.type = log.type; login.machine = log.machine; login.login = log.login; login.password = log.password; login.macdef = log.macdef; break; } } return true; } void NetRC::reload() { d->isDirty = true; } bool NetRC::parse(const QString &fileName) { QFile file(fileName); if (file.permissions() != (QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)) { return false; } if (!file.open(QIODevice::ReadOnly)) { return false; } d->fstream.setDevice(&file); QString line; while (!d->fstream.atEnd()) { line = d->fstream.readLine().simplified(); // If line is a comment or is empty, read next line if ((line.startsWith(QLatin1Char('#')) || line.isEmpty())) { continue; } // If line refers to a machine, maybe it is spread in more lines. // getMachinePart() will take care of getting all the info and putting it into loginMap. if ((line.startsWith(QLatin1String("machine")) || line.startsWith(QLatin1String("default")) || line.startsWith(QLatin1String("preset")))) { d->getMachinePart(line); continue; } // If line refers to a macdef, it will be more than one line. // getMacdefPart() will take care of getting all the lines of the macro // and putting them into loginMap if (line.startsWith(QLatin1String("macdef"))) { d->getMacdefPart(line); continue; } } return true; } QString NetRC::NetRCPrivate::extract(const QString &buf, const QString &key) { QStringList stringList = buf.split(QLatin1Char(' '), QString::SkipEmptyParts); int i = stringList.indexOf(key); if ((i != -1) && (i + 1 < stringList.size())) { return stringList.at(i + 1); } else { return QString(); } } void NetRC::NetRCPrivate::getMachinePart(const QString &line) { QString buf = line; while (!(buf.contains(QStringLiteral("login")) && (buf.contains(QStringLiteral("password")) || buf.contains(QStringLiteral("account")) || buf.contains(QStringLiteral("type"))))) { buf += QLatin1Char(' ') + fstream.readLine().simplified(); } // Once we've got all the info, process it. AutoLogin l; l.machine = extract(buf, QStringLiteral("machine")); if (l.machine.isEmpty()) { if (buf.contains(QStringLiteral("default"))) { l.machine = QStringLiteral("default"); } else if (buf.contains(QStringLiteral("preset"))) { l.machine = QStringLiteral("preset"); } } l.login = extract(buf, QStringLiteral("login")); l.password = extract(buf, QStringLiteral("password")); if (l.password.isEmpty()) { l.password = extract(buf, QStringLiteral("account")); } type = l.type = extract(buf, QStringLiteral("type")); if (l.type.isEmpty() && !l.machine.isEmpty()) { type = l.type = QStringLiteral("ftp"); } loginMap[l.type].append(l); index = loginMap[l.type].count() - 1; } void NetRC::NetRCPrivate::getMacdefPart(const QString &line) { QString buf = line; QString macro = extract(buf, QStringLiteral("macdef")); QString newLine; while (!fstream.atEnd()) { newLine = fstream.readLine().simplified(); if (!newLine.isEmpty()) { buf += QLatin1Char('\n') + newLine; } else { break; } } loginMap[type][index].macdef[macro].append(buf); } diff --git a/src/core/batchrenamejob.cpp b/src/core/batchrenamejob.cpp index 64a863e1..387bab67 100644 --- a/src/core/batchrenamejob.cpp +++ b/src/core/batchrenamejob.cpp @@ -1,213 +1,212 @@ /* This file is part of the KDE libraries Copyright (C) 2017 by Chinmoy Ranjan Pradhan 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.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. 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, see . */ #include "batchrenamejob.h" #include "job_p.h" #include "copyjob.h" #include -#include #include #include using namespace KIO; class KIO::BatchRenameJobPrivate : public KIO::JobPrivate { public: BatchRenameJobPrivate(const QList &src, const QString &newName, int index, QChar placeHolder, JobFlags flags) : JobPrivate(), m_srcList(src), m_newName(newName), m_index(index), m_placeHolder(placeHolder), m_listIterator(m_srcList.constBegin()), m_allExtensionsDifferent(true), m_useIndex(true), m_appendIndex(false), m_flags(flags) { // There occur four cases when renaming multiple files, // 1. All files have different extension and $newName contains a valid placeholder. // 2. At least two files have same extension and $newName contains a valid placeholder. // In these two cases the placeholder character will be replaced by an integer($index). // 3. All files have different extension and new name contains an invalid placeholder // (this means either $newName doesn't contain the placeholder or the placeholders // are not in a connected sequence). // In this case nothing is substituted and all files have the same $newName. // 4. At least two files have same extension and $newName contains an invalid placeholder. // In this case $index is appended to $newName. // Check for extensions. QSet extensions; QMimeDatabase db; foreach (const QUrl &url, m_srcList) { const QString extension = db.suffixForFileName(url.toDisplayString().toLower()); if (extensions.contains(extension)) { m_allExtensionsDifferent = false; break; } extensions.insert(extension); } // Check for exactly one placeholder character or exactly one sequence of placeholders. int pos = newName.indexOf(placeHolder); if (pos != -1) { while (pos < newName.size() && newName.at(pos) == placeHolder) { pos++; } } const bool validPlaceholder = (newName.indexOf(placeHolder, pos) == -1); if (!validPlaceholder) { if (!m_allExtensionsDifferent) { m_appendIndex = true; } else { m_useIndex = false; } } } QList m_srcList; QString m_newName; int m_index; QChar m_placeHolder; QList::const_iterator m_listIterator; bool m_allExtensionsDifferent; bool m_useIndex; bool m_appendIndex; QUrl m_newUrl; // for fileRenamed signal const JobFlags m_flags; Q_DECLARE_PUBLIC(BatchRenameJob) void slotStart(); QString indexedName(const QString& name, int index, QChar placeHolder) const; static inline BatchRenameJob *newJob(const QList &src, const QString &newName, int index, QChar placeHolder, JobFlags flags) { BatchRenameJob *job = new BatchRenameJob(*new BatchRenameJobPrivate(src, newName, index, placeHolder, flags)); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); if (!(flags & HideProgressInfo)) { KIO::getJobTracker()->registerJob(job); } if (!(flags & NoPrivilegeExecution)) { job->d_func()->m_privilegeExecutionEnabled = true; job->d_func()->m_operationType = Rename; } return job; } }; BatchRenameJob::BatchRenameJob(BatchRenameJobPrivate &dd) : Job(dd) { QTimer::singleShot(0, this, SLOT(slotStart())); } BatchRenameJob::~BatchRenameJob() { } QString BatchRenameJobPrivate::indexedName(const QString& name, int index, QChar placeHolder) const { if (!m_useIndex) { return name; } QString newName = name; QString indexString = QString::number(index); if (m_appendIndex) { newName.append(indexString); return newName; } // Insert leading zeros if necessary const int minIndexLength = name.count(placeHolder); indexString.prepend(QString(minIndexLength - indexString.length(), QLatin1Char('0'))); // Replace the index placeholders by the indexString const int placeHolderStart = newName.indexOf(placeHolder); newName.replace(placeHolderStart, minIndexLength, indexString); return newName; } void BatchRenameJobPrivate::slotStart() { Q_Q(BatchRenameJob); if (m_listIterator == m_srcList.constBegin()) { // emit total q->setTotalAmount(KJob::Files, m_srcList.count()); } if (m_listIterator != m_srcList.constEnd()) { QString newName = indexedName(m_newName, m_index, m_placeHolder); const QUrl oldUrl = *m_listIterator; QMimeDatabase db; const QString extension = db.suffixForFileName(oldUrl.path().toLower()); if (!extension.isEmpty()) { newName += QLatin1Char('.') + extension; } m_newUrl = oldUrl.adjusted(QUrl::RemoveFilename); m_newUrl.setPath(m_newUrl.path() + KIO::encodeFileName(newName)); KIO::Job * job = KIO::moveAs(oldUrl, m_newUrl, KIO::HideProgressInfo); job->setParentJob(q); q->addSubjob(job); q->setProcessedAmount(KJob::Files, q->processedAmount(KJob::Files) + 1); } else { q->emitResult(); } } void BatchRenameJob::slotResult(KJob *job) { Q_D(BatchRenameJob); if (job->error()) { KIO::Job::slotResult(job); return; } removeSubjob(job); emit fileRenamed(*d->m_listIterator, d->m_newUrl); ++d->m_listIterator; ++d->m_index; emitPercent(d->m_listIterator - d->m_srcList.constBegin(), d->m_srcList.count()); d->slotStart(); } BatchRenameJob * KIO::batchRename(const QList &src, const QString &newName, int index, QChar placeHolder, KIO::JobFlags flags) { return BatchRenameJobPrivate::newJob(src, newName, index, placeHolder, flags); } #include "moc_batchrenamejob.cpp" diff --git a/src/core/chmodjob.cpp b/src/core/chmodjob.cpp index 0c013da7..51b32910 100644 --- a/src/core/chmodjob.cpp +++ b/src/core/chmodjob.cpp @@ -1,294 +1,293 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow David Faure 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 "chmodjob.h" #include "../pathhelpers_p.h" #include #include -#include #include #include #include "listjob.h" #include "job_p.h" #include "jobuidelegatefactory.h" #include "kioglobal_p.h" namespace KIO { struct ChmodInfo { QUrl url; int permissions; }; enum ChmodJobState { CHMODJOB_STATE_LISTING, CHMODJOB_STATE_CHMODING }; class ChmodJobPrivate: public KIO::JobPrivate { public: ChmodJobPrivate(const KFileItemList &lstItems, int permissions, int mask, KUserId newOwner, KGroupId newGroup, bool recursive) : state(CHMODJOB_STATE_LISTING) , m_permissions(permissions) , m_mask(mask) , m_newOwner(newOwner) , m_newGroup(newGroup) , m_recursive(recursive) , m_bAutoSkipFiles(false) , m_lstItems(lstItems) { } ChmodJobState state; int m_permissions; int m_mask; KUserId m_newOwner; KGroupId m_newGroup; bool m_recursive; bool m_bAutoSkipFiles; KFileItemList m_lstItems; QLinkedList m_infos; // linkedlist since we keep removing the first item void _k_chmodNextFile(); void _k_slotEntries(KIO::Job *, const KIO::UDSEntryList &); void _k_processList(); Q_DECLARE_PUBLIC(ChmodJob) static inline ChmodJob *newJob(const KFileItemList &lstItems, int permissions, int mask, KUserId newOwner, KGroupId newGroup, bool recursive, JobFlags flags) { ChmodJob *job = new ChmodJob(*new ChmodJobPrivate(lstItems, permissions, mask, newOwner, newGroup, recursive)); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); if (!(flags & HideProgressInfo)) { KIO::getJobTracker()->registerJob(job); } if (!(flags & NoPrivilegeExecution)) { job->d_func()->m_privilegeExecutionEnabled = true; job->d_func()->m_operationType = ChangeAttr; } return job; } }; } // namespace KIO using namespace KIO; ChmodJob::ChmodJob(ChmodJobPrivate &dd) : KIO::Job(dd) { QMetaObject::invokeMethod(this, "_k_processList", Qt::QueuedConnection); } ChmodJob::~ChmodJob() { } void ChmodJobPrivate::_k_processList() { Q_Q(ChmodJob); while (!m_lstItems.isEmpty()) { const KFileItem item = m_lstItems.first(); if (!item.isLink()) { // don't do anything with symlinks // File or directory -> remember to chmod ChmodInfo info; info.url = item.url(); // This is a toplevel file, we apply changes directly (no +X emulation here) const mode_t permissions = item.permissions() & 0777; // get rid of "set gid" and other special flags info.permissions = (m_permissions & m_mask) | (permissions & ~m_mask); /*//qDebug() << "toplevel url:" << info.url << "\n current permissions=" << QString::number(permissions,8) << "\n wanted permission=" << QString::number(m_permissions,8) << "\n with mask=" << QString::number(m_mask,8) << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~m_mask,8) << "\n bits we keep =" << QString::number(permissions & ~m_mask,8) << "\n new permissions = " << QString::number(info.permissions,8);*/ m_infos.prepend(info); //qDebug() << "processList : Adding info for " << info.url; // Directory and recursive -> list if (item.isDir() && m_recursive) { //qDebug() << "ChmodJob::processList dir -> listing"; KIO::ListJob *listJob = KIO::listRecursive(item.url(), KIO::HideProgressInfo); q->connect(listJob, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), SLOT(_k_slotEntries(KIO::Job*,KIO::UDSEntryList))); q->addSubjob(listJob); return; // we'll come back later, when this one's finished } } m_lstItems.removeFirst(); } //qDebug() << "ChmodJob::processList -> going to STATE_CHMODING"; // We have finished, move on state = CHMODJOB_STATE_CHMODING; _k_chmodNextFile(); } void ChmodJobPrivate::_k_slotEntries(KIO::Job *, const KIO::UDSEntryList &list) { KIO::UDSEntryList::ConstIterator it = list.begin(); KIO::UDSEntryList::ConstIterator end = list.end(); for (; it != end; ++it) { const KIO::UDSEntry &entry = *it; const bool isLink = !entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST).isEmpty(); const QString relativePath = entry.stringValue(KIO::UDSEntry::UDS_NAME); if (!isLink && relativePath != QLatin1String("..")) { const mode_t permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS) & 0777; // get rid of "set gid" and other special flags ChmodInfo info; info.url = m_lstItems.first().url(); // base directory info.url.setPath(concatPaths(info.url.path(), relativePath)); int mask = m_mask; // Emulate -X: only give +x to files that had a +x bit already // So the check is the opposite : if the file had no x bit, don't touch x bits // For dirs this doesn't apply if (!entry.isDir()) { int newPerms = m_permissions & mask; if ((newPerms & 0111) && !(permissions & 0111)) { // don't interfere with mandatory file locking if (newPerms & 02000) { mask = mask & ~0101; } else { mask = mask & ~0111; } } } info.permissions = (m_permissions & mask) | (permissions & ~mask); /*//qDebug() << info.url << "\n current permissions=" << QString::number(permissions,8) << "\n wanted permission=" << QString::number(m_permissions,8) << "\n with mask=" << QString::number(mask,8) << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~mask,8) << "\n bits we keep =" << QString::number(permissions & ~mask,8) << "\n new permissions = " << QString::number(info.permissions,8);*/ // Prepend this info in our todo list. // This way, the toplevel dirs are done last. m_infos.prepend(info); } } } void ChmodJobPrivate::_k_chmodNextFile() { Q_Q(ChmodJob); if (!m_infos.isEmpty()) { ChmodInfo info = m_infos.takeFirst(); // First update group / owner (if local file) // (permissions have to set after, in case of suid and sgid) if (info.url.isLocalFile() && (m_newOwner.isValid() || m_newGroup.isValid())) { QString path = info.url.toLocalFile(); if (!KIOPrivate::changeOwnership(path, m_newOwner, m_newGroup)) { if (!m_uiDelegateExtension) { emit q->warning(q, i18n("Could not modify the ownership of file %1", path)); } else if (!m_bAutoSkipFiles) { const QString errMsg = i18n("Could not modify the ownership of file %1. You have insufficient access to the file to perform the change.", path); SkipDialog_Options options; if (m_infos.count() > 1) { options |= SkipDialog_MultipleItems; } const SkipDialog_Result skipResult = m_uiDelegateExtension->askSkip(q, options, errMsg); switch (skipResult) { case Result_AutoSkip: m_bAutoSkipFiles = true; // fall through Q_FALLTHROUGH(); case Result_Skip: QMetaObject::invokeMethod(q, "_k_chmodNextFile", Qt::QueuedConnection); return; case Result_Retry: m_infos.prepend(info); QMetaObject::invokeMethod(q, "_k_chmodNextFile", Qt::QueuedConnection); return; case Result_Cancel: default: q->setError(ERR_USER_CANCELED); q->emitResult(); return; } } } } /*qDebug() << "chmod'ing" << info.url << "to" << QString::number(info.permissions,8);*/ KIO::SimpleJob *job = KIO::chmod(info.url, info.permissions); job->setParentJob(q); // copy the metadata for acl and default acl const QString aclString = q->queryMetaData(QStringLiteral("ACL_STRING")); const QString defaultAclString = q->queryMetaData(QStringLiteral("DEFAULT_ACL_STRING")); if (!aclString.isEmpty()) { job->addMetaData(QStringLiteral("ACL_STRING"), aclString); } if (!defaultAclString.isEmpty()) { job->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), defaultAclString); } q->addSubjob(job); } else // We have finished { q->emitResult(); } } void ChmodJob::slotResult(KJob *job) { Q_D(ChmodJob); removeSubjob(job); if (job->error()) { setError(job->error()); setErrorText(job->errorText()); emitResult(); return; } //qDebug() << "d->m_lstItems:" << d->m_lstItems.count(); switch (d->state) { case CHMODJOB_STATE_LISTING: d->m_lstItems.removeFirst(); //qDebug() << "-> processList"; d->_k_processList(); return; case CHMODJOB_STATE_CHMODING: //qDebug() << "-> chmodNextFile"; d->_k_chmodNextFile(); return; default: Q_ASSERT(false); return; } } ChmodJob *KIO::chmod(const KFileItemList &lstItems, int permissions, int mask, const QString &owner, const QString &group, bool recursive, JobFlags flags) { KUserId uid = KUserId::fromName(owner); KGroupId gid = KGroupId::fromName(group); return ChmodJobPrivate::newJob(lstItems, permissions, mask, uid, gid, recursive, flags); } #include "moc_chmodjob.cpp" diff --git a/src/core/connection.cpp b/src/core/connection.cpp index f5094e08..dfe5318f 100644 --- a/src/core/connection.cpp +++ b/src/core/connection.cpp @@ -1,241 +1,239 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow David Faure Copyright (C) 2007 Thiago Macieira 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 "connection_p.h" #include #include "connectionbackend_p.h" #include "kiocoredebug.h" #include -#include -#include using namespace KIO; void ConnectionPrivate::dequeue() { if (!backend || suspended) { return; } foreach (const Task &task, outgoingTasks) { q->sendnow(task.cmd, task.data); } outgoingTasks.clear(); if (!incomingTasks.isEmpty()) { emit q->readyRead(); } } void ConnectionPrivate::commandReceived(const Task &task) { //qDebug() << this << "Command " << task.cmd << " added to the queue"; if (!suspended && incomingTasks.isEmpty() && readMode == Connection::ReadMode::EventDriven) { QMetaObject::invokeMethod(q, "dequeue", Qt::QueuedConnection); } incomingTasks.append(task); } void ConnectionPrivate::disconnected() { q->close(); if (readMode == Connection::ReadMode::EventDriven) { QMetaObject::invokeMethod(q, "readyRead", Qt::QueuedConnection); } } void ConnectionPrivate::setBackend(ConnectionBackend *b) { delete backend; backend = b; if (backend) { q->connect(backend, SIGNAL(commandReceived(Task)), SLOT(commandReceived(Task))); q->connect(backend, SIGNAL(disconnected()), SLOT(disconnected())); backend->setSuspended(suspended); } } Connection::Connection(QObject *parent) : QObject(parent), d(new ConnectionPrivate) { d->q = this; } Connection::~Connection() { close(); delete d; } void Connection::suspend() { //qDebug() << this << "Suspended"; d->suspended = true; if (d->backend) { d->backend->setSuspended(true); } } void Connection::resume() { // send any outgoing or incoming commands that may be in queue if (d->readMode == Connection::ReadMode::EventDriven) { QMetaObject::invokeMethod(this, "dequeue", Qt::QueuedConnection); } //qDebug() << this << "Resumed"; d->suspended = false; if (d->backend) { d->backend->setSuspended(false); } } void Connection::close() { if (d->backend) { d->backend->disconnect(this); d->backend->deleteLater(); d->backend = nullptr; } d->outgoingTasks.clear(); d->incomingTasks.clear(); } bool Connection::isConnected() const { return d->backend && d->backend->state == ConnectionBackend::Connected; } bool Connection::inited() const { return d->backend; } bool Connection::suspended() const { return d->suspended; } void Connection::connectToRemote(const QUrl &address) { //qDebug() << "Connection requested to " << address; const QString scheme = address.scheme(); if (scheme == QLatin1String("local")) { d->setBackend(new ConnectionBackend(ConnectionBackend::LocalSocketMode, this)); } else if (scheme == QLatin1String("tcp")) { d->setBackend(new ConnectionBackend(ConnectionBackend::TcpSocketMode, this)); } else { qCWarning(KIO_CORE) << "Unknown protocol requested:" << scheme << "(" << address << ")"; Q_ASSERT(0); return; } // connection succeeded if (!d->backend->connectToRemote(address)) { //kWarning(7017) << "could not connect to" << address << "using scheme" << scheme ; delete d->backend; d->backend = nullptr; return; } d->dequeue(); } QString Connection::errorString() const { if (d->backend) { return d->backend->errorString; } return QString(); } bool Connection::send(int cmd, const QByteArray &data) { if (!inited() || !d->outgoingTasks.isEmpty()) { Task task; task.cmd = cmd; task.data = data; d->outgoingTasks.append(std::move(task)); return true; } else { return sendnow(cmd, data); } } bool Connection::sendnow(int cmd, const QByteArray &data) { if (!d->backend || data.size() > 0xffffff || !isConnected()) { return false; } //qDebug() << this << "Sending command " << _cmd << " of size " << data.size(); return d->backend->sendCommand(cmd, data); } bool Connection::hasTaskAvailable() const { return !d->incomingTasks.isEmpty(); } bool Connection::waitForIncomingTask(int ms) { if (!isConnected()) { return false; } if (d->backend) { return d->backend->waitForIncomingTask(ms); } return false; } int Connection::read(int *_cmd, QByteArray &data) { // if it's still empty, then it's an error if (d->incomingTasks.isEmpty()) { //kWarning() << this << "Task list is empty!"; return -1; } const Task& task = d->incomingTasks.constFirst(); //qDebug() << this << "Command " << task.cmd << " removed from the queue (size " // << task.data.size() << ")"; *_cmd = task.cmd; data = task.data; d->incomingTasks.removeFirst(); // if we didn't empty our reading queue, emit again if (!d->suspended && !d->incomingTasks.isEmpty() && d->readMode == Connection::ReadMode::EventDriven) { QMetaObject::invokeMethod(this, "dequeue", Qt::QueuedConnection); } return data.size(); } void Connection::setReadMode(ReadMode readMode) { d->readMode = readMode; } #include "moc_connection_p.cpp" diff --git a/src/core/connectionbackend.cpp b/src/core/connectionbackend.cpp index b3e34bc9..f5924fdf 100644 --- a/src/core/connectionbackend.cpp +++ b/src/core/connectionbackend.cpp @@ -1,340 +1,339 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow David Faure Copyright (C) 2007 Thiago Macieira 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 "connectionbackend_p.h" #include #include #include #include "klocalsocket.h" #include #include #include #include #include -#include #include #include "kiocoredebug.h" using namespace KIO; ConnectionBackend::ConnectionBackend(Mode m, QObject *parent) : QObject(parent), state(Idle), socket(nullptr), len(-1), cmd(0), signalEmitted(false), mode(m) { localServer = nullptr; } ConnectionBackend::~ConnectionBackend() { if (mode == LocalSocketMode && localServer && localServer->localSocketType() == KLocalSocket::UnixSocket) { QFile::remove(localServer->localPath()); } } void ConnectionBackend::setSuspended(bool enable) { if (state != Connected) { return; } Q_ASSERT(socket); Q_ASSERT(!localServer); // !tcpServer as well if (enable) { //qCDebug(KIO_CORE) << socket << "suspending"; socket->setReadBufferSize(1); } else { //qCDebug(KIO_CORE) << socket << "resuming"; // Calling setReadBufferSize from a readyRead slot leads to a bug in Qt, fixed in 13c246ee119 socket->setReadBufferSize(StandardBufferSize); if (socket->bytesAvailable() >= HeaderSize) { // there are bytes available QMetaObject::invokeMethod(this, "socketReadyRead", Qt::QueuedConnection); } // We read all bytes here, but we don't use readAll() because we need // to read at least one byte (even if there isn't any) so that the // socket notifier is reenabled QByteArray data = socket->read(socket->bytesAvailable() + 1); for (int i = data.size(); --i >= 0;) { socket->ungetChar(data[i]); } // Workaround Qt5 bug, readyRead isn't always emitted here... QMetaObject::invokeMethod(this, "socketReadyRead", Qt::QueuedConnection); } } bool ConnectionBackend::connectToRemote(const QUrl &url) { Q_ASSERT(state == Idle); Q_ASSERT(!socket); Q_ASSERT(!localServer); // !tcpServer as well if (mode == LocalSocketMode) { KLocalSocket *sock = new KLocalSocket(this); QString path = url.path(); #if 0 // TODO: Activate once abstract socket support is implemented in Qt. KLocalSocket::LocalSocketType type = KLocalSocket::UnixSocket; if (url.queryItem(QLatin1String("abstract")) == QLatin1String("1")) { type = KLocalSocket::AbstractUnixSocket; } #endif sock->connectToPath(path); socket = sock; } else { socket = new QTcpSocket(this); socket->connectToHost(url.host(), url.port()); if (!socket->waitForConnected(1000)) { state = Idle; qCDebug(KIO_CORE) << "could not connect to" << url; return false; } } connect(socket, &QIODevice::readyRead, this, &ConnectionBackend::socketReadyRead); connect(socket, &QAbstractSocket::disconnected, this, &ConnectionBackend::socketDisconnected); state = Connected; return true; } void ConnectionBackend::socketDisconnected() { state = Idle; emit disconnected(); } bool ConnectionBackend::listenForRemote() { Q_ASSERT(state == Idle); Q_ASSERT(!socket); Q_ASSERT(!localServer); // !tcpServer as well if (mode == LocalSocketMode) { const QString prefix = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); static QBasicAtomicInt s_socketCounter = Q_BASIC_ATOMIC_INITIALIZER(1); QString appName = QCoreApplication::instance()->applicationName(); appName.replace(QLatin1Char('/'), QLatin1Char('_')); // #357499 QTemporaryFile socketfile(prefix + QLatin1Char('/') + appName + QStringLiteral("XXXXXX.%1.slave-socket").arg(s_socketCounter.fetchAndAddAcquire(1))); if (!socketfile.open()) { errorString = i18n("Unable to create io-slave: %1", QString::fromUtf8(strerror(errno))); return false; } QString sockname = socketfile.fileName(); address.clear(); address.setScheme(QStringLiteral("local")); address.setPath(sockname); socketfile.setAutoRemove(false); socketfile.remove(); // can't bind if there is such a file localServer = new KLocalSocketServer(this); if (!localServer->listen(sockname, KLocalSocket::UnixSocket)) { errorString = localServer->errorString(); delete localServer; localServer = nullptr; return false; } connect(localServer, &KLocalSocketServer::newConnection, this, &ConnectionBackend::newConnection); } else { tcpServer = new QTcpServer(this); tcpServer->listen(QHostAddress::LocalHost); if (!tcpServer->isListening()) { errorString = tcpServer->errorString(); delete tcpServer; tcpServer = nullptr; return false; } address = QUrl(QLatin1String("tcp://127.0.0.1:") + QString::number(tcpServer->serverPort())); connect(tcpServer, &QTcpServer::newConnection, this, &ConnectionBackend::newConnection); } state = Listening; return true; } bool ConnectionBackend::waitForIncomingTask(int ms) { Q_ASSERT(state == Connected); Q_ASSERT(socket); if (socket->state() != QAbstractSocket::ConnectedState) { state = Idle; return false; // socket has probably closed, what do we do? } signalEmitted = false; if (socket->bytesAvailable()) { socketReadyRead(); } if (signalEmitted) { return true; // there was enough data in the socket } // not enough data in the socket, so wait for more QElapsedTimer timer; timer.start(); while (socket->state() == QAbstractSocket::ConnectedState && !signalEmitted && (ms == -1 || timer.elapsed() < ms)) if (!socket->waitForReadyRead(ms == -1 ? -1 : ms - timer.elapsed())) { break; } if (signalEmitted) { return true; } if (socket->state() != QAbstractSocket::ConnectedState) { state = Idle; } return false; } bool ConnectionBackend::sendCommand(int cmd, const QByteArray &data) const { Q_ASSERT(state == Connected); Q_ASSERT(socket); char buffer[HeaderSize + 2]; sprintf(buffer, "%6x_%2x_", data.size(), cmd); socket->write(buffer, HeaderSize); socket->write(data); //qCDebug(KIO_CORE) << this << "Sending command" << hex << cmd << "of" // << data.size() << "bytes (" << socket->bytesToWrite() // << "bytes left to write )"; // blocking mode: while (socket->bytesToWrite() > 0 && socket->state() == QAbstractSocket::ConnectedState) { socket->waitForBytesWritten(-1); } return socket->state() == QAbstractSocket::ConnectedState; } ConnectionBackend *ConnectionBackend::nextPendingConnection() { Q_ASSERT(state == Listening); Q_ASSERT(localServer || tcpServer); Q_ASSERT(!socket); //qCDebug(KIO_CORE) << "Got a new connection"; QTcpSocket *newSocket; if (mode == LocalSocketMode) { newSocket = localServer->nextPendingConnection(); } else { newSocket = tcpServer->nextPendingConnection(); } if (!newSocket) { return nullptr; // there was no connection... } ConnectionBackend *result = new ConnectionBackend(Mode(mode)); result->state = Connected; result->socket = newSocket; newSocket->setParent(result); connect(newSocket, &QIODevice::readyRead, result, &ConnectionBackend::socketReadyRead); connect(newSocket, &QAbstractSocket::disconnected, result, &ConnectionBackend::socketDisconnected); return result; } void ConnectionBackend::socketReadyRead() { bool shouldReadAnother; do { if (!socket) // might happen if the invokeMethods were delivered after we disconnected { return; } //qCDebug(KIO_CORE) << this << "Got" << socket->bytesAvailable() << "bytes"; if (len == -1) { // We have to read the header char buffer[HeaderSize]; if (socket->bytesAvailable() < HeaderSize) { return; // wait for more data } socket->read(buffer, sizeof buffer); buffer[6] = 0; buffer[9] = 0; char *p = buffer; while (*p == ' ') { p++; } len = strtol(p, nullptr, 16); p = buffer + 7; while (*p == ' ') { p++; } cmd = strtol(p, nullptr, 16); //qCDebug(KIO_CORE) << this << "Beginning of command" << hex << cmd << "of size" << len; } QPointer that = this; //qCDebug(KIO_CORE) << socket << "Want to read" << len << "bytes"; if (socket->bytesAvailable() >= len) { Task task; task.cmd = cmd; if (len) { task.data = socket->read(len); } len = -1; signalEmitted = true; emit commandReceived(task); } else if (len > StandardBufferSize) { qCDebug(KIO_CORE) << socket << "Jumbo packet of" << len << "bytes"; // Calling setReadBufferSize from a readyRead slot leads to a bug in Qt, fixed in 13c246ee119 socket->setReadBufferSize(len + 1); } // If we're dead, better don't try anything. if (that.isNull()) { return; } // Do we have enough for an another read? if (len == -1) { shouldReadAnother = socket->bytesAvailable() >= HeaderSize; } else { shouldReadAnother = socket->bytesAvailable() >= len; } } while (shouldReadAnother); } diff --git a/src/core/dataprotocol.cpp b/src/core/dataprotocol.cpp index 77791d8a..bd06920b 100644 --- a/src/core/dataprotocol.cpp +++ b/src/core/dataprotocol.cpp @@ -1,329 +1,328 @@ // dataprotocol.cpp // ================== // // Implementation of the data protocol (rfc 2397) // // Author: Leo Savernik // Email: l.savernik@aon.at // Copyright (C) 2002, 2003 by Leo Savernik // Created: Sam Dez 28 14:11:18 CET 2002 /*************************************************************************** * * * This program 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; version 2. * * * ***************************************************************************/ // TODO: remove me #undef QT_NO_CAST_FROM_ASCII #include "dataprotocol_p.h" #include "global.h" #include #include -#include #include #ifdef DATAKIOSLAVE # include # include #endif #if !defined(DATAKIOSLAVE) # define DISPATCH(f) dispatch_##f #else # define DISPATCH(f) f #endif using namespace KIO; #ifdef DATAKIOSLAVE extern "C" { int kdemain(int argc, char **argv) { //qDebug() << "*** Starting kio_data "; if (argc != 4) { //qDebug() << "Usage: kio_data protocol domain-socket1 domain-socket2"; exit(-1); } DataProtocol slave(argv[2], argv[3]); slave.dispatchLoop(); //qDebug() << "*** kio_data Done"; return 0; } } #endif /** structure containing header information */ struct DataHeader { QString mime_type; // mime type of content (lowercase) MetaData attributes; // attribute/value pairs (attribute lowercase, // value unchanged) bool is_base64; // true if data is base64 encoded QByteArray url; // reference to decoded url int data_offset; // zero-indexed position within url // where the real data begins. May point beyond // the end to indicate that there is no data }; /** returns the position of the first occurrence of any of the given * characters @p c1 or comma (',') or semicolon (';') or buf.length() * if none is contained. * * @param buf buffer where to look for c * @param begin zero-indexed starting position * @param c1 character to find or '\0' to ignore */ static int find(const QByteArray &buf, int begin, const char c1) { static const char comma = ','; static const char semicolon = ';'; int pos = begin; int size = buf.length(); while (pos < size) { const char ch = buf[pos]; if (ch == comma || ch == semicolon || (c1 != '\0' && ch == c1)) { break; } pos++; }/*wend*/ return pos; } /** extracts the string between the current position @p pos and the first * occurrence of either @p c1 or comma (',') or semicolon (';') exclusively * and updates @p pos to point at the found delimiter or at the end of the * buffer if neither character occurred. * @param buf buffer where to look for * @param pos zero-indexed position within buffer * @param c1 character to find or '\0' to ignore */ static inline QString extract(const QByteArray &buf, int &pos, const char c1 = '\0') { int oldpos = pos; pos = find(buf, oldpos, c1); return buf.mid(oldpos, pos - oldpos); } /** ignores all whitespaces * @param buf buffer to operate on * @param pos position to shift to first non-whitespace character * Upon return @p pos will either point to the first non-whitespace * character or to the end of the buffer. */ static inline void ignoreWS(const QString &buf, int &pos) { int size = buf.length(); while (pos < size && buf[pos].isSpace()) { ++pos; } } /** parses a quoted string as per rfc 822. * * If trailing quote is missing, the whole rest of the buffer is returned. * @param buf buffer to operate on * @param pos position pointing to the leading quote * @return the extracted string. @p pos will be updated to point to the * character following the trailing quote. */ static QString parseQuotedString(const QString &buf, int &pos) { int size = buf.length(); QString res; res.reserve(size); // can't be larger than buf pos++; // jump over leading quote bool escaped = false; // if true means next character is literal bool parsing = true; // true as long as end quote not found while (parsing && pos < size) { const QChar ch = buf[pos++]; if (escaped) { res += ch; escaped = false; } else { switch (ch.unicode()) { case '"': parsing = false; break; case '\\': escaped = true; break; default: res += ch; break; }/*end switch*/ }/*end if*/ }/*wend*/ res.squeeze(); return res; } /** parses the header of a data url * @param url the data url * @param mimeOnly if the only interesting information is the mime type * @return DataHeader structure with the header information */ static DataHeader parseDataHeader(const QUrl &url, const bool mimeOnly) { DataHeader header_info; // initialize header info members header_info.mime_type = QStringLiteral("text/plain"); header_info.attributes.insert(QStringLiteral("charset"), QStringLiteral("us-ascii")); header_info.is_base64 = false; // decode url and save it const QByteArray &raw_url = header_info.url = QByteArray::fromPercentEncoding(url.path(QUrl::FullyEncoded).toLatin1()); const int raw_url_len = raw_url.length(); header_info.data_offset = 0; // read mime type if (raw_url_len == 0) { return header_info; } const QString mime_type = extract(raw_url, header_info.data_offset).trimmed(); if (!mime_type.isEmpty()) { header_info.mime_type = mime_type; } if (mimeOnly) { return header_info; } if (header_info.data_offset >= raw_url_len) { return header_info; } // jump over delimiter token and return if data reached if (raw_url[header_info.data_offset++] == QLatin1Char(',')) { return header_info; } // read all attributes and store them bool data_begin_reached = false; while (!data_begin_reached && header_info.data_offset < raw_url_len) { // read attribute const QString attribute = extract(raw_url, header_info.data_offset, '=').trimmed(); if (header_info.data_offset >= raw_url_len || raw_url[header_info.data_offset] != QLatin1Char('=')) { // no assignment, must be base64 option if (attribute == QLatin1String("base64")) { header_info.is_base64 = true; } } else { header_info.data_offset++; // jump over '=' token // read value ignoreWS(raw_url, header_info.data_offset); if (header_info.data_offset >= raw_url_len) { return header_info; } QString value; if (raw_url[header_info.data_offset] == QLatin1Char('"')) { value = parseQuotedString(raw_url, header_info.data_offset); ignoreWS(raw_url, header_info.data_offset); } else { value = extract(raw_url, header_info.data_offset).trimmed(); } // add attribute to map header_info.attributes[attribute.toLower()] = value; }/*end if*/ if (header_info.data_offset < raw_url_len && raw_url[header_info.data_offset] == QLatin1Char(',')) { data_begin_reached = true; } header_info.data_offset++; // jump over separator token }/*wend*/ return header_info; } #ifdef DATAKIOSLAVE DataProtocol::DataProtocol(const QByteArray &pool_socket, const QByteArray &app_socket) : SlaveBase("kio_data", pool_socket, app_socket) { #else DataProtocol::DataProtocol() { #endif //qDebug(); } /* --------------------------------------------------------------------- */ DataProtocol::~DataProtocol() { //qDebug(); } /* --------------------------------------------------------------------- */ void DataProtocol::get(const QUrl &url) { ref(); //qDebug() << this; const DataHeader hdr = parseDataHeader(url, false); const int size = hdr.url.length(); const int data_ofs = qMin(hdr.data_offset, size); // FIXME: string is copied, would be nice if we could have a reference only const QByteArray url_data = hdr.url.mid(data_ofs); QByteArray outData; if (hdr.is_base64) { // base64 stuff is expected to contain the correct charset, so we just // decode it and pass it to the receiver outData = QByteArray::fromBase64(url_data); } else { QTextCodec *codec = QTextCodec::codecForName(hdr.attributes[QStringLiteral("charset")].toLatin1()); if (codec != nullptr) { outData = codec->toUnicode(url_data).toUtf8(); } else { outData = url_data; }/*end if*/ }/*end if*/ //qDebug() << "emit mimeType@"< * * 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 "filejob.h" #include "slavebase.h" #include "scheduler.h" #include "slave.h" -#include #include "job_p.h" class KIO::FileJobPrivate: public KIO::SimpleJobPrivate { public: FileJobPrivate(const QUrl &url, const QByteArray &packedArgs) : SimpleJobPrivate(url, CMD_OPEN, packedArgs), m_open(false), m_size(0) {} bool m_open; QString m_mimetype; KIO::filesize_t m_size; void slotRedirection(const QUrl &url); void slotData(const QByteArray &data); void slotMimetype(const QString &mimetype); void slotOpen(); void slotWritten(KIO::filesize_t); void slotFinished(); void slotPosition(KIO::filesize_t); void slotTotalSize(KIO::filesize_t); /** * @internal * Called by the scheduler when a @p slave gets to * work on this job. * @param slave the slave that starts working on this job */ void start(Slave *slave) override; Q_DECLARE_PUBLIC(FileJob) static inline FileJob *newJob(const QUrl &url, const QByteArray &packedArgs) { FileJob *job = new FileJob(*new FileJobPrivate(url, packedArgs)); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); return job; } }; using namespace KIO; FileJob::FileJob(FileJobPrivate &dd) : SimpleJob(dd) { } FileJob::~FileJob() { } void FileJob::read(KIO::filesize_t size) { Q_D(FileJob); if (!d->m_open) { return; } KIO_ARGS << size; d->m_slave->send(CMD_READ, packedArgs); } void FileJob::write(const QByteArray &_data) { Q_D(FileJob); if (!d->m_open) { return; } d->m_slave->send(CMD_WRITE, _data); } void FileJob::seek(KIO::filesize_t offset) { Q_D(FileJob); if (!d->m_open) { return; } KIO_ARGS << KIO::filesize_t(offset); d->m_slave->send(CMD_SEEK, packedArgs); } void FileJob::close() { Q_D(FileJob); if (!d->m_open) { return; } d->m_slave->send(CMD_CLOSE); // ### close? } KIO::filesize_t FileJob::size() { Q_D(FileJob); if (!d->m_open) { return 0; } return d->m_size; } // Slave sends data void FileJobPrivate::slotData(const QByteArray &_data) { Q_Q(FileJob); emit q_func()->data(q, _data); } void FileJobPrivate::slotRedirection(const QUrl &url) { Q_Q(FileJob); //qDebug() << url; emit q->redirection(q, url); } void FileJobPrivate::slotMimetype(const QString &type) { Q_Q(FileJob); m_mimetype = type; emit q->mimetype(q, m_mimetype); } void FileJobPrivate::slotPosition(KIO::filesize_t pos) { Q_Q(FileJob); emit q->position(q, pos); } void FileJobPrivate::slotTotalSize(KIO::filesize_t t_size) { m_size = t_size; Q_Q(FileJob); q->setTotalAmount(KJob::Bytes, m_size); } void FileJobPrivate::slotOpen() { Q_Q(FileJob); m_open = true; emit q->open(q); } void FileJobPrivate::slotWritten(KIO::filesize_t t_written) { Q_Q(FileJob); emit q->written(q, t_written); } void FileJobPrivate::slotFinished() { Q_Q(FileJob); //qDebug() << this << m_url; emit q->close(q); // Return slave to the scheduler slaveDone(); // Scheduler::doJob(this); q->emitResult(); } void FileJobPrivate::start(Slave *slave) { Q_Q(FileJob); q->connect(slave, SIGNAL(data(QByteArray)), SLOT(slotData(QByteArray))); q->connect(slave, SIGNAL(redirection(QUrl)), SLOT(slotRedirection(QUrl))); q->connect(slave, SIGNAL(mimeType(QString)), SLOT(slotMimetype(QString))); q->connect(slave, SIGNAL(open()), SLOT(slotOpen())); q->connect(slave, SIGNAL(position(KIO::filesize_t)), SLOT(slotPosition(KIO::filesize_t))); q->connect(slave, SIGNAL(written(KIO::filesize_t)), SLOT(slotWritten(KIO::filesize_t))); q->connect(slave, SIGNAL(totalSize(KIO::filesize_t)), SLOT(slotTotalSize(KIO::filesize_t))); SimpleJobPrivate::start(slave); } FileJob *KIO::open(const QUrl &url, QIODevice::OpenMode mode) { // Send decoded path and encoded query KIO_ARGS << url << mode; return FileJobPrivate::newJob(url, packedArgs); } #include "moc_filejob.cpp" diff --git a/src/core/global.cpp b/src/core/global.cpp index e2c3256e..abaa200c 100644 --- a/src/core/global.cpp +++ b/src/core/global.cpp @@ -1,304 +1,302 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "global.h" #include "kioglobal_p.h" #include "faviconscache_p.h" #include #include #include #include #include #include #include #include #include #include -#include -#include #include "kiocoredebug.h" KFormat::BinaryUnitDialect _k_loadBinaryDialect(); Q_GLOBAL_STATIC_WITH_ARGS(KFormat::BinaryUnitDialect, _k_defaultBinaryDialect, (_k_loadBinaryDialect())) KFormat::BinaryUnitDialect _k_loadBinaryDialect() { KConfigGroup mainGroup(KSharedConfig::openConfig(), "Locale"); KFormat::BinaryUnitDialect dialect(KFormat::BinaryUnitDialect(mainGroup.readEntry("BinaryUnitDialect", int(KFormat::DefaultBinaryDialect)))); dialect = static_cast(mainGroup.readEntry("BinaryUnitDialect", int(dialect))); // Error checking if (dialect <= KFormat::DefaultBinaryDialect || dialect > KFormat::LastBinaryDialect) { dialect = KFormat::IECBinaryDialect; } return dialect; } KIOCORE_EXPORT QString KIO::convertSize(KIO::filesize_t fileSize) { const KFormat::BinaryUnitDialect dialect = *_k_defaultBinaryDialect(); return KFormat().formatByteSize(fileSize, 1, dialect); } KIOCORE_EXPORT QString KIO::convertSizeFromKiB(KIO::filesize_t kibSize) { return convertSize(kibSize * 1024); } KIOCORE_EXPORT QString KIO::number(KIO::filesize_t size) { char charbuf[256]; sprintf(charbuf, "%lld", size); return QLatin1String(charbuf); } KIOCORE_EXPORT unsigned int KIO::calculateRemainingSeconds(KIO::filesize_t totalSize, KIO::filesize_t processedSize, KIO::filesize_t speed) { if ((speed != 0) && (totalSize != 0)) { return (totalSize - processedSize) / speed; } else { return 0; } } KIOCORE_EXPORT QString KIO::convertSeconds(unsigned int seconds) { unsigned int days = seconds / 86400; unsigned int hours = (seconds - (days * 86400)) / 3600; unsigned int mins = (seconds - (days * 86400) - (hours * 3600)) / 60; seconds = (seconds - (days * 86400) - (hours * 3600) - (mins * 60)); const QTime time(hours, mins, seconds); const QString timeStr(time.toString(QStringLiteral("hh:mm:ss"))); if (days > 0) { return i18np("1 day %2", "%1 days %2", days, timeStr); } else { return timeStr; } } #ifndef KIOCORE_NO_DEPRECATED KIOCORE_EXPORT QTime KIO::calculateRemaining(KIO::filesize_t totalSize, KIO::filesize_t processedSize, KIO::filesize_t speed) { QTime remainingTime; if (speed != 0) { KIO::filesize_t secs; if (totalSize == 0) { secs = 0; } else { secs = (totalSize - processedSize) / speed; } if (secs >= (24 * 60 * 60)) { // Limit to 23:59:59 secs = (24 * 60 * 60) - 1; } int hr = secs / (60 * 60); int mn = (secs - hr * 60 * 60) / 60; int sc = (secs - hr * 60 * 60 - mn * 60); remainingTime.setHMS(hr, mn, sc); } return remainingTime; } #endif KIOCORE_EXPORT QString KIO::itemsSummaryString(uint items, uint files, uint dirs, KIO::filesize_t size, bool showSize) { if (files == 0 && dirs == 0 && items == 0) { return i18np("%1 Item", "%1 Items", 0); } QString summary; const QString foldersText = i18np("1 Folder", "%1 Folders", dirs); const QString filesText = i18np("1 File", "%1 Files", files); if (files > 0 && dirs > 0) { summary = showSize ? i18nc("folders, files (size)", "%1, %2 (%3)", foldersText, filesText, KIO::convertSize(size)) : i18nc("folders, files", "%1, %2", foldersText, filesText); } else if (files > 0) { summary = showSize ? i18nc("files (size)", "%1 (%2)", filesText, KIO::convertSize(size)) : filesText; } else if (dirs > 0) { summary = foldersText; } if (items > dirs + files) { const QString itemsText = i18np("%1 Item", "%1 Items", items); summary = summary.isEmpty() ? itemsText : i18nc("items: folders, files (size)", "%1: %2", itemsText, summary); } return summary; } KIOCORE_EXPORT QString KIO::encodeFileName(const QString &_str) { QString str(_str); str.replace(QLatin1Char('/'), QChar(0x2044)); // "Fraction slash" return str; } KIOCORE_EXPORT QString KIO::decodeFileName(const QString &_str) { // Nothing to decode. "Fraction slash" is fine in filenames. return _str; } /*************************************************************** * * Utility functions * ***************************************************************/ KIO::CacheControl KIO::parseCacheControl(const QString &cacheControl) { QString tmp = cacheControl.toLower(); if (tmp == QLatin1String("cacheonly")) { return KIO::CC_CacheOnly; } if (tmp == QLatin1String("cache")) { return KIO::CC_Cache; } if (tmp == QLatin1String("verify")) { return KIO::CC_Verify; } if (tmp == QLatin1String("refresh")) { return KIO::CC_Refresh; } if (tmp == QLatin1String("reload")) { return KIO::CC_Reload; } qCDebug(KIO_CORE) << "unrecognized Cache control option:" << cacheControl; return KIO::CC_Verify; } QString KIO::getCacheControlString(KIO::CacheControl cacheControl) { if (cacheControl == KIO::CC_CacheOnly) { return QStringLiteral("CacheOnly"); } if (cacheControl == KIO::CC_Cache) { return QStringLiteral("Cache"); } if (cacheControl == KIO::CC_Verify) { return QStringLiteral("Verify"); } if (cacheControl == KIO::CC_Refresh) { return QStringLiteral("Refresh"); } if (cacheControl == KIO::CC_Reload) { return QStringLiteral("Reload"); } qCDebug(KIO_CORE) << "unrecognized Cache control enum value:" << cacheControl; return QString(); } QString KIO::favIconForUrl(const QUrl &url) { if (url.isLocalFile() || !url.scheme().startsWith(QLatin1String("http"))) { return QString(); } return FavIconsCache::instance()->iconForUrl(url); } QString KIO::iconNameForUrl(const QUrl &url) { const QLatin1String unknown("unknown"); if (url.scheme().isEmpty()) { // empty URL or relative URL (e.g. '~') return unknown; } QMimeDatabase db; const QMimeType mt = db.mimeTypeForUrl(url); const QString mimeTypeIcon = mt.iconName(); QString i = mimeTypeIcon; if (url.isLocalFile()) { // Check to see whether it's an xdg location (e.g. Pictures folder) if (mt.inherits(QStringLiteral("inode/directory"))) { i = KIOPrivate::iconForStandardPath(url.toLocalFile()); } // Let KFileItem::iconName handle things for us if (i == unknown || i.isEmpty() || mt.isDefault()) { const KFileItem item(url, mt.name()); i = item.iconName(); } } else { // It's non-local and maybe on a slow filesystem // Look for a favicon if (url.scheme().startsWith(QLatin1String("http"))) { i = favIconForUrl(url); } // Then handle the trash else if (url.scheme() == QLatin1String("trash") && url.path().length() <= 1) { KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig); if (trashConfig.group("Status").readEntry("Empty", true)) { i = QStringLiteral("user-trash"); } else { i = QStringLiteral("user-trash-full"); } } if (i.isEmpty()) { i = KProtocolInfo::icon(url.scheme()); } // root of protocol: if we found nothing, revert to mimeTypeIcon (which is usually "folder") if (url.path().length() <= 1 && (i == unknown || i.isEmpty())) { i = mimeTypeIcon; } } return !i.isEmpty() ? i : unknown; } QUrl KIO::upUrl(const QUrl &url) { if (!url.isValid() || url.isRelative()) { return QUrl(); } QUrl u(url); if (url.hasQuery()) { u.setQuery(QString()); return u; } if (url.hasFragment()) { u.setFragment(QString()); } u = u.adjusted(QUrl::StripTrailingSlash); /// don't combine with the line below return u.adjusted(QUrl::RemoveFilename); } #ifndef KIOCORE_NO_DEPRECATED QString KIO::suggestName(const QUrl &baseURL, const QString &oldName) { return KFileUtils::suggestName(baseURL, oldName); } #endif diff --git a/src/core/job.cpp b/src/core/job.cpp index ff959f06..2a87983b 100644 --- a/src/core/job.cpp +++ b/src/core/job.cpp @@ -1,403 +1,401 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow 2000-2009 David Faure 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 "job.h" #include "job_p.h" #include -#include -#include #include #include #include #include "slave.h" #include "scheduler.h" using namespace KIO; Job::Job() : KCompositeJob(nullptr) , d_ptr(new JobPrivate) { d_ptr->q_ptr = this; setCapabilities(KJob::Killable | KJob::Suspendable); } Job::Job(JobPrivate &dd) : KCompositeJob(nullptr) , d_ptr(&dd) { d_ptr->q_ptr = this; setCapabilities(KJob::Killable | KJob::Suspendable); } Job::~Job() { delete d_ptr; } // Exists for historical reasons only KJobUiDelegate *Job::ui() const { return uiDelegate(); } JobUiDelegateExtension *Job::uiDelegateExtension() const { Q_D(const Job); return d->m_uiDelegateExtension; } void Job::setUiDelegateExtension(JobUiDelegateExtension *extension) { Q_D(Job); d->m_uiDelegateExtension = extension; } bool Job::addSubjob(KJob *jobBase) { //qDebug() << "addSubjob(" << jobBase << ") this=" << this; bool ok = KCompositeJob::addSubjob(jobBase); KIO::Job *job = qobject_cast(jobBase); if (ok && job) { // Copy metadata into subjob (e.g. window-id, user-timestamp etc.) Q_D(Job); job->mergeMetaData(d->m_outgoingMetaData); // Forward information from that subjob. connect(job, &KJob::speed, this, [this](KJob *job, ulong speed) { Q_UNUSED(job); emitSpeed(speed); }); job->setProperty("window", property("window")); // see KJobWidgets job->setProperty("userTimestamp", property("userTimestamp")); // see KJobWidgets job->setUiDelegateExtension(d->m_uiDelegateExtension); } return ok; } bool Job::removeSubjob(KJob *jobBase) { //qDebug() << "removeSubjob(" << jobBase << ") this=" << this << "subjobs=" << subjobs().count(); return KCompositeJob::removeSubjob(jobBase); } static QString url_description_string(const QUrl& url) { return url.scheme() == QLatin1String("data") ? QStringLiteral("data:[...]") : KStringHandler::csqueeze(url.toDisplayString(QUrl::PreferLocalFile), 100); } KIO::JobPrivate::~JobPrivate() { } void JobPrivate::emitMoving(KIO::Job *job, const QUrl &src, const QUrl &dest) { static const QString s_title = i18nc("@title job", "Moving"); static const QString s_source = i18nc("The source of a file operation", "Source"); static const QString s_destination = i18nc("The destination of a file operation", "Destination"); emit job->description(job, s_title, qMakePair(s_source, url_description_string(src)), qMakePair(s_destination, url_description_string(dest))); } void JobPrivate::emitCopying(KIO::Job *job, const QUrl &src, const QUrl &dest) { static const QString s_title = i18nc("@title job", "Copying"); static const QString s_source = i18nc("The source of a file operation", "Source"); static const QString s_destination = i18nc("The destination of a file operation", "Destination"); emit job->description(job, s_title, qMakePair(s_source, url_description_string(src)), qMakePair(s_destination, url_description_string(dest))); } void JobPrivate::emitCreatingDir(KIO::Job *job, const QUrl &dir) { static const QString s_title = i18nc("@title job", "Creating directory"); static const QString s_directory = i18n("Directory"); emit job->description(job, s_title, qMakePair(s_directory, url_description_string(dir))); } void JobPrivate::emitDeleting(KIO::Job *job, const QUrl &url) { static const QString s_title = i18nc("@title job", "Deleting"); static const QString s_file = i18n("File"); emit job->description(job, s_title, qMakePair(s_file, url_description_string(url))); } void JobPrivate::emitStating(KIO::Job *job, const QUrl &url) { static const QString s_title = i18nc("@title job", "Examining"); static const QString s_file = i18n("File"); emit job->description(job, s_title, qMakePair(s_file, url_description_string(url))); } void JobPrivate::emitTransferring(KIO::Job *job, const QUrl &url) { static const QString s_title = i18nc("@title job", "Transferring"); static const QString s_source = i18nc("The source of a file operation", "Source"); emit job->description(job, s_title, qMakePair(s_source, url_description_string(url))); } void JobPrivate::emitMounting(KIO::Job *job, const QString &dev, const QString &point) { emit job->description(job, i18nc("@title job", "Mounting"), qMakePair(i18n("Device"), dev), qMakePair(i18n("Mountpoint"), point)); } void JobPrivate::emitUnmounting(KIO::Job *job, const QString &point) { emit job->description(job, i18nc("@title job", "Unmounting"), qMakePair(i18n("Mountpoint"), point)); } bool Job::doKill() { // kill all subjobs, without triggering their result slot Q_FOREACH (KJob *it, subjobs()) { it->kill(KJob::Quietly); } clearSubjobs(); return true; } bool Job::doSuspend() { Q_FOREACH (KJob *it, subjobs()) { if (!it->suspend()) { return false; } } return true; } bool Job::doResume() { Q_FOREACH (KJob *it, subjobs()) { if (!it->resume()) { return false; } } return true; } //Job::errorString is implemented in job_error.cpp void Job::setParentJob(Job *job) { Q_D(Job); Q_ASSERT(d->m_parentJob == nullptr); Q_ASSERT(job); d->m_parentJob = job; } Job *Job::parentJob() const { return d_func()->m_parentJob; } MetaData Job::metaData() const { return d_func()->m_incomingMetaData; } QString Job::queryMetaData(const QString &key) { return d_func()->m_incomingMetaData.value(key, QString()); } void Job::setMetaData(const KIO::MetaData &_metaData) { Q_D(Job); d->m_outgoingMetaData = _metaData; } void Job::addMetaData(const QString &key, const QString &value) { d_func()->m_outgoingMetaData.insert(key, value); } void Job::addMetaData(const QMap &values) { Q_D(Job); QMap::const_iterator it = values.begin(); for (; it != values.end(); ++it) { d->m_outgoingMetaData.insert(it.key(), it.value()); } } void Job::mergeMetaData(const QMap &values) { Q_D(Job); QMap::const_iterator it = values.begin(); for (; it != values.end(); ++it) // there's probably a faster way if (!d->m_outgoingMetaData.contains(it.key())) { d->m_outgoingMetaData.insert(it.key(), it.value()); } } MetaData Job::outgoingMetaData() const { return d_func()->m_outgoingMetaData; } QByteArray JobPrivate::privilegeOperationData() { PrivilegeOperationStatus status = OperationNotAllowed; if (m_parentJob) { QByteArray jobData = m_parentJob->d_func()->privilegeOperationData(); // Copy meta-data from parent job m_incomingMetaData.insert(QStringLiteral("TestData"), m_parentJob->queryMetaData(QStringLiteral("TestData"))); return jobData; } else { if (m_privilegeExecutionEnabled) { status = OperationAllowed; switch (m_operationType) { case ChangeAttr: m_caption = i18n("Change Attribute"); m_message = i18n("Root privileges are required to change file attributes. " "Do you want to continue?"); break; case Copy: m_caption = i18n("Copy Files"); m_message = i18n("Root privileges are required to complete the copy operation. " "Do you want to continue?"); break; case Delete: m_caption = i18n("Delete Files"); m_message = i18n("Root privileges are required to complete the delete operation. " "However, doing so may damage your system. Do you want to continue?"); break; case MkDir: m_caption = i18n("Create Folder"); m_message = i18n("Root privileges are required to create this folder. " "Do you want to continue?"); break; case Move: m_caption = i18n("Move Items"); m_message = i18n("Root privileges are required to complete the move operation. " "Do you want to continue?"); break; case Rename: m_caption = i18n("Rename"); m_message = i18n("Root privileges are required to complete renaming. " "Do you want to continue?"); break; case Symlink: m_caption = i18n("Create Symlink"); m_message = i18n("Root privileges are required to create a symlink. " "Do you want to continue?"); break; case Transfer: m_caption = i18n("Transfer data"); m_message = i18n("Root privileges are required to complete transferring data. " "Do you want to continue?"); Q_FALLTHROUGH(); default: break; } if (m_outgoingMetaData.value(QStringLiteral("UnitTesting")) == QLatin1String("true")) { // Set meta-data for the top-level job m_incomingMetaData.insert(QStringLiteral("TestData"), QStringLiteral("PrivilegeOperationAllowed")); } } } QByteArray parentJobData; QDataStream ds(&parentJobData, QIODevice::WriteOnly); ds << status << m_caption << m_message; return parentJobData; } ////////////////////////// class KIO::DirectCopyJobPrivate: public KIO::SimpleJobPrivate { public: DirectCopyJobPrivate(const QUrl &url, int command, const QByteArray &packedArgs) : SimpleJobPrivate(url, command, packedArgs) {} /** * @internal * Called by the scheduler when a @p slave gets to * work on this job. * @param slave the slave that starts working on this job */ void start(Slave *slave) override; Q_DECLARE_PUBLIC(DirectCopyJob) }; DirectCopyJob::DirectCopyJob(const QUrl &url, const QByteArray &packedArgs) : SimpleJob(*new DirectCopyJobPrivate(url, CMD_COPY, packedArgs)) { setUiDelegate(KIO::createDefaultJobUiDelegate()); } DirectCopyJob::~DirectCopyJob() { } void DirectCopyJobPrivate::start(Slave *slave) { Q_Q(DirectCopyJob); q->connect(slave, &SlaveInterface::canResume, q, &DirectCopyJob::slotCanResume); SimpleJobPrivate::start(slave); } void DirectCopyJob::slotCanResume(KIO::filesize_t offset) { emit canResume(this, offset); } ////////////////////////// SimpleJob *KIO::file_delete(const QUrl &src, JobFlags flags) { KIO_ARGS << src << qint8(true); // isFile SimpleJob *job = SimpleJobPrivate::newJob(src, CMD_DEL, packedArgs, flags); if (job->uiDelegateExtension()) { job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::RemoveContent); } return job; } ////////// //// #include "moc_job_base.cpp" #include "moc_job_p.cpp" diff --git a/src/core/kfileitem.cpp b/src/core/kfileitem.cpp index 804303cc..f30fec7a 100644 --- a/src/core/kfileitem.cpp +++ b/src/core/kfileitem.cpp @@ -1,1698 +1,1697 @@ /* This file is part of the KDE project Copyright (C) 1999-2011 David Faure 2001 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 "kfileitem.h" #include "kioglobal_p.h" #include "kiocoredebug.h" #include "../pathhelpers_p.h" #include #include #include #include -#include #include #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include #include #endif #include class KFileItemPrivate : public QSharedData { public: KFileItemPrivate(const KIO::UDSEntry &entry, mode_t mode, mode_t permissions, const QUrl &itemOrDirUrl, bool urlIsDirectory, bool delayedMimeTypes, KFileItem::MimeTypeDetermination mimeTypeDetermination) : m_entry(entry), m_url(itemOrDirUrl), m_strName(), m_strText(), m_iconName(), m_strLowerCaseName(), m_mimeType(), m_fileMode(mode), m_permissions(permissions), m_bLink(false), m_bIsLocalUrl(itemOrDirUrl.isLocalFile()), m_bMimeTypeKnown(false), m_delayedMimeTypes(delayedMimeTypes), m_useIconNameCache(false), m_hidden(Auto), m_slow(SlowUnknown), m_bSkipMimeTypeFromContent(mimeTypeDetermination == KFileItem::SkipMimeTypeFromContent), m_bInitCalled(false) { if (entry.count() != 0) { readUDSEntry(urlIsDirectory); } else { Q_ASSERT(!urlIsDirectory); m_strName = itemOrDirUrl.fileName(); m_strText = KIO::decodeFileName(m_strName); } } /** * Call init() if not yet done. */ void ensureInitialized() const; /** * Computes the text and mode from the UDSEntry. */ void init() const; QString localPath() const; KIO::filesize_t size() const; QDateTime time(KFileItem::FileTimes which) const; void setTime(KFileItem::FileTimes which, uint time_t_val) const; void setTime(KFileItem::FileTimes which, const QDateTime &val) const; bool cmp(const KFileItemPrivate &item) const; bool isSlow() const; /** * Extracts the data from the UDSEntry member and updates the KFileItem * accordingly. */ void readUDSEntry(bool _urlIsDirectory); /** * Parses the given permission set and provides it for access() */ QString parsePermissions(mode_t perm) const; /** * Mime type helper */ void determineMimeTypeHelper(const QUrl &url) const; /** * The UDSEntry that contains the data for this fileitem, if it came from a directory listing. */ mutable KIO::UDSEntry m_entry; /** * The url of the file */ QUrl m_url; /** * The text for this item, i.e. the file name without path, */ QString m_strName; /** * The text for this item, i.e. the file name without path, decoded * ('%%' becomes '%', '%2F' becomes '/') */ QString m_strText; /** * The icon name for this item. */ mutable QString m_iconName; /** * The filename in lower case (to speed up sorting) */ mutable QString m_strLowerCaseName; /** * The mimetype of the file */ mutable QMimeType m_mimeType; /** * The file mode */ mutable mode_t m_fileMode; /** * The permissions */ mutable mode_t m_permissions; /** * Whether the file is a link */ mutable bool m_bLink: 1; /** * True if local file */ bool m_bIsLocalUrl: 1; mutable bool m_bMimeTypeKnown: 1; mutable bool m_delayedMimeTypes: 1; /** True if m_iconName should be used as cache. */ mutable bool m_useIconNameCache: 1; // Auto: check leading dot. enum { Auto, Hidden, Shown } m_hidden: 3; // Slow? (nfs/smb/ssh) mutable enum { SlowUnknown, Fast, Slow } m_slow: 3; /** * True if mime type determination by content should be skipped */ bool m_bSkipMimeTypeFromContent: 1; /** * True if init() was called on demand */ mutable bool m_bInitCalled: 1; // For special case like link to dirs over FTP QString m_guessedMimeType; mutable QString m_access; }; void KFileItemPrivate::ensureInitialized() const { if (!m_bInitCalled) { init(); } } void KFileItemPrivate::init() const { m_access.clear(); // metaInfo = KFileMetaInfo(); // stat() local files if needed if (m_fileMode == KFileItem::Unknown || m_permissions == KFileItem::Unknown || m_entry.count() == 0) { if (m_url.isLocalFile()) { /* directories may not have a slash at the end if * we want to stat() them; it requires that we * change into it .. which may not be allowed * stat("/is/unaccessible") -> rwx------ * stat("/is/unaccessible/") -> EPERM H.Z. * This is the reason for the StripTrailingSlash */ QT_STATBUF buf; const QString path = m_url.adjusted(QUrl::StripTrailingSlash).toLocalFile(); const QByteArray pathBA = QFile::encodeName(path); if (QT_LSTAT(pathBA.constData(), &buf) == 0) { m_entry.reserve(9); m_entry.replace(KIO::UDSEntry::UDS_DEVICE_ID, buf.st_dev); m_entry.replace(KIO::UDSEntry::UDS_INODE, buf.st_ino); mode_t mode = buf.st_mode; if ((buf.st_mode & QT_STAT_MASK) == QT_STAT_LNK) { m_bLink = true; if (QT_STAT(pathBA.constData(), &buf) == 0) { mode = buf.st_mode; } else {// link pointing to nowhere (see FileProtocol::createUDSEntry() in ioslaves/file/file.cpp) mode = (QT_STAT_MASK - 1) | S_IRWXU | S_IRWXG | S_IRWXO; } } m_entry.replace(KIO::UDSEntry::UDS_SIZE, buf.st_size); m_entry.replace(KIO::UDSEntry::UDS_FILE_TYPE, buf.st_mode & QT_STAT_MASK); // extract file type m_entry.replace(KIO::UDSEntry::UDS_ACCESS, buf.st_mode & 07777); // extract permissions m_entry.replace(KIO::UDSEntry::UDS_MODIFICATION_TIME, buf.st_mtime); // TODO: we could use msecs too... m_entry.replace(KIO::UDSEntry::UDS_ACCESS_TIME, buf.st_atime); #ifndef Q_OS_WIN m_entry.replace(KIO::UDSEntry::UDS_USER, KUser(buf.st_uid).loginName()); m_entry.replace(KIO::UDSEntry::UDS_GROUP, KUserGroup(buf.st_gid).name()); #endif // TODO: these can be removed, we can use UDS_FILE_TYPE and UDS_ACCESS everywhere if (m_fileMode == KFileItem::Unknown) { m_fileMode = mode & QT_STAT_MASK; // extract file type } if (m_permissions == KFileItem::Unknown) { m_permissions = mode & 07777; // extract permissions } } } } m_bInitCalled = true; } void KFileItemPrivate::readUDSEntry(bool _urlIsDirectory) { // extract fields from the KIO::UDS Entry m_fileMode = m_entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE, KFileItem::Unknown); m_permissions = m_entry.numberValue(KIO::UDSEntry::UDS_ACCESS, KFileItem::Unknown); m_strName = m_entry.stringValue(KIO::UDSEntry::UDS_NAME); const QString displayName = m_entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); if (!displayName.isEmpty()) { m_strText = displayName; } else { m_strText = KIO::decodeFileName(m_strName); } const QString urlStr = m_entry.stringValue(KIO::UDSEntry::UDS_URL); const bool UDS_URL_seen = !urlStr.isEmpty(); if (UDS_URL_seen) { m_url = QUrl(urlStr); if (m_url.isLocalFile()) { m_bIsLocalUrl = true; } } QMimeDatabase db; const QString mimeTypeStr = m_entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE); m_bMimeTypeKnown = !mimeTypeStr.isEmpty(); if (m_bMimeTypeKnown) { m_mimeType = db.mimeTypeForName(mimeTypeStr); } m_guessedMimeType = m_entry.stringValue(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE); m_bLink = !m_entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST).isEmpty(); // we don't store the link dest const int hiddenVal = m_entry.numberValue(KIO::UDSEntry::UDS_HIDDEN, -1); m_hidden = hiddenVal == 1 ? Hidden : (hiddenVal == 0 ? Shown : Auto); if (_urlIsDirectory && !UDS_URL_seen && !m_strName.isEmpty() && m_strName != QLatin1String(".")) { m_url.setPath(concatPaths(m_url.path(), m_strName)); } m_iconName.clear(); } inline //because it is used only in one place KIO::filesize_t KFileItemPrivate::size() const { ensureInitialized(); // Extract it from the KIO::UDSEntry long long fieldVal = m_entry.numberValue(KIO::UDSEntry::UDS_SIZE, -1); if (fieldVal != -1) { return fieldVal; } // If not in the KIO::UDSEntry, or if UDSEntry empty, use stat() [if local URL] if (m_bIsLocalUrl) { return QFileInfo(m_url.toLocalFile()).size(); } return 0; } static uint udsFieldForTime(KFileItem::FileTimes mappedWhich) { switch (mappedWhich) { case KFileItem::ModificationTime: return KIO::UDSEntry::UDS_MODIFICATION_TIME; case KFileItem::AccessTime: return KIO::UDSEntry::UDS_ACCESS_TIME; case KFileItem::CreationTime: return KIO::UDSEntry::UDS_CREATION_TIME; } return 0; } void KFileItemPrivate::setTime(KFileItem::FileTimes mappedWhich, uint time_t_val) const { m_entry.replace(udsFieldForTime(mappedWhich), time_t_val); } void KFileItemPrivate::setTime(KFileItem::FileTimes mappedWhich, const QDateTime &val) const { const QDateTime dt = val.toLocalTime(); // #160979 setTime(mappedWhich, dt.toSecsSinceEpoch()); } QDateTime KFileItemPrivate::time(KFileItem::FileTimes mappedWhich) const { ensureInitialized(); // Extract it from the KIO::UDSEntry const uint uds = udsFieldForTime(mappedWhich); if (uds > 0) { const long long fieldVal = m_entry.numberValue(uds, -1); if (fieldVal != -1) { return QDateTime::fromMSecsSinceEpoch(1000 * fieldVal); } } return QDateTime(); } inline //because it is used only in one place bool KFileItemPrivate::cmp(const KFileItemPrivate &item) const { if (item.m_bInitCalled) { ensureInitialized(); } if (m_bInitCalled) { item.ensureInitialized(); } #if 0 //qDebug() << "Comparing" << m_url << "and" << item.m_url; //qDebug() << " name" << (m_strName == item.m_strName); //qDebug() << " local" << (m_bIsLocalUrl == item.m_bIsLocalUrl); //qDebug() << " mode" << (m_fileMode == item.m_fileMode); //qDebug() << " perm" << (m_permissions == item.m_permissions); //qDebug() << " UDS_EXTENDED_ACL" << (m_entry.stringValue( KIO::UDSEntry::UDS_EXTENDED_ACL ) == item.m_entry.stringValue( KIO::UDSEntry::UDS_EXTENDED_ACL )); //qDebug() << " UDS_ACL_STRING" << (m_entry.stringValue( KIO::UDSEntry::UDS_ACL_STRING ) == item.m_entry.stringValue( KIO::UDSEntry::UDS_ACL_STRING )); //qDebug() << " UDS_DEFAULT_ACL_STRING" << (m_entry.stringValue( KIO::UDSEntry::UDS_DEFAULT_ACL_STRING ) == item.m_entry.stringValue( KIO::UDSEntry::UDS_DEFAULT_ACL_STRING )); //qDebug() << " m_bLink" << (m_bLink == item.m_bLink); //qDebug() << " m_hidden" << (m_hidden == item.m_hidden); //qDebug() << " size" << (size() == item.size()); //qDebug() << " ModificationTime" << m_entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME) << item.m_entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME); //qDebug() << " UDS_ICON_NAME" << (m_entry.stringValue( KIO::UDSEntry::UDS_ICON_NAME ) == item.m_entry.stringValue( KIO::UDSEntry::UDS_ICON_NAME )); #endif return (m_strName == item.m_strName && m_bIsLocalUrl == item.m_bIsLocalUrl && m_fileMode == item.m_fileMode && m_permissions == item.m_permissions && m_entry.stringValue(KIO::UDSEntry::UDS_EXTENDED_ACL) == item.m_entry.stringValue(KIO::UDSEntry::UDS_EXTENDED_ACL) && m_entry.stringValue(KIO::UDSEntry::UDS_ACL_STRING) == item.m_entry.stringValue(KIO::UDSEntry::UDS_ACL_STRING) && m_entry.stringValue(KIO::UDSEntry::UDS_DEFAULT_ACL_STRING) == item.m_entry.stringValue(KIO::UDSEntry::UDS_DEFAULT_ACL_STRING) && m_bLink == item.m_bLink && m_hidden == item.m_hidden && size() == item.size() && m_entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME) == item.m_entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME) && m_entry.stringValue(KIO::UDSEntry::UDS_ICON_NAME) == item.m_entry.stringValue(KIO::UDSEntry::UDS_ICON_NAME) && m_entry.stringValue(KIO::UDSEntry::UDS_TARGET_URL) == item.m_entry.stringValue(KIO::UDSEntry::UDS_TARGET_URL) && m_entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) == item.m_entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) ); // Don't compare the mimetypes here. They might not be known, and we don't want to // do the slow operation of determining them here. } inline //because it is used only in one place QString KFileItemPrivate::parsePermissions(mode_t perm) const { ensureInitialized(); static char buffer[ 12 ]; char uxbit, gxbit, oxbit; if ((perm & (S_IXUSR | S_ISUID)) == (S_IXUSR | S_ISUID)) { uxbit = 's'; } else if ((perm & (S_IXUSR | S_ISUID)) == S_ISUID) { uxbit = 'S'; } else if ((perm & (S_IXUSR | S_ISUID)) == S_IXUSR) { uxbit = 'x'; } else { uxbit = '-'; } if ((perm & (S_IXGRP | S_ISGID)) == (S_IXGRP | S_ISGID)) { gxbit = 's'; } else if ((perm & (S_IXGRP | S_ISGID)) == S_ISGID) { gxbit = 'S'; } else if ((perm & (S_IXGRP | S_ISGID)) == S_IXGRP) { gxbit = 'x'; } else { gxbit = '-'; } if ((perm & (S_IXOTH | S_ISVTX)) == (S_IXOTH | S_ISVTX)) { oxbit = 't'; } else if ((perm & (S_IXOTH | S_ISVTX)) == S_ISVTX) { oxbit = 'T'; } else if ((perm & (S_IXOTH | S_ISVTX)) == S_IXOTH) { oxbit = 'x'; } else { oxbit = '-'; } // Include the type in the first char like ls does; people are more used to seeing it, // even though it's not really part of the permissions per se. if (m_bLink) { buffer[0] = 'l'; } else if (m_fileMode != KFileItem::Unknown) { if ((m_fileMode & QT_STAT_MASK) == QT_STAT_DIR) { buffer[0] = 'd'; } #ifdef Q_OS_UNIX else if (S_ISSOCK(m_fileMode)) { buffer[0] = 's'; } else if (S_ISCHR(m_fileMode)) { buffer[0] = 'c'; } else if (S_ISBLK(m_fileMode)) { buffer[0] = 'b'; } else if (S_ISFIFO(m_fileMode)) { buffer[0] = 'p'; } #endif // Q_OS_UNIX else { buffer[0] = '-'; } } else { buffer[0] = '-'; } buffer[1] = (((perm & S_IRUSR) == S_IRUSR) ? 'r' : '-'); buffer[2] = (((perm & S_IWUSR) == S_IWUSR) ? 'w' : '-'); buffer[3] = uxbit; buffer[4] = (((perm & S_IRGRP) == S_IRGRP) ? 'r' : '-'); buffer[5] = (((perm & S_IWGRP) == S_IWGRP) ? 'w' : '-'); buffer[6] = gxbit; buffer[7] = (((perm & S_IROTH) == S_IROTH) ? 'r' : '-'); buffer[8] = (((perm & S_IWOTH) == S_IWOTH) ? 'w' : '-'); buffer[9] = oxbit; // if (hasExtendedACL()) if (m_entry.contains(KIO::UDSEntry::UDS_EXTENDED_ACL)) { buffer[10] = '+'; buffer[11] = 0; } else { buffer[10] = 0; } return QString::fromLatin1(buffer); } void KFileItemPrivate::determineMimeTypeHelper(const QUrl &url) const { QMimeDatabase db; if (m_bSkipMimeTypeFromContent) { const QString scheme = url.scheme(); if (scheme.startsWith(QLatin1String("http")) || scheme == QLatin1String("mailto")) m_mimeType = db.mimeTypeForName(QLatin1String("application/octet-stream")); else m_mimeType = db.mimeTypeForFile(url.path(), QMimeDatabase::MatchMode::MatchExtension); } else { m_mimeType = db.mimeTypeForUrl(url); } } /////// KFileItem::KFileItem() : d(nullptr) { } KFileItem::KFileItem(const KIO::UDSEntry &entry, const QUrl &itemOrDirUrl, bool delayedMimeTypes, bool urlIsDirectory) : d(new KFileItemPrivate(entry, KFileItem::Unknown, KFileItem::Unknown, itemOrDirUrl, urlIsDirectory, delayedMimeTypes, KFileItem::NormalMimeTypeDetermination)) { } KFileItem::KFileItem(mode_t mode, mode_t permissions, const QUrl &url, bool delayedMimeTypes) : d(new KFileItemPrivate(KIO::UDSEntry(), mode, permissions, url, false, delayedMimeTypes, KFileItem::NormalMimeTypeDetermination)) { } KFileItem::KFileItem(const QUrl &url, const QString &mimeType, mode_t mode) : d(new KFileItemPrivate(KIO::UDSEntry(), mode, KFileItem::Unknown, url, false, false, KFileItem::NormalMimeTypeDetermination)) { d->m_bMimeTypeKnown = !mimeType.isEmpty(); if (d->m_bMimeTypeKnown) { QMimeDatabase db; d->m_mimeType = db.mimeTypeForName(mimeType); } } KFileItem::KFileItem(const QUrl &url, KFileItem::MimeTypeDetermination mimeTypeDetermination) : d(new KFileItemPrivate(KIO::UDSEntry(), KFileItem::Unknown, KFileItem::Unknown, url, false, false, mimeTypeDetermination)) { } // Default implementations for: // - Copy constructor // - Move constructor // - Copy assignment // - Move assignment // - Destructor // The compiler will now generate the content of those. KFileItem::KFileItem(const KFileItem&) = default; KFileItem::~KFileItem() = default; KFileItem::KFileItem(KFileItem&&) = default; KFileItem& KFileItem::operator=(const KFileItem&) = default; KFileItem& KFileItem::operator=(KFileItem&&) = default; void KFileItem::refresh() { if (!d) { qCWarning(KIO_CORE) << "null item"; return; } d->m_fileMode = KFileItem::Unknown; d->m_permissions = KFileItem::Unknown; d->m_hidden = KFileItemPrivate::Auto; refreshMimeType(); // Basically, we can't trust any information we got while listing. // Everything could have changed... // Clearing m_entry makes it possible to detect changes in the size of the file, // the time information, etc. d->m_entry.clear(); d->init(); // re-populates d->m_entry } void KFileItem::refreshMimeType() { if (!d) { return; } d->m_mimeType = QMimeType(); d->m_bMimeTypeKnown = false; d->m_iconName.clear(); } void KFileItem::setDelayedMimeTypes(bool b) { if (!d) { return; } d->m_delayedMimeTypes = b; } void KFileItem::setUrl(const QUrl &url) { if (!d) { qCWarning(KIO_CORE) << "null item"; return; } d->m_url = url; setName(url.fileName()); } void KFileItem::setLocalPath(const QString &path) { if (!d) { qCWarning(KIO_CORE) << "null item"; return; } d->m_entry.replace(KIO::UDSEntry::UDS_LOCAL_PATH, path); } void KFileItem::setName(const QString &name) { if (!d) { qCWarning(KIO_CORE) << "null item"; return; } d->ensureInitialized(); d->m_strName = name; if (!d->m_strName.isEmpty()) { d->m_strText = KIO::decodeFileName(d->m_strName); } if (d->m_entry.contains(KIO::UDSEntry::UDS_NAME)) { d->m_entry.replace(KIO::UDSEntry::UDS_NAME, d->m_strName); // #195385 } } QString KFileItem::linkDest() const { if (!d) { return QString(); } d->ensureInitialized(); // Extract it from the KIO::UDSEntry const QString linkStr = d->m_entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); if (!linkStr.isEmpty()) { return linkStr; } // If not in the KIO::UDSEntry, or if UDSEntry empty, use readlink() [if local URL] if (d->m_bIsLocalUrl) { return QFile::symLinkTarget(d->m_url.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } return QString(); } QString KFileItemPrivate::localPath() const { if (m_bIsLocalUrl) { return m_url.toLocalFile(); } ensureInitialized(); // Extract the local path from the KIO::UDSEntry return m_entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); } QString KFileItem::localPath() const { if (!d) { return QString(); } return d->localPath(); } KIO::filesize_t KFileItem::size() const { if (!d) { return 0; } return d->size(); } bool KFileItem::hasExtendedACL() const { if (!d) { return false; } // Check if the field exists; its value doesn't matter return entry().contains(KIO::UDSEntry::UDS_EXTENDED_ACL); } KACL KFileItem::ACL() const { if (!d) { return KACL(); } if (hasExtendedACL()) { // Extract it from the KIO::UDSEntry const QString fieldVal = d->m_entry.stringValue(KIO::UDSEntry::UDS_ACL_STRING); if (!fieldVal.isEmpty()) { return KACL(fieldVal); } } // create one from the basic permissions return KACL(d->m_permissions); } KACL KFileItem::defaultACL() const { if (!d) { return KACL(); } // Extract it from the KIO::UDSEntry const QString fieldVal = entry().stringValue(KIO::UDSEntry::UDS_DEFAULT_ACL_STRING); if (!fieldVal.isEmpty()) { return KACL(fieldVal); } else { return KACL(); } } QDateTime KFileItem::time(FileTimes which) const { if (!d) { return QDateTime(); } return d->time(which); } QString KFileItem::user() const { if (!d) { return QString(); } return entry().stringValue(KIO::UDSEntry::UDS_USER); } QString KFileItem::group() const { if (!d) { return QString(); } return entry().stringValue(KIO::UDSEntry::UDS_GROUP); } bool KFileItemPrivate::isSlow() const { if (m_slow == SlowUnknown) { const QString path = localPath(); if (!path.isEmpty()) { const KFileSystemType::Type fsType = KFileSystemType::fileSystemType(path); m_slow = (fsType == KFileSystemType::Nfs || fsType == KFileSystemType::Smb) ? Slow : Fast; } else { m_slow = Slow; } } return m_slow == Slow; } bool KFileItem::isSlow() const { if (!d) { return false; } return d->isSlow(); } QString KFileItem::mimetype() const { if (!d) { return QString(); } KFileItem *that = const_cast(this); return that->determineMimeType().name(); } QMimeType KFileItem::determineMimeType() const { if (!d) { return QMimeType(); } if (!d->m_mimeType.isValid() || !d->m_bMimeTypeKnown) { QMimeDatabase db; if (isDir()) { d->m_mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); } else { bool isLocalUrl; const QUrl url = mostLocalUrl(&isLocalUrl); d->determineMimeTypeHelper(url); // was: d->m_mimeType = KMimeType::findByUrl( url, d->m_fileMode, isLocalUrl ); // => we are no longer using d->m_fileMode for remote URLs. Q_ASSERT(d->m_mimeType.isValid()); //qDebug() << d << "finding final mimetype for" << url << ":" << d->m_mimeType.name(); } d->m_bMimeTypeKnown = true; } if (d->m_delayedMimeTypes) { // if we delayed getting the iconName up till now, this is the right point in time to do so d->m_delayedMimeTypes = false; d->m_useIconNameCache = false; (void)iconName(); } return d->m_mimeType; } bool KFileItem::isMimeTypeKnown() const { if (!d) { return false; } // The mimetype isn't known if determineMimeType was never called (on-demand determination) // or if this fileitem has a guessed mimetype (e.g. ftp symlink) - in which case // it always remains "not fully determined" return d->m_bMimeTypeKnown && d->m_guessedMimeType.isEmpty(); } static bool isDirectoryMounted(const QUrl &url) { // Stating .directory files can cause long freezes when e.g. /home // uses autofs for every user's home directory, i.e. opening /home // in a file dialog will mount every single home directory. // These non-mounted directories can be identified by having 0 size. // There are also other directories with 0 size, such as /proc, that may // be mounted, but those are unlikely to contain .directory (and checking // this would require checking with KMountPoint). // TODO: maybe this could be checked with KFileSystemType instead? QFileInfo info(url.toLocalFile()); if (info.isDir() && info.size() == 0) { return false; } return true; } bool KFileItem::isFinalIconKnown() const { if (!d) { return false; } return d->m_bMimeTypeKnown && (!d->m_delayedMimeTypes); } // KDE5 TODO: merge with comment()? Need to see what lxr says about the usage of both. QString KFileItem::mimeComment() const { if (!d) { return QString(); } const QString displayType = d->m_entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_TYPE); if (!displayType.isEmpty()) { return displayType; } bool isLocalUrl; QUrl url = mostLocalUrl(&isLocalUrl); QMimeType mime = currentMimeType(); // This cannot move to kio_file (with UDS_DISPLAY_TYPE) because it needs // the mimetype to be determined, which is done here, and possibly delayed... if (isLocalUrl && !d->isSlow() && mime.inherits(QStringLiteral("application/x-desktop"))) { KDesktopFile cfg(url.toLocalFile()); QString comment = cfg.desktopGroup().readEntry("Comment"); if (!comment.isEmpty()) { return comment; } } // Support for .directory file in directories if (isLocalUrl && isDir() && !d->isSlow() && isDirectoryMounted(url)) { QUrl u(url); u.setPath(concatPaths(u.path(), QStringLiteral(".directory"))); const KDesktopFile cfg(u.toLocalFile()); const QString comment = cfg.readComment(); if (!comment.isEmpty()) { return comment; } } const QString comment = mime.comment(); //qDebug() << "finding comment for " << url.url() << " : " << d->m_mimeType->name(); if (!comment.isEmpty()) { return comment; } else { return mime.name(); } } static QString iconFromDirectoryFile(const QString &path) { const QString filePath = path + QLatin1String("/.directory"); if (!QFileInfo(filePath).isFile()) { // exists -and- is a file return QString(); } KDesktopFile cfg(filePath); QString icon = cfg.readIcon(); const KConfigGroup group = cfg.desktopGroup(); const QString emptyIcon = group.readEntry("EmptyIcon"); if (!emptyIcon.isEmpty()) { bool isDirEmpty = true; QDirIterator dirIt(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); while (dirIt.hasNext()) { dirIt.next(); if (dirIt.fileName() != QLatin1String(".directory")) { isDirEmpty = false; break; } } if (isDirEmpty) { icon = emptyIcon; } } if (icon.startsWith(QLatin1String("./"))) { // path is relative with respect to the location // of the .directory file (#73463) return path + icon.midRef(1); } return icon; } static QString iconFromDesktopFile(const QString &path) { KDesktopFile cfg(path); const QString icon = cfg.readIcon(); if (cfg.hasLinkType()) { const KConfigGroup group = cfg.desktopGroup(); const QString emptyIcon = group.readEntry("EmptyIcon"); if (!emptyIcon.isEmpty()) { const QString u = cfg.readUrl(); const QUrl url(u); if (url.scheme() == QLatin1String("trash")) { // We need to find if the trash is empty, preferably without using a KIO job. // So instead kio_trash leaves an entry in its config file for us. KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig); if (trashConfig.group("Status").readEntry("Empty", true)) { return emptyIcon; } } } } return icon; } QString KFileItem::iconName() const { if (!d) { return QString(); } if (d->m_useIconNameCache && !d->m_iconName.isEmpty()) { return d->m_iconName; } d->m_iconName = d->m_entry.stringValue(KIO::UDSEntry::UDS_ICON_NAME); if (!d->m_iconName.isEmpty()) { d->m_useIconNameCache = d->m_bMimeTypeKnown; return d->m_iconName; } bool isLocalUrl; QUrl url = mostLocalUrl(&isLocalUrl); QMimeDatabase db; QMimeType mime; // Use guessed mimetype for the icon if (!d->m_guessedMimeType.isEmpty()) { mime = db.mimeTypeForName(d->m_guessedMimeType); } else { mime = currentMimeType(); } const bool delaySlowOperations = d->m_delayedMimeTypes; if (isLocalUrl && !delaySlowOperations && mime.inherits(QStringLiteral("application/x-desktop"))) { d->m_iconName = iconFromDesktopFile(url.toLocalFile()); if (!d->m_iconName.isEmpty()) { d->m_useIconNameCache = d->m_bMimeTypeKnown; return d->m_iconName; } } if (isLocalUrl && !delaySlowOperations && isDir()) { if (isDirectoryMounted(url)) { d->m_iconName = iconFromDirectoryFile(url.toLocalFile()); if (!d->m_iconName.isEmpty()) { d->m_useIconNameCache = d->m_bMimeTypeKnown; return d->m_iconName; } } d->m_iconName = KIOPrivate::iconForStandardPath(url.toLocalFile()); if (!d->m_iconName.isEmpty()) { d->m_useIconNameCache = d->m_bMimeTypeKnown; return d->m_iconName; } } d->m_iconName = mime.iconName(); d->m_useIconNameCache = d->m_bMimeTypeKnown; return d->m_iconName; } /** * Returns true if this is a desktop file. * Mimetype determination is optional. */ static bool checkDesktopFile(const KFileItem &item, bool _determineMimeType) { // only local files bool isLocalUrl; item.mostLocalUrl(&isLocalUrl); if (!isLocalUrl) { return false; } // only regular files if (!item.isRegularFile()) { return false; } // only if readable if (!item.isReadable()) { return false; } // return true if desktop file QMimeType mime = _determineMimeType ? item.determineMimeType() : item.currentMimeType(); return mime.inherits(QStringLiteral("application/x-desktop")); } QStringList KFileItem::overlays() const { if (!d) { return QStringList(); } d->ensureInitialized(); QStringList names = d->m_entry.stringValue(KIO::UDSEntry::UDS_ICON_OVERLAY_NAMES).split(QLatin1Char(','), QString::SkipEmptyParts); if (d->m_bLink) { names.append(QStringLiteral("emblem-symbolic-link")); } if (!isReadable()) { names.append(QStringLiteral("emblem-locked")); } if (checkDesktopFile(*this, false)) { KDesktopFile cfg(localPath()); const KConfigGroup group = cfg.desktopGroup(); // Add a warning emblem if this is an executable desktop file // which is untrusted. if (group.hasKey("Exec") && !KDesktopFile::isAuthorizedDesktopFile(localPath())) { names.append(QStringLiteral("emblem-important")); } if (cfg.hasDeviceType()) { const QString dev = cfg.readDevice(); if (!dev.isEmpty()) { KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByDevice(dev); if (mountPoint) { // mounted? names.append(QStringLiteral("emblem-mounted")); } } } } if (isHidden()) { names.append(QStringLiteral("hidden")); } #ifndef Q_OS_WIN if (isDir()) { bool isLocalUrl; const QUrl url = mostLocalUrl(&isLocalUrl); if (isLocalUrl) { const QString path = url.toLocalFile(); if (KSambaShare::instance()->isDirectoryShared(path) || KNFSShare::instance()->isDirectoryShared(path)) { names.append(QStringLiteral("emblem-shared")); } } } #endif // Q_OS_WIN return names; } QString KFileItem::comment() const { if (!d) { return QString(); } return d->m_entry.stringValue(KIO::UDSEntry::UDS_COMMENT); } bool KFileItem::isReadable() const { if (!d) { return false; } d->ensureInitialized(); /* struct passwd * user = getpwuid( geteuid() ); bool isMyFile = (QString::fromLocal8Bit(user->pw_name) == d->m_user); // This gets ugly for the group.... // Maybe we want a static QString for the user and a static QStringList // for the groups... then we need to handle the deletion properly... */ if (d->m_permissions != KFileItem::Unknown) { const mode_t readMask = S_IRUSR | S_IRGRP | S_IROTH; // No read permission at all if ((d->m_permissions & readMask) == 0) { return false; } // Read permissions for all: save a stat call if ((d->m_permissions & readMask) == readMask) { return true; } } // Or if we can't read it - not network transparent if (d->m_bIsLocalUrl && !QFileInfo(d->m_url.toLocalFile()).isReadable()) { return false; } return true; } bool KFileItem::isWritable() const { if (!d) { return false; } d->ensureInitialized(); /* struct passwd * user = getpwuid( geteuid() ); bool isMyFile = (QString::fromLocal8Bit(user->pw_name) == d->m_user); // This gets ugly for the group.... // Maybe we want a static QString for the user and a static QStringList // for the groups... then we need to handle the deletion properly... */ if (d->m_permissions != KFileItem::Unknown) { // No write permission at all if (!(S_IWUSR & d->m_permissions) && !(S_IWGRP & d->m_permissions) && !(S_IWOTH & d->m_permissions)) { return false; } } // Or if we can't write it - not network transparent if (d->m_bIsLocalUrl && !QFileInfo(d->m_url.toLocalFile()).isWritable()) { return false; } return true; } bool KFileItem::isHidden() const { if (!d) { return false; } // The kioslave can specify explicitly that a file is hidden or shown if (d->m_hidden != KFileItemPrivate::Auto) { return d->m_hidden == KFileItemPrivate::Hidden; } // Prefer the filename that is part of the URL, in case the display name is different. QString fileName = d->m_url.fileName(); if (fileName.isEmpty()) { // e.g. "trash:/" fileName = d->m_strName; } return fileName.length() > 1 && fileName[0] == QLatin1Char('.'); // Just "." is current directory, not hidden. } void KFileItem::setHidden() { if (d) { d->m_hidden = KFileItemPrivate::Hidden; } } bool KFileItem::isDir() const { if (!d) { return false; } if (d->m_bSkipMimeTypeFromContent) { return false; } d->ensureInitialized(); if (d->m_fileMode == KFileItem::Unknown) { // Probably the file was deleted already, and KDirLister hasn't told the world yet. //qDebug() << d << url() << "can't say -> false"; return false; // can't say for sure, so no } return (d->m_fileMode & QT_STAT_MASK) == QT_STAT_DIR; } bool KFileItem::isFile() const { if (!d) { return false; } return !isDir(); } #ifndef KIOCORE_NO_DEPRECATED bool KFileItem::acceptsDrops() const { // A directory ? if (isDir()) { return isWritable(); } // But only local .desktop files and executables if (!d->m_bIsLocalUrl) { return false; } if (mimetype() == QLatin1String("application/x-desktop")) { return true; } // Executable, shell script ... ? if (QFileInfo(d->m_url.toLocalFile()).isExecutable()) { return true; } return false; } #endif QString KFileItem::getStatusBarInfo() const { if (!d) { return QString(); } QString text = d->m_strText; const QString comment = mimeComment(); if (d->m_bLink) { text += QLatin1Char(' '); if (comment.isEmpty()) { text += i18n("(Symbolic Link to %1)", linkDest()); } else { text += i18n("(%1, Link to %2)", comment, linkDest()); } } else if (targetUrl() != url()) { text += i18n(" (Points to %1)", targetUrl().toDisplayString()); } else if ((d->m_fileMode & QT_STAT_MASK) == QT_STAT_REG) { text += QStringLiteral(" (%1, %2)").arg(comment, KIO::convertSize(size())); } else { text += QStringLiteral(" (%1)").arg(comment); } return text; } bool KFileItem::cmp(const KFileItem &item) const { if (!d && !item.d) { return true; } if (!d || !item.d) { return false; } return d->cmp(*item.d); } bool KFileItem::operator==(const KFileItem &other) const { if (!d && !other.d) { return true; } if (!d || !other.d) { return false; } return d->m_url == other.d->m_url; } bool KFileItem::operator!=(const KFileItem &other) const { return !operator==(other); } bool KFileItem::operator<(const KFileItem &other) const { if (!other.d) { return false; } if (!d) { return other.d->m_url.isValid(); } return d->m_url < other.d->m_url; } bool KFileItem::operator<(const QUrl &other) const { if (!d) { return other.isValid(); } return d->m_url < other; } KFileItem::operator QVariant() const { return QVariant::fromValue(*this); } QString KFileItem::permissionsString() const { if (!d) { return QString(); } d->ensureInitialized(); if (d->m_access.isNull() && d->m_permissions != KFileItem::Unknown) { d->m_access = d->parsePermissions(d->m_permissions); } return d->m_access; } // check if we need to cache this QString KFileItem::timeString(FileTimes which) const { if (!d) { return QString(); } return d->time(which).toString(); } #ifndef KIOCORE_NO_DEPRECATED QString KFileItem::timeString(unsigned int which) const { if (!d) { return QString(); } switch (which) { case KIO::UDSEntry::UDS_ACCESS_TIME: return timeString(AccessTime); case KIO::UDSEntry::UDS_CREATION_TIME: return timeString(CreationTime); case KIO::UDSEntry::UDS_MODIFICATION_TIME: default: return timeString(ModificationTime); } } #endif #ifndef KIOCORE_NO_DEPRECATED void KFileItem::assign(const KFileItem &item) { *this = item; } #endif QUrl KFileItem::mostLocalUrl(bool *local) const { if (!d) { return QUrl(); } const QString local_path = localPath(); if (!local_path.isEmpty()) { if (local) { *local = true; } return QUrl::fromLocalFile(local_path); } else { if (local) { *local = d->m_bIsLocalUrl; } return d->m_url; } } QDataStream &operator<< (QDataStream &s, const KFileItem &a) { if (a.d) { // We don't need to save/restore anything that refresh() invalidates, // since that means we can re-determine those by ourselves. s << a.d->m_url; s << a.d->m_strName; s << a.d->m_strText; } else { s << QUrl(); s << QString(); s << QString(); } return s; } QDataStream &operator>> (QDataStream &s, KFileItem &a) { QUrl url; QString strName, strText; s >> url; s >> strName; s >> strText; if (!a.d) { qCWarning(KIO_CORE) << "null item"; return s; } if (url.isEmpty()) { a.d = nullptr; return s; } a.d->m_url = url; a.d->m_strName = strName; a.d->m_strText = strText; a.d->m_bIsLocalUrl = a.d->m_url.isLocalFile(); a.d->m_bMimeTypeKnown = false; a.refresh(); return s; } QUrl KFileItem::url() const { if (!d) { return QUrl(); } return d->m_url; } mode_t KFileItem::permissions() const { if (!d) { return 0; } d->ensureInitialized(); return d->m_permissions; } mode_t KFileItem::mode() const { if (!d) { return 0; } d->ensureInitialized(); return d->m_fileMode; } bool KFileItem::isLink() const { if (!d) { return false; } d->ensureInitialized(); return d->m_bLink; } bool KFileItem::isLocalFile() const { if (!d) { return false; } return d->m_bIsLocalUrl; } QString KFileItem::text() const { if (!d) { return QString(); } return d->m_strText; } QString KFileItem::name(bool lowerCase) const { if (!d) { return QString(); } if (!lowerCase) { return d->m_strName; } else if (d->m_strLowerCaseName.isNull()) { d->m_strLowerCaseName = d->m_strName.toLower(); } return d->m_strLowerCaseName; } QUrl KFileItem::targetUrl() const { if (!d) { return QUrl(); } const QString targetUrlStr = d->m_entry.stringValue(KIO::UDSEntry::UDS_TARGET_URL); if (!targetUrlStr.isEmpty()) { return QUrl(targetUrlStr); } else { return url(); } } /* * Mimetype handling. * * Initial state: m_mimeType = QMimeType(). * When currentMimeType() is called first: fast mimetype determination, * might either find an accurate mimetype (-> Final state), otherwise we * set m_mimeType but not m_bMimeTypeKnown (-> Intermediate state) * Intermediate state: determineMimeType() does the real determination -> Final state. * * If delayedMimeTypes isn't set, then we always go to the Final state directly. */ QMimeType KFileItem::currentMimeType() const { if (!d) { return QMimeType(); } if (!d->m_mimeType.isValid()) { // On-demand fast (but not always accurate) mimetype determination Q_ASSERT(!d->m_url.isEmpty()); QMimeDatabase db; if (isDir()) { d->m_mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); return d->m_mimeType; } const QUrl url = mostLocalUrl(); if (d->m_delayedMimeTypes) { const QList mimeTypes = db.mimeTypesForFileName(url.path()); if (mimeTypes.isEmpty()) { d->m_mimeType = db.mimeTypeForName(QStringLiteral("application/octet-stream")); d->m_bMimeTypeKnown = false; } else { d->m_mimeType = mimeTypes.first(); // If there were conflicting globs. determineMimeType will be able to do better. d->m_bMimeTypeKnown = (mimeTypes.count() == 1); } } else { // ## d->m_fileMode isn't used anymore (for remote urls) d->determineMimeTypeHelper(url); d->m_bMimeTypeKnown = true; } } return d->m_mimeType; } KIO::UDSEntry KFileItem::entry() const { if (!d) { return KIO::UDSEntry(); } d->ensureInitialized(); return d->m_entry; } bool KFileItem::isNull() const { return d == nullptr; } KFileItemList::KFileItemList() { } KFileItemList::KFileItemList(const QList &items) : QList(items) { } KFileItem KFileItemList::findByName(const QString &fileName) const { const_iterator it = begin(); const const_iterator itend = end(); for (; it != itend; ++it) { if ((*it).name() == fileName) { return *it; } } return KFileItem(); } KFileItem KFileItemList::findByUrl(const QUrl &url) const { const_iterator it = begin(); const const_iterator itend = end(); for (; it != itend; ++it) { if ((*it).url() == url) { return *it; } } return KFileItem(); } QList KFileItemList::urlList() const { QList lst; const_iterator it = begin(); const const_iterator itend = end(); for (; it != itend; ++it) { lst.append((*it).url()); } return lst; } QList KFileItemList::targetUrlList() const { QList lst; const_iterator it = begin(); const const_iterator itend = end(); for (; it != itend; ++it) { lst.append((*it).targetUrl()); } return lst; } bool KFileItem::isDesktopFile() const { return checkDesktopFile(*this, true); } bool KFileItem::isRegularFile() const { if (!d) { return false; } d->ensureInitialized(); return (d->m_fileMode & QT_STAT_MASK) == QT_STAT_REG; } QDebug operator<<(QDebug stream, const KFileItem &item) { QDebugStateSaver saver(stream); stream.nospace(); if (item.isNull()) { stream << "[null KFileItem]"; } else { stream << "[KFileItem for " << item.url() << "]"; } return stream; } diff --git a/src/core/kioglobal_p.h b/src/core/kioglobal_p.h index 543cb6ce..b93caed7 100644 --- a/src/core/kioglobal_p.h +++ b/src/core/kioglobal_p.h @@ -1,133 +1,132 @@ /* This file is part of the KDE libraries Copyright (C) 2014 Alex Richardson 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. */ #ifndef KIO_KIOGLOBAL_P_H #define KIO_KIOGLOBAL_P_H -#include #include #include "kiocore_export.h" #include #ifdef Q_OS_WIN // windows just sets the mode_t access rights bits to the same value for user+group+other. // This means using the Linux values here is fine. #ifndef S_IRUSR #define S_IRUSR 0400 #endif #ifndef S_IRGRP #define S_IRGRP 0040 #endif #ifndef S_IROTH #define S_IROTH 0004 #endif #ifndef S_IWUSR #define S_IWUSR 0200 #endif #ifndef S_IWGRP #define S_IWGRP 0020 #endif #ifndef S_IWOTH #define S_IWOTH 0002 #endif #ifndef S_IXUSR #define S_IXUSR 0100 #endif #ifndef S_IXGRP #define S_IXGRP 0010 #endif #ifndef S_IXOTH #define S_IXOTH 0001 #endif #ifndef S_IRWXU #define S_IRWXU S_IRUSR | S_IWUSR | S_IXUSR #endif #ifndef S_IRWXG #define S_IRWXG S_IRGRP | S_IWGRP | S_IXGRP #endif #ifndef S_IRWXO #define S_IRWXO S_IROTH | S_IWOTH | S_IXOTH #endif Q_STATIC_ASSERT(S_IRUSR == _S_IREAD && S_IWUSR == _S_IWRITE && S_IXUSR == _S_IEXEC); // these three will never be set in st_mode #ifndef S_ISUID #define S_ISUID 04000 // SUID bit does not exist on windows #endif #ifndef S_ISGID #define S_ISGID 02000 // SGID bit does not exist on windows #endif #ifndef S_ISVTX #define S_ISVTX 01000 // sticky bit does not exist on windows #endif // Windows does not have S_IFBLK and S_IFSOCK, just use the Linux values, they won't conflict #ifndef S_IFBLK #define S_IFBLK 0060000 #endif #ifndef S_IFSOCK #define S_IFSOCK 0140000 #endif /** performs a QT_STAT and add QT_STAT_LNK to st_mode if the path is a symlink */ KIOCORE_EXPORT int kio_windows_lstat(const char* path, QT_STATBUF* buffer); #ifndef QT_LSTAT #define QT_LSTAT kio_windows_lstat #endif #ifndef QT_STAT_LNK # define QT_STAT_LNK 0120000 #endif // QT_STAT_LNK #endif //Q_OS_WIN namespace KIOPrivate { /** @return true if the process with given PID is currently running */ KIOCORE_EXPORT bool isProcessAlive(qint64 pid); /** Send a terminate signal (SIGTERM on UNIX) to the process with given PID. */ KIOCORE_EXPORT void sendTerminateSignal(qint64 pid); enum SymlinkType { GuessSymlinkType, FileSymlink, DirectorySymlink }; /** Creates a symbolic link at @p destination pointing to @p source * Unlink UNIX, Windows needs to know whether the symlink points to a file or a directory * when creating the link. This information can be passed in @p type. If @p type is not given * the windows code will guess the type based on the source file. * @note On Windows this requires the current user to have the SeCreateSymbolicLink privilege which * is usually only given to administrators. * @return true on success, false on error */ KIOCORE_EXPORT bool createSymlink(const QString &source, const QString &destination, SymlinkType type = GuessSymlinkType); /** Changes the ownership of @p file (like chown()) */ KIOCORE_EXPORT bool changeOwnership(const QString& file, KUserId newOwner, KGroupId newGroup); /** Returns an icon name for a standard path, * e.g. folder-pictures for any path in QStandardPaths::PicturesLocation */ QString iconForStandardPath(const QString &localDirectory); } #endif // KIO_KIOGLOBAL_P_H diff --git a/src/core/knfsshare.cpp b/src/core/knfsshare.cpp index 166dbe3d..41886e30 100644 --- a/src/core/knfsshare.cpp +++ b/src/core/knfsshare.cpp @@ -1,233 +1,232 @@ /* This file is part of the KDE project Copyright (c) 2004 Jan Schaefer 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 "knfsshare.h" #include #include #include -#include #include #include #include #include #include #include "kiocoredebug.h" class Q_DECL_HIDDEN KNFSShare::KNFSSharePrivate { public: explicit KNFSSharePrivate(KNFSShare *parent); void _k_slotFileChange(const QString &); bool readExportsFile(); bool findExportsFile(); KNFSShare * const q; QSet sharedPaths; QString exportsFile; }; KNFSShare::KNFSSharePrivate::KNFSSharePrivate(KNFSShare *parent) : q(parent) { if (findExportsFile()) { readExportsFile(); } } /** * Try to find the nfs config file path * First tries the kconfig, then checks * several well-known paths * @return whether an 'exports' file was found. **/ bool KNFSShare::KNFSSharePrivate::findExportsFile() { KConfig knfsshare(QStringLiteral("knfsshare")); KConfigGroup config(&knfsshare, "General"); exportsFile = config.readPathEntry("exportsFile", QString()); if (!exportsFile.isEmpty() && QFileInfo::exists(exportsFile)) { return true; } if (QFile::exists(QStringLiteral("/etc/exports"))) { exportsFile = QStringLiteral("/etc/exports"); } else { //qDebug() << "Could not find exports file! /etc/exports doesn't exist. Configure it in share/config/knfsshare, [General], exportsFile=...."; return false; } config.writeEntry("exportsFile", exportsFile); return true; } /** * Reads all paths from the exports file * and fills the sharedPaths dict with the values */ bool KNFSShare::KNFSSharePrivate::readExportsFile() { QFile f(exportsFile); //qDebug() << exportsFile; if (!f.open(QIODevice::ReadOnly)) { qCWarning(KIO_CORE) << "KNFSShare: Could not open" << exportsFile; return false; } sharedPaths.clear(); QTextStream s(&f); bool continuedLine = false; // is true if the line before ended with a backslash QString completeLine; while (!s.atEnd()) { QString currentLine = s.readLine().trimmed(); if (continuedLine) { completeLine += currentLine; continuedLine = false; } else { completeLine = currentLine; } // is the line continued in the next line ? if (completeLine.endsWith(QLatin1Char('\\'))) { continuedLine = true; // remove the ending backslash completeLine.chop(1); continue; } // comments or empty lines if (completeLine.startsWith(QLatin1Char('#')) || completeLine.isEmpty()) { continue; } QString path; // Handle quotation marks if (completeLine[0] == QLatin1Char('\"')) { int i = completeLine.indexOf(QLatin1Char('"'), 1); if (i == -1) { qCWarning(KIO_CORE) << "KNFSShare: Parse error: Missing quotation mark:" << completeLine; continue; } path = completeLine.mid(1, i - 1); } else { // no quotation marks int i = completeLine.indexOf(QLatin1Char(' ')); if (i == -1) { i = completeLine.indexOf(QLatin1Char('\t')); } if (i == -1) { path = completeLine; } else { path = completeLine.left(i); } } //qDebug() << "KNFSShare: Found path: " << path; if (!path.isEmpty()) { // normalize path if (!path.endsWith(QLatin1Char('/'))) { path += QLatin1Char('/'); } sharedPaths.insert(path); } } return true; } KNFSShare::KNFSShare() : d(new KNFSSharePrivate(this)) { if (!d->exportsFile.isEmpty() && QFileInfo::exists(d->exportsFile)) { KDirWatch::self()->addFile(d->exportsFile); connect(KDirWatch::self(), SIGNAL(dirty(QString)), this, SLOT(_k_slotFileChange(QString))); } } KNFSShare::~KNFSShare() { // This is not needed, we're exiting the process anyway, and KDirWatch is already deleted. //if (QFile::exists(d->exportsFile)) { // KDirWatch::self()->removeFile(d->exportsFile); //} delete d; } bool KNFSShare::isDirectoryShared(const QString &path) const { if (path.isEmpty()) { return false; } QString fixedPath = path; if (!fixedPath.endsWith(QLatin1Char('/'))) { fixedPath += QLatin1Char('/'); } return d->sharedPaths.contains(fixedPath); } QStringList KNFSShare::sharedDirectories() const { return d->sharedPaths.values(); } QString KNFSShare::exportsPath() const { return d->exportsFile; } void KNFSShare::KNFSSharePrivate::_k_slotFileChange(const QString &path) { if (path == exportsFile) { readExportsFile(); } emit q->changed(); } class KNFSShareSingleton { public: KNFSShare instance; }; Q_GLOBAL_STATIC(KNFSShareSingleton, _instance) KNFSShare *KNFSShare::instance() { return &_instance()->instance; } #include "moc_knfsshare.cpp" diff --git a/src/core/kpasswdserverclient.cpp b/src/core/kpasswdserverclient.cpp index 4dbc4c9a..28ad1fc8 100644 --- a/src/core/kpasswdserverclient.cpp +++ b/src/core/kpasswdserverclient.cpp @@ -1,154 +1,152 @@ /* * This file is part of the KDE libraries * Copyright (c) 2009 Michael Leupold * * 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.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * 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, see . */ #include "kpasswdserverclient.h" #include "kiocoredebug.h" #include #include -#include -#include #include "kpasswdserverloop_p.h" #include "kpasswdserver_interface.h" class KPasswdServerClientPrivate { public: KPasswdServerClientPrivate() : seqNr(0) {} qlonglong seqNr; QString lastHost; }; KPasswdServerClient::KPasswdServerClient() : m_interface(new OrgKdeKPasswdServerInterface(QStringLiteral("org.kde.kpasswdserver"), QStringLiteral("/modules/kpasswdserver"), QDBusConnection::sessionBus())), d(new KPasswdServerClientPrivate) { } KPasswdServerClient::~KPasswdServerClient() { delete m_interface; delete d; } bool KPasswdServerClient::checkAuthInfo(KIO::AuthInfo *info, qlonglong windowId, qlonglong usertime) { //qDebug() << "window-id=" << windowId << "url=" << info.url; if (!QCoreApplication::instance()) { qCWarning(KIO_CORE) << "kioslave is not a QCoreApplication! This is required for checkAuthInfo."; return false; } // create the loop for waiting for a result before sending the request KPasswdServerLoop loop; QObject::connect(m_interface, &OrgKdeKPasswdServerInterface::checkAuthInfoAsyncResult, &loop, &KPasswdServerLoop::slotQueryResult); QDBusReply reply = m_interface->checkAuthInfoAsync(*info, windowId, usertime); if (!reply.isValid()) { qCWarning(KIO_CORE) << "Can't communicate with kiod_kpasswdserver (for checkAuthInfo)!"; //qDebug() << reply.error().name() << reply.error().message(); return false; } if (!loop.waitForResult(reply.value())) { qCWarning(KIO_CORE) << "kiod_kpasswdserver died while waiting for reply!"; return false; } if (loop.authInfo().isModified()) { //qDebug() << "username=" << info.username << "password=[hidden]"; *info = loop.authInfo(); return true; } return false; } int KPasswdServerClient::queryAuthInfo(KIO::AuthInfo *info, const QString &errorMsg, qlonglong windowId, qlonglong usertime) { if (info->url.host() != d->lastHost) { // see kpasswdserver/DESIGN d->lastHost = info->url.host(); d->seqNr = 0; } //qDebug() << "window-id=" << windowId; if (!QCoreApplication::instance()) { qCWarning(KIO_CORE) << "kioslave is not a QCoreApplication! This is required for queryAuthInfo."; return KIO::ERR_PASSWD_SERVER; } // create the loop for waiting for a result before sending the request KPasswdServerLoop loop; QObject::connect(m_interface, &OrgKdeKPasswdServerInterface::queryAuthInfoAsyncResult, &loop, &KPasswdServerLoop::slotQueryResult); QDBusReply reply = m_interface->queryAuthInfoAsync(*info, errorMsg, windowId, d->seqNr, usertime); if (!reply.isValid()) { qCWarning(KIO_CORE) << "Can't communicate with kiod_kpasswdserver (for queryAuthInfo)!"; //qDebug() << reply.error().name() << reply.error().message(); return KIO::ERR_PASSWD_SERVER; } if (!loop.waitForResult(reply.value())) { qCWarning(KIO_CORE) << "kiod_kpasswdserver died while waiting for reply!"; return KIO::ERR_PASSWD_SERVER; } *info = loop.authInfo(); //qDebug() << "username=" << info->username << "password=[hidden]"; const qlonglong newSeqNr = loop.seqNr(); if (newSeqNr > 0) { d->seqNr = newSeqNr; if (info->isModified()) { return KJob::NoError; } } return KIO::ERR_USER_CANCELED; } void KPasswdServerClient::addAuthInfo(const KIO::AuthInfo &info, qlonglong windowId) { m_interface->addAuthInfo(info, windowId); } void KPasswdServerClient::removeAuthInfo(const QString &host, const QString &protocol, const QString &user) { m_interface->removeAuthInfo(host, protocol, user); } diff --git a/src/core/kpasswdserverloop_p.h b/src/core/kpasswdserverloop_p.h index 0b902f99..88699ca7 100644 --- a/src/core/kpasswdserverloop_p.h +++ b/src/core/kpasswdserverloop_p.h @@ -1,56 +1,55 @@ /* * This file is part of the KDE libraries * Copyright (c) 2009 Michael Leupold * * 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.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * 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, see . */ #ifndef KPASSWDSERVERLOOP_P_H #define KPASSWDSERVERLOOP_P_H #include -#include #include // Wait for the result of an asynchronous D-Bus request to KPasswdServer. // Objects of this class are one-way ie. as soon as they have received // a result you can't call waitForResult() again. class KPasswdServerLoop : public QEventLoop { Q_OBJECT public: KPasswdServerLoop(); virtual ~KPasswdServerLoop(); bool waitForResult(qlonglong requestId); qlonglong seqNr() const; const KIO::AuthInfo &authInfo() const; public Q_SLOTS: void slotQueryResult(qlonglong requestId, qlonglong seqNr, const KIO::AuthInfo &authInfo); private Q_SLOTS: void kdedServiceUnregistered(); private: qlonglong m_requestId; qlonglong m_seqNr; KIO::AuthInfo m_authInfo; }; #endif diff --git a/src/core/kprotocolmanager.cpp b/src/core/kprotocolmanager.cpp index 0f9078b8..dcc2c47f 100644 --- a/src/core/kprotocolmanager.cpp +++ b/src/core/kprotocolmanager.cpp @@ -1,1328 +1,1327 @@ /* This file is part of the KDE libraries Copyright (C) 1999 Torben Weis Copyright (C) 2000- Waldo Bastain Copyright (C) 2000- Dawit Alemayehu Copyright (C) 2008 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kprotocolmanager.h" #include "kprotocolinfo_p.h" #include "hostinfo.h" #include #include #include #ifdef Q_OS_WIN #include #undef interface //windows.h defines this, breaks QtDBus since it has parameters named interface #else #include #endif #include #include #include #include #include -#include #include #include #include #include #include #include #if !defined(QT_NO_NETWORKPROXY) && (defined (Q_OS_WIN32) || defined(Q_OS_MAC)) #include #include #endif #include #include #include #include #include #include "slaveconfig.h" #include "ioslave_defaults.h" #include "http_slave_defaults.h" #define QL1S(x) QLatin1String(x) #define QL1C(x) QLatin1Char(x) typedef QPair SubnetPair; /* Domain suffix match. E.g. return true if host is "cuzco.inka.de" and nplist is "inka.de,hadiko.de" or if host is "localhost" and nplist is "localhost". */ static bool revmatch(const char *host, const char *nplist) { if (host == nullptr) { return false; } const char *hptr = host + strlen(host) - 1; const char *nptr = nplist + strlen(nplist) - 1; const char *shptr = hptr; while (nptr >= nplist) { if (*hptr != *nptr) { hptr = shptr; // Try to find another domain or host in the list while (--nptr >= nplist && *nptr != ',' && *nptr != ' '); // Strip out multiple spaces and commas while (--nptr >= nplist && (*nptr == ',' || *nptr == ' ')); } else { if (nptr == nplist || nptr[-1] == ',' || nptr[-1] == ' ') { return true; } if (nptr[-1] == '/' && hptr == host) { // "bugs.kde.org" vs "http://bugs.kde.org", the config UI says URLs are ok return true; } if (hptr == host) { // e.g. revmatch("bugs.kde.org","mybugs.kde.org") return false; } hptr--; nptr--; } } return false; } class KProxyData : public QObject { Q_OBJECT public: KProxyData(const QString &slaveProtocol, const QStringList &proxyAddresses) : protocol(slaveProtocol) , proxyList(proxyAddresses) { } void removeAddress(const QString &address) { proxyList.removeAll(address); } QString protocol; QStringList proxyList; }; class KProtocolManagerPrivate { public: KProtocolManagerPrivate(); ~KProtocolManagerPrivate(); bool shouldIgnoreProxyFor(const QUrl &url); void sync(); KProtocolManager::ProxyType proxyType(); bool useReverseProxy(); QString readNoProxyFor(); QString proxyFor(const QString &protocol); QStringList getSystemProxyFor(const QUrl &url); QMutex mutex; // protects all member vars KSharedConfig::Ptr configPtr; KSharedConfig::Ptr http_config; QString modifiers; QString useragent; QString noProxyFor; QList noProxySubnets; QCache cachedProxyData; QMap protocolForArchiveMimetypes; }; Q_GLOBAL_STATIC(KProtocolManagerPrivate, kProtocolManagerPrivate) static void syncOnExit() { if (kProtocolManagerPrivate.exists()) kProtocolManagerPrivate()->sync(); } KProtocolManagerPrivate::KProtocolManagerPrivate() { // post routine since KConfig::sync() breaks if called too late qAddPostRoutine(syncOnExit); cachedProxyData.setMaxCost(200); // double the max cost. } KProtocolManagerPrivate::~KProtocolManagerPrivate() { } /* * Returns true if url is in the no proxy list. */ bool KProtocolManagerPrivate::shouldIgnoreProxyFor(const QUrl &url) { bool isMatch = false; const KProtocolManager::ProxyType type = proxyType(); const bool useRevProxy = ((type == KProtocolManager::ManualProxy) && useReverseProxy()); const bool useNoProxyList = (type == KProtocolManager::ManualProxy || type == KProtocolManager::EnvVarProxy); // No proxy only applies to ManualProxy and EnvVarProxy types... if (useNoProxyList && noProxyFor.isEmpty()) { QStringList noProxyForList(readNoProxyFor().split(QL1C(','))); QMutableStringListIterator it(noProxyForList); while (it.hasNext()) { SubnetPair subnet = QHostAddress::parseSubnet(it.next()); if (!subnet.first.isNull()) { noProxySubnets << subnet; it.remove(); } } noProxyFor = noProxyForList.join(QLatin1Char(',')); } if (!noProxyFor.isEmpty()) { QString qhost = url.host().toLower(); QByteArray host = qhost.toLatin1(); const QString qno_proxy = noProxyFor.trimmed().toLower(); const QByteArray no_proxy = qno_proxy.toLatin1(); isMatch = revmatch(host.constData(), no_proxy.constData()); // If no match is found and the request url has a port // number, try the combination of "host:port". This allows // users to enter host:port in the No-proxy-For list. if (!isMatch && url.port() > 0) { qhost += QL1C(':') + QString::number(url.port()); host = qhost.toLatin1(); isMatch = revmatch(host.constData(), no_proxy.constData()); } // If the hostname does not contain a dot, check if // is part of noProxy. if (!isMatch && !host.isEmpty() && (strchr(host.constData(), '.') == nullptr)) { isMatch = revmatch("", no_proxy.constData()); } } const QString host(url.host()); if (!noProxySubnets.isEmpty() && !host.isEmpty()) { QHostAddress address(host); // If request url is not IP address, do a DNS lookup of the hostname. // TODO: Perhaps we should make configurable ? if (address.isNull()) { //qDebug() << "Performing DNS lookup for" << host; QHostInfo info = KIO::HostInfo::lookupHost(host, 2000); const QList addresses = info.addresses(); if (!addresses.isEmpty()) { address = addresses.first(); } } if (!address.isNull()) { Q_FOREACH (const SubnetPair &subnet, noProxySubnets) { if (address.isInSubnet(subnet)) { isMatch = true; break; } } } } return (useRevProxy != isMatch); } void KProtocolManagerPrivate::sync() { QMutexLocker lock(&mutex); if (http_config) { http_config->sync(); } if (configPtr) { configPtr->sync(); } } #define PRIVATE_DATA \ KProtocolManagerPrivate *d = kProtocolManagerPrivate() void KProtocolManager::reparseConfiguration() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); if (d->http_config) { d->http_config->reparseConfiguration(); } if (d->configPtr) { d->configPtr->reparseConfiguration(); } d->cachedProxyData.clear(); d->noProxyFor.clear(); d->modifiers.clear(); d->useragent.clear(); lock.unlock(); // Force the slave config to re-read its config... KIO::SlaveConfig::self()->reset(); } static KSharedConfig::Ptr config() { PRIVATE_DATA; Q_ASSERT(!d->mutex.tryLock()); // the caller must have locked the mutex if (!d->configPtr) { d->configPtr = KSharedConfig::openConfig(QStringLiteral("kioslaverc"), KConfig::NoGlobals); } return d->configPtr; } KProtocolManager::ProxyType KProtocolManagerPrivate::proxyType() { KConfigGroup cg(config(), "Proxy Settings"); return static_cast(cg.readEntry("ProxyType", 0)); } bool KProtocolManagerPrivate::useReverseProxy() { KConfigGroup cg(config(), "Proxy Settings"); return cg.readEntry("ReversedException", false); } QString KProtocolManagerPrivate::readNoProxyFor() { QString noProxy = config()->group("Proxy Settings").readEntry("NoProxyFor"); if (proxyType() == KProtocolManager::EnvVarProxy) { noProxy = QString::fromLocal8Bit(qgetenv(noProxy.toLocal8Bit().constData())); } return noProxy; } QMap KProtocolManager::entryMap(const QString &group) { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->entryMap(group); } static KConfigGroup http_config() { PRIVATE_DATA; Q_ASSERT(!d->mutex.tryLock()); // the caller must have locked the mutex if (!d->http_config) { d->http_config = KSharedConfig::openConfig(QStringLiteral("kio_httprc"), KConfig::NoGlobals); } return KConfigGroup(d->http_config, QString()); } /*=============================== TIMEOUT SETTINGS ==========================*/ int KProtocolManager::readTimeout() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), QString()); int val = cg.readEntry("ReadTimeout", DEFAULT_READ_TIMEOUT); return qMax(MIN_TIMEOUT_VALUE, val); } int KProtocolManager::connectTimeout() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), QString()); int val = cg.readEntry("ConnectTimeout", DEFAULT_CONNECT_TIMEOUT); return qMax(MIN_TIMEOUT_VALUE, val); } int KProtocolManager::proxyConnectTimeout() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), QString()); int val = cg.readEntry("ProxyConnectTimeout", DEFAULT_PROXY_CONNECT_TIMEOUT); return qMax(MIN_TIMEOUT_VALUE, val); } int KProtocolManager::responseTimeout() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), QString()); int val = cg.readEntry("ResponseTimeout", DEFAULT_RESPONSE_TIMEOUT); return qMax(MIN_TIMEOUT_VALUE, val); } /*========================== PROXY SETTINGS =================================*/ bool KProtocolManager::useProxy() { return proxyType() != NoProxy; } bool KProtocolManager::useReverseProxy() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return d->useReverseProxy(); } KProtocolManager::ProxyType KProtocolManager::proxyType() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return d->proxyType(); } KProtocolManager::ProxyAuthMode KProtocolManager::proxyAuthMode() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); KConfigGroup cg(config(), "Proxy Settings"); return static_cast(cg.readEntry("AuthMode", 0)); } /*========================== CACHING =====================================*/ bool KProtocolManager::useCache() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return http_config().readEntry("UseCache", true); } KIO::CacheControl KProtocolManager::cacheControl() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); QString tmp = http_config().readEntry("cache"); if (tmp.isEmpty()) { return DEFAULT_CACHE_CONTROL; } return KIO::parseCacheControl(tmp); } QString KProtocolManager::cacheDir() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return http_config().readPathEntry("CacheDir", QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_http")); } int KProtocolManager::maxCacheAge() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return http_config().readEntry("MaxCacheAge", DEFAULT_MAX_CACHE_AGE); } int KProtocolManager::maxCacheSize() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return http_config().readEntry("MaxCacheSize", DEFAULT_MAX_CACHE_SIZE); } QString KProtocolManager::noProxyFor() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return d->readNoProxyFor(); } static QString adjustProtocol(const QString &scheme) { if (scheme.compare(QL1S("webdav"), Qt::CaseInsensitive) == 0) { return QStringLiteral("http"); } if (scheme.compare(QL1S("webdavs"), Qt::CaseInsensitive) == 0) { return QStringLiteral("https"); } return scheme.toLower(); } QString KProtocolManager::proxyFor(const QString &protocol) { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return d->proxyFor(protocol); } QString KProtocolManagerPrivate::proxyFor(const QString &protocol) { const QString key = adjustProtocol(protocol) + QL1S("Proxy"); QString proxyStr(config()->group("Proxy Settings").readEntry(key)); const int index = proxyStr.lastIndexOf(QL1C(' ')); if (index > -1) { bool ok = false; const QStringRef portStr(proxyStr.rightRef(proxyStr.length() - index - 1)); portStr.toInt(&ok); if (ok) { proxyStr = proxyStr.leftRef(index) + QL1C(':') + portStr; } else { proxyStr.clear(); } } return proxyStr; } QString KProtocolManager::proxyForUrl(const QUrl &url) { const QStringList proxies = proxiesForUrl(url); if (proxies.isEmpty()) { return QString(); } return proxies.first(); } QStringList KProtocolManagerPrivate::getSystemProxyFor(const QUrl &url) { QStringList proxies; #if !defined(QT_NO_NETWORKPROXY) && (defined(Q_OS_WIN32) || defined(Q_OS_MAC)) QNetworkProxyQuery query(url); const QList proxyList = QNetworkProxyFactory::systemProxyForQuery(query); proxies.reserve(proxyList.size()); for (const QNetworkProxy &proxy : proxyList) { QUrl url; const QNetworkProxy::ProxyType type = proxy.type(); if (type == QNetworkProxy::NoProxy || type == QNetworkProxy::DefaultProxy) { proxies << QL1S("DIRECT"); continue; } if (type == QNetworkProxy::HttpProxy || type == QNetworkProxy::HttpCachingProxy) { url.setScheme(QL1S("http")); } else if (type == QNetworkProxy::Socks5Proxy) { url.setScheme(QL1S("socks")); } else if (type == QNetworkProxy::FtpCachingProxy) { url.setScheme(QL1S("ftp")); } url.setHost(proxy.hostName()); url.setPort(proxy.port()); url.setUserName(proxy.user()); proxies << url.url(); } #else // On Unix/Linux use system environment variables if any are set. QString proxyVar(proxyFor(url.scheme())); // Check for SOCKS proxy, if not proxy is found for given url. if (!proxyVar.isEmpty()) { const QString proxy(QString::fromLocal8Bit(qgetenv(proxyVar.toLocal8Bit().constData())).trimmed()); if (!proxy.isEmpty()) { proxies << proxy; } } // Add the socks proxy as an alternate proxy if it exists, proxyVar = proxyFor(QStringLiteral("socks")); if (!proxyVar.isEmpty()) { QString proxy = QString::fromLocal8Bit(qgetenv(proxyVar.toLocal8Bit().constData())).trimmed(); // Make sure the scheme of SOCKS proxy is always set to "socks://". const int index = proxy.indexOf(QL1S("://")); const int offset = (index == -1) ? 0 : (index + 3); proxy = QL1S("socks://") + proxy.midRef(offset); if (!proxy.isEmpty()) { proxies << proxy; } } #endif return proxies; } QStringList KProtocolManager::proxiesForUrl(const QUrl &url) { QStringList proxyList; PRIVATE_DATA; QMutexLocker lock(&d->mutex); if (!d->shouldIgnoreProxyFor(url)) { switch (d->proxyType()) { case PACProxy: case WPADProxy: { QUrl u(url); const QString protocol = adjustProtocol(u.scheme()); u.setScheme(protocol); if (protocol.startsWith(QLatin1String("http")) || protocol.startsWith(QLatin1String("ftp"))) { QDBusReply reply = QDBusInterface(QStringLiteral("org.kde.kded5"), QStringLiteral("/modules/proxyscout"), QStringLiteral("org.kde.KPAC.ProxyScout")) .call(QStringLiteral("proxiesForUrl"), u.toString()); proxyList = reply; } break; } case EnvVarProxy: proxyList = d->getSystemProxyFor(url); break; case ManualProxy: { QString proxy(d->proxyFor(url.scheme())); if (!proxy.isEmpty()) { proxyList << proxy; } // Add the socks proxy as an alternate proxy if it exists, proxy = d->proxyFor(QStringLiteral("socks")); if (!proxy.isEmpty()) { // Make sure the scheme of SOCKS proxy is always set to "socks://". const int index = proxy.indexOf(QL1S("://")); const int offset = (index == -1) ? 0 : (index + 3); proxy = QL1S("socks://") + proxy.midRef(offset); proxyList << proxy; } } break; case NoProxy: break; } } if (proxyList.isEmpty()) { proxyList << QStringLiteral("DIRECT"); } return proxyList; } void KProtocolManager::badProxy(const QString &proxy) { QDBusInterface(QStringLiteral("org.kde.kded5"), QStringLiteral("/modules/proxyscout")) .asyncCall(QStringLiteral("blackListProxy"), proxy); PRIVATE_DATA; QMutexLocker lock(&d->mutex); const QStringList keys(d->cachedProxyData.keys()); for (const QString &key : keys) { d->cachedProxyData[key]->removeAddress(proxy); } } QString KProtocolManager::slaveProtocol(const QUrl &url, QString &proxy) { QStringList proxyList; const QString protocol = KProtocolManager::slaveProtocol(url, proxyList); if (!proxyList.isEmpty()) { proxy = proxyList.first(); } return protocol; } // Generates proxy cache key from request given url. static void extractProxyCacheKeyFromUrl(const QUrl &u, QString *key) { if (!key) { return; } *key = u.scheme(); *key += u.host(); if (u.port() > 0) { *key += QString::number(u.port()); } } QString KProtocolManager::slaveProtocol(const QUrl &url, QStringList &proxyList) { #if 0 if (url.hasSubUrl()) { // We don't want the suburl's protocol const QUrl::List list = QUrl::split(url); return slaveProtocol(list.last(), proxyList); } #endif proxyList.clear(); // Do not perform a proxy lookup for any url classified as a ":local" url or // one that does not have a host component or if proxy is disabled. QString protocol(url.scheme()); if (url.host().isEmpty() || KProtocolInfo::protocolClass(protocol) == QL1S(":local") || KProtocolManager::proxyType() == KProtocolManager::NoProxy) { return protocol; } QString proxyCacheKey; extractProxyCacheKeyFromUrl(url, &proxyCacheKey); PRIVATE_DATA; QMutexLocker lock(&d->mutex); // Look for cached proxy information to avoid more work. if (d->cachedProxyData.contains(proxyCacheKey)) { KProxyData *data = d->cachedProxyData.object(proxyCacheKey); proxyList = data->proxyList; return data->protocol; } lock.unlock(); const QStringList proxies = proxiesForUrl(url); const int count = proxies.count(); if (count > 0 && !(count == 1 && proxies.first() == QL1S("DIRECT"))) { for (const QString &proxy : proxies) { if (proxy == QL1S("DIRECT")) { proxyList << proxy; } else { QUrl u(proxy); if (!u.isEmpty() && u.isValid() && !u.scheme().isEmpty()) { proxyList << proxy; } } } } // The idea behind slave protocols is not applicable to http // and webdav protocols as well as protocols unknown to KDE. if (!proxyList.isEmpty() && !protocol.startsWith(QLatin1String("http")) && !protocol.startsWith(QLatin1String("webdav")) && KProtocolInfo::isKnownProtocol(protocol)) { Q_FOREACH (const QString &proxy, proxyList) { QUrl u(proxy); if (u.isValid() && KProtocolInfo::isKnownProtocol(u.scheme())) { protocol = u.scheme(); break; } } } lock.relock(); // cache the proxy information... d->cachedProxyData.insert(proxyCacheKey, new KProxyData(protocol, proxyList)); return protocol; } /*================================= USER-AGENT SETTINGS =====================*/ QString KProtocolManager::userAgentForHost(const QString &hostname) { const QString sendUserAgent = KIO::SlaveConfig::self()->configData(QStringLiteral("http"), hostname.toLower(), QStringLiteral("SendUserAgent")).toLower(); if (sendUserAgent == QL1S("false")) { return QString(); } const QString useragent = KIO::SlaveConfig::self()->configData(QStringLiteral("http"), hostname.toLower(), QStringLiteral("UserAgent")); // Return the default user-agent if none is specified // for the requested host. if (useragent.isEmpty()) { return defaultUserAgent(); } return useragent; } QString KProtocolManager::defaultUserAgent() { const QString modifiers = KIO::SlaveConfig::self()->configData(QStringLiteral("http"), QString(), QStringLiteral("UserAgentKeys")); return defaultUserAgent(modifiers); } static QString defaultUserAgentFromPreferredService() { QString agentStr; // Check if the default COMPONENT contains a custom default UA string... KService::Ptr service = KMimeTypeTrader::self()->preferredService(QStringLiteral("text/html"), QStringLiteral("KParts/ReadOnlyPart")); if (service && service->showInCurrentDesktop()) agentStr = service->property(QStringLiteral("X-KDE-Default-UserAgent"), QVariant::String).toString(); return agentStr; } // This is not the OS, but the windowing system, e.g. X11 on Unix/Linux. static QString platform() { #if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) return QStringLiteral("X11"); #elif defined(Q_OS_MAC) return QStringLiteral("Macintosh"); #elif defined(Q_OS_WIN) return QStringLiteral("Windows"); #else return QStringLiteral("Unknown"); #endif } QString KProtocolManager::defaultUserAgent(const QString &_modifiers) { PRIVATE_DATA; QMutexLocker lock(&d->mutex); QString modifiers = _modifiers.toLower(); if (modifiers.isEmpty()) { modifiers = QStringLiteral(DEFAULT_USER_AGENT_KEYS); } if (d->modifiers == modifiers && !d->useragent.isEmpty()) { return d->useragent; } d->modifiers = modifiers; /* The following code attempts to determine the default user agent string from the 'X-KDE-UA-DEFAULT-STRING' property of the desktop file for the preferred service that was configured to handle the 'text/html' mime type. If the preferred service's desktop file does not specify this property, the long standing default user agent string will be used. The following keyword placeholders are automatically converted when the user agent string is read from the property: %SECURITY% Expands to"N" when SSL is not supported, otherwise it is ignored. %OSNAME% Expands to operating system name, e.g. Linux. %OSVERSION% Expands to operating system version, e.g. 2.6.32 %SYSTYPE% Expands to machine or system type, e.g. i386 %PLATFORM% Expands to windowing system, e.g. X11 on Unix/Linux. %LANGUAGE% Expands to default language in use, e.g. en-US. %APPVERSION% Expands to QCoreApplication applicationName()/applicationVerison(), e.g. Konqueror/4.5.0. If application name and/or application version number are not set, then "KDE" and the runtime KDE version numbers are used respectively. All of the keywords are handled case-insensitively. */ QString systemName, systemVersion, machine, supp; const bool sysInfoFound = getSystemNameVersionAndMachine(systemName, systemVersion, machine); QString agentStr = defaultUserAgentFromPreferredService(); if (agentStr.isEmpty()) { supp += platform(); if (sysInfoFound) { if (modifiers.contains(QL1C('o'))) { supp += QL1S("; ") + systemName; if (modifiers.contains(QL1C('v'))) { supp += QL1C(' ') + systemVersion; } if (modifiers.contains(QL1C('m'))) { supp += QL1C(' ') + machine; } } if (modifiers.contains(QL1C('l'))) { supp += QL1S("; ") + QLocale::languageToString(QLocale().language()); } } // Full format: Mozilla/5.0 (Linux d->useragent = QL1S("Mozilla/5.0 (") + supp + QL1S(") KHTML/") + QString::number(KIO_VERSION_MAJOR) + QL1C('.') + QString::number(KIO_VERSION_MINOR) + QL1C('.') + QString::number(KIO_VERSION_PATCH) + QL1S(" (like Gecko) Konqueror/") + QString::number(KIO_VERSION_MAJOR) + QL1S(" KIO/") + QString::number(KIO_VERSION_MAJOR) + QL1C('.') + QString::number(KIO_VERSION_MINOR); } else { QString appName = QCoreApplication::applicationName(); if (appName.isEmpty() || appName.startsWith(QLatin1String("kcmshell"), Qt::CaseInsensitive)) { appName = QStringLiteral("KDE"); } QString appVersion = QCoreApplication::applicationVersion(); if (appVersion.isEmpty()) { appVersion += QString::number(KIO_VERSION_MAJOR) + QL1C('.') + QString::number(KIO_VERSION_MINOR) + QL1C('.') + QString::number(KIO_VERSION_PATCH); } appName += QL1C('/') + appVersion; agentStr.replace(QL1S("%appversion%"), appName, Qt::CaseInsensitive); if (!QSslSocket::supportsSsl()) { agentStr.replace(QLatin1String("%security%"), QL1S("N"), Qt::CaseInsensitive); } else { agentStr.remove(QStringLiteral("%security%"), Qt::CaseInsensitive); } if (sysInfoFound) { // Platform (e.g. X11). It is no longer configurable from UI. agentStr.replace(QL1S("%platform%"), platform(), Qt::CaseInsensitive); // Operating system (e.g. Linux) if (modifiers.contains(QL1C('o'))) { agentStr.replace(QL1S("%osname%"), systemName, Qt::CaseInsensitive); // OS version (e.g. 2.6.36) if (modifiers.contains(QL1C('v'))) { agentStr.replace(QL1S("%osversion%"), systemVersion, Qt::CaseInsensitive); } else { agentStr.remove(QStringLiteral("%osversion%"), Qt::CaseInsensitive); } // Machine type (i686, x86-64, etc.) if (modifiers.contains(QL1C('m'))) { agentStr.replace(QL1S("%systype%"), machine, Qt::CaseInsensitive); } else { agentStr.remove(QStringLiteral("%systype%"), Qt::CaseInsensitive); } } else { agentStr.remove(QStringLiteral("%osname%"), Qt::CaseInsensitive); agentStr.remove(QStringLiteral("%osversion%"), Qt::CaseInsensitive); agentStr.remove(QStringLiteral("%systype%"), Qt::CaseInsensitive); } // Language (e.g. en_US) if (modifiers.contains(QL1C('l'))) { agentStr.replace(QL1S("%language%"), QLocale::languageToString(QLocale().language()), Qt::CaseInsensitive); } else { agentStr.remove(QStringLiteral("%language%"), Qt::CaseInsensitive); } // Clean up unnecessary separators that could be left over from the // possible keyword removal above... agentStr.replace(QRegExp(QL1S("[(]\\s*[;]\\s*")), QStringLiteral("(")); agentStr.replace(QRegExp(QL1S("[;]\\s*[;]\\s*")), QStringLiteral("; ")); agentStr.replace(QRegExp(QL1S("\\s*[;]\\s*[)]")), QStringLiteral(")")); } else { agentStr.remove(QStringLiteral("%osname%")); agentStr.remove(QStringLiteral("%osversion%")); agentStr.remove(QStringLiteral("%platform%")); agentStr.remove(QStringLiteral("%systype%")); agentStr.remove(QStringLiteral("%language%")); } d->useragent = agentStr.simplified(); } //qDebug() << "USERAGENT STRING:" << d->useragent; return d->useragent; } QString KProtocolManager::userAgentForApplication(const QString &appName, const QString &appVersion, const QStringList &extraInfo) { QString systemName, systemVersion, machine, info; if (getSystemNameVersionAndMachine(systemName, systemVersion, machine)) { info += systemName + QL1C('/') + systemVersion + QL1S("; "); } info += QL1S("KDE/") + QString::number(KIO_VERSION_MAJOR) + QL1C('.') + QString::number(KIO_VERSION_MINOR) + QL1C('.') + QString::number(KIO_VERSION_PATCH); if (!machine.isEmpty()) { info += QL1S("; ") + machine; } info += QL1S("; ") + extraInfo.join(QStringLiteral("; ")); return (appName + QL1C('/') + appVersion + QStringLiteral(" (") + info + QL1C(')')); } bool KProtocolManager::getSystemNameVersionAndMachine( QString &systemName, QString &systemVersion, QString &machine) { #if defined(Q_OS_WIN) && !defined(_WIN32_WCE) // we do not use unameBuf.sysname information constructed in kdewin32 // because we want to get separate name and version systemName = QStringLiteral("Windows"); OSVERSIONINFOEX versioninfo; ZeroMemory(&versioninfo, sizeof(OSVERSIONINFOEX)); // try calling GetVersionEx using the OSVERSIONINFOEX, if that fails, try using the OSVERSIONINFO versioninfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); bool ok = GetVersionEx((OSVERSIONINFO *) &versioninfo); if (!ok) { versioninfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); ok = GetVersionEx((OSVERSIONINFO *) &versioninfo); } if (ok) { systemVersion = QString::number(versioninfo.dwMajorVersion); systemVersion += QL1C('.'); systemVersion += QString::number(versioninfo.dwMinorVersion); } #else struct utsname unameBuf; if (0 != uname(&unameBuf)) { return false; } systemName = QString::fromUtf8(unameBuf.sysname); systemVersion = QString::fromUtf8(unameBuf.release); machine = QString::fromUtf8(unameBuf.machine); #endif return true; } QString KProtocolManager::acceptLanguagesHeader() { const QLatin1String english("en"); // User's desktop language preference. QStringList languageList = QLocale().uiLanguages(); // Replace possible "C" in the language list with "en", unless "en" is // already pressent. This is to keep user's priorities in order. // If afterwards "en" is still not present, append it. int idx = languageList.indexOf(QStringLiteral("C")); if (idx != -1) { if (languageList.contains(english)) { languageList.removeAt(idx); } else { languageList[idx] = english; } } if (!languageList.contains(english)) { languageList += english; } // Some languages may have web codes different from locale codes, // read them from the config and insert in proper order. KConfig acclangConf(QStringLiteral("accept-languages.codes"), KConfig::NoGlobals); KConfigGroup replacementCodes(&acclangConf, "ReplacementCodes"); QStringList languageListFinal; for (const QString &lang : qAsConst(languageList)) { const QStringList langs = replacementCodes.readEntry(lang, QStringList()); if (langs.isEmpty()) { languageListFinal += lang; } else { languageListFinal += langs; } } // The header is composed of comma separated languages, with an optional // associated priority estimate (q=1..0) defaulting to 1. // As our language tags are already sorted by priority, we'll just decrease // the value evenly int prio = 10; QString header; for (const QString &lang : qAsConst(languageListFinal)) { header += lang; if (prio < 10) { header += QL1S(";q=0.") + QString::number(prio); } // do not add cosmetic whitespace in here : it is less compatible (#220677) header += QL1C(','); if (prio > 1) { --prio; } } header.chop(1); // Some of the languages may have country specifier delimited by // underscore, or modifier delimited by at-sign. // The header should use dashes instead. header.replace(QL1C('_'), QL1C('-')); header.replace(QL1C('@'), QL1C('-')); return header; } /*==================================== OTHERS ===============================*/ bool KProtocolManager::markPartial() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("MarkPartial", true); } int KProtocolManager::minimumKeepSize() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); // 5000 byte } bool KProtocolManager::autoResume() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("AutoResume", false); } bool KProtocolManager::persistentConnections() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("PersistentConnections", true); } bool KProtocolManager::persistentProxyConnection() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group(QByteArray()).readEntry("PersistentProxyConnection", false); } QString KProtocolManager::proxyConfigScript() { PRIVATE_DATA; QMutexLocker lock(&d->mutex); return config()->group("Proxy Settings").readEntry("Proxy Config Script"); } /* =========================== PROTOCOL CAPABILITIES ============== */ static KProtocolInfoPrivate *findProtocol(const QUrl &url) { if (!url.isValid()) { return nullptr; } QString protocol = url.scheme(); if (!KProtocolInfo::proxiedBy(protocol).isEmpty()) { QString dummy; protocol = KProtocolManager::slaveProtocol(url, dummy); } return KProtocolInfoFactory::self()->findProtocol(protocol); } KProtocolInfo::Type KProtocolManager::inputType(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return KProtocolInfo::T_NONE; } return prot->m_inputType; } KProtocolInfo::Type KProtocolManager::outputType(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return KProtocolInfo::T_NONE; } return prot->m_outputType; } bool KProtocolManager::isSourceProtocol(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_isSourceProtocol; } bool KProtocolManager::supportsListing(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsListing; } QStringList KProtocolManager::listing(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return QStringList(); } return prot->m_listing; } bool KProtocolManager::supportsReading(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsReading; } bool KProtocolManager::supportsWriting(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsWriting; } bool KProtocolManager::supportsMakeDir(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsMakeDir; } bool KProtocolManager::supportsDeleting(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsDeleting; } bool KProtocolManager::supportsLinking(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsLinking; } bool KProtocolManager::supportsMoving(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsMoving; } bool KProtocolManager::supportsOpening(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_supportsOpening; } bool KProtocolManager::canCopyFromFile(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canCopyFromFile; } bool KProtocolManager::canCopyToFile(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canCopyToFile; } bool KProtocolManager::canRenameFromFile(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canRenameFromFile; } bool KProtocolManager::canRenameToFile(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canRenameToFile; } bool KProtocolManager::canDeleteRecursive(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return false; } return prot->m_canDeleteRecursive; } KProtocolInfo::FileNameUsedForCopying KProtocolManager::fileNameUsedForCopying(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return KProtocolInfo::FromUrl; } return prot->m_fileNameUsedForCopying; } QString KProtocolManager::defaultMimetype(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); if (!prot) { return QString(); } return prot->m_defaultMimetype; } QString KProtocolManager::protocolForArchiveMimetype(const QString &mimeType) { PRIVATE_DATA; QMutexLocker lock(&d->mutex); if (d->protocolForArchiveMimetypes.isEmpty()) { const QList allProtocols = KProtocolInfoFactory::self()->allProtocols(); for (QList::const_iterator it = allProtocols.begin(); it != allProtocols.end(); ++it) { const QStringList archiveMimetypes = (*it)->m_archiveMimeTypes; for (const QString &mime : archiveMimetypes) { d->protocolForArchiveMimetypes.insert(mime, (*it)->m_name); } } } return d->protocolForArchiveMimetypes.value(mimeType); } QString KProtocolManager::charsetFor(const QUrl &url) { return KIO::SlaveConfig::self()->configData(url.scheme(), url.host(), QStringLiteral("Charset")); } #undef PRIVATE_DATA #include "kprotocolmanager.moc" diff --git a/src/core/ksambashare.cpp b/src/core/ksambashare.cpp index 777a20e0..ef67af54 100644 --- a/src/core/ksambashare.cpp +++ b/src/core/ksambashare.cpp @@ -1,529 +1,528 @@ /* This file is part of the KDE project Copyright (c) 2004 Jan Schaefer Copyright 2010 Rodrigo Belem This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ksambashare.h" #include "ksambashare_p.h" #include "ksambasharedata.h" #include "ksambasharedata_p.h" #include "kiocoredebug.h" #include -#include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE) Q_LOGGING_CATEGORY(KIO_CORE_SAMBASHARE, "kf5.kio.core.sambashare", QtWarningMsg) // Default smb.conf locations // sorted by priority, most priority first static const char *const DefaultSambaConfigFilePathList[] = { "/etc/samba/smb.conf", "/etc/smb.conf", "/usr/local/etc/smb.conf", "/usr/local/samba/lib/smb.conf", "/usr/samba/lib/smb.conf", "/usr/lib/smb.conf", "/usr/local/lib/smb.conf" }; static const int DefaultSambaConfigFilePathListSize = sizeof(DefaultSambaConfigFilePathList) / sizeof(char *); KSambaSharePrivate::KSambaSharePrivate(KSambaShare *parent) : q_ptr(parent) , data() , smbConf() , userSharePath() , skipUserShare(false) { setUserSharePath(); findSmbConf(); data = parse(getNetUserShareInfo()); } KSambaSharePrivate::~KSambaSharePrivate() { } bool KSambaSharePrivate::isSambaInstalled() { if (QFile::exists(QStringLiteral("/usr/sbin/smbd")) || QFile::exists(QStringLiteral("/usr/local/sbin/smbd"))) { return true; } //qDebug() << "Samba is not installed!"; return false; } // Try to find the samba config file path // in several well-known paths bool KSambaSharePrivate::findSmbConf() { for (int i = 0; i < DefaultSambaConfigFilePathListSize; ++i) { const QString filePath = QString::fromLatin1(DefaultSambaConfigFilePathList[i]); if (QFile::exists(filePath)) { smbConf = filePath; return true; } } qCDebug(KIO_CORE_SAMBASHARE) << "KSambaShare: Could not find smb.conf!"; return false; } void KSambaSharePrivate::setUserSharePath() { const QString rawString = testparmParamValue(QStringLiteral("usershare path")); const QFileInfo fileInfo(rawString); if (fileInfo.isDir()) { userSharePath = rawString; } } int KSambaSharePrivate::runProcess(const QString &progName, const QStringList &args, QByteArray &stdOut, QByteArray &stdErr) { QProcess process; process.setProcessChannelMode(QProcess::SeparateChannels); process.start(progName, args); //TODO: make it async in future process.waitForFinished(); stdOut = process.readAllStandardOutput(); stdErr = process.readAllStandardError(); return process.exitCode(); } QString KSambaSharePrivate::testparmParamValue(const QString ¶meterName) { if (!isSambaInstalled()) { return QString(); } QByteArray stdErr; QByteArray stdOut; const QStringList args{ QStringLiteral("-d0"), QStringLiteral("-s"), QStringLiteral("--parameter-name"), parameterName, }; runProcess(QStringLiteral("testparm"), args, stdOut, stdErr); //TODO: parse and process error messages. // create a parser for the error output and // send error message somewhere if (!stdErr.isEmpty()) { QList err; err << stdErr.trimmed().split('\n'); if ((err.count() == 2) && err.at(0).startsWith("Load smb config files from") && err.at(1).startsWith("Loaded services file OK.")) { //qDebug() << "Running testparm" << args; } else { qCWarning(KIO_CORE) << "We got some errors while running testparm" << stdErr; } } if (!stdOut.isEmpty()) { return QString::fromLocal8Bit(stdOut.trimmed()); } return QString(); } QByteArray KSambaSharePrivate::getNetUserShareInfo() { if (skipUserShare || !isSambaInstalled()) { return QByteArray(); } QByteArray stdOut; QByteArray stdErr; const QStringList args{ QStringLiteral("usershare"), QStringLiteral("info"), }; runProcess(QStringLiteral("net"), args, stdOut, stdErr); if (!stdErr.isEmpty()) { if (stdErr.contains("You do not have permission to create a usershare")) { skipUserShare = true; } else if (stdErr.contains("usershares are currently disabled")) { skipUserShare = true; } else { //TODO: parse and process other error messages. // create a parser for the error output and // send error message somewhere qCWarning(KIO_CORE) << "We got some errors while running 'net usershare info'"; qCWarning(KIO_CORE) << stdErr; } } return stdOut; } QStringList KSambaSharePrivate::shareNames() const { return data.keys(); } QStringList KSambaSharePrivate::sharedDirs() const { QStringList dirs; QMap::ConstIterator i; for (i = data.constBegin(); i != data.constEnd(); ++i) { if (!dirs.contains(i.value().path())) { dirs << i.value().path(); } } return dirs; } KSambaShareData KSambaSharePrivate::getShareByName(const QString &shareName) const { return data.value(shareName); } QList KSambaSharePrivate::getSharesByPath(const QString &path) const { QList shares; QMap::ConstIterator i; for (i = data.constBegin(); i != data.constEnd(); ++i) { if (i.value().path() == path) { shares << i.value(); } } return shares; } bool KSambaSharePrivate::isShareNameValid(const QString &name) const { // Samba forbidden chars const QRegExp notToMatchRx(QStringLiteral("[%<>*\?|/\\+=;:\",]")); return (notToMatchRx.indexIn(name) == -1); } bool KSambaSharePrivate::isDirectoryShared(const QString &path) const { QMap::ConstIterator i; for (i = data.constBegin(); i != data.constEnd(); ++i) { if (i.value().path() == path) { return true; } } return false; } bool KSambaSharePrivate::isShareNameAvailable(const QString &name) const { // Samba does not allow to name a share with a user name registered in the system return (!KUser::allUserNames().contains(name) || !data.contains(name)); } KSambaShareData::UserShareError KSambaSharePrivate::isPathValid(const QString &path) const { QFileInfo pathInfo = path; if (!pathInfo.exists()) { return KSambaShareData::UserSharePathNotExists; } if (!pathInfo.isDir()) { return KSambaShareData::UserSharePathNotDirectory; } if (pathInfo.isRelative()) { if (pathInfo.makeAbsolute()) { return KSambaShareData::UserSharePathNotAbsolute; } } // TODO: check if the user is root if (KSambaSharePrivate::testparmParamValue(QStringLiteral("usershare owner only")) == QLatin1String("Yes")) { if (!pathInfo.permission(QFile::ReadUser | QFile::WriteUser)) { return KSambaShareData::UserSharePathNotAllowed; } } return KSambaShareData::UserSharePathOk; } KSambaShareData::UserShareError KSambaSharePrivate::isAclValid(const QString &acl) const { const QRegExp aclRx(QStringLiteral("(?:(?:(\\w(\\w|\\s)*)\\\\|)(\\w+\\s*):([fFrRd]{1})(?:,|))*")); // TODO: check if user is a valid smb user return aclRx.exactMatch(acl) ? KSambaShareData::UserShareAclOk : KSambaShareData::UserShareAclInvalid; } KSambaShareData::UserShareError KSambaSharePrivate::guestsAllowed(const KSambaShareData::GuestPermission &guestok) const { if (guestok == KSambaShareData::GuestsAllowed) { if (KSambaSharePrivate::testparmParamValue(QStringLiteral("usershare allow guests")) == QLatin1String("No")) { return KSambaShareData::UserShareGuestsNotAllowed; } } return KSambaShareData::UserShareGuestsOk; } KSambaShareData::UserShareError KSambaSharePrivate::add(const KSambaShareData &shareData) { // TODO: // * check for usershare max shares if (!isSambaInstalled()) { return KSambaShareData::UserShareSystemError; } QByteArray stdOut; QByteArray stdErr; if (data.contains(shareData.name())) { if (data.value(shareData.name()).path() != shareData.path()) { return KSambaShareData::UserShareNameInUse; } } else { // It needs to be added here, otherwise another instance of KSambaShareDataPrivate // will be created and added to data. data.insert(shareData.name(), shareData); } QString guestok = QStringLiteral("guest_ok=%1").arg( (shareData.guestPermission() == KSambaShareData::GuestsNotAllowed) ? QStringLiteral("n") : QStringLiteral("y")); const QStringList args{ QStringLiteral("usershare"), QStringLiteral("add"), shareData.name(), shareData.path(), shareData.comment(), shareData.acl(), guestok, }; int ret = runProcess(QStringLiteral("net"), args, stdOut, stdErr); //TODO: parse and process error messages. if (!stdErr.isEmpty()) { // create a parser for the error output and // send error message somewhere qCWarning(KIO_CORE) << "We got some errors while running 'net usershare add'" << args; qCWarning(KIO_CORE) << stdErr; } return (ret == 0) ? KSambaShareData::UserShareOk : KSambaShareData::UserShareSystemError; } KSambaShareData::UserShareError KSambaSharePrivate::remove(const KSambaShareData &shareData) const { if (!isSambaInstalled()) { return KSambaShareData::UserShareSystemError; } if (!data.contains(shareData.name())) { return KSambaShareData::UserShareNameInvalid; } const QStringList args{ QStringLiteral("usershare"), QStringLiteral("delete"), shareData.name(), }; int result = QProcess::execute(QStringLiteral("net"), args); return (result == 0) ? KSambaShareData::UserShareOk : KSambaShareData::UserShareSystemError; } QMap KSambaSharePrivate::parse(const QByteArray &usershareData) { const QRegExp headerRx(QString::fromLatin1("^\\s*\\[" "([^%<>*\?|/\\+=;:\",]+)" "\\]")); const QRegExp OptValRx(QString::fromLatin1("^\\s*([\\w\\d\\s]+)" "=" "(.*)$")); QTextStream stream(usershareData); QString currentShare; QMap shares; while (!stream.atEnd()) { const QString line = stream.readLine().trimmed(); if (headerRx.exactMatch(line)) { currentShare = headerRx.cap(1).trimmed(); if (!shares.contains(currentShare)) { KSambaShareData shareData; shareData.dd->name = currentShare; shares.insert(currentShare, shareData); } } else if (OptValRx.exactMatch(line)) { const QString key = OptValRx.cap(1).trimmed(); const QString value = OptValRx.cap(2).trimmed(); KSambaShareData shareData = shares[currentShare]; if (key == QLatin1String("path")) { // Samba accepts paths with and w/o trailing slash, we // use and expect path without slash if (value.endsWith(QLatin1Char('/'))) { shareData.dd->path = value.left(value.size() - 1); } else { shareData.dd->path = value; } } else if (key == QLatin1String("comment")) { shareData.dd->comment = value; } else if (key == QLatin1String("usershare_acl")) { shareData.dd->acl = value; } else if (key == QLatin1String("guest_ok")) { shareData.dd->guestPermission = value; } else { qCWarning(KIO_CORE) << "Something nasty happen while parsing 'net usershare info'" << "share:" << currentShare << "key:" << key; } } else if (line.trimmed().isEmpty()) { continue; } else { return shares; } } return shares; } void KSambaSharePrivate::_k_slotFileChange(const QString &path) { if (path != userSharePath) { return; } data = parse(getNetUserShareInfo()); //qDebug() << "path changed:" << path; Q_Q(KSambaShare); emit q->changed(); } KSambaShare::KSambaShare() : QObject(nullptr) , d_ptr(new KSambaSharePrivate(this)) { Q_D(const KSambaShare); if (!d->userSharePath.isEmpty() && QFileInfo::exists(d->userSharePath)) { KDirWatch::self()->addDir(d->userSharePath, KDirWatch::WatchFiles); connect(KDirWatch::self(), SIGNAL(dirty(QString)), this, SLOT(_k_slotFileChange(QString))); } } KSambaShare::~KSambaShare() { Q_D(const KSambaShare); if (KDirWatch::exists() && KDirWatch::self()->contains(d->userSharePath)) { KDirWatch::self()->removeDir(d->userSharePath); } delete d_ptr; } #ifndef KIOCORE_NO_DEPRECATED QString KSambaShare::smbConfPath() const { Q_D(const KSambaShare); return d->smbConf; } #endif bool KSambaShare::isDirectoryShared(const QString &path) const { Q_D(const KSambaShare); return d->isDirectoryShared(path); } bool KSambaShare::isShareNameAvailable(const QString &name) const { Q_D(const KSambaShare); return d->isShareNameValid(name) && d->isShareNameAvailable(name); } QStringList KSambaShare::shareNames() const { Q_D(const KSambaShare); return d->shareNames(); } QStringList KSambaShare::sharedDirectories() const { Q_D(const KSambaShare); return d->sharedDirs(); } KSambaShareData KSambaShare::getShareByName(const QString &name) const { Q_D(const KSambaShare); return d->getShareByName(name); } QList KSambaShare::getSharesByPath(const QString &path) const { Q_D(const KSambaShare); return d->getSharesByPath(path); } class KSambaShareSingleton { public: KSambaShare instance; }; Q_GLOBAL_STATIC(KSambaShareSingleton, _instance) KSambaShare *KSambaShare::instance() { return &_instance()->instance; } #include "moc_ksambashare.cpp" diff --git a/src/core/ksambasharedata.cpp b/src/core/ksambasharedata.cpp index 14f446d8..c4b7c08f 100644 --- a/src/core/ksambasharedata.cpp +++ b/src/core/ksambasharedata.cpp @@ -1,172 +1,169 @@ /* * Copyright 2010 Rodrigo Belem * * 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.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * 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, see */ #include "ksambasharedata.h" #include "ksambasharedata_p.h" -#include -#include -#include #include "ksambashare.h" #include "ksambashare_p.h" //TODO: add support for this samba options // usershare allow guests=P_BOOL,FLAG_ADVANCED // usershare max shares=P_INTEGER,FLAG_ADVANCED // usershare owner only=P_BOOL,FLAG_ADVANCED // usershare path=P_STRING,FLAG_ADVANCED // usershare prefix allow list=P_LIST,FLAG_ADVANCED // usershare prefix deny list=P_LIST,FLAG_ADVANCED // usershare template share=P_STRING,FLAG_ADVANCED KSambaShareData::KSambaShareData() : dd(new KSambaShareDataPrivate) { } KSambaShareData::KSambaShareData(const KSambaShareData &other) : dd(other.dd) { } KSambaShareData::~KSambaShareData() { } QString KSambaShareData::name() const { return dd->name; } QString KSambaShareData::path() const { return dd->path; } QString KSambaShareData::comment() const { return dd->comment; } QString KSambaShareData::acl() const { return dd->acl; } KSambaShareData::GuestPermission KSambaShareData::guestPermission() const { return (dd->guestPermission == QLatin1String("n")) ? GuestsNotAllowed : GuestsAllowed; } KSambaShareData::UserShareError KSambaShareData::setName(const QString &name) { if (!KSambaShare::instance()->d_func()->isShareNameValid(name)) { return UserShareNameInvalid; } if (!KSambaShare::instance()->d_func()->isShareNameAvailable(name)) { return UserShareNameInUse; } if (!dd->name.isEmpty()) { dd.detach(); } dd->name = name; return UserShareNameOk; } KSambaShareData::UserShareError KSambaShareData::setPath(const QString &path) { UserShareError result = KSambaShare::instance()->d_func()->isPathValid(path); if (result == UserSharePathOk) { dd->path = path; } return result; } KSambaShareData::UserShareError KSambaShareData::setComment(const QString &comment) { dd->comment = comment; return UserShareCommentOk; } KSambaShareData::UserShareError KSambaShareData::setAcl(const QString &acl) { UserShareError result = KSambaShare::instance()->d_func()->isAclValid(acl); if (result == UserShareAclOk) { dd->acl = acl; } return result; } KSambaShareData::UserShareError KSambaShareData::setGuestPermission(const GuestPermission &permission) { UserShareError result = KSambaShare::instance()->d_func()->guestsAllowed(permission); if (result == UserShareGuestsOk) { dd->guestPermission = (permission == GuestsNotAllowed) ? QStringLiteral("n") : QStringLiteral("y"); } return result; } KSambaShareData::UserShareError KSambaShareData::save() { if (dd->name.isEmpty()) { return UserShareNameInvalid; } else if (dd->path.isEmpty()) { return UserSharePathInvalid; } else { return KSambaShare::instance()->d_func()->add(*this); } } KSambaShareData::UserShareError KSambaShareData::remove() { if (dd->name.isEmpty()) { return UserShareNameInvalid; } else { return KSambaShare::instance()->d_func()->remove(*this); } } KSambaShareData &KSambaShareData::operator=(const KSambaShareData &other) { if (&other != this) { dd = other.dd; } return *this; } bool KSambaShareData::operator==(const KSambaShareData &other) const { return other.dd == dd; } bool KSambaShareData::operator!=(const KSambaShareData &other) const { return !(&other == this); } diff --git a/src/core/kssl/ksslsettings.cpp b/src/core/kssl/ksslsettings.cpp index f5ad0605..7bcaf186 100644 --- a/src/core/kssl/ksslsettings.cpp +++ b/src/core/kssl/ksslsettings.cpp @@ -1,244 +1,243 @@ /* This file is part of the KDE project * * Copyright (C) 2000 George Staikos * * 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 "ksslsettings.h" -#include #include #include class CipherNode { public: CipherNode(const char *_name, int _keylen) : name(QString::fromUtf8(_name)), keylen(_keylen) {} QString name; int keylen; inline int operator==(CipherNode &x) { return ((x.keylen == keylen) && (x.name == name)); } inline int operator< (CipherNode &x) { return keylen < x.keylen; } inline int operator<=(CipherNode &x) { return keylen <= x.keylen; } inline int operator> (CipherNode &x) { return keylen > x.keylen; } inline int operator>=(CipherNode &x) { return keylen >= x.keylen; } }; class KSSLSettingsPrivate { public: KSSLSettingsPrivate() { } ~KSSLSettingsPrivate() { } bool m_bUseEGD; bool m_bUseEFile; QString m_EGDPath; bool m_bSendX509; bool m_bPromptX509; KConfig *m_cfg; bool m_bWarnOnEnter, m_bWarnOnUnencrypted, m_bWarnOnLeave, m_bWarnOnMixed; bool m_bWarnSelfSigned, m_bWarnRevoked, m_bWarnExpired; QStringList m_v3ciphers; QStringList m_v3selectedciphers; QList m_v3bits; }; // // FIXME // Implementation note: for now, we only read cipher settings from disk, // and do not store them in memory. This should change. // KSSLSettings::KSSLSettings(bool readConfig) : d(new KSSLSettingsPrivate) { d->m_cfg = new KConfig(QStringLiteral("cryptodefaults"), KConfig::NoGlobals); if (readConfig) { load(); } } // we don't save settings incase it was a temporary object KSSLSettings::~KSSLSettings() { delete d->m_cfg; delete d; } QString KSSLSettings::getCipherList() { QString clist; // TODO fill in list here (or just remove this method!) return clist; } // FIXME - sync these up so that we can use them with the control module!! void KSSLSettings::load() { d->m_cfg->reparseConfiguration(); KConfigGroup cfg(d->m_cfg, "Warnings"); d->m_bWarnOnEnter = cfg.readEntry("OnEnter", false); d->m_bWarnOnLeave = cfg.readEntry("OnLeave", true); d->m_bWarnOnUnencrypted = cfg.readEntry("OnUnencrypted", false); d->m_bWarnOnMixed = cfg.readEntry("OnMixed", true); cfg = KConfigGroup(d->m_cfg, "Validation"); d->m_bWarnSelfSigned = cfg.readEntry("WarnSelfSigned", true); d->m_bWarnExpired = cfg.readEntry("WarnExpired", true); d->m_bWarnRevoked = cfg.readEntry("WarnRevoked", true); cfg = KConfigGroup(d->m_cfg, "EGD"); d->m_bUseEGD = cfg.readEntry("UseEGD", false); d->m_bUseEFile = cfg.readEntry("UseEFile", false); d->m_EGDPath = cfg.readPathEntry("EGDPath", QString()); cfg = KConfigGroup(d->m_cfg, "Auth"); d->m_bSendX509 = (QLatin1String("send") == cfg.readEntry("AuthMethod", "")); d->m_bPromptX509 = (QLatin1String("prompt") == cfg.readEntry("AuthMethod", "")); } void KSSLSettings::defaults() { d->m_bWarnOnEnter = false; d->m_bWarnOnLeave = true; d->m_bWarnOnUnencrypted = true; d->m_bWarnOnMixed = true; d->m_bWarnSelfSigned = true; d->m_bWarnExpired = true; d->m_bWarnRevoked = true; d->m_bUseEGD = false; d->m_bUseEFile = false; d->m_EGDPath = QLatin1String(""); } void KSSLSettings::save() { KConfigGroup cfg(d->m_cfg, "Warnings"); cfg.writeEntry("OnEnter", d->m_bWarnOnEnter); cfg.writeEntry("OnLeave", d->m_bWarnOnLeave); cfg.writeEntry("OnUnencrypted", d->m_bWarnOnUnencrypted); cfg.writeEntry("OnMixed", d->m_bWarnOnMixed); cfg = KConfigGroup(d->m_cfg, "Validation"); cfg.writeEntry("WarnSelfSigned", d->m_bWarnSelfSigned); cfg.writeEntry("WarnExpired", d->m_bWarnExpired); cfg.writeEntry("WarnRevoked", d->m_bWarnRevoked); cfg = KConfigGroup(d->m_cfg, "EGD"); cfg.writeEntry("UseEGD", d->m_bUseEGD); cfg.writeEntry("UseEFile", d->m_bUseEFile); cfg.writePathEntry("EGDPath", d->m_EGDPath); d->m_cfg->sync(); // FIXME - ciphers #if 0 #if KSSL_HAVE_SSL cfg.setGroup("SSLv3"); for (unsigned int i = 0; i < v3ciphers.count(); i++) { QString ciphername; ciphername.sprintf("cipher_%s", v3ciphers[i].ascii()); if (v3selectedciphers.contains(v3ciphers[i])) { cfg.writeEntry(ciphername, true); } else { cfg.writeEntry(ciphername, false); } } d->m_cfg->sync(); #endif // insure proper permissions -- contains sensitive data QString cfgName(QStandardPaths::locate(QStandardPaths::GenericConfigLocation, "cryptodefaults")); if (!cfgName.isEmpty()) { KDE::chmod(cfgName, 0600); } #endif } bool KSSLSettings::warnOnEnter() const { return d->m_bWarnOnEnter; } void KSSLSettings::setWarnOnEnter(bool x) { d->m_bWarnOnEnter = x; } bool KSSLSettings::warnOnUnencrypted() const { return d->m_bWarnOnUnencrypted; } void KSSLSettings::setWarnOnUnencrypted(bool x) { d->m_bWarnOnUnencrypted = x; } bool KSSLSettings::warnOnLeave() const { return d->m_bWarnOnLeave; } void KSSLSettings::setWarnOnLeave(bool x) { d->m_bWarnOnLeave = x; } bool KSSLSettings::warnOnMixed() const { return d->m_bWarnOnMixed; } bool KSSLSettings::useEGD() const { return d->m_bUseEGD; } bool KSSLSettings::useEFile() const { return d->m_bUseEFile; } bool KSSLSettings::autoSendX509() const { return d->m_bSendX509; } bool KSSLSettings::promptSendX509() const { return d->m_bPromptX509; } QString &KSSLSettings::getEGDPath() { return d->m_EGDPath; } diff --git a/src/core/ksslcertificatemanager.cpp b/src/core/ksslcertificatemanager.cpp index 604bf3a8..f00b3447 100644 --- a/src/core/ksslcertificatemanager.cpp +++ b/src/core/ksslcertificatemanager.cpp @@ -1,499 +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 "ktcpsocket_p.h" #include #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()); 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())) { 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/core/ktcpsocket.cpp b/src/core/ktcpsocket.cpp index 88c98fce..626a7946 100644 --- a/src/core/ktcpsocket.cpp +++ b/src/core/ktcpsocket.cpp @@ -1,1117 +1,1115 @@ /* This file is part of the KDE libraries Copyright (C) 2007, 2008 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 "ktcpsocket.h" #include "ktcpsocket_p.h" #include "kiocoredebug.h" #include #include -#include #include -#include #include #include #include #include #include static KTcpSocket::SslVersion kSslVersionFromQ(QSsl::SslProtocol protocol) { switch (protocol) { case QSsl::SslV2: return KTcpSocket::SslV2; case QSsl::SslV3: return KTcpSocket::SslV3; case QSsl::TlsV1_0: return KTcpSocket::TlsV1; case QSsl::TlsV1_1: return KTcpSocket::TlsV1_1; case QSsl::TlsV1_2: return KTcpSocket::TlsV1_2; #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) case QSsl::TlsV1_3: return KTcpSocket::TlsV1_3; #endif case QSsl::AnyProtocol: return KTcpSocket::AnySslVersion; case QSsl::TlsV1SslV3: return KTcpSocket::TlsV1SslV3; case QSsl::SecureProtocols: return KTcpSocket::SecureProtocols; default: return KTcpSocket::UnknownSslVersion; } } static QSsl::SslProtocol qSslProtocolFromK(KTcpSocket::SslVersion sslVersion) { //### this lowlevel bit-banging is a little dangerous and a likely source of bugs if (sslVersion == KTcpSocket::AnySslVersion) { return QSsl::AnyProtocol; } //does it contain any valid protocol? KTcpSocket::SslVersions validVersions(KTcpSocket::SslV2 | KTcpSocket::SslV3 | KTcpSocket::TlsV1); validVersions |= KTcpSocket::TlsV1_1; validVersions |= KTcpSocket::TlsV1_2; #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) validVersions |= KTcpSocket::TlsV1_3; #endif validVersions |= KTcpSocket::TlsV1SslV3; validVersions |= KTcpSocket::SecureProtocols; if (!(sslVersion & validVersions)) { return QSsl::UnknownProtocol; } switch (sslVersion) { case KTcpSocket::SslV2: return QSsl::SslV2; case KTcpSocket::SslV3: return QSsl::SslV3; case KTcpSocket::TlsV1_0: return QSsl::TlsV1_0; case KTcpSocket::TlsV1_1: return QSsl::TlsV1_1; case KTcpSocket::TlsV1_2: return QSsl::TlsV1_2; #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) case KTcpSocket::TlsV1_3: return QSsl::TlsV1_3; #endif case KTcpSocket::TlsV1SslV3: return QSsl::TlsV1SslV3; case KTcpSocket::SecureProtocols: return QSsl::SecureProtocols; default: //QSslSocket doesn't really take arbitrary combinations. It's one or all. return QSsl::AnyProtocol; } } static QString protocolString(QSsl::SslProtocol protocol) { switch (protocol) { case QSsl::SslV2: return QStringLiteral("SSLv2"); case QSsl::SslV3: return QStringLiteral("SSLv3"); case QSsl::TlsV1_0: return QStringLiteral("TLSv1.0"); case QSsl::TlsV1_1: return QStringLiteral("TLSv1.1"); case QSsl::TlsV1_2: return QStringLiteral("TLSv1.2"); #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) case QSsl::TlsV1_3: return QStringLiteral("TLSv1.3"); #endif default: return QStringLiteral("Unknown");; } } //cipher class converter KSslCipher -> QSslCipher class CipherCc { public: CipherCc() { foreach (const QSslCipher &c, QSslConfiguration::supportedCiphers()) { allCiphers.insert(c.name(), c); } } QSslCipher converted(const KSslCipher &ksc) { return allCiphers.value(ksc.name()); } private: QHash allCiphers; }; class KSslErrorPrivate { public: static KSslError::Error errorFromQSslError(QSslError::SslError e) { switch (e) { case QSslError::NoError: return KSslError::NoError; case QSslError::UnableToGetLocalIssuerCertificate: case QSslError::InvalidCaCertificate: return KSslError::InvalidCertificateAuthorityCertificate; case QSslError::InvalidNotBeforeField: case QSslError::InvalidNotAfterField: case QSslError::CertificateNotYetValid: case QSslError::CertificateExpired: return KSslError::ExpiredCertificate; case QSslError::UnableToDecodeIssuerPublicKey: case QSslError::SubjectIssuerMismatch: case QSslError::AuthorityIssuerSerialNumberMismatch: return KSslError::InvalidCertificate; case QSslError::SelfSignedCertificate: case QSslError::SelfSignedCertificateInChain: return KSslError::SelfSignedCertificate; case QSslError::CertificateRevoked: return KSslError::RevokedCertificate; case QSslError::InvalidPurpose: return KSslError::InvalidCertificatePurpose; case QSslError::CertificateUntrusted: return KSslError::UntrustedCertificate; case QSslError::CertificateRejected: return KSslError::RejectedCertificate; case QSslError::NoPeerCertificate: return KSslError::NoPeerCertificate; case QSslError::HostNameMismatch: return KSslError::HostNameMismatch; case QSslError::UnableToVerifyFirstCertificate: case QSslError::UnableToDecryptCertificateSignature: case QSslError::UnableToGetIssuerCertificate: case QSslError::CertificateSignatureFailed: return KSslError::CertificateSignatureFailed; case QSslError::PathLengthExceeded: return KSslError::PathLengthExceeded; case QSslError::UnspecifiedError: case QSslError::NoSslSupport: default: return KSslError::UnknownError; } } static QString errorString(KSslError::Error e) { switch (e) { case KSslError::NoError: return i18nc("SSL error", "No error"); case KSslError::InvalidCertificateAuthorityCertificate: return i18nc("SSL error", "The certificate authority's certificate is invalid"); case KSslError::ExpiredCertificate: return i18nc("SSL error", "The certificate has expired"); case KSslError::InvalidCertificate: return i18nc("SSL error", "The certificate is invalid"); case KSslError::SelfSignedCertificate: return i18nc("SSL error", "The certificate is not signed by any trusted certificate authority"); case KSslError::RevokedCertificate: return i18nc("SSL error", "The certificate has been revoked"); case KSslError::InvalidCertificatePurpose: return i18nc("SSL error", "The certificate is unsuitable for this purpose"); case KSslError::UntrustedCertificate: return i18nc("SSL error", "The root certificate authority's certificate is not trusted for this purpose"); case KSslError::RejectedCertificate: return i18nc("SSL error", "The certificate authority's certificate is marked to reject this certificate's purpose"); case KSslError::NoPeerCertificate: return i18nc("SSL error", "The peer did not present any certificate"); case KSslError::HostNameMismatch: return i18nc("SSL error", "The certificate does not apply to the given host"); case KSslError::CertificateSignatureFailed: return i18nc("SSL error", "The certificate cannot be verified for internal reasons"); case KSslError::PathLengthExceeded: return i18nc("SSL error", "The certificate chain is too long"); case KSslError::UnknownError: default: return i18nc("SSL error", "Unknown error"); } } KSslError::Error error; QSslCertificate certificate; }; KSslError::KSslError(Error errorCode, const QSslCertificate &certificate) : d(new KSslErrorPrivate()) { d->error = errorCode; d->certificate = certificate; } KSslError::KSslError(const QSslError &other) : d(new KSslErrorPrivate()) { d->error = KSslErrorPrivate::errorFromQSslError(other.error()); d->certificate = other.certificate(); } KSslError::KSslError(const KSslError &other) : d(new KSslErrorPrivate()) { *d = *other.d; } KSslError::~KSslError() { delete d; } KSslError &KSslError::operator=(const KSslError &other) { *d = *other.d; return *this; } KSslError::Error KSslError::error() const { return d->error; } QString KSslError::errorString() const { return KSslErrorPrivate::errorString(d->error); } QSslCertificate KSslError::certificate() const { return d->certificate; } class KTcpSocketPrivate { public: explicit KTcpSocketPrivate(KTcpSocket *qq) : q(qq), certificatesLoaded(false), emittedReadyRead(false) { // create the instance, which sets Qt's static internal cert set to empty. KSslCertificateManager::self(); } KTcpSocket::State state(QAbstractSocket::SocketState s) { switch (s) { case QAbstractSocket::UnconnectedState: return KTcpSocket::UnconnectedState; case QAbstractSocket::HostLookupState: return KTcpSocket::HostLookupState; case QAbstractSocket::ConnectingState: return KTcpSocket::ConnectingState; case QAbstractSocket::ConnectedState: return KTcpSocket::ConnectedState; case QAbstractSocket::ClosingState: return KTcpSocket::ClosingState; case QAbstractSocket::BoundState: case QAbstractSocket::ListeningState: //### these two are not relevant as long as this can't be a server socket default: return KTcpSocket::UnconnectedState; //the closest to "error" } } KTcpSocket::EncryptionMode encryptionMode(QSslSocket::SslMode mode) { switch (mode) { case QSslSocket::SslClientMode: return KTcpSocket::SslClientMode; case QSslSocket::SslServerMode: return KTcpSocket::SslServerMode; default: return KTcpSocket::UnencryptedMode; } } KTcpSocket::Error errorFromAbsSocket(QAbstractSocket::SocketError e) { switch (e) { case QAbstractSocket::ConnectionRefusedError: return KTcpSocket::ConnectionRefusedError; case QAbstractSocket::RemoteHostClosedError: return KTcpSocket::RemoteHostClosedError; case QAbstractSocket::HostNotFoundError: return KTcpSocket::HostNotFoundError; case QAbstractSocket::SocketAccessError: return KTcpSocket::SocketAccessError; case QAbstractSocket::SocketResourceError: return KTcpSocket::SocketResourceError; case QAbstractSocket::SocketTimeoutError: return KTcpSocket::SocketTimeoutError; case QAbstractSocket::NetworkError: return KTcpSocket::NetworkError; case QAbstractSocket::UnsupportedSocketOperationError: return KTcpSocket::UnsupportedSocketOperationError; case QAbstractSocket::SslHandshakeFailedError: return KTcpSocket::SslHandshakeFailedError; case QAbstractSocket::DatagramTooLargeError: //we don't do UDP case QAbstractSocket::AddressInUseError: case QAbstractSocket::SocketAddressNotAvailableError: //### own values if/when we ever get server socket support case QAbstractSocket::ProxyAuthenticationRequiredError: //### maybe we need an enum value for this case QAbstractSocket::UnknownSocketError: default: return KTcpSocket::UnknownError; } } //private slots void reemitSocketError(QAbstractSocket::SocketError e) { q->setErrorString(sock.errorString()); emit q->error(errorFromAbsSocket(e)); } void reemitSslErrors(const QList &errors) { q->setErrorString(sock.errorString()); q->showSslErrors(); //H4X QList kErrors; kErrors.reserve(errors.size()); for (const QSslError &e : errors) { kErrors.append(KSslError(e)); } emit q->sslErrors(kErrors); } void reemitStateChanged(QAbstractSocket::SocketState s) { emit q->stateChanged(state(s)); } void reemitModeChanged(QSslSocket::SslMode m) { emit q->encryptionModeChanged(encryptionMode(m)); } // This method is needed because we might emit readyRead() due to this QIODevice // having some data buffered, so we need to care about blocking, too. //### useless ATM as readyRead() now just calls d->sock.readyRead(). void reemitReadyRead() { if (!emittedReadyRead) { emittedReadyRead = true; emit q->readyRead(); emittedReadyRead = false; } } void maybeLoadCertificates() { if (!certificatesLoaded) { q->setCaCertificates(KSslCertificateManager::self()->caCertificates()); } } KTcpSocket *const q; bool certificatesLoaded; bool emittedReadyRead; QSslSocket sock; QList ciphers; KTcpSocket::SslVersion advertisedSslVersion; CipherCc ccc; }; KTcpSocket::KTcpSocket(QObject *parent) : QIODevice(parent), d(new KTcpSocketPrivate(this)) { d->advertisedSslVersion = SslV3; connect(&d->sock, &QIODevice::aboutToClose, this, &QIODevice::aboutToClose); connect(&d->sock, &QIODevice::bytesWritten, this, &QIODevice::bytesWritten); connect(&d->sock, &QSslSocket::encryptedBytesWritten, this, &KTcpSocket::encryptedBytesWritten); connect(&d->sock, SIGNAL(readyRead()), this, SLOT(reemitReadyRead())); connect(&d->sock, &QAbstractSocket::connected, this, &KTcpSocket::connected); connect(&d->sock, &QSslSocket::encrypted, this, &KTcpSocket::encrypted); connect(&d->sock, &QAbstractSocket::disconnected, this, &KTcpSocket::disconnected); #ifndef QT_NO_NETWORKPROXY connect(&d->sock, &QAbstractSocket::proxyAuthenticationRequired, this, &KTcpSocket::proxyAuthenticationRequired); #endif connect(&d->sock, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(reemitSocketError(QAbstractSocket::SocketError))); connect(&d->sock, SIGNAL(sslErrors(QList)), this, SLOT(reemitSslErrors(QList))); connect(&d->sock, &QAbstractSocket::hostFound, this, &KTcpSocket::hostFound); connect(&d->sock, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(reemitStateChanged(QAbstractSocket::SocketState))); connect(&d->sock, SIGNAL(modeChanged(QSslSocket::SslMode)), this, SLOT(reemitModeChanged(QSslSocket::SslMode))); } KTcpSocket::~KTcpSocket() { delete d; } ////////////////////////////// (mostly) virtuals from QIODevice bool KTcpSocket::atEnd() const { return d->sock.atEnd() && QIODevice::atEnd(); } qint64 KTcpSocket::bytesAvailable() const { return d->sock.bytesAvailable() + QIODevice::bytesAvailable(); } qint64 KTcpSocket::bytesToWrite() const { return d->sock.bytesToWrite(); } bool KTcpSocket::canReadLine() const { return d->sock.canReadLine() || QIODevice::canReadLine(); } void KTcpSocket::close() { d->sock.close(); QIODevice::close(); } bool KTcpSocket::isSequential() const { return true; } bool KTcpSocket::open(QIODevice::OpenMode open) { bool ret = d->sock.open(open); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); return ret; } bool KTcpSocket::waitForBytesWritten(int msecs) { return d->sock.waitForBytesWritten(msecs); } bool KTcpSocket::waitForReadyRead(int msecs) { return d->sock.waitForReadyRead(msecs); } qint64 KTcpSocket::readData(char *data, qint64 maxSize) { return d->sock.read(data, maxSize); } qint64 KTcpSocket::writeData(const char *data, qint64 maxSize) { return d->sock.write(data, maxSize); } ////////////////////////////// public methods from QAbstractSocket void KTcpSocket::abort() { d->sock.abort(); } void KTcpSocket::connectToHost(const QString &hostName, quint16 port, ProxyPolicy policy) { if (policy == AutoProxy) { //### } d->sock.connectToHost(hostName, port); // there are enough layers of buffers between us and the network, and there is a quirk // in QIODevice that can make it try to readData() twice per read() call if buffered and // reaData() does not deliver enough data the first time. like when the other side is // simply not sending any more data... // this can *apparently* lead to long delays sometimes which stalls applications. // do not want. setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } void KTcpSocket::connectToHost(const QHostAddress &hostAddress, quint16 port, ProxyPolicy policy) { if (policy == AutoProxy) { //### } d->sock.connectToHost(hostAddress, port); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } void KTcpSocket::connectToHost(const QUrl &url, ProxyPolicy policy) { if (policy == AutoProxy) { //### } d->sock.connectToHost(url.host(), url.port()); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } void KTcpSocket::disconnectFromHost() { d->sock.disconnectFromHost(); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } KTcpSocket::Error KTcpSocket::error() const { return d->errorFromAbsSocket(d->sock.error()); } QList KTcpSocket::sslErrors() const { //### pretty slow; also consider throwing out duplicate error codes. We may get // duplicates even though there were none in the original list because KSslError // has a smallest common denominator range of SSL error codes. const auto qsslErrors = d->sock.sslErrors(); QList ret; ret.reserve(qsslErrors.size()); for (const QSslError &e : qsslErrors) { ret.append(KSslError(e)); } return ret; } bool KTcpSocket::flush() { return d->sock.flush(); } bool KTcpSocket::isValid() const { return d->sock.isValid(); } QHostAddress KTcpSocket::localAddress() const { return d->sock.localAddress(); } QHostAddress KTcpSocket::peerAddress() const { return d->sock.peerAddress(); } QString KTcpSocket::peerName() const { return d->sock.peerName(); } quint16 KTcpSocket::peerPort() const { return d->sock.peerPort(); } #ifndef QT_NO_NETWORKPROXY QNetworkProxy KTcpSocket::proxy() const { return d->sock.proxy(); } #endif qint64 KTcpSocket::readBufferSize() const { return d->sock.readBufferSize(); } #ifndef QT_NO_NETWORKPROXY void KTcpSocket::setProxy(const QNetworkProxy &proxy) { d->sock.setProxy(proxy); } #endif void KTcpSocket::setReadBufferSize(qint64 size) { d->sock.setReadBufferSize(size); } KTcpSocket::State KTcpSocket::state() const { return d->state(d->sock.state()); } bool KTcpSocket::waitForConnected(int msecs) { bool ret = d->sock.waitForConnected(msecs); if (!ret) { setErrorString(d->sock.errorString()); } setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); return ret; } bool KTcpSocket::waitForDisconnected(int msecs) { bool ret = d->sock.waitForDisconnected(msecs); if (!ret) { setErrorString(d->sock.errorString()); } setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); return ret; } ////////////////////////////// public methods from QSslSocket void KTcpSocket::addCaCertificate(const QSslCertificate &certificate) { d->maybeLoadCertificates(); d->sock.addCaCertificate(certificate); } /* bool KTcpSocket::addCaCertificates(const QString &path, QSsl::EncodingFormat format, QRegExp::PatternSyntax syntax) { d->maybeLoadCertificates(); return d->sock.addCaCertificates(path, format, syntax); } */ void KTcpSocket::addCaCertificates(const QList &certificates) { d->maybeLoadCertificates(); d->sock.addCaCertificates(certificates); } QList KTcpSocket::caCertificates() const { d->maybeLoadCertificates(); return d->sock.sslConfiguration().caCertificates(); } QList KTcpSocket::ciphers() const { return d->ciphers; } void KTcpSocket::connectToHostEncrypted(const QString &hostName, quint16 port, OpenMode openMode) { d->maybeLoadCertificates(); d->sock.setProtocol(qSslProtocolFromK(d->advertisedSslVersion)); d->sock.connectToHostEncrypted(hostName, port, openMode); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } QSslCertificate KTcpSocket::localCertificate() const { return d->sock.localCertificate(); } QList KTcpSocket::peerCertificateChain() const { return d->sock.peerCertificateChain(); } KSslKey KTcpSocket::privateKey() const { return KSslKey(d->sock.privateKey()); } KSslCipher KTcpSocket::sessionCipher() const { return KSslCipher(d->sock.sessionCipher()); } void KTcpSocket::setCaCertificates(const QList &certificates) { QSslConfiguration configuration = d->sock.sslConfiguration(); configuration.setCaCertificates(certificates); d->sock.setSslConfiguration(configuration); d->certificatesLoaded = true; } void KTcpSocket::setCiphers(const QList &ciphers) { d->ciphers = ciphers; QList cl; cl.reserve(d->ciphers.size()); foreach (const KSslCipher &c, d->ciphers) { cl.append(d->ccc.converted(c)); } QSslConfiguration configuration = d->sock.sslConfiguration(); configuration.setCiphers(cl); d->sock.setSslConfiguration(configuration); } void KTcpSocket::setLocalCertificate(const QSslCertificate &certificate) { d->sock.setLocalCertificate(certificate); } void KTcpSocket::setLocalCertificate(const QString &fileName, QSsl::EncodingFormat format) { d->sock.setLocalCertificate(fileName, format); } void KTcpSocket::setVerificationPeerName(const QString &hostName) { d->sock.setPeerVerifyName(hostName); } void KTcpSocket::setPrivateKey(const KSslKey &key) { // We cannot map KSslKey::Algorithm:Dh to anything in QSsl::KeyAlgorithm. if (key.algorithm() == KSslKey::Dh) { return; } QSslKey _key(key.toDer(), (key.algorithm() == KSslKey::Rsa) ? QSsl::Rsa : QSsl::Dsa, QSsl::Der, (key.secrecy() == KSslKey::PrivateKey) ? QSsl::PrivateKey : QSsl::PublicKey); d->sock.setPrivateKey(_key); } void KTcpSocket::setPrivateKey(const QString &fileName, KSslKey::Algorithm algorithm, QSsl::EncodingFormat format, const QByteArray &passPhrase) { // We cannot map KSslKey::Algorithm:Dh to anything in QSsl::KeyAlgorithm. if (algorithm == KSslKey::Dh) { return; } d->sock.setPrivateKey(fileName, (algorithm == KSslKey::Rsa) ? QSsl::Rsa : QSsl::Dsa, format, passPhrase); } bool KTcpSocket::waitForEncrypted(int msecs) { return d->sock.waitForEncrypted(msecs); } KTcpSocket::EncryptionMode KTcpSocket::encryptionMode() const { return d->encryptionMode(d->sock.mode()); } QVariant KTcpSocket::socketOption(QAbstractSocket::SocketOption options) const { return d->sock.socketOption(options); } void KTcpSocket::setSocketOption(QAbstractSocket::SocketOption options, const QVariant &value) { d->sock.setSocketOption(options, value); } QSslConfiguration KTcpSocket::sslConfiguration() const { return d->sock.sslConfiguration(); } void KTcpSocket::setSslConfiguration(const QSslConfiguration &configuration) { d->sock.setSslConfiguration(configuration); } //slot void KTcpSocket::ignoreSslErrors() { d->sock.ignoreSslErrors(); } //slot void KTcpSocket::startClientEncryption() { d->maybeLoadCertificates(); d->sock.setProtocol(qSslProtocolFromK(d->advertisedSslVersion)); d->sock.startClientEncryption(); } //debugging H4X void KTcpSocket::showSslErrors() { foreach (const QSslError &e, d->sock.sslErrors()) { qCDebug(KIO_CORE) << e.errorString(); } } void KTcpSocket::setAdvertisedSslVersion(KTcpSocket::SslVersion version) { d->advertisedSslVersion = version; } KTcpSocket::SslVersion KTcpSocket::advertisedSslVersion() const { return d->advertisedSslVersion; } KTcpSocket::SslVersion KTcpSocket::negotiatedSslVersion() const { if (!d->sock.isEncrypted()) { return UnknownSslVersion; } return kSslVersionFromQ(d->sock.sessionProtocol()); } QString KTcpSocket::negotiatedSslVersionName() const { if (!d->sock.isEncrypted()) { return QString(); } return protocolString(d->sock.sessionProtocol()); } ////////////////////////////// KSslKey class KSslKeyPrivate { public: KSslKey::Algorithm convertAlgorithm(QSsl::KeyAlgorithm a) { switch (a) { case QSsl::Dsa: return KSslKey::Dsa; default: return KSslKey::Rsa; } } KSslKey::Algorithm algorithm; KSslKey::KeySecrecy secrecy; bool isExportable; QByteArray der; }; KSslKey::KSslKey() : d(new KSslKeyPrivate) { d->algorithm = Rsa; d->secrecy = PublicKey; d->isExportable = true; } KSslKey::KSslKey(const KSslKey &other) : d(new KSslKeyPrivate) { *d = *other.d; } KSslKey::KSslKey(const QSslKey &qsk) : d(new KSslKeyPrivate) { d->algorithm = d->convertAlgorithm(qsk.algorithm()); d->secrecy = (qsk.type() == QSsl::PrivateKey) ? PrivateKey : PublicKey; d->isExportable = true; d->der = qsk.toDer(); } KSslKey::~KSslKey() { delete d; } KSslKey &KSslKey::operator=(const KSslKey &other) { *d = *other.d; return *this; } KSslKey::Algorithm KSslKey::algorithm() const { return d->algorithm; } bool KSslKey::isExportable() const { return d->isExportable; } KSslKey::KeySecrecy KSslKey::secrecy() const { return d->secrecy; } QByteArray KSslKey::toDer() const { return d->der; } ////////////////////////////// KSslCipher //nice-to-have: make implicitly shared class KSslCipherPrivate { public: QString authenticationMethod; QString encryptionMethod; QString keyExchangeMethod; QString name; bool isNull; int supportedBits; int usedBits; }; KSslCipher::KSslCipher() : d(new KSslCipherPrivate) { d->isNull = true; d->supportedBits = 0; d->usedBits = 0; } KSslCipher::KSslCipher(const KSslCipher &other) : d(new KSslCipherPrivate) { *d = *other.d; } KSslCipher::KSslCipher(const QSslCipher &qsc) : d(new KSslCipherPrivate) { d->authenticationMethod = qsc.authenticationMethod(); d->encryptionMethod = qsc.encryptionMethod(); //Qt likes to append the number of bits (usedBits?) to the algorithm, //for example "AES(256)". We only want the pure algorithm name, though. int parenIdx = d->encryptionMethod.indexOf(QLatin1Char('(')); if (parenIdx > 0) { d->encryptionMethod.truncate(parenIdx); } d->keyExchangeMethod = qsc.keyExchangeMethod(); d->name = qsc.name(); d->isNull = qsc.isNull(); d->supportedBits = qsc.supportedBits(); d->usedBits = qsc.usedBits(); } KSslCipher::~KSslCipher() { delete d; } KSslCipher &KSslCipher::operator=(const KSslCipher &other) { *d = *other.d; return *this; } bool KSslCipher::isNull() const { return d->isNull; } QString KSslCipher::authenticationMethod() const { return d->authenticationMethod; } QString KSslCipher::encryptionMethod() const { return d->encryptionMethod; } QString KSslCipher::keyExchangeMethod() const { return d->keyExchangeMethod; } QString KSslCipher::digestMethod() const { //### This is not really backend neutral. It works for OpenSSL and // for RFC compliant names, though. if (d->name.endsWith(QLatin1String("SHA"))) { return QStringLiteral("SHA-1"); } else if (d->name.endsWith(QLatin1String("MD5"))) { return QStringLiteral("MD5"); } else { return QString(); } } QString KSslCipher::name() const { return d->name; } int KSslCipher::supportedBits() const { return d->supportedBits; } int KSslCipher::usedBits() const { return d->usedBits; } //static QList KSslCipher::supportedCiphers() { QList ret; const QList candidates = QSslConfiguration::supportedCiphers(); ret.reserve(candidates.size()); for (const QSslCipher &c : candidates) { ret.append(KSslCipher(c)); } return ret; } KSslErrorUiData::KSslErrorUiData() : d(new Private()) { d->usedBits = 0; d->bits = 0; } KSslErrorUiData::KSslErrorUiData(const KTcpSocket *socket) : d(new Private()) { d->certificateChain = socket->peerCertificateChain(); d->sslErrors = socket->sslErrors(); d->ip = socket->peerAddress().toString(); d->host = socket->peerName(); d->sslProtocol = socket->negotiatedSslVersionName(); d->cipher = socket->sessionCipher().name(); d->usedBits = socket->sessionCipher().usedBits(); d->bits = socket->sessionCipher().supportedBits(); } KSslErrorUiData::KSslErrorUiData(const QSslSocket *socket) : d(new Private()) { d->certificateChain = socket->peerCertificateChain(); // See KTcpSocket::sslErrors() const auto qsslErrors = socket->sslErrors(); d->sslErrors.reserve(qsslErrors.size()); for (const QSslError &e : qsslErrors) { d->sslErrors.append(KSslError(e)); } d->ip = socket->peerAddress().toString(); d->host = socket->peerName(); if (socket->isEncrypted()) { d->sslProtocol = socket->sessionCipher().protocolString(); } d->cipher = socket->sessionCipher().name(); d->usedBits = socket->sessionCipher().usedBits(); d->bits = socket->sessionCipher().supportedBits(); } KSslErrorUiData::KSslErrorUiData(const KSslErrorUiData &other) : d(new Private(*other.d)) {} KSslErrorUiData::~KSslErrorUiData() { delete d; } KSslErrorUiData &KSslErrorUiData::operator=(const KSslErrorUiData &other) { *d = *other.d; return *this; } #include "moc_ktcpsocket.cpp" diff --git a/src/core/listjob.cpp b/src/core/listjob.cpp index 25d482d0..e3d21b29 100644 --- a/src/core/listjob.cpp +++ b/src/core/listjob.cpp @@ -1,304 +1,305 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow 2000-2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 "listjob.h" #include "job_p.h" #include "scheduler.h" #include #include "slave.h" #include "../pathhelpers_p.h" +#include #include using namespace KIO; class KIO::ListJobPrivate: public KIO::SimpleJobPrivate { public: ListJobPrivate(const QUrl &url, bool _recursive, const QString &prefix, const QString &displayPrefix, bool _includeHidden) : SimpleJobPrivate(url, CMD_LISTDIR, QByteArray()), recursive(_recursive), includeHidden(_includeHidden), m_prefix(prefix), m_displayPrefix(displayPrefix), m_processedEntries(0) {} bool recursive; bool includeHidden; QString m_prefix; QString m_displayPrefix; unsigned long m_processedEntries; QUrl m_redirectionURL; /** * @internal * Called by the scheduler when a @p slave gets to * work on this job. * @param slave the slave that starts working on this job */ void start(Slave *slave) override; void slotListEntries(const KIO::UDSEntryList &list); void slotRedirection(const QUrl &url); void gotEntries(KIO::Job *subjob, const KIO::UDSEntryList &list); void slotSubError(ListJob* job, ListJob* subJob); Q_DECLARE_PUBLIC(ListJob) static inline ListJob *newJob(const QUrl &u, bool _recursive, const QString &prefix, const QString &displayPrefix, bool _includeHidden, JobFlags flags = HideProgressInfo) { ListJob *job = new ListJob(*new ListJobPrivate(u, _recursive, prefix, displayPrefix, _includeHidden)); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); if (!(flags & HideProgressInfo)) { KIO::getJobTracker()->registerJob(job); } return job; } static inline ListJob *newJobNoUi(const QUrl &u, bool _recursive, const QString &prefix, const QString &displayPrefix, bool _includeHidden) { return new ListJob(*new ListJobPrivate(u, _recursive, prefix, displayPrefix, _includeHidden)); } }; ListJob::ListJob(ListJobPrivate &dd) : SimpleJob(dd) { Q_D(ListJob); // We couldn't set the args when calling the parent constructor, // so do it now. QDataStream stream(&d->m_packedArgs, QIODevice::WriteOnly); stream << d->m_url; } ListJob::~ListJob() { } void ListJobPrivate::slotListEntries(const KIO::UDSEntryList &list) { Q_Q(ListJob); // Emit progress info (takes care of emit processedSize and percent) m_processedEntries += list.count(); slotProcessedSize(m_processedEntries); if (recursive) { UDSEntryList::ConstIterator it = list.begin(); const UDSEntryList::ConstIterator end = list.end(); for (; it != end; ++it) { const UDSEntry &entry = *it; QUrl itemURL; const QString udsUrl = entry.stringValue(KIO::UDSEntry::UDS_URL); QString filename; if (!udsUrl.isEmpty()) { itemURL = QUrl(udsUrl); filename = itemURL.fileName(); } else { // no URL, use the name itemURL = q->url(); filename = entry.stringValue(KIO::UDSEntry::UDS_NAME); Q_ASSERT(!filename.isEmpty()); // we'll recurse forever otherwise :) itemURL.setPath(concatPaths(itemURL.path(), filename)); } if (entry.isDir() && !entry.isLink()) { Q_ASSERT(!filename.isEmpty()); QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); if (displayName.isEmpty()) { displayName = filename; } // skip hidden dirs when listing if requested if (filename != QLatin1String("..") && filename != QLatin1String(".") && (includeHidden || filename[0] != QLatin1Char('.'))) { ListJob *job = ListJobPrivate::newJobNoUi(itemURL, true /*recursive*/, m_prefix + filename + QLatin1Char('/'), m_displayPrefix + displayName + QLatin1Char('/'), includeHidden); Scheduler::setJobPriority(job, 1); QObject::connect(job, &ListJob::entries, q, [this](KIO::Job *job, const KIO::UDSEntryList &list) {gotEntries(job, list);} ); QObject::connect(job, &ListJob::subError, q, [this](KIO::ListJob *job, KIO::ListJob *ljob) {slotSubError(job, ljob);} ); q->addSubjob(job); } } } } // Not recursive, or top-level of recursive listing : return now (send . and .. as well) // exclusion of hidden files also requires the full sweep, but the case for full-listing // a single dir is probably common enough to justify the shortcut if (m_prefix.isNull() && includeHidden) { emit q->entries(q, list); } else { // cull the unwanted hidden dirs and/or parent dir references from the listing, then emit that UDSEntryList newlist; UDSEntryList::const_iterator it = list.begin(); const UDSEntryList::const_iterator end = list.end(); for (; it != end; ++it) { // Modify the name in the UDSEntry UDSEntry newone = *it; const QString filename = newone.stringValue(KIO::UDSEntry::UDS_NAME); QString displayName = newone.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); if (displayName.isEmpty()) { displayName = filename; } // Avoid returning entries like subdir/. and subdir/.., but include . and .. for // the toplevel dir, and skip hidden files/dirs if that was requested if ((m_prefix.isNull() || (filename != QLatin1String("..") && filename != QLatin1String("."))) && (includeHidden || (filename[0] != QLatin1Char('.')))) { // ## Didn't find a way to use the iterator instead of re-doing a key lookup newone.replace(KIO::UDSEntry::UDS_NAME, m_prefix + filename); newone.replace(KIO::UDSEntry::UDS_DISPLAY_NAME, m_displayPrefix + displayName); newlist.append(newone); } } emit q->entries(q, newlist); } } void ListJobPrivate::gotEntries(KIO::Job *, const KIO::UDSEntryList &list) { // Forward entries received by subjob - faking we received them ourselves Q_Q(ListJob); emit q->entries(q, list); } void ListJobPrivate::slotSubError(KIO::ListJob* /*job*/, KIO::ListJob* subJob) { Q_Q(ListJob); emit q->subError(q, subJob); // Let the signal of subError go up } void ListJob::slotResult(KJob *job) { Q_D(ListJob); if (job->error()) { // If we can't list a subdir, the result is still ok // This is why we override KCompositeJob::slotResult - to not set // an error on parent job. // Let's emit a signal about this though emit subError(this, static_cast(job)); } removeSubjob(job); if (!hasSubjobs() && !d->m_slave) { // if the main directory listing is still running, it will emit result in SimpleJob::slotFinished() emitResult(); } } void ListJobPrivate::slotRedirection(const QUrl &url) { Q_Q(ListJob); if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("redirect"), m_url, url)) { qCWarning(KIO_CORE) << "Redirection from" << m_url << "to" << url << "REJECTED!"; return; } m_redirectionURL = url; // We'll remember that when the job finishes emit q->redirection(q, m_redirectionURL); } void ListJob::slotFinished() { Q_D(ListJob); if (!d->m_redirectionURL.isEmpty() && d->m_redirectionURL.isValid() && !error()) { //qDebug() << "Redirection to " << d->m_redirectionURL; if (queryMetaData(QStringLiteral("permanent-redirect")) == QLatin1String("true")) { emit permanentRedirection(this, d->m_url, d->m_redirectionURL); } if (d->m_redirectionHandlingEnabled) { d->m_packedArgs.truncate(0); QDataStream stream(&d->m_packedArgs, QIODevice::WriteOnly); stream << d->m_redirectionURL; d->restartAfterRedirection(&d->m_redirectionURL); return; } } // Return slave to the scheduler SimpleJob::slotFinished(); } void ListJob::slotMetaData(const KIO::MetaData &_metaData) { Q_D(ListJob); SimpleJob::slotMetaData(_metaData); storeSSLSessionFromJob(d->m_redirectionURL); } ListJob *KIO::listDir(const QUrl &url, JobFlags flags, bool includeHidden) { return ListJobPrivate::newJob(url, false, QString(), QString(), includeHidden, flags); } ListJob *KIO::listRecursive(const QUrl &url, JobFlags flags, bool includeHidden) { return ListJobPrivate::newJob(url, true, QString(), QString(), includeHidden, flags); } void ListJob::setUnrestricted(bool unrestricted) { Q_D(ListJob); if (unrestricted) { d->m_extraFlags |= JobPrivate::EF_ListJobUnrestricted; } else { d->m_extraFlags &= ~JobPrivate::EF_ListJobUnrestricted; } } void ListJobPrivate::start(Slave *slave) { Q_Q(ListJob); if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("list"), m_url, m_url) && !(m_extraFlags & EF_ListJobUnrestricted)) { q->setError(ERR_ACCESS_DENIED); q->setErrorText(m_url.toDisplayString()); QTimer::singleShot(0, q, &ListJob::slotFinished); return; } QObject::connect(slave, &Slave::listEntries, q, [this](const KIO::UDSEntryList &list){ slotListEntries(list);} ); QObject::connect(slave, &Slave::totalSize, q, [this](KIO::filesize_t size){ slotTotalSize(size);} ); QObject::connect(slave, &Slave::redirection, q, [this](const QUrl &url){ slotRedirection(url);} ); SimpleJobPrivate::start(slave); } const QUrl &ListJob::redirectionUrl() const { return d_func()->m_redirectionURL; } #include "moc_listjob.cpp" diff --git a/src/core/mkpathjob.cpp b/src/core/mkpathjob.cpp index 500915d6..07528483 100644 --- a/src/core/mkpathjob.cpp +++ b/src/core/mkpathjob.cpp @@ -1,159 +1,157 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow 2000-2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 "mkpathjob.h" #include "job_p.h" #include "mkdirjob.h" #include "../pathhelpers_p.h" #include -#include -#include #include using namespace KIO; class KIO::MkpathJobPrivate : public KIO::JobPrivate { public: MkpathJobPrivate(const QUrl &url, const QUrl &baseUrl, JobFlags flags) : JobPrivate(), m_url(url), m_pathComponents(url.path().split(QLatin1Char('/'), QString::SkipEmptyParts)), m_pathIterator(), m_flags(flags) { const QStringList basePathComponents = baseUrl.path().split(QLatin1Char('/'), QString::SkipEmptyParts); m_url.setPath(QStringLiteral("/")); int i = 0; for (; i < basePathComponents.count() && i < m_pathComponents.count(); ++i) { const QString pathComponent = m_pathComponents.at(i); if (pathComponent == basePathComponents.at(i)) { m_url.setPath(concatPaths(m_url.path(), pathComponent)); } else { break; } } if (i > 0) { m_pathComponents.erase(m_pathComponents.begin(), m_pathComponents.begin() + i); } // fast path for local files using QFileInfo::isDir if (m_url.isLocalFile()) { i = 0; for (; i < m_pathComponents.count(); ++i) { const QString localFile = m_url.toLocalFile(); QString testDir; if (localFile == QLatin1String("/")) { testDir = localFile + m_pathComponents.at(i); } else { testDir = localFile + QLatin1Char('/') + m_pathComponents.at(i); } if (QFileInfo(testDir).isDir()) { m_url.setPath(testDir); } else { break; } } if (i > 0) { m_pathComponents.erase(m_pathComponents.begin(), m_pathComponents.begin() + i); } } m_pathIterator = m_pathComponents.constBegin(); } QUrl m_url; QUrl m_baseUrl; QStringList m_pathComponents; QStringList::const_iterator m_pathIterator; const JobFlags m_flags; Q_DECLARE_PUBLIC(MkpathJob) void slotStart(); static inline MkpathJob *newJob(const QUrl &url, const QUrl &baseUrl, JobFlags flags) { MkpathJob *job = new MkpathJob(*new MkpathJobPrivate(url, baseUrl, flags)); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); if (!(flags & HideProgressInfo)) { KIO::getJobTracker()->registerJob(job); } if (!(flags & NoPrivilegeExecution)) { job->d_func()->m_privilegeExecutionEnabled = true; job->d_func()->m_operationType = MkDir; } return job; } }; MkpathJob::MkpathJob(MkpathJobPrivate &dd) : Job(dd) { QTimer::singleShot(0, this, SLOT(slotStart())); } MkpathJob::~MkpathJob() { } void MkpathJobPrivate::slotStart() { Q_Q(MkpathJob); if (m_pathIterator == m_pathComponents.constBegin()) { // first time: emit total q->setTotalAmount(KJob::Directories, m_pathComponents.count()); } if (m_pathIterator != m_pathComponents.constEnd()) { m_url.setPath(concatPaths(m_url.path(), *m_pathIterator)); KIO::Job* job = KIO::mkdir(m_url); job->setParentJob(q); q->addSubjob(job); q->setProcessedAmount(KJob::Directories, q->processedAmount(KJob::Directories) + 1); } else { q->emitResult(); } } void MkpathJob::slotResult(KJob *job) { Q_D(MkpathJob); if (job->error() && job->error() != KIO::ERR_DIR_ALREADY_EXIST) { KIO::Job::slotResult(job); // will set the error and emit result(this) return; } removeSubjob(job); emit directoryCreated(d->m_url); // Move on to next one ++d->m_pathIterator; emitPercent(d->m_pathIterator - d->m_pathComponents.constBegin(), d->m_pathComponents.count()); d->slotStart(); } MkpathJob * KIO::mkpath(const QUrl &url, const QUrl &baseUrl, KIO::JobFlags flags) { return MkpathJobPrivate::newJob(url, baseUrl, flags); } #include "moc_mkpathjob.cpp" diff --git a/src/core/multigetjob.cpp b/src/core/multigetjob.cpp index 39ef2d86..93d65b3d 100644 --- a/src/core/multigetjob.cpp +++ b/src/core/multigetjob.cpp @@ -1,249 +1,248 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow 2000-2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 "multigetjob.h" #include "job_p.h" #include "scheduler.h" #include "slave.h" #include #include -#include using namespace KIO; class KIO::MultiGetJobPrivate: public KIO::TransferJobPrivate { public: explicit MultiGetJobPrivate(const QUrl &url) : TransferJobPrivate(url, 0, QByteArray(), QByteArray()), m_currentEntry(0, QUrl(), MetaData()) {} struct GetRequest { GetRequest(long _id, const QUrl &_url, const MetaData &_metaData) : id(_id), url(_url), metaData(_metaData) { } long id; QUrl url; MetaData metaData; inline bool operator==(const GetRequest &req) const { return req.id == id; } }; typedef QLinkedList RequestQueue; RequestQueue m_waitQueue; RequestQueue m_activeQueue; GetRequest m_currentEntry; bool b_multiGetActive; /** * @internal * Called by the scheduler when a @p slave gets to * work on this job. * @param slave the slave that starts working on this job */ void start(Slave *slave) override; bool findCurrentEntry(); void flushQueue(QLinkedList &queue); Q_DECLARE_PUBLIC(MultiGetJob) static inline MultiGetJob *newJob(const QUrl &url) { MultiGetJob *job = new MultiGetJob(*new MultiGetJobPrivate(url)); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); return job; } }; MultiGetJob::MultiGetJob(MultiGetJobPrivate &dd) : TransferJob(dd) { } MultiGetJob::~MultiGetJob() { } void MultiGetJob::get(long id, const QUrl &url, const MetaData &metaData) { Q_D(MultiGetJob); MultiGetJobPrivate::GetRequest entry(id, url, metaData); entry.metaData[QStringLiteral("request-id")] = QString::number(id); d->m_waitQueue.append(entry); } void MultiGetJobPrivate::flushQueue(RequestQueue &queue) { // Use multi-get // Scan all jobs in m_waitQueue RequestQueue::iterator wqit = m_waitQueue.begin(); const RequestQueue::iterator wqend = m_waitQueue.end(); while (wqit != wqend) { const GetRequest &entry = *wqit; if ((m_url.scheme() == entry.url.scheme()) && (m_url.host() == entry.url.host()) && (m_url.port() == entry.url.port()) && (m_url.userName() == entry.url.userName())) { queue.append(entry); wqit = m_waitQueue.erase(wqit); } else { ++wqit; } } // Send number of URLs, (URL, metadata)* KIO_ARGS << (qint32) queue.count(); RequestQueue::const_iterator qit = queue.constBegin(); const RequestQueue::const_iterator qend = queue.constEnd(); for (; qit != qend; ++qit) { stream << (*qit).url << (*qit).metaData; } m_packedArgs = packedArgs; m_command = CMD_MULTI_GET; m_outgoingMetaData.clear(); } void MultiGetJobPrivate::start(Slave *slave) { // Add first job from m_waitQueue and add it to m_activeQueue GetRequest entry = m_waitQueue.takeFirst(); m_activeQueue.append(entry); m_url = entry.url; if (!entry.url.scheme().startsWith(QLatin1String("http"))) { // Use normal get KIO_ARGS << entry.url; m_packedArgs = packedArgs; m_outgoingMetaData = entry.metaData; m_command = CMD_GET; b_multiGetActive = false; } else { flushQueue(m_activeQueue); b_multiGetActive = true; } TransferJobPrivate::start(slave); // Anything else to do?? } bool MultiGetJobPrivate::findCurrentEntry() { if (b_multiGetActive) { long id = m_incomingMetaData[QStringLiteral("request-id")].toLong(); RequestQueue::const_iterator qit = m_activeQueue.constBegin(); const RequestQueue::const_iterator qend = m_activeQueue.constEnd(); for (; qit != qend; ++qit) { if ((*qit).id == id) { m_currentEntry = *qit; return true; } } m_currentEntry.id = 0; return false; } else { if (m_activeQueue.isEmpty()) { return false; } m_currentEntry = m_activeQueue.first(); return true; } } void MultiGetJob::slotRedirection(const QUrl &url) { Q_D(MultiGetJob); if (!d->findCurrentEntry()) { return; // Error } if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("redirect"), d->m_url, url)) { qCWarning(KIO_CORE) << "Redirection from" << d->m_currentEntry.url << "to" << url << "REJECTED!"; return; } d->m_redirectionURL = url; get(d->m_currentEntry.id, d->m_redirectionURL, d->m_currentEntry.metaData); // Try again } void MultiGetJob::slotFinished() { Q_D(MultiGetJob); if (!d->findCurrentEntry()) { return; } if (d->m_redirectionURL.isEmpty()) { // No redirection, tell the world that we are finished. emit result(d->m_currentEntry.id); } d->m_redirectionURL = QUrl(); setError(0); d->m_incomingMetaData.clear(); d->m_activeQueue.removeAll(d->m_currentEntry); if (d->m_activeQueue.count() == 0) { if (d->m_waitQueue.count() == 0) { // All done TransferJob::slotFinished(); } else { // return slave to pool // fetch new slave for first entry in d->m_waitQueue and call start // again. d->slaveDone(); d->m_url = d->m_waitQueue.first().url; if ((d->m_extraFlags & JobPrivate::EF_KillCalled) == 0) { Scheduler::doJob(this); } } } } void MultiGetJob::slotData(const QByteArray &_data) { Q_D(MultiGetJob); if (d->m_redirectionURL.isEmpty() || !d->m_redirectionURL.isValid() || error()) { emit data(d->m_currentEntry.id, _data); } } void MultiGetJob::slotMimetype(const QString &_mimetype) { Q_D(MultiGetJob); if (d->b_multiGetActive) { MultiGetJobPrivate::RequestQueue newQueue; d->flushQueue(newQueue); if (!newQueue.isEmpty()) { d->m_activeQueue += newQueue; d->m_slave->send(d->m_command, d->m_packedArgs); } } if (!d->findCurrentEntry()) { return; // Error, unknown request! } emit mimetype(d->m_currentEntry.id, _mimetype); } MultiGetJob *KIO::multi_get(long id, const QUrl &url, const MetaData &metaData) { MultiGetJob *job = MultiGetJobPrivate::newJob(url); job->get(id, url, metaData); return job; } #include "moc_multigetjob.cpp" diff --git a/src/core/restorejob.cpp b/src/core/restorejob.cpp index 4f521ccc..22d1cae1 100644 --- a/src/core/restorejob.cpp +++ b/src/core/restorejob.cpp @@ -1,117 +1,116 @@ /* This file is part of the KDE libraries Copyright 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 "restorejob.h" #include "kiocoredebug.h" #include "job_p.h" #include -#include #include using namespace KIO; class KIO::RestoreJobPrivate : public KIO::JobPrivate { public: RestoreJobPrivate(const QList &urls, JobFlags flags) : JobPrivate(), m_urls(urls), m_urlsIterator(m_urls.constBegin()), m_progress(0), m_flags(flags) { } QList m_urls; QList::const_iterator m_urlsIterator; int m_progress; JobFlags m_flags; void slotStart(); Q_DECLARE_PUBLIC(RestoreJob) static inline RestoreJob *newJob(const QList &urls, JobFlags flags) { RestoreJob *job = new RestoreJob(*new RestoreJobPrivate(urls, flags)); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); if (!(flags & HideProgressInfo)) { KIO::getJobTracker()->registerJob(job); } return job; } }; RestoreJob::RestoreJob(RestoreJobPrivate &dd) : Job(dd) { QTimer::singleShot(0, this, SLOT(slotStart())); } RestoreJob::~RestoreJob() { } void RestoreJobPrivate::slotStart() { Q_Q(RestoreJob); if (m_urlsIterator == m_urls.constBegin()) { // first time: emit total q->setTotalAmount(KJob::Files, m_urls.count()); } if (m_urlsIterator != m_urls.constEnd()) { const QUrl& url = *m_urlsIterator; Q_ASSERT(url.scheme() == QLatin1String("trash")); QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << int(3) << url; KIO::Job* job = KIO::special(url, packedArgs, m_flags); q->addSubjob(job); q->setProcessedAmount(KJob::Files, q->processedAmount(KJob::Files) + 1); } else { org::kde::KDirNotify::emitFilesRemoved(m_urls); q->emitResult(); } } void RestoreJob::slotResult(KJob *job) { Q_D(RestoreJob); if (job->error()) { qCDebug(KIO_CORE) << job->errorString(); KIO::Job::slotResult(job); // will set the error and emit result(this) return; } removeSubjob(job); // Move on to next one ++d->m_urlsIterator; ++d->m_progress; emitPercent(d->m_progress, d->m_urls.count()); d->slotStart(); } RestoreJob *KIO::restoreFromTrash(const QList &urls, JobFlags flags) { return RestoreJobPrivate::newJob(urls, flags); } #include "moc_restorejob.cpp" diff --git a/src/core/scheduler.cpp b/src/core/scheduler.cpp index ae5c2664..37d72072 100644 --- a/src/core/scheduler.cpp +++ b/src/core/scheduler.cpp @@ -1,1319 +1,1318 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow Waldo Bastian Copyright (C) 2009, 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 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 "scheduler.h" #include "scheduler_p.h" #include "sessiondata_p.h" #include "slaveconfig.h" #include "slave.h" #include "connection_p.h" #include "job_p.h" #include #include //#include #include #include #include -#include #include #include // Slaves may be idle for a certain time (3 minutes) before they are killed. static const int s_idleSlaveLifetime = 3 * 60; using namespace KIO; static inline Slave *jobSlave(SimpleJob *job) { return SimpleJobPrivate::get(job)->m_slave; } static inline int jobCommand(SimpleJob *job) { return SimpleJobPrivate::get(job)->m_command; } static inline void startJob(SimpleJob *job, Slave *slave) { SimpleJobPrivate::get(job)->start(slave); } // here be uglies // forward declaration to break cross-dependency of SlaveKeeper and SchedulerPrivate static void setupSlave(KIO::Slave *slave, const QUrl &url, const QString &protocol, const QStringList &proxyList, bool newSlave, const KIO::MetaData *config = nullptr); // same reason as above static Scheduler *scheduler(); static Slave *heldSlaveForJob(SimpleJob *job); int SerialPicker::changedPrioritySerial(int oldSerial, int newPriority) const { Q_ASSERT(newPriority >= -10 && newPriority <= 10); newPriority = qBound(-10, newPriority, 10); int unbiasedSerial = oldSerial % m_jobsPerPriority; return unbiasedSerial + newPriority * m_jobsPerPriority; } SlaveKeeper::SlaveKeeper() { m_grimTimer.setSingleShot(true); connect(&m_grimTimer, &QTimer::timeout, this, &SlaveKeeper::grimReaper); } SlaveKeeper::~SlaveKeeper() { grimReaper(); } void SlaveKeeper::returnSlave(Slave *slave) { Q_ASSERT(slave); slave->setIdle(); m_idleSlaves.insert(slave->host(), slave); scheduleGrimReaper(); } Slave *SlaveKeeper::takeSlaveForJob(SimpleJob *job) { Slave *slave = heldSlaveForJob(job); if (slave) { return slave; } QUrl url = SimpleJobPrivate::get(job)->m_url; // TODO take port, username and password into account QMultiHash::Iterator it = m_idleSlaves.find(url.host()); if (it == m_idleSlaves.end()) { it = m_idleSlaves.begin(); } if (it == m_idleSlaves.end()) { return nullptr; } slave = it.value(); m_idleSlaves.erase(it); return slave; } bool SlaveKeeper::removeSlave(Slave *slave) { // ### performance not so great QMultiHash::Iterator it = m_idleSlaves.begin(); for (; it != m_idleSlaves.end(); ++it) { if (it.value() == slave) { m_idleSlaves.erase(it); return true; } } return false; } void SlaveKeeper::clear() { m_idleSlaves.clear(); } QList SlaveKeeper::allSlaves() const { return m_idleSlaves.values(); } void SlaveKeeper::scheduleGrimReaper() { if (!m_grimTimer.isActive()) { m_grimTimer.start((s_idleSlaveLifetime / 2) * 1000); } } //private slot void SlaveKeeper::grimReaper() { QMultiHash::Iterator it = m_idleSlaves.begin(); while (it != m_idleSlaves.end()) { Slave *slave = it.value(); if (slave->idleTime() >= s_idleSlaveLifetime) { it = m_idleSlaves.erase(it); if (slave->job()) { //qDebug() << "Idle slave" << slave << "still has job" << slave->job(); } slave->kill(); // avoid invoking slotSlaveDied() because its cleanup services are not needed slave->deref(); } else { ++it; } } if (!m_idleSlaves.isEmpty()) { scheduleGrimReaper(); } } int HostQueue::lowestSerial() const { QMap::ConstIterator first = m_queuedJobs.constBegin(); if (first != m_queuedJobs.constEnd()) { return first.key(); } return SerialPicker::maxSerial; } void HostQueue::queueJob(SimpleJob *job) { const int serial = SimpleJobPrivate::get(job)->m_schedSerial; Q_ASSERT(serial != 0); Q_ASSERT(!m_queuedJobs.contains(serial)); Q_ASSERT(!m_runningJobs.contains(job)); m_queuedJobs.insert(serial, job); } SimpleJob *HostQueue::takeFirstInQueue() { Q_ASSERT(!m_queuedJobs.isEmpty()); QMap::iterator first = m_queuedJobs.begin(); SimpleJob *job = first.value(); m_queuedJobs.erase(first); m_runningJobs.insert(job); return job; } bool HostQueue::removeJob(SimpleJob *job) { const int serial = SimpleJobPrivate::get(job)->m_schedSerial; if (m_runningJobs.remove(job)) { Q_ASSERT(!m_queuedJobs.contains(serial)); return true; } if (m_queuedJobs.remove(serial)) { return true; } return false; } QList HostQueue::allSlaves() const { QList ret; ret.reserve(m_runningJobs.size()); Q_FOREACH (SimpleJob *job, m_runningJobs) { Slave *slave = jobSlave(job); Q_ASSERT(slave); ret.append(slave); } return ret; } ConnectedSlaveQueue::ConnectedSlaveQueue() { m_startJobsTimer.setSingleShot(true); connect(&m_startJobsTimer, &QTimer::timeout, this, &ConnectedSlaveQueue::startRunnableJobs); } bool ConnectedSlaveQueue::queueJob(SimpleJob *job, Slave *slave) { QHash::Iterator it = m_connectedSlaves.find(slave); if (it == m_connectedSlaves.end()) { return false; } SimpleJobPrivate::get(job)->m_slave = slave; PerSlaveQueue &jobs = it.value(); jobs.waitingList.append(job); if (!jobs.runningJob) { // idle slave now has a job to run m_runnableSlaves.insert(slave); m_startJobsTimer.start(); } return true; } bool ConnectedSlaveQueue::removeJob(SimpleJob *job) { Slave *slave = jobSlave(job); Q_ASSERT(slave); QHash::Iterator it = m_connectedSlaves.find(slave); if (it == m_connectedSlaves.end()) { return false; } PerSlaveQueue &jobs = it.value(); if (jobs.runningJob || jobs.waitingList.isEmpty()) { // a slave that was busy running a job was not runnable. // a slave that has no waiting job(s) was not runnable either. Q_ASSERT(!m_runnableSlaves.contains(slave)); } const bool removedRunning = jobs.runningJob == job; const bool removedWaiting = jobs.waitingList.removeAll(job) != 0; if (removedRunning) { jobs.runningJob = nullptr; Q_ASSERT(!removedWaiting); } const bool removedTheJob = removedRunning || removedWaiting; if (!slave->isAlive()) { removeSlave(slave); return removedTheJob; } if (removedRunning && jobs.waitingList.count()) { m_runnableSlaves.insert(slave); m_startJobsTimer.start(); } if (removedWaiting && jobs.waitingList.isEmpty()) { m_runnableSlaves.remove(slave); } return removedTheJob; } void ConnectedSlaveQueue::addSlave(Slave *slave) { Q_ASSERT(slave); if (!m_connectedSlaves.contains(slave)) { m_connectedSlaves.insert(slave, PerSlaveQueue()); } } bool ConnectedSlaveQueue::removeSlave(Slave *slave) { QHash::Iterator it = m_connectedSlaves.find(slave); if (it == m_connectedSlaves.end()) { return false; } PerSlaveQueue &jobs = it.value(); Q_FOREACH (SimpleJob *job, jobs.waitingList) { // ### for compatibility with the old scheduler we don't touch the running job, if any. // make sure that the job doesn't call back into Scheduler::cancelJob(); this would // a) crash and b) be unnecessary because we clean up just fine. SimpleJobPrivate::get(job)->m_schedSerial = 0; job->kill(); } m_connectedSlaves.erase(it); m_runnableSlaves.remove(slave); slave->kill(); return true; } // KDE5: only one caller, for doubtful reasons. remove this if possible. bool ConnectedSlaveQueue::isIdle(Slave *slave) { QHash::Iterator it = m_connectedSlaves.find(slave); if (it == m_connectedSlaves.end()) { return false; } return it.value().runningJob == nullptr; } //private slot void ConnectedSlaveQueue::startRunnableJobs() { QSet::Iterator it = m_runnableSlaves.begin(); while (it != m_runnableSlaves.end()) { Slave *slave = *it; if (!slave->isConnected()) { // this polling is somewhat inefficient... m_startJobsTimer.start(); ++it; continue; } it = m_runnableSlaves.erase(it); PerSlaveQueue &jobs = m_connectedSlaves[slave]; SimpleJob *job = jobs.waitingList.takeFirst(); Q_ASSERT(!jobs.runningJob); jobs.runningJob = job; const QUrl url = job->url(); // no port is -1 in QUrl, but in kde3 we used 0 and the kioslaves assume that. const int port = url.port() == -1 ? 0 : url.port(); if (slave->host() == QLatin1String("")) { MetaData configData = SlaveConfig::self()->configData(url.scheme(), url.host()); slave->setConfig(configData); slave->setProtocol(url.scheme()); slave->setHost(url.host(), port, url.userName(), url.password()); } Q_ASSERT(slave->protocol() == url.scheme()); Q_ASSERT(slave->host() == url.host()); Q_ASSERT(slave->port() == port); startJob(job, slave); } } static void ensureNoDuplicates(QMap *queuesBySerial) { Q_UNUSED(queuesBySerial); #ifdef SCHEDULER_DEBUG // a host queue may *never* be in queuesBySerial twice. QSet seen; Q_FOREACH (HostQueue *hq, *queuesBySerial) { Q_ASSERT(!seen.contains(hq)); seen.insert(hq); } #endif } static void verifyRunningJobsCount(QHash *queues, int runningJobsCount) { Q_UNUSED(queues); Q_UNUSED(runningJobsCount); #ifdef SCHEDULER_DEBUG int realRunningJobsCount = 0; Q_FOREACH (const HostQueue &hq, *queues) { realRunningJobsCount += hq.runningJobsCount(); } Q_ASSERT(realRunningJobsCount == runningJobsCount); // ...and of course we may never run the same job twice! QSet seenJobs; Q_FOREACH (const HostQueue &hq, *queues) { Q_FOREACH (SimpleJob *job, hq.runningJobs()) { Q_ASSERT(!seenJobs.contains(job)); seenJobs.insert(job); } } #endif } ProtoQueue::ProtoQueue(int maxSlaves, int maxSlavesPerHost) : m_maxConnectionsPerHost(maxSlavesPerHost ? maxSlavesPerHost : maxSlaves), m_maxConnectionsTotal(qMax(maxSlaves, maxSlavesPerHost)), m_runningJobsCount(0) { /*qDebug() << "m_maxConnectionsTotal:" << m_maxConnectionsTotal << "m_maxConnectionsPerHost:" << m_maxConnectionsPerHost;*/ Q_ASSERT(m_maxConnectionsPerHost >= 1); Q_ASSERT(maxSlaves >= maxSlavesPerHost); m_startJobTimer.setSingleShot(true); connect(&m_startJobTimer, &QTimer::timeout, this, &ProtoQueue::startAJob); } ProtoQueue::~ProtoQueue() { // Gather list of all slaves first const QList slaves = allSlaves(); // Clear the idle slaves in the keeper to avoid dangling pointers m_slaveKeeper.clear(); for (Slave *slave : slaves) { // kill the slave process, then remove the interface in our process slave->kill(); slave->deref(); } } void ProtoQueue::queueJob(SimpleJob *job) { QString hostname = SimpleJobPrivate::get(job)->m_url.host(); HostQueue &hq = m_queuesByHostname[hostname]; const int prevLowestSerial = hq.lowestSerial(); Q_ASSERT(hq.runningJobsCount() <= m_maxConnectionsPerHost); // nevert insert a job twice Q_ASSERT(SimpleJobPrivate::get(job)->m_schedSerial == 0); SimpleJobPrivate::get(job)->m_schedSerial = m_serialPicker.next(); const bool wasQueueEmpty = hq.isQueueEmpty(); hq.queueJob(job); // note that HostQueue::queueJob() into an empty queue changes its lowestSerial() too... // the queue's lowest serial job may have changed, so update the ordered list of queues. // however, we ignore all jobs that would cause more connections to a host than allowed. if (prevLowestSerial != hq.lowestSerial()) { if (hq.runningJobsCount() < m_maxConnectionsPerHost) { // if the connection limit didn't keep the HQ unscheduled it must have been lack of jobs if (m_queuesBySerial.remove(prevLowestSerial) == 0) { Q_UNUSED(wasQueueEmpty); Q_ASSERT(wasQueueEmpty); } m_queuesBySerial.insert(hq.lowestSerial(), &hq); } else { #ifdef SCHEDULER_DEBUG // ### this assertion may fail if the limits were modified at runtime! // if the per-host connection limit is already reached the host queue's lowest serial // should not be queued. Q_ASSERT(!m_queuesBySerial.contains(prevLowestSerial)); #endif } } // just in case; startAJob() will refuse to start a job if it shouldn't. m_startJobTimer.start(); ensureNoDuplicates(&m_queuesBySerial); } void ProtoQueue::changeJobPriority(SimpleJob *job, int newPrio) { SimpleJobPrivate *jobPriv = SimpleJobPrivate::get(job); QHash::Iterator it = m_queuesByHostname.find(jobPriv->m_url.host()); if (it == m_queuesByHostname.end()) { return; } HostQueue &hq = it.value(); const int prevLowestSerial = hq.lowestSerial(); if (hq.isJobRunning(job) || !hq.removeJob(job)) { return; } jobPriv->m_schedSerial = m_serialPicker.changedPrioritySerial(jobPriv->m_schedSerial, newPrio); hq.queueJob(job); const bool needReinsert = hq.lowestSerial() != prevLowestSerial; // the host queue might be absent from m_queuesBySerial because the connections per host limit // for that host has been reached. if (needReinsert && m_queuesBySerial.remove(prevLowestSerial)) { m_queuesBySerial.insert(hq.lowestSerial(), &hq); } ensureNoDuplicates(&m_queuesBySerial); } void ProtoQueue::removeJob(SimpleJob *job) { SimpleJobPrivate *jobPriv = SimpleJobPrivate::get(job); HostQueue &hq = m_queuesByHostname[jobPriv->m_url.host()]; const int prevLowestSerial = hq.lowestSerial(); const int prevRunningJobs = hq.runningJobsCount(); Q_ASSERT(hq.runningJobsCount() <= m_maxConnectionsPerHost); if (hq.removeJob(job)) { if (hq.lowestSerial() != prevLowestSerial) { // we have dequeued the not yet running job with the lowest serial Q_ASSERT(!jobPriv->m_slave); Q_ASSERT(prevRunningJobs == hq.runningJobsCount()); if (m_queuesBySerial.remove(prevLowestSerial) == 0) { // make sure that the queue was not scheduled for a good reason Q_ASSERT(hq.runningJobsCount() == m_maxConnectionsPerHost); } } else { if (prevRunningJobs != hq.runningJobsCount()) { // we have dequeued a previously running job Q_ASSERT(prevRunningJobs - 1 == hq.runningJobsCount()); m_runningJobsCount--; Q_ASSERT(m_runningJobsCount >= 0); } } if (!hq.isQueueEmpty() && hq.runningJobsCount() < m_maxConnectionsPerHost) { // this may be a no-op, but it's faster than first checking if it's already in. m_queuesBySerial.insert(hq.lowestSerial(), &hq); } if (hq.isEmpty()) { // no queued jobs, no running jobs. this destroys hq from above. m_queuesByHostname.remove(jobPriv->m_url.host()); } if (jobPriv->m_slave && jobPriv->m_slave->isAlive()) { m_slaveKeeper.returnSlave(jobPriv->m_slave); } // just in case; startAJob() will refuse to start a job if it shouldn't. m_startJobTimer.start(); } else { // should be a connected slave // if the assertion fails the job has probably changed the host part of its URL while // running, so we can't find it by hostname. don't do this. const bool removed = m_connectedSlaveQueue.removeJob(job); Q_UNUSED(removed); Q_ASSERT(removed); } ensureNoDuplicates(&m_queuesBySerial); } Slave *ProtoQueue::createSlave(const QString &protocol, SimpleJob *job, const QUrl &url) { int error; QString errortext; Slave *slave = Slave::createSlave(protocol, url, error, errortext); if (slave) { scheduler()->connect(slave, SIGNAL(slaveDied(KIO::Slave*)), SLOT(slotSlaveDied(KIO::Slave*))); scheduler()->connect(slave, SIGNAL(slaveStatus(qint64,QByteArray,QString,bool)), SLOT(slotSlaveStatus(qint64,QByteArray,QString,bool))); } else { qCWarning(KIO_CORE) << "couldn't create slave:" << errortext; if (job) { job->slotError(error, errortext); } } return slave; } bool ProtoQueue::removeSlave(KIO::Slave *slave) { const bool removedConnected = m_connectedSlaveQueue.removeSlave(slave); const bool removedUnconnected = m_slaveKeeper.removeSlave(slave); Q_ASSERT(!(removedConnected && removedUnconnected)); return removedConnected || removedUnconnected; } QList ProtoQueue::allSlaves() const { QList ret(m_slaveKeeper.allSlaves()); Q_FOREACH (const HostQueue &hq, m_queuesByHostname) { ret.append(hq.allSlaves()); } ret.append(m_connectedSlaveQueue.allSlaves()); return ret; } //private slot void ProtoQueue::startAJob() { ensureNoDuplicates(&m_queuesBySerial); verifyRunningJobsCount(&m_queuesByHostname, m_runningJobsCount); #ifdef SCHEDULER_DEBUG //qDebug() << "m_runningJobsCount:" << m_runningJobsCount; Q_FOREACH (const HostQueue &hq, m_queuesByHostname) { Q_FOREACH (SimpleJob *job, hq.runningJobs()) { //qDebug() << SimpleJobPrivate::get(job)->m_url; } } #endif if (m_runningJobsCount >= m_maxConnectionsTotal) { #ifdef SCHEDULER_DEBUG //qDebug() << "not starting any jobs because maxConnectionsTotal has been reached."; #endif return; } QMap::iterator first = m_queuesBySerial.begin(); if (first != m_queuesBySerial.end()) { // pick a job and maintain the queue invariant: lower serials first HostQueue *hq = first.value(); const int prevLowestSerial = first.key(); Q_UNUSED(prevLowestSerial); Q_ASSERT(hq->lowestSerial() == prevLowestSerial); // the following assertions should hold due to queueJob(), takeFirstInQueue() and // removeJob() being correct Q_ASSERT(hq->runningJobsCount() < m_maxConnectionsPerHost); SimpleJob *startingJob = hq->takeFirstInQueue(); Q_ASSERT(hq->runningJobsCount() <= m_maxConnectionsPerHost); Q_ASSERT(hq->lowestSerial() != prevLowestSerial); m_queuesBySerial.erase(first); // we've increased hq's runningJobsCount() by calling nexStartingJob() // so we need to check again. if (!hq->isQueueEmpty() && hq->runningJobsCount() < m_maxConnectionsPerHost) { m_queuesBySerial.insert(hq->lowestSerial(), hq); } // always increase m_runningJobsCount because it's correct if there is a slave and if there // is no slave, removeJob() will balance the number again. removeJob() would decrease the // number too much otherwise. // Note that createSlave() can call slotError() on a job which in turn calls removeJob(), // so increase the count here already. m_runningJobsCount++; bool isNewSlave = false; Slave *slave = m_slaveKeeper.takeSlaveForJob(startingJob); SimpleJobPrivate *jobPriv = SimpleJobPrivate::get(startingJob); if (!slave) { isNewSlave = true; slave = createSlave(jobPriv->m_protocol, startingJob, jobPriv->m_url); } if (slave) { jobPriv->m_slave = slave; setupSlave(slave, jobPriv->m_url, jobPriv->m_protocol, jobPriv->m_proxyList, isNewSlave); startJob(startingJob, slave); } else { // dispose of our records about the job and mark the job as unknown // (to prevent crashes later) // note that the job's slotError() can have called removeJob() first, so check that // it's not a ghost job with null serial already. if (jobPriv->m_schedSerial) { removeJob(startingJob); jobPriv->m_schedSerial = 0; } } } else { #ifdef SCHEDULER_DEBUG //qDebug() << "not starting any jobs because there is no queued job."; #endif } if (!m_queuesBySerial.isEmpty()) { m_startJobTimer.start(); } } class KIO::SchedulerPrivate { public: SchedulerPrivate() : q(new Scheduler()), m_slaveOnHold(nullptr), m_checkOnHold(true), // !! Always check with KLauncher for the first request m_ignoreConfigReparse(false) { } ~SchedulerPrivate() { delete q; q = nullptr; Q_FOREACH (ProtoQueue *p, m_protocols) { Q_FOREACH (Slave *slave, p->allSlaves()) { slave->kill(); } } qDeleteAll(m_protocols); } Scheduler *q; Slave *m_slaveOnHold; QUrl m_urlOnHold; bool m_checkOnHold; bool m_ignoreConfigReparse; SessionData sessionData; void doJob(SimpleJob *job); #ifndef KIOCORE_NO_DEPRECATED void scheduleJob(SimpleJob *job); #endif void setJobPriority(SimpleJob *job, int priority); void cancelJob(SimpleJob *job); void jobFinished(KIO::SimpleJob *job, KIO::Slave *slave); void putSlaveOnHold(KIO::SimpleJob *job, const QUrl &url); void removeSlaveOnHold(); Slave *getConnectedSlave(const QUrl &url, const KIO::MetaData &metaData); bool assignJobToSlave(KIO::Slave *slave, KIO::SimpleJob *job); bool disconnectSlave(KIO::Slave *slave); void checkSlaveOnHold(bool b); void publishSlaveOnHold(); Slave *heldSlaveForJob(KIO::SimpleJob *job); bool isSlaveOnHoldFor(const QUrl &url); void updateInternalMetaData(SimpleJob *job); MetaData metaDataFor(const QString &protocol, const QStringList &proxyList, const QUrl &url); void setupSlave(KIO::Slave *slave, const QUrl &url, const QString &protocol, const QStringList &proxyList, bool newSlave, const KIO::MetaData *config = nullptr); void slotSlaveDied(KIO::Slave *slave); void slotSlaveStatus(qint64 pid, const QByteArray &protocol, const QString &host, bool connected); void slotReparseSlaveConfiguration(const QString &, const QDBusMessage &); void slotSlaveOnHoldListChanged(); void slotSlaveConnected(); void slotSlaveError(int error, const QString &errorMsg); ProtoQueue *protoQ(const QString &protocol, const QString &host) { ProtoQueue *pq = m_protocols.value(protocol, nullptr); if (!pq) { //qDebug() << "creating ProtoQueue instance for" << protocol; const int maxSlaves = KProtocolInfo::maxSlaves(protocol); int maxSlavesPerHost = -1; if (!host.isEmpty()) { bool ok = false; const int value = SlaveConfig::self()->configData(protocol, host, QStringLiteral("MaxConnections")).toInt(&ok); if (ok) { maxSlavesPerHost = value; } } if (maxSlavesPerHost == -1) { maxSlavesPerHost = KProtocolInfo::maxSlavesPerHost(protocol); } // Never allow maxSlavesPerHost to exceed maxSlaves. pq = new ProtoQueue(maxSlaves, qMin(maxSlaves, maxSlavesPerHost)); m_protocols.insert(protocol, pq); } return pq; } private: QHash m_protocols; }; static QThreadStorage s_storage; static SchedulerPrivate *schedulerPrivate() { if (!s_storage.hasLocalData()) { s_storage.setLocalData(new SchedulerPrivate); } return s_storage.localData(); } Scheduler *Scheduler::self() { return schedulerPrivate()->q; } SchedulerPrivate *Scheduler::d_func() { return schedulerPrivate(); } //static Scheduler *scheduler() { return schedulerPrivate()->q; } //static Slave *heldSlaveForJob(SimpleJob *job) { return schedulerPrivate()->heldSlaveForJob(job); } Scheduler::Scheduler() { setObjectName(QStringLiteral("scheduler")); const QString dbusPath = QStringLiteral("/KIO/Scheduler"); const QString dbusInterface = QStringLiteral("org.kde.KIO.Scheduler"); QDBusConnection dbus = QDBusConnection::sessionBus(); // Not needed, right? We just want to emit two signals. //dbus.registerObject("/KIO/Scheduler", this, QDBusConnection::ExportScriptableSlots | // QDBusConnection::ExportScriptableSignals); dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("reparseSlaveConfiguration"), this, SLOT(slotReparseSlaveConfiguration(QString,QDBusMessage))); dbus.connect(QString(), dbusPath, dbusInterface, QStringLiteral("slaveOnHoldListChanged"), this, SLOT(slotSlaveOnHoldListChanged())); } Scheduler::~Scheduler() { } void Scheduler::doJob(SimpleJob *job) { schedulerPrivate()->doJob(job); } #ifndef KIOCORE_NO_DEPRECATED void Scheduler::scheduleJob(SimpleJob *job) { schedulerPrivate()->scheduleJob(job); } #endif void Scheduler::setJobPriority(SimpleJob *job, int priority) { schedulerPrivate()->setJobPriority(job, priority); } void Scheduler::cancelJob(SimpleJob *job) { schedulerPrivate()->cancelJob(job); } void Scheduler::jobFinished(KIO::SimpleJob *job, KIO::Slave *slave) { schedulerPrivate()->jobFinished(job, slave); } void Scheduler::putSlaveOnHold(KIO::SimpleJob *job, const QUrl &url) { schedulerPrivate()->putSlaveOnHold(job, url); } void Scheduler::removeSlaveOnHold() { schedulerPrivate()->removeSlaveOnHold(); } void Scheduler::publishSlaveOnHold() { schedulerPrivate()->publishSlaveOnHold(); } bool Scheduler::isSlaveOnHoldFor(const QUrl &url) { return schedulerPrivate()->isSlaveOnHoldFor(url); } void Scheduler::updateInternalMetaData(SimpleJob *job) { schedulerPrivate()->updateInternalMetaData(job); } KIO::Slave *Scheduler::getConnectedSlave(const QUrl &url, const KIO::MetaData &config) { return schedulerPrivate()->getConnectedSlave(url, config); } bool Scheduler::assignJobToSlave(KIO::Slave *slave, KIO::SimpleJob *job) { return schedulerPrivate()->assignJobToSlave(slave, job); } bool Scheduler::disconnectSlave(KIO::Slave *slave) { return schedulerPrivate()->disconnectSlave(slave); } bool Scheduler::connect(const char *signal, const QObject *receiver, const char *member) { return QObject::connect(self(), signal, receiver, member); } bool Scheduler::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member) { return QObject::connect(sender, signal, receiver, member); } bool Scheduler::disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *member) { return QObject::disconnect(sender, signal, receiver, member); } bool Scheduler::connect(const QObject *sender, const char *signal, const char *member) { return QObject::connect(sender, signal, member); } void Scheduler::checkSlaveOnHold(bool b) { schedulerPrivate()->checkSlaveOnHold(b); } void Scheduler::emitReparseSlaveConfiguration() { // Do it immediately in this process, otherwise we might send a request before reparsing // (e.g. when changing useragent in the plugin) schedulerPrivate()->slotReparseSlaveConfiguration(QString(), QDBusMessage()); schedulerPrivate()->m_ignoreConfigReparse = true; emit self()->reparseSlaveConfiguration(QString()); } void SchedulerPrivate::slotReparseSlaveConfiguration(const QString &proto, const QDBusMessage &) { if (m_ignoreConfigReparse) { //qDebug() << "Ignoring signal sent by myself"; m_ignoreConfigReparse = false; return; } //qDebug() << "proto=" << proto; KProtocolManager::reparseConfiguration(); SlaveConfig::self()->reset(); sessionData.reset(); NetRC::self()->reload(); QHash::ConstIterator it = proto.isEmpty() ? m_protocols.constBegin() : m_protocols.constFind(proto); // not found? if (it == m_protocols.constEnd()) { return; } QHash::ConstIterator endIt = proto.isEmpty() ? m_protocols.constEnd() : it + 1; for (; it != endIt; ++it) { Q_FOREACH (Slave *slave, (*it)->allSlaves()) { slave->send(CMD_REPARSECONFIGURATION); slave->resetHost(); } } } void SchedulerPrivate::slotSlaveOnHoldListChanged() { m_checkOnHold = true; } static bool mayReturnContent(int cmd, const QString &protocol) { if (cmd == CMD_GET) { return true; } if (cmd == CMD_MULTI_GET) { return true; } if (cmd == CMD_SPECIAL && protocol.startsWith(QLatin1String("http"), Qt::CaseInsensitive)) { return true; } return false; } void SchedulerPrivate::doJob(SimpleJob *job) { //qDebug() << job; KIO::SimpleJobPrivate *const jobPriv = SimpleJobPrivate::get(job); jobPriv->m_proxyList.clear(); jobPriv->m_protocol = KProtocolManager::slaveProtocol(job->url(), jobPriv->m_proxyList); if (mayReturnContent(jobCommand(job), jobPriv->m_protocol)) { jobPriv->m_checkOnHold = m_checkOnHold; m_checkOnHold = false; } ProtoQueue *proto = protoQ(jobPriv->m_protocol, job->url().host()); proto->queueJob(job); } #ifndef KIOCORE_NO_DEPRECATED void SchedulerPrivate::scheduleJob(SimpleJob *job) { //qDebug() << job; setJobPriority(job, 1); } #endif void SchedulerPrivate::setJobPriority(SimpleJob *job, int priority) { //qDebug() << job << priority; const QString protocol = SimpleJobPrivate::get(job)->m_protocol; if (!protocol.isEmpty()) { ProtoQueue *proto = protoQ(SimpleJobPrivate::get(job)->m_protocol, job->url().host()); proto->changeJobPriority(job, priority); } } void SchedulerPrivate::cancelJob(SimpleJob *job) { // this method is called all over the place in job.cpp, so just do this check here to avoid // much boilerplate in job code. if (SimpleJobPrivate::get(job)->m_schedSerial == 0) { //qDebug() << "Doing nothing because I don't know job" << job; return; } Slave *slave = jobSlave(job); //qDebug() << job << slave; if (slave) { //qDebug() << "Scheduler: killing slave " << slave->slave_pid(); slave->kill(); } jobFinished(job, slave); } void SchedulerPrivate::jobFinished(SimpleJob *job, Slave *slave) { //qDebug() << job << slave; KIO::SimpleJobPrivate *const jobPriv = SimpleJobPrivate::get(job); // make sure that we knew about the job! Q_ASSERT(jobPriv->m_schedSerial); ProtoQueue *pq = m_protocols.value(jobPriv->m_protocol); if (pq) { pq->removeJob(job); } if (slave) { // If we have internal meta-data, tell existing ioslaves to reload // their configuration. if (jobPriv->m_internalMetaData.count()) { //qDebug() << "Updating ioslaves with new internal metadata information"; ProtoQueue *queue = m_protocols.value(slave->protocol()); if (queue) { QListIterator it(queue->allSlaves()); while (it.hasNext()) { Slave *runningSlave = it.next(); if (slave->host() == runningSlave->host()) { slave->setConfig(metaDataFor(slave->protocol(), jobPriv->m_proxyList, job->url())); /*qDebug() << "Updated configuration of" << slave->protocol() << "ioslave, pid=" << slave->slave_pid();*/ } } } } slave->setJob(nullptr); slave->disconnect(job); } jobPriv->m_schedSerial = 0; // this marks the job as unscheduled again jobPriv->m_slave = nullptr; // Clear the values in the internal metadata container since they have // already been taken care of above... jobPriv->m_internalMetaData.clear(); } // static void setupSlave(KIO::Slave *slave, const QUrl &url, const QString &protocol, const QStringList &proxyList, bool newSlave, const KIO::MetaData *config) { schedulerPrivate()->setupSlave(slave, url, protocol, proxyList, newSlave, config); } MetaData SchedulerPrivate::metaDataFor(const QString &protocol, const QStringList &proxyList, const QUrl &url) { const QString host = url.host(); MetaData configData = SlaveConfig::self()->configData(protocol, host); sessionData.configDataFor(configData, protocol, host); if (proxyList.isEmpty()) { configData.remove(QStringLiteral("UseProxy")); configData.remove(QStringLiteral("ProxyUrls")); } else { configData[QStringLiteral("UseProxy")] = proxyList.first(); configData[QStringLiteral("ProxyUrls")] = proxyList.join(QLatin1Char(',')); } if (configData.contains(QStringLiteral("EnableAutoLogin")) && configData.value(QStringLiteral("EnableAutoLogin")).compare(QLatin1String("true"), Qt::CaseInsensitive) == 0) { NetRC::AutoLogin l; l.login = url.userName(); bool usern = (protocol == QLatin1String("ftp")); if (NetRC::self()->lookup(url, l, usern)) { configData[QStringLiteral("autoLoginUser")] = l.login; configData[QStringLiteral("autoLoginPass")] = l.password; if (usern) { QString macdef; QMap::ConstIterator it = l.macdef.constBegin(); for (; it != l.macdef.constEnd(); ++it) { macdef += it.key() + QLatin1Char('\\') + it.value().join(QLatin1Char('\\')) + QLatin1Char('\n'); } configData[QStringLiteral("autoLoginMacro")] = macdef; } } } return configData; } void SchedulerPrivate::setupSlave(KIO::Slave *slave, const QUrl &url, const QString &protocol, const QStringList &proxyList, bool newSlave, const KIO::MetaData *config) { int port = url.port(); if (port == -1) { // no port is -1 in QUrl, but in kde3 we used 0 and the kioslaves assume that. port = 0; } const QString host = url.host(); const QString user = url.userName(); const QString passwd = url.password(); if (newSlave || slave->host() != host || slave->port() != port || slave->user() != user || slave->passwd() != passwd) { MetaData configData = metaDataFor(protocol, proxyList, url); if (config) { configData += *config; } slave->setConfig(configData); slave->setProtocol(url.scheme()); slave->setHost(host, port, user, passwd); } } void SchedulerPrivate::slotSlaveStatus(qint64, const QByteArray &, const QString &, bool) { } void SchedulerPrivate::slotSlaveDied(KIO::Slave *slave) { //qDebug() << slave; Q_ASSERT(slave); Q_ASSERT(!slave->isAlive()); ProtoQueue *pq = m_protocols.value(slave->protocol()); if (pq) { if (slave->job()) { pq->removeJob(slave->job()); } // in case this was a connected slave... pq->removeSlave(slave); } if (slave == m_slaveOnHold) { m_slaveOnHold = nullptr; m_urlOnHold.clear(); } // can't use slave->deref() here because we need to use deleteLater slave->aboutToDelete(); slave->deleteLater(); } void SchedulerPrivate::putSlaveOnHold(KIO::SimpleJob *job, const QUrl &url) { Slave *slave = jobSlave(job); //qDebug() << job << url << slave; slave->disconnect(job); // prevent the fake death of the slave from trying to kill the job again; // cf. Slave::hold(const QUrl &url) called in SchedulerPrivate::publishSlaveOnHold(). slave->setJob(nullptr); SimpleJobPrivate::get(job)->m_slave = nullptr; if (m_slaveOnHold) { m_slaveOnHold->kill(); } m_slaveOnHold = slave; m_urlOnHold = url; m_slaveOnHold->suspend(); } void SchedulerPrivate::publishSlaveOnHold() { //qDebug() << m_slaveOnHold; if (!m_slaveOnHold) { return; } m_slaveOnHold->hold(m_urlOnHold); emit q->slaveOnHoldListChanged(); } bool SchedulerPrivate::isSlaveOnHoldFor(const QUrl &url) { if (url.isValid() && m_urlOnHold.isValid() && url == m_urlOnHold) { return true; } return Slave::checkForHeldSlave(url); } Slave *SchedulerPrivate::heldSlaveForJob(SimpleJob *job) { Slave *slave = nullptr; KIO::SimpleJobPrivate *const jobPriv = SimpleJobPrivate::get(job); if (jobPriv->m_checkOnHold) { slave = Slave::holdSlave(jobPriv->m_protocol, job->url()); } if (!slave && m_slaveOnHold) { // Make sure that the job wants to do a GET or a POST, and with no offset const int cmd = jobPriv->m_command; bool canJobReuse = (cmd == CMD_GET || cmd == CMD_MULTI_GET); if (KIO::TransferJob *tJob = qobject_cast(job)) { canJobReuse = (canJobReuse || cmd == CMD_SPECIAL); if (canJobReuse) { KIO::MetaData outgoing = tJob->outgoingMetaData(); const QString resume = outgoing.value(QStringLiteral("resume")); const QString rangeStart = outgoing.value(QStringLiteral("range-start")); //qDebug() << "Resume metadata is" << resume; canJobReuse = (resume.isEmpty() || resume == QLatin1String("0")) && (rangeStart.isEmpty() || rangeStart == QLatin1String("0")); } } if (job->url() == m_urlOnHold) { if (canJobReuse) { //qDebug() << "HOLD: Reusing held slave (" << m_slaveOnHold << ")"; slave = m_slaveOnHold; } else { //qDebug() << "HOLD: Discarding held slave (" << m_slaveOnHold << ")"; m_slaveOnHold->kill(); } m_slaveOnHold = nullptr; m_urlOnHold.clear(); } } else if (slave) { //qDebug() << "HOLD: Reusing klauncher held slave (" << slave << ")"; } return slave; } void SchedulerPrivate::removeSlaveOnHold() { //qDebug() << m_slaveOnHold; if (m_slaveOnHold) { m_slaveOnHold->kill(); } m_slaveOnHold = nullptr; m_urlOnHold.clear(); } Slave *SchedulerPrivate::getConnectedSlave(const QUrl &url, const KIO::MetaData &config) { QStringList proxyList; const QString protocol = KProtocolManager::slaveProtocol(url, proxyList); ProtoQueue *pq = protoQ(protocol, url.host()); Slave *slave = pq->createSlave(protocol, /* job */nullptr, url); if (slave) { setupSlave(slave, url, protocol, proxyList, true, &config); pq->m_connectedSlaveQueue.addSlave(slave); slave->send(CMD_CONNECT); q->connect(slave, SIGNAL(connected()), SLOT(slotSlaveConnected())); q->connect(slave, SIGNAL(error(int,QString)), SLOT(slotSlaveError(int,QString))); } //qDebug() << url << slave; return slave; } void SchedulerPrivate::slotSlaveConnected() { //qDebug(); Slave *slave = static_cast(q->sender()); slave->setConnected(true); q->disconnect(slave, SIGNAL(connected()), q, SLOT(slotSlaveConnected())); emit q->slaveConnected(slave); } void SchedulerPrivate::slotSlaveError(int errorNr, const QString &errorMsg) { Slave *slave = static_cast(q->sender()); //qDebug() << slave << errorNr << errorMsg; ProtoQueue *pq = protoQ(slave->protocol(), slave->host()); if (!slave->isConnected() || pq->m_connectedSlaveQueue.isIdle(slave)) { // Only forward to application if slave is idle or still connecting. // ### KDE5: can we remove this apparently arbitrary behavior and just always emit SlaveError? emit q->slaveError(slave, errorNr, errorMsg); } } bool SchedulerPrivate::assignJobToSlave(KIO::Slave *slave, SimpleJob *job) { //qDebug() << slave << job; // KDE5: queueing of jobs can probably be removed, it provides very little benefit ProtoQueue *pq = m_protocols.value(slave->protocol()); if (pq) { pq->removeJob(job); return pq->m_connectedSlaveQueue.queueJob(job, slave); } return false; } bool SchedulerPrivate::disconnectSlave(KIO::Slave *slave) { //qDebug() << slave; ProtoQueue *pq = m_protocols.value(slave->protocol()); return (pq ? pq->m_connectedSlaveQueue.removeSlave(slave) : false); } void SchedulerPrivate::checkSlaveOnHold(bool b) { //qDebug() << b; m_checkOnHold = b; } void SchedulerPrivate::updateInternalMetaData(SimpleJob *job) { KIO::SimpleJobPrivate *const jobPriv = SimpleJobPrivate::get(job); // Preserve all internal meta-data so they can be sent back to the // ioslaves as needed... const QUrl jobUrl = job->url(); //qDebug() << job << jobPriv->m_internalMetaData; QMapIterator it(jobPriv->m_internalMetaData); while (it.hasNext()) { it.next(); if (it.key().startsWith(QLatin1String("{internal~currenthost}"), Qt::CaseInsensitive)) { SlaveConfig::self()->setConfigData(jobUrl.scheme(), jobUrl.host(), it.key().mid(22), it.value()); } else if (it.key().startsWith(QLatin1String("{internal~allhosts}"), Qt::CaseInsensitive)) { SlaveConfig::self()->setConfigData(jobUrl.scheme(), QString(), it.key().mid(19), it.value()); } } } #include "moc_scheduler.cpp" #include "moc_scheduler_p.cpp" diff --git a/src/core/scheduler_p.h b/src/core/scheduler_p.h index 279f29f2..da8b8363 100644 --- a/src/core/scheduler_p.h +++ b/src/core/scheduler_p.h @@ -1,190 +1,190 @@ /* This file is part of the KDE libraries Copyright (C) 2009, 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 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. */ #ifndef SCHEDULER_P_H #define SCHEDULER_P_H #include - +#include // #define SCHEDULER_DEBUG namespace KIO { // The slave keeper manages the list of idle slaves that can be reused class SlaveKeeper : public QObject { Q_OBJECT public: SlaveKeeper(); ~SlaveKeeper(); void returnSlave(KIO::Slave *slave); // pick suitable slave for job and return it, return null if no slave found. // the slave is removed from the keeper. KIO::Slave *takeSlaveForJob(KIO::SimpleJob *job); // remove slave from keeper bool removeSlave(KIO::Slave *slave); // remove all slaves from keeper void clear(); QList allSlaves() const; private: void scheduleGrimReaper(); private Q_SLOTS: void grimReaper(); private: QMultiHash m_idleSlaves; QTimer m_grimTimer; }; class HostQueue { public: int lowestSerial() const; bool isQueueEmpty() const { return m_queuedJobs.isEmpty(); } bool isEmpty() const { return m_queuedJobs.isEmpty() && m_runningJobs.isEmpty(); } int runningJobsCount() const { return m_runningJobs.count(); } #ifdef SCHEDULER_DEBUG QList runningJobs() const { return m_runningJobs.toList(); } #endif bool isJobRunning(KIO::SimpleJob *job) const { return m_runningJobs.contains(job); } void queueJob(KIO::SimpleJob *job); KIO::SimpleJob *takeFirstInQueue(); bool removeJob(KIO::SimpleJob *job); QList allSlaves() const; private: QMap m_queuedJobs; QSet m_runningJobs; }; struct PerSlaveQueue { PerSlaveQueue() : runningJob(nullptr) {} QList waitingList; SimpleJob *runningJob; }; class ConnectedSlaveQueue : public QObject { Q_OBJECT public: ConnectedSlaveQueue(); bool queueJob(KIO::SimpleJob *job, KIO::Slave *slave); bool removeJob(KIO::SimpleJob *job); void addSlave(KIO::Slave *slave); bool removeSlave(KIO::Slave *slave); // KDE5: only one caller, for doubtful reasons. remove this if possible. bool isIdle(KIO::Slave *slave); bool isEmpty() const { return m_connectedSlaves.isEmpty(); } QList allSlaves() const { return m_connectedSlaves.keys(); } private Q_SLOTS: void startRunnableJobs(); private: // note that connected slaves stay here when idle, they are not returned to SlaveKeeper QHash m_connectedSlaves; QSet m_runnableSlaves; QTimer m_startJobsTimer; }; class SchedulerPrivate; class SerialPicker { public: // note that serial number zero is the default value from job_p.h and invalid! SerialPicker() : m_offset(1) {} int next() { if (m_offset >= m_jobsPerPriority) { m_offset = 1; } return m_offset++; } int changedPrioritySerial(int oldSerial, int newPriority) const; private: static const uint m_jobsPerPriority = 100000000; uint m_offset; public: static const int maxSerial = m_jobsPerPriority * 20; }; class ProtoQueue : public QObject { Q_OBJECT public: ProtoQueue(int maxSlaves, int maxSlavesPerHost); ~ProtoQueue(); void queueJob(KIO::SimpleJob *job); void changeJobPriority(KIO::SimpleJob *job, int newPriority); void removeJob(KIO::SimpleJob *job); KIO::Slave *createSlave(const QString &protocol, KIO::SimpleJob *job, const QUrl &url); bool removeSlave(KIO::Slave *slave); QList allSlaves() const; ConnectedSlaveQueue m_connectedSlaveQueue; private Q_SLOTS: // start max one (non-connected) job and return void startAJob(); private: SerialPicker m_serialPicker; QTimer m_startJobTimer; QMap m_queuesBySerial; QHash m_queuesByHostname; SlaveKeeper m_slaveKeeper; int m_maxConnectionsPerHost; int m_maxConnectionsTotal; int m_runningJobsCount; }; } // namespace KIO #endif //SCHEDULER_P_H diff --git a/src/core/sessiondata.cpp b/src/core/sessiondata.cpp index 7ec1f58a..6df55943 100644 --- a/src/core/sessiondata.cpp +++ b/src/core/sessiondata.cpp @@ -1,142 +1,141 @@ /* This file is part of the KDE project Copyright (C) 2000 Dawit Alemayehu This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser 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 Lesser 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 "sessiondata_p.h" #include -#include #include #include #include #include #include #include #include #include "http_slave_defaults.h" namespace KIO { /***************************** SessionData::AuthData ************************/ #if 0 struct SessionData::AuthData { public: AuthData() {} AuthData(const QByteArray &k, const QByteArray &g, bool p) { key = k; group = g; persist = p; } bool isKeyMatch(const QByteArray &val) const { return (val == key); } bool isGroupMatch(const QByteArray &val) const { return (val == group); } QByteArray key; QByteArray group; bool persist; }; #endif /********************************* SessionData ****************************/ class SessionData::SessionDataPrivate { public: SessionDataPrivate() { useCookie = true; initDone = false; } bool initDone; bool useCookie; QString charsets; QString language; }; SessionData::SessionData() : d(new SessionDataPrivate) { // authData = 0; } SessionData::~SessionData() { delete d; } void SessionData::configDataFor(MetaData &configData, const QString &proto, const QString &) { if ((proto.startsWith(QLatin1String("http"), Qt::CaseInsensitive)) || (proto.startsWith(QLatin1String("webdav"), Qt::CaseInsensitive))) { if (!d->initDone) { reset(); } // These might have already been set so check first // to make sure that we do not trumpt settings sent // by apps or end-user. if (configData[QStringLiteral("Cookies")].isEmpty()) { configData[QStringLiteral("Cookies")] = d->useCookie ? QStringLiteral("true") : QStringLiteral("false"); } if (configData[QStringLiteral("Languages")].isEmpty()) { configData[QStringLiteral("Languages")] = d->language; } if (configData[QStringLiteral("Charsets")].isEmpty()) { configData[QStringLiteral("Charsets")] = d->charsets; } if (configData[QStringLiteral("CacheDir")].isEmpty()) { const QString httpCacheDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_http"); QDir().mkpath(httpCacheDir); configData[QStringLiteral("CacheDir")] = httpCacheDir; } if (configData[QStringLiteral("UserAgent")].isEmpty()) { configData[QStringLiteral("UserAgent")] = KProtocolManager::defaultUserAgent(); } } } void SessionData::reset() { d->initDone = true; // Get Cookie settings... d->useCookie = KSharedConfig::openConfig(QStringLiteral("kcookiejarrc"), KConfig::NoGlobals)-> group("Cookie Policy"). readEntry("Cookies", true); d->language = KProtocolManager::acceptLanguagesHeader(); d->charsets = QString::fromLatin1(QTextCodec::codecForLocale()->name()).toLower(); KProtocolManager::reparseConfiguration(); } } diff --git a/src/core/slaveinterface.cpp b/src/core/slaveinterface.cpp index 88f649c8..d7d2ef8d 100644 --- a/src/core/slaveinterface.cpp +++ b/src/core/slaveinterface.cpp @@ -1,441 +1,437 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "slaveinterface.h" #include "slaveinterface_p.h" #include "usernotificationhandler_p.h" #include "slavebase.h" #include "connection_p.h" #include "commands_p.h" #include "hostinfo.h" #include #include #include #include #include - -#include -#include +#include #include -#include -#include using namespace KIO; Q_GLOBAL_STATIC(UserNotificationHandler, globalUserNotificationHandler) SlaveInterface::SlaveInterface(SlaveInterfacePrivate &dd, QObject *parent) : QObject(parent), d_ptr(&dd) { connect(&d_ptr->speed_timer, &QTimer::timeout, this, &SlaveInterface::calcSpeed); } SlaveInterface::~SlaveInterface() { // Note: no Debug() here (scheduler is deleted very late) delete d_ptr; } void SlaveInterface::setConnection(Connection *connection) { Q_D(SlaveInterface); d->connection = connection; } Connection *SlaveInterface::connection() const { const Q_D(SlaveInterface); return d->connection; } static KIO::filesize_t readFilesize_t(QDataStream &stream) { KIO::filesize_t result; stream >> result; return result; } bool SlaveInterface::dispatch() { Q_D(SlaveInterface); Q_ASSERT(d->connection); int cmd; QByteArray data; int ret = d->connection->read(&cmd, data); if (ret == -1) { return false; } return dispatch(cmd, data); } void SlaveInterface::calcSpeed() { Q_D(SlaveInterface); if (d->slave_calcs_speed || !d->connection->isConnected()) { // killing a job results in disconnection but the timer never stops d->speed_timer.stop(); return; } const qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); const qint64 diff = currentTime - d->start_time; if (diff - d->last_time >= 900) { d->last_time = diff; if (d->nums == max_nums) { // let's hope gcc can optimize that well enough // otherwise I'd try memcpy :) for (unsigned int i = 1; i < max_nums; ++i) { d->times[i - 1] = d->times[i]; d->sizes[i - 1] = d->sizes[i]; } d->nums--; } d->times[d->nums] = diff; d->sizes[d->nums++] = d->filesize - d->offset; KIO::filesize_t lspeed = 1000 * (d->sizes[d->nums - 1] - d->sizes[0]) / (d->times[d->nums - 1] - d->times[0]); //qDebug() << (long)d->filesize << diff // << long(d->sizes[d->nums-1] - d->sizes[0]) // << d->times[d->nums-1] - d->times[0] // << long(lspeed) << double(d->filesize) / diff // << convertSize(lspeed) // << convertSize(long(double(d->filesize) / diff) * 1000); if (!lspeed) { d->nums = 1; d->times[0] = diff; d->sizes[0] = d->filesize - d->offset; } emit speed(lspeed); } } bool SlaveInterface::dispatch(int _cmd, const QByteArray &rawdata) { Q_D(SlaveInterface); //qDebug() << "dispatch " << _cmd; QDataStream stream(rawdata); QString str1; qint32 i; qint8 b; quint32 ul; switch (_cmd) { case MSG_DATA: emit data(rawdata); break; case MSG_DATA_REQ: emit dataReq(); break; case MSG_OPENED: emit open(); break; case MSG_FINISHED: //qDebug() << "Finished [this = " << this << "]"; d->offset = 0; d->speed_timer.stop(); emit finished(); break; case MSG_STAT_ENTRY: { UDSEntry entry; stream >> entry; emit statEntry(entry); break; } case MSG_LIST_ENTRIES: { UDSEntryList list; UDSEntry entry; while (!stream.atEnd()) { stream >> entry; list.append(entry); } emit listEntries(list); break; } case MSG_RESUME: { // From the put job d->offset = readFilesize_t(stream); emit canResume(d->offset); break; } case MSG_CANRESUME: // From the get job d->filesize = d->offset; emit canResume(0); // the arg doesn't matter break; case MSG_ERROR: stream >> i >> str1; //qDebug() << "error " << i << " " << str1; emit error(i, str1); break; case MSG_SLAVE_STATUS: case MSG_SLAVE_STATUS_V2: { qint64 pid; QByteArray protocol; stream >> pid >> protocol >> str1 >> b; emit slaveStatus(pid, protocol, str1, (b != 0)); break; } case MSG_CONNECTED: emit connected(); break; case MSG_WRITTEN: { KIO::filesize_t size = readFilesize_t(stream); emit written(size); break; } case INF_TOTAL_SIZE: { KIO::filesize_t size = readFilesize_t(stream); d->start_time = QDateTime::currentMSecsSinceEpoch(); d->last_time = 0; d->filesize = d->offset; d->sizes[0] = d->filesize - d->offset; d->times[0] = 0; d->nums = 1; d->speed_timer.start(1000); d->slave_calcs_speed = false; emit totalSize(size); break; } case INF_PROCESSED_SIZE: { KIO::filesize_t size = readFilesize_t(stream); emit processedSize(size); d->filesize = size; break; } case INF_POSITION: { KIO::filesize_t pos = readFilesize_t(stream); emit position(pos); break; } case INF_SPEED: stream >> ul; d->slave_calcs_speed = true; d->speed_timer.stop(); emit speed(ul); break; case INF_GETTING_FILE: break; case INF_ERROR_PAGE: emit errorPage(); break; case INF_REDIRECTION: { QUrl url; stream >> url; emit redirection(url); break; } case INF_MIME_TYPE: stream >> str1; emit mimeType(str1); if (!d->connection->suspended()) { d->connection->sendnow(CMD_NONE, QByteArray()); } break; case INF_WARNING: stream >> str1; emit warning(str1); break; case INF_MESSAGEBOX: { //qDebug() << "needs a msg box"; QString text, caption, buttonYes, buttonNo, dontAskAgainName; int type; stream >> type >> text >> caption >> buttonYes >> buttonNo; if (stream.atEnd()) { messageBox(type, text, caption, buttonYes, buttonNo); } else { stream >> dontAskAgainName; messageBox(type, text, caption, buttonYes, buttonNo, dontAskAgainName); } break; } case INF_INFOMESSAGE: { QString msg; stream >> msg; emit infoMessage(msg); break; } case INF_META_DATA: { MetaData m; stream >> m; if (m.contains(QStringLiteral("ssl_in_use"))) { const QLatin1String ssl_("ssl_"); const MetaData constM = m; for (MetaData::ConstIterator it = constM.lowerBound(ssl_); it != constM.constEnd(); ++it) { if (it.key().startsWith(ssl_)) { d->sslMetaData.insert(it.key(), it.value()); } else { // we're past the ssl_* entries; remember that QMap is ordered. break; } } } emit metaData(m); break; } case MSG_NET_REQUEST: { QString host; QString slaveid; stream >> host >> slaveid; requestNetwork(host, slaveid); break; } case MSG_NET_DROP: { QString host; QString slaveid; stream >> host >> slaveid; dropNetwork(host, slaveid); break; } case MSG_NEED_SUBURL_DATA: { emit needSubUrlData(); break; } case MSG_HOST_INFO_REQ: { QString hostName; stream >> hostName; HostInfo::lookupHost(hostName, this, SLOT(slotHostInfo(QHostInfo))); break; } case MSG_PRIVILEGE_EXEC: emit privilegeOperationRequested(); break; default: qCWarning(KIO_CORE) << "Slave sends unknown command (" << _cmd << "), dropping slave"; return false; } return true; } void SlaveInterface::setOffset(KIO::filesize_t o) { Q_D(SlaveInterface); d->offset = o; } KIO::filesize_t SlaveInterface::offset() const { const Q_D(SlaveInterface); return d->offset; } void SlaveInterface::requestNetwork(const QString &host, const QString &slaveid) { Q_D(SlaveInterface); Q_UNUSED(host); Q_UNUSED(slaveid); //qDebug() << "requestNetwork " << host << slaveid; // This is old stuff. We just always return true... QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << true; d->connection->sendnow(INF_NETWORK_STATUS, packedArgs); } void SlaveInterface::dropNetwork(const QString &host, const QString &slaveid) { Q_UNUSED(host); Q_UNUSED(slaveid); //qDebug() << "dropNetwork " << host << slaveid; } void SlaveInterface::sendResumeAnswer(bool resume) { Q_D(SlaveInterface); //qDebug() << "ok for resuming:" << resume; d->connection->sendnow(resume ? CMD_RESUMEANSWER : CMD_NONE, QByteArray()); } void SlaveInterface::sendMessageBoxAnswer(int result) { Q_D(SlaveInterface); if (!d->connection) { return; } if (d->connection->suspended()) { d->connection->resume(); } QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << result; d->connection->sendnow(CMD_MESSAGEBOXANSWER, packedArgs); // qDebug() << "message box answer" << result; } void SlaveInterface::messageBox(int type, const QString &text, const QString &_caption, const QString &buttonYes, const QString &buttonNo) { messageBox(type, text, _caption, buttonYes, buttonNo, QString()); } void SlaveInterface::messageBox(int type, const QString &text, const QString &caption, const QString &buttonYes, const QString &buttonNo, const QString &dontAskAgainName) { Q_D(SlaveInterface); if (d->connection) { d->connection->suspend(); } QHash data; data.insert(UserNotificationHandler::MSG_TEXT, text); data.insert(UserNotificationHandler::MSG_CAPTION, caption); data.insert(UserNotificationHandler::MSG_YES_BUTTON_TEXT, buttonYes); data.insert(UserNotificationHandler::MSG_NO_BUTTON_TEXT, buttonNo); data.insert(UserNotificationHandler::MSG_DONT_ASK_AGAIN, dontAskAgainName); // SMELL: the braindead way to support button icons // TODO: Fix this in KIO::SlaveBase. if (buttonYes == i18n("&Details")) { data.insert(UserNotificationHandler::MSG_YES_BUTTON_ICON, QLatin1String("help-about")); } else if (buttonYes == i18n("&Forever")) { data.insert(UserNotificationHandler::MSG_YES_BUTTON_ICON, QLatin1String("flag-green")); } if (buttonNo == i18n("Co&ntinue")) { data.insert(UserNotificationHandler::MSG_NO_BUTTON_ICON, QLatin1String("arrow-right")); } else if (buttonNo == i18n("&Current Session only")) { data.insert(UserNotificationHandler::MSG_NO_BUTTON_ICON, QLatin1String("chronometer")); } if (type == KIO::SlaveBase::SSLMessageBox) { data.insert(UserNotificationHandler::MSG_META_DATA, d->sslMetaData.toVariant()); } globalUserNotificationHandler()->requestMessageBox(this, type, data); } void SlaveInterfacePrivate::slotHostInfo(const QHostInfo &info) { QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); stream << info.hostName() << info.addresses() << info.error() << info.errorString(); connection->send(CMD_HOST_INFO, data); } #include "moc_slaveinterface.cpp" diff --git a/src/core/slaveinterface_p.h b/src/core/slaveinterface_p.h index a4cb5d3f..b3fb566e 100644 --- a/src/core/slaveinterface_p.h +++ b/src/core/slaveinterface_p.h @@ -1,68 +1,67 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIO_SLAVEINTERFACEPRIVATE_H #define KIO_SLAVEINTERFACEPRIVATE_H #ifdef Q_OS_WIN #include #include // struct timeval #endif #include "global.h" #include "connection_p.h" #include -#include #include #include "kiocoredebug.h" static const unsigned int max_nums = 8; class KIO::SlaveInterfacePrivate { public: SlaveInterfacePrivate() : connection(nullptr), filesize(0), offset(0), last_time(0), start_time(0), nums(0), slave_calcs_speed(false) { } virtual ~SlaveInterfacePrivate() { delete connection; } Connection *connection; QTimer speed_timer; // We need some metadata here for our SSL code in messageBox() and for sslMetaData(). MetaData sslMetaData; KIO::filesize_t sizes[max_nums]; qint64 times[max_nums]; KIO::filesize_t filesize, offset; size_t last_time; qint64 start_time; uint nums; bool slave_calcs_speed; void slotHostInfo(const QHostInfo &info); }; #endif diff --git a/src/core/tcpslavebase.cpp b/src/core/tcpslavebase.cpp index bd1c86f7..ba2fea95 100644 --- a/src/core/tcpslavebase.cpp +++ b/src/core/tcpslavebase.cpp @@ -1,942 +1,937 @@ /* * Copyright (C) 2000 Alex Zepeda * Copyright (C) 2001-2003 George Staikos * Copyright (C) 2001 Dawit Alemayehu * Copyright (C) 2007,2008 Andreas Hartmetz * Copyright (C) 2008 Roland Harnau * Copyright (C) 2010 Richard Moore * * This file is part of the KDE project * * 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 "tcpslavebase.h" #include "kiocoredebug.h" #include #include #include #include #include -#include -#include -#include -#include -#include #include using namespace KIO; //using namespace KNetwork; namespace KIO { Q_DECLARE_OPERATORS_FOR_FLAGS(TCPSlaveBase::SslResult) } //TODO Proxy support whichever way works; KPAC reportedly does *not* work. //NOTE kded_proxyscout may or may not be interesting //TODO resurrect SSL session recycling; this means save the session on disconnect and look //for a reusable session on connect. Consider how HTTP persistent connections interact with that. //TODO in case we support SSL-lessness we need static KTcpSocket::sslAvailable() and check it //in most places we ATM check for d->isSSL. //TODO check if d->isBlocking is honored everywhere it makes sense //TODO fold KSSLSetting and KSSLCertificateHome into KSslSettings and use that everywhere. //TODO recognize partially encrypted websites as "somewhat safe" /* List of dialogs/messageboxes we need to use (current code location in parentheses) - Can the "dontAskAgainName" thing be improved? - "SSLCertDialog" [select client cert] (SlaveInterface) - Enter password for client certificate (inline) - Password for client cert was wrong. Please reenter. (inline) - Setting client cert failed. [doesn't give reason] (inline) - "SSLInfoDialog" [mostly server cert info] (SlaveInterface) - You are about to enter secure mode. Security information/Display SSL information/Connect (inline) - You are about to leave secure mode. Security information/Continue loading/Abort (inline) - Hostname mismatch: Continue/Details/Cancel (inline) - IP address mismatch: Continue/Details/Cancel (inline) - Certificate failed authenticity check: Continue/Details/Cancel (inline) - Would you like to accept this certificate forever: Yes/No/Current sessions only (inline) */ /** @internal */ class Q_DECL_HIDDEN TCPSlaveBase::TcpSlaveBasePrivate { public: explicit TcpSlaveBasePrivate(TCPSlaveBase *qq) : q(qq) {} void setSslMetaData() { sslMetaData.insert(QStringLiteral("ssl_in_use"), QStringLiteral("TRUE")); KSslCipher cipher = socket.sessionCipher(); sslMetaData.insert(QStringLiteral("ssl_protocol_version"), socket.negotiatedSslVersionName()); const QString sslCipher = cipher.encryptionMethod() + QLatin1Char('\n') + cipher.authenticationMethod() + QLatin1Char('\n') + cipher.keyExchangeMethod() + QLatin1Char('\n') + cipher.digestMethod(); sslMetaData.insert(QStringLiteral("ssl_cipher"), sslCipher); sslMetaData.insert(QStringLiteral("ssl_cipher_name"), cipher.name()); sslMetaData.insert(QStringLiteral("ssl_cipher_used_bits"), QString::number(cipher.usedBits())); sslMetaData.insert(QStringLiteral("ssl_cipher_bits"), QString::number(cipher.supportedBits())); sslMetaData.insert(QStringLiteral("ssl_peer_ip"), ip); // try to fill in the blanks, i.e. missing certificates, and just assume that // those belong to the peer (==website or similar) certificate. for (int i = 0; i < sslErrors.count(); i++) { if (sslErrors[i].certificate().isNull()) { const QList peerCertificateChain = socket.peerCertificateChain(); sslErrors[i] = KSslError(sslErrors[i].error(), peerCertificateChain[0]); } } QString errorStr; // encode the two-dimensional numeric error list using '\n' and '\t' as outer and inner separators Q_FOREACH (const QSslCertificate &cert, socket.peerCertificateChain()) { Q_FOREACH (const KSslError &error, sslErrors) { if (error.certificate() == cert) { errorStr += QString::number(static_cast(error.error())) + QLatin1Char('\t'); } } if (errorStr.endsWith(QLatin1Char('\t'))) { errorStr.chop(1); } errorStr += QLatin1Char('\n'); } errorStr.chop(1); sslMetaData.insert(QStringLiteral("ssl_cert_errors"), errorStr); QString peerCertChain; Q_FOREACH (const QSslCertificate &cert, socket.peerCertificateChain()) { peerCertChain += QString::fromUtf8(cert.toPem()) + QLatin1Char('\x01'); } peerCertChain.chop(1); sslMetaData.insert(QStringLiteral("ssl_peer_chain"), peerCertChain); sendSslMetaData(); } void clearSslMetaData() { sslMetaData.clear(); sslMetaData.insert(QStringLiteral("ssl_in_use"), QStringLiteral("FALSE")); sendSslMetaData(); } void sendSslMetaData() { MetaData::ConstIterator it = sslMetaData.constBegin(); for (; it != sslMetaData.constEnd(); ++it) { q->setMetaData(it.key(), it.value()); } } SslResult startTLSInternal(KTcpSocket::SslVersion sslVersion, int waitForEncryptedTimeout = -1); TCPSlaveBase * const q; bool isBlocking; KTcpSocket socket; QString host; QString ip; quint16 port; QByteArray serviceName; KSSLSettings sslSettings; bool usingSSL; bool autoSSL; bool sslNoUi; // If true, we just drop the connection silently // if SSL certificate check fails in some way. QList sslErrors; MetaData sslMetaData; }; //### uh, is this a good idea?? QIODevice *TCPSlaveBase::socket() const { return &d->socket; } TCPSlaveBase::TCPSlaveBase(const QByteArray &protocol, const QByteArray &poolSocket, const QByteArray &appSocket, bool autoSSL) : SlaveBase(protocol, poolSocket, appSocket), d(new TcpSlaveBasePrivate(this)) { d->isBlocking = true; d->port = 0; d->serviceName = protocol; d->usingSSL = false; d->autoSSL = autoSSL; d->sslNoUi = false; // Limit the read buffer size to 14 MB (14*1024*1024) (based on the upload limit // in TransferJob::slotDataReq). See the docs for QAbstractSocket::setReadBufferSize // and the BR# 187876 to understand why setting this limit is necessary. d->socket.setReadBufferSize(14680064); } TCPSlaveBase::~TCPSlaveBase() { delete d; } ssize_t TCPSlaveBase::write(const char *data, ssize_t len) { ssize_t written = d->socket.write(data, len); if (written == -1) { /*qDebug() << "d->socket.write() returned -1! Socket error is" << d->socket.error() << ", Socket state is" << d->socket.state();*/ } bool success = false; if (d->isBlocking) { // Drain the tx buffer success = d->socket.waitForBytesWritten(-1); } else { // ### I don't know how to make sure that all data does get written at some point // without doing it now. There is no event loop to do it behind the scenes. // Polling in the dispatch() loop? Something timeout based? success = d->socket.waitForBytesWritten(0); } d->socket.flush(); //this is supposed to get the data on the wire faster if (d->socket.state() != KTcpSocket::ConnectedState || !success) { /*qDebug() << "Write failed, will return -1! Socket error is" << d->socket.error() << ", Socket state is" << d->socket.state() << "Return value of waitForBytesWritten() is" << success;*/ return -1; } return written; } ssize_t TCPSlaveBase::read(char *data, ssize_t len) { if (d->usingSSL && (d->socket.encryptionMode() != KTcpSocket::SslClientMode)) { d->clearSslMetaData(); //qDebug() << "lost SSL connection."; return -1; } if (!d->socket.bytesAvailable()) { const int timeout = d->isBlocking ? -1 : (readTimeout() * 1000); d->socket.waitForReadyRead(timeout); } #if 0 // Do not do this because its only benefit is to cause a nasty side effect // upstream in Qt. See BR# 260769. else if (d->socket.encryptionMode() != KTcpSocket::SslClientMode || QNetworkProxy::applicationProxy().type() == QNetworkProxy::NoProxy) { // we only do this when it doesn't trigger Qt socket bugs. When it doesn't break anything // it seems to help performance. d->socket.waitForReadyRead(0); } #endif return d->socket.read(data, len); } ssize_t TCPSlaveBase::readLine(char *data, ssize_t len) { if (d->usingSSL && (d->socket.encryptionMode() != KTcpSocket::SslClientMode)) { d->clearSslMetaData(); //qDebug() << "lost SSL connection."; return -1; } const int timeout = (d->isBlocking ? -1 : (readTimeout() * 1000)); ssize_t readTotal = 0; do { if (!d->socket.bytesAvailable()) { d->socket.waitForReadyRead(timeout); } ssize_t readStep = d->socket.readLine(&data[readTotal], len - readTotal); if (readStep == -1 || (readStep == 0 && d->socket.state() != KTcpSocket::ConnectedState)) { return -1; } readTotal += readStep; } while (readTotal == 0 || data[readTotal - 1] != '\n'); return readTotal; } bool TCPSlaveBase::connectToHost(const QString &/*protocol*/, const QString &host, quint16 port) { QString errorString; const int errCode = connectToHost(host, port, &errorString); if (errCode == 0) { return true; } error(errCode, errorString); return false; } int TCPSlaveBase::connectToHost(const QString &host, quint16 port, QString *errorString) { d->clearSslMetaData(); //We have separate connection and SSL setup phases if (errorString) { errorString->clear(); // clear prior error messages. } d->socket.setVerificationPeerName(host); // Used for ssl certificate verification (SNI) // - leaving SSL - warn before we even connect //### see if it makes sense to move this into the HTTP ioslave which is the only // user. if (metaData(QStringLiteral("main_frame_request")) == QLatin1String("TRUE") //### this looks *really* unreliable && metaData(QStringLiteral("ssl_activate_warnings")) == QLatin1String("TRUE") && metaData(QStringLiteral("ssl_was_in_use")) == QLatin1String("TRUE") && !d->autoSSL) { if (d->sslSettings.warnOnLeave()) { int result = messageBox(i18n("You are about to leave secure " "mode. Transmissions will no " "longer be encrypted.\nThis " "means that a third party could " "observe your data in transit."), WarningContinueCancel, i18n("Security Information"), i18n("C&ontinue Loading"), QString(), QStringLiteral("WarnOnLeaveSSLMode")); if (result == SlaveBase::Cancel) { if (errorString) { *errorString = host; } return ERR_USER_CANCELED; } } } const int timeout = (connectTimeout() * 1000); // 20 sec timeout value disconnectFromHost(); //Reset some state, even if we are already disconnected d->host = host; d->socket.connectToHost(host, port); /*const bool connectOk = */d->socket.waitForConnected(timeout > -1 ? timeout : -1); /*qDebug() << "Socket: state=" << d->socket.state() << ", error=" << d->socket.error() << ", connected?" << connectOk;*/ if (d->socket.state() != KTcpSocket::ConnectedState) { if (errorString) { *errorString = host + QLatin1String(": ") + d->socket.errorString(); } switch (d->socket.error()) { case KTcpSocket::UnsupportedSocketOperationError: return ERR_UNSUPPORTED_ACTION; case KTcpSocket::RemoteHostClosedError: return ERR_CONNECTION_BROKEN; case KTcpSocket::SocketTimeoutError: return ERR_SERVER_TIMEOUT; case KTcpSocket::HostNotFoundError: return ERR_UNKNOWN_HOST; default: return ERR_CANNOT_CONNECT; } } //### check for proxyAuthenticationRequiredError d->ip = d->socket.peerAddress().toString(); d->port = d->socket.peerPort(); if (d->autoSSL) { const SslResult res = d->startTLSInternal(KTcpSocket::SecureProtocols, timeout); if (res & ResultFailed) { if (errorString) { *errorString = i18nc("%1 is a host name", "%1: SSL negotiation failed", host); } return ERR_CANNOT_CONNECT; } } return 0; } void TCPSlaveBase::disconnectFromHost() { //qDebug(); d->host.clear(); d->ip.clear(); d->usingSSL = false; if (d->socket.state() == KTcpSocket::UnconnectedState) { // discard incoming data - the remote host might have disconnected us in the meantime // but the visible effect of disconnectFromHost() should stay the same. d->socket.close(); return; } //### maybe save a session for reuse on SSL shutdown if and when QSslSocket // does that. QCA::TLS can do it apparently but that is not enough if // we want to present that as KDE API. Not a big loss in any case. d->socket.disconnectFromHost(); if (d->socket.state() != KTcpSocket::UnconnectedState) { d->socket.waitForDisconnected(-1); // wait for unsent data to be sent } d->socket.close(); //whatever that means on a socket } bool TCPSlaveBase::isAutoSsl() const { return d->autoSSL; } bool TCPSlaveBase::isUsingSsl() const { return d->usingSSL; } quint16 TCPSlaveBase::port() const { return d->port; } bool TCPSlaveBase::atEnd() const { return d->socket.atEnd(); } bool TCPSlaveBase::startSsl() { if (d->usingSSL) { return false; } return d->startTLSInternal(KTcpSocket::SecureProtocols) & ResultOk; } TCPSlaveBase::SslResult TCPSlaveBase::TcpSlaveBasePrivate::startTLSInternal(KTcpSocket::SslVersion version, int waitForEncryptedTimeout) { q->selectClientCertificate(); //setMetaData("ssl_session_id", d->kssl->session()->toString()); //### we don't support session reuse for now... usingSSL = true; // Set the SSL version to use... socket.setAdvertisedSslVersion(version); /* Usually ignoreSslErrors() would be called in the slot invoked by the sslErrors() signal but that would mess up the flow of control. We will check for errors anyway to decide if we want to continue connecting. Otherwise ignoreSslErrors() before connecting would be very insecure. */ socket.ignoreSslErrors(); socket.startClientEncryption(); const bool encryptionStarted = socket.waitForEncrypted(waitForEncryptedTimeout); //Set metadata, among other things for the "SSL Details" dialog KSslCipher cipher = socket.sessionCipher(); if (!encryptionStarted || socket.encryptionMode() != KTcpSocket::SslClientMode || cipher.isNull() || cipher.usedBits() == 0 || socket.peerCertificateChain().isEmpty()) { usingSSL = false; clearSslMetaData(); /*qDebug() << "Initial SSL handshake failed. encryptionStarted is" << encryptionStarted << ", cipher.isNull() is" << cipher.isNull() << ", cipher.usedBits() is" << cipher.usedBits() << ", length of certificate chain is" << socket.peerCertificateChain().count() << ", the socket says:" << socket.errorString() << "and the list of SSL errors contains" << socket.sslErrors().count() << "items.";*/ /*Q_FOREACH(const KSslError& sslError, socket.sslErrors()) { qDebug() << "SSL ERROR: (" << sslError.error() << ")" << sslError.errorString(); }*/ return ResultFailed | ResultFailedEarly; } /*qDebug() << "Cipher info - " << " advertised SSL protocol version" << socket.advertisedSslVersion() << " negotiated SSL protocol version" << socket.negotiatedSslVersion() << " authenticationMethod:" << cipher.authenticationMethod() << " encryptionMethod:" << cipher.encryptionMethod() << " keyExchangeMethod:" << cipher.keyExchangeMethod() << " name:" << cipher.name() << " supportedBits:" << cipher.supportedBits() << " usedBits:" << cipher.usedBits();*/ sslErrors = socket.sslErrors(); // TODO: review / rewrite / remove the comment // The app side needs the metadata now for the SSL error dialog (if any) but // the same metadata will be needed later, too. When "later" arrives the slave // may actually be connected to a different application that doesn't know // the metadata the slave sent to the previous application. // The quite important SSL indicator icon in Konqi's URL bar relies on metadata // from here, for example. And Konqi will be the second application to connect // to the slave. // Therefore we choose to have our metadata and send it, too :) setSslMetaData(); q->sendAndKeepMetaData(); SslResult rc = q->verifyServerCertificate(); if (rc & ResultFailed) { usingSSL = false; clearSslMetaData(); //qDebug() << "server certificate verification failed."; socket.disconnectFromHost(); //Make the connection fail (cf. ignoreSslErrors()) return ResultFailed; } else if (rc & ResultOverridden) { //qDebug() << "server certificate verification failed but continuing at user's request."; } //"warn" when starting SSL/TLS if (q->metaData(QStringLiteral("ssl_activate_warnings")) == QLatin1String("TRUE") && q->metaData(QStringLiteral("ssl_was_in_use")) == QLatin1String("FALSE") && sslSettings.warnOnEnter()) { int msgResult = q->messageBox(i18n("You are about to enter secure mode. " "All transmissions will be encrypted " "unless otherwise noted.\nThis means " "that no third party will be able to " "easily observe your data in transit."), WarningYesNo, i18n("Security Information"), i18n("Display SSL &Information"), i18n("C&onnect"), QStringLiteral("WarnOnEnterSSLMode")); if (msgResult == SlaveBase::Yes) { q->messageBox(SSLMessageBox /*==the SSL info dialog*/, host); } } return rc; } void TCPSlaveBase::selectClientCertificate() { #if 0 //hehe QString certname; // the cert to use this session bool send = false, prompt = false, save = false, forcePrompt = false; KSSLCertificateHome::KSSLAuthAction aa; setMetaData("ssl_using_client_cert", "FALSE"); // we change this if needed if (metaData("ssl_no_client_cert") == "TRUE") { return; } forcePrompt = (metaData("ssl_force_cert_prompt") == "TRUE"); // Delete the old cert since we're certainly done with it now if (d->pkcs) { delete d->pkcs; d->pkcs = NULL; } if (!d->kssl) { return; } // Look for a general certificate if (!forcePrompt) { certname = KSSLCertificateHome::getDefaultCertificateName(&aa); switch (aa) { case KSSLCertificateHome::AuthSend: send = true; prompt = false; break; case KSSLCertificateHome::AuthDont: send = false; prompt = false; certname.clear(); break; case KSSLCertificateHome::AuthPrompt: send = false; prompt = true; break; default: break; } } // Look for a certificate on a per-host basis as an override QString tmpcn = KSSLCertificateHome::getDefaultCertificateName(d->host, &aa); if (aa != KSSLCertificateHome::AuthNone) { // we must override switch (aa) { case KSSLCertificateHome::AuthSend: send = true; prompt = false; certname = tmpcn; break; case KSSLCertificateHome::AuthDont: send = false; prompt = false; certname.clear(); break; case KSSLCertificateHome::AuthPrompt: send = false; prompt = true; certname = tmpcn; break; default: break; } } // Finally, we allow the application to override anything. if (hasMetaData("ssl_demand_certificate")) { certname = metaData("ssl_demand_certificate"); if (!certname.isEmpty()) { forcePrompt = false; prompt = false; send = true; } } if (certname.isEmpty() && !prompt && !forcePrompt) { return; } // Ok, we're supposed to prompt the user.... if (prompt || forcePrompt) { QStringList certs = KSSLCertificateHome::getCertificateList(); QStringList::const_iterator it = certs.begin(); while (it != certs.end()) { KSSLPKCS12 *pkcs = KSSLCertificateHome::getCertificateByName(*it); if (pkcs && (!pkcs->getCertificate() || !pkcs->getCertificate()->x509V3Extensions().certTypeSSLClient())) { it = certs.erase(it); } else { ++it; } delete pkcs; } if (certs.isEmpty()) { return; // we had nothing else, and prompt failed } QDBusConnectionInterface *bus = QDBusConnection::sessionBus().interface(); if (!bus->isServiceRegistered("org.kde.kio.uiserver")) { bus->startService("org.kde.kuiserver"); } QDBusInterface uis("org.kde.kio.uiserver", "/UIServer", "org.kde.KIO.UIServer"); QDBusMessage retVal = uis.call("showSSLCertDialog", d->host, certs, metaData("window-id").toLongLong()); if (retVal.type() == QDBusMessage::ReplyMessage) { if (retVal.arguments().at(0).toBool()) { send = retVal.arguments().at(1).toBool(); save = retVal.arguments().at(2).toBool(); certname = retVal.arguments().at(3).toString(); } } } // The user may have said to not send the certificate, // but to save the choice if (!send) { if (save) { KSSLCertificateHome::setDefaultCertificate(certname, d->host, false, false); } return; } // We're almost committed. If we can read the cert, we'll send it now. KSSLPKCS12 *pkcs = KSSLCertificateHome::getCertificateByName(certname); if (!pkcs && KSSLCertificateHome::hasCertificateByName(certname)) { // We need the password KIO::AuthInfo ai; bool first = true; do { ai.prompt = i18n("Enter the certificate password:"); ai.caption = i18n("SSL Certificate Password"); ai.url.setScheme("kssl"); ai.url.setHost(certname); ai.username = certname; ai.keepPassword = true; bool showprompt; if (first) { showprompt = !checkCachedAuthentication(ai); } else { showprompt = true; } if (showprompt) { if (!openPasswordDialog(ai, first ? QString() : i18n("Unable to open the certificate. Try a new password?"))) { break; } } first = false; pkcs = KSSLCertificateHome::getCertificateByName(certname, ai.password); } while (!pkcs); } // If we could open the certificate, let's send it if (pkcs) { if (!d->kssl->setClientCertificate(pkcs)) { messageBox(Information, i18n("The procedure to set the " "client certificate for the session " "failed."), i18n("SSL")); delete pkcs; // we don't need this anymore pkcs = 0L; } else { //qDebug() << "Client SSL certificate is being used."; setMetaData("ssl_using_client_cert", "TRUE"); if (save) { KSSLCertificateHome::setDefaultCertificate(certname, d->host, true, false); } } d->pkcs = pkcs; } #endif } TCPSlaveBase::SslResult TCPSlaveBase::verifyServerCertificate() { d->sslNoUi = hasMetaData(QStringLiteral("ssl_no_ui")) && (metaData(QStringLiteral("ssl_no_ui")) != QLatin1String("FALSE")); if (d->sslErrors.isEmpty()) { return ResultOk; } else if (d->sslNoUi) { return ResultFailed; } QList fatalErrors = KSslCertificateManager::nonIgnorableErrors(d->sslErrors); if (!fatalErrors.isEmpty()) { //TODO message "sorry, fatal error, you can't override it" return ResultFailed; } QList peerCertificationChain = d->socket.peerCertificateChain(); KSslCertificateManager *const cm = KSslCertificateManager::self(); KSslCertificateRule rule = cm->rule(peerCertificationChain.first(), d->host); // remove previously seen and acknowledged errors QList remainingErrors = rule.filterErrors(d->sslErrors); if (remainingErrors.isEmpty()) { //qDebug() << "Error list empty after removing errors to be ignored. Continuing."; return ResultOk | ResultOverridden; } //### We don't ask to permanently reject the certificate QString message = i18n("The server failed the authenticity check (%1).\n\n", d->host); for (const KSslError &err : qAsConst(d->sslErrors)) { message += err.errorString() + QLatin1Char('\n'); } message = message.trimmed(); int msgResult; QDateTime ruleExpiry = QDateTime::currentDateTime(); do { msgResult = messageBox(WarningYesNoCancel, message, i18n("Server Authentication"), i18n("&Details"), i18n("Co&ntinue")); switch (msgResult) { case SlaveBase::Yes: //Details was chosen- show the certificate and error details messageBox(SSLMessageBox /*the SSL info dialog*/, d->host); break; case SlaveBase::No: { //fall through on SlaveBase::No const int result = messageBox(WarningYesNoCancel, i18n("Would you like to accept this " "certificate forever without " "being prompted?"), i18n("Server Authentication"), i18n("&Forever"), i18n("&Current Session only")); if (result == SlaveBase::Yes) { //accept forever ("for a very long time") ruleExpiry = ruleExpiry.addYears(1000); } else if (result == SlaveBase::No) { //accept "for a short time", half an hour. ruleExpiry = ruleExpiry.addSecs(30*60); } else { msgResult = SlaveBase::Yes; } break; } case SlaveBase::Cancel: return ResultFailed; default: qCWarning(KIO_CORE) << "Unexpected MessageBox response received:" << msgResult; return ResultFailed; } } while (msgResult == SlaveBase::Yes); //TODO special cases for wildcard domain name in the certificate! //rule = KSslCertificateRule(d->socket.peerCertificateChain().first(), whatever); rule.setExpiryDateTime(ruleExpiry); rule.setIgnoredErrors(d->sslErrors); cm->setRule(rule); return ResultOk | ResultOverridden; #if 0 //### need to do something like the old code about the main and subframe stuff //qDebug() << "SSL HTTP frame the parent? " << metaData("main_frame_request"); if (!hasMetaData("main_frame_request") || metaData("main_frame_request") == "TRUE") { // Since we're the parent, we need to teach the child. setMetaData("ssl_parent_ip", d->ip); setMetaData("ssl_parent_cert", pc.toString()); // - Read from cache and see if there is a policy for this KSSLCertificateCache::KSSLCertificatePolicy cp = d->certCache->getPolicyByCertificate(pc); // - validation code if (ksv != KSSLCertificate::Ok) { if (d->sslNoUi) { return -1; } if (cp == KSSLCertificateCache::Unknown || cp == KSSLCertificateCache::Ambiguous) { cp = KSSLCertificateCache::Prompt; } else { // A policy was already set so let's honor that. permacache = d->certCache->isPermanent(pc); } if (!_IPmatchesCN && cp == KSSLCertificateCache::Accept) { cp = KSSLCertificateCache::Prompt; // ksv = KSSLCertificate::Ok; } ////// SNIP SNIP ////////// // - cache the results d->certCache->addCertificate(pc, cp, permacache); if (doAddHost) { d->certCache->addHost(pc, d->host); } } else { // Child frame // - Read from cache and see if there is a policy for this KSSLCertificateCache::KSSLCertificatePolicy cp = d->certCache->getPolicyByCertificate(pc); isChild = true; // Check the cert and IP to make sure they're the same // as the parent frame bool certAndIPTheSame = (d->ip == metaData("ssl_parent_ip") && pc.toString() == metaData("ssl_parent_cert")); if (ksv == KSSLCertificate::Ok) { if (certAndIPTheSame) { // success rc = 1; setMetaData("ssl_action", "accept"); } else { /* if (d->sslNoUi) { return -1; } result = messageBox(WarningYesNo, i18n("The certificate is valid but does not appear to have been assigned to this server. Do you wish to continue loading?"), i18n("Server Authentication")); if (result == SlaveBase::Yes) { // success rc = 1; setMetaData("ssl_action", "accept"); } else { // fail rc = -1; setMetaData("ssl_action", "reject"); } */ setMetaData("ssl_action", "accept"); rc = 1; // Let's accept this now. It's bad, but at least the user // will see potential attacks in KDE3 with the pseudo-lock // icon on the toolbar, and can investigate with the RMB } } else { if (d->sslNoUi) { return -1; } if (cp == KSSLCertificateCache::Accept) { if (certAndIPTheSame) { // success rc = 1; setMetaData("ssl_action", "accept"); } else { // fail result = messageBox(WarningYesNo, i18n("You have indicated that you wish to accept this certificate, but it is not issued to the server who is presenting it. Do you wish to continue loading?"), i18n("Server Authentication")); if (result == SlaveBase::Yes) { rc = 1; setMetaData("ssl_action", "accept"); d->certCache->addHost(pc, d->host); } else { rc = -1; setMetaData("ssl_action", "reject"); } } } else if (cp == KSSLCertificateCache::Reject) { // fail messageBox(Information, i18n("SSL certificate is being rejected as requested. You can disable this in the KDE System Settings."), i18n("Server Authentication")); rc = -1; setMetaData("ssl_action", "reject"); } else { //////// SNIP SNIP ////////// return rc; } } } } #endif //#if 0 } bool TCPSlaveBase::isConnected() const { //QSslSocket::isValid() and therefore KTcpSocket::isValid() are shady... return d->socket.state() == KTcpSocket::ConnectedState; } bool TCPSlaveBase::waitForResponse(int t) { if (d->socket.bytesAvailable()) { return true; } return d->socket.waitForReadyRead(t * 1000); } void TCPSlaveBase::setBlocking(bool b) { if (!b) { qCWarning(KIO_CORE) << "Caller requested non-blocking mode, but that doesn't work"; return; } d->isBlocking = b; } void TCPSlaveBase::virtual_hook(int id, void *data) { if (id == SlaveBase::AppConnectionMade) { d->sendSslMetaData(); } else { SlaveBase::virtual_hook(id, data); } } diff --git a/src/core/usernotificationhandler.cpp b/src/core/usernotificationhandler.cpp index 951103c2..3bc539f3 100644 --- a/src/core/usernotificationhandler.cpp +++ b/src/core/usernotificationhandler.cpp @@ -1,114 +1,113 @@ /* This file is part of the KDE libraries Copyright (C) 2012 Dawit Alemayehu This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License 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 "usernotificationhandler_p.h" #include "slave.h" #include "job_p.h" #include "kiocoredebug.h" -#include #include using namespace KIO; QString UserNotificationHandler::Request::key() const { QString key; if (slave) { key = slave->protocol(); key += slave->host(); key += slave->port(); key += QLatin1Char('-'); key += type; } return key; } UserNotificationHandler::UserNotificationHandler(QObject *parent) : QObject(parent) { } UserNotificationHandler::~UserNotificationHandler() { qDeleteAll(m_pendingRequests); } void UserNotificationHandler::requestMessageBox(SlaveInterface *iface, int type, const QHash &data) { Request *r = new Request; r->type = type; r->slave = qobject_cast(iface); r->data = data; m_pendingRequests.append(r); if (m_pendingRequests.count() == 1) { QTimer::singleShot(0, this, &UserNotificationHandler::processRequest); } } void UserNotificationHandler::processRequest() { if (m_pendingRequests.isEmpty()) { return; } int result = -1; Request *r = m_pendingRequests.first(); if (r->slave) { const QString key = r->key(); if (m_cachedResults.contains(key)) { result = *(m_cachedResults[key]); } else { JobUiDelegateExtension *delegateExtension = nullptr; if (r->slave->job()) delegateExtension = SimpleJobPrivate::get(r->slave->job())->m_uiDelegateExtension; if (!delegateExtension) delegateExtension = KIO::defaultJobUiDelegateExtension(); if (delegateExtension) { const JobUiDelegateExtension::MessageBoxType type = static_cast(r->type); result = delegateExtension->requestMessageBox(type, r->data.value(MSG_TEXT).toString(), r->data.value(MSG_CAPTION).toString(), r->data.value(MSG_YES_BUTTON_TEXT).toString(), r->data.value(MSG_NO_BUTTON_TEXT).toString(), r->data.value(MSG_YES_BUTTON_ICON).toString(), r->data.value(MSG_NO_BUTTON_ICON).toString(), r->data.value(MSG_DONT_ASK_AGAIN).toString(), r->data.value(MSG_META_DATA).toMap()); } m_cachedResults.insert(key, new int(result)); } } else { qCWarning(KIO_CORE) << "Cannot prompt user because the requesting ioslave died!" << r->slave; } r->slave->sendMessageBoxAnswer(result); m_pendingRequests.removeFirst(); delete r; if (m_pendingRequests.isEmpty()) { m_cachedResults.clear(); } else { QTimer::singleShot(0, this, &UserNotificationHandler::processRequest); } } diff --git a/src/filewidgets/kdiroperator.cpp b/src/filewidgets/kdiroperator.cpp index af6f3eef..9a3f5bee 100644 --- a/src/filewidgets/kdiroperator.cpp +++ b/src/filewidgets/kdiroperator.cpp @@ -1,2797 +1,2798 @@ /* 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; 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)), 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(); 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->delta() > 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(QStringLiteral("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::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()) { 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) { 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/kdiroperatordetailview.cpp b/src/filewidgets/kdiroperatordetailview.cpp index fa8094f8..0d759197 100644 --- a/src/filewidgets/kdiroperatordetailview.cpp +++ b/src/filewidgets/kdiroperatordetailview.cpp @@ -1,135 +1,132 @@ /***************************************************************************** * Copyright (C) 2007 by Peter Penz * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License 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 "kdiroperatordetailview_p.h" #include #include -#include #include -#include -#include +#include #include #include #include #include -#include #include KDirOperatorDetailView::KDirOperatorDetailView(QWidget *parent) : QTreeView(parent), m_hideDetailColumns(false) { setRootIsDecorated(false); setSortingEnabled(true); setUniformRowHeights(true); setDragDropMode(QListView::DragOnly); setSelectionBehavior(QAbstractItemView::SelectRows); setEditTriggers(QAbstractItemView::NoEditTriggers); setVerticalScrollMode(QListView::ScrollPerPixel); setHorizontalScrollMode(QListView::ScrollPerPixel); } KDirOperatorDetailView::~KDirOperatorDetailView() { } bool KDirOperatorDetailView::setViewMode(KFile::FileView viewMode) { bool tree = false; if (KFile::isDetailView(viewMode)) { m_hideDetailColumns = false; setAlternatingRowColors(true); } else if (KFile::isTreeView(viewMode)) { m_hideDetailColumns = true; setAlternatingRowColors(false); tree = true; } else if (KFile::isDetailTreeView(viewMode)) { m_hideDetailColumns = false; setAlternatingRowColors(true); tree = true; } else { return false; } setRootIsDecorated(tree); setItemsExpandable(tree); // This allows to have a horizontal scrollbar in case this view is used as // a plain treeview instead of cutting off filenames, especially useful when // using KDirOperator in horizontally limited parts of an app. if (tree && m_hideDetailColumns) { header()->setSectionResizeMode(QHeaderView::ResizeToContents); } else { header()->setSectionResizeMode(QHeaderView::Interactive); } return true; } bool KDirOperatorDetailView::event(QEvent *event) { if (event->type() == QEvent::Polish) { QHeaderView *headerView = header(); headerView->setSectionResizeMode(0, QHeaderView::Stretch); headerView->setSectionResizeMode(1, QHeaderView::ResizeToContents); headerView->setSectionResizeMode(2, QHeaderView::ResizeToContents); headerView->setStretchLastSection(false); headerView->setSectionsMovable(false); setColumnHidden(KDirModel::Size, m_hideDetailColumns); setColumnHidden(KDirModel::ModifiedTime, m_hideDetailColumns); hideColumn(KDirModel::Type); hideColumn(KDirModel::Permissions); hideColumn(KDirModel::Owner); hideColumn(KDirModel::Group); } else if (event->type() == QEvent::UpdateRequest) { // A wheel movement will scroll 4 items if (model()->rowCount()) { verticalScrollBar()->setSingleStep((sizeHintForRow(0) / 3) * 4); } } return QTreeView::event(event); } void KDirOperatorDetailView::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls()) { event->acceptProposedAction(); } } void KDirOperatorDetailView::mousePressEvent(QMouseEvent *event) { QTreeView::mousePressEvent(event); const QModelIndex index = indexAt(event->pos()); if (!index.isValid() || (index.column() != KDirModel::Name)) { const Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); if (!(modifiers & Qt::ShiftModifier) && !(modifiers & Qt::ControlModifier)) { clearSelection(); } } } void KDirOperatorDetailView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { QTreeView::currentChanged(current, previous); } diff --git a/src/filewidgets/kencodingfiledialog.cpp b/src/filewidgets/kencodingfiledialog.cpp index 929e354c..a7905afe 100644 --- a/src/filewidgets/kencodingfiledialog.cpp +++ b/src/filewidgets/kencodingfiledialog.cpp @@ -1,287 +1,286 @@ // -*- c++ -*- /* This file is part of the KDE libraries Copyright (C) 2003 Joseph Wenninger 2003 Andras Mantia 2013 Teo Mrnjavac This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // TODO: remove me #undef QT_NO_CAST_FROM_ASCII #include "kencodingfiledialog.h" #include "kfilewidget.h" #include #include #include #include #include #include #include #include #include -#include #include #include struct KEncodingFileDialogPrivate { KEncodingFileDialogPrivate() : cfgGroup(KSharedConfig::openConfig(), ConfigGroup) {} KComboBox *encoding; KFileWidget *w; KConfigGroup cfgGroup; }; KEncodingFileDialog::KEncodingFileDialog(const QUrl &startDir, const QString &encoding, const QString &filter, const QString &caption, QFileDialog::AcceptMode type, QWidget *parent) : QDialog(parent, Qt::Dialog) , d(new KEncodingFileDialogPrivate) { d->w = new KFileWidget(startDir, this); d->w->setFilter(filter); if (type == QFileDialog::AcceptOpen) { d->w->setOperationMode(KFileWidget::Opening); } else { d->w->setOperationMode(KFileWidget::Saving); } setWindowTitle(caption); //ops->clearHistory(); KWindowConfig::restoreWindowSize(windowHandle(), d->cfgGroup); QBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(d->w); d->w->okButton()->show(); connect(d->w->okButton(), &QAbstractButton::clicked, this, &KEncodingFileDialog::slotOk); d->w->cancelButton()->show(); connect(d->w->cancelButton(), &QAbstractButton::clicked, this, &KEncodingFileDialog::slotCancel); connect(d->w, &KFileWidget::accepted, this, &KEncodingFileDialog::accept); d->encoding = new KComboBox(this); d->w->setCustomWidget(i18n("Encoding:"), d->encoding); d->encoding->clear(); QString sEncoding = encoding; QString systemEncoding = QTextCodec::codecForLocale()->name(); if (sEncoding.isEmpty() || sEncoding == QLatin1String("System")) { sEncoding = systemEncoding; } const QStringList encodings(KCharsets::charsets()->availableEncodingNames()); int insert = 0, system = 0; bool foundRequested = false; for (const QString &encoding : encodings) { bool found = false; QTextCodec *codecForEnc = KCharsets::charsets()->codecForName(encoding, found); if (found) { d->encoding->addItem(encoding); if ((codecForEnc->name() == sEncoding) || (encoding == sEncoding)) { d->encoding->setCurrentIndex(insert); foundRequested = true; } if ((codecForEnc->name() == systemEncoding) || (encoding == systemEncoding)) { system = insert; } insert++; } } if (!foundRequested) { d->encoding->setCurrentIndex(system); } } KEncodingFileDialog::~KEncodingFileDialog() { delete d; } QString KEncodingFileDialog::selectedEncoding() const { if (d->encoding) { return d->encoding->currentText(); } else { return QString(); } } KEncodingFileDialog::Result KEncodingFileDialog::getOpenFileNameAndEncoding(const QString &encoding, const QUrl &startDir, const QString &filter, QWidget *parent, const QString &caption) { KEncodingFileDialog dlg(startDir, encoding, filter, caption.isNull() ? i18n("Open") : caption, QFileDialog::AcceptOpen, parent); dlg.d->w->setMode(KFile::File | KFile::LocalOnly); dlg.exec(); Result res; res.fileNames << dlg.d->w->selectedFile(); res.encoding = dlg.selectedEncoding(); return res; } KEncodingFileDialog::Result KEncodingFileDialog::getOpenFileNamesAndEncoding(const QString &encoding, const QUrl &startDir, const QString &filter, QWidget *parent, const QString &caption) { KEncodingFileDialog dlg(startDir, encoding, filter, caption.isNull() ? i18n("Open") : caption, QFileDialog::AcceptOpen, parent); dlg.d->w->setMode(KFile::Files | KFile::LocalOnly); dlg.exec(); Result res; res.fileNames = dlg.d->w->selectedFiles(); res.encoding = dlg.selectedEncoding(); return res; } KEncodingFileDialog::Result KEncodingFileDialog::getOpenUrlAndEncoding(const QString &encoding, const QUrl &startDir, const QString &filter, QWidget *parent, const QString &caption) { KEncodingFileDialog dlg(startDir, encoding, filter, caption.isNull() ? i18n("Open") : caption, QFileDialog::AcceptOpen, parent); dlg.d->w->setMode(KFile::File); dlg.exec(); Result res; res.URLs << dlg.d->w->selectedUrl(); res.encoding = dlg.selectedEncoding(); return res; } KEncodingFileDialog::Result KEncodingFileDialog::getOpenUrlsAndEncoding(const QString &encoding, const QUrl &startDir, const QString &filter, QWidget *parent, const QString &caption) { KEncodingFileDialog dlg(startDir, encoding, filter, caption.isNull() ? i18n("Open") : caption, QFileDialog::AcceptOpen, parent); dlg.d->w->setMode(KFile::Files); dlg.exec(); Result res; res.URLs = dlg.d->w->selectedUrls(); res.encoding = dlg.selectedEncoding(); return res; } KEncodingFileDialog::Result KEncodingFileDialog::getSaveFileNameAndEncoding(const QString &encoding, const QUrl &dir, const QString &filter, QWidget *parent, const QString &caption) { KEncodingFileDialog dlg(dir, encoding, filter, caption.isNull() ? i18n("Save As") : caption, QFileDialog::AcceptSave, parent); dlg.d->w->setMode(KFile::File); dlg.exec(); QString filename = dlg.d->w->selectedFile(); if (!filename.isEmpty()) { KRecentDocument::add(QUrl::fromLocalFile(filename)); } Result res; res.fileNames << filename; res.encoding = dlg.selectedEncoding(); return res; } KEncodingFileDialog::Result KEncodingFileDialog::getSaveUrlAndEncoding(const QString &encoding, const QUrl &dir, const QString &filter, QWidget *parent, const QString &caption) { KEncodingFileDialog dlg(dir, encoding, filter, caption.isNull() ? i18n("Save As") : caption, QFileDialog::AcceptSave, parent); dlg.d->w->setMode(KFile::File); Result res; if (dlg.exec() == QDialog::Accepted) { QUrl url = dlg.d->w->selectedUrl(); if (url.isValid()) { KRecentDocument::add(url); } res.URLs << url; res.encoding = dlg.selectedEncoding(); } return res; } QSize KEncodingFileDialog::sizeHint() const { return d->w->dialogSizeHint(); } void KEncodingFileDialog::hideEvent(QHideEvent *e) { KWindowConfig::saveWindowSize(windowHandle(), d->cfgGroup, KConfigBase::Persistent); QDialog::hideEvent(e); } void KEncodingFileDialog::accept() { d->w->accept(); QDialog::accept(); } void KEncodingFileDialog::slotOk() { d->w->slotOk(); } void KEncodingFileDialog::slotCancel() { d->w->slotCancel(); reject(); } #include "moc_kencodingfiledialog.cpp" diff --git a/src/filewidgets/kfilecopytomenu.cpp b/src/filewidgets/kfilecopytomenu.cpp index 01f5a31d..5700f19a 100644 --- a/src/filewidgets/kfilecopytomenu.cpp +++ b/src/filewidgets/kfilecopytomenu.cpp @@ -1,268 +1,267 @@ /* Copyright 2008, 2009, 2015 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfilecopytomenu.h" #include "kfilecopytomenu_p.h" #include -#include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include "windows.h" #endif KFileCopyToMenuPrivate::KFileCopyToMenuPrivate(KFileCopyToMenu *qq, QWidget *parentWidget) : q(qq), m_urls(), m_parentWidget(parentWidget), m_readOnly(false), m_autoErrorHandling(false) { } //// KFileCopyToMenu::KFileCopyToMenu(QWidget *parentWidget) : QObject(parentWidget), d(new KFileCopyToMenuPrivate(this, parentWidget)) { } KFileCopyToMenu::~KFileCopyToMenu() { delete d; } void KFileCopyToMenu::setUrls(const QList &urls) { d->m_urls = urls; } void KFileCopyToMenu::setReadOnly(bool ro) { d->m_readOnly = ro; } void KFileCopyToMenu::setAutoErrorHandlingEnabled(bool b) { d->m_autoErrorHandling = b; } void KFileCopyToMenu::addActionsTo(QMenu *menu) const { QMenu *mainCopyMenu = new KFileCopyToMainMenu(menu, d, Copy); mainCopyMenu->setTitle(i18nc("@title:menu", "Copy To")); mainCopyMenu->menuAction()->setObjectName(QStringLiteral("copyTo_submenu")); // for the unittest menu->addMenu(mainCopyMenu); if (!d->m_readOnly) { QMenu *mainMoveMenu = new KFileCopyToMainMenu(menu, d, Move); mainMoveMenu->setTitle(i18nc("@title:menu", "Move To")); mainMoveMenu->menuAction()->setObjectName(QStringLiteral("moveTo_submenu")); // for the unittest menu->addMenu(mainMoveMenu); } } //// KFileCopyToMainMenu::KFileCopyToMainMenu(QMenu *parent, KFileCopyToMenuPrivate *_d, MenuType menuType) : QMenu(parent), m_menuType(menuType), m_actionGroup(static_cast(nullptr)), d(_d), m_recentDirsGroup(KSharedConfig::openConfig(), m_menuType == Copy ? "kuick-copy" : "kuick-move") { connect(this, &KFileCopyToMainMenu::aboutToShow, this, &KFileCopyToMainMenu::slotAboutToShow); connect(&m_actionGroup, &QActionGroup::triggered, this, &KFileCopyToMainMenu::slotTriggered); } void KFileCopyToMainMenu::slotAboutToShow() { clear(); KFileCopyToDirectoryMenu *subMenu; // Home Folder subMenu = new KFileCopyToDirectoryMenu(this, this, QDir::homePath()); subMenu->setTitle(i18nc("@title:menu", "Home Folder")); subMenu->setIcon(QIcon::fromTheme(QStringLiteral("go-home"))); QAction *act = addMenu(subMenu); act->setObjectName(QStringLiteral("home")); // Root Folder #ifndef Q_OS_WIN subMenu = new KFileCopyToDirectoryMenu(this, this, QDir::rootPath()); subMenu->setTitle(i18nc("@title:menu", "Root Folder")); subMenu->setIcon(QIcon::fromTheme(QStringLiteral("folder-red"))); act = addMenu(subMenu); act->setObjectName(QStringLiteral("root")); #else foreach (const QFileInfo &info, QDir::drives()) { QString driveIcon = QStringLiteral("drive-harddisk"); const uint type = GetDriveTypeW((wchar_t *)info.absoluteFilePath().utf16()); switch (type) { case DRIVE_REMOVABLE: driveIcon = QStringLiteral("drive-removable-media"); break; case DRIVE_FIXED: driveIcon = QStringLiteral("drive-harddisk"); break; case DRIVE_REMOTE: driveIcon = QStringLiteral("network-server"); break; case DRIVE_CDROM: driveIcon = QStringLiteral("drive-optical"); break; case DRIVE_RAMDISK: case DRIVE_UNKNOWN: case DRIVE_NO_ROOT_DIR: default: driveIcon = QStringLiteral("drive-harddisk"); } subMenu = new KFileCopyToDirectoryMenu(this, this, info.absoluteFilePath()); subMenu->setTitle(info.absoluteFilePath()); subMenu->setIcon(QIcon::fromTheme(driveIcon)); addMenu(subMenu); } #endif // Browse... action, shows a file dialog QAction *browseAction = new QAction(i18nc("@title:menu in Copy To or Move To submenu", "Browse..."), this); browseAction->setObjectName(QStringLiteral("browse")); connect(browseAction, &QAction::triggered, this, &KFileCopyToMainMenu::slotBrowse); addAction(browseAction); addSeparator(); // Qt handles removing it automatically if it's last in the menu, nice. // Recent Destinations const QStringList recentDirs = m_recentDirsGroup.readPathEntry("Paths", QStringList()); for (const QString &recentDir : recentDirs) { const QUrl url = QUrl::fromLocalFile(recentDir); const QString text = KStringHandler::csqueeze(url.toDisplayString(QUrl::PreferLocalFile), 60); // shorten very long paths (#61386) QAction *act = new QAction(text, this); act->setObjectName(recentDir); act->setData(url); m_actionGroup.addAction(act); addAction(act); } } void KFileCopyToMainMenu::slotBrowse() { const QUrl dest = QFileDialog::getExistingDirectoryUrl(d->m_parentWidget ? d->m_parentWidget : this); if (!dest.isEmpty()) { copyOrMoveTo(dest); } } void KFileCopyToMainMenu::slotTriggered(QAction *action) { const QUrl url = action->data().toUrl(); Q_ASSERT(!url.isEmpty()); copyOrMoveTo(url); } void KFileCopyToMainMenu::copyOrMoveTo(const QUrl &dest) { // Insert into the recent destinations list QStringList recentDirs = m_recentDirsGroup.readPathEntry("Paths", QStringList()); const QString niceDest = dest.toDisplayString(QUrl::PreferLocalFile); if (!recentDirs.contains(niceDest)) { // don't change position if already there, moving stuff is bad usability recentDirs.prepend(niceDest); while (recentDirs.size() > 10) { // hardcoded max size recentDirs.removeLast(); } m_recentDirsGroup.writePathEntry("Paths", recentDirs); } // #199549: add a trailing slash to avoid unexpected results when the // dest doesn't exist anymore: it was creating a file with the name of // the now non-existing dest. QUrl dirDest = dest; if (!dirDest.path().endsWith(QLatin1Char('/'))) { dirDest.setPath(dirDest.path() + QLatin1Char('/')); } // And now let's do the copy or move -- with undo/redo support. KIO::CopyJob *job = m_menuType == Copy ? KIO::copy(d->m_urls, dirDest) : KIO::move(d->m_urls, dirDest); KIO::FileUndoManager::self()->recordCopyJob(job); KJobWidgets::setWindow(job, d->m_parentWidget ? d->m_parentWidget : this); job->uiDelegate()->setAutoErrorHandlingEnabled(d->m_autoErrorHandling); connect(job, &KIO::CopyJob::result, this, [this](KJob * job) { emit d->q->error(job->error(), job->errorString()); }); } //// KFileCopyToDirectoryMenu::KFileCopyToDirectoryMenu(QMenu *parent, KFileCopyToMainMenu *mainMenu, const QString &path) : QMenu(parent), m_mainMenu(mainMenu), m_path(path) { if (!m_path.endsWith(QLatin1Char('/'))) { m_path.append(QLatin1Char('/')); } connect(this, &KFileCopyToDirectoryMenu::aboutToShow, this, &KFileCopyToDirectoryMenu::slotAboutToShow); } void KFileCopyToDirectoryMenu::slotAboutToShow() { clear(); QAction *act = new QAction(m_mainMenu->menuType() == Copy ? i18nc("@title:menu", "Copy Here") : i18nc("@title:menu", "Move Here"), this); act->setData(QUrl::fromLocalFile(m_path)); act->setEnabled(QFileInfo(m_path).isWritable()); m_mainMenu->actionGroup().addAction(act); addAction(act); addSeparator(); // Qt handles removing it automatically if it's last in the menu, nice. // List directory // All we need is sub folder names, their permissions, their icon. // KDirLister or KIO::listDir would fetch much more info, and would be async, // and we only care about local directories so we use QDir directly. QDir dir(m_path); const QStringList entries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::LocaleAware); const QMimeDatabase db; const QMimeType dirMime = db.mimeTypeForName(QStringLiteral("inode/directory")); for (const QString &subDir : entries) { QString subPath = m_path + subDir; KFileCopyToDirectoryMenu *subMenu = new KFileCopyToDirectoryMenu(this, m_mainMenu, subPath); QString menuTitle(subDir); // Replace '&' by "&&" to make sure that '&' inside the directory name is displayed // correctly and not misinterpreted as an indicator for a keyboard shortcut subMenu->setTitle(menuTitle.replace(QLatin1Char('&'), QLatin1String("&&"))); const QString iconName = dirMime.iconName(); subMenu->setIcon(QIcon::fromTheme(iconName)); if (QFileInfo(subPath).isSymLink()) { QFont font = subMenu->menuAction()->font(); font.setItalic(true); subMenu->menuAction()->setFont(font); } addMenu(subMenu); } } diff --git a/src/filewidgets/kfilecopytomenu_p.h b/src/filewidgets/kfilecopytomenu_p.h index 09337c7c..ff310955 100644 --- a/src/filewidgets/kfilecopytomenu_p.h +++ b/src/filewidgets/kfilecopytomenu_p.h @@ -1,90 +1,89 @@ /* Copyright 2008, 2015 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KFILECOPYTOMENU_P_H #define KFILECOPYTOMENU_P_H #include #include #include -#include #include class KFileCopyToMenuPrivate { public: KFileCopyToMenuPrivate(KFileCopyToMenu *qq, QWidget *parentWidget); KFileCopyToMenu * const q; QList m_urls; QWidget *m_parentWidget; bool m_readOnly; bool m_autoErrorHandling; }; enum MenuType { Copy, Move }; // The main menu, shown when opening "Copy To" or "Move To" // It contains Home Folder, Root Folder, Browse, and recent destinations class KFileCopyToMainMenu : public QMenu { Q_OBJECT public: KFileCopyToMainMenu(QMenu *parent, KFileCopyToMenuPrivate *d, MenuType menuType); QActionGroup &actionGroup() { return m_actionGroup; // used by submenus } MenuType menuType() const { return m_menuType; // used by submenus } private Q_SLOTS: void slotAboutToShow(); void slotBrowse(); void slotTriggered(QAction *action); private: void copyOrMoveTo(const QUrl &dest); private: MenuType m_menuType; QActionGroup m_actionGroup; KFileCopyToMenuPrivate *d; // this isn't our own d pointer, it's the one for the public class KConfigGroup m_recentDirsGroup; }; // The menu that lists a directory class KFileCopyToDirectoryMenu : public QMenu { Q_OBJECT public: KFileCopyToDirectoryMenu(QMenu *parent, KFileCopyToMainMenu *mainMenu, const QString &path); private Q_SLOTS: void slotAboutToShow(); private: KFileCopyToMainMenu *m_mainMenu; QString m_path; }; #endif diff --git a/src/filewidgets/kfileplaceeditdialog.cpp b/src/filewidgets/kfileplaceeditdialog.cpp index bd65dda1..aab6f4d1 100644 --- a/src/filewidgets/kfileplaceeditdialog.cpp +++ b/src/filewidgets/kfileplaceeditdialog.cpp @@ -1,222 +1,214 @@ /* This file is part of the KDE libraries Copyright (C) 2001,2002,2003 Carsten Pfeiffer Copyright (C) 2007 Kevin Ottens 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, version 2. 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 "kfileplaceeditdialog.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 bool KFilePlaceEditDialog::getInformation(bool allowGlobal, QUrl &url, QString &label, QString &icon, bool isAddingNewPlace, bool &appLocal, int iconSize, QWidget *parent) { KFilePlaceEditDialog *dialog = new KFilePlaceEditDialog(allowGlobal, url, label, icon, isAddingNewPlace, appLocal, iconSize, parent); if (dialog->exec() == QDialog::Accepted) { // set the return parameters url = dialog->url(); label = dialog->label(); if (dialog->isIconEditable()) { icon = dialog->icon(); } appLocal = dialog->applicationLocal(); delete dialog; return true; } delete dialog; return false; } KFilePlaceEditDialog::KFilePlaceEditDialog(bool allowGlobal, const QUrl &url, const QString &label, const QString &icon, bool isAddingNewPlace, bool appLocal, int iconSize, QWidget *parent) : QDialog(parent), m_iconButton(nullptr) { if (isAddingNewPlace) { setWindowTitle(i18n("Add Places Entry")); } else { setWindowTitle(i18n("Edit Places Entry")); } setModal(true); QVBoxLayout *box = new QVBoxLayout(this); QFormLayout *layout = new QFormLayout(); box->addLayout(layout); QString whatsThisText = i18n("This is the text that will appear in the Places panel.

" "The label should consist of one or two words " "that will help you remember what this entry refers to. " "If you do not enter a label, it will be derived from " "the location's URL.
"); m_labelEdit = new QLineEdit(this); layout->addRow(i18n("L&abel:"), m_labelEdit); m_labelEdit->setText(label); m_labelEdit->setPlaceholderText(i18n("Enter descriptive label here")); m_labelEdit->setWhatsThis(whatsThisText); layout->labelForField(m_labelEdit)->setWhatsThis(whatsThisText); whatsThisText = i18n("This is the location associated with the entry. Any valid URL may be used. For example:

" "%1
http://www.kde.org
ftp://ftp.kde.org/pub/kde/stable

" "By clicking on the button next to the text edit box you can browse to an " "appropriate URL.
", QDir::homePath()); m_urlEdit = new KUrlRequester(url, this); m_urlEdit->setMode(KFile::Directory); layout->addRow(i18n("&Location:"), m_urlEdit); m_urlEdit->setWhatsThis(whatsThisText); layout->labelForField(m_urlEdit)->setWhatsThis(whatsThisText); // Room for at least 40 chars (average char width is half of height) m_urlEdit->setMinimumWidth(m_urlEdit->fontMetrics().height() * (40 / 2)); if (url.scheme() != QLatin1String("trash")) { whatsThisText = i18n("This is the icon that will appear in the Places panel.

" "Click on the button to select a different icon.
"); m_iconButton = new KIconButton(this); layout->addRow(i18n("Choose an &icon:"), m_iconButton); m_iconButton->setObjectName(QStringLiteral("icon button")); m_iconButton->setIconSize(iconSize); m_iconButton->setIconType(KIconLoader::NoGroup, KIconLoader::Place); if (icon.isEmpty()) { m_iconButton->setIcon(KIO::iconNameForUrl(url)); } else { m_iconButton->setIcon(icon); } m_iconButton->setWhatsThis(whatsThisText); layout->labelForField(m_iconButton)->setWhatsThis(whatsThisText); } if (allowGlobal) { QString appName; appName = QGuiApplication::applicationDisplayName(); if (appName.isEmpty()) { appName = QCoreApplication::applicationName(); } m_appLocal = new QCheckBox(i18n("&Only show when using this application (%1)", appName), this); m_appLocal->setChecked(appLocal); m_appLocal->setWhatsThis(i18n("Select this setting if you want this " "entry to show only when using the current application (%1).

" "If this setting is not selected, the entry will be available in all " "applications.
", appName)); box->addWidget(m_appLocal); } else { m_appLocal = nullptr; } connect(m_urlEdit->lineEdit(), &QLineEdit::textChanged, this, &KFilePlaceEditDialog::urlChanged); if (!label.isEmpty()) { // editing existing entry m_labelEdit->setFocus(); } else { // new entry m_urlEdit->setFocus(); } m_buttonBox = new QDialogButtonBox(this); m_buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); box->addWidget(m_buttonBox); setLayout(box); } KFilePlaceEditDialog::~KFilePlaceEditDialog() { } void KFilePlaceEditDialog::urlChanged(const QString &text) { m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); } QUrl KFilePlaceEditDialog::url() const { return m_urlEdit->url(); } QString KFilePlaceEditDialog::label() const { if (!m_labelEdit->text().isEmpty()) { return m_labelEdit->text(); } // derive descriptive label from the URL QUrl url = m_urlEdit->url(); if (!url.fileName().isEmpty()) { return url.fileName(); } if (!url.host().isEmpty()) { return url.host(); } return url.scheme(); } QString KFilePlaceEditDialog::icon() const { return m_iconButton->icon(); } bool KFilePlaceEditDialog::applicationLocal() const { if (!m_appLocal) { return true; } return m_appLocal->isChecked(); } bool KFilePlaceEditDialog::isIconEditable() const { return m_iconButton; } diff --git a/src/filewidgets/kfileplacesitem_p.h b/src/filewidgets/kfileplacesitem_p.h index be6745b3..3917c737 100644 --- a/src/filewidgets/kfileplacesitem_p.h +++ b/src/filewidgets/kfileplacesitem_p.h @@ -1,122 +1,121 @@ /* This file is part of the KDE project Copyright (C) 2007 Kevin Ottens 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. */ #ifndef KFILEPLACESITEM_P_H #define KFILEPLACESITEM_P_H #include #include -#include #include #include #include #include #include "kfileplacesmodel.h" class KDirLister; namespace Solid { class StorageAccess; class StorageVolume; class StorageDrive; class NetworkShare; class OpticalDisc; class PortableMediaPlayer; } class KFilePlacesItem : public QObject { Q_OBJECT public: enum GroupType { PlacesType, RemoteType, RecentlySavedType, SearchForType, DevicesType, RemovableDevicesType, TagsType }; KFilePlacesItem(KBookmarkManager *manager, const QString &address, const QString &udi = QString()); ~KFilePlacesItem(); QString id() const; bool isDevice() const; KBookmark bookmark() const; void setBookmark(const KBookmark &bookmark); Solid::Device device() const; QVariant data(int role) const; KFilePlacesModel::GroupType groupType() const; bool isHidden() const; void setHidden(bool hide); static KBookmark createBookmark(KBookmarkManager *manager, const QString &label, const QUrl &url, const QString &iconName, KFilePlacesItem *after = nullptr); static KBookmark createSystemBookmark(KBookmarkManager *manager, const QString &untranslatedLabel, const QString &translatedLabel, const QUrl &url, const QString &iconName); static KBookmark createDeviceBookmark(KBookmarkManager *manager, const QString &udi); static KBookmark createTagBookmark(KBookmarkManager *manager, const QString &tag); Q_SIGNALS: void itemChanged(const QString &id); private Q_SLOTS: void onAccessibilityChanged(bool); private: QVariant bookmarkData(int role) const; QVariant deviceData(int role) const; QString iconNameForBookmark(const KBookmark &bookmark) const; static QString generateNewId(); bool updateDeviceInfo(const QString &udi); KBookmarkManager *m_manager; KBookmark m_bookmark; bool m_folderIsEmpty; bool m_isCdrom; bool m_isAccessible; QString m_text; Solid::Device m_device; QPointer m_access; QPointer m_volume; QPointer m_drive; QPointer m_disc; QPointer m_mtp; QPointer m_networkShare; QString m_iconPath; QStringList m_emblems; QString m_groupName; }; #endif diff --git a/src/filewidgets/kfileplacesmodel.cpp b/src/filewidgets/kfileplacesmodel.cpp index 6fcc490c..49abb5e9 100644 --- a/src/filewidgets/kfileplacesmodel.cpp +++ b/src/filewidgets/kfileplacesmodel.cpp @@ -1,1352 +1,1350 @@ /* This file is part of the KDE project Copyright (C) 2007 Kevin Ottens 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 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. */ // TODO: remove me #undef QT_NO_CAST_FROM_ASCII #include "kfileplacesmodel.h" #include "kfileplacesitem_p.h" #ifdef _WIN32_WCE #include "Windows.h" #include "WinBase.h" #endif #include #include #include #include #include -#include #include -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { QString stateNameForGroupType(KFilePlacesModel::GroupType type) { switch (type) { case KFilePlacesModel::PlacesType: return QStringLiteral("GroupState-Places-IsHidden"); case KFilePlacesModel::RemoteType: return QStringLiteral("GroupState-Remote-IsHidden"); case KFilePlacesModel::RecentlySavedType: return QStringLiteral("GroupState-RecentlySaved-IsHidden"); case KFilePlacesModel::SearchForType: return QStringLiteral("GroupState-SearchFor-IsHidden"); case KFilePlacesModel::DevicesType: return QStringLiteral("GroupState-Devices-IsHidden"); case KFilePlacesModel::RemovableDevicesType: return QStringLiteral("GroupState-RemovableDevices-IsHidden"); case KFilePlacesModel::TagsType: return QStringLiteral("GroupState-Tags-IsHidden"); default: Q_UNREACHABLE(); } } static bool isFileIndexingEnabled() { KConfig config(QStringLiteral("baloofilerc")); KConfigGroup basicSettings = config.group("Basic Settings"); return basicSettings.readEntry("Indexing-Enabled", true); } static QString timelineDateString(int year, int month, int day = 0) { const QString dateFormat = QStringLiteral("%1-%2"); QString date = dateFormat.arg(year).arg(month, 2, 10, QLatin1Char('0')); if (day > 0) { date += QStringLiteral("-%1").arg(day, 2, 10, QLatin1Char('0')); } return date; } static QUrl createTimelineUrl(const QUrl &url) { // based on dolphin urls const QString timelinePrefix = QStringLiteral("timeline:") + QLatin1Char('/'); QUrl timelineUrl; const QString path = url.toDisplayString(QUrl::PreferLocalFile); if (path.endsWith(QLatin1String("/yesterday"))) { const QDate date = QDate::currentDate().addDays(-1); const int year = date.year(); const int month = date.month(); const int day = date.day(); timelineUrl = QUrl(timelinePrefix + timelineDateString(year, month) + QLatin1Char('/') + timelineDateString(year, month, day)); } else if (path.endsWith(QLatin1String("/thismonth"))) { const QDate date = QDate::currentDate(); timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month())); } else if (path.endsWith(QLatin1String("/lastmonth"))) { const QDate date = QDate::currentDate().addMonths(-1); timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month())); } else { Q_ASSERT(path.endsWith(QLatin1String("/today"))); timelineUrl = url; } return timelineUrl; } static QUrl createSearchUrl(const QUrl &url) { QUrl searchUrl = url; const QString path = url.toDisplayString(QUrl::PreferLocalFile); const QStringList validSearchPaths = { QStringLiteral("/documents"), QStringLiteral("/images"), QStringLiteral("/audio"), QStringLiteral("/videos") }; for (const QString &validPath : validSearchPaths) { if (path.endsWith(validPath)) { searchUrl.setScheme(QStringLiteral("baloosearch")); return searchUrl; } } qWarning() << "Invalid search url:" << url; return searchUrl; } } class Q_DECL_HIDDEN KFilePlacesModel::Private { public: explicit Private(KFilePlacesModel *self) : q(self), bookmarkManager(nullptr), fileIndexingEnabled(isFileIndexingEnabled()), tags(), tagsLister(new KCoreDirLister()) { if (KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) { connect(tagsLister, &KCoreDirLister::itemsAdded, q, [this](const QUrl&, const KFileItemList& items) { if(tags.isEmpty()) { QList existingBookmarks; KBookmarkGroup root = bookmarkManager->root(); KBookmark bookmark = root.first(); while (!bookmark.isNull()) { existingBookmarks.append(bookmark.url()); bookmark = root.next(bookmark); } if (!existingBookmarks.contains(QUrl(tagsUrlBase))) { KBookmark alltags = KFilePlacesItem::createSystemBookmark(bookmarkManager, QStringLiteral("All tags"), i18n("All tags"), QUrl(tagsUrlBase), QStringLiteral("tag")); } } for (const KFileItem &item: items) { const QString name = item.name(); if (!tags.contains(name)) { tags.append(name); } } _k_reloadBookmarks(); }); connect(tagsLister, &KCoreDirLister::itemsDeleted, q, [this](const KFileItemList& items) { for (const KFileItem &item: items) { tags.removeAll(item.name()); } _k_reloadBookmarks(); }); tagsLister->openUrl(QUrl(tagsUrlBase), KCoreDirLister::OpenUrlFlag::Reload); } } ~Private() { qDeleteAll(items); } KFilePlacesModel * const q; QList items; QVector availableDevices; QMap setupInProgress; QStringList supportedSchemes; Solid::Predicate predicate; KBookmarkManager *bookmarkManager; const bool fileIndexingEnabled; QString alternativeApplicationName; void reloadAndSignal(); QList loadBookmarkList(); int findNearestPosition(int source, int target); QVector tags; const QString tagsUrlBase = QStringLiteral("tags:/"); KCoreDirLister* tagsLister; void _k_initDeviceList(); void _k_deviceAdded(const QString &udi); void _k_deviceRemoved(const QString &udi); void _k_itemChanged(const QString &udi); void _k_reloadBookmarks(); void _k_storageSetupDone(Solid::ErrorType error, const QVariant &errorData); void _k_storageTeardownDone(Solid::ErrorType error, const QVariant &errorData); private: bool isBalooUrl(const QUrl &url) const; }; KFilePlacesModel::KFilePlacesModel(const QString &alternativeApplicationName, QObject *parent) : QAbstractItemModel(parent), d(new Private(this)) { const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/user-places.xbel"); d->bookmarkManager = KBookmarkManager::managerForExternalFile(file); d->alternativeApplicationName = alternativeApplicationName; // Let's put some places in there if it's empty. KBookmarkGroup root = d->bookmarkManager->root(); const auto setDefaultMetadataItemForGroup = [&root](KFilePlacesModel::GroupType type) { root.setMetaDataItem(stateNameForGroupType(type), QStringLiteral("false")); }; if (root.first().isNull() || !QFile::exists(file)) { // NOTE: The context for these I18N_NOOP2 calls has to be "KFile System Bookmarks". // The real i18nc call is made later, with this context, so the two must match. // // createSystemBookmark actually does nothing with its third argument, // but we have to give it something so the I18N_NOOP2 calls stay here for now. // // (coles, 13th May 2009) KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Home"), I18N_NOOP2("KFile System Bookmarks", "Home"), QUrl::fromLocalFile(QDir::homePath()), QStringLiteral("user-home")); // Some distros may not create various standard XDG folders by default // so check for their existence before adding bookmarks for them const QString desktopFolder = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); if (QDir(desktopFolder).exists()) { KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Desktop"), I18N_NOOP2("KFile System Bookmarks", "Desktop"), QUrl::fromLocalFile(desktopFolder), QStringLiteral("user-desktop")); } const QString documentsFolder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); if (QDir(documentsFolder).exists()) { KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Documents"), I18N_NOOP2("KFile System Bookmarks", "Documents"), QUrl::fromLocalFile(documentsFolder), QStringLiteral("folder-documents")); } const QString downloadFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); if (QDir(downloadFolder).exists()) { KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Downloads"), I18N_NOOP2("KFile System Bookmarks", "Downloads"), QUrl::fromLocalFile(downloadFolder), QStringLiteral("folder-downloads")); } KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Network"), I18N_NOOP2("KFile System Bookmarks", "Network"), QUrl(QStringLiteral("remote:/")), QStringLiteral("folder-network")); KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Trash"), I18N_NOOP2("KFile System Bookmarks", "Trash"), QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash")); setDefaultMetadataItemForGroup(PlacesType); setDefaultMetadataItemForGroup(RemoteType); setDefaultMetadataItemForGroup(DevicesType); setDefaultMetadataItemForGroup(RemovableDevicesType); setDefaultMetadataItemForGroup(TagsType); // Force bookmarks to be saved. If on open/save dialog and the bookmarks are not saved, QFile::exists // will always return false, which opening/closing all the time the open/save dialog would case the // bookmarks to be added once each time, having lots of times each bookmark. (ereslibre) d->bookmarkManager->saveAs(file); } // if baloo is enabled, add new urls even if the bookmark file is not empty if (d->fileIndexingEnabled && root.metaDataItem(QStringLiteral("withBaloo")) != QLatin1String("true")) { root.setMetaDataItem(QStringLiteral("withBaloo"), QStringLiteral("true")); KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Today"), I18N_NOOP2("KFile System Bookmarks", "Today"), QUrl(QStringLiteral("timeline:/today")), QStringLiteral("go-jump-today")); KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Yesterday"), I18N_NOOP2("KFile System Bookmarks", "Yesterday"), QUrl(QStringLiteral("timeline:/yesterday")), QStringLiteral("view-calendar-day")); KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Documents"), I18N_NOOP2("KFile System Bookmarks", "Documents"), QUrl(QStringLiteral("search:/documents")), QStringLiteral("folder-text")); KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Images"), I18N_NOOP2("KFile System Bookmarks", "Images"), QUrl(QStringLiteral("search:/images")), QStringLiteral("folder-images")); KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Audio Files"), I18N_NOOP2("KFile System Bookmarks", "Audio Files"), QUrl(QStringLiteral("search:/audio")), QStringLiteral("folder-sound")); KFilePlacesItem::createSystemBookmark(d->bookmarkManager, QStringLiteral("Videos"), I18N_NOOP2("KFile System Bookmarks", "Videos"), QUrl(QStringLiteral("search:/videos")), QStringLiteral("folder-videos")); setDefaultMetadataItemForGroup(SearchForType); setDefaultMetadataItemForGroup(RecentlySavedType); d->bookmarkManager->save(); } QString predicate(QString::fromLatin1("[[[[ StorageVolume.ignored == false AND [ StorageVolume.usage == 'FileSystem' OR StorageVolume.usage == 'Encrypted' ]]" " OR " "[ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ]]" " OR " "OpticalDisc.availableContent & 'Audio' ]" " OR " "StorageAccess.ignored == false ]")); if (KProtocolInfo::isKnownProtocol(QStringLiteral("mtp"))) { predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'mtp']"); } d->predicate = Solid::Predicate::fromString(predicate); Q_ASSERT(d->predicate.isValid()); connect(d->bookmarkManager, SIGNAL(changed(QString,QString)), this, SLOT(_k_reloadBookmarks())); connect(d->bookmarkManager, SIGNAL(bookmarksChanged(QString)), this, SLOT(_k_reloadBookmarks())); d->_k_reloadBookmarks(); QTimer::singleShot(0, this, SLOT(_k_initDeviceList())); } KFilePlacesModel::KFilePlacesModel(QObject *parent) : KFilePlacesModel({}, parent) { } KFilePlacesModel::~KFilePlacesModel() { delete d; } QUrl KFilePlacesModel::url(const QModelIndex &index) const { return data(index, UrlRole).toUrl(); } bool KFilePlacesModel::setupNeeded(const QModelIndex &index) const { return data(index, SetupNeededRole).toBool(); } QIcon KFilePlacesModel::icon(const QModelIndex &index) const { return data(index, Qt::DecorationRole).value(); } QString KFilePlacesModel::text(const QModelIndex &index) const { return data(index, Qt::DisplayRole).toString(); } bool KFilePlacesModel::isHidden(const QModelIndex &index) const { //Note: we do not want to show an index if its parent is hidden return data(index, HiddenRole).toBool() || isGroupHidden(index); } bool KFilePlacesModel::isGroupHidden(const GroupType type) const { const QString hidden = d->bookmarkManager->root().metaDataItem(stateNameForGroupType(type)); return hidden == QStringLiteral("true") ? true : false; } bool KFilePlacesModel::isGroupHidden(const QModelIndex &index) const { if (!index.isValid()) { return false; } KFilePlacesItem *item = static_cast(index.internalPointer()); return isGroupHidden(item->groupType()); } bool KFilePlacesModel::isDevice(const QModelIndex &index) const { if (!index.isValid()) { return false; } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->isDevice(); } Solid::Device KFilePlacesModel::deviceForIndex(const QModelIndex &index) const { if (!index.isValid()) { return Solid::Device(); } KFilePlacesItem *item = static_cast(index.internalPointer()); if (item->isDevice()) { return item->device(); } else { return Solid::Device(); } } KBookmark KFilePlacesModel::bookmarkForIndex(const QModelIndex &index) const { if (!index.isValid()) { return KBookmark(); } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->bookmark(); } KFilePlacesModel::GroupType KFilePlacesModel::groupType(const QModelIndex &index) const { if (!index.isValid()) { return UnknownType; } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->groupType(); } QModelIndexList KFilePlacesModel::groupIndexes(const KFilePlacesModel::GroupType type) const { if (type == UnknownType) { return QModelIndexList(); } QModelIndexList indexes; const int rows = rowCount(); for (int row = 0; row < rows ; ++row) { const QModelIndex current = index(row, 0); if (groupType(current) == type) { indexes << current; } } return indexes; } QVariant KFilePlacesModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } KFilePlacesItem *item = static_cast(index.internalPointer()); if (role == KFilePlacesModel::GroupHiddenRole) { return isGroupHidden(item->groupType()); } else { return item->data(role); } } QModelIndex KFilePlacesModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0 || row >= d->items.size()) { return QModelIndex(); } if (parent.isValid()) { return QModelIndex(); } return createIndex(row, column, d->items.at(row)); } QModelIndex KFilePlacesModel::parent(const QModelIndex &child) const { Q_UNUSED(child); return QModelIndex(); } int KFilePlacesModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } else { return d->items.size(); } } int KFilePlacesModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) // We only know 1 piece of information for a particular entry return 1; } QModelIndex KFilePlacesModel::closestItem(const QUrl &url) const { int foundRow = -1; int maxLength = 0; // Search the item which is equal to the URL or at least is a parent URL. // If there are more than one possible item URL candidates, choose the item // which covers the bigger range of the URL. for (int row = 0; row < d->items.size(); ++row) { KFilePlacesItem *item = d->items[row]; const QUrl itemUrl(item->data(UrlRole).toUrl()); if (itemUrl.matches(url, QUrl::StripTrailingSlash) || itemUrl.isParentOf(url)) { const int length = itemUrl.toString().length(); if (length > maxLength) { foundRow = row; maxLength = length; } } } if (foundRow == -1) { return QModelIndex(); } else { return createIndex(foundRow, 0, d->items[foundRow]); } } void KFilePlacesModel::Private::_k_initDeviceList() { Solid::DeviceNotifier *notifier = Solid::DeviceNotifier::instance(); connect(notifier, SIGNAL(deviceAdded(QString)), q, SLOT(_k_deviceAdded(QString))); connect(notifier, SIGNAL(deviceRemoved(QString)), q, SLOT(_k_deviceRemoved(QString))); const QList &deviceList = Solid::Device::listFromQuery(predicate); availableDevices.reserve(deviceList.size()); for (const Solid::Device &device : deviceList) { availableDevices << device.udi(); } _k_reloadBookmarks(); } void KFilePlacesModel::Private::_k_deviceAdded(const QString &udi) { Solid::Device d(udi); if (predicate.matches(d)) { availableDevices << udi; _k_reloadBookmarks(); } } void KFilePlacesModel::Private::_k_deviceRemoved(const QString &udi) { auto it = std::find(availableDevices.begin(), availableDevices.end(), udi); if (it != availableDevices.end()) { availableDevices.erase(it); _k_reloadBookmarks(); } } void KFilePlacesModel::Private::_k_itemChanged(const QString &id) { for (int row = 0; row < items.size(); ++row) { if (items.at(row)->id() == id) { QModelIndex index = q->index(row, 0); emit q->dataChanged(index, index); } } } void KFilePlacesModel::Private::_k_reloadBookmarks() { QList currentItems = loadBookmarkList(); QList::Iterator it_i = items.begin(); QList::Iterator it_c = currentItems.begin(); QList::Iterator end_i = items.end(); QList::Iterator end_c = currentItems.end(); while (it_i != end_i || it_c != end_c) { if (it_i == end_i && it_c != end_c) { int row = items.count(); q->beginInsertRows(QModelIndex(), row, row); it_i = items.insert(it_i, *it_c); ++it_i; it_c = currentItems.erase(it_c); end_i = items.end(); end_c = currentItems.end(); q->endInsertRows(); } else if (it_i != end_i && it_c == end_c) { int row = items.indexOf(*it_i); q->beginRemoveRows(QModelIndex(), row, row); delete *it_i; it_i = items.erase(it_i); end_i = items.end(); end_c = currentItems.end(); q->endRemoveRows(); } else if ((*it_i)->id() == (*it_c)->id()) { bool shouldEmit = !((*it_i)->bookmark() == (*it_c)->bookmark()); (*it_i)->setBookmark((*it_c)->bookmark()); if (shouldEmit) { int row = items.indexOf(*it_i); QModelIndex idx = q->index(row, 0); emit q->dataChanged(idx, idx); } ++it_i; ++it_c; } else if ((*it_i)->id() != (*it_c)->id()) { int row = items.indexOf(*it_i); if (it_i + 1 != end_i && (*(it_i + 1))->id() == (*it_c)->id()) { // if the next one matches, it's a remove q->beginRemoveRows(QModelIndex(), row, row); delete *it_i; it_i = items.erase(it_i); end_i = items.end(); end_c = currentItems.end(); q->endRemoveRows(); } else { q->beginInsertRows(QModelIndex(), row, row); it_i = items.insert(it_i, *it_c); ++it_i; it_c = currentItems.erase(it_c); end_i = items.end(); end_c = currentItems.end(); q->endInsertRows(); } } } qDeleteAll(currentItems); currentItems.clear(); } bool KFilePlacesModel::Private::isBalooUrl(const QUrl &url) const { const QString scheme = url.scheme(); return ((scheme == QLatin1String("timeline")) || (scheme == QLatin1String("search"))); } QList KFilePlacesModel::Private::loadBookmarkList() { QList items; KBookmarkGroup root = bookmarkManager->root(); KBookmark bookmark = root.first(); QVector devices = availableDevices; QVector tagsList = tags; while (!bookmark.isNull()) { const QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); const QUrl url = bookmark.url(); const QString tag = bookmark.metaDataItem(QStringLiteral("tag")); if (!udi.isEmpty() || url.isValid()) { QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); // If it's not a tag it's a device if (tag.isEmpty()) { auto it = std::find(devices.begin(), devices.end(), udi); bool deviceAvailable = (it != devices.end()); if (deviceAvailable) { devices.erase(it); } bool allowedHere = appName.isEmpty() || ((appName == QCoreApplication::instance()->applicationName()) || (appName == alternativeApplicationName)); bool isSupportedUrl = isBalooUrl(url) ? fileIndexingEnabled : true; bool isSupportedScheme = supportedSchemes.isEmpty() || supportedSchemes.contains(url.scheme()); if (isSupportedScheme && ((isSupportedUrl && udi.isEmpty() && allowedHere) || deviceAvailable)) { KFilePlacesItem *item; if (deviceAvailable) { item = new KFilePlacesItem(bookmarkManager, bookmark.address(), udi); // TODO: Update bookmark internal element } else { item = new KFilePlacesItem(bookmarkManager, bookmark.address()); } connect(item, SIGNAL(itemChanged(QString)), q, SLOT(_k_itemChanged(QString))); items << item; } } else { auto it = std::find(tagsList.begin(), tagsList.end(), tag); if (it != tagsList.end()) { tagsList.removeAll(tag); KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address()); items << item; connect(item, SIGNAL(itemChanged(QString)), q, SLOT(_k_itemChanged(QString))); } } } bookmark = root.next(bookmark); } // Add bookmarks for the remaining devices, they were previously unknown for (const QString &udi : qAsConst(devices)) { bookmark = KFilePlacesItem::createDeviceBookmark(bookmarkManager, udi); if (!bookmark.isNull()) { KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), udi); connect(item, SIGNAL(itemChanged(QString)), q, SLOT(_k_itemChanged(QString))); // TODO: Update bookmark internal element items << item; } } for (const QString& tag: tagsList) { bookmark = KFilePlacesItem::createTagBookmark(bookmarkManager, tag); if (!bookmark.isNull()) { KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), tag); connect(item, SIGNAL(itemChanged(QString)), q, SLOT(_k_itemChanged(QString))); items << item; } } // return a sorted list based on groups std::stable_sort(items.begin(), items.end(), [](KFilePlacesItem *itemA, KFilePlacesItem *itemB) { return (itemA->groupType() < itemB->groupType()); }); return items; } int KFilePlacesModel::Private::findNearestPosition(int source, int target) { const KFilePlacesItem *item = items.at(source); const KFilePlacesModel::GroupType groupType = item->groupType(); int newTarget = qMin(target, items.count() - 1); // moving inside the same group is ok if ((items.at(newTarget)->groupType() == groupType)) { return target; } if (target > source) { // moving down, move it to the end of the group int groupFooter = source; while (items.at(groupFooter)->groupType() == groupType) { groupFooter++; // end of the list move it there if (groupFooter == items.count()) { break; } } target = groupFooter; } else { // moving up, move it to beginning of the group int groupHead = source; while (items.at(groupHead)->groupType() == groupType) { groupHead--; // beginning of the list move it there if (groupHead == 0) { break; } } target = groupHead; } return target; } void KFilePlacesModel::Private::reloadAndSignal() { bookmarkManager->emitChanged(bookmarkManager->root()); // ... we'll get relisted anyway } Qt::DropActions KFilePlacesModel::supportedDropActions() const { return Qt::ActionMask; } Qt::ItemFlags KFilePlacesModel::flags(const QModelIndex &index) const { Qt::ItemFlags res = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (index.isValid()) { res |= Qt::ItemIsDragEnabled; } if (!index.isValid()) { res |= Qt::ItemIsDropEnabled; } return res; } static QString _k_internalMimetype(const KFilePlacesModel *const self) { return QStringLiteral("application/x-kfileplacesmodel-") + QString::number(reinterpret_cast(self)); } QStringList KFilePlacesModel::mimeTypes() const { QStringList types; types << _k_internalMimetype(this) << QStringLiteral("text/uri-list"); return types; } QMimeData *KFilePlacesModel::mimeData(const QModelIndexList &indexes) const { QList urls; QByteArray itemData; QDataStream stream(&itemData, QIODevice::WriteOnly); for (const QModelIndex &index : qAsConst(indexes)) { QUrl itemUrl = url(index); if (itemUrl.isValid()) { urls << itemUrl; } stream << index.row(); } QMimeData *mimeData = new QMimeData(); if (!urls.isEmpty()) { mimeData->setUrls(urls); } mimeData->setData(_k_internalMimetype(this), itemData); return mimeData; } bool KFilePlacesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { if (action == Qt::IgnoreAction) { return true; } if (column > 0) { return false; } if (row == -1 && parent.isValid()) { return false; // Don't allow to move an item onto another one, // too easy for the user to mess something up // If we really really want to allow copying files this way, // let's do it in the views to get the good old drop menu } if (data->hasFormat(_k_internalMimetype(this))) { // The operation is an internal move QByteArray itemData = data->data(_k_internalMimetype(this)); QDataStream stream(&itemData, QIODevice::ReadOnly); int itemRow; stream >> itemRow; if (!movePlace(itemRow, row)) { return false; } } else if (data->hasFormat(QStringLiteral("text/uri-list"))) { // The operation is an add QMimeDatabase db; KBookmark afterBookmark; if (row == -1) { // The dropped item is moved or added to the last position KFilePlacesItem *lastItem = d->items.last(); afterBookmark = lastItem->bookmark(); } else { // The dropped item is moved or added before position 'row', ie after position 'row-1' if (row > 0) { KFilePlacesItem *afterItem = d->items[row - 1]; afterBookmark = afterItem->bookmark(); } } const QList urls = KUrlMimeData::urlsFromMimeData(data); KBookmarkGroup group = d->bookmarkManager->root(); for (const QUrl &url : urls) { // TODO: use KIO::stat in order to get the UDS_DISPLAY_NAME too KIO::MimetypeJob *job = KIO::mimetype(url); QString mimeString; if (!job->exec()) { mimeString = QStringLiteral("unknown"); } else { mimeString = job->mimetype(); } QMimeType mimetype = db.mimeTypeForName(mimeString); if (!mimetype.isValid()) { qWarning() << "URL not added to Places as mimetype could not be determined!"; continue; } if (!mimetype.inherits(QStringLiteral("inode/directory"))) { // Only directories are allowed continue; } KFileItem item(url, mimetype.name(), S_IFDIR); KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, url.fileName(), url, item.iconName()); group.moveBookmark(bookmark, afterBookmark); afterBookmark = bookmark; } } else { // Oops, shouldn't happen thanks to mimeTypes() qWarning() << ": received wrong mimedata, " << data->formats(); return false; } refresh(); return true; } void KFilePlacesModel::refresh() const { d->reloadAndSignal(); } QUrl KFilePlacesModel::convertedUrl(const QUrl &url) { QUrl newUrl = url; if (url.scheme() == QLatin1String("timeline")) { newUrl = createTimelineUrl(url); } else if (url.scheme() == QLatin1String("search")) { newUrl = createSearchUrl(url); } return newUrl; } void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName) { addPlace(text, url, iconName, appName, QModelIndex()); } void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName, const QModelIndex &after) { KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, text, url, iconName); if (!appName.isEmpty()) { bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName); } if (after.isValid()) { KFilePlacesItem *item = static_cast(after.internalPointer()); d->bookmarkManager->root().moveBookmark(bookmark, item->bookmark()); } refresh(); } void KFilePlacesModel::editPlace(const QModelIndex &index, const QString &text, const QUrl &url, const QString &iconName, const QString &appName) { if (!index.isValid()) { return; } KFilePlacesItem *item = static_cast(index.internalPointer()); if (item->isDevice()) { return; } KBookmark bookmark = item->bookmark(); if (bookmark.isNull()) { return; } bool changed = false; if (text != bookmark.fullText()) { bookmark.setFullText(text); changed = true; } if (url != bookmark.url()) { bookmark.setUrl(url); changed = true; } if (iconName != bookmark.icon()) { bookmark.setIcon(iconName); changed = true; } const QString onlyInApp = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); if (appName != onlyInApp) { bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName); changed = true; } if (changed) { refresh(); emit dataChanged(index, index); } } void KFilePlacesModel::removePlace(const QModelIndex &index) const { if (!index.isValid()) { return; } KFilePlacesItem *item = static_cast(index.internalPointer()); if (item->isDevice()) { return; } KBookmark bookmark = item->bookmark(); if (bookmark.isNull()) { return; } d->bookmarkManager->root().deleteBookmark(bookmark); refresh(); } void KFilePlacesModel::setPlaceHidden(const QModelIndex &index, bool hidden) { if (!index.isValid()) { return; } KFilePlacesItem *item = static_cast(index.internalPointer()); if (item->bookmark().isNull() || item->isHidden() == hidden) { return; } const bool groupHidden = isGroupHidden(item->groupType()); const bool hidingChildOnShownParent = hidden && !groupHidden; const bool showingChildOnShownParent = !hidden && !groupHidden; if (hidingChildOnShownParent || showingChildOnShownParent) { item->setHidden(hidden); d->reloadAndSignal(); emit dataChanged(index, index); } } void KFilePlacesModel::setGroupHidden(const GroupType type, bool hidden) { if (isGroupHidden(type) == hidden) return; d->bookmarkManager->root().setMetaDataItem(stateNameForGroupType(type), (hidden ? QStringLiteral("true") : QStringLiteral("false"))); d->reloadAndSignal(); emit groupHiddenChanged(type, hidden); } bool KFilePlacesModel::movePlace(int itemRow, int row) { KBookmark afterBookmark; if ((itemRow < 0) || (itemRow >= d->items.count())) { return false; } if (row >= d->items.count()) { row = -1; } if (row == -1) { // The dropped item is moved or added to the last position KFilePlacesItem *lastItem = d->items.last(); afterBookmark = lastItem->bookmark(); } else { // The dropped item is moved or added before position 'row', ie after position 'row-1' if (row > 0) { KFilePlacesItem *afterItem = d->items[row - 1]; afterBookmark = afterItem->bookmark(); } } KFilePlacesItem *item = d->items[itemRow]; KBookmark bookmark = item->bookmark(); int destRow = row == -1 ? d->items.count() : row; // avoid move item away from its group destRow = d->findNearestPosition(itemRow, destRow); // The item is not moved when the drop indicator is on either item edge if (itemRow == destRow || itemRow + 1 == destRow) { return false; } beginMoveRows(QModelIndex(), itemRow, itemRow, QModelIndex(), destRow); d->bookmarkManager->root().moveBookmark(bookmark, afterBookmark); // Move item ourselves so that _k_reloadBookmarks() does not consider // the move as a remove + insert. // // 2nd argument of QList::move() expects the final destination index, // but 'row' is the value of the destination index before the moved // item has been removed from its original position. That is why we // adjust if necessary. d->items.move(itemRow, itemRow < destRow ? (destRow - 1) : destRow); endMoveRows(); return true; } int KFilePlacesModel::hiddenCount() const { int rows = rowCount(); int hidden = 0; for (int i = 0; i < rows; ++i) { if (isHidden(index(i, 0))) { hidden++; } } return hidden; } QAction *KFilePlacesModel::teardownActionForIndex(const QModelIndex &index) const { Solid::Device device = deviceForIndex(index); if (device.is() && device.as()->isAccessible()) { Solid::StorageDrive *drive = device.as(); if (drive == nullptr) { drive = device.parent().as(); } bool hotpluggable = false; bool removable = false; if (drive != nullptr) { hotpluggable = drive->isHotpluggable(); removable = drive->isRemovable(); } QString iconName; QString text; QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&")); if (device.is()) { text = i18n("&Release '%1'", label); } else if (removable || hotpluggable) { text = i18n("&Safely Remove '%1'", label); iconName = QStringLiteral("media-eject"); } else { text = i18n("&Unmount '%1'", label); iconName = QStringLiteral("media-eject"); } if (!iconName.isEmpty()) { return new QAction(QIcon::fromTheme(iconName), text, nullptr); } else { return new QAction(text, nullptr); } } return nullptr; } QAction *KFilePlacesModel::ejectActionForIndex(const QModelIndex &index) const { Solid::Device device = deviceForIndex(index); if (device.is()) { QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&")); QString text = i18n("&Eject '%1'", label); return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), text, nullptr); } return nullptr; } void KFilePlacesModel::requestTeardown(const QModelIndex &index) { Solid::Device device = deviceForIndex(index); Solid::StorageAccess *access = device.as(); if (access != nullptr) { connect(access, SIGNAL(teardownDone(Solid::ErrorType,QVariant,QString)), this, SLOT(_k_storageTeardownDone(Solid::ErrorType,QVariant))); access->teardown(); } } void KFilePlacesModel::requestEject(const QModelIndex &index) { Solid::Device device = deviceForIndex(index); Solid::OpticalDrive *drive = device.parent().as(); if (drive != nullptr) { connect(drive, SIGNAL(ejectDone(Solid::ErrorType,QVariant,QString)), this, SLOT(_k_storageTeardownDone(Solid::ErrorType,QVariant))); drive->eject(); } else { QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&")); QString message = i18n("The device '%1' is not a disk and cannot be ejected.", label); emit errorMessage(message); } } void KFilePlacesModel::requestSetup(const QModelIndex &index) { Solid::Device device = deviceForIndex(index); if (device.is() && !d->setupInProgress.contains(device.as()) && !device.as()->isAccessible()) { Solid::StorageAccess *access = device.as(); d->setupInProgress[access] = index; connect(access, SIGNAL(setupDone(Solid::ErrorType,QVariant,QString)), this, SLOT(_k_storageSetupDone(Solid::ErrorType,QVariant))); access->setup(); } } void KFilePlacesModel::Private::_k_storageSetupDone(Solid::ErrorType error, const QVariant &errorData) { QPersistentModelIndex index = setupInProgress.take(q->sender()); if (!index.isValid()) { return; } if (!error) { emit q->setupDone(index, true); } else { if (errorData.isValid()) { emit q->errorMessage(i18n("An error occurred while accessing '%1', the system responded: %2", q->text(index), errorData.toString())); } else { emit q->errorMessage(i18n("An error occurred while accessing '%1'", q->text(index))); } emit q->setupDone(index, false); } } void KFilePlacesModel::Private::_k_storageTeardownDone(Solid::ErrorType error, const QVariant &errorData) { if (error && errorData.isValid()) { emit q->errorMessage(errorData.toString()); } } void KFilePlacesModel::setSupportedSchemes(const QStringList &schemes) { d->supportedSchemes = schemes; d->_k_reloadBookmarks(); } QStringList KFilePlacesModel::supportedSchemes() const { return d->supportedSchemes; } #include "moc_kfileplacesmodel.cpp" diff --git a/src/filewidgets/kfileplacesview.cpp b/src/filewidgets/kfileplacesview.cpp index fcc5da1a..4555b4fd 100644 --- a/src/filewidgets/kfileplacesview.cpp +++ b/src/filewidgets/kfileplacesview.cpp @@ -1,1479 +1,1478 @@ /* 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 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; 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; }; 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 (url.isLocalFile() && contentsOpacity(index) > 0) { const QString mountPointPath = url.toLocalFile(); const KDiskFreeSpaceInfo info = KDiskFreeSpaceInfo::freeSpaceInfo(mountPointPath); 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))); } } } 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; } 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(); 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); } 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())); } } 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())); } } 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/kfilepreviewgenerator.cpp b/src/filewidgets/kfilepreviewgenerator.cpp index c0746a7c..2a64f2c3 100644 --- a/src/filewidgets/kfilepreviewgenerator.cpp +++ b/src/filewidgets/kfilepreviewgenerator.cpp @@ -1,1283 +1,1281 @@ /******************************************************************************* * Copyright (C) 2008-2009 by Peter Penz * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public License * * along with this library; see the file COPYING.LIB. If not, write to * * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "kfilepreviewgenerator.h" #include "defaultviewadapter_p.h" #include // from kiowidgets #include // for HAVE_XRENDER #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include -#include #include #if HAVE_X11 && HAVE_XRENDER # include # include # include #endif /** * If the passed item view is an instance of QListView, expensive * layout operations are blocked in the constructor and are unblocked * again in the destructor. * * This helper class is a workaround for the following huge performance * problem when having directories with several 1000 items: * - each change of an icon emits a dataChanged() signal from the model * - QListView iterates through all items on each dataChanged() signal * and invokes QItemDelegate::sizeHint() * - the sizeHint() implementation of KFileItemDelegate is quite complex, * invoking it 1000 times for each icon change might block the UI * * QListView does not invoke QItemDelegate::sizeHint() when the * uniformItemSize property has been set to true, so this property is * set before exchanging a block of icons. */ class KFilePreviewGenerator::LayoutBlocker { public: LayoutBlocker(QAbstractItemView *view) : m_uniformSizes(false), m_view(qobject_cast(view)) { if (m_view != nullptr) { m_uniformSizes = m_view->uniformItemSizes(); m_view->setUniformItemSizes(true); } } ~LayoutBlocker() { if (m_view != nullptr) { m_view->setUniformItemSizes(m_uniformSizes); /* The QListView did the layout with uniform item * sizes, so trigger a relayout with the expected sizes. */ if (!m_uniformSizes) { m_view->setGridSize(m_view->gridSize()); } } } private: bool m_uniformSizes; QListView *m_view; }; /** Helper class for drawing frames for image previews. */ class KFilePreviewGenerator::TileSet { public: enum { LeftMargin = 3, TopMargin = 2, RightMargin = 3, BottomMargin = 4 }; enum Tile { TopLeftCorner = 0, TopSide, TopRightCorner, LeftSide, RightSide, BottomLeftCorner, BottomSide, BottomRightCorner, NumTiles }; TileSet() { QImage image(8 * 3, 8 * 3, QImage::Format_ARGB32_Premultiplied); QPainter p(&image); p.setCompositionMode(QPainter::CompositionMode_Source); p.fillRect(image.rect(), Qt::transparent); p.fillRect(image.rect().adjusted(3, 3, -3, -3), Qt::black); p.end(); KIO::ImageFilter::shadowBlur(image, 3, Qt::black); QPixmap pixmap = QPixmap::fromImage(image); m_tiles[TopLeftCorner] = pixmap.copy(0, 0, 8, 8); m_tiles[TopSide] = pixmap.copy(8, 0, 8, 8); m_tiles[TopRightCorner] = pixmap.copy(16, 0, 8, 8); m_tiles[LeftSide] = pixmap.copy(0, 8, 8, 8); m_tiles[RightSide] = pixmap.copy(16, 8, 8, 8); m_tiles[BottomLeftCorner] = pixmap.copy(0, 16, 8, 8); m_tiles[BottomSide] = pixmap.copy(8, 16, 8, 8); m_tiles[BottomRightCorner] = pixmap.copy(16, 16, 8, 8); } void paint(QPainter *p, const QRect &r) { p->drawPixmap(r.topLeft(), m_tiles[TopLeftCorner]); if (r.width() - 16 > 0) { p->drawTiledPixmap(r.x() + 8, r.y(), r.width() - 16, 8, m_tiles[TopSide]); } p->drawPixmap(r.right() - 8 + 1, r.y(), m_tiles[TopRightCorner]); if (r.height() - 16 > 0) { p->drawTiledPixmap(r.x(), r.y() + 8, 8, r.height() - 16, m_tiles[LeftSide]); p->drawTiledPixmap(r.right() - 8 + 1, r.y() + 8, 8, r.height() - 16, m_tiles[RightSide]); } p->drawPixmap(r.x(), r.bottom() - 8 + 1, m_tiles[BottomLeftCorner]); if (r.width() - 16 > 0) { p->drawTiledPixmap(r.x() + 8, r.bottom() - 8 + 1, r.width() - 16, 8, m_tiles[BottomSide]); } p->drawPixmap(r.right() - 8 + 1, r.bottom() - 8 + 1, m_tiles[BottomRightCorner]); const QRect contentRect = r.adjusted(LeftMargin + 1, TopMargin + 1, -(RightMargin + 1), -(BottomMargin + 1)); p->fillRect(contentRect, Qt::transparent); } private: QPixmap m_tiles[NumTiles]; }; class Q_DECL_HIDDEN KFilePreviewGenerator::Private { public: Private(KFilePreviewGenerator *parent, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model); ~Private(); /** * Requests a new icon for the item \a index. * @param sequenceIndex If this is zero, the standard icon is requested, else another one. */ void requestSequenceIcon(const QModelIndex &index, int sequenceIndex); /** * Generates previews for the items \a items asynchronously. */ void updateIcons(const KFileItemList &items); /** * Generates previews for the indices within \a topLeft * and \a bottomRight asynchronously. */ void updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight); /** * Adds the preview \a pixmap for the item \a item to the preview * queue and starts a timer which will dispatch the preview queue * later. */ void addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap); /** * Is invoked when the preview job has been finished and * removes the job from the m_previewJobs list. */ void slotPreviewJobFinished(KJob *job); /** Synchronizes the icon of all items with the clipboard of cut items. */ void updateCutItems(); /** * Reset all icons of the items from m_cutItemsCache and clear * the cache. */ void clearCutItemsCache(); /** * Dispatches the preview queue block by block within * time slices. */ void dispatchIconUpdateQueue(); /** * Pauses all icon updates and invokes KFilePreviewGenerator::resumeIconUpdates() * after a short delay. Is invoked as soon as the user has moved * a scrollbar. */ void pauseIconUpdates(); /** * Resumes the icons updates that have been paused after moving the * scrollbar. The previews for the current visible area are * generated first. */ void resumeIconUpdates(); /** * Starts the resolving of the MIME types from * the m_pendingItems queue. */ void startMimeTypeResolving(); /** * Resolves the MIME type for exactly one item of the * m_pendingItems queue. */ void resolveMimeType(); /** * Returns true, if the item \a item has been cut into * the clipboard. */ bool isCutItem(const KFileItem &item) const; /** * Applies a cut-item effect to all given \a items, if they * are marked as cut in the clipboard. */ void applyCutItemEffect(const KFileItemList &items); /** * Applies a frame around the icon. False is returned if * no frame has been added because the icon is too small. */ bool applyImageFrame(QPixmap &icon); /** * Resizes the icon to \a maxSize if the icon size does not * fit into the maximum size. The aspect ratio of the icon * is kept. */ void limitToSize(QPixmap &icon, const QSize &maxSize); /** * Creates previews by starting new preview jobs for the items * and triggers the preview timer. */ void createPreviews(const KFileItemList &items); /** * Helper method for createPreviews(): Starts a preview job for the given * items. For each returned preview addToPreviewQueue() will get invoked. */ void startPreviewJob(const KFileItemList &items, int width, int height); /** Kills all ongoing preview jobs. */ void killPreviewJobs(); /** * Orders the items \a items in a way that the visible items * are moved to the front of the list. When passing this * list to a preview job, the visible items will get generated * first. */ void orderItems(KFileItemList &items); /** * Helper method for KFilePreviewGenerator::updateIcons(). Adds * recursively all items from the model to the list \a list. */ void addItemsToList(const QModelIndex &index, KFileItemList &list); /** * Updates the icons of files that are constantly changed due to a copy * operation. See m_changedItems and m_changedItemsTimer for details. */ void delayedIconUpdate(); /** * Any items that are removed from the model are also removed from m_changedItems. */ void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); /** Remembers the pixmap for an item specified by an URL. */ struct ItemInfo { QUrl url; QPixmap pixmap; }; /** * During the lifetime of a DataChangeObtainer instance changing * the data of the model won't trigger generating a preview. */ class DataChangeObtainer { public: DataChangeObtainer(KFilePreviewGenerator::Private *generator) : m_gen(generator) { ++m_gen->m_internalDataChange; } ~DataChangeObtainer() { --m_gen->m_internalDataChange; } private: KFilePreviewGenerator::Private *m_gen; }; bool m_previewShown; /** * True, if m_pendingItems and m_dispatchedItems should be * cleared when the preview jobs have been finished. */ bool m_clearItemQueues; /** * True if a selection has been done which should cut items. */ bool m_hasCutSelection; /** * True if the updates of icons has been paused by pauseIconUpdates(). * The value is reset by resumeIconUpdates(). */ bool m_iconUpdatesPaused; /** * If the value is 0, the slot * updateIcons(const QModelIndex&, const QModelIndex&) has * been triggered by an external data change. */ int m_internalDataChange; int m_pendingVisibleIconUpdates; KAbstractViewAdapter *m_viewAdapter; QAbstractItemView *m_itemView; QTimer *m_iconUpdateTimer; QTimer *m_scrollAreaTimer; QList m_previewJobs; QPointer m_dirModel; QAbstractProxyModel *m_proxyModel; /** * Set of all items that already have the 'cut' effect applied, together with the pixmap it was applied to * This is used to make sure that the 'cut' effect is applied max. once for each pixmap * * Referencing the pixmaps here imposes no overhead, as they were also given to KDirModel::setData(), * and thus are held anyway. */ QHash m_cutItemsCache; QList m_previews; QMap m_sequenceIndices; /** * When huge items are copied, it must be prevented that a preview gets generated * for each item size change. m_changedItems keeps track of the changed items and it * is assured that a final preview is only done if an item does not change within * at least 5 seconds. */ QHash m_changedItems; QTimer *m_changedItemsTimer; /** * Contains all items where a preview must be generated, but * where the preview job has not dispatched the items yet. */ KFileItemList m_pendingItems; /** * Contains all items, where a preview has already been * generated by the preview jobs. */ KFileItemList m_dispatchedItems; KFileItemList m_resolvedMimeTypes; QStringList m_enabledPlugins; TileSet *m_tileSet; private: KFilePreviewGenerator *const q; }; KFilePreviewGenerator::Private::Private(KFilePreviewGenerator *parent, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model) : m_previewShown(true), m_clearItemQueues(true), m_hasCutSelection(false), m_iconUpdatesPaused(false), m_internalDataChange(0), m_pendingVisibleIconUpdates(0), m_viewAdapter(viewAdapter), m_itemView(nullptr), m_iconUpdateTimer(nullptr), m_scrollAreaTimer(nullptr), m_previewJobs(), m_proxyModel(nullptr), m_cutItemsCache(), m_previews(), m_sequenceIndices(), m_changedItems(), m_changedItemsTimer(nullptr), m_pendingItems(), m_dispatchedItems(), m_resolvedMimeTypes(), m_enabledPlugins(), m_tileSet(nullptr), q(parent) { if (!m_viewAdapter->iconSize().isValid()) { m_previewShown = false; } m_proxyModel = qobject_cast(model); m_dirModel = (m_proxyModel == nullptr) ? qobject_cast(model) : qobject_cast(m_proxyModel->sourceModel()); if (!m_dirModel) { // previews can only get generated for directory models m_previewShown = false; } else { KDirModel *dirModel = m_dirModel.data(); connect(dirModel->dirLister(), SIGNAL(newItems(KFileItemList)), q, SLOT(updateIcons(KFileItemList))); connect(dirModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), q, SLOT(updateIcons(QModelIndex,QModelIndex))); connect(dirModel, SIGNAL(needSequenceIcon(QModelIndex,int)), q, SLOT(requestSequenceIcon(QModelIndex,int))); connect(dirModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), q, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int))); } QClipboard *clipboard = QApplication::clipboard(); connect(clipboard, SIGNAL(dataChanged()), q, SLOT(updateCutItems())); m_iconUpdateTimer = new QTimer(q); m_iconUpdateTimer->setSingleShot(true); m_iconUpdateTimer->setInterval(200); connect(m_iconUpdateTimer, SIGNAL(timeout()), q, SLOT(dispatchIconUpdateQueue())); // Whenever the scrollbar values have been changed, the pending previews should // be reordered in a way that the previews for the visible items are generated // first. The reordering is done with a small delay, so that during moving the // scrollbars the CPU load is kept low. m_scrollAreaTimer = new QTimer(q); m_scrollAreaTimer->setSingleShot(true); m_scrollAreaTimer->setInterval(200); connect(m_scrollAreaTimer, SIGNAL(timeout()), q, SLOT(resumeIconUpdates())); m_viewAdapter->connect(KAbstractViewAdapter::IconSizeChanged, q, SLOT(updateIcons())); m_viewAdapter->connect(KAbstractViewAdapter::ScrollBarValueChanged, q, SLOT(pauseIconUpdates())); m_changedItemsTimer = new QTimer(q); m_changedItemsTimer->setSingleShot(true); m_changedItemsTimer->setInterval(5000); connect(m_changedItemsTimer, SIGNAL(timeout()), q, SLOT(delayedIconUpdate())); KConfigGroup globalConfig(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), "PreviewSettings"); m_enabledPlugins = globalConfig.readEntry("Plugins", QStringList{ QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")}); // Compatibility update: in 4.7, jpegrotatedthumbnail was merged into (or // replaced with?) jpegthumbnail if (m_enabledPlugins.contains(QStringLiteral("jpegrotatedthumbnail"))) { m_enabledPlugins.removeAll(QStringLiteral("jpegrotatedthumbnail")); m_enabledPlugins.append(QStringLiteral("jpegthumbnail")); globalConfig.writeEntry("Plugins", m_enabledPlugins); globalConfig.sync(); } } KFilePreviewGenerator::Private::~Private() { killPreviewJobs(); m_pendingItems.clear(); m_dispatchedItems.clear(); delete m_tileSet; } void KFilePreviewGenerator::Private::requestSequenceIcon(const QModelIndex &index, int sequenceIndex) { if (m_pendingItems.isEmpty() || (sequenceIndex == 0)) { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } KFileItem item = dirModel->itemForIndex(index); if (sequenceIndex == 0) { m_sequenceIndices.remove(item.url()); } else { m_sequenceIndices.insert(item.url(), sequenceIndex); } ///@todo Update directly, without using m_sequenceIndices updateIcons(KFileItemList() << item); } } void KFilePreviewGenerator::Private::updateIcons(const KFileItemList &items) { if (items.isEmpty()) { return; } applyCutItemEffect(items); KFileItemList orderedItems = items; orderItems(orderedItems); m_pendingItems.reserve(m_pendingItems.size() + orderedItems.size()); foreach (const KFileItem &item, orderedItems) { m_pendingItems.append(item); } if (m_previewShown) { createPreviews(orderedItems); } else { startMimeTypeResolving(); } } void KFilePreviewGenerator::Private::updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight) { if (m_internalDataChange > 0) { // QAbstractItemModel::setData() has been invoked internally by the KFilePreviewGenerator. // The signal dataChanged() is connected with this method, but previews only need // to be generated when an external data change has occurred. return; } // dataChanged emitted for the root dir (e.g. permission changes) if (!topLeft.isValid() || !bottomRight.isValid()) { return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } KFileItemList itemList; for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { const QModelIndex index = dirModel->index(row, 0); if (!index.isValid()) { continue; } const KFileItem item = dirModel->itemForIndex(index); Q_ASSERT(!item.isNull()); if (m_previewShown) { const QUrl url = item.url(); const bool hasChanged = m_changedItems.contains(url); // O(1) m_changedItems.insert(url, hasChanged); if (!hasChanged) { // only update the icon if it has not been already updated within // the last 5 seconds (the other icons will be updated later with // the help of m_changedItemsTimer) itemList.append(item); } } else { itemList.append(item); } } updateIcons(itemList); m_changedItemsTimer->start(); } void KFilePreviewGenerator::Private::addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap) { KIO::PreviewJob *senderJob = qobject_cast(q->sender()); Q_ASSERT(senderJob != nullptr); if (senderJob != nullptr) { QMap::iterator it = m_sequenceIndices.find(item.url()); if (senderJob->sequenceIndex() && (it == m_sequenceIndices.end() || *it != senderJob->sequenceIndex())) { return; // the sequence index does not match the one we want } if (!senderJob->sequenceIndex() && it != m_sequenceIndices.end()) { return; // the sequence index does not match the one we want } m_sequenceIndices.erase(it); } if (!m_previewShown) { // the preview has been canceled in the meantime return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } // check whether the item is part of the directory lister (it is possible // that a preview from an old directory lister is received) bool isOldPreview = true; const QUrl itemParentDir = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); foreach (const QUrl &dir, dirModel->dirLister()->directories()) { if (dir == itemParentDir || dir.path().isEmpty()) { isOldPreview = false; break; } } if (isOldPreview) { return; } QPixmap icon = pixmap; const QString mimeType = item.mimetype(); const int slashIndex = mimeType.indexOf(QLatin1Char('/')); const QStringRef mimeTypeGroup = mimeType.leftRef(slashIndex); if ((mimeTypeGroup != QLatin1String("image")) || !applyImageFrame(icon)) { limitToSize(icon, m_viewAdapter->iconSize()); } if (m_hasCutSelection && isCutItem(item)) { // apply the disabled effect to the icon for marking it as "cut item" // and apply the icon to the item KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); icon = iconEffect->apply(icon, KIconLoader::Desktop, KIconLoader::DisabledState); } KIconLoader::global()->drawOverlays(item.overlays(), icon, KIconLoader::Desktop); // remember the preview and URL, so that it can be applied to the model // in KFilePreviewGenerator::dispatchIconUpdateQueue() ItemInfo preview; preview.url = item.url(); preview.pixmap = icon; m_previews.append(preview); m_pendingItems.removeOne(item); m_dispatchedItems.append(item); } void KFilePreviewGenerator::Private::slotPreviewJobFinished(KJob *job) { const int index = m_previewJobs.indexOf(job); m_previewJobs.removeAt(index); if (m_previewJobs.isEmpty()) { foreach (const KFileItem &item, m_pendingItems) { if (item.isMimeTypeKnown()) { m_resolvedMimeTypes.append(item); } } if (m_clearItemQueues) { m_pendingItems.clear(); m_dispatchedItems.clear(); m_pendingVisibleIconUpdates = 0; QMetaObject::invokeMethod(q, "dispatchIconUpdateQueue", Qt::QueuedConnection); } m_sequenceIndices.clear(); // just to be sure that we don't leak anything } } void KFilePreviewGenerator::Private::updateCutItems() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } DataChangeObtainer obt(this); clearCutItemsCache(); KFileItemList items; KDirLister *dirLister = dirModel->dirLister(); const QList dirs = dirLister->directories(); items.reserve(dirs.size()); for (const QUrl &url : dirs) { items << dirLister->itemsForDir(url); } applyCutItemEffect(items); } void KFilePreviewGenerator::Private::clearCutItemsCache() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } DataChangeObtainer obt(this); KFileItemList previews; // Reset the icons of all items that are stored in the cache // to use their default MIME type icon. foreach (const QUrl &url, m_cutItemsCache.keys()) { const QModelIndex index = dirModel->indexForUrl(url); if (index.isValid()) { dirModel->setData(index, QIcon(), Qt::DecorationRole); if (m_previewShown) { previews.append(dirModel->itemForIndex(index)); } } } m_cutItemsCache.clear(); if (!previews.isEmpty()) { // assure that the previews gets restored Q_ASSERT(m_previewShown); orderItems(previews); updateIcons(previews); } } void KFilePreviewGenerator::Private::dispatchIconUpdateQueue() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } const int count = m_previews.count() + m_resolvedMimeTypes.count(); if (count > 0) { LayoutBlocker blocker(m_itemView); DataChangeObtainer obt(this); if (m_previewShown) { // dispatch preview queue foreach (const ItemInfo &preview, m_previews) { const QModelIndex idx = dirModel->indexForUrl(preview.url); if (idx.isValid() && (idx.column() == 0)) { dirModel->setData(idx, QIcon(preview.pixmap), Qt::DecorationRole); } } m_previews.clear(); } // dispatch mime type queue foreach (const KFileItem &item, m_resolvedMimeTypes) { const QModelIndex idx = dirModel->indexForItem(item); dirModel->itemChanged(idx); } m_resolvedMimeTypes.clear(); m_pendingVisibleIconUpdates -= count; if (m_pendingVisibleIconUpdates < 0) { m_pendingVisibleIconUpdates = 0; } } if (m_pendingVisibleIconUpdates > 0) { // As long as there are pending previews for visible items, poll // the preview queue periodically. If there are no pending previews, // the queue is dispatched in slotPreviewJobFinished(). m_iconUpdateTimer->start(); } } void KFilePreviewGenerator::Private::pauseIconUpdates() { m_iconUpdatesPaused = true; foreach (KJob *job, m_previewJobs) { Q_ASSERT(job != nullptr); job->suspend(); } m_scrollAreaTimer->start(); } void KFilePreviewGenerator::Private::resumeIconUpdates() { m_iconUpdatesPaused = false; // Before creating new preview jobs the m_pendingItems queue must be // cleaned up by removing the already dispatched items. Implementation // note: The order of the m_dispatchedItems queue and the m_pendingItems // queue is usually equal. So even when having a lot of elements the // nested loop is no performance bottle neck, as the inner loop is only // entered once in most cases. foreach (const KFileItem &item, m_dispatchedItems) { KFileItemList::iterator begin = m_pendingItems.begin(); KFileItemList::iterator end = m_pendingItems.end(); for (KFileItemList::iterator it = begin; it != end; ++it) { if ((*it).url() == item.url()) { m_pendingItems.erase(it); break; } } } m_dispatchedItems.clear(); m_pendingVisibleIconUpdates = 0; dispatchIconUpdateQueue(); if (m_previewShown) { KFileItemList orderedItems = m_pendingItems; orderItems(orderedItems); // Kill all suspended preview jobs. Usually when a preview job // has been finished, slotPreviewJobFinished() clears all item queues. // This is not wanted in this case, as a new job is created afterwards // for m_pendingItems. m_clearItemQueues = false; killPreviewJobs(); m_clearItemQueues = true; createPreviews(orderedItems); } else { orderItems(m_pendingItems); startMimeTypeResolving(); } } void KFilePreviewGenerator::Private::startMimeTypeResolving() { resolveMimeType(); m_iconUpdateTimer->start(); } void KFilePreviewGenerator::Private::resolveMimeType() { if (m_pendingItems.isEmpty()) { return; } // resolve at least one MIME type bool resolved = false; do { KFileItem item = m_pendingItems.takeFirst(); if (item.isMimeTypeKnown()) { if (m_pendingVisibleIconUpdates > 0) { // The item is visible and the MIME type already known. // Decrease the update counter for dispatchIconUpdateQueue(): --m_pendingVisibleIconUpdates; } } else { // The MIME type is unknown and must get resolved. The // directory model is not informed yet, as a single update // would be very expensive. Instead the item is remembered in // m_resolvedMimeTypes and will be dispatched later // by dispatchIconUpdateQueue(). item.determineMimeType(); m_resolvedMimeTypes.append(item); resolved = true; } } while (!resolved && !m_pendingItems.isEmpty()); if (m_pendingItems.isEmpty()) { // All MIME types have been resolved now. Assure // that the directory model gets informed about // this, so that an update of the icons is done. dispatchIconUpdateQueue(); } else if (!m_iconUpdatesPaused) { // assure that the MIME type of the next // item will be resolved asynchronously QMetaObject::invokeMethod(q, "resolveMimeType", Qt::QueuedConnection); } } bool KFilePreviewGenerator::Private::isCutItem(const KFileItem &item) const { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); const QList cutUrls = KUrlMimeData::urlsFromMimeData(mimeData); return cutUrls.contains(item.url()); } void KFilePreviewGenerator::Private::applyCutItemEffect(const KFileItemList &items) { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); if (!m_hasCutSelection) { return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } const QSet cutUrls = KUrlMimeData::urlsFromMimeData(mimeData).toSet(); DataChangeObtainer obt(this); KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); for (const KFileItem &item : items) { if (cutUrls.contains(item.url())) { const QModelIndex index = dirModel->indexForItem(item); const QVariant value = dirModel->data(index, Qt::DecorationRole); if (value.type() == QVariant::Icon) { const QIcon icon(qvariant_cast(value)); const QSize actualSize = icon.actualSize(m_viewAdapter->iconSize()); QPixmap pixmap = icon.pixmap(actualSize); const QHash::const_iterator cacheIt = m_cutItemsCache.constFind(item.url()); if ((cacheIt == m_cutItemsCache.constEnd()) || (cacheIt->cacheKey() != pixmap.cacheKey())) { pixmap = iconEffect->apply(pixmap, KIconLoader::Desktop, KIconLoader::DisabledState); dirModel->setData(index, QIcon(pixmap), Qt::DecorationRole); m_cutItemsCache.insert(item.url(), pixmap); } } } } } bool KFilePreviewGenerator::Private::applyImageFrame(QPixmap &icon) { const QSize maxSize = m_viewAdapter->iconSize(); const bool applyFrame = (maxSize.width() > KIconLoader::SizeSmallMedium) && (maxSize.height() > KIconLoader::SizeSmallMedium) && !icon.hasAlpha(); if (!applyFrame) { // the maximum size or the image itself is too small for a frame return false; } // resize the icon to the maximum size minus the space required for the frame const QSize size(maxSize.width() - TileSet::LeftMargin - TileSet::RightMargin, maxSize.height() - TileSet::TopMargin - TileSet::BottomMargin); limitToSize(icon, size); if (m_tileSet == nullptr) { m_tileSet = new TileSet(); } QPixmap framedIcon(icon.size().width() + TileSet::LeftMargin + TileSet::RightMargin, icon.size().height() + TileSet::TopMargin + TileSet::BottomMargin); framedIcon.fill(Qt::transparent); QPainter painter; painter.begin(&framedIcon); painter.setCompositionMode(QPainter::CompositionMode_Source); m_tileSet->paint(&painter, framedIcon.rect()); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); painter.drawPixmap(TileSet::LeftMargin, TileSet::TopMargin, icon); painter.end(); icon = framedIcon; return true; } void KFilePreviewGenerator::Private::limitToSize(QPixmap &icon, const QSize &maxSize) { if ((icon.width() > maxSize.width()) || (icon.height() > maxSize.height())) { #pragma message("Cannot use XRender with QPixmap anymore. Find equivalent with Qt API.") #if 0 // HAVE_X11 && HAVE_XRENDER // Assume that the texture size limit is 2048x2048 if ((icon.width() <= 2048) && (icon.height() <= 2048) && icon.x11PictureHandle()) { QSize size = icon.size(); size.scale(maxSize, Qt::KeepAspectRatio); const qreal factor = size.width() / qreal(icon.width()); XTransform xform = {{ { XDoubleToFixed(1 / factor), 0, 0 }, { 0, XDoubleToFixed(1 / factor), 0 }, { 0, 0, XDoubleToFixed(1) } } }; QPixmap pixmap(size); pixmap.fill(Qt::transparent); Display *dpy = QX11Info::display(); XRenderPictureAttributes attr; attr.repeat = RepeatPad; XRenderChangePicture(dpy, icon.x11PictureHandle(), CPRepeat, &attr); XRenderSetPictureFilter(dpy, icon.x11PictureHandle(), FilterBilinear, 0, 0); XRenderSetPictureTransform(dpy, icon.x11PictureHandle(), &xform); XRenderComposite(dpy, PictOpOver, icon.x11PictureHandle(), None, pixmap.x11PictureHandle(), 0, 0, 0, 0, 0, 0, pixmap.width(), pixmap.height()); icon = pixmap; } else { icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::FastTransformation); } #else icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); #endif } } void KFilePreviewGenerator::Private::createPreviews(const KFileItemList &items) { if (items.count() == 0) { return; } const QMimeData *mimeData = QApplication::clipboard()->mimeData(); m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); // PreviewJob internally caches items always with the size of // 128 x 128 pixels or 256 x 256 pixels. A downscaling is done // by PreviewJob if a smaller size is requested. For images KFilePreviewGenerator must // do a downscaling anyhow because of the frame, so in this case only the provided // cache sizes are requested. KFileItemList imageItems; KFileItemList otherItems; QString mimeType; foreach (const KFileItem &item, items) { mimeType = item.mimetype(); const int slashIndex = mimeType.indexOf(QLatin1Char('/')); const QStringRef mimeTypeGroup = mimeType.leftRef(slashIndex); if (mimeTypeGroup == QLatin1String("image")) { imageItems.append(item); } else { otherItems.append(item); } } const QSize size = m_viewAdapter->iconSize(); startPreviewJob(otherItems, size.width(), size.height()); const int cacheSize = (size.width() > 128) || (size.height() > 128) ? 256 : 128; startPreviewJob(imageItems, cacheSize, cacheSize); m_iconUpdateTimer->start(); } void KFilePreviewGenerator::Private::startPreviewJob(const KFileItemList &items, int width, int height) { if (!items.isEmpty()) { KIO::PreviewJob *job = KIO::filePreview(items, QSize(width, height), &m_enabledPlugins); // Set the sequence index to the target. We only need to check if items.count() == 1, // because requestSequenceIcon(..) creates exactly such a request. if (!m_sequenceIndices.isEmpty() && (items.count() == 1)) { QMap::iterator it = m_sequenceIndices.find(items[0].url()); if (it != m_sequenceIndices.end()) { job->setSequenceIndex(*it); } } connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)), q, SLOT(addToPreviewQueue(KFileItem,QPixmap))); connect(job, SIGNAL(finished(KJob*)), q, SLOT(slotPreviewJobFinished(KJob*))); m_previewJobs.append(job); } } void KFilePreviewGenerator::Private::killPreviewJobs() { foreach (KJob *job, m_previewJobs) { Q_ASSERT(job != nullptr); job->kill(); } m_previewJobs.clear(); m_sequenceIndices.clear(); m_iconUpdateTimer->stop(); m_scrollAreaTimer->stop(); m_changedItemsTimer->stop(); } void KFilePreviewGenerator::Private::orderItems(KFileItemList &items) { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } // Order the items in a way that the preview for the visible items // is generated first, as this improves the feeled performance a lot. const bool hasProxy = (m_proxyModel != nullptr); const int itemCount = items.count(); const QRect visibleArea = m_viewAdapter->visibleArea(); QModelIndex dirIndex; QRect itemRect; int insertPos = 0; for (int i = 0; i < itemCount; ++i) { dirIndex = dirModel->indexForItem(items.at(i)); // O(n) (n = number of rows) if (hasProxy) { const QModelIndex proxyIndex = m_proxyModel->mapFromSource(dirIndex); itemRect = m_viewAdapter->visualRect(proxyIndex); } else { itemRect = m_viewAdapter->visualRect(dirIndex); } if (itemRect.intersects(visibleArea)) { // The current item is (at least partly) visible. Move it // to the front of the list, so that the preview is // generated earlier. items.insert(insertPos, items.at(i)); items.removeAt(i + 1); ++insertPos; ++m_pendingVisibleIconUpdates; } } } void KFilePreviewGenerator::Private::addItemsToList(const QModelIndex &index, KFileItemList &list) { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } const int rowCount = dirModel->rowCount(index); for (int row = 0; row < rowCount; ++row) { const QModelIndex subIndex = dirModel->index(row, 0, index); KFileItem item = dirModel->itemForIndex(subIndex); list.append(item); if (dirModel->rowCount(subIndex) > 0) { // the model is hierarchical (treeview) addItemsToList(subIndex, list); } } } void KFilePreviewGenerator::Private::delayedIconUpdate() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } // Precondition: No items have been changed within the last // 5 seconds. This means that items that have been changed constantly // due to a copy operation should be updated now. KFileItemList itemList; QHash::const_iterator it = m_changedItems.constBegin(); while (it != m_changedItems.constEnd()) { const bool hasChanged = it.value(); if (hasChanged) { const QModelIndex index = dirModel->indexForUrl(it.key()); const KFileItem item = dirModel->itemForIndex(index); itemList.append(item); } ++it; } m_changedItems.clear(); updateIcons(itemList); } void KFilePreviewGenerator::Private::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { if (m_changedItems.isEmpty()) { return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } for (int row = start; row <= end; row++) { const QModelIndex index = dirModel->index(row, 0, parent); const KFileItem item = dirModel->itemForIndex(index); if (!item.isNull()) { m_changedItems.remove(item.url()); } if (dirModel->hasChildren(index)) { rowsAboutToBeRemoved(index, 0, dirModel->rowCount(index) - 1); } } } KFilePreviewGenerator::KFilePreviewGenerator(QAbstractItemView *parent) : QObject(parent), d(new Private(this, new KIO::DefaultViewAdapter(parent, this), parent->model())) { d->m_itemView = parent; } KFilePreviewGenerator::KFilePreviewGenerator(KAbstractViewAdapter *parent, QAbstractProxyModel *model) : QObject(parent), d(new Private(this, parent, model)) { } KFilePreviewGenerator::~KFilePreviewGenerator() { delete d; } void KFilePreviewGenerator::setPreviewShown(bool show) { if (d->m_previewShown == show) { return; } KDirModel *dirModel = d->m_dirModel.data(); if (show && (!d->m_viewAdapter->iconSize().isValid() || !dirModel)) { // The view must provide an icon size and a directory model, // otherwise the showing the previews will get ignored return; } d->m_previewShown = show; if (!show) { dirModel->clearAllPreviews(); } updateIcons(); } bool KFilePreviewGenerator::isPreviewShown() const { return d->m_previewShown; } // deprecated (use updateIcons() instead) void KFilePreviewGenerator::updatePreviews() { updateIcons(); } void KFilePreviewGenerator::updateIcons() { d->killPreviewJobs(); d->clearCutItemsCache(); d->m_pendingItems.clear(); d->m_dispatchedItems.clear(); KFileItemList itemList; d->addItemsToList(QModelIndex(), itemList); d->updateIcons(itemList); } void KFilePreviewGenerator::cancelPreviews() { d->killPreviewJobs(); d->m_pendingItems.clear(); d->m_dispatchedItems.clear(); updateIcons(); } void KFilePreviewGenerator::setEnabledPlugins(const QStringList &plugins) { d->m_enabledPlugins = plugins; } QStringList KFilePreviewGenerator::enabledPlugins() const { return d->m_enabledPlugins; } #include "moc_kfilepreviewgenerator.cpp" diff --git a/src/filewidgets/kfilewidget.cpp b/src/filewidgets/kfilewidget.cpp index f2448f02..82f489f9 100644 --- a/src/filewidgets/kfilewidget.cpp +++ b/src/filewidgets/kfilewidget.cpp @@ -1,2965 +1,2966 @@ // -*- c++ -*- /* This file is part of the KDE libraries Copyright (C) 1997, 1998 Richard Moore 1998 Stephan Kulow 1998 Daniel Grana 1999,2000,2001,2002,2003 Carsten Pfeiffer 2003 Clarence Dang 2007 David Faure 2008 Rafael Fernández López This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfilewidget.h" #include "../pathhelpers_p.h" #include "kfileplacesview.h" #include "kfileplacesmodel.h" #include "kfilebookmarkhandler_p.h" #include "kurlcombobox.h" #include "kurlnavigator.h" #include "kfilepreviewgenerator.h" #include "kfilewidgetdocktitlebar_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include class KFileWidgetPrivate { public: explicit KFileWidgetPrivate(KFileWidget *widget) : q(widget), boxLayout(nullptr), placesDock(nullptr), placesView(nullptr), placesViewSplitter(nullptr), placesViewWidth(-1), labeledCustomWidget(nullptr), bottomCustomWidget(nullptr), autoSelectExtCheckBox(nullptr), operationMode(KFileWidget::Opening), bookmarkHandler(nullptr), toolbar(nullptr), locationEdit(nullptr), ops(nullptr), filterWidget(nullptr), autoSelectExtChecked(false), keepLocation(false), hasView(false), hasDefaultFilter(false), inAccept(false), dummyAdded(false), confirmOverwrite(false), differentHierarchyLevelItemsEntered(false), iconSizeSlider(nullptr), zoomOutAction(nullptr), zoomInAction(nullptr) { } ~KFileWidgetPrivate() { delete bookmarkHandler; // Should be deleted before ops! delete ops; } void updateLocationWhatsThis(); void updateAutoSelectExtension(); void initSpeedbar(); void setPlacesViewSplitterSizes(); void setLafBoxColumnWidth(); void initGUI(); void readViewConfig(); void writeViewConfig(); void setNonExtSelection(); void setLocationText(const QUrl &); void setLocationText(const QList &); void appendExtension(QUrl &url); void updateLocationEditExtension(const QString &); QString findMatchingFilter(const QString &filter, const QString &filename) const; void updateFilter(); void updateFilterText(); QList &parseSelectedUrls(); /** * Parses the string "line" for files. If line doesn't contain any ", the * whole line will be interpreted as one file. If the number of " is odd, * an empty list will be returned. Otherwise, all items enclosed in " " * will be returned as correct urls. */ QList tokenize(const QString &line) const; /** * Reads the recent used files and inserts them into the location combobox */ void readRecentFiles(); /** * Saves the entries from the location combobox. */ void saveRecentFiles(); /** * called when an item is highlighted/selected in multiselection mode. * handles setting the locationEdit. */ void multiSelectionChanged(); /** * Returns the absolute version of the URL specified in locationEdit. */ QUrl getCompleteUrl(const QString &) const; /** * Sets the dummy entry on the history combo box. If the dummy entry * already exists, it is overwritten with this information. */ void setDummyHistoryEntry(const QString &text, const QPixmap &icon = QPixmap(), bool usePreviousPixmapIfNull = true); /** * Removes the dummy entry of the history combo box. */ void removeDummyHistoryEntry(); /** * Asks for overwrite confirmation using a KMessageBox and returns * true if the user accepts. * * @since 4.2 */ bool toOverwrite(const QUrl &); // private slots void _k_slotLocationChanged(const QString &); void _k_urlEntered(const QUrl &); void _k_enterUrl(const QUrl &); void _k_enterUrl(const QString &); void _k_locationAccepted(const QString &); void _k_slotFilterChanged(); void _k_fileHighlighted(const KFileItem &); void _k_fileSelected(const KFileItem &); void _k_slotLoadingFinished(); void _k_fileCompletion(const QString &); void _k_toggleSpeedbar(bool); void _k_toggleBookmarks(bool); void _k_slotAutoSelectExtClicked(); void _k_placesViewSplitterMoved(int, int); void _k_activateUrlNavigator(); void _k_zoomOutIconsSize(); void _k_zoomInIconsSize(); void _k_slotIconSizeSliderMoved(int); void _k_slotIconSizeChanged(int); void _k_slotViewDoubleClicked(const QModelIndex&); void _k_slotViewKeyEnterReturnPressed(); void addToRecentDocuments(); QString locationEditCurrentText() const; /** * KIO::NetAccess::mostLocalUrl local replacement. * This method won't show any progress dialogs for stating, since * they are very annoying when stating. */ QUrl mostLocalUrl(const QUrl &url); void setInlinePreviewShown(bool show); KFileWidget * const q; // the last selected url QUrl url; // the selected filenames in multiselection mode -- FIXME QString filenames; // now following all kind of widgets, that I need to rebuild // the geometry management QBoxLayout *boxLayout; QGridLayout *lafBox; QVBoxLayout *vbox; QLabel *locationLabel; QWidget *opsWidget; QWidget *pathSpacer; QLabel *filterLabel; KUrlNavigator *urlNavigator; QPushButton *okButton, *cancelButton; QDockWidget *placesDock; KFilePlacesView *placesView; QSplitter *placesViewSplitter; // caches the places view width. This value will be updated when the splitter // is moved. This allows us to properly set a value when the dialog itself // is resized int placesViewWidth; QWidget *labeledCustomWidget; QWidget *bottomCustomWidget; // Automatically Select Extension stuff QCheckBox *autoSelectExtCheckBox; QString extension; // current extension for this filter QList statJobs; QList urlList; //the list of selected urls KFileWidget::OperationMode operationMode; // The file class used for KRecentDirs QString fileClass; KFileBookmarkHandler *bookmarkHandler; KActionMenu *bookmarkButton; KToolBar *toolbar; KUrlComboBox *locationEdit; KDirOperator *ops; KFileFilterCombo *filterWidget; QTimer filterDelayTimer; KFilePlacesModel *model; // whether or not the _user_ has checked the above box bool autoSelectExtChecked : 1; // indicates if the location edit should be kept or cleared when changing // directories bool keepLocation : 1; // the KDirOperators view is set in KFileWidget::show(), so to avoid // setting it again and again, we have this nice little boolean :) bool hasView : 1; bool hasDefaultFilter : 1; // necessary for the operationMode bool autoDirectoryFollowing : 1; bool inAccept : 1; // true between beginning and end of accept() bool dummyAdded : 1; // if the dummy item has been added. This prevents the combo from having a // blank item added when loaded bool confirmOverwrite : 1; bool differentHierarchyLevelItemsEntered; QSlider *iconSizeSlider; QAction *zoomOutAction; QAction *zoomInAction; // The group which stores app-specific settings. These settings are recent // files and urls. Visual settings (view mode, sorting criteria...) are not // app-specific and are stored in kdeglobals KConfigGroup configGroup; }; Q_GLOBAL_STATIC(QUrl, lastDirectory) // to set the start path static const char autocompletionWhatsThisText[] = I18N_NOOP("While typing in the text area, you may be presented " "with possible matches. " "This feature can be controlled by clicking with the right mouse button " "and selecting a preferred mode from the Text Completion menu."); // returns true if the string contains ":/" sequence, where is at least 2 alpha chars static bool containsProtocolSection(const QString &string) { int len = string.length(); static const char prot[] = ":/"; for (int i = 0; i < len;) { i = string.indexOf(QLatin1String(prot), i); if (i == -1) { return false; } int j = i - 1; for (; j >= 0; j--) { const QChar &ch(string[j]); if (ch.toLatin1() == 0 || !ch.isLetter()) { break; } if (ch.isSpace() && (i - j - 1) >= 2) { return true; } } if (j < 0 && i >= 2) { return true; // at least two letters before ":/" } i += 3; // skip : and / and one char } return false; } // this string-to-url conversion function handles relative paths, full paths and URLs // without the http-prepending that QUrl::fromUserInput does. static QUrl urlFromString(const QString& str) { if (QDir::isAbsolutePath(str)) { return QUrl::fromLocalFile(str); } QUrl url(str); if (url.isRelative()) { url.clear(); url.setPath(str); } return url; } KFileWidget::KFileWidget(const QUrl &_startDir, QWidget *parent) : QWidget(parent), d(new KFileWidgetPrivate(this)) { QUrl startDir(_startDir); // qDebug() << "startDir" << startDir; QString filename; d->okButton = new QPushButton(this); KGuiItem::assign(d->okButton, KStandardGuiItem::ok()); d->okButton->setDefault(true); d->cancelButton = new QPushButton(this); KGuiItem::assign(d->cancelButton, KStandardGuiItem::cancel()); // The dialog shows them d->okButton->hide(); d->cancelButton->hide(); d->opsWidget = new QWidget(this); QVBoxLayout *opsWidgetLayout = new QVBoxLayout(d->opsWidget); opsWidgetLayout->setContentsMargins(0, 0, 0, 0); opsWidgetLayout->setSpacing(0); //d->toolbar = new KToolBar(this, true); d->toolbar = new KToolBar(d->opsWidget, true); d->toolbar->setObjectName(QStringLiteral("KFileWidget::toolbar")); d->toolbar->setMovable(false); opsWidgetLayout->addWidget(d->toolbar); d->model = new KFilePlacesModel(this); // Resolve this now so that a 'kfiledialog:' URL, if specified, // does not get inserted into the urlNavigator history. d->url = getStartUrl(startDir, d->fileClass, filename); startDir = d->url; // Don't pass startDir to the KUrlNavigator at this stage: as well as // the above, it may also contain a file name which should not get // inserted in that form into the old-style navigation bar history. // Wait until the KIO::stat has been done later. // // The stat cannot be done before this point, bug 172678. d->urlNavigator = new KUrlNavigator(d->model, QUrl(), d->opsWidget); //d->toolbar); d->urlNavigator->setPlacesSelectorVisible(false); opsWidgetLayout->addWidget(d->urlNavigator); QUrl u; KUrlComboBox *pathCombo = d->urlNavigator->editor(); #ifdef Q_OS_WIN #if 0 foreach (const QFileInfo &drive, QFSFileEngine::drives()) { u = QUrl::fromLocalFile(drive.filePath()); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), i18n("Drive: %1", u.toLocalFile())); } #else #pragma message("QT5 PORT") #endif #else u = QUrl::fromLocalFile(QDir::rootPath()); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), u.toLocalFile()); #endif u = QUrl::fromLocalFile(QDir::homePath()); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), u.toLocalFile()); QUrl docPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); if (u.adjusted(QUrl::StripTrailingSlash) != docPath.adjusted(QUrl::StripTrailingSlash) && QDir(docPath.toLocalFile()).exists()) { pathCombo->addDefaultUrl(docPath, KIO::pixmapForUrl(docPath, 0, KIconLoader::Small), docPath.toLocalFile()); } u = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), u.toLocalFile()); d->ops = new KDirOperator(QUrl(), d->opsWidget); d->ops->setObjectName(QStringLiteral("KFileWidget::ops")); d->ops->setIsSaving(d->operationMode == Saving); opsWidgetLayout->addWidget(d->ops); connect(d->ops, SIGNAL(urlEntered(QUrl)), SLOT(_k_urlEntered(QUrl))); connect(d->ops, SIGNAL(fileHighlighted(KFileItem)), SLOT(_k_fileHighlighted(KFileItem))); connect(d->ops, SIGNAL(fileSelected(KFileItem)), SLOT(_k_fileSelected(KFileItem))); connect(d->ops, SIGNAL(finishedLoading()), SLOT(_k_slotLoadingFinished())); connect(d->ops, SIGNAL(keyEnterReturnPressed()), SLOT(_k_slotViewKeyEnterReturnPressed())); d->ops->setupMenu(KDirOperator::SortActions | KDirOperator::FileActions | KDirOperator::ViewActions); KActionCollection *coll = d->ops->actionCollection(); coll->addAssociatedWidget(this); // add nav items to the toolbar // // NOTE: The order of the button icons here differs from that // found in the file manager and web browser, but has been discussed // and agreed upon on the kde-core-devel mailing list: // // http://lists.kde.org/?l=kde-core-devel&m=116888382514090&w=2 coll->action(QStringLiteral("up"))->setWhatsThis(i18n("Click this button to enter the parent folder.

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

" "These bookmarks are specific to the file dialog, but otherwise operate " "like bookmarks elsewhere in KDE.
")); toolbar->addAction(bookmarkButton); } else if (bookmarkHandler) { delete bookmarkHandler; bookmarkHandler = nullptr; delete bookmarkButton; bookmarkButton = nullptr; } static_cast(q->actionCollection()->action(QStringLiteral("toggleBookmarks")))->setChecked(show); } // static, overloaded QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass) { QString fileName; // result discarded return getStartUrl(startDir, recentDirClass, fileName); } // static, overloaded QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass, QString &fileName) { recentDirClass.clear(); fileName.clear(); QUrl ret; bool useDefaultStartDir = startDir.isEmpty(); if (!useDefaultStartDir) { if (startDir.scheme() == QLatin1String("kfiledialog")) { // The startDir URL with this protocol may be in the format: // directory() fileName() // 1. kfiledialog:///keyword "/" keyword // 2. kfiledialog:///keyword?global "/" keyword // 3. kfiledialog:///keyword/ "/" keyword // 4. kfiledialog:///keyword/?global "/" keyword // 5. kfiledialog:///keyword/filename /keyword filename // 6. kfiledialog:///keyword/filename?global /keyword filename QString keyword; QString urlDir = startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); QString urlFile = startDir.fileName(); if (urlDir == QLatin1String("/")) { // '1'..'4' above keyword = urlFile; fileName.clear(); } else { // '5' or '6' above keyword = urlDir.mid(1); fileName = urlFile; } if (startDir.query() == QLatin1String("global")) { recentDirClass = QStringLiteral("::%1").arg(keyword); } else { recentDirClass = QStringLiteral(":%1").arg(keyword); } ret = QUrl::fromLocalFile(KRecentDirs::dir(recentDirClass)); } else { // not special "kfiledialog" URL // "foo.png" only gives us a file name, the default start dir will be used. // "file:foo.png" (from KHTML/webkit, due to fromPath()) means the same // (and is the reason why we don't just use QUrl::isRelative()). // In all other cases (startDir contains a directory path, or has no // fileName for us anyway, such as smb://), startDir is indeed a dir url. if (!startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty() || startDir.fileName().isEmpty()) { // can use start directory ret = startDir; // will be checked by stat later // If we won't be able to list it (e.g. http), then use default if (!KProtocolManager::supportsListing(ret)) { useDefaultStartDir = true; fileName = startDir.fileName(); } } else { // file name only fileName = startDir.fileName(); useDefaultStartDir = true; } } } if (useDefaultStartDir) { if (lastDirectory()->isEmpty()) { *lastDirectory() = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); const QUrl home(QUrl::fromLocalFile(QDir::homePath())); // if there is no docpath set (== home dir), we prefer the current // directory over it. We also prefer the homedir when our CWD is // different from our homedirectory or when the document dir // does not exist if (lastDirectory()->adjusted(QUrl::StripTrailingSlash) == home.adjusted(QUrl::StripTrailingSlash) || QDir::currentPath() != QDir::homePath() || !QDir(lastDirectory()->toLocalFile()).exists()) { *lastDirectory() = QUrl::fromLocalFile(QDir::currentPath()); } } ret = *lastDirectory(); } // qDebug() << "for" << startDir << "->" << ret << "recentDirClass" << recentDirClass << "fileName" << fileName; return ret; } void KFileWidget::setStartDir(const QUrl &directory) { if (directory.isValid()) { *lastDirectory() = directory; } } void KFileWidgetPrivate::setNonExtSelection() { // Enhanced rename: Don't highlight the file extension. QString filename = locationEditCurrentText(); QMimeDatabase db; QString extension = db.suffixForFileName(filename); if (!extension.isEmpty()) { locationEdit->lineEdit()->setSelection(0, filename.length() - extension.length() - 1); } else { int lastDot = filename.lastIndexOf(QLatin1Char('.')); if (lastDot > 0) { locationEdit->lineEdit()->setSelection(0, lastDot); } else { locationEdit->lineEdit()->selectAll(); } } } // Sets the filter text to "File type" if the dialog is saving and a mimetype // filter has been set; otherwise, the text is "Filter:" void KFileWidgetPrivate::updateFilterText() { QString label; QString whatsThisText; if (operationMode == KFileWidget::Saving && filterWidget->isMimeFilter()) { label = i18n("&File type:"); whatsThisText = i18n("This is the file type selector. It is used to select the format that the file will be saved as."); } else { label = i18n("&Filter:"); whatsThisText = i18n("This is the filter to apply to the file list. " "File names that do not match the filter will not be shown.

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

" "Wildcards such as * and ? are allowed.

"); } if (filterLabel) { filterLabel->setText(label); filterLabel->setWhatsThis(whatsThisText); } if (filterWidget) { filterWidget->setWhatsThis(whatsThisText); } } KToolBar *KFileWidget::toolBar() const { return d->toolbar; } void KFileWidget::setCustomWidget(QWidget *widget) { delete d->bottomCustomWidget; d->bottomCustomWidget = widget; // add it to the dialog, below the filter list box. // Change the parent so that this widget is a child of the main widget d->bottomCustomWidget->setParent(this); d->vbox->addWidget(d->bottomCustomWidget); //d->vbox->addSpacing(3); // can't do this every time... // FIXME: This should adjust the tab orders so that the custom widget // comes after the Cancel button. The code appears to do this, but the result // somehow screws up the tab order of the file path combo box. Not a major // problem, but ideally the tab order with a custom widget should be // the same as the order without one. setTabOrder(d->cancelButton, d->bottomCustomWidget); setTabOrder(d->bottomCustomWidget, d->urlNavigator); } void KFileWidget::setCustomWidget(const QString &text, QWidget *widget) { delete d->labeledCustomWidget; d->labeledCustomWidget = widget; QLabel *label = new QLabel(text, this); label->setAlignment(Qt::AlignRight); d->lafBox->addWidget(label, 2, 0, Qt::AlignVCenter); d->lafBox->addWidget(widget, 2, 1, Qt::AlignVCenter); } KDirOperator *KFileWidget::dirOperator() { return d->ops; } void KFileWidget::readConfig(KConfigGroup &group) { d->configGroup = group; d->readViewConfig(); d->readRecentFiles(); } QString KFileWidgetPrivate::locationEditCurrentText() const { return QDir::fromNativeSeparators(locationEdit->currentText()); } QUrl KFileWidgetPrivate::mostLocalUrl(const QUrl &url) { if (url.isLocalFile()) { return url; } KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, q); bool res = statJob->exec(); if (!res) { return url; } const QString path = statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); if (!path.isEmpty()) { QUrl newUrl; newUrl.setPath(path); return newUrl; } return url; } void KFileWidgetPrivate::setInlinePreviewShown(bool show) { ops->setInlinePreviewShown(show); } void KFileWidget::setConfirmOverwrite(bool enable) { d->confirmOverwrite = enable; } void KFileWidget::setInlinePreviewShown(bool show) { d->setInlinePreviewShown(show); } QSize KFileWidget::dialogSizeHint() const { int fontSize = fontMetrics().height(); QSize goodSize(48 * fontSize, 30 * fontSize); QSize screenSize = QApplication::desktop()->availableGeometry(this).size(); QSize minSize(screenSize / 2); QSize maxSize(screenSize * qreal(0.9)); return (goodSize.expandedTo(minSize).boundedTo(maxSize)); } void KFileWidget::setViewMode(KFile::FileView mode) { d->ops->setView(mode); d->hasView = true; } void KFileWidget::setSupportedSchemes(const QStringList &schemes) { d->model->setSupportedSchemes(schemes); d->ops->setSupportedSchemes(schemes); d->urlNavigator->setCustomProtocols(schemes); } QStringList KFileWidget::supportedSchemes() const { return d->model->supportedSchemes(); } #include "moc_kfilewidget.cpp" diff --git a/src/filewidgets/kimagefilepreview.cpp b/src/filewidgets/kimagefilepreview.cpp index c4c5522a..e1a8d996 100644 --- a/src/filewidgets/kimagefilepreview.cpp +++ b/src/filewidgets/kimagefilepreview.cpp @@ -1,279 +1,277 @@ /* * This file is part of the KDE project * Copyright (C) 2001 Martin R. Jones * 2001 Carsten Pfeiffer * 2008 Rafael Fernández López * * You can Freely distribute this program under the GNU Library General Public * License. See the file "COPYING" for the exact licensing terms. */ #include "kimagefilepreview.h" -#include #include #include -#include #include #include -#include #include +#include +#include #include #include -#include #include #include #include #include /**** KImageFilePreview ****/ class Q_DECL_HIDDEN KImageFilePreview::KImageFilePreviewPrivate { public: KImageFilePreviewPrivate() : m_job(nullptr) , clear(true) { m_timeLine = new QTimeLine(150); m_timeLine->setCurveShape(QTimeLine::EaseInCurve); m_timeLine->setDirection(QTimeLine::Forward); m_timeLine->setFrameRange(0, 100); } ~KImageFilePreviewPrivate() { delete m_timeLine; } void _k_slotResult(KJob *); void _k_slotFailed(const KFileItem &); void _k_slotStepAnimation(int frame); void _k_slotFinished(); void _k_slotActuallyClear(); QUrl currentURL; QUrl lastShownURL; QLabel *imageLabel; KIO::PreviewJob *m_job; QTimeLine *m_timeLine; QPixmap m_pmCurrent; QPixmap m_pmTransition; float m_pmCurrentOpacity; float m_pmTransitionOpacity; bool clear; }; KImageFilePreview::KImageFilePreview(QWidget *parent) : KPreviewWidgetBase(parent), d(new KImageFilePreviewPrivate) { QVBoxLayout *vb = new QVBoxLayout(this); vb->setContentsMargins(0, 0, 0, 0); d->imageLabel = new QLabel(this); d->imageLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); d->imageLabel->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)); vb->addWidget(d->imageLabel); setSupportedMimeTypes(KIO::PreviewJob::supportedMimeTypes()); setMinimumWidth(50); connect(d->m_timeLine, SIGNAL(frameChanged(int)), this, SLOT(_k_slotStepAnimation(int))); connect(d->m_timeLine, SIGNAL(finished()), this, SLOT(_k_slotFinished())); } KImageFilePreview::~KImageFilePreview() { if (d->m_job) { d->m_job->kill(); } delete d; } void KImageFilePreview::showPreview() { // Pass a copy since clearPreview() will clear currentURL QUrl url = d->currentURL; showPreview(url, true); } // called via KPreviewWidgetBase interface void KImageFilePreview::showPreview(const QUrl &url) { showPreview(url, false); } void KImageFilePreview::showPreview(const QUrl &url, bool force) { if (!url.isValid() || (d->lastShownURL.isValid() && url.matches(d->lastShownURL, QUrl::StripTrailingSlash) && d->currentURL.isValid())) { return; } d->clear = false; d->currentURL = url; d->lastShownURL = url; int w = d->imageLabel->contentsRect().width() - 4; int h = d->imageLabel->contentsRect().height() - 4; if (d->m_job) { disconnect(d->m_job, SIGNAL(result(KJob*)), this, SLOT(_k_slotResult(KJob*))); disconnect(d->m_job, &KIO::PreviewJob::gotPreview, this, &KImageFilePreview::gotPreview); disconnect(d->m_job, SIGNAL(failed(KFileItem)), this, SLOT(_k_slotFailed(KFileItem))); d->m_job->kill(); } d->m_job = createJob(url, w, h); if (force) { // explicitly requested previews shall always be generated! d->m_job->setIgnoreMaximumSize(true); } connect(d->m_job, SIGNAL(result(KJob*)), this, SLOT(_k_slotResult(KJob*))); connect(d->m_job, &KIO::PreviewJob::gotPreview, this, &KImageFilePreview::gotPreview); connect(d->m_job, SIGNAL(failed(KFileItem)), this, SLOT(_k_slotFailed(KFileItem))); } void KImageFilePreview::resizeEvent(QResizeEvent *) { clearPreview(); d->currentURL = QUrl(); // force this to actually happen showPreview(d->lastShownURL); } QSize KImageFilePreview::sizeHint() const { return QSize(100, 200); } KIO::PreviewJob *KImageFilePreview::createJob(const QUrl &url, int w, int h) { if (url.isValid()) { KFileItemList items; items.append(KFileItem(url)); QStringList plugins = KIO::PreviewJob::availablePlugins(); KIO::PreviewJob *previewJob = KIO::filePreview(items, QSize(w, h), &plugins); previewJob->setOverlayIconAlpha(0); previewJob->setScaleType(KIO::PreviewJob::Scaled); return previewJob; } else { return nullptr; } } void KImageFilePreview::gotPreview(const KFileItem &item, const QPixmap &pm) { if (item.url() == d->currentURL) { // should always be the case if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this)) { if (d->m_timeLine->state() == QTimeLine::Running) { d->m_timeLine->setCurrentTime(0); } d->m_pmTransition = pm; d->m_pmTransitionOpacity = 0; d->m_pmCurrentOpacity = 1; d->m_timeLine->setDirection(QTimeLine::Forward); d->m_timeLine->start(); } else { d->imageLabel->setPixmap(pm); } } } void KImageFilePreview::KImageFilePreviewPrivate::_k_slotFailed(const KFileItem &item) { if (item.isDir()) { imageLabel->clear(); } else if (item.url() == currentURL) // should always be the case imageLabel->setPixmap(SmallIcon(QStringLiteral("image-missing"), KIconLoader::SizeLarge, KIconLoader::DisabledState)); } void KImageFilePreview::KImageFilePreviewPrivate::_k_slotResult(KJob *job) { if (job == m_job) { m_job = nullptr; } } void KImageFilePreview::KImageFilePreviewPrivate::_k_slotStepAnimation(int frame) { Q_UNUSED(frame) QPixmap pm(QSize(qMax(m_pmCurrent.size().width(), m_pmTransition.size().width()), qMax(m_pmCurrent.size().height(), m_pmTransition.size().height()))); pm.fill(Qt::transparent); QPainter p(&pm); p.setOpacity(m_pmCurrentOpacity); //If we have a current pixmap if (!m_pmCurrent.isNull()) p.drawPixmap(QPoint(((float) pm.size().width() - m_pmCurrent.size().width()) / 2.0, ((float) pm.size().height() - m_pmCurrent.size().height()) / 2.0), m_pmCurrent); if (!m_pmTransition.isNull()) { p.setOpacity(m_pmTransitionOpacity); p.drawPixmap(QPoint(((float) pm.size().width() - m_pmTransition.size().width()) / 2.0, ((float) pm.size().height() - m_pmTransition.size().height()) / 2.0), m_pmTransition); } p.end(); imageLabel->setPixmap(pm); m_pmCurrentOpacity = qMax(m_pmCurrentOpacity - 0.4, 0.0); // krazy:exclude=qminmax m_pmTransitionOpacity = qMin(m_pmTransitionOpacity + 0.4, 1.0); //krazy:exclude=qminmax } void KImageFilePreview::KImageFilePreviewPrivate::_k_slotFinished() { m_pmCurrent = m_pmTransition; m_pmTransitionOpacity = 0; m_pmCurrentOpacity = 1; m_pmTransition = QPixmap(); // The animation might have lost some frames. Be sure that if the last one // was dropped, the last image shown is the opaque one. imageLabel->setPixmap(m_pmCurrent); clear = false; } void KImageFilePreview::clearPreview() { if (d->m_job) { d->m_job->kill(); d->m_job = nullptr; } if (d->clear || d->m_timeLine->state() == QTimeLine::Running) { return; } if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this)) { d->m_pmTransition = QPixmap(); //If we add a previous preview then we run the animation if (!d->m_pmCurrent.isNull()) { d->m_timeLine->setCurrentTime(0); d->m_timeLine->setDirection(QTimeLine::Backward); d->m_timeLine->start(); } d->currentURL = QUrl(); d->clear = true; } else { d->imageLabel->clear(); } } #include "moc_kimagefilepreview.cpp" diff --git a/src/filewidgets/knameandurlinputdialog.cpp b/src/filewidgets/knameandurlinputdialog.cpp index 5e9dd324..cbc4c5ed 100644 --- a/src/filewidgets/knameandurlinputdialog.cpp +++ b/src/filewidgets/knameandurlinputdialog.cpp @@ -1,154 +1,153 @@ /* Copyright (c) 1998, 2008, 2009 David Faure 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) version 3 or, at the discretion of KDE e.V. (which shall act as a proxy as in section 14 of the GPLv3), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser 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 "knameandurlinputdialog.h" #include #include #include #include #include -#include #include class KNameAndUrlInputDialogPrivate { public: explicit KNameAndUrlInputDialogPrivate(KNameAndUrlInputDialog *qq) : m_fileNameEdited(false) , q(qq) {} void _k_slotNameTextChanged(const QString &); void _k_slotURLTextChanged(const QString &); /** * The line edit widget for the fileName */ QLineEdit *m_leName; /** * The URL requester for the URL :) */ KUrlRequester *m_urlRequester; /** * True if the filename was manually edited. */ bool m_fileNameEdited; QDialogButtonBox *m_buttonBox; KNameAndUrlInputDialog * const q; }; KNameAndUrlInputDialog::KNameAndUrlInputDialog(const QString &nameLabel, const QString &urlLabel, const QUrl &startDir, QWidget *parent) : QDialog(parent), d(new KNameAndUrlInputDialogPrivate(this)) { QVBoxLayout *topLayout = new QVBoxLayout; setLayout(topLayout); QFormLayout *formLayout = new QFormLayout; formLayout->setContentsMargins(0, 0, 0, 0); // First line: filename d->m_leName = new QLineEdit(this); d->m_leName->setMinimumWidth(d->m_leName->sizeHint().width() * 3); d->m_leName->setSelection(0, d->m_leName->text().length()); // autoselect connect(d->m_leName, SIGNAL(textChanged(QString)), SLOT(_k_slotNameTextChanged(QString))); formLayout->addRow(nameLabel, d->m_leName); // Second line: url d->m_urlRequester = new KUrlRequester(this); d->m_urlRequester->setStartDir(startDir); d->m_urlRequester->setMode(KFile::File | KFile::Directory); d->m_urlRequester->setMinimumWidth(d->m_urlRequester->sizeHint().width() * 3); connect(d->m_urlRequester->lineEdit(), SIGNAL(textChanged(QString)), SLOT(_k_slotURLTextChanged(QString))); formLayout->addRow(urlLabel, d->m_urlRequester); topLayout->addLayout(formLayout); d->m_buttonBox = new QDialogButtonBox(this); d->m_buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(d->m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(d->m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); topLayout->addWidget(d->m_buttonBox); d->m_fileNameEdited = false; d->m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!d->m_leName->text().isEmpty() && !d->m_urlRequester->url().isEmpty()); d->m_leName->setFocus(); } KNameAndUrlInputDialog::~KNameAndUrlInputDialog() { delete d; } QUrl KNameAndUrlInputDialog::url() const { return d->m_urlRequester->url(); } QString KNameAndUrlInputDialog::urlText() const { return d->m_urlRequester->text(); } QString KNameAndUrlInputDialog::name() const { return d->m_leName->text(); } void KNameAndUrlInputDialogPrivate::_k_slotNameTextChanged(const QString &) { m_fileNameEdited = true; m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!m_leName->text().isEmpty() && !m_urlRequester->url().isEmpty()); } void KNameAndUrlInputDialogPrivate::_k_slotURLTextChanged(const QString &) { if (!m_fileNameEdited) { // use URL as default value for the filename // (we copy only its filename if protocol supports listing, // but for HTTP we don't want tons of index.html links) QUrl url(m_urlRequester->url()); if (KProtocolManager::supportsListing(url) && !url.fileName().isEmpty()) { m_leName->setText(url.fileName()); } else { m_leName->setText(url.toString()); } m_fileNameEdited = false; // slotNameTextChanged set it to true erroneously } m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!m_leName->text().isEmpty() && !m_urlRequester->url().isEmpty()); } void KNameAndUrlInputDialog::setSuggestedName(const QString &name) { d->m_leName->setText(name); d->m_urlRequester->setFocus(); } void KNameAndUrlInputDialog::setSuggestedUrl(const QUrl &url) { d->m_urlRequester->setUrl(url); } #include "moc_knameandurlinputdialog.cpp" diff --git a/src/filewidgets/kurlnavigator.cpp b/src/filewidgets/kurlnavigator.cpp index 4ca19e68..80aa9753 100644 --- a/src/filewidgets/kurlnavigator.cpp +++ b/src/filewidgets/kurlnavigator.cpp @@ -1,1330 +1,1327 @@ /***************************************************************************** * Copyright (C) 2006-2010 by Peter Penz * * Copyright (C) 2006 by Aaron J. Seigo * * Copyright (C) 2007 by Kevin Ottens * * Copyright (C) 2007 by Urs Wolfer * * * * 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 "kurlnavigator.h" #include "kurlnavigatorplacesselector_p.h" #include "kurlnavigatorprotocolcombo_p.h" #include "kurlnavigatordropdownbutton_p.h" #include "kurlnavigatorbutton_p.h" #include "kurlnavigatortogglebutton_p.h" #include "kurlnavigatorpathselectoreventfilter_p.h" #include "urlutil_p.h" #include #include #include #include #include #include #include #include #include #include #include -#include #include #include #include -#include #include -#include -#include #include #include +#include using namespace KDEPrivate; struct LocationData { QUrl url; #ifndef KIOFILEWIDGETS_NO_DEPRECATED QUrl rootUrl; // KDE5: remove after the deprecated methods have been removed QPoint pos; // KDE5: remove after the deprecated methods have been removed #endif QByteArray state; }; class Q_DECL_HIDDEN KUrlNavigator::Private { public: Private(KUrlNavigator *q, KFilePlacesModel *placesModel); void initialize(const QUrl &url); /** Applies the edited URL in m_pathBox to the URL navigator */ void applyUncommittedUrl(); void slotReturnPressed(); void slotProtocolChanged(const QString &); void openPathSelectorMenu(); /** * Appends the widget at the end of the URL navigator. It is assured * that the filler widget remains as last widget to fill the remaining * width. */ void appendWidget(QWidget *widget, int stretch = 0); /** * This slot is connected to the clicked signal of the navigation bar button. It calls switchView(). * Moreover, if switching from "editable" mode to the breadcrumb view, it calls applyUncommittedUrl(). */ void slotToggleEditableButtonPressed(); /** * Switches the navigation bar between the breadcrumb view and the * traditional view (see setUrlEditable()). */ void switchView(); /** Emits the signal urlsDropped(). */ void dropUrls(const QUrl &destination, QDropEvent *event); /** * Is invoked when a navigator button has been clicked. Changes the URL * of the navigator if the left mouse button has been used. If the middle * mouse button has been used, the signal tabRequested() will be emitted. */ void slotNavigatorButtonClicked(const QUrl &url, Qt::MouseButton button, Qt::KeyboardModifiers modifiers); void openContextMenu(const QPoint &p); void slotPathBoxChanged(const QString &text); void updateContent(); /** * Updates all buttons to have one button for each part of the * current URL. Existing buttons, which are available by m_navButtons, * are reused if possible. If the URL is longer, new buttons will be * created, if the URL is shorter, the remaining buttons will be deleted. * @param startIndex Start index of URL part (/), where the buttons * should be created for each following part. */ void updateButtons(int startIndex); /** * Updates the visibility state of all buttons describing the URL. If the * width of the URL navigator is too small, the buttons representing the upper * paths of the URL will be hidden and moved to a drop down menu. */ void updateButtonVisibility(); /** * @return Text for the first button of the URL navigator. */ QString firstButtonText() const; /** * Returns the URL that should be applied for the button with the index \a index. */ QUrl buttonUrl(int index) const; void switchToBreadcrumbMode(); /** * Deletes all URL navigator buttons. m_navButtons is * empty after this operation. */ void deleteButtons(); /** * Retrieves the place url for the current url. * E. g. for the path "fish://root@192.168.0.2/var/lib" the string * "fish://root@192.168.0.2" will be returned, which leads to the * navigation indication 'Custom Path > var > lib". For e. g. * "settings:///System/" the path "settings://" will be returned. */ QUrl retrievePlaceUrl() const; /** * Returns true, if the MIME type of the path represents a * compressed file like TAR or ZIP. */ bool isCompressedPath(const QUrl &path) const; void removeTrailingSlash(QString &url) const; /** * Returns the current history index, if \a historyIndex is * smaller than 0. If \a historyIndex is greater or equal than * the number of available history items, the largest possible * history index is returned. For the other cases just \a historyIndex * is returned. */ int adjustedHistoryIndex(int historyIndex) const; bool m_editable : 1; bool m_active : 1; bool m_showPlacesSelector : 1; bool m_showFullPath : 1; int m_historyIndex; QHBoxLayout *m_layout; QList m_history; KUrlNavigatorPlacesSelector *m_placesSelector; KUrlComboBox *m_pathBox; KUrlNavigatorProtocolCombo *m_protocols; KUrlNavigatorDropDownButton *m_dropDownButton; QList m_navButtons; KUrlNavigatorButtonBase *m_toggleEditableMode; QUrl m_homeUrl; QStringList m_customProtocols; QWidget *m_dropWidget; KUrlNavigator * const q; }; KUrlNavigator::Private::Private(KUrlNavigator *q, KFilePlacesModel *placesModel) : m_editable(false), m_active(true), m_showPlacesSelector(placesModel != nullptr), m_showFullPath(false), m_historyIndex(0), m_layout(new QHBoxLayout), m_placesSelector(nullptr), m_pathBox(nullptr), m_protocols(nullptr), m_dropDownButton(nullptr), m_navButtons(), m_toggleEditableMode(nullptr), m_homeUrl(), m_customProtocols(QStringList()), m_dropWidget(nullptr), q(q) { m_layout->setSpacing(0); m_layout->setContentsMargins(0, 0, 0, 0); // initialize the places selector q->setAutoFillBackground(false); if (placesModel != nullptr) { m_placesSelector = new KUrlNavigatorPlacesSelector(q, placesModel); connect(m_placesSelector, &KUrlNavigatorPlacesSelector::placeActivated, q, &KUrlNavigator::setLocationUrl); connect(m_placesSelector, &KUrlNavigatorPlacesSelector::tabRequested, q, &KUrlNavigator::tabRequested); connect(placesModel, SIGNAL(rowsInserted(QModelIndex,int,int)), q, SLOT(updateContent())); connect(placesModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), q, SLOT(updateContent())); connect(placesModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), q, SLOT(updateContent())); } // create protocol combo m_protocols = new KUrlNavigatorProtocolCombo(QString(), q); connect(m_protocols, SIGNAL(activated(QString)), q, SLOT(slotProtocolChanged(QString))); // create drop down button for accessing all paths of the URL m_dropDownButton = new KUrlNavigatorDropDownButton(q); m_dropDownButton->setForegroundRole(QPalette::WindowText); m_dropDownButton->installEventFilter(q); connect(m_dropDownButton, SIGNAL(clicked()), q, SLOT(openPathSelectorMenu())); // initialize the path box of the traditional view m_pathBox = new KUrlComboBox(KUrlComboBox::Directories, true, q); m_pathBox->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength); m_pathBox->installEventFilter(q); KUrlCompletion *kurlCompletion = new KUrlCompletion(KUrlCompletion::DirCompletion); m_pathBox->setCompletionObject(kurlCompletion); m_pathBox->setAutoDeleteCompletionObject(true); connect(m_pathBox, SIGNAL(returnPressed()), q, SLOT(slotReturnPressed())); connect(m_pathBox, &KUrlComboBox::urlActivated, q, &KUrlNavigator::setLocationUrl); connect(m_pathBox, SIGNAL(editTextChanged(QString)), q, SLOT(slotPathBoxChanged(QString))); // create toggle button which allows to switch between // the breadcrumb and traditional view m_toggleEditableMode = new KUrlNavigatorToggleButton(q); m_toggleEditableMode->installEventFilter(q); m_toggleEditableMode->setMinimumWidth(20); connect(m_toggleEditableMode, SIGNAL(clicked()), q, SLOT(slotToggleEditableButtonPressed())); if (m_placesSelector != nullptr) { m_layout->addWidget(m_placesSelector); } m_layout->addWidget(m_protocols); m_layout->addWidget(m_dropDownButton); m_layout->addWidget(m_pathBox, 1); m_layout->addWidget(m_toggleEditableMode); q->setContextMenuPolicy(Qt::CustomContextMenu); connect(q, SIGNAL(customContextMenuRequested(QPoint)), q, SLOT(openContextMenu(QPoint))); } void KUrlNavigator::Private::initialize(const QUrl &url) { LocationData data; data.url = url.adjusted(QUrl::NormalizePathSegments); m_history.prepend(data); q->setLayoutDirection(Qt::LeftToRight); const int minHeight = m_pathBox->sizeHint().height(); q->setMinimumHeight(minHeight); q->setLayout(m_layout); q->setMinimumWidth(100); updateContent(); } void KUrlNavigator::Private::appendWidget(QWidget *widget, int stretch) { m_layout->insertWidget(m_layout->count() - 1, widget, stretch); } void KUrlNavigator::Private::applyUncommittedUrl() { // Parts of the following code have been taken // from the class KateFileSelector located in // kate/app/katefileselector.hpp of Kate. // Copyright (C) 2001 Christoph Cullmann // Copyright (C) 2001 Joseph Wenninger // Copyright (C) 2001 Anders Lund const QUrl typedUrl = q->uncommittedUrl(); QStringList urls = m_pathBox->urls(); urls.removeAll(typedUrl.toString()); urls.prepend(typedUrl.toString()); m_pathBox->setUrls(urls, KUrlComboBox::RemoveBottom); q->setLocationUrl(typedUrl); // The URL might have been adjusted by KUrlNavigator::setUrl(), hence // synchronize the result in the path box. const QUrl currentUrl = q->locationUrl(); m_pathBox->setUrl(currentUrl); } void KUrlNavigator::Private::slotReturnPressed() { applyUncommittedUrl(); emit q->returnPressed(); if (QApplication::keyboardModifiers() & Qt::ControlModifier) { // Pressing Ctrl+Return automatically switches back to the breadcrumb mode. // The switch must be done asynchronously, as we are in the context of the // editor. QMetaObject::invokeMethod(q, "switchToBreadcrumbMode", Qt::QueuedConnection); } } void KUrlNavigator::Private::slotProtocolChanged(const QString &protocol) { Q_ASSERT(m_editable); QUrl url; url.setScheme(protocol); if (protocol == QLatin1String("file")) { url.setPath(QStringLiteral("/")); } else { // With no authority set we'll get e.g. "ftp:" instead of "ftp://". // We want the latter, so let's set an empty authority. url.setAuthority(QString()); } m_pathBox->setEditUrl(url); } void KUrlNavigator::Private::openPathSelectorMenu() { if (m_navButtons.count() <= 0) { return; } const QUrl firstVisibleUrl = m_navButtons.first()->url(); QString spacer; QPointer popup = new QMenu(q); auto *popupFilter = new KUrlNavigatorPathSelectorEventFilter(popup.data()); connect(popupFilter, &KUrlNavigatorPathSelectorEventFilter::tabRequested, q, &KUrlNavigator::tabRequested); popup->installEventFilter(popupFilter); popup->setLayoutDirection(Qt::LeftToRight); const QUrl placeUrl = retrievePlaceUrl(); int idx = placeUrl.path().count(QLatin1Char('/')); // idx points to the first directory // after the place path const QString path = m_history[m_historyIndex].url.path(); QString dirName = path.section(QLatin1Char('/'), idx, idx); if (dirName.isEmpty()) { if (placeUrl.isLocalFile()) { dirName = QStringLiteral("/"); } else { dirName = placeUrl.toDisplayString(); } } do { const QString text = spacer + dirName; QAction *action = new QAction(text, popup); const QUrl currentUrl = buttonUrl(idx); if (currentUrl == firstVisibleUrl) { popup->addSeparator(); } action->setData(QVariant(currentUrl.toString())); popup->addAction(action); ++idx; spacer.append(QLatin1String(" ")); dirName = path.section(QLatin1Char('/'), idx, idx); } while (!dirName.isEmpty()); const QPoint pos = q->mapToGlobal(m_dropDownButton->geometry().bottomRight()); const QAction *activatedAction = popup->exec(pos); if (activatedAction != nullptr) { const QUrl url(activatedAction->data().toString()); q->setLocationUrl(url); } // Delete the menu, unless it has been deleted in its own nested event loop already. if (popup) { popup->deleteLater(); } } void KUrlNavigator::Private::slotToggleEditableButtonPressed() { if (m_editable) { applyUncommittedUrl(); } switchView(); } void KUrlNavigator::Private::switchView() { m_toggleEditableMode->setFocus(); m_editable = !m_editable; m_toggleEditableMode->setChecked(m_editable); updateContent(); if (q->isUrlEditable()) { m_pathBox->setFocus(); } q->requestActivation(); emit q->editableStateChanged(m_editable); } void KUrlNavigator::Private::dropUrls(const QUrl &destination, QDropEvent *event) { if (event->mimeData()->hasUrls()) { m_dropWidget = qobject_cast(q->sender()); emit q->urlsDropped(destination, event); } } void KUrlNavigator::Private::slotNavigatorButtonClicked(const QUrl &url, Qt::MouseButton button, Qt::KeyboardModifiers modifiers) { if (button & Qt::MidButton || (button & Qt::LeftButton && modifiers & Qt::ControlModifier)) { emit q->tabRequested(url); } else if (button & Qt::LeftButton) { q->setLocationUrl(url); } } void KUrlNavigator::Private::openContextMenu(const QPoint &p) { q->setActive(true); QPointer popup = new QMenu(q); // provide 'Copy' action, which copies the current URL of // the URL navigator into the clipboard QAction *copyAction = popup->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy")); // provide 'Paste' action, which copies the current clipboard text // into the URL navigator QAction *pasteAction = popup->addAction(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Paste")); QClipboard *clipboard = QApplication::clipboard(); pasteAction->setEnabled(!clipboard->text().isEmpty()); popup->addSeparator(); //We are checking for receivers because it's odd to have a tab entry even if it's not supported, like in the case of the open dialog if (q->receivers(SIGNAL(tabRequested(QUrl))) > 0) { for (auto button : qAsConst(m_navButtons)) { if (button->geometry().contains(p)) { const auto url = button->url(); QAction* openInTab = popup->addAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18n("Open %1 in tab", button->text())); q->connect(openInTab, &QAction::triggered, q, [this, url](){ Q_EMIT q->tabRequested(url); }); break; } } } // provide radiobuttons for toggling between the edit and the navigation mode QAction *editAction = popup->addAction(i18n("Edit")); editAction->setCheckable(true); QAction *navigateAction = popup->addAction(i18n("Navigate")); navigateAction->setCheckable(true); QActionGroup *modeGroup = new QActionGroup(popup); modeGroup->addAction(editAction); modeGroup->addAction(navigateAction); if (q->isUrlEditable()) { editAction->setChecked(true); } else { navigateAction->setChecked(true); } popup->addSeparator(); // allow showing of the full path QAction *showFullPathAction = popup->addAction(i18n("Show Full Path")); showFullPathAction->setCheckable(true); showFullPathAction->setChecked(q->showFullPath()); QAction *activatedAction = popup->exec(QCursor::pos()); if (activatedAction == copyAction) { QMimeData *mimeData = new QMimeData(); mimeData->setText(q->locationUrl().toDisplayString(QUrl::PreferLocalFile)); clipboard->setMimeData(mimeData); } else if (activatedAction == pasteAction) { q->setLocationUrl(QUrl::fromUserInput(clipboard->text())); } else if (activatedAction == editAction) { q->setUrlEditable(true); } else if (activatedAction == navigateAction) { q->setUrlEditable(false); } else if (activatedAction == showFullPathAction) { q->setShowFullPath(showFullPathAction->isChecked()); } // Delete the menu, unless it has been deleted in its own nested event loop already. if (popup) { popup->deleteLater(); } } void KUrlNavigator::Private::slotPathBoxChanged(const QString &text) { if (text.isEmpty()) { const QString protocol = q->locationUrl().scheme(); m_protocols->setProtocol(protocol); if (m_customProtocols.count() != 1) { m_protocols->show(); } } else { m_protocols->hide(); } } void KUrlNavigator::Private::updateContent() { const QUrl currentUrl = q->locationUrl(); if (m_placesSelector != nullptr) { m_placesSelector->updateSelection(currentUrl); } if (m_editable) { m_protocols->hide(); m_dropDownButton->hide(); deleteButtons(); m_toggleEditableMode->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); q->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); m_pathBox->show(); m_pathBox->setUrl(currentUrl); } else { m_pathBox->hide(); m_protocols->hide(); m_toggleEditableMode->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); q->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); // Calculate the start index for the directories that should be shown as buttons // and create the buttons QUrl placeUrl; if ((m_placesSelector != nullptr) && !m_showFullPath) { placeUrl = m_placesSelector->selectedPlaceUrl(); } if (!placeUrl.isValid()) { placeUrl = retrievePlaceUrl(); } QString placePath = placeUrl.path(); removeTrailingSlash(placePath); const int startIndex = placePath.count(QLatin1Char('/')); updateButtons(startIndex); } } void KUrlNavigator::Private::updateButtons(int startIndex) { QUrl currentUrl = q->locationUrl(); if (!currentUrl.isValid()) { // QFileDialog::setDirectory not called yet return; } const QString path = currentUrl.path(); bool createButton = false; const int oldButtonCount = m_navButtons.count(); int idx = startIndex; bool hasNext = true; do { createButton = (idx - startIndex >= oldButtonCount); const bool isFirstButton = (idx == startIndex); const QString dirName = path.section(QLatin1Char('/'), idx, idx); hasNext = isFirstButton || !dirName.isEmpty(); if (hasNext) { KUrlNavigatorButton *button = nullptr; if (createButton) { button = new KUrlNavigatorButton(buttonUrl(idx), q); button->installEventFilter(q); button->setForegroundRole(QPalette::WindowText); connect(button, SIGNAL(urlsDropped(QUrl,QDropEvent*)), q, SLOT(dropUrls(QUrl,QDropEvent*))); connect(button, SIGNAL(clicked(QUrl,Qt::MouseButton,Qt::KeyboardModifiers)), q, SLOT(slotNavigatorButtonClicked(QUrl,Qt::MouseButton,Qt::KeyboardModifiers))); connect(button, SIGNAL(finishedTextResolving()), q, SLOT(updateButtonVisibility())); appendWidget(button); } else { button = m_navButtons[idx - startIndex]; button->setUrl(buttonUrl(idx)); } if (isFirstButton) { button->setText(firstButtonText()); } button->setActive(q->isActive()); if (createButton) { if (!isFirstButton) { setTabOrder(m_navButtons.last(), button); } m_navButtons.append(button); } ++idx; button->setActiveSubDirectory(path.section(QLatin1Char('/'), idx, idx)); } } while (hasNext); // delete buttons which are not used anymore const int newButtonCount = idx - startIndex; if (newButtonCount < oldButtonCount) { const QList::iterator itBegin = m_navButtons.begin() + newButtonCount; const QList::iterator itEnd = m_navButtons.end(); QList::iterator it = itBegin; while (it != itEnd) { (*it)->hide(); (*it)->deleteLater(); ++it; } m_navButtons.erase(itBegin, itEnd); } setTabOrder(m_dropDownButton, m_navButtons.first()); setTabOrder(m_navButtons.last(), m_toggleEditableMode); updateButtonVisibility(); } void KUrlNavigator::Private::updateButtonVisibility() { if (m_editable) { return; } const int buttonsCount = m_navButtons.count(); if (buttonsCount == 0) { m_dropDownButton->hide(); return; } // Subtract all widgets from the available width, that must be shown anyway int availableWidth = q->width() - m_toggleEditableMode->minimumWidth(); if ((m_placesSelector != nullptr) && m_placesSelector->isVisible()) { availableWidth -= m_placesSelector->width(); } if ((m_protocols != nullptr) && m_protocols->isVisible()) { availableWidth -= m_protocols->width(); } // Check whether buttons must be hidden at all... int requiredButtonWidth = 0; foreach (const KUrlNavigatorButton *button, m_navButtons) { requiredButtonWidth += button->minimumWidth(); } if (requiredButtonWidth > availableWidth) { // At least one button must be hidden. This implies that the // drop-down button must get visible, which again decreases the // available width. availableWidth -= m_dropDownButton->width(); } // Hide buttons... QList::const_iterator it = m_navButtons.constEnd(); const QList::const_iterator itBegin = m_navButtons.constBegin(); bool isLastButton = true; bool hasHiddenButtons = false; QLinkedList buttonsToShow; while (it != itBegin) { --it; KUrlNavigatorButton *button = (*it); availableWidth -= button->minimumWidth(); if ((availableWidth <= 0) && !isLastButton) { button->hide(); hasHiddenButtons = true; } else { // Don't show the button immediately, as setActive() // might change the size and a relayout gets triggered // after showing the button. So the showing of all buttons // is postponed until all buttons have the correct // activation state. buttonsToShow.append(button); } isLastButton = false; } // All buttons have the correct activation state and // can be shown now foreach (KUrlNavigatorButton *button, buttonsToShow) { button->show(); } if (hasHiddenButtons) { m_dropDownButton->show(); } else { // Check whether going upwards is possible. If this is the case, show the drop-down button. QUrl url(m_navButtons.front()->url()); const bool visible = !url.matches(KIO::upUrl(url), QUrl::StripTrailingSlash) && (url.scheme() != QLatin1String("nepomuksearch")); m_dropDownButton->setVisible(visible); } } QString KUrlNavigator::Private::firstButtonText() const { QString text; // The first URL navigator button should get the name of the // place instead of the directory name if ((m_placesSelector != nullptr) && !m_showFullPath) { text = m_placesSelector->selectedPlaceText(); } if (text.isEmpty()) { const QUrl currentUrl = q->locationUrl(); if (currentUrl.isLocalFile()) { #ifdef Q_OS_WIN text = currentUrl.path().length() > 1 ? currentUrl.path().left(2) : QDir::rootPath(); #else text = m_showFullPath ? QStringLiteral("/") : i18n("Custom Path"); #endif } else { text = currentUrl.scheme() + QLatin1Char(':'); if (!currentUrl.host().isEmpty()) { text += QLatin1Char(' ') + currentUrl.host(); } } } return text; } QUrl KUrlNavigator::Private::buttonUrl(int index) const { if (index < 0) { index = 0; } // Keep scheme, hostname etc. as this is needed for e. g. browsing // FTP directories QUrl url = q->locationUrl(); QString path = url.path(); if (!path.isEmpty()) { if (index == 0) { // prevent the last "/" from being stripped // or we end up with an empty path #ifdef Q_OS_WIN path = path.length() > 1 ? path.left(2) : QDir::rootPath(); #else path = QStringLiteral("/"); #endif } else { path = path.section(QLatin1Char('/'), 0, index); } } url.setPath(path); return url; } void KUrlNavigator::Private::switchToBreadcrumbMode() { q->setUrlEditable(false); } void KUrlNavigator::Private::deleteButtons() { foreach (KUrlNavigatorButton *button, m_navButtons) { button->hide(); button->deleteLater(); } m_navButtons.clear(); } QUrl KUrlNavigator::Private::retrievePlaceUrl() const { QUrl currentUrl = q->locationUrl(); currentUrl.setPath(QString()); return currentUrl; } bool KUrlNavigator::Private::isCompressedPath(const QUrl &url) const { QMimeDatabase db; const QMimeType mime = db.mimeTypeForUrl(QUrl(url.toString(QUrl::StripTrailingSlash))); // Note: this list of MIME types depends on the protocols implemented by kio_archive return mime.inherits(QStringLiteral("application/x-compressed-tar")) || mime.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mime.inherits(QStringLiteral("application/x-lzma-compressed-tar")) || mime.inherits(QStringLiteral("application/x-xz-compressed-tar")) || mime.inherits(QStringLiteral("application/x-tar")) || mime.inherits(QStringLiteral("application/x-tarz")) || mime.inherits(QStringLiteral("application/x-tzo")) || // (not sure KTar supports those?) mime.inherits(QStringLiteral("application/zip")) || mime.inherits(QStringLiteral("application/x-archive")); } void KUrlNavigator::Private::removeTrailingSlash(QString &url) const { const int length = url.length(); if ((length > 0) && (url.at(length - 1) == QLatin1Char('/'))) { url.remove(length - 1, 1); } } int KUrlNavigator::Private::adjustedHistoryIndex(int historyIndex) const { if (historyIndex < 0) { historyIndex = m_historyIndex; } else if (historyIndex >= m_history.size()) { historyIndex = m_history.size() - 1; Q_ASSERT(historyIndex >= 0); // m_history.size() must always be > 0 } return historyIndex; } // ------------------------------------------------------------------------------------------------ KUrlNavigator::KUrlNavigator(QWidget *parent) : QWidget(parent), d(new Private(this, nullptr)) { d->initialize(QUrl()); } KUrlNavigator::KUrlNavigator(KFilePlacesModel *placesModel, const QUrl &url, QWidget *parent) : QWidget(parent), d(new Private(this, placesModel)) { d->initialize(url); } KUrlNavigator::~KUrlNavigator() { delete d; } QUrl KUrlNavigator::locationUrl(int historyIndex) const { historyIndex = d->adjustedHistoryIndex(historyIndex); return d->m_history[historyIndex].url; } void KUrlNavigator::saveLocationState(const QByteArray &state) { d->m_history[d->m_historyIndex].state = state; } QByteArray KUrlNavigator::locationState(int historyIndex) const { historyIndex = d->adjustedHistoryIndex(historyIndex); return d->m_history[historyIndex].state; } bool KUrlNavigator::goBack() { const int count = d->m_history.count(); if (d->m_historyIndex < count - 1) { const QUrl newUrl = locationUrl(d->m_historyIndex + 1); emit urlAboutToBeChanged(newUrl); ++d->m_historyIndex; d->updateContent(); emit historyChanged(); emit urlChanged(locationUrl()); return true; } return false; } bool KUrlNavigator::goForward() { if (d->m_historyIndex > 0) { const QUrl newUrl = locationUrl(d->m_historyIndex - 1); emit urlAboutToBeChanged(newUrl); --d->m_historyIndex; d->updateContent(); emit historyChanged(); emit urlChanged(locationUrl()); return true; } return false; } bool KUrlNavigator::goUp() { const QUrl currentUrl = locationUrl(); const QUrl upUrl = KIO::upUrl(currentUrl); if (upUrl != currentUrl) { // TODO use url.matches(KIO::upUrl(url), QUrl::StripTrailingSlash) setLocationUrl(upUrl); return true; } return false; } void KUrlNavigator::goHome() { if (d->m_homeUrl.isEmpty() || !d->m_homeUrl.isValid()) { setLocationUrl(QUrl::fromLocalFile(QDir::homePath())); } else { setLocationUrl(d->m_homeUrl); } } void KUrlNavigator::setHomeUrl(const QUrl &url) { d->m_homeUrl = url; } QUrl KUrlNavigator::homeUrl() const { return d->m_homeUrl; } void KUrlNavigator::setUrlEditable(bool editable) { if (d->m_editable != editable) { d->switchView(); } } bool KUrlNavigator::isUrlEditable() const { return d->m_editable; } void KUrlNavigator::setShowFullPath(bool show) { if (d->m_showFullPath != show) { d->m_showFullPath = show; d->updateContent(); } } bool KUrlNavigator::showFullPath() const { return d->m_showFullPath; } void KUrlNavigator::setActive(bool active) { if (active != d->m_active) { d->m_active = active; d->m_dropDownButton->setActive(active); foreach (KUrlNavigatorButton *button, d->m_navButtons) { button->setActive(active); } update(); if (active) { emit activated(); } } } bool KUrlNavigator::isActive() const { return d->m_active; } void KUrlNavigator::setPlacesSelectorVisible(bool visible) { if (visible == d->m_showPlacesSelector) { return; } if (visible && (d->m_placesSelector == nullptr)) { // the places selector cannot get visible as no // places model is available return; } d->m_showPlacesSelector = visible; d->m_placesSelector->setVisible(visible); } bool KUrlNavigator::isPlacesSelectorVisible() const { return d->m_showPlacesSelector; } QUrl KUrlNavigator::uncommittedUrl() const { KUriFilterData filteredData(d->m_pathBox->currentText().trimmed()); filteredData.setCheckForExecutables(false); if (KUriFilter::self()->filterUri(filteredData, QStringList{ QStringLiteral("kshorturifilter"), QStringLiteral("kurisearchfilter")})) { return filteredData.uri(); } else { return QUrl::fromUserInput(filteredData.typedString()); } } void KUrlNavigator::setLocationUrl(const QUrl &newUrl) { if (newUrl == locationUrl()) { return; } QUrl url = newUrl.adjusted(QUrl::NormalizePathSegments); // This will be used below; we define it here because in the lower part of the // code locationUrl() and url become the same URLs QUrl firstChildUrl = KIO::UrlUtil::firstChildUrl(locationUrl(), url); if ((url.scheme() == QLatin1String("tar")) || (url.scheme() == QLatin1String("zip")) || (url.scheme() == QLatin1String("krarc"))) { // The URL represents a tar- or zip-file, or an archive file supported by krarc. // Check whether the URL is really part of the archive file, otherwise // replace it by the local path again. bool insideCompressedPath = d->isCompressedPath(url); if (!insideCompressedPath) { QUrl prevUrl = url; QUrl parentUrl = KIO::upUrl(url); while (parentUrl != prevUrl) { if (d->isCompressedPath(parentUrl)) { insideCompressedPath = true; break; } prevUrl = parentUrl; parentUrl = KIO::upUrl(parentUrl); } } if (!insideCompressedPath) { // drop the tar: or zip: or krarc: protocol since we are not // inside the compressed path url.setScheme(QStringLiteral("file")); firstChildUrl.setScheme(QStringLiteral("file")); } } // Check whether current history element has the same URL. // If this is the case, just ignore setting the URL. const LocationData &data = d->m_history[d->m_historyIndex]; const bool isUrlEqual = url.matches(locationUrl(), QUrl::StripTrailingSlash) || (!url.isValid() && url.matches(data.url, QUrl::StripTrailingSlash)); if (isUrlEqual) { return; } emit urlAboutToBeChanged(url); if (d->m_historyIndex > 0) { // If an URL is set when the history index is not at the end (= 0), // then clear all previous history elements so that a new history // tree is started from the current position. QList::iterator begin = d->m_history.begin(); QList::iterator end = begin + d->m_historyIndex; d->m_history.erase(begin, end); d->m_historyIndex = 0; } Q_ASSERT(d->m_historyIndex == 0); LocationData newData; newData.url = url; d->m_history.insert(0, newData); // Prevent an endless growing of the history: remembering // the last 100 Urls should be enough... const int historyMax = 100; if (d->m_history.size() > historyMax) { QList::iterator begin = d->m_history.begin() + historyMax; QList::iterator end = d->m_history.end(); d->m_history.erase(begin, end); } emit historyChanged(); emit urlChanged(url); if (firstChildUrl.isValid()) { emit urlSelectionRequested(firstChildUrl); } d->updateContent(); requestActivation(); } void KUrlNavigator::requestActivation() { setActive(true); } void KUrlNavigator::setFocus() { if (isUrlEditable()) { d->m_pathBox->setFocus(); } else { QWidget::setFocus(); } } #ifndef KIOFILEWIDGETS_NO_DEPRECATED void KUrlNavigator::setUrl(const QUrl &url) { // deprecated setLocationUrl(url); } #endif #ifndef KIOFILEWIDGETS_NO_DEPRECATED void KUrlNavigator::saveRootUrl(const QUrl &url) { // deprecated d->m_history[d->m_historyIndex].rootUrl = url; } #endif #ifndef KIOFILEWIDGETS_NO_DEPRECATED void KUrlNavigator::savePosition(int x, int y) { // deprecated d->m_history[d->m_historyIndex].pos = QPoint(x, y); } #endif void KUrlNavigator::keyPressEvent(QKeyEvent *event) { if (isUrlEditable() && (event->key() == Qt::Key_Escape)) { setUrlEditable(false); } else { QWidget::keyPressEvent(event); } } void KUrlNavigator::keyReleaseEvent(QKeyEvent *event) { QWidget::keyReleaseEvent(event); } void KUrlNavigator::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::MiddleButton) { requestActivation(); } QWidget::mousePressEvent(event); } void KUrlNavigator::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::MidButton) { const QRect bounds = d->m_toggleEditableMode->geometry(); if (bounds.contains(event->pos())) { // The middle mouse button has been clicked above the // toggle-editable-mode-button. Paste the clipboard content // as location URL. QClipboard *clipboard = QApplication::clipboard(); const QMimeData *mimeData = clipboard->mimeData(); if (mimeData->hasText()) { const QString text = mimeData->text(); setLocationUrl(QUrl::fromUserInput(text)); } } } QWidget::mouseReleaseEvent(event); } void KUrlNavigator::resizeEvent(QResizeEvent *event) { QTimer::singleShot(0, this, SLOT(updateButtonVisibility())); QWidget::resizeEvent(event); } void KUrlNavigator::wheelEvent(QWheelEvent *event) { setActive(true); QWidget::wheelEvent(event); } bool KUrlNavigator::eventFilter(QObject *watched, QEvent *event) { switch (event->type()) { case QEvent::FocusIn: if (watched == d->m_pathBox) { requestActivation(); setFocus(); } foreach (KUrlNavigatorButton *button, d->m_navButtons) { button->setShowMnemonic(true); } break; case QEvent::FocusOut: foreach (KUrlNavigatorButton *button, d->m_navButtons) { button->setShowMnemonic(false); } break; default: break; } return QWidget::eventFilter(watched, event); } int KUrlNavigator::historySize() const { return d->m_history.count(); } int KUrlNavigator::historyIndex() const { return d->m_historyIndex; } KUrlComboBox *KUrlNavigator::editor() const { return d->m_pathBox; } void KUrlNavigator::setCustomProtocols(const QStringList &protocols) { d->m_customProtocols = protocols; d->m_protocols->setCustomProtocols(d->m_customProtocols); } QStringList KUrlNavigator::customProtocols() const { return d->m_customProtocols; } QWidget *KUrlNavigator::dropWidget() const { return d->m_dropWidget; } #ifndef KIOFILEWIDGETS_NO_DEPRECATED const QUrl &KUrlNavigator::url() const { // deprecated // Workaround required because of flawed interface ('const QUrl&' is returned // instead of 'QUrl'): remember the URL to prevent a dangling pointer static QUrl url; url = locationUrl(); return url; } #endif #ifndef KIOFILEWIDGETS_NO_DEPRECATED QUrl KUrlNavigator::url(int index) const { // deprecated return d->buttonUrl(index); } #endif #ifndef KIOFILEWIDGETS_NO_DEPRECATED QUrl KUrlNavigator::historyUrl(int historyIndex) const { // deprecated return locationUrl(historyIndex); } #endif #ifndef KIOFILEWIDGETS_NO_DEPRECATED const QUrl &KUrlNavigator::savedRootUrl() const { // deprecated // Workaround required because of flawed interface ('const QUrl&' is returned // instead of 'QUrl'): remember the root URL to prevent a dangling pointer static QUrl rootUrl; rootUrl = d->m_history[d->m_historyIndex].rootUrl; return rootUrl; } #endif #ifndef KIOFILEWIDGETS_NO_DEPRECATED QPoint KUrlNavigator::savedPosition() const { // deprecated return d->m_history[d->m_historyIndex].pos; } #endif #ifndef KIOFILEWIDGETS_NO_DEPRECATED void KUrlNavigator::setHomeUrl(const QString &homeUrl) { // deprecated setLocationUrl(QUrl::fromUserInput(homeUrl)); } #endif #include "moc_kurlnavigator.cpp" diff --git a/src/filewidgets/kurlnavigatorbuttonbase.cpp b/src/filewidgets/kurlnavigatorbuttonbase.cpp index c30100ab..8108a509 100644 --- a/src/filewidgets/kurlnavigatorbuttonbase.cpp +++ b/src/filewidgets/kurlnavigatorbuttonbase.cpp @@ -1,151 +1,148 @@ /***************************************************************************** * Copyright (C) 2006-2010 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 "kurlnavigatorbuttonbase_p.h" #include -#include -#include -#include #include -#include +#include namespace KDEPrivate { KUrlNavigatorButtonBase::KUrlNavigatorButtonBase(QWidget *parent) : QPushButton(parent), m_active(true), m_displayHint(0) { setFocusPolicy(Qt::TabFocus); setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); setMinimumHeight(parent->minimumHeight()); setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(this, SIGNAL(pressed()), parent, SLOT(requestActivation())); } KUrlNavigatorButtonBase::~KUrlNavigatorButtonBase() { } void KUrlNavigatorButtonBase::setActive(bool active) { if (m_active != active) { m_active = active; update(); } } bool KUrlNavigatorButtonBase::isActive() const { return m_active; } void KUrlNavigatorButtonBase::setDisplayHintEnabled(DisplayHint hint, bool enable) { if (enable) { m_displayHint = m_displayHint | hint; } else { m_displayHint = m_displayHint & ~hint; } update(); } bool KUrlNavigatorButtonBase::isDisplayHintEnabled(DisplayHint hint) const { return (m_displayHint & hint) > 0; } void KUrlNavigatorButtonBase::focusInEvent(QFocusEvent *event) { setDisplayHintEnabled(EnteredHint, true); QPushButton::focusInEvent(event); } void KUrlNavigatorButtonBase::focusOutEvent(QFocusEvent *event) { setDisplayHintEnabled(EnteredHint, false); QPushButton::focusOutEvent(event); } void KUrlNavigatorButtonBase::enterEvent(QEvent *event) { QPushButton::enterEvent(event); setDisplayHintEnabled(EnteredHint, true); update(); } void KUrlNavigatorButtonBase::leaveEvent(QEvent *event) { QPushButton::leaveEvent(event); setDisplayHintEnabled(EnteredHint, false); update(); } void KUrlNavigatorButtonBase::drawHoverBackground(QPainter *painter) { const bool isHighlighted = isDisplayHintEnabled(EnteredHint) || isDisplayHintEnabled(DraggedHint) || isDisplayHintEnabled(PopupActiveHint); QColor backgroundColor = isHighlighted ? palette().color(QPalette::Highlight) : Qt::transparent; if (!m_active && isHighlighted) { backgroundColor.setAlpha(128); } if (backgroundColor != Qt::transparent) { // TODO: the backgroundColor should be applied to the style QStyleOptionViewItem option; option.initFrom(this); option.state = QStyle::State_Enabled | QStyle::State_MouseOver; option.viewItemPosition = QStyleOptionViewItem::OnlyOne; style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, this); } } QColor KUrlNavigatorButtonBase::foregroundColor() const { const bool isHighlighted = isDisplayHintEnabled(EnteredHint) || isDisplayHintEnabled(DraggedHint) || isDisplayHintEnabled(PopupActiveHint); QColor foregroundColor = palette().color(foregroundRole()); int alpha = m_active ? 255 : 128; if (!m_active && !isHighlighted) { alpha -= alpha / 4; } foregroundColor.setAlpha(alpha); return foregroundColor; } void KUrlNavigatorButtonBase::activate() { setActive(true); } } // namespace KDEPrivate #include "moc_kurlnavigatorbuttonbase_p.cpp" diff --git a/src/filewidgets/kurlnavigatortogglebutton.cpp b/src/filewidgets/kurlnavigatortogglebutton.cpp index f1bcfa2f..c613374e 100644 --- a/src/filewidgets/kurlnavigatortogglebutton.cpp +++ b/src/filewidgets/kurlnavigatortogglebutton.cpp @@ -1,109 +1,108 @@ /***************************************************************************** * Copyright (C) 2006 by Peter Penz * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public License * * along with this library; see the file COPYING.LIB. If not, write to * * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301, USA. * *****************************************************************************/ #include "kurlnavigatortogglebutton_p.h" #include #include +#include #include -#include -#include namespace KDEPrivate { KUrlNavigatorToggleButton::KUrlNavigatorToggleButton(QWidget *parent) : KUrlNavigatorButtonBase(parent) { setCheckable(true); connect(this, &QAbstractButton::toggled, this, &KUrlNavigatorToggleButton::updateToolTip); connect(this, &QAbstractButton::clicked, this, &KUrlNavigatorToggleButton::updateCursor); m_pixmap = QIcon::fromTheme(QStringLiteral("dialog-ok")).pixmap(QSize(22, 22).expandedTo(iconSize())); #ifndef QT_NO_ACCESSIBILITY setAccessibleName(i18n("Edit mode")); #endif updateToolTip(); } KUrlNavigatorToggleButton::~KUrlNavigatorToggleButton() { } QSize KUrlNavigatorToggleButton::sizeHint() const { QSize size = KUrlNavigatorButtonBase::sizeHint(); size.setWidth(m_pixmap.width() + 4); return size; } void KUrlNavigatorToggleButton::enterEvent(QEvent *event) { KUrlNavigatorButtonBase::enterEvent(event); updateCursor(); } void KUrlNavigatorToggleButton::leaveEvent(QEvent *event) { KUrlNavigatorButtonBase::leaveEvent(event); setCursor(Qt::ArrowCursor); } void KUrlNavigatorToggleButton::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setClipRect(event->rect()); const int buttonWidth = width(); const int buttonHeight = height(); if (isChecked()) { drawHoverBackground(&painter); style()->drawItemPixmap(&painter, rect(), Qt::AlignCenter, m_pixmap); } else if (isDisplayHintEnabled(EnteredHint)) { painter.setPen(Qt::NoPen); painter.setBrush(palette().color(foregroundRole())); const int verticalGap = 4; const int caretWidth = 2; const int x = (layoutDirection() == Qt::LeftToRight) ? 0 : buttonWidth - caretWidth; painter.drawRect(x, verticalGap, caretWidth, buttonHeight - 2 * verticalGap); } } void KUrlNavigatorToggleButton::updateToolTip() { if (isChecked()) { setToolTip(i18n("Click for Location Navigation")); } else { setToolTip(i18n("Click to Edit Location")); } } void KUrlNavigatorToggleButton::updateCursor() { setCursor(isChecked() ? Qt::ArrowCursor : Qt::IBeamCursor); } } // namespace KDEPrivate #include "moc_kurlnavigatortogglebutton_p.cpp" diff --git a/src/gui/faviconrequestjob.cpp b/src/gui/faviconrequestjob.cpp index d55cca00..0ba10a39 100644 --- a/src/gui/faviconrequestjob.cpp +++ b/src/gui/faviconrequestjob.cpp @@ -1,215 +1,212 @@ /* * This file is part of the KDE libraries * Copyright (c) 2016 David Faure * Copyright (C) 2001 Malte Starostik * * 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, see . */ #include "faviconrequestjob.h" #include #include "favicons_debug.h" #include -#include #include #include #include #include #include -#include #include #include #include -#include #include #include #include using namespace KIO; static bool isIconOld(const QString &icon) { const QFileInfo info(icon); if (!info.exists()) { qCDebug(FAVICONS_LOG) << "isIconOld" << icon << "yes, no such file"; return true; // Trigger a new download on error } const QDate date = info.lastModified().date(); qCDebug(FAVICONS_LOG) << "isIconOld" << icon << "?"; return date.daysTo(QDate::currentDate()) > 7; // arbitrary value (one week) } class KIO::FavIconRequestJobPrivate { public: FavIconRequestJobPrivate(const QUrl &hostUrl, KIO::LoadType reload) : m_hostUrl(hostUrl), m_reload(reload) {} // slots void slotData(KIO::Job *job, const QByteArray &data); QUrl m_hostUrl; QUrl m_iconUrl; QString m_iconFile; QByteArray m_iconData; KIO::LoadType m_reload; }; FavIconRequestJob::FavIconRequestJob(const QUrl &hostUrl, LoadType reload, QObject *parent) : KCompositeJob(parent), d(new FavIconRequestJobPrivate(hostUrl, reload)) { QMetaObject::invokeMethod(this, "doStart", Qt::QueuedConnection); } FavIconRequestJob::~FavIconRequestJob() { delete d; } void FavIconRequestJob::setIconUrl(const QUrl &iconUrl) { d->m_iconUrl = iconUrl; } QString FavIconRequestJob::iconFile() const { return d->m_iconFile; } QUrl FavIconRequestJob::hostUrl() const { return d->m_hostUrl; } void FavIconRequestJob::doStart() { KIO::FavIconsCache *cache = KIO::FavIconsCache::instance(); QUrl iconUrl = d->m_iconUrl; const bool isNewIconUrl = !iconUrl.isEmpty(); if (isNewIconUrl) { cache->setIconForUrl(d->m_hostUrl, d->m_iconUrl); } else { iconUrl = cache->iconUrlForUrl(d->m_hostUrl); } if (d->m_reload == NoReload) { const QString iconFile = cache->cachePathForIconUrl(iconUrl); if (!isIconOld(iconFile)) { qCDebug(FAVICONS_LOG) << "existing icon not old, reload not requested -> doing nothing"; d->m_iconFile = iconFile; emitResult(); return; } if (cache->isFailedDownload(iconUrl)) { qCDebug(FAVICONS_LOG) << iconUrl << "already in failedDownloads, emitting error"; setError(KIO::ERR_DOES_NOT_EXIST); setErrorText(i18n("No favicon found for %1", d->m_hostUrl.host())); emitResult(); return; } } qCDebug(FAVICONS_LOG) << "downloading" << iconUrl; KIO::TransferJob *job = KIO::get(iconUrl, d->m_reload, KIO::HideProgressInfo); QMap metaData; metaData.insert(QStringLiteral("ssl_no_client_cert"), QStringLiteral("true")); metaData.insert(QStringLiteral("ssl_no_ui"), QStringLiteral("true")); metaData.insert(QStringLiteral("UseCache"), QStringLiteral("false")); metaData.insert(QStringLiteral("cookies"), QStringLiteral("none")); metaData.insert(QStringLiteral("no-www-auth"), QStringLiteral("true")); metaData.insert(QStringLiteral("errorPage"), QStringLiteral("false")); job->addMetaData(metaData); QObject::connect(job, SIGNAL(data(KIO::Job*,QByteArray)), this, SLOT(slotData(KIO::Job*,QByteArray))); addSubjob(job); } void FavIconRequestJob::slotResult(KJob *job) { KIO::TransferJob *tjob = static_cast(job); const QUrl iconUrl = tjob->url(); KIO::FavIconsCache *cache = KIO::FavIconsCache::instance(); if (!job->error()) { QBuffer buffer(&d->m_iconData); buffer.open(QIODevice::ReadOnly); QImageReader ir(&buffer); QSize desired(16, 16); if (ir.canRead()) { while (ir.imageCount() > 1 && ir.currentImageRect() != QRect(0, 0, desired.width(), desired.height())) { if (!ir.jumpToNextImage()) { break; } } ir.setScaledSize(desired); const QImage img = ir.read(); if (!img.isNull()) { cache->ensureCacheExists(); const QString localPath = cache->cachePathForIconUrl(iconUrl); qCDebug(FAVICONS_LOG) << "Saving image to" << localPath; QSaveFile saveFile(localPath); if (saveFile.open(QIODevice::WriteOnly) && img.save(&saveFile, "PNG") && saveFile.commit()) { d->m_iconFile = localPath; } else { setError(KIO::ERR_COULD_NOT_WRITE); setErrorText(i18n("Error saving image to %1", localPath)); } } else { qCDebug(FAVICONS_LOG) << "QImageReader read() returned a null image"; } } else { qCDebug(FAVICONS_LOG) << "QImageReader canRead returned false"; } } else if (job->error() == KJob::KilledJobError) { // we killed it in slotData setError(KIO::ERR_SLAVE_DEFINED); setErrorText(i18n("Icon file too big, download aborted")); } else { setError(job->error()); setErrorText(job->errorString()); // not errorText(), because "this" is a KJob, with no errorString building logic } d->m_iconData.clear(); // release memory if (d->m_iconFile.isEmpty()) { qCDebug(FAVICONS_LOG) << "adding" << iconUrl << "to failed downloads due to error:" << errorString(); cache->addFailedDownload(iconUrl); } else { cache->removeFailedDownload(iconUrl); } KCompositeJob::removeSubjob(job); emitResult(); } void FavIconRequestJobPrivate::slotData(Job *job, const QByteArray &data) { KIO::TransferJob *tjob = static_cast(job); unsigned int oldSize = m_iconData.size(); // Size limit. Stop downloading if the file is huge. // Testcase (as of june 2008, at least): http://planet-soc.com/favicon.ico, 136K and strange format. // Another case: sites which redirect from "/favicon.ico" to "/" and return the main page. if (oldSize > 0x10000) { // 65K qCDebug(FAVICONS_LOG) << "Favicon too big, aborting download of" << tjob->url(); const QUrl iconUrl = tjob->url(); KIO::FavIconsCache::instance()->addFailedDownload(iconUrl); tjob->kill(KJob::EmitResult); } else { m_iconData.resize(oldSize + data.size()); memcpy(m_iconData.data() + oldSize, data.data(), data.size()); } } #include "moc_faviconrequestjob.cpp" diff --git a/src/ioslaves/file/file.cpp b/src/ioslaves/file/file.cpp index a25fd8ce..428c35d4 100644 --- a/src/ioslaves/file/file.cpp +++ b/src/ioslaves/file/file.cpp @@ -1,1557 +1,1555 @@ /* Copyright (C) 2000-2002 Stephan Kulow Copyright (C) 2000-2002 David Faure Copyright (C) 2000-2002 Waldo Bastian Copyright (C) 2006 Allan Sandfeld Jensen Copyright (C) 2007 Thiago Macieira 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. */ #include "file.h" #include #include #include #include #include "kioglobal_p.h" #ifdef Q_OS_UNIX #include "legacycodec.h" #endif #include #include #ifdef Q_OS_WIN #include #include #include //struct timeval #else #include #endif -#include #include #include #include -#include #include #include #ifdef Q_OS_WIN #include #include #endif #include #include #include #include #include #include #include #include #if HAVE_STATX #include #endif #if HAVE_VOLMGT #include #include #endif #include #include Q_LOGGING_CATEGORY(KIO_FILE, "kf5.kio.kio_file") // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.file" FILE "file.json") }; using namespace KIO; #define MAX_IPC_SIZE (1024*32) static QString readLogFile(const QByteArray &_filename); #if HAVE_POSIX_ACL static void appendACLAtoms(const QByteArray &path, UDSEntry &entry, mode_t type); #endif extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) { QCoreApplication app(argc, argv); // needed for QSocketNotifier app.setApplicationName(QStringLiteral("kio_file")); if (argc != 4) { fprintf(stderr, "Usage: kio_file protocol domain-socket1 domain-socket2\n"); exit(-1); } #ifdef Q_OS_UNIX LegacyCodec codec; #endif FileProtocol slave(argv[2], argv[3]); // Make sure the first kDebug is after the slave ctor (which sets a SIGPIPE handler) // This is useful in case kdeinit was autostarted by another app, which then exited and closed fd2 // (e.g. ctest does that, or closing the terminal window would do that) //qDebug() << "Starting" << getpid(); slave.dispatchLoop(); //qDebug() << "Done"; return 0; } static QFile::Permissions modeToQFilePermissions(int mode) { QFile::Permissions perms; if (mode & S_IRUSR) { perms |= QFile::ReadOwner; } if (mode & S_IWUSR) { perms |= QFile::WriteOwner; } if (mode & S_IXUSR) { perms |= QFile::ExeOwner; } if (mode & S_IRGRP) { perms |= QFile::ReadGroup; } if (mode & S_IWGRP) { perms |= QFile::WriteGroup; } if (mode & S_IXGRP) { perms |= QFile::ExeGroup; } if (mode & S_IROTH) { perms |= QFile::ReadOther; } if (mode & S_IWOTH) { perms |= QFile::WriteOther; } if (mode & S_IXOTH) { perms |= QFile::ExeOther; } return perms; } FileProtocol::FileProtocol(const QByteArray &pool, const QByteArray &app) : SlaveBase(QByteArrayLiteral("file"), pool, app), mFile(nullptr) { } FileProtocol::~FileProtocol() { } #if HAVE_POSIX_ACL static QString aclToText(acl_t acl) { ssize_t size = 0; char *txt = acl_to_text(acl, &size); const QString ret = QString::fromLatin1(txt, size); acl_free(txt); return ret; } #endif int FileProtocol::setACL(const char *path, mode_t perm, bool directoryDefault) { int ret = 0; #if HAVE_POSIX_ACL const QString ACLString = metaData(QStringLiteral("ACL_STRING")); const QString defaultACLString = metaData(QStringLiteral("DEFAULT_ACL_STRING")); // Empty strings mean leave as is if (!ACLString.isEmpty()) { acl_t acl = nullptr; if (ACLString == QLatin1String("ACL_DELETE")) { // user told us to delete the extended ACL, so let's write only // the minimal (UNIX permission bits) part acl = acl_from_mode(perm); } acl = acl_from_text(ACLString.toLatin1().constData()); if (acl_valid(acl) == 0) { // let's be safe ret = acl_set_file(path, ACL_TYPE_ACCESS, acl); // qDebug() << "Set ACL on:" << path << "to:" << aclToText(acl); } acl_free(acl); if (ret != 0) { return ret; // better stop trying right away } } if (directoryDefault && !defaultACLString.isEmpty()) { if (defaultACLString == QLatin1String("ACL_DELETE")) { // user told us to delete the default ACL, do so ret += acl_delete_def_file(path); } else { acl_t acl = acl_from_text(defaultACLString.toLatin1().constData()); if (acl_valid(acl) == 0) { // let's be safe ret += acl_set_file(path, ACL_TYPE_DEFAULT, acl); // qDebug() << "Set Default ACL on:" << path << "to:" << aclToText(acl); } acl_free(acl); } } #else Q_UNUSED(path); Q_UNUSED(perm); Q_UNUSED(directoryDefault); #endif return ret; } void FileProtocol::chmod(const QUrl &url, int permissions) { const QString path(url.toLocalFile()); const QByteArray _path(QFile::encodeName(path)); /* FIXME: Should be atomic */ #ifdef Q_OS_UNIX // QFile::Permissions does not support special attributes like sticky if (::chmod(_path.constData(), permissions) == -1 || #else if (!QFile::setPermissions(path, modeToQFilePermissions(permissions)) || #endif (setACL(_path.data(), permissions, false) == -1) || /* if not a directory, cannot set default ACLs */ (setACL(_path.data(), permissions, true) == -1 && errno != ENOTDIR)) { if (auto err = execWithElevatedPrivilege(CHMOD, {_path, permissions}, errno)) { if (!err.wasCanceled()) { switch (err) { case EPERM: case EACCES: error(KIO::ERR_ACCESS_DENIED, path); break; #if defined(ENOTSUP) case ENOTSUP: // from setACL since chmod can't return ENOTSUP error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Setting ACL for %1", path)); break; #endif case ENOSPC: error(KIO::ERR_DISK_FULL, path); break; default: error(KIO::ERR_CANNOT_CHMOD, path); } return; } } } finished(); } void FileProtocol::setModificationTime(const QUrl &url, const QDateTime &mtime) { const QString path(url.toLocalFile()); QT_STATBUF statbuf; if (QT_LSTAT(QFile::encodeName(path).constData(), &statbuf) == 0) { struct utimbuf utbuf; utbuf.actime = statbuf.st_atime; // access time, unchanged utbuf.modtime = mtime.toSecsSinceEpoch(); // modification time if (::utime(QFile::encodeName(path).constData(), &utbuf) != 0) { if (auto err = execWithElevatedPrivilege(UTIME, {path, qint64(utbuf.actime), qint64(utbuf.modtime)}, errno)) { if (!err.wasCanceled()) { // TODO: errno could be EACCES, EPERM, EROFS error(KIO::ERR_CANNOT_SETTIME, path); } } } else { finished(); } } else { error(KIO::ERR_DOES_NOT_EXIST, path); } } void FileProtocol::mkdir(const QUrl &url, int permissions) { const QString path(url.toLocalFile()); // qDebug() << path << "permission=" << permissions; // Remove existing file or symlink, if requested (#151851) if (metaData(QStringLiteral("overwrite")) == QLatin1String("true")) { if (!QFile::remove(path)) { execWithElevatedPrivilege(DEL, {path}, errno); } } QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(path).constData(), &buff) == -1) { bool dirCreated = QDir().mkdir(path); if (!dirCreated) { if (auto err = execWithElevatedPrivilege(MKDIR, {path}, errno)) { if (!err.wasCanceled()) { //TODO: add access denied & disk full (or another reasons) handling (into Qt, possibly) error(KIO::ERR_CANNOT_MKDIR, path); } return; } dirCreated = true; } if (dirCreated) { if (permissions != -1) { chmod(url, permissions); } else { finished(); } return; } } if ((buff.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { // qDebug() << "ERR_DIR_ALREADY_EXIST"; error(KIO::ERR_DIR_ALREADY_EXIST, path); return; } error(KIO::ERR_FILE_ALREADY_EXIST, path); return; } void FileProtocol::redirect(const QUrl &url) { QUrl redir(url); redir.setScheme(config()->readEntry("DefaultRemoteProtocol", "smb")); // if we would redirect into the Windows world, let's also check for the // DavWWWRoot "token" which in the Windows world tells win explorer to access // a webdav url // https://www.webdavsystem.com/server/access/windows if ((redir.scheme() == QLatin1String("smb")) && redir.path().startsWith(QLatin1String("/DavWWWRoot/"))) { redir.setPath(redir.path().mid(11)); // remove /DavWWWRoot redir.setScheme(QStringLiteral("webdav")); } redirection(redir); finished(); } void FileProtocol::get(const QUrl &url) { if (!url.isLocalFile()) { redirect(url); return; } const QString path(url.toLocalFile()); QT_STATBUF buff; if (QT_STAT(QFile::encodeName(path).constData(), &buff) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, path); } else { error(KIO::ERR_DOES_NOT_EXIST, path); } return; } if ((buff.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { error(KIO::ERR_IS_DIRECTORY, path); return; } if ((buff.st_mode & QT_STAT_MASK) != QT_STAT_REG) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); return; } QFile f(path); if (!f.open(QIODevice::ReadOnly)) { if (auto err = tryOpen(f, QFile::encodeName(path), O_RDONLY, S_IRUSR, errno)) { if (!err.wasCanceled()) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); } return; } } #if HAVE_FADVISE //TODO check return code posix_fadvise(f.handle(), 0, 0, POSIX_FADV_SEQUENTIAL); #endif // Determine the mimetype of the file to be retrieved, and emit it. // This is mandatory in all slaves (for KRun/BrowserRun to work) // In real "remote" slaves, this is usually done using mimeTypeForFileNameAndData // after receiving some data. But we don't know how much data the mimemagic rules // need, so for local files, better use mimeTypeForFile. QMimeDatabase db; QMimeType mt = db.mimeTypeForFile(url.toLocalFile()); mimeType(mt.name()); // Emit total size AFTER mimetype totalSize(buff.st_size); KIO::filesize_t processed_size = 0; QString resumeOffset = metaData(QStringLiteral("range-start")); if (resumeOffset.isEmpty()) { resumeOffset = metaData(QStringLiteral("resume")); // old name } if (!resumeOffset.isEmpty()) { bool ok; KIO::fileoffset_t offset = resumeOffset.toLongLong(&ok); if (ok && (offset > 0) && (offset < buff.st_size)) { if (f.seek(offset)) { canResume(); processed_size = offset; // qDebug() << "Resume offset:" << KIO::number(offset); } } } char buffer[ MAX_IPC_SIZE ]; QByteArray array; while (1) { int n = f.read(buffer, MAX_IPC_SIZE); if (n == -1) { if (errno == EINTR) { continue; } error(KIO::ERR_CANNOT_READ, path); f.close(); return; } if (n == 0) { break; // Finished } array = QByteArray::fromRawData(buffer, n); data(array); array.clear(); processed_size += n; processedSize(processed_size); //qDebug() << "Processed: " << KIO::number (processed_size); } data(QByteArray()); f.close(); processedSize(buff.st_size); finished(); } void FileProtocol::open(const QUrl &url, QIODevice::OpenMode mode) { // qDebug() << url; QString openPath = url.toLocalFile(); QT_STATBUF buff; if (QT_STAT(QFile::encodeName(openPath).constData(), &buff) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, openPath); } else { error(KIO::ERR_DOES_NOT_EXIST, openPath); } return; } if ((buff.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { error(KIO::ERR_IS_DIRECTORY, openPath); return; } if ((buff.st_mode & QT_STAT_MASK) != QT_STAT_REG) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, openPath); return; } mFile = new QFile(openPath); if (!mFile->open(mode)) { if (mode & QIODevice::ReadOnly) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, openPath); } else { error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, openPath); } return; } // Determine the mimetype of the file to be retrieved, and emit it. // This is mandatory in all slaves (for KRun/BrowserRun to work). // If we're not opening the file ReadOnly or ReadWrite, don't attempt to // read the file and send the mimetype. if (mode & QIODevice::ReadOnly) { QMimeDatabase db; QMimeType mt = db.mimeTypeForFile(url.toLocalFile()); mimeType(mt.name()); } totalSize(buff.st_size); position(0); opened(); } void FileProtocol::read(KIO::filesize_t bytes) { // qDebug() << "File::open -- read"; Q_ASSERT(mFile && mFile->isOpen()); while (true) { QByteArray res = mFile->read(bytes); if (!res.isEmpty()) { data(res); bytes -= res.size(); } else { // empty array designates eof data(QByteArray()); if (!mFile->atEnd()) { error(KIO::ERR_CANNOT_READ, mFile->fileName()); close(); } break; } if (bytes <= 0) { break; } } } void FileProtocol::write(const QByteArray &data) { // qDebug() << "File::open -- write"; Q_ASSERT(mFile && mFile->isWritable()); if (mFile->write(data) != data.size()) { if (mFile->error() == QFileDevice::ResourceError) { // disk full error(KIO::ERR_DISK_FULL, mFile->fileName()); close(); } else { qCWarning(KIO_FILE) << "Couldn't write. Error:" << mFile->errorString(); error(KIO::ERR_CANNOT_WRITE, mFile->fileName()); close(); } } else { written(data.size()); } } void FileProtocol::seek(KIO::filesize_t offset) { // qDebug() << "File::open -- seek"; Q_ASSERT(mFile && mFile->isOpen()); if (mFile->seek(offset)) { position(offset); } else { error(KIO::ERR_CANNOT_SEEK, mFile->fileName()); close(); } } void FileProtocol::close() { // qDebug() << "File::open -- close "; Q_ASSERT(mFile); delete mFile; mFile = nullptr; finished(); } void FileProtocol::put(const QUrl &url, int _mode, KIO::JobFlags _flags) { if (privilegeOperationUnitTestMode()) { finished(); return; } const QString dest_orig = url.toLocalFile(); // qDebug() << dest_orig << "mode=" << _mode; QString dest_part(dest_orig + QLatin1String(".part")); QT_STATBUF buff_orig; const bool bOrigExists = (QT_LSTAT(QFile::encodeName(dest_orig).constData(), &buff_orig) != -1); bool bPartExists = false; const bool bMarkPartial = config()->readEntry("MarkPartial", true); if (bMarkPartial) { QT_STATBUF buff_part; bPartExists = (QT_LSTAT(QFile::encodeName(dest_part).constData(), &buff_part) != -1); if (bPartExists && !(_flags & KIO::Resume) && !(_flags & KIO::Overwrite) && buff_part.st_size > 0 && ((buff_part.st_mode & QT_STAT_MASK) == QT_STAT_REG)) { // qDebug() << "calling canResume with" << KIO::number(buff_part.st_size); // Maybe we can use this partial file for resuming // Tell about the size we have, and the app will tell us // if it's ok to resume or not. _flags |= canResume(buff_part.st_size) ? KIO::Resume : KIO::DefaultFlags; // qDebug() << "got answer" << (_flags & KIO::Resume); } } if (bOrigExists && !(_flags & KIO::Overwrite) && !(_flags & KIO::Resume)) { if ((buff_orig.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { error(KIO::ERR_DIR_ALREADY_EXIST, dest_orig); } else { error(KIO::ERR_FILE_ALREADY_EXIST, dest_orig); } return; } int result; QString dest; QFile f; // Loop until we got 0 (end of data) do { QByteArray buffer; dataReq(); // Request for data result = readData(buffer); if (result >= 0) { if (dest.isEmpty()) { if (bMarkPartial) { // qDebug() << "Appending .part extension to" << dest_orig; dest = dest_part; if (bPartExists && !(_flags & KIO::Resume)) { // qDebug() << "Deleting partial file" << dest_part; QFile::remove(dest_part); // Catch errors when we try to open the file. } } else { dest = dest_orig; if (bOrigExists && !(_flags & KIO::Resume)) { // qDebug() << "Deleting destination file" << dest_orig; QFile::remove(dest_orig); // Catch errors when we try to open the file. } } f.setFileName(dest); if ((_flags & KIO::Resume)) { f.open(QIODevice::ReadWrite | QIODevice::Append); } else { f.open(QIODevice::Truncate | QIODevice::WriteOnly); if (_mode != -1) { // WABA: Make sure that we keep writing permissions ourselves, // otherwise we can be in for a surprise on NFS. mode_t initialMode = _mode | S_IWUSR | S_IRUSR; f.setPermissions(modeToQFilePermissions(initialMode)); } } if (!f.isOpen()) { int oflags = 0; int filemode = _mode; if ((_flags & KIO::Resume)) { oflags = O_RDWR | O_APPEND; } else { oflags = O_WRONLY | O_TRUNC | O_CREAT; if (_mode != -1) { filemode = _mode | S_IWUSR | S_IRUSR; } } if (auto err = tryOpen(f, QFile::encodeName(dest), oflags, filemode, errno)) { if (!err.wasCanceled()) { // qDebug() << "####################### COULD NOT WRITE" << dest << "_mode=" << _mode; // qDebug() << "QFile error==" << f.error() << "(" << f.errorString() << ")"; if (f.error() == QFileDevice::PermissionsError) { error(KIO::ERR_WRITE_ACCESS_DENIED, dest); } else { error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); } } return; } else { #ifndef Q_OS_WIN if ((_flags & KIO::Resume)) { execWithElevatedPrivilege(CHOWN, {dest, getuid(), getgid()}, errno); QFile::setPermissions(dest, modeToQFilePermissions(filemode)); } #endif } } } if (f.write(buffer) == -1) { if (f.error() == QFile::ResourceError) { // disk full error(KIO::ERR_DISK_FULL, dest_orig); result = -2; // means: remove dest file } else { qCWarning(KIO_FILE) << "Couldn't write. Error:" << f.errorString(); error(KIO::ERR_CANNOT_WRITE, dest_orig); result = -1; } } } } while (result > 0); // An error occurred deal with it. if (result < 0) { // qDebug() << "Error during 'put'. Aborting."; if (f.isOpen()) { f.close(); QT_STATBUF buff; if (QT_STAT(QFile::encodeName(dest).constData(), &buff) == 0) { int size = config()->readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); if (buff.st_size < size) { QFile::remove(dest); } } } ::exit(255); } if (!f.isOpen()) { // we got nothing to write out, so we never opened the file finished(); return; } f.close(); if (f.error() != QFile::NoError) { qCWarning(KIO_FILE) << "Error when closing file descriptor:" << f.errorString(); error(KIO::ERR_CANNOT_WRITE, dest_orig); return; } // after full download rename the file back to original name if (bMarkPartial) { //QFile::rename() never overwrites the destination file unlike ::remove, //so we must remove it manually first if (_flags & KIO::Overwrite) { if (!QFile::remove(dest_orig)) { execWithElevatedPrivilege(DEL, {dest_orig}, errno); } } if (!QFile::rename(dest, dest_orig)) { if (auto err = execWithElevatedPrivilege(RENAME, {dest, dest_orig}, errno)) { if (!err.wasCanceled()) { qCWarning(KIO_FILE) << " Couldn't rename " << dest << " to " << dest_orig; error(KIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig); } return; } } org::kde::KDirNotify::emitFileRenamed(QUrl::fromLocalFile(dest), QUrl::fromLocalFile(dest_orig)); } // set final permissions if (_mode != -1 && !(_flags & KIO::Resume)) { if (!QFile::setPermissions(dest_orig, modeToQFilePermissions(_mode))) { // couldn't chmod. Eat the error if the filesystem apparently doesn't support it. KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest_orig); if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod)) { if (tryChangeFileAttr(CHMOD, {dest_orig, _mode}, errno)) { warning(i18n("Could not change permissions for\n%1", dest_orig)); } } } } // set modification time const QString mtimeStr = metaData(QStringLiteral("modified")); if (!mtimeStr.isEmpty()) { QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate); if (dt.isValid()) { QT_STATBUF dest_statbuf; if (QT_STAT(QFile::encodeName(dest_orig).constData(), &dest_statbuf) == 0) { #ifndef Q_OS_WIN struct timeval utbuf[2]; // access time utbuf[0].tv_sec = dest_statbuf.st_atime; // access time, unchanged ## TODO preserve msec utbuf[0].tv_usec = 0; // modification time utbuf[1].tv_sec = dt.toSecsSinceEpoch(); utbuf[1].tv_usec = dt.time().msec() * 1000; utimes(QFile::encodeName(dest_orig).constData(), utbuf); #else struct utimbuf utbuf; utbuf.actime = dest_statbuf.st_atime; utbuf.modtime = dt.toSecsSinceEpoch(); if (utime(QFile::encodeName(dest_orig).constData(), &utbuf) != 0) { tryChangeFileAttr(UTIME, {dest_orig, qint64(utbuf.actime), qint64(utbuf.modtime)}, errno); } #endif } } } // We have done our job => finish finished(); } QString FileProtocol::getUserName(KUserId uid) const { if (Q_UNLIKELY(!uid.isValid())) { return QString(); } auto it = mUsercache.find(uid); if (it == mUsercache.end()) { KUser user(uid); QString name = user.loginName(); if (name.isEmpty()) { name = uid.toString(); } it = mUsercache.insert(uid, name); } return *it; } QString FileProtocol::getGroupName(KGroupId gid) const { if (Q_UNLIKELY(!gid.isValid())) { return QString(); } auto it = mGroupcache.find(gid); if (it == mGroupcache.end()) { KUserGroup group(gid); QString name = group.name(); if (name.isEmpty()) { name = gid.toString(); } it = mGroupcache.insert(gid, name); } return *it; } #if HAVE_STATX // statx syscall is available inline int LSTAT(const char* path, struct statx * buff) { return statx(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW, STATX_BASIC_STATS | STATX_BTIME, buff); } inline int STAT(const char* path, struct statx * buff) { return statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT, STATX_BASIC_STATS | STATX_BTIME, buff); } inline static uint16_t stat_mode(struct statx &buf) { return buf.stx_mode; } inline static uint32_t stat_dev(struct statx &buf) { return buf.stx_dev_major; } inline static uint64_t stat_ino(struct statx &buf) { return buf.stx_ino; } inline static uint64_t stat_size(struct statx &buf) { return buf.stx_size; } inline static uint32_t stat_uid(struct statx &buf) { return buf.stx_uid; } inline static uint32_t stat_gid(struct statx &buf) { return buf.stx_gid; } inline static int64_t stat_atime(struct statx &buf) { return buf.stx_atime.tv_sec; } inline static int64_t stat_mtime(struct statx &buf) { return buf.stx_mtime.tv_sec; } #else // regular stat struct inline int LSTAT(const char* path, QT_STATBUF * buff) { return QT_LSTAT(path, buff); } inline int STAT(const char* path, QT_STATBUF * buff) { return QT_STAT(path, buff); } inline static mode_t stat_mode(QT_STATBUF &buf) { return buf.st_mode; } inline static dev_t stat_dev(QT_STATBUF &buf) { return buf.st_dev; } inline static ino_t stat_ino(QT_STATBUF &buf) { return buf.st_ino; } inline static off_t stat_size(QT_STATBUF &buf) { return buf.st_size; } #ifndef Q_OS_WIN inline static uid_t stat_uid(QT_STATBUF &buf) { return buf.st_uid; } inline static gid_t stat_gid(QT_STATBUF &buf) { return buf.st_gid; } #endif inline static time_t stat_atime(QT_STATBUF &buf) { return buf.st_atime; } inline static time_t stat_mtime(QT_STATBUF &buf) { return buf.st_mtime; } #endif bool FileProtocol::createUDSEntry(const QString &filename, const QByteArray &path, UDSEntry &entry, short int details) { assert(entry.count() == 0); // by contract :-) switch (details) { case 0: // filename, access, type, size, linkdest entry.reserve(5); break; case 1: // uid, gid, atime, mtime, btime entry.reserve(10); break; case 2: // acl data entry.reserve(13); break; default: // case details > 2 // dev, inode entry.reserve(15); break; } entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); mode_t type; mode_t access; bool isBrokenSymLink = false; signed long long size = 0LL; #if HAVE_POSIX_ACL QByteArray targetPath = path; #endif #if HAVE_STATX // statx syscall is available struct statx buff; #else QT_STATBUF buff; #endif if (LSTAT(path.data(), &buff) == 0) { if (details > 2) { entry.fastInsert(KIO::UDSEntry::UDS_DEVICE_ID, stat_dev(buff)); entry.fastInsert(KIO::UDSEntry::UDS_INODE, stat_ino(buff)); } if ((stat_mode(buff) & QT_STAT_MASK) == QT_STAT_LNK) { #ifdef Q_OS_WIN const QString linkTarget = QFile::symLinkTarget(QFile::decodeName(path)); #else // Use readlink on Unix because symLinkTarget turns relative targets into absolute (#352927) #if HAVE_STATX size_t lowerBound = 256; size_t higherBound = 1024; uint64_t s = stat_size(buff); if (s > SIZE_MAX) { qCWarning(KIO_FILE) << "file size bigger than SIZE_MAX, too big for readlink use!" << path; return false; } size_t size = static_cast(s); using SizeType = size_t; #else off_t lowerBound = 256; off_t higherBound = 1024; off_t size = stat_size(buff); using SizeType = off_t; #endif SizeType bufferSize = qBound(lowerBound, size +1, higherBound); QByteArray linkTargetBuffer; linkTargetBuffer.resize(bufferSize); while (true) { ssize_t n = readlink(path.constData(), linkTargetBuffer.data(), bufferSize); if (n < 0 && errno != ERANGE) { qCWarning(KIO_FILE) << "readlink failed!" << path; return false; } else if (n > 0 && static_cast(n) != bufferSize) { // the buffer was not filled in the last iteration // we are finished reading, break the loop linkTargetBuffer.truncate(n); break; } bufferSize *= 2; linkTargetBuffer.resize(bufferSize); } const QString linkTarget = QFile::decodeName(linkTargetBuffer); #endif entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, linkTarget); // A symlink -> follow it only if details>1 if (details > 1) { if (STAT(path.constData(), &buff) == -1) { isBrokenSymLink = true; } else { #if HAVE_POSIX_ACL // valid symlink, will get the ACLs of the destination targetPath = linkTargetBuffer; #endif } } } } else { // qCWarning(KIO_FILE) << "lstat didn't work on " << path.data(); return false; } if (isBrokenSymLink) { // It is a link pointing to nowhere type = S_IFMT - 1; access = S_IRWXU | S_IRWXG | S_IRWXO; size = 0LL; } else { type = stat_mode(buff) & S_IFMT; // extract file type access = stat_mode(buff) & 07777; // extract permissions size = stat_size(buff); } entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, type); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, access); entry.fastInsert(KIO::UDSEntry::UDS_SIZE, size); #if HAVE_POSIX_ACL if (details > 1) { /* Append an atom indicating whether the file has extended acl information * and if withACL is specified also one with the acl itself. If it's a directory * and it has a default ACL, also append that. */ appendACLAtoms(targetPath, entry, type); } #endif if (details > 0) { entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, stat_mtime(buff)); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, stat_atime(buff)); #ifndef Q_OS_WIN entry.fastInsert(KIO::UDSEntry::UDS_USER, getUserName(KUserId(stat_uid(buff)))); entry.fastInsert(KIO::UDSEntry::UDS_GROUP, getGroupName(KGroupId(stat_gid(buff)))); #else #pragma message("TODO: st_uid and st_gid are always zero, use GetSecurityInfo to find the owner") #endif #ifdef st_birthtime /* For example FreeBSD's and NetBSD's stat contains a field for * the inode birth time: st_birthtime * This however only works on UFS and ZFS, and not, on say, NFS. * Instead of setting a bogus fallback like st_mtime, only use * it if it is greater than 0. */ if (buff.st_birthtime > 0) { entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.st_birthtime); } #elif defined __st_birthtime /* As above, but OpenBSD calls it slightly differently. */ if (buff.__st_birthtime > 0) { entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.__st_birthtime); } #elif HAVE_STATX /* And linux version using statx syscall */ if (buff.stx_mask & STATX_BTIME) { entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.stx_btime.tv_sec); } #endif } return true; } void FileProtocol::special(const QByteArray &data) { int tmp; QDataStream stream(data); stream >> tmp; switch (tmp) { case 1: { QString fstype, dev, point; qint8 iRo; stream >> iRo >> fstype >> dev >> point; bool ro = (iRo != 0); // qDebug() << "MOUNTING fstype=" << fstype << " dev=" << dev << " point=" << point << " ro=" << ro; bool ok = pmount(dev); if (ok) { finished(); } else { mount(ro, fstype.toLatin1().constData(), dev, point); } } break; case 2: { QString point; stream >> point; bool ok = pumount(point); if (ok) { finished(); } else { unmount(point); } } break; default: break; } } static QStringList fallbackSystemPath() { return QStringList{ QStringLiteral("/sbin"), QStringLiteral("/bin"), }; } void FileProtocol::mount(bool _ro, const char *_fstype, const QString &_dev, const QString &_point) { // qDebug() << "fstype=" << _fstype; #ifndef _WIN32_WCE #if HAVE_VOLMGT /* * support for Solaris volume management */ QString err; QByteArray devname = QFile::encodeName(_dev); if (volmgt_running()) { // qDebug() << "VOLMGT: vold ok."; if (volmgt_check(devname.data()) == 0) { // qDebug() << "VOLMGT: no media in " << devname.data(); err = i18n("No Media inserted or Media not recognized."); error(KIO::ERR_CANNOT_MOUNT, err); return; } else { // qDebug() << "VOLMGT: " << devname.data() << ": media ok"; finished(); return; } } else { err = i18n("\"vold\" is not running."); // qDebug() << "VOLMGT: " << err; error(KIO::ERR_CANNOT_MOUNT, err); return; } #else QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.open(); QByteArray tmpFileName = QFile::encodeName(tmpFile.fileName()); QByteArray dev; if (_dev.startsWith(QLatin1String("LABEL="))) { // turn LABEL=foo into -L foo (#71430) QString labelName = _dev.mid(6); dev = "-L " + QFile::encodeName(KShell::quoteArg(labelName)); // is it correct to assume same encoding as filesystem? } else if (_dev.startsWith(QLatin1String("UUID="))) { // and UUID=bar into -U bar QString uuidName = _dev.mid(5); dev = "-U " + QFile::encodeName(KShell::quoteArg(uuidName)); } else { dev = QFile::encodeName(KShell::quoteArg(_dev)); // get those ready to be given to a shell } QByteArray point = QFile::encodeName(KShell::quoteArg(_point)); bool fstype_empty = !_fstype || !*_fstype; QByteArray fstype = KShell::quoteArg(QString::fromLatin1(_fstype)).toLatin1(); // good guess QByteArray readonly = _ro ? "-r" : ""; QByteArray mountProg = QStandardPaths::findExecutable(QStringLiteral("mount")).toLocal8Bit(); if (mountProg.isEmpty()) { mountProg = QStandardPaths::findExecutable(QStringLiteral("mount"), fallbackSystemPath()).toLocal8Bit(); } if (mountProg.isEmpty()) { error(KIO::ERR_CANNOT_MOUNT, i18n("Could not find program \"mount\"")); return; } // Two steps, in case mount doesn't like it when we pass all options for (int step = 0; step <= 1; step++) { QByteArray buffer = mountProg + ' '; // Mount using device only if no fstype nor mountpoint (KDE-1.x like) if (!dev.isEmpty() && _point.isEmpty() && fstype_empty) { buffer += dev; } else // Mount using the mountpoint, if no fstype nor device (impossible in first step) if (!_point.isEmpty() && dev.isEmpty() && fstype_empty) { buffer += point; } else // mount giving device + mountpoint but no fstype if (!_point.isEmpty() && !dev.isEmpty() && fstype_empty) { buffer += readonly + ' ' + dev + ' ' + point; } else // mount giving device + mountpoint + fstype #if defined(__svr4__) && defined(Q_OS_SOLARIS) // MARCO for Solaris 8 and I // believe this is true for SVR4 in general buffer += "-F " + fstype + ' ' + (_ro ? "-oro" : "") + ' ' + dev + ' ' + point; #else buffer += readonly + " -t " + fstype + ' ' + dev + ' ' + point; #endif buffer += " 2>" + tmpFileName; // qDebug() << buffer; int mount_ret = system(buffer.constData()); QString err = readLogFile(tmpFileName); if (err.isEmpty() && mount_ret == 0) { finished(); return; } else { // Didn't work - or maybe we just got a warning KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(_dev); // Is the device mounted ? if (mp && mount_ret == 0) { // qDebug() << "mount got a warning:" << err; warning(err); finished(); return; } else { if ((step == 0) && !_point.isEmpty()) { // qDebug() << err; // qDebug() << "Mounting with those options didn't work, trying with only mountpoint"; fstype = ""; fstype_empty = true; dev = ""; // The reason for trying with only mountpoint (instead of // only device) is that some people (hi Malte!) have the // same device associated with two mountpoints // for different fstypes, like /dev/fd0 /mnt/e2floppy and // /dev/fd0 /mnt/dosfloppy. // If the user has the same mountpoint associated with two // different devices, well they shouldn't specify the // mountpoint but just the device. } else { error(KIO::ERR_CANNOT_MOUNT, err); return; } } } } #endif /* ! HAVE_VOLMGT */ #else QString err; err = i18n("mounting is not supported by wince."); error(KIO::ERR_CANNOT_MOUNT, err); #endif } void FileProtocol::unmount(const QString &_point) { #ifndef _WIN32_WCE QByteArray buffer; QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.open(); QByteArray tmpFileName = QFile::encodeName(tmpFile.fileName()); QString err; #if HAVE_VOLMGT /* * support for Solaris volume management */ char *devname; char *ptr; FILE *mnttab; struct mnttab mnt; if (volmgt_running()) { // qDebug() << "VOLMGT: looking for " << _point.toLocal8Bit(); if ((mnttab = QT_FOPEN(MNTTAB, "r")) == nullptr) { err = QLatin1String("could not open mnttab"); // qDebug() << "VOLMGT: " << err; error(KIO::ERR_CANNOT_UNMOUNT, err); return; } /* * since there's no way to derive the device name from * the mount point through the volmgt library (and * media_findname() won't work in this case), we have to * look ourselves... */ devname = nullptr; rewind(mnttab); while (getmntent(mnttab, &mnt) == nullptr) { if (strcmp(_point.toLocal8Bit(), mnt.mnt_mountp) == 0) { devname = mnt.mnt_special; break; } } fclose(mnttab); if (devname == nullptr) { err = QLatin1String("not in mnttab"); // qDebug() << "VOLMGT: " << QFile::encodeName(_point).data() << ": " << err; error(KIO::ERR_CANNOT_UNMOUNT, err); return; } /* * strip off the directory name (volume name) * the eject(1) command will handle unmounting and * physically eject the media (if possible) */ ptr = strrchr(devname, '/'); *ptr = '\0'; QByteArray qdevname(QFile::encodeName(KShell::quoteArg(QFile::decodeName(QByteArray(devname)))).data()); buffer = "/usr/bin/eject " + qdevname + " 2>" + tmpFileName; // qDebug() << "VOLMGT: eject " << qdevname; /* * from eject(1): exit status == 0 => need to manually eject * exit status == 4 => media was ejected */ if (WEXITSTATUS(system(buffer.constData())) == 4) { /* * this is not an error, so skip "readLogFile()" * to avoid wrong/confusing error popup. The * temporary file is removed by QTemporaryFile's * destructor, so don't do that manually. */ finished(); return; } } else { /* * eject(1) should do its job without vold(1M) running, * so we probably could call eject anyway, but since the * media is mounted now, vold must've died for some reason * during the user's session, so it should be restarted... */ err = i18n("\"vold\" is not running."); // qDebug() << "VOLMGT: " << err; error(KIO::ERR_CANNOT_UNMOUNT, err); return; } #else QByteArray umountProg = QStandardPaths::findExecutable(QStringLiteral("umount")).toLocal8Bit(); if (umountProg.isEmpty()) { umountProg = QStandardPaths::findExecutable(QStringLiteral("umount"), fallbackSystemPath()).toLocal8Bit(); } if (umountProg.isEmpty()) { error(KIO::ERR_CANNOT_UNMOUNT, i18n("Could not find program \"umount\"")); return; } buffer = umountProg + ' ' + QFile::encodeName(KShell::quoteArg(_point)) + " 2>" + tmpFileName; system(buffer.constData()); #endif /* HAVE_VOLMGT */ err = readLogFile(tmpFileName); if (err.isEmpty()) { finished(); } else { error(KIO::ERR_CANNOT_UNMOUNT, err); } #else QString err; err = i18n("unmounting is not supported by wince."); error(KIO::ERR_CANNOT_MOUNT, err); #endif } /************************************* * * pmount handling * *************************************/ bool FileProtocol::pmount(const QString &dev) { #ifndef _WIN32_WCE QString pmountProg = QStandardPaths::findExecutable(QStringLiteral("pmount")); if (pmountProg.isEmpty()) { pmountProg = QStandardPaths::findExecutable(QStringLiteral("pmount"), fallbackSystemPath()); } if (pmountProg.isEmpty()) { return false; } QByteArray buffer = QFile::encodeName(pmountProg) + ' ' + QFile::encodeName(KShell::quoteArg(dev)); int res = system(buffer.constData()); return res == 0; #else return false; #endif } bool FileProtocol::pumount(const QString &point) { #ifndef _WIN32_WCE KMountPoint::Ptr mp = KMountPoint::currentMountPoints(KMountPoint::NeedRealDeviceName).findByPath(point); if (!mp) { return false; } QString dev = mp->realDeviceName(); if (dev.isEmpty()) { return false; } QString pumountProg = QStandardPaths::findExecutable(QStringLiteral("pumount")); if (pumountProg.isEmpty()) { pumountProg = QStandardPaths::findExecutable(QStringLiteral("pumount"), fallbackSystemPath()); } if (pumountProg.isEmpty()) { return false; } const QByteArray buffer = QFile::encodeName(pumountProg) + ' ' + QFile::encodeName(KShell::quoteArg(dev)); int res = system(buffer.data()); return res == 0; #else return false; #endif } /************************************* * * Utilities * *************************************/ static QString readLogFile(const QByteArray &_filename) { QString result; QFile file(QFile::decodeName(_filename)); if (file.open(QIODevice::ReadOnly)) { result = QString::fromLocal8Bit(file.readAll()); } (void)file.remove(); return result; } /************************************* * * ACL handling helpers * *************************************/ #if HAVE_POSIX_ACL bool FileProtocol::isExtendedACL(acl_t acl) { return (acl_equiv_mode(acl, nullptr) != 0); } static void appendACLAtoms(const QByteArray &path, UDSEntry &entry, mode_t type) { // first check for a noop if (acl_extended_file(path.data()) == 0) { return; } acl_t acl = nullptr; acl_t defaultAcl = nullptr; bool isDir = (type & QT_STAT_MASK) == QT_STAT_DIR; // do we have an acl for the file, and/or a default acl for the dir, if it is one? acl = acl_get_file(path.data(), ACL_TYPE_ACCESS); /* Sadly libacl does not provided a means of checking for extended ACL and default * ACL separately. Since a directory can have both, we need to check again. */ if (isDir) { if (acl) { if (!FileProtocol::isExtendedACL(acl)) { acl_free(acl); acl = nullptr; } } defaultAcl = acl_get_file(path.data(), ACL_TYPE_DEFAULT); } if (acl || defaultAcl) { // qDebug() << path.constData() << "has extended ACL entries"; entry.fastInsert(KIO::UDSEntry::UDS_EXTENDED_ACL, 1); if (acl) { const QString str = aclToText(acl); entry.fastInsert(KIO::UDSEntry::UDS_ACL_STRING, str); // qDebug() << path.constData() << "ACL:" << str; acl_free(acl); } if (defaultAcl) { const QString str = aclToText(defaultAcl); entry.fastInsert(KIO::UDSEntry::UDS_DEFAULT_ACL_STRING, str); // qDebug() << path.constData() << "DEFAULT ACL:" << str; acl_free(defaultAcl); } } } #endif // We could port this to KTempDir::removeDir but then we wouldn't be able to tell the user // where exactly the deletion failed, in case of errors. bool FileProtocol::deleteRecursive(const QString &path) { //qDebug() << path; QDirIterator it(path, QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden, QDirIterator::Subdirectories); QStringList dirsToDelete; while (it.hasNext()) { const QString itemPath = it.next(); //qDebug() << "itemPath=" << itemPath; const QFileInfo info = it.fileInfo(); if (info.isDir() && !info.isSymLink()) { dirsToDelete.prepend(itemPath); } else { //qDebug() << "QFile::remove" << itemPath; if (!QFile::remove(itemPath)) { if (auto err = execWithElevatedPrivilege(DEL, {itemPath}, errno)) { if (!err.wasCanceled()) { error(KIO::ERR_CANNOT_DELETE, itemPath); } return false; } } } } QDir dir; for (const QString &itemPath : qAsConst(dirsToDelete)) { //qDebug() << "QDir::rmdir" << itemPath; if (!dir.rmdir(itemPath)) { if (auto err = execWithElevatedPrivilege(RMDIR, {itemPath}, errno)) { if (!err.wasCanceled()) { error(KIO::ERR_CANNOT_DELETE, itemPath); } return false; } } } return true; } void FileProtocol::fileSystemFreeSpace(const QUrl &url) { if (url.isLocalFile()) { const KDiskFreeSpaceInfo spaceInfo = KDiskFreeSpaceInfo::freeSpaceInfo(url.toLocalFile()); if (spaceInfo.isValid()) { setMetaData(QStringLiteral("total"), QString::number(spaceInfo.size())); setMetaData(QStringLiteral("available"), QString::number(spaceInfo.available())); finished(); } else { error(KIO::ERR_COULD_NOT_STAT, url.url()); } } else { error(KIO::ERR_UNSUPPORTED_PROTOCOL, url.url()); } } void FileProtocol::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); } break; } } // needed for JSON file embedding #include "file.moc" diff --git a/src/ioslaves/help/kio_help.cpp b/src/ioslaves/help/kio_help.cpp index e2940120..c5380130 100644 --- a/src/ioslaves/help/kio_help.cpp +++ b/src/ioslaves/help/kio_help.cpp @@ -1,402 +1,399 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Matthias Hoelzer-Kluepfel Copyright (C) 2001 Stephan Kulow Copyright (C) 2003 Cornelius Schumacher This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "kio_help.h" #include "xslt_help.h" #include #include #include #include #include #include -#include #include #include -#include #include -#include #include #include #include using namespace KIO; QString HelpProtocol::langLookup(const QString &fname) { QStringList search; // assemble the local search paths const QStringList localDoc = KDocTools::documentationDirs(); QStringList langs = KLocalizedString::languages(); langs.append(QStringLiteral("en")); langs.removeAll(QStringLiteral("C")); // this is kind of compat hack as we install our docs in en/ but the // default language is en_US for (QStringList::Iterator it = langs.begin(); it != langs.end(); ++it) if (*it == QLatin1String("en_US")) { *it = QStringLiteral("en"); } // look up the different languages int ldCount = localDoc.count(); search.reserve(ldCount * langs.size()); for (int id = 0; id < ldCount; id++) { QStringList::ConstIterator lang; for (lang = langs.constBegin(); lang != langs.constEnd(); ++lang) { search.append(QStringLiteral("%1/%2/%3").arg(localDoc[id], *lang, fname)); } } // try to locate the file for (QStringList::ConstIterator it = search.constBegin(); it != search.constEnd(); ++it) { //qDebug() << "Looking for help in: " << *it; QFileInfo info(*it); if (info.exists() && info.isFile() && info.isReadable()) { return *it; } if ((*it).endsWith(QLatin1String(".html"))) { const QString file = (*it).leftRef((*it).lastIndexOf(QLatin1Char('/'))) + QLatin1String("/index.docbook"); //qDebug() << "Looking for help in: " << file; info.setFile(file); if (info.exists() && info.isFile() && info.isReadable()) { return *it; } } } return QString(); } QString HelpProtocol::lookupFile(const QString &fname, const QString &query, bool &redirect) { redirect = false; const QString path = fname; QString result = langLookup(path); if (result.isEmpty()) { result = langLookup(path + QLatin1String("/index.html")); if (!result.isEmpty()) { QUrl red; red.setScheme(QStringLiteral("help")); red.setPath(path + QLatin1String("/index.html")); red.setQuery(query); redirection(red); //qDebug() << "redirect to " << red; redirect = true; } else { const QString documentationNotFound = QStringLiteral("kioslave5/help/documentationnotfound/index.html"); if (!langLookup(documentationNotFound).isEmpty()) { QUrl red; red.setScheme(QStringLiteral("help")); red.setPath(documentationNotFound); red.setQuery(query); redirection(red); redirect = true; } else { sendError(i18n("There is no documentation available for %1.", path.toHtmlEscaped())); return QString(); } } } else { //qDebug() << "result " << result; } return result; } void HelpProtocol::sendError(const QString &t) { data(QStringLiteral( "\n%1").arg(t.toHtmlEscaped()).toUtf8()); } HelpProtocol *slave = nullptr; HelpProtocol::HelpProtocol(bool ghelp, const QByteArray &pool, const QByteArray &app) : SlaveBase(ghelp ? QByteArrayLiteral("ghelp") : QByteArrayLiteral("help"), pool, app), mGhelp(ghelp) { slave = this; } void HelpProtocol::get(const QUrl &url) { ////qDebug() << "path=" << url.path() //<< "query=" << url.query(); bool redirect; QString doc = QDir::cleanPath(url.path()); if (doc.contains(QLatin1String(".."))) { error(KIO::ERR_DOES_NOT_EXIST, url.toString()); return; } if (!mGhelp) { if (!doc.startsWith(QLatin1Char('/'))) { doc = doc.prepend(QLatin1Char('/')); } if (doc.endsWith(QLatin1Char('/'))) { doc += QLatin1String("index.html"); } } infoMessage(i18n("Looking up correct file")); if (!mGhelp) { doc = lookupFile(doc, url.query(), redirect); if (redirect) { finished(); return; } } if (doc.isEmpty()) { error(KIO::ERR_DOES_NOT_EXIST, url.toString()); return; } QUrl target; target.setPath(doc); if (url.hasFragment()) { target.setFragment(url.fragment()); } //qDebug() << "target " << target; QString file = target.isLocalFile() ? target.toLocalFile() : target.path(); if (mGhelp) { if (!file.endsWith(QLatin1String(".xml"))) { get_file(file); return; } } else { const QString docbook_file = file.leftRef(file.lastIndexOf(QLatin1Char('/'))) + QLatin1String("/index.docbook"); if (!QFile::exists(file)) { file = docbook_file; } else { QFileInfo fi(file); if (fi.isDir()) { file = file + QLatin1String("/index.docbook"); } else { if (!file.endsWith(QLatin1String(".html")) || !compareTimeStamps(file, docbook_file)) { get_file(file); return; } else { file = docbook_file; } } } } infoMessage(i18n("Preparing document")); mimeType(QStringLiteral("text/html")); if (mGhelp) { QString xsl = QStringLiteral("customization/kde-nochunk.xsl"); mParsed = KDocTools::transform(file, KDocTools::locateFileInDtdResource(xsl)); //qDebug() << "parsed " << mParsed.length(); if (mParsed.isEmpty()) { sendError(i18n("The requested help file could not be parsed:
%1", file)); } else { int pos1 = mParsed.indexOf(QStringLiteral("charset=")); if (pos1 > 0) { int pos2 = mParsed.indexOf(QLatin1Char('"'), pos1); if (pos2 > 0) { mParsed.replace(pos1, pos2 - pos1, QStringLiteral("charset=UTF-8")); } } data(mParsed.toUtf8()); } } else { //qDebug() << "look for cache for " << file; mParsed = lookForCache(file); //qDebug() << "cached parsed " << mParsed.length(); if (mParsed.isEmpty()) { mParsed = KDocTools::transform(file, KDocTools::locateFileInDtdResource(QStringLiteral("customization/kde-chunk.xsl"))); if (!mParsed.isEmpty()) { infoMessage(i18n("Saving to cache")); #ifdef Q_OS_WIN QFileInfo fi(file); // make sure filenames do not contain the base path, otherwise // accessing user data from another location invalids cached files // Accessing user data under a different path is possible // when using usb sticks - this may affect unix/mac systems also const QString installPath = KDocTools::documentationDirs().last(); QString cache = QLatin1Char('/') + fi.absolutePath().remove(installPath, Qt::CaseInsensitive).replace(QLatin1Char('/'), QLatin1Char('_')) + QLatin1Char('_') + fi.baseName() + QLatin1Char('.'); #else QString cache = file.left(file.length() - 7); #endif KDocTools::saveToCache(mParsed, QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_help") + cache + QLatin1String("cache.bz2")); } } else { infoMessage(i18n("Using cached version")); } //qDebug() << "parsed " << mParsed.length(); if (mParsed.isEmpty()) { sendError(i18n("The requested help file could not be parsed:
%1", file)); } else { QString anchor; QString query = url.query(); // if we have a query, look if it contains an anchor if (!query.isEmpty()) if (query.startsWith(QLatin1String("?anchor="))) { anchor = query.mid(8).toLower(); QUrl redirURL(url); redirURL.setQuery(QString()); redirURL.setFragment(anchor); redirection(redirURL); finished(); return; } if (anchor.isEmpty() && url.hasFragment()) { anchor = url.fragment(); } //qDebug() << "anchor: " << anchor; if (!anchor.isEmpty()) { int index = 0; while (true) { index = mParsed.indexOf(QRegExp(QStringLiteral("
").arg(anchor)) { index = mParsed.lastIndexOf(QLatin1String(" #include -#include #include #include #include #include #include #include #include #include #include #include #include #include // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.ghelp" FILE "ghelp.json") }; extern "C" { Q_DECL_EXPORT int kdemain(int argc, char **argv) { KDocTools::setupStandardDirs(); //qDebug() << "Starting " << getpid(); if (argc != 4) { fprintf(stderr, "Usage: kio_ghelp protocol domain-socket1 domain-socket2\n"); exit(-1); } LIBXML_TEST_VERSION xmlSubstituteEntitiesDefault(1); xmlLoadExtDtdDefaultValue = 1; exsltRegisterAll(); HelpProtocol slave(true, argv[2], argv[3]); slave.dispatchLoop(); //qDebug() << "Done"; return 0; } } // needed for JSON file embedding #include "main_ghelp.moc" diff --git a/src/ioslaves/help/xslt_help.cpp b/src/ioslaves/help/xslt_help.cpp index 14ec4ac0..3393d315 100644 --- a/src/ioslaves/help/xslt_help.cpp +++ b/src/ioslaves/help/xslt_help.cpp @@ -1,100 +1,99 @@ #include "xslt_help.h" #include #include #include #include #include #include #include #include -#include #include -#include #include +#include #include #include static bool readCache(const QString &filename, const QString &cache, QString &output) { //qDebug() << filename << cache; if (!compareTimeStamps(filename, cache)) { return false; } if (!compareTimeStamps(KDocTools::locateFileInDtdResource(QStringLiteral("customization/kde-chunk.xsl")), cache)) { return false; } //qDebug() << "create filter"; KFilterDev fd(cache); if (!fd.open(QIODevice::ReadOnly)) { QFile::remove(cache); return false; } //qDebug() << "reading"; char buffer[32000]; int n; QByteArray text; // Also end loop in case of error, when -1 is returned while ((n = fd.read(buffer, 31900)) > 0) { buffer[n] = 0; text += buffer; } //qDebug() << "read " << text.length(); fd.close(); output = QString::fromUtf8(text); if (n == -1) { return false; } //qDebug() << "finished "; return true; } QString lookForCache(const QString &filename) { //qDebug() << "lookForCache" << filename; Q_ASSERT(filename.endsWith(QLatin1String(".docbook"))); Q_ASSERT(QDir::isAbsolutePath(filename)); QString cache = filename.left(filename.length() - 7); QString output; if (readCache(filename, cache + QLatin1String("cache.bz2"), output)) { return output; } #ifdef Q_OS_WIN QFileInfo fi(filename); // make sure filenames do not contain the base path, otherwise // accessing user data from another location invalids cached files. // Accessing user data under a different path is possible // when using usb sticks - this may affect unix/mac systems also const QString installPath = KDocTools::documentationDirs().last(); cache = QLatin1Char('/') + fi.absolutePath().remove(installPath, Qt::CaseInsensitive).replace(QLatin1Char('/'), QLatin1Char('_')) + QLatin1Char('_') + fi.baseName() + QLatin1Char('.'); #endif if (readCache(filename, QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_help") + cache + QLatin1String("cache.bz2"), output)) { return output; } return QString(); } bool compareTimeStamps(const QString &older, const QString &newer) { QFileInfo _older(older); QFileInfo _newer(newer); Q_ASSERT(_older.exists()); if (!_newer.exists()) { return false; } return (_newer.lastModified() > _older.lastModified()); } diff --git a/src/ioslaves/http/http.cpp b/src/ioslaves/http/http.cpp index 53af024a..f89f4b7d 100644 --- a/src/ioslaves/http/http.cpp +++ b/src/ioslaves/http/http.cpp @@ -1,5628 +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 #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() == QLatin1String("T")) { foundExecutable = true; } } else if (property.tagName() == QLatin1String("getlastmodified")) { // Last modification date entry.replace(KIO::UDSEntry::UDS_MODIFICATION_TIME, parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).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); cacheFileClose(); // this sends an update command to the cache cleaner process } m_request = savedRequest; finished(); break; } case 5: { // WebDAV lock QUrl url; QString scope, type, owner; stream >> url >> scope >> type >> owner; davLock(url, scope, type, owner); break; } case 6: { // WebDAV unlock QUrl url; stream >> url; davUnlock(url); break; } case 7: { // Generic WebDAV QUrl url; int method; qint64 size; stream >> url >> method >> size; davGeneric(url, (KIO::HTTP_METHOD) method, size); break; } case 99: { // Close Connection httpCloseConnection(); break; } default: // Some command we don't understand. // Just ignore it, it may come from some future version of KDE. break; } } /** * Read a chunk from the data stream. */ int HTTPProtocol::readChunked() { if ((m_iBytesLeft == 0) || (m_iBytesLeft == NO_SIZE)) { // discard CRLF from previous chunk, if any, and read size of next chunk int bufPos = 0; m_receiveBuf.resize(4096); bool foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); if (foundCrLf && bufPos == 2) { // The previous read gave us the CRLF from the previous chunk. As bufPos includes // the trailing CRLF it has to be > 2 to possibly include the next chunksize. bufPos = 0; foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); } if (!foundCrLf) { qCDebug(KIO_HTTP) << "Failed to read chunk header."; return -1; } Q_ASSERT(bufPos > 2); long long nextChunkSize = STRTOLL(m_receiveBuf.data(), nullptr, 16); if (nextChunkSize < 0) { qCDebug(KIO_HTTP) << "Negative chunk size"; return -1; } m_iBytesLeft = nextChunkSize; qCDebug(KIO_HTTP) << "Chunk size =" << m_iBytesLeft << "bytes"; if (m_iBytesLeft == 0) { // Last chunk; read and discard chunk trailer. // The last trailer line ends with CRLF and is followed by another CRLF // so we have CRLFCRLF like at the end of a standard HTTP header. // Do not miss a CRLFCRLF spread over two of our 4K blocks: keep three previous bytes. //NOTE the CRLF after the chunksize also counts if there is no trailer. Copy it over. char trash[4096]; trash[0] = m_receiveBuf.constData()[bufPos - 2]; trash[1] = m_receiveBuf.constData()[bufPos - 1]; int trashBufPos = 2; bool done = false; while (!done && !m_isEOF) { if (trashBufPos > 3) { // shift everything but the last three bytes out of the buffer for (int i = 0; i < 3; i++) { trash[i] = trash[trashBufPos - 3 + i]; } trashBufPos = 3; } done = readDelimitedText(trash, &trashBufPos, 4096, 2); } if (m_isEOF && !done) { qCDebug(KIO_HTTP) << "Failed to read chunk trailer."; return -1; } return 0; } } int bytesReceived = readLimited(); if (!m_iBytesLeft) { m_iBytesLeft = NO_SIZE; // Don't stop, continue with next chunk } return bytesReceived; } int HTTPProtocol::readLimited() { if (!m_iBytesLeft) { return 0; } m_receiveBuf.resize(4096); int bytesToReceive; if (m_iBytesLeft > KIO::filesize_t(m_receiveBuf.size())) { bytesToReceive = m_receiveBuf.size(); } else { bytesToReceive = m_iBytesLeft; } const int bytesReceived = readBuffered(m_receiveBuf.data(), bytesToReceive, false); if (bytesReceived <= 0) { return -1; // Error: connection lost } m_iBytesLeft -= bytesReceived; return bytesReceived; } int HTTPProtocol::readUnlimited() { if (m_request.isKeepAlive) { qCDebug(KIO_HTTP) << "Unbounded datastream on a Keep-alive connection!"; m_request.isKeepAlive = false; } m_receiveBuf.resize(4096); int result = readBuffered(m_receiveBuf.data(), m_receiveBuf.size()); if (result > 0) { return result; } m_isEOF = true; m_iBytesLeft = 0; return 0; } void HTTPProtocol::slotData(const QByteArray &_d) { if (!_d.size()) { m_isEOD = true; return; } if (m_iContentLeft != NO_SIZE) { if (m_iContentLeft >= KIO::filesize_t(_d.size())) { m_iContentLeft -= _d.size(); } else { m_iContentLeft = NO_SIZE; } } QByteArray d = _d; if (!m_dataInternal) { // If a broken server does not send the mime-type, // we try to id it from the content before dealing // with the content itself. if (m_mimeType.isEmpty() && !m_isRedirection && !(m_request.responseCode >= 300 && m_request.responseCode <= 399)) { qCDebug(KIO_HTTP) << "Determining mime-type from content..."; int old_size = m_mimeTypeBuffer.size(); m_mimeTypeBuffer.resize(old_size + d.size()); memcpy(m_mimeTypeBuffer.data() + old_size, d.data(), d.size()); if ((m_iBytesLeft != NO_SIZE) && (m_iBytesLeft > 0) && (m_mimeTypeBuffer.size() < 1024)) { m_cpMimeBuffer = true; return; // Do not send up the data since we do not yet know its mimetype! } qCDebug(KIO_HTTP) << "Mimetype buffer size:" << m_mimeTypeBuffer.size(); QMimeDatabase db; QMimeType mime = db.mimeTypeForFileNameAndData(m_request.url.adjusted(QUrl::StripTrailingSlash).path(), m_mimeTypeBuffer); if (mime.isValid() && !mime.isDefault()) { m_mimeType = mime.name(); qCDebug(KIO_HTTP) << "Mimetype from content:" << m_mimeType; } if (m_mimeType.isEmpty()) { m_mimeType = QStringLiteral(DEFAULT_MIME_TYPE); qCDebug(KIO_HTTP) << "Using default mimetype:" << m_mimeType; } //### we could also open the cache file here if (m_cpMimeBuffer) { d.resize(0); d.resize(m_mimeTypeBuffer.size()); memcpy(d.data(), m_mimeTypeBuffer.data(), d.size()); } mimeType(m_mimeType); m_mimeTypeBuffer.resize(0); } //qDebug() << "Sending data of size" << d.size(); data(d); if (m_request.cacheTag.ioMode == WriteToCache) { cacheFileWritePayload(d); } } else { uint old_size = m_webDavDataBuf.size(); m_webDavDataBuf.resize(old_size + d.size()); memcpy(m_webDavDataBuf.data() + old_size, d.data(), d.size()); } } /** * This function is our "receive" function. It is responsible for * downloading the message (not the header) from the HTTP server. It * is called either as a response to a client's KIOJob::dataEnd() * (meaning that the client is done sending data) or by 'sendQuery()' * (if we are in the process of a PUT/POST request). It can also be * called by a webDAV function, to receive stat/list/property/etc. * data; in this case the data is stored in m_webDavDataBuf. */ bool HTTPProtocol::readBody(bool dataInternal /* = false */) { // special case for reading cached body since we also do it in this function. oh well. if (!canHaveResponseBody(m_request.responseCode, m_request.method) && !(m_request.cacheTag.ioMode == ReadFromCache && m_request.responseCode == 304 && m_request.method != HTTP_HEAD)) { return true; } m_isEOD = false; // Note that when dataInternal is true, we are going to: // 1) save the body data to a member variable, m_webDavDataBuf // 2) _not_ advertise the data, speed, size, etc., through the // corresponding functions. // This is used for returning data to WebDAV. m_dataInternal = dataInternal; if (dataInternal) { m_webDavDataBuf.clear(); } // Check if we need to decode the data. // If we are in copy mode, then use only transfer decoding. bool useMD5 = !m_contentMD5.isEmpty(); // Deal with the size of the file. KIO::filesize_t sz = m_request.offset; if (sz) { m_iSize += sz; } if (!m_isRedirection) { // Update the application with total size except when // it is compressed, or when the data is to be handled // internally (webDAV). If compressed we have to wait // until we uncompress to find out the actual data size if (!dataInternal) { if ((m_iSize > 0) && (m_iSize != NO_SIZE)) { totalSize(m_iSize); infoMessage(i18n("Retrieving %1 from %2...", KIO::convertSize(m_iSize), m_request.url.host())); } else { totalSize(0); } } if (m_request.cacheTag.ioMode == ReadFromCache) { qCDebug(KIO_HTTP) << "reading data from cache..."; m_iContentLeft = NO_SIZE; QByteArray d; while (true) { d = cacheFileReadPayload(MAX_IPC_SIZE); if (d.isEmpty()) { break; } slotData(d); sz += d.size(); if (!dataInternal) { processedSize(sz); } } m_receiveBuf.resize(0); if (!dataInternal) { data(QByteArray()); } return true; } } if (m_iSize != NO_SIZE) { m_iBytesLeft = m_iSize - sz; } else { m_iBytesLeft = NO_SIZE; } m_iContentLeft = m_iBytesLeft; if (m_isChunked) { m_iBytesLeft = NO_SIZE; } qCDebug(KIO_HTTP) << KIO::number(m_iBytesLeft) << "bytes left."; // Main incoming loop... Gather everything while we can... m_cpMimeBuffer = false; m_mimeTypeBuffer.resize(0); HTTPFilterChain chain; // redirection ignores the body if (!m_isRedirection) { QObject::connect(&chain, &HTTPFilterBase::output, this, &HTTPProtocol::slotData); } QObject::connect(&chain, &HTTPFilterBase::error, this, &HTTPProtocol::slotFilterError); // decode all of the transfer encodings while (!m_transferEncodings.isEmpty()) { QString enc = m_transferEncodings.takeLast(); if (enc == QLatin1String("gzip")) { chain.addFilter(new HTTPFilterGZip); } else if (enc == QLatin1String("deflate")) { chain.addFilter(new HTTPFilterDeflate); } } // From HTTP 1.1 Draft 6: // The MD5 digest is computed based on the content of the entity-body, // including any content-coding that has been applied, but not including // any transfer-encoding applied to the message-body. If the message is // received with a transfer-encoding, that encoding MUST be removed // prior to checking the Content-MD5 value against the received entity. HTTPFilterMD5 *md5Filter = nullptr; if (useMD5) { md5Filter = new HTTPFilterMD5; chain.addFilter(md5Filter); } // now decode all of the content encodings // -- Why ?? We are not // -- a proxy server, be a client side implementation!! The applications // -- are capable of determining how to extract the encoded implementation. // WB: That's a misunderstanding. We are free to remove the encoding. // WB: Some braindead www-servers however, give .tgz files an encoding // WB: of "gzip" (or even "x-gzip") and a content-type of "applications/tar" // WB: They shouldn't do that. We can work around that though... while (!m_contentEncodings.isEmpty()) { QString enc = m_contentEncodings.takeLast(); if (enc == QLatin1String("gzip")) { chain.addFilter(new HTTPFilterGZip); } else if (enc == QLatin1String("deflate")) { chain.addFilter(new HTTPFilterDeflate); } } while (!m_isEOF) { int bytesReceived; if (m_isChunked) { bytesReceived = readChunked(); } else if (m_iSize != NO_SIZE) { bytesReceived = readLimited(); } else { bytesReceived = readUnlimited(); } // make sure that this wasn't an error, first qCDebug(KIO_HTTP) << "bytesReceived:" << bytesReceived << " m_iSize:" << (int)m_iSize << " Chunked:" << m_isChunked << " BytesLeft:"<< (int)m_iBytesLeft; if (bytesReceived == -1) { if (m_iContentLeft == 0) { // gzip'ed data sometimes reports a too long content-length. // (The length of the unzipped data) m_iBytesLeft = 0; break; } // Oh well... log an error and bug out qCDebug(KIO_HTTP) << "bytesReceived==-1 sz=" << (int)sz << " Connection broken !"; error(ERR_CONNECTION_BROKEN, m_request.url.host()); return false; } // I guess that nbytes == 0 isn't an error.. but we certainly // won't work with it! if (bytesReceived > 0) { // Important: truncate the buffer to the actual size received! // Otherwise garbage will be passed to the app m_receiveBuf.truncate(bytesReceived); chain.slotInput(m_receiveBuf); if (m_kioError) { return false; } sz += bytesReceived; if (!dataInternal) { processedSize(sz); } } m_receiveBuf.resize(0); // res if (m_iBytesLeft && m_isEOD && !m_isChunked) { // gzip'ed data sometimes reports a too long content-length. // (The length of the unzipped data) m_iBytesLeft = 0; } if (m_iBytesLeft == 0) { qCDebug(KIO_HTTP) << "EOD received! Left ="<< KIO::number(m_iBytesLeft); break; } } chain.slotInput(QByteArray()); // Flush chain. if (useMD5) { QString calculatedMD5 = md5Filter->md5(); if (m_contentMD5 != calculatedMD5) qCWarning(KIO_HTTP) << "MD5 checksum MISMATCH! Expected:" << calculatedMD5 << ", Got:" << m_contentMD5; } // Close cache entry if (m_iBytesLeft == 0) { cacheFileClose(); // no-op if not necessary } if (!dataInternal && sz <= 1) { if (m_request.responseCode >= 500 && m_request.responseCode <= 599) { error(ERR_INTERNAL_SERVER, m_request.url.host()); return false; } else if (m_request.responseCode >= 400 && m_request.responseCode <= 499 && !isAuthenticationRequired(m_request.responseCode)) { error(ERR_DOES_NOT_EXIST, m_request.url.host()); return false; } } if (!dataInternal && !m_isRedirection) { data(QByteArray()); } return true; } void HTTPProtocol::slotFilterError(const QString &text) { error(KIO::ERR_SLAVE_DEFINED, text); } void HTTPProtocol::error(int _err, const QString &_text) { // Close the connection only on connection errors. Otherwise, honor the // keep alive flag. if (_err == ERR_CONNECTION_BROKEN || _err == ERR_CANNOT_CONNECT) { httpClose(false); } else { httpClose(m_request.isKeepAlive); } if (!m_request.id.isEmpty()) { forwardHttpResponseHeader(); sendMetaData(); } // It's over, we don't need it anymore clearPostDataBuffer(); SlaveBase::error(_err, _text); m_kioError = _err; } void HTTPProtocol::addCookies(const QString &url, const QByteArray &cookieHeader) { qlonglong windowId = m_request.windowId.toLongLong(); QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); (void)kcookiejar.call(QDBus::NoBlock, QStringLiteral("addCookies"), url, cookieHeader, windowId); } QString HTTPProtocol::findCookies(const QString &url) { qlonglong windowId = m_request.windowId.toLongLong(); QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); QDBusReply reply = kcookiejar.call(QStringLiteral("findCookies"), url, windowId); if (!reply.isValid()) { qCWarning(KIO_HTTP) << "Can't communicate with kded_kcookiejar!"; return QString(); } return reply; } /******************************* CACHING CODE ****************************/ HTTPProtocol::CacheTag::CachePlan HTTPProtocol::CacheTag::plan(int maxCacheAge) const { //notable omission: we're not checking cache file presence or integrity switch (policy) { case KIO::CC_Refresh: // Conditional GET requires the presence of either an ETag or // last modified date. if (lastModifiedDate.isValid() || !etag.isEmpty()) { return ValidateCached; } break; case KIO::CC_Reload: return IgnoreCached; case KIO::CC_CacheOnly: case KIO::CC_Cache: return UseCached; default: break; } Q_ASSERT((policy == CC_Verify || policy == CC_Refresh)); QDateTime currentDate = QDateTime::currentDateTime(); if ((servedDate.isValid() && (currentDate > servedDate.addSecs(maxCacheAge))) || (expireDate.isValid() && (currentDate > expireDate))) { return ValidateCached; } return UseCached; } // !START SYNC! // The following code should be kept in sync // with the code in http_cache_cleaner.cpp // we use QDataStream; this is just an illustration struct BinaryCacheFileHeader { quint8 version[2]; quint8 compression; // for now fixed to 0 quint8 reserved; // for now; also alignment qint32 useCount; qint64 servedDate; qint64 lastModifiedDate; qint64 expireDate; qint32 bytesCached; // packed size should be 36 bytes; we explicitly set it here to make sure that no compiler // padding ruins it. We write the fields to disk without any padding. static const int size = 36; }; enum CacheCleanerCommandCode { InvalidCommand = 0, CreateFileNotificationCommand, UpdateFileCommand }; // illustration for cache cleaner update "commands" struct CacheCleanerCommand { BinaryCacheFileHeader header; quint32 commandCode; // filename in ASCII, binary isn't worth the coding and decoding quint8 filename[s_hashedUrlNibbles]; }; QByteArray HTTPProtocol::CacheTag::serialize() const { QByteArray ret; QDataStream stream(&ret, QIODevice::WriteOnly); stream << quint8('A'); stream << quint8('\n'); stream << quint8(0); stream << quint8(0); stream << fileUseCount; stream << servedDate.toMSecsSinceEpoch() / 1000; stream << lastModifiedDate.toMSecsSinceEpoch() / 1000; stream << expireDate.toMSecsSinceEpoch() / 1000; stream << bytesCached; Q_ASSERT(ret.size() == BinaryCacheFileHeader::size); return ret; } static bool compareByte(QDataStream *stream, quint8 value) { quint8 byte; *stream >> byte; return byte == value; } // If starting a new file cacheFileWriteVariableSizeHeader() must have been called *before* // calling this! This is to fill in the headerEnd field. // If the file is not new headerEnd has already been read from the file and in fact the variable // size header *may* not be rewritten because a size change would mess up the file layout. bool HTTPProtocol::CacheTag::deserialize(const QByteArray &d) { if (d.size() != BinaryCacheFileHeader::size) { return false; } QDataStream stream(d); stream.setVersion(QDataStream::Qt_4_5); bool ok = true; ok = ok && compareByte(&stream, 'A'); ok = ok && compareByte(&stream, '\n'); ok = ok && compareByte(&stream, 0); ok = ok && compareByte(&stream, 0); if (!ok) { return false; } stream >> fileUseCount; qint64 servedDateMs; stream >> servedDateMs; servedDate = QDateTime::fromMSecsSinceEpoch(servedDateMs * 1000); qint64 lastModifiedDateMs; stream >> lastModifiedDateMs; lastModifiedDate = QDateTime::fromMSecsSinceEpoch(lastModifiedDateMs * 1000); qint64 expireDateMs; stream >> expireDateMs; expireDate = QDateTime::fromMSecsSinceEpoch(expireDateMs * 1000); stream >> bytesCached; return true; } /* Text part of the header, directly following the binary first part: URL\n etag\n mimetype\n header line\n header line\n ... \n */ static QUrl storableUrl(const QUrl &url) { QUrl ret(url); ret.setPassword(QString()); ret.setFragment(QString()); return ret; } static void writeLine(QIODevice *dev, const QByteArray &line) { static const char linefeed = '\n'; dev->write(line); dev->write(&linefeed, 1); } void HTTPProtocol::cacheFileWriteTextHeader() { QFile *&file = m_request.cacheTag.file; Q_ASSERT(file); Q_ASSERT(file->openMode() & QIODevice::WriteOnly); file->seek(BinaryCacheFileHeader::size); writeLine(file, storableUrl(m_request.url).toEncoded()); writeLine(file, m_request.cacheTag.etag.toLatin1()); writeLine(file, m_mimeType.toLatin1()); writeLine(file, m_responseHeaders.join(QLatin1Char('\n')).toLatin1()); // join("\n") adds no \n to the end, but writeLine() does. // Add another newline to mark the end of text. writeLine(file, QByteArray()); } static bool readLineChecked(QIODevice *dev, QByteArray *line) { *line = dev->readLine(MAX_IPC_SIZE); // if nothing read or the line didn't fit into 8192 bytes(!) if (line->isEmpty() || !line->endsWith('\n')) { return false; } // we don't actually want the newline! line->chop(1); return true; } bool HTTPProtocol::cacheFileReadTextHeader1(const QUrl &desiredUrl) { QFile *&file = m_request.cacheTag.file; Q_ASSERT(file); Q_ASSERT(file->openMode() == QIODevice::ReadOnly); QByteArray readBuf; bool ok = readLineChecked(file, &readBuf); if (storableUrl(desiredUrl).toEncoded() != readBuf) { qCDebug(KIO_HTTP) << "You have witnessed a very improbable hash collision!"; return false; } ok = ok && readLineChecked(file, &readBuf); m_request.cacheTag.etag = toQString(readBuf); return ok; } bool HTTPProtocol::cacheFileReadTextHeader2() { QFile *&file = m_request.cacheTag.file; Q_ASSERT(file); Q_ASSERT(file->openMode() == QIODevice::ReadOnly); bool ok = true; QByteArray readBuf; #ifndef NDEBUG // we assume that the URL and etag have already been read qint64 oldPos = file->pos(); file->seek(BinaryCacheFileHeader::size); ok = ok && readLineChecked(file, &readBuf); ok = ok && readLineChecked(file, &readBuf); Q_ASSERT(file->pos() == oldPos); #endif ok = ok && readLineChecked(file, &readBuf); m_mimeType = toQString(readBuf); m_responseHeaders.clear(); // read as long as no error and no empty line found while (true) { ok = ok && readLineChecked(file, &readBuf); if (ok && !readBuf.isEmpty()) { m_responseHeaders.append(toQString(readBuf)); } else { break; } } return ok; // it may still be false ;) } static QString filenameFromUrl(const QUrl &url) { QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(storableUrl(url).toEncoded()); return toQString(hash.result().toHex()); } QString HTTPProtocol::cacheFilePathFromUrl(const QUrl &url) const { QString filePath = m_strCacheDir; if (!filePath.endsWith(QLatin1Char('/'))) { filePath.append(QLatin1Char('/')); } filePath.append(filenameFromUrl(url)); return filePath; } bool HTTPProtocol::cacheFileOpenRead() { qCDebug(KIO_HTTP); QString filename = cacheFilePathFromUrl(m_request.url); QFile *&file = m_request.cacheTag.file; if (file) { qCDebug(KIO_HTTP) << "File unexpectedly open; old file is" << file->fileName() << "new name is" << filename; Q_ASSERT(file->fileName() == filename); } Q_ASSERT(!file); file = new QFile(filename); if (file->open(QIODevice::ReadOnly)) { QByteArray header = file->read(BinaryCacheFileHeader::size); if (!m_request.cacheTag.deserialize(header)) { qCDebug(KIO_HTTP) << "Cache file header is invalid."; file->close(); } } if (file->isOpen() && !cacheFileReadTextHeader1(m_request.url)) { file->close(); } if (!file->isOpen()) { cacheFileClose(); return false; } return true; } bool HTTPProtocol::cacheFileOpenWrite() { qCDebug(KIO_HTTP); QString filename = cacheFilePathFromUrl(m_request.url); // if we open a cache file for writing while we have a file open for reading we must have // found out that the old cached content is obsolete, so delete the file. QFile *&file = m_request.cacheTag.file; if (file) { // ensure that the file is in a known state - either open for reading or null Q_ASSERT(!qobject_cast(file)); Q_ASSERT((file->openMode() & QIODevice::WriteOnly) == 0); Q_ASSERT(file->fileName() == filename); qCDebug(KIO_HTTP) << "deleting expired cache entry and recreating."; file->remove(); delete file; file = nullptr; } // note that QTemporaryFile will automatically append random chars to filename file = new QTemporaryFile(filename); file->open(QIODevice::WriteOnly); // if we have started a new file we have not initialized some variables from disk data. m_request.cacheTag.fileUseCount = 0; // the file has not been *read* yet m_request.cacheTag.bytesCached = 0; if ((file->openMode() & QIODevice::WriteOnly) == 0) { qCDebug(KIO_HTTP) << "Could not open file for writing: QTemporaryFile(" << filename << ")" << "due to error" << file->error(); cacheFileClose(); return false; } return true; } static QByteArray makeCacheCleanerCommand(const HTTPProtocol::CacheTag &cacheTag, CacheCleanerCommandCode cmd) { QByteArray ret = cacheTag.serialize(); QDataStream stream(&ret, QIODevice::ReadWrite); stream.setVersion(QDataStream::Qt_4_5); stream.skipRawData(BinaryCacheFileHeader::size); // append the command code stream << quint32(cmd); // append the filename QString fileName = cacheTag.file->fileName(); int basenameStart = fileName.lastIndexOf(QLatin1Char('/')) + 1; const QByteArray baseName = fileName.midRef(basenameStart, s_hashedUrlNibbles).toLatin1(); stream.writeRawData(baseName.constData(), baseName.size()); Q_ASSERT(ret.size() == BinaryCacheFileHeader::size + sizeof(quint32) + s_hashedUrlNibbles); return ret; } //### not yet 100% sure when and when not to call this void HTTPProtocol::cacheFileClose() { qCDebug(KIO_HTTP); QFile *&file = m_request.cacheTag.file; if (!file) { return; } m_request.cacheTag.ioMode = NoCache; QByteArray ccCommand; QTemporaryFile *tempFile = qobject_cast(file); if (file->openMode() & QIODevice::WriteOnly) { Q_ASSERT(tempFile); if (m_request.cacheTag.bytesCached && !m_kioError) { QByteArray header = m_request.cacheTag.serialize(); tempFile->seek(0); tempFile->write(header); ccCommand = makeCacheCleanerCommand(m_request.cacheTag, CreateFileNotificationCommand); QString oldName = tempFile->fileName(); QString newName = oldName; int basenameStart = newName.lastIndexOf(QLatin1Char('/')) + 1; // remove the randomized name part added by QTemporaryFile newName.chop(newName.length() - basenameStart - s_hashedUrlNibbles); qCDebug(KIO_HTTP) << "Renaming temporary file" << oldName << "to" << newName; // on windows open files can't be renamed tempFile->setAutoRemove(false); delete tempFile; file = nullptr; if (!QFile::rename(oldName, newName)) { // ### currently this hides a minor bug when force-reloading a resource. We // should not even open a new file for writing in that case. qCDebug(KIO_HTTP) << "Renaming temporary file failed, deleting it instead."; QFile::remove(oldName); ccCommand.clear(); // we have nothing of value to tell the cache cleaner } } else { // oh, we've never written payload data to the cache file. // the temporary file is closed and removed and no proper cache entry is created. } } else if (file->openMode() == QIODevice::ReadOnly) { Q_ASSERT(!tempFile); ccCommand = makeCacheCleanerCommand(m_request.cacheTag, UpdateFileCommand); } delete file; file = nullptr; if (!ccCommand.isEmpty()) { sendCacheCleanerCommand(ccCommand); } } void HTTPProtocol::sendCacheCleanerCommand(const QByteArray &command) { qCDebug(KIO_HTTP); if (!qEnvironmentVariableIsEmpty("KIO_DISABLE_CACHE_CLEANER")) // for autotests return; Q_ASSERT(command.size() == BinaryCacheFileHeader::size + s_hashedUrlNibbles + sizeof(quint32)); if (m_cacheCleanerConnection.state() != QLocalSocket::ConnectedState) { QString socketFileName = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QLatin1Char('/') + QLatin1String("kio_http_cache_cleaner"); m_cacheCleanerConnection.connectToServer(socketFileName, QIODevice::WriteOnly); if (m_cacheCleanerConnection.state() == QLocalSocket::UnconnectedState) { // An error happened. // Most likely the cache cleaner is not running, let's start it. // search paths const QStringList searchPaths = QStringList() << QCoreApplication::applicationDirPath() // then look where our application binary is located << QLibraryInfo::location(QLibraryInfo::LibraryExecutablesPath) // look where libexec path is (can be set in qt.conf) << QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5); // look at our installation location const QString exe = QStandardPaths::findExecutable(QStringLiteral("kio_http_cache_cleaner"), searchPaths); if (exe.isEmpty()) { qCWarning(KIO_HTTP) << "kio_http_cache_cleaner not found in" << searchPaths; return; } qCDebug(KIO_HTTP) << "starting" << exe; QProcess::startDetached(exe, QStringList()); for (int i = 0 ; i < 30 && m_cacheCleanerConnection.state() == QLocalSocket::UnconnectedState; ++i) { // Server is not listening yet; let's hope it does so under 3 seconds QThread::msleep(100); m_cacheCleanerConnection.connectToServer(socketFileName, QIODevice::WriteOnly); if (m_cacheCleanerConnection.state() != QLocalSocket::UnconnectedState) { break; // connecting or connected, sounds good } } } if (!m_cacheCleanerConnection.waitForConnected(1500)) { // updating the stats is not vital, so we just give up. qCDebug(KIO_HTTP) << "Could not connect to cache cleaner, not updating stats of this cache file."; return; } qCDebug(KIO_HTTP) << "Successfully connected to cache cleaner."; } Q_ASSERT(m_cacheCleanerConnection.state() == QLocalSocket::ConnectedState); m_cacheCleanerConnection.write(command); m_cacheCleanerConnection.flush(); } QByteArray HTTPProtocol::cacheFileReadPayload(int maxLength) { Q_ASSERT(m_request.cacheTag.file); Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache); Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly); QByteArray ret = m_request.cacheTag.file->read(maxLength); if (ret.isEmpty()) { cacheFileClose(); } return ret; } void HTTPProtocol::cacheFileWritePayload(const QByteArray &d) { if (!m_request.cacheTag.file) { return; } // If the file being downloaded is so big that it exceeds the max cache size, // do not cache it! See BR# 244215. NOTE: this can be improved upon in the // future... if (m_iSize >= KIO::filesize_t(m_maxCacheSize * 1024)) { qCDebug(KIO_HTTP) << "Caching disabled because content size is too big."; cacheFileClose(); return; } Q_ASSERT(m_request.cacheTag.ioMode == WriteToCache); Q_ASSERT(m_request.cacheTag.file->openMode() & QIODevice::WriteOnly); if (d.isEmpty()) { cacheFileClose(); } //TODO: abort if file grows too big! // write the variable length text header as soon as we start writing to the file if (!m_request.cacheTag.bytesCached) { cacheFileWriteTextHeader(); } m_request.cacheTag.bytesCached += d.size(); m_request.cacheTag.file->write(d); } void HTTPProtocol::cachePostData(const QByteArray &data) { if (!m_POSTbuf) { m_POSTbuf = createPostBufferDeviceFor(qMax(m_iPostDataSize, static_cast(data.size()))); if (!m_POSTbuf) { return; } } m_POSTbuf->write(data.constData(), data.size()); } void HTTPProtocol::clearPostDataBuffer() { if (!m_POSTbuf) { return; } delete m_POSTbuf; m_POSTbuf = nullptr; } bool HTTPProtocol::retrieveAllData() { if (!m_POSTbuf) { m_POSTbuf = createPostBufferDeviceFor(s_MaxInMemPostBufSize + 1); } if (!m_POSTbuf) { error(ERR_OUT_OF_MEMORY, m_request.url.host()); return false; } while (true) { dataReq(); QByteArray buffer; const int bytesRead = readData(buffer); if (bytesRead < 0) { error(ERR_ABORTED, m_request.url.host()); return false; } if (bytesRead == 0) { break; } m_POSTbuf->write(buffer.constData(), buffer.size()); } return true; } // The above code should be kept in sync // with the code in http_cache_cleaner.cpp // !END SYNC! //************************** AUTHENTICATION CODE ********************/ QString HTTPProtocol::authenticationHeader() { QByteArray ret; // If the internal meta-data "cached-www-auth" is set, then check for cached // authentication data and preemptively send the authentication header if a // matching one is found. if (!m_wwwAuth && config()->readEntry("cached-www-auth", false)) { KIO::AuthInfo authinfo; authinfo.url = m_request.url; authinfo.realmValue = config()->readEntry("www-auth-realm", QString()); // If no realm metadata, then make sure path matching is turned on. authinfo.verifyPath = (authinfo.realmValue.isEmpty()); const bool useCachedAuth = (m_request.responseCode == 401 || !config()->readEntry("no-preemptive-auth-reuse", false)); if (useCachedAuth && checkCachedAuthentication(authinfo)) { const QByteArray cachedChallenge = config()->readEntry("www-auth-challenge", QByteArray()); if (!cachedChallenge.isEmpty()) { m_wwwAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config()); if (m_wwwAuth) { qCDebug(KIO_HTTP) << "creating www authentication header from cached info"; m_wwwAuth->setChallenge(cachedChallenge, m_request.url, m_request.sentMethodString); m_wwwAuth->generateResponse(authinfo.username, authinfo.password); } } } } // If the internal meta-data "cached-proxy-auth" is set, then check for cached // authentication data and preemptively send the authentication header if a // matching one is found. if (!m_proxyAuth && config()->readEntry("cached-proxy-auth", false)) { KIO::AuthInfo authinfo; authinfo.url = m_request.proxyUrl; authinfo.realmValue = config()->readEntry("proxy-auth-realm", QString()); // If no realm metadata, then make sure path matching is turned on. authinfo.verifyPath = (authinfo.realmValue.isEmpty()); if (checkCachedAuthentication(authinfo)) { const QByteArray cachedChallenge = config()->readEntry("proxy-auth-challenge", QByteArray()); if (!cachedChallenge.isEmpty()) { m_proxyAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config()); if (m_proxyAuth) { qCDebug(KIO_HTTP) << "creating proxy authentication header from cached info"; m_proxyAuth->setChallenge(cachedChallenge, m_request.proxyUrl, m_request.sentMethodString); m_proxyAuth->generateResponse(authinfo.username, authinfo.password); } } } } // the authentication classes don't know if they are for proxy or webserver authentication... if (m_wwwAuth && !m_wwwAuth->isError()) { ret += "Authorization: " + m_wwwAuth->headerFragment(); } if (m_proxyAuth && !m_proxyAuth->isError()) { ret += "Proxy-Authorization: " + m_proxyAuth->headerFragment(); } return toQString(ret); // ## encoding ok? } static QString protocolForProxyType(QNetworkProxy::ProxyType type) { switch (type) { case QNetworkProxy::DefaultProxy: break; case QNetworkProxy::Socks5Proxy: return QStringLiteral("socks"); case QNetworkProxy::NoProxy: break; case QNetworkProxy::HttpProxy: case QNetworkProxy::HttpCachingProxy: case QNetworkProxy::FtpCachingProxy: break; } return QStringLiteral("http"); } void HTTPProtocol::proxyAuthenticationForSocket(const QNetworkProxy &proxy, QAuthenticator *authenticator) { qCDebug(KIO_HTTP) << "realm:" << authenticator->realm() << "user:" << authenticator->user(); // Set the proxy URL... m_request.proxyUrl.setScheme(protocolForProxyType(proxy.type())); m_request.proxyUrl.setUserName(proxy.user()); m_request.proxyUrl.setHost(proxy.hostName()); m_request.proxyUrl.setPort(proxy.port()); AuthInfo info; info.url = m_request.proxyUrl; info.realmValue = authenticator->realm(); info.username = authenticator->user(); info.verifyPath = info.realmValue.isEmpty(); const bool haveCachedCredentials = checkCachedAuthentication(info); const bool retryAuth = (m_socketProxyAuth != nullptr); // if m_socketProxyAuth is a valid pointer then authentication has been attempted before, // and it was not successful. see below and saveProxyAuthenticationForSocket(). if (!haveCachedCredentials || retryAuth) { // Save authentication info if the connection succeeds. We need to disconnect // this after saving the auth data (or an error) so we won't save garbage afterwards! connect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); //### fillPromptInfo(&info); info.prompt = i18n("You need to supply a username and a password for " "the proxy server listed below before you are allowed " "to access any sites."); info.keepPassword = true; info.commentLabel = i18n("Proxy:"); info.comment = i18n("%1 at %2", info.realmValue.toHtmlEscaped(), m_request.proxyUrl.host()); const QString errMsg((retryAuth ? i18n("Proxy Authentication Failed.") : QString())); const int errorCode = openPasswordDialogV2(info, errMsg); if (errorCode) { qCDebug(KIO_HTTP) << "proxy auth cancelled by user, or communication error"; error(errorCode, QString()); delete m_proxyAuth; m_proxyAuth = nullptr; return; } } authenticator->setUser(info.username); authenticator->setPassword(info.password); authenticator->setOption(QStringLiteral("keepalive"), info.keepPassword); if (m_socketProxyAuth) { *m_socketProxyAuth = *authenticator; } else { m_socketProxyAuth = new QAuthenticator(*authenticator); } if (!m_request.proxyUrl.userName().isEmpty()) { m_request.proxyUrl.setUserName(info.username); } } void HTTPProtocol::saveProxyAuthenticationForSocket() { qCDebug(KIO_HTTP) << "Saving authenticator"; disconnect(socket(), SIGNAL(connected()), this, SLOT(saveProxyAuthenticationForSocket())); Q_ASSERT(m_socketProxyAuth); if (m_socketProxyAuth) { qCDebug(KIO_HTTP) << "realm:" << m_socketProxyAuth->realm() << "user:" << m_socketProxyAuth->user(); KIO::AuthInfo a; a.verifyPath = true; a.url = m_request.proxyUrl; a.realmValue = m_socketProxyAuth->realm(); a.username = m_socketProxyAuth->user(); a.password = m_socketProxyAuth->password(); a.keepPassword = m_socketProxyAuth->option(QStringLiteral("keepalive")).toBool(); cacheAuthentication(a); } delete m_socketProxyAuth; m_socketProxyAuth = nullptr; } void HTTPProtocol::saveAuthenticationData() { KIO::AuthInfo authinfo; bool alreadyCached = false; KAbstractHttpAuthentication *auth = nullptr; switch (m_request.prevResponseCode) { case 401: auth = m_wwwAuth; alreadyCached = config()->readEntry("cached-www-auth", false); break; case 407: auth = m_proxyAuth; alreadyCached = config()->readEntry("cached-proxy-auth", false); break; default: Q_ASSERT(false); // should never happen! } // Prevent recaching of the same credentials over and over again. if (auth && (!auth->realm().isEmpty() || !alreadyCached)) { auth->fillKioAuthInfo(&authinfo); if (auth == m_wwwAuth) { setMetaData(QStringLiteral("{internal~currenthost}cached-www-auth"), QStringLiteral("true")); if (!authinfo.realmValue.isEmpty()) { setMetaData(QStringLiteral("{internal~currenthost}www-auth-realm"), authinfo.realmValue); } if (!authinfo.digestInfo.isEmpty()) { setMetaData(QStringLiteral("{internal~currenthost}www-auth-challenge"), authinfo.digestInfo); } } else { setMetaData(QStringLiteral("{internal~allhosts}cached-proxy-auth"), QStringLiteral("true")); if (!authinfo.realmValue.isEmpty()) { setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-realm"), authinfo.realmValue); } if (!authinfo.digestInfo.isEmpty()) { setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-challenge"), authinfo.digestInfo); } } qCDebug(KIO_HTTP) << "Cache authentication info ?" << authinfo.keepPassword; if (authinfo.keepPassword) { cacheAuthentication(authinfo); qCDebug(KIO_HTTP) << "Cached authentication for" << m_request.url; } } // Update our server connection state which includes www and proxy username and password. m_server.updateCredentials(m_request); } bool HTTPProtocol::handleAuthenticationHeader(const HeaderTokenizer *tokenizer) { KIO::AuthInfo authinfo; QList authTokens; KAbstractHttpAuthentication **auth; QList *blacklistedAuthTokens; TriedCredentials *triedCredentials; if (m_request.responseCode == 401) { auth = &m_wwwAuth; blacklistedAuthTokens = &m_blacklistedWwwAuthMethods; triedCredentials = &m_triedWwwCredentials; authTokens = tokenizer->iterator("www-authenticate").all(); authinfo.url = m_request.url; authinfo.username = m_server.url.userName(); authinfo.prompt = i18n("You need to supply a username and a " "password to access this site."); authinfo.commentLabel = i18n("Site:"); } else { // make sure that the 407 header hasn't escaped a lower layer when it shouldn't. // this may break proxy chains which were never tested anyway, and AFAIK they are // rare to nonexistent in the wild. Q_ASSERT(QNetworkProxy::applicationProxy().type() == QNetworkProxy::NoProxy); auth = &m_proxyAuth; blacklistedAuthTokens = &m_blacklistedProxyAuthMethods; triedCredentials = &m_triedProxyCredentials; authTokens = tokenizer->iterator("proxy-authenticate").all(); authinfo.url = m_request.proxyUrl; authinfo.username = m_request.proxyUrl.userName(); authinfo.prompt = i18n("You need to supply a username and a password for " "the proxy server listed below before you are allowed " "to access any sites."); authinfo.commentLabel = i18n("Proxy:"); } bool authRequiresAnotherRoundtrip = false; // Workaround brain dead server responses that violate the spec and // incorrectly return a 401/407 without the required WWW/Proxy-Authenticate // header fields. See bug 215736... if (!authTokens.isEmpty()) { QString errorMsg; authRequiresAnotherRoundtrip = true; if (m_request.responseCode == m_request.prevResponseCode && *auth) { // Authentication attempt failed. Retry... if ((*auth)->wasFinalStage()) { errorMsg = (m_request.responseCode == 401 ? i18n("Authentication Failed.") : i18n("Proxy Authentication Failed.")); // The authentication failed in its final stage. If the chosen method didn't use a password or // if it failed with both the supplied and prompted password then blacklist this method and try // again with another one if possible. if (!(*auth)->needCredentials() || *triedCredentials > JobCredentials) { QByteArray scheme((*auth)->scheme().trimmed()); qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; blacklistedAuthTokens->append(scheme); } delete *auth; *auth = nullptr; } else { // Create authentication header // WORKAROUND: The following piece of code prevents brain dead IIS // servers that send back multiple "WWW-Authenticate" headers from // screwing up our authentication logic during the challenge // phase (Type 2) of NTLM authentication. QMutableListIterator it(authTokens); const QByteArray authScheme((*auth)->scheme().trimmed()); while (it.hasNext()) { if (qstrnicmp(authScheme.constData(), it.next().constData(), authScheme.length()) != 0) { it.remove(); } } } } QList::iterator it = authTokens.begin(); while (it != authTokens.end()) { QByteArray scheme = *it; // Separate the method name from any additional parameters (for ex. nonce or realm). int index = it->indexOf(' '); if (index > 0) { scheme.truncate(index); } if (blacklistedAuthTokens->contains(scheme)) { it = authTokens.erase(it); } else { it++; } } try_next_auth_scheme: QByteArray bestOffer = KAbstractHttpAuthentication::bestOffer(authTokens); if (*auth) { const QByteArray authScheme((*auth)->scheme().trimmed()); if (qstrnicmp(authScheme.constData(), bestOffer.constData(), authScheme.length()) != 0) { // huh, the strongest authentication scheme offered has changed. delete *auth; *auth = nullptr; } } if (!(*auth)) { *auth = KAbstractHttpAuthentication::newAuth(bestOffer, config()); } if (*auth) { qCDebug(KIO_HTTP) << "Trying authentication scheme:" << (*auth)->scheme(); // remove trailing space from the method string, or digest auth will fail (*auth)->setChallenge(bestOffer, authinfo.url, m_request.sentMethodString); QString username, password; bool generateAuthHeader = true; if ((*auth)->needCredentials()) { // use credentials supplied by the application if available if (!m_request.url.userName().isEmpty() && !m_request.url.password().isEmpty() && *triedCredentials == NoCredentials) { username = m_request.url.userName(); password = m_request.url.password(); // don't try this password any more *triedCredentials = JobCredentials; } else { // try to get credentials from kpasswdserver's cache, then try asking the user. authinfo.verifyPath = false; // we have realm, no path based checking please! authinfo.realmValue = (*auth)->realm(); if (authinfo.realmValue.isEmpty() && !(*auth)->supportsPathMatching()) { authinfo.realmValue = QLatin1String((*auth)->scheme()); } // Save the current authinfo url because it can be modified by the call to // checkCachedAuthentication. That way we can restore it if the call // modified it. const QUrl reqUrl = authinfo.url; if (!errorMsg.isEmpty() || !checkCachedAuthentication(authinfo)) { // Reset url to the saved url... authinfo.url = reqUrl; authinfo.keepPassword = true; authinfo.comment = i18n("%1 at %2", authinfo.realmValue.toHtmlEscaped(), authinfo.url.host()); const int errorCode = openPasswordDialogV2(authinfo, errorMsg); if (errorCode) { generateAuthHeader = false; authRequiresAnotherRoundtrip = false; if (!sendErrorPageNotification()) { error(ERR_ACCESS_DENIED, reqUrl.host()); } qCDebug(KIO_HTTP) << "looks like the user canceled the authentication dialog"; delete *auth; *auth = nullptr; } *triedCredentials = UserInputCredentials; } else { *triedCredentials = CachedCredentials; } username = authinfo.username; password = authinfo.password; } } if (generateAuthHeader) { (*auth)->generateResponse(username, password); (*auth)->setCachePasswordEnabled(authinfo.keepPassword); qCDebug(KIO_HTTP) << "isError=" << (*auth)->isError() << "needCredentials=" << (*auth)->needCredentials() << "forceKeepAlive=" << (*auth)->forceKeepAlive() << "forceDisconnect=" << (*auth)->forceDisconnect(); if ((*auth)->isError()) { QByteArray scheme((*auth)->scheme().trimmed()); qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; authTokens.removeOne(scheme); blacklistedAuthTokens->append(scheme); if (!authTokens.isEmpty()) { goto try_next_auth_scheme; } else { if (!sendErrorPageNotification()) { error(ERR_UNSUPPORTED_ACTION, i18n("Authorization failed.")); } authRequiresAnotherRoundtrip = false; } //### return false; ? } else if ((*auth)->forceKeepAlive()) { //### think this through for proxied / not proxied m_request.isKeepAlive = true; } else if ((*auth)->forceDisconnect()) { //### think this through for proxied / not proxied m_request.isKeepAlive = false; httpCloseConnection(); } } } else { authRequiresAnotherRoundtrip = false; if (!sendErrorPageNotification()) { error(ERR_UNSUPPORTED_ACTION, i18n("Unknown Authorization method.")); } } } return authRequiresAnotherRoundtrip; } void HTTPProtocol::copyPut(const QUrl& src, const QUrl& dest, JobFlags flags) { qCDebug(KIO_HTTP) << src << "->" << dest; if (!maybeSetRequestUrl(dest)) { return; } resetSessionSettings(); if (!(flags & KIO::Overwrite)) { // check to make sure this host supports WebDAV if (!davHostOk()) { return; } // Checks if the destination exists and return an error if it does. if (!davStatDestination()) { return; } } m_POSTbuf = new QFile (src.toLocalFile()); if (!m_POSTbuf->open(QFile::ReadOnly)) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, QString()); return; } m_request.method = HTTP_PUT; m_request.cacheTag.policy = CC_Reload; proceedUntilResponseContent(); } bool HTTPProtocol::davStatDestination() { const QByteArray request ("" "" "" "" "" "" ""); davSetRequest(request); // WebDAV Stat or List... m_request.method = DAV_PROPFIND; m_request.url.setQuery(QString()); m_request.cacheTag.policy = CC_Reload; m_request.davData.depth = 0; proceedUntilResponseContent(true); if (!m_request.isKeepAlive) { httpCloseConnection(); // close connection if server requested it. m_request.isKeepAlive = true; // reset the keep alive flag. } if (m_request.responseCode == 207) { error(ERR_FILE_ALREADY_EXIST, QString()); return false; } // force re-authentication... delete m_wwwAuth; m_wwwAuth = nullptr; return true; } void HTTPProtocol::fileSystemFreeSpace(const QUrl &url) { qCDebug(KIO_HTTP) << url; if (!maybeSetRequestUrl(url)) { return; } resetSessionSettings(); davStatList(url); } void HTTPProtocol::virtual_hook(int id, void *data) { switch(id) { case SlaveBase::GetFileSystemFreeSpace: { QUrl *url = static_cast(data); fileSystemFreeSpace(*url); } break; default: SlaveBase::virtual_hook(id, data); } } // needed for JSON file embedding #include "http.moc" diff --git a/src/ioslaves/http/kcookiejar/kcookiejar.cpp b/src/ioslaves/http/kcookiejar/kcookiejar.cpp index 327f5b8e..8cd9ef8d 100644 --- a/src/ioslaves/http/kcookiejar/kcookiejar.cpp +++ b/src/ioslaves/http/kcookiejar/kcookiejar.cpp @@ -1,1612 +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 #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 + QStringLiteral("$Version=") + QString::number(protVersion) + QStringLiteral("; "); } 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", "# Host", "Domain", "Path", "Exp.date", "Prot", "Name", "Sec", "Value"); ts << s.toLatin1().constData(); QStringListIterator it(m_domainList); while (it.hasNext()) { const QString &domain = it.next(); bool domainPrinted = false; KHttpCookieList *cookieList = m_cookieDomains.value(domain); if (!cookieList) { continue; } QMutableListIterator cookieIterator(*cookieList); while (cookieIterator.hasNext()) { const KHttpCookie &cookie = cookieIterator.next(); if (cookie.isExpired()) { // Delete expired cookies cookieIterator.remove(); continue; } if (cookieIsPersistent(cookie)) { // Only save cookies that are not "session-only cookies" if (!domainPrinted) { domainPrinted = true; ts << '[' << domain.toLocal8Bit().data() << "]\n"; } // Store persistent cookies const QString path = QL1C('"') + cookie.path() + QL1C('"'); const QString domain = QL1C('"') + cookie.domain() + QL1C('"'); const QString host = hostWithPort(&cookie); // TODO: replace with direct QTextStream output ? s.sprintf("%-20s %-20s %-12s %10lld %3d %-20s %-4i %s\n", host.toLatin1().constData(), domain.toLatin1().constData(), path.toLatin1().constData(), cookie.expireDate(), cookie.protocolVersion(), cookie.name().isEmpty() ? cookie.value().toLatin1().constData() : cookie.name().toLatin1().constData(), (cookie.isSecure() ? 1 : 0) + (cookie.isHttpOnly() ? 2 : 0) + (cookie.hasExplicitPath() ? 4 : 0) + (cookie.name().isEmpty() ? 8 : 0), cookie.value().toLatin1().constData()); ts << s.toLatin1().constData(); } } } if (cookieFile.commit()) { QFile::setPermissions(_filename, QFile::ReadUser | QFile::WriteUser); return true; } return false; } static const char *parseField(char *&buffer, bool keepQuotes = false) { char *result; if (!keepQuotes && (*buffer == '\"')) { // Find terminating " buffer++; result = buffer; while ((*buffer != '\"') && (*buffer)) { buffer++; } } else { // Find first white space result = buffer; while ((*buffer != ' ') && (*buffer != '\t') && (*buffer != '\n') && (*buffer)) { buffer++; } } if (!*buffer) { return result; // } *buffer++ = '\0'; // Skip white-space while ((*buffer == ' ') || (*buffer == '\t') || (*buffer == '\n')) { buffer++; } return result; } static QString extractHostAndPorts(const QString &str, QList *ports = nullptr) { if (str.isEmpty()) { return str; } const int index = str.indexOf(QL1C(':')); if (index == -1) { return str; } const QString host = str.left(index); if (ports) { bool ok; QStringList portList = str.mid(index + 1).split(QL1C(',')); Q_FOREACH (const QString &portStr, portList) { const int portNum = portStr.toInt(&ok); if (ok) { ports->append(portNum); } } } return host; } // // Reloads all cookies from the file '_filename'. // On success 'true' is returned. // On failure 'false' is returned. bool KCookieJar::loadCookies(const QString &_filename) { QFile cookieFile(_filename); if (!cookieFile.open(QIODevice::ReadOnly)) { return false; } int version = 1; bool success = false; char *buffer = new char[READ_BUFFER_SIZE]; qint64 len = cookieFile.readLine(buffer, READ_BUFFER_SIZE - 1); if (len != -1) { if (qstrcmp(buffer, "# KDE Cookie File\n") == 0) { success = true; } else if (qstrcmp(buffer, "# KDE Cookie File v") > 0) { bool ok = false; const int verNum = QByteArray(buffer + 19, len - 19).trimmed().toInt(&ok); if (ok) { version = verNum; success = true; } } } if (success) { const qint64 currentTime = epoch(); QList ports; while (cookieFile.readLine(buffer, READ_BUFFER_SIZE - 1) != -1) { char *line = buffer; // Skip lines which begin with '#' or '[' if ((line[0] == '#') || (line[0] == '[')) { continue; } const QString host = extractHostAndPorts(QL1S(parseField(line)), &ports); const QString domain = QL1S(parseField(line)); if (host.isEmpty() && domain.isEmpty()) { continue; } const QString path = QL1S(parseField(line)); const QString expStr = QL1S(parseField(line)); if (expStr.isEmpty()) { continue; } const qint64 expDate = expStr.toLongLong(); const QString verStr = QL1S(parseField(line)); if (verStr.isEmpty()) { continue; } int protVer = verStr.toInt(); QString name = QL1S(parseField(line)); bool keepQuotes = false; bool secure = false; bool httpOnly = false; bool explicitPath = false; const char *value = nullptr; if ((version == 2) || (protVer >= 200)) { if (protVer >= 200) { protVer -= 200; } int i = atoi(parseField(line)); secure = i & 1; httpOnly = i & 2; explicitPath = i & 4; if (i & 8) { name = QLatin1String(""); } line[strlen(line) - 1] = '\0'; // Strip LF. value = line; } else { if (protVer >= 100) { protVer -= 100; keepQuotes = true; } value = parseField(line, keepQuotes); secure = QByteArray(parseField(line)).toShort(); } // Expired or parse error if (!value || expDate == 0 || expDate < currentTime) { continue; } KHttpCookie cookie(host, domain, path, name, QString::fromUtf8(value), expDate, protVer, secure, httpOnly, explicitPath); if (!ports.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/parsinghelpers.cpp b/src/ioslaves/http/parsinghelpers.cpp index 3d7d3ffc..def0ce14 100644 --- a/src/ioslaves/http/parsinghelpers.cpp +++ b/src/ioslaves/http/parsinghelpers.cpp @@ -1,601 +1,600 @@ /* This file is part of the KDE libraries Copyright (C) 2008 Andreas Hartmetz Copyright (C) 2010,2011 Rolf Eike Beer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include -#include #include // Advance *pos beyond spaces / tabs static void skipSpace(const char input[], int *pos, int end) { int idx = *pos; while (idx < end && (input[idx] == ' ' || input[idx] == '\t')) { idx++; } *pos = idx; return; } // Advance *pos to start of next line while being forgiving about line endings. // Return false if the end of the header has been reached, true otherwise. static bool nextLine(const char input[], int *pos, int end) { int idx = *pos; while (idx < end && input[idx] != '\r' && input[idx] != '\n') { idx++; } int rCount = 0; int nCount = 0; while (idx < end && qMax(rCount, nCount) < 2 && (input[idx] == '\r' || input[idx] == '\n')) { input[idx] == '\r' ? rCount++ : nCount++; idx++; } if (idx < end && qMax(rCount, nCount) == 2 && qMin(rCount, nCount) == 1) { // if just one of the others is missing eat it too. // this ensures that conforming headers using the proper // \r\n sequence (and also \n\r) will be parsed correctly. if ((rCount == 1 && input[idx] == '\r') || (nCount == 1 && input[idx] == '\n')) { idx++; } } *pos = idx; return idx < end && rCount < 2 && nCount < 2; } // QByteArray::fromPercentEncoding() does not notify us about encoding errors so we need // to check here if this is valid at all. static bool isValidPercentEncoding(const QByteArray &data) { int i = 0; const int last = data.length() - 1; const char *d = data.constData(); while ((i = data.indexOf('%', i)) != -1) { if (i >= last - 2) { return false; } if (! isxdigit(d[i + 1])) { return false; } if (! isxdigit(d[i + 2])) { return false; } i++; } return true; } QByteArray TokenIterator::next() { QPair token = m_tokens[m_currentToken++]; //fromRawData brings some speed advantage but also the requirement to keep the text buffer //around. this together with implicit sharing (you don't know where copies end up) //is dangerous! //return QByteArray::fromRawData(&m_buffer[token.first], token.second - token.first); return QByteArray(&m_buffer[token.first], token.second - token.first); } QByteArray TokenIterator::current() const { QPair token = m_tokens[m_currentToken - 1]; //return QByteArray::fromRawData(&m_buffer[token.first], token.second - token.first); return QByteArray(&m_buffer[token.first], token.second - token.first); } QList TokenIterator::all() const { QList ret; ret.reserve(m_tokens.count()); for (int i = 0; i < m_tokens.count(); i++) { QPair token = m_tokens[i]; ret.append(QByteArray(&m_buffer[token.first], token.second - token.first)); } return ret; } HeaderTokenizer::HeaderTokenizer(char *buffer) : m_buffer(buffer) { // add information about available headers and whether they have one or multiple, // comma-separated values. //The following response header fields are from RFC 2616 unless otherwise specified. //Hint: search the web for e.g. 'http "accept-ranges header"' to find information about //a header field. static const HeaderFieldTemplate headerFieldTemplates[] = { {"accept-ranges", false}, {"age", false}, {"cache-control", true}, {"connection", true}, {"content-disposition", false}, //is multi-valued in a way, but with ";" separator! {"content-encoding", true}, {"content-language", true}, {"content-length", false}, {"content-location", false}, {"content-md5", false}, {"content-type", false}, {"date", false}, {"dav", true}, //RFC 2518 {"etag", false}, {"expires", false}, {"keep-alive", true}, //RFC 2068 {"last-modified", false}, {"link", false}, //RFC 2068, multi-valued with ";" separator {"location", false}, {"p3p", true}, // http://www.w3.org/TR/P3P/ {"pragma", true}, {"proxy-authenticate", false}, //complicated multi-valuedness: quoted commas don't separate //multiple values. we handle this at a higher level. {"proxy-connection", true}, //inofficial but well-known; to avoid misunderstandings //when using "connection" when talking to a proxy. {"refresh", false}, //not sure, only found some mailing list posts mentioning it {"set-cookie", false}, //RFC 2109; the multi-valuedness seems to be usually achieved //by sending several instances of this field as opposed to //usually comma-separated lists with maybe multiple instances. {"transfer-encoding", true}, {"upgrade", true}, {"warning", true}, {"www-authenticate", false} //see proxy-authenticate }; for (uint i = 0; i < sizeof(headerFieldTemplates) / sizeof(HeaderFieldTemplate); i++) { const HeaderFieldTemplate &ft = headerFieldTemplates[i]; insert(QByteArray(ft.name), HeaderField(ft.isMultiValued)); } } int HeaderTokenizer::tokenize(int begin, int end) { char *buf = m_buffer; //keep line length in check :/ int idx = begin; int startIdx = begin; //multi-purpose start of current token bool multiValuedEndedWithComma = false; //did the last multi-valued line end with a comma? QByteArray headerKey; do { if (buf[idx] == ' ' || buf [idx] == '\t') { // line continuation; preserve startIdx except (see below) if (headerKey.isEmpty()) { continue; } // turn CR/LF into spaces for later parsing convenience int backIdx = idx - 1; while (backIdx >= begin && (buf[backIdx] == '\r' || buf[backIdx] == '\n')) { buf[backIdx--] = ' '; } // multiple values, comma-separated: add new value or continue previous? if (operator[](headerKey).isMultiValued) { if (multiValuedEndedWithComma) { // start new value; this is almost like no line continuation skipSpace(buf, &idx, end); startIdx = idx; } else { // continue previous value; this is tricky. unit tests to the rescue! if (operator[](headerKey).beginEnd.last().first == startIdx) { // remove entry, it will be re-added because already idx != startIdx operator[](headerKey).beginEnd.removeLast(); } else { // no comma, no entry: the prev line was whitespace only - start new value skipSpace(buf, &idx, end); startIdx = idx; } } } } else { // new field startIdx = idx; // also make sure that there is at least one char after the colon while (idx < (end - 1) && buf[idx] != ':' && buf[idx] != '\r' && buf[idx] != '\n') { buf[idx] = tolower(buf[idx]); idx++; } if (buf[idx] != ':') { //malformed line: no colon headerKey.clear(); continue; } headerKey = QByteArray(&buf[startIdx], idx - startIdx); if (!contains(headerKey)) { //we don't recognize this header line headerKey.clear(); continue; } // skip colon & leading whitespace idx++; skipSpace(buf, &idx, end); startIdx = idx; } // we have the name/key of the field, now parse the value if (!operator[](headerKey).isMultiValued) { // scan to end of line while (idx < end && buf[idx] != '\r' && buf[idx] != '\n') { idx++; } if (!operator[](headerKey).beginEnd.isEmpty()) { // there already is an entry; are we just in a line continuation? if (operator[](headerKey).beginEnd.last().first == startIdx) { // line continuation: delete previous entry and later insert a new, longer one. operator[](headerKey).beginEnd.removeLast(); } } operator[](headerKey).beginEnd.append(QPair(startIdx, idx)); } else { // comma-separated list while (true) { //skip one value while (idx < end && buf[idx] != '\r' && buf[idx] != '\n' && buf[idx] != ',') { idx++; } if (idx != startIdx) { operator[](headerKey).beginEnd.append(QPair(startIdx, idx)); } multiValuedEndedWithComma = buf[idx] == ','; //skip comma(s) and leading whitespace, if any respectively while (idx < end && buf[idx] == ',') { idx++; } skipSpace(buf, &idx, end); //next value or end-of-line / end of header? if (buf[idx] >= end || buf[idx] == '\r' || buf[idx] == '\n') { break; } //next value startIdx = idx; } } } while (nextLine(buf, &idx, end)); return idx; } TokenIterator HeaderTokenizer::iterator(const char *key) const { QByteArray keyBa = QByteArray::fromRawData(key, strlen(key)); if (contains(keyBa)) { return TokenIterator(value(keyBa).beginEnd, m_buffer); } else { return TokenIterator(m_nullTokens, m_buffer); } } static void skipLWS(const QString &str, int &pos) { while (pos < str.length() && (str[pos] == QLatin1Char(' ') || str[pos] == QLatin1Char('\t'))) { ++pos; } } // keep the common ending, this allows the compiler to join them static const char typeSpecials[] = "{}*'%()<>@,;:\\\"/[]?="; static const char attrSpecials[] = "'%()<>@,;:\\\"/[]?="; static const char valueSpecials[] = "()<>@,;:\\\"/[]?="; static bool specialChar(const QChar &ch, const char *specials) { // WORKAROUND: According to RFC 2616, any character other than ascii // characters should NOT be allowed in unquoted content-disposition file // names. However, since none of the major browsers follow this rule, we do // the same thing here and allow all printable unicode characters. See // https://bugs.kde.org/show_bug.cgi?id=261223 for the details. if (!ch.isPrint()) { return true; } for (int i = qstrlen(specials) - 1; i >= 0; i--) { if (ch == QLatin1Char(specials[i])) { return true; } } return false; } /** * read and parse the input until the given terminator * @param str input string to parse * @param term terminator * @param pos position marker in the input string * @param specials characters forbidden in this section * @return the next section or an empty string if it was invalid * * Extracts token-like input until terminator char or EOL. * Also skips over the terminator. * * pos is correctly incremented even if this functions returns * an empty string so this can be used to skip over invalid * parts and continue. */ static QString extractUntil(const QString &str, QChar term, int &pos, const char *specials) { QString out; skipLWS(str, pos); bool valid = true; while (pos < str.length() && (str[pos] != term)) { out += str[pos]; valid = (valid && !specialChar(str[pos], specials)); ++pos; } if (pos < str.length()) { // Stopped due to finding term ++pos; } if (!valid) { return QString(); } // Remove trailing linear whitespace... while (out.endsWith(QLatin1Char(' ')) || out.endsWith(QLatin1Char('\t'))) { out.chop(1); } if (out.contains(QLatin1Char(' '))) { out.clear(); } return out; } // As above, but also handles quotes.. // pos is set to -1 on parse error static QString extractMaybeQuotedUntil(const QString &str, int &pos) { const QChar term = QLatin1Char(';'); skipLWS(str, pos); // Are we quoted? if (pos < str.length() && str[pos] == QLatin1Char('"')) { QString out; // Skip the quote... ++pos; // when quoted we also need an end-quote bool endquote = false; // Parse until trailing quote... while (pos < str.length()) { if (str[pos] == QLatin1Char('\\') && pos + 1 < str.length()) { // quoted-pair = "\" CHAR out += str[pos + 1]; pos += 2; // Skip both... } else if (str[pos] == QLatin1Char('"')) { ++pos; endquote = true; break; } else if (!str[pos].isPrint()) { // Don't allow CTL's RFC 2616 sec 2.2 break; } else { out += str[pos]; ++pos; } } if (!endquote) { pos = -1; return QString(); } // Skip until term.. while (pos < str.length() && (str[pos] != term)) { if ((str[pos] != QLatin1Char(' ')) && (str[pos] != QLatin1Char('\t'))) { pos = -1; return QString(); } ++pos; } if (pos < str.length()) { // Stopped due to finding term ++pos; } return out; } else { return extractUntil(str, term, pos, valueSpecials); } } static QMap contentDispositionParserInternal(const QString &disposition) { // qDebug() << "disposition: " << disposition; int pos = 0; const QString strDisposition = extractUntil(disposition, QLatin1Char(';'), pos, typeSpecials).toLower(); QMap parameters; QMap contparams; // all parameters that contain continuations QMap encparams; // all parameters that have character encoding // the type is invalid, the complete header is junk if (strDisposition.isEmpty()) { return parameters; } parameters.insert(QStringLiteral("type"), strDisposition); while (pos < disposition.length()) { QString key = extractUntil(disposition, QLatin1Char('='), pos, attrSpecials).toLower(); if (key.isEmpty()) { // parse error in this key: do not parse more, but add up // everything we already got // qDebug() << "parse error in key, abort parsing"; break; } QString val; if (key.endsWith(QLatin1Char('*'))) { val = extractUntil(disposition, QLatin1Char(';'), pos, valueSpecials); } else { val = extractMaybeQuotedUntil(disposition, pos); } if (val.isEmpty()) { if (pos == -1) { // qDebug() << "parse error in value, abort parsing"; break; } continue; } const int spos = key.indexOf(QLatin1Char('*')); if (spos == key.length() - 1) { key.chop(1); encparams.insert(key, val); } else if (spos >= 0) { contparams.insert(key, val); } else if (parameters.contains(key)) { // qDebug() << "duplicate key" << key << "found, ignoring everything more"; parameters.remove(key); return parameters; } else { parameters.insert(key, val); } } QMap::iterator i = contparams.begin(); while (i != contparams.end()) { QString key = i.key(); int spos = key.indexOf(QLatin1Char('*')); bool hasencoding = false; if (key.at(spos + 1) != QLatin1Char('0')) { ++i; continue; } // no leading zeros allowed, so delete the junk int klen = key.length(); if (klen > spos + 2) { // nothing but continuations and encodings may insert * into parameter name if ((klen > spos + 3) || ((klen == spos + 3) && (key.at(spos + 2) != QLatin1Char('*')))) { // qDebug() << "removing invalid key " << key << "with val" << i.value() << key.at(spos + 2); i = contparams.erase(i); continue; } hasencoding = true; } int seqnum = 1; QMap::iterator partsi; // we do not need to care about encoding specifications: only the first // part is allowed to have one QString val = i.value(); key.chop(hasencoding ? 2 : 1); while ((partsi = contparams.find(key + QString::number(seqnum))) != contparams.end()) { val += partsi.value(); contparams.erase(partsi); } i = contparams.erase(i); key.chop(1); if (hasencoding) { encparams.insert(key, val); } else { if (parameters.contains(key)) { // qDebug() << "duplicate key" << key << "found, ignoring everything more"; parameters.remove(key); return parameters; } parameters.insert(key, val); } } for (QMap::iterator i = encparams.begin(); i != encparams.end(); ++i) { QString val = i.value(); // RfC 2231 encoded character set in filename int spos = val.indexOf(QLatin1Char('\'')); if (spos == -1) { continue; } int npos = val.indexOf(QLatin1Char('\''), spos + 1); if (npos == -1) { continue; } const QStringRef charset = val.leftRef(spos); const QByteArray encodedVal = val.midRef(npos + 1).toLatin1(); if (! isValidPercentEncoding(encodedVal)) { continue; } const QByteArray rawval = QByteArray::fromPercentEncoding(encodedVal); if (charset.isEmpty() || (charset == QLatin1String("us-ascii"))) { bool valid = true; for (int j = rawval.length() - 1; (j >= 0) && valid; j--) { valid = (rawval.at(j) >= 32); } if (!valid) { continue; } val = QString::fromLatin1(rawval.constData()); } else { QTextCodec *codec = QTextCodec::codecForName(charset.toLatin1()); if (!codec) { continue; } val = codec->toUnicode(rawval); } parameters.insert(i.key(), val); } return parameters; } static QMap contentDispositionParser(const QString &disposition) { QMap parameters = contentDispositionParserInternal(disposition); const QLatin1String fn("filename"); if (parameters.contains(fn)) { // Content-Disposition is not allowed to dictate directory // path, thus we extract the filename only. const QString val = QDir::toNativeSeparators(parameters[fn]); int slpos = val.lastIndexOf(QDir::separator()); if (slpos > -1) { parameters.insert(fn, val.mid(slpos + 1)); } } return parameters; } diff --git a/src/ioslaves/trash/discspaceutil.cpp b/src/ioslaves/trash/discspaceutil.cpp index 4800c882..7fc5cd90 100644 --- a/src/ioslaves/trash/discspaceutil.cpp +++ b/src/ioslaves/trash/discspaceutil.cpp @@ -1,96 +1,95 @@ /* This file is part of the KDE project Copyright (C) 2008 Tobias Koenig 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 "discspaceutil.h" #include "kiotrashdebug.h" #include #include #include -#include #include // QT_LSTAT, QT_STAT, QT_STATBUF DiscSpaceUtil::DiscSpaceUtil(const QString &directory) : mDirectory(directory), mFullSize(0) { calculateFullSize(); } qulonglong DiscSpaceUtil::sizeOfPath(const QString &path) { QFileInfo info(path); if (!info.exists()) { return 0; } if (info.isSymLink()) { // QFileInfo::size does not return the actual size of a symlink. #253776 QT_STATBUF buff; return static_cast(QT_LSTAT(QFile::encodeName(path).constData(), &buff) == 0 ? buff.st_size : 0); } else if (info.isFile()) { return info.size(); } else if (info.isDir()) { QDirIterator it(path, QDirIterator::NoIteratorFlags); qulonglong sum = 0; while (it.hasNext()) { const QFileInfo info = it.next(); if (info.fileName() != QLatin1String(".") && info.fileName() != QLatin1String("..")) { sum += sizeOfPath(info.absoluteFilePath()); } } return sum; } else { return 0; } } double DiscSpaceUtil::usage(qulonglong size) const { if (mFullSize == 0) { return 0; } return (((double)size * 100) / (double)mFullSize); } qulonglong DiscSpaceUtil::size() const { return mFullSize; } QString DiscSpaceUtil::mountPoint() const { return mMountPoint; } void DiscSpaceUtil::calculateFullSize() { KDiskFreeSpaceInfo info = KDiskFreeSpaceInfo::freeSpaceInfo(mDirectory); if (info.isValid()) { mFullSize = info.size(); mMountPoint = info.mountPoint(); } } diff --git a/src/ioslaves/trash/kcmtrash.cpp b/src/ioslaves/trash/kcmtrash.cpp index ca9e3dea..b62ee44a 100644 --- a/src/ioslaves/trash/kcmtrash.cpp +++ b/src/ioslaves/trash/kcmtrash.cpp @@ -1,316 +1,314 @@ /*************************************************************************** * Copyright (C) 2008 by Tobias Koenig * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kcmtrash.h" #include "discspaceutil.h" #include "trashimpl.h" #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include -#include K_PLUGIN_FACTORY(KCMTrashConfigFactory, registerPlugin(QStringLiteral("trash"));) K_EXPORT_PLUGIN(KCMTrashConfigFactory("kcmtrash")) TrashConfigModule::TrashConfigModule(QWidget *parent, const QVariantList &) : KCModule( //KCMTrashConfigFactory::componentData(), parent), trashInitialize(false) { mTrashImpl = new TrashImpl(); mTrashImpl->init(); readConfig(); setupGui(); useTypeChanged(); connect(mUseTimeLimit, &QAbstractButton::toggled, this, QOverload<>::of(&TrashConfigModule::changed)); connect(mUseTimeLimit, &QAbstractButton::toggled, this, &TrashConfigModule::useTypeChanged); connect(mDays, QOverload::of(&QSpinBox::valueChanged), this, QOverload<>::of(&TrashConfigModule::changed)); connect(mUseSizeLimit, &QAbstractButton::toggled, this, QOverload<>::of(&TrashConfigModule::changed)); connect(mUseSizeLimit, &QAbstractButton::toggled, this, &TrashConfigModule::useTypeChanged); connect(mPercent, QOverload::of(&QDoubleSpinBox::valueChanged), this, &TrashConfigModule::percentChanged); connect(mPercent, QOverload::of(&QDoubleSpinBox::valueChanged), this, QOverload<>::of(&TrashConfigModule::changed)); connect(mLimitReachedAction, QOverload::of(&QComboBox::currentIndexChanged), this, QOverload<>::of(&TrashConfigModule::changed)); trashChanged(0); trashInitialize = true; } TrashConfigModule::~TrashConfigModule() { } void TrashConfigModule::save() { if (!mCurrentTrash.isEmpty()) { ConfigEntry entry; entry.useTimeLimit = mUseTimeLimit->isChecked(); entry.days = mDays->value(); entry.useSizeLimit = mUseSizeLimit->isChecked(); entry.percent = mPercent->value(), entry.actionType = mLimitReachedAction->currentIndex(); mConfigMap.insert(mCurrentTrash, entry); } writeConfig(); } void TrashConfigModule::defaults() { ConfigEntry entry; entry.useTimeLimit = false; entry.days = 7; entry.useSizeLimit = true; entry.percent = 10.0; entry.actionType = 0; mConfigMap.insert(mCurrentTrash, entry); trashInitialize = false; trashChanged(0); } void TrashConfigModule::percentChanged(double percent) { DiscSpaceUtil util(mCurrentTrash); qulonglong partitionSize = util.size(); double size = ((double)(partitionSize / 100)) * percent; KFormat format; mSizeLabel->setText(QLatin1Char('(') + format.formatByteSize(size, 2) + QLatin1Char(')')); } void TrashConfigModule::trashChanged(QListWidgetItem *item) { trashChanged(item->data(Qt::UserRole).toInt()); } void TrashConfigModule::trashChanged(int value) { const TrashImpl::TrashDirMap map = mTrashImpl->trashDirectories(); if (!mCurrentTrash.isEmpty() && trashInitialize) { ConfigEntry entry; entry.useTimeLimit = mUseTimeLimit->isChecked(); entry.days = mDays->value(); entry.useSizeLimit = mUseSizeLimit->isChecked(); entry.percent = mPercent->value(), entry.actionType = mLimitReachedAction->currentIndex(); mConfigMap.insert(mCurrentTrash, entry); } mCurrentTrash = map[ value ]; const auto currentTrashIt = mConfigMap.constFind(mCurrentTrash); if (currentTrashIt != mConfigMap.constEnd()) { const ConfigEntry &entry = *currentTrashIt; mUseTimeLimit->setChecked(entry.useTimeLimit); mDays->setValue(entry.days); mUseSizeLimit->setChecked(entry.useSizeLimit); mPercent->setValue(entry.percent); mLimitReachedAction->setCurrentIndex(entry.actionType); } else { mUseTimeLimit->setChecked(false); mDays->setValue(7); mUseSizeLimit->setChecked(true); mPercent->setValue(10.0); mLimitReachedAction->setCurrentIndex(0); } mDays->setSuffix(i18n(" days")); // missing in Qt: plural form handling percentChanged(mPercent->value()); } void TrashConfigModule::useTypeChanged() { mDays->setEnabled(mUseTimeLimit->isChecked()); mPercent->setEnabled(mUseSizeLimit->isChecked()); mSizeLabel->setEnabled(mUseSizeLimit->isChecked()); } void TrashConfigModule::readConfig() { KConfig config(QStringLiteral("ktrashrc")); mConfigMap.clear(); const QStringList groups = config.groupList(); for (int i = 0; i < groups.count(); ++i) { if (groups[ i ].startsWith(QLatin1Char('/'))) { const KConfigGroup group = config.group(groups[ i ]); ConfigEntry entry; entry.useTimeLimit = group.readEntry("UseTimeLimit", false); entry.days = group.readEntry("Days", 7); entry.useSizeLimit = group.readEntry("UseSizeLimit", true); entry.percent = group.readEntry("Percent", 10.0); entry.actionType = group.readEntry("LimitReachedAction", 0); mConfigMap.insert(groups[ i ], entry); } } } void TrashConfigModule::writeConfig() { KConfig config(QStringLiteral("ktrashrc")); // first delete all existing groups const QStringList groups = config.groupList(); for (int i = 0; i < groups.count(); ++i) if (groups[ i ].startsWith(QLatin1Char('/'))) { config.deleteGroup(groups[ i ]); } QMapIterator it(mConfigMap); while (it.hasNext()) { it.next(); KConfigGroup group = config.group(it.key()); group.writeEntry("UseTimeLimit", it.value().useTimeLimit); group.writeEntry("Days", it.value().days); group.writeEntry("UseSizeLimit", it.value().useSizeLimit); group.writeEntry("Percent", it.value().percent); group.writeEntry("LimitReachedAction", it.value().actionType); } config.sync(); } void TrashConfigModule::setupGui() { QVBoxLayout *layout = new QVBoxLayout(this); #ifdef Q_OS_OSX QLabel *infoText = new QLabel( i18n( "KDE's wastebin is configured to use the Finder's Trash.
" ) ); infoText->setWhatsThis( i18nc( "@info:whatsthis", "Emptying KDE's wastebin will remove only KDE's trash items, while
" "emptying the Trash through the Finder will delete everything.
" "KDE's trash items will show up in a folder called KDE.trash, in the Trash can." ) ); layout->addWidget( infoText ); #endif TrashImpl::TrashDirMap map = mTrashImpl->trashDirectories(); if (map.count() != 1) { // If we have multiple trashes, we setup a widget to choose // which trash to configure QListWidget *mountPoints = new QListWidget(this); layout->addWidget(mountPoints); QMapIterator it(map); while (it.hasNext()) { it.next(); DiscSpaceUtil util(it.value()); QListWidgetItem *item = new QListWidgetItem(QIcon(QStringLiteral("folder")), util.mountPoint()); item->setData(Qt::UserRole, it.key()); mountPoints->addItem(item); } mountPoints->setCurrentRow(0); connect(mountPoints, &QListWidget::currentItemChanged, this, QOverload::of(&TrashConfigModule::trashChanged)); } else { mCurrentTrash = map.value(0); } QFormLayout* formLayout = new QFormLayout(); layout->addLayout(formLayout); QHBoxLayout *daysLayout = new QHBoxLayout(); mUseTimeLimit = new QCheckBox(i18n("Delete files older than"), this); mUseTimeLimit->setWhatsThis(xi18nc("@info:whatsthis", "Check this box to allow automatic deletion of files that are older than the value specified. " "Leave this disabled to not automatically delete any items after a certain timespan")); daysLayout->addWidget(mUseTimeLimit); mDays = new QSpinBox(this); mDays->setRange(1, 365); mDays->setSingleStep(1); mDays->setSuffix(i18np(" day", " days", mDays->value())); mDays->setWhatsThis(xi18nc("@info:whatsthis", "Set the number of days that files can remain in the trash. " "Any files older than this will be automatically deleted.")); daysLayout->addWidget(mDays); daysLayout->addStretch(); formLayout->addRow(i18n("Cleanup:"), daysLayout); QHBoxLayout *maximumSizeLayout = new QHBoxLayout(); mUseSizeLimit = new QCheckBox(i18n("Limit to"), this); mUseSizeLimit->setWhatsThis(xi18nc("@info:whatsthis", "Check this box to limit the trash to the maximum amount of disk space that you specify below. " "Otherwise, it will be unlimited.")); maximumSizeLayout->addWidget(mUseSizeLimit); formLayout->addRow(i18n("Size:"), maximumSizeLayout); mPercent = new QDoubleSpinBox(this); mPercent->setRange(0.001, 100); mPercent->setDecimals(3); mPercent->setSingleStep(1); mPercent->setSuffix(QStringLiteral(" %")); mPercent->setWhatsThis(xi18nc("@info:whatsthis", "This is the maximum percent of disk space that will be used for the trash.")); maximumSizeLayout->addWidget(mPercent); mSizeLabel = new QLabel(this); mSizeLabel->setWhatsThis(xi18nc("@info:whatsthis", "This is the calculated amount of disk space that will be allowed for the trash, the maximum.")); maximumSizeLayout->addWidget(mSizeLabel); mLimitReachedAction = new QComboBox(); mLimitReachedAction->addItem(i18n("Show a Warning")); mLimitReachedAction->addItem(i18n("Delete Oldest Files From Trash")); mLimitReachedAction->addItem(i18n("Delete Biggest Files From Trash")); mLimitReachedAction->setWhatsThis(xi18nc("@info:whatsthis", "When the size limit is reached, it will prefer to delete the type of files that you specify, first. " "If this is set to warn you, it will do so instead of automatically deleting files.")); formLayout->addRow(i18n("Full Trash:"), mLimitReachedAction); layout->addStretch(); } #include "kcmtrash.moc" diff --git a/src/ioslaves/trash/kinterprocesslock.cpp b/src/ioslaves/trash/kinterprocesslock.cpp index 9d8f02f4..5a193b2a 100644 --- a/src/ioslaves/trash/kinterprocesslock.cpp +++ b/src/ioslaves/trash/kinterprocesslock.cpp @@ -1,93 +1,92 @@ /* This file is part of the KDE Copyright (C) 2009 Tobias Koenig (tokoe@kde.org) This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this library; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kinterprocesslock.h" #include "kiotrashdebug.h" #include #include -#include class KInterProcessLockPrivate { Q_DECLARE_PUBLIC(KInterProcessLock) KInterProcessLock * const q_ptr; public: KInterProcessLockPrivate(const QString &resource, KInterProcessLock *q) : q_ptr(q) , m_resource(resource) { m_serviceName = QStringLiteral("org.kde.private.lock-%1").arg(m_resource); q_ptr->connect(QDBusConnection::sessionBus().interface(), SIGNAL(serviceRegistered(QString)), q_ptr, SLOT(_k_serviceRegistered(QString))); } ~KInterProcessLockPrivate() { } void _k_serviceRegistered(const QString &service) { if (service == m_serviceName) { emit q_ptr->lockGranted(q_ptr); } } QString m_resource; QString m_serviceName; }; KInterProcessLock::KInterProcessLock(const QString &resource) : d_ptr(new KInterProcessLockPrivate(resource, this)) { } KInterProcessLock::~KInterProcessLock() { delete d_ptr; } QString KInterProcessLock::resource() const { return d_ptr->m_resource; } void KInterProcessLock::lock() { QDBusConnection::sessionBus().interface()->registerService(d_ptr->m_serviceName, QDBusConnectionInterface::QueueService, QDBusConnectionInterface::DontAllowReplacement); } void KInterProcessLock::unlock() { QDBusConnection::sessionBus().interface()->unregisterService(d_ptr->m_serviceName); } void KInterProcessLock::waitForLockGranted() { QEventLoop loop; connect(this, &KInterProcessLock::lockGranted, &loop, &QEventLoop::quit); loop.exec(); } #include "moc_kinterprocesslock.cpp" diff --git a/src/ioslaves/trash/kio_trash.cpp b/src/ioslaves/trash/kio_trash.cpp index 3d57cc5e..ad54ba8f 100644 --- a/src/ioslaves/trash/kio_trash.cpp +++ b/src/ioslaves/trash/kio_trash.cpp @@ -1,671 +1,670 @@ /* This file is part of the KDE project Copyright (C) 2004 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kio_trash.h" #include "kiotrashdebug.h" #include "../../pathhelpers_p.h" #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include #include // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.trash" FILE "trash.json") }; extern "C" { int Q_DECL_EXPORT kdemain(int argc, char **argv) { // necessary to use other kio slaves QCoreApplication app(argc, argv); KIO::setDefaultJobUiDelegateExtension(nullptr); // start the slave TrashProtocol slave(argv[1], argv[2], argv[3]); slave.dispatchLoop(); return 0; } } static bool isTopLevelEntry(const QUrl &url) { const QString dir = url.adjusted(QUrl::RemoveFilename).path(); return dir.length() <= 1; } #define INIT_IMPL \ if ( !impl.init() ) { \ error( impl.lastErrorCode(), impl.lastErrorMessage() ); \ return; \ } TrashProtocol::TrashProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app) : SlaveBase(protocol, pool, app) { struct passwd *user = getpwuid(getuid()); if (user) { m_userName = QString::fromLatin1(user->pw_name); } struct group *grp = getgrgid(getgid()); if (grp) { m_groupName = QString::fromLatin1(grp->gr_name); } } TrashProtocol::~TrashProtocol() { } void TrashProtocol::enterLoop() { QEventLoop eventLoop; connect(this, &TrashProtocol::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } void TrashProtocol::restore(const QUrl &trashURL) { int trashId; QString fileId, relativePath; bool ok = TrashImpl::parseURL(trashURL, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", trashURL.toString())); return; } TrashedFileInfo info; ok = impl.infoForFile(trashId, fileId, info); if (!ok) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } QUrl dest = QUrl::fromLocalFile(info.origPath); if (!relativePath.isEmpty()) { dest.setPath(concatPaths(dest.path(), relativePath)); } // Check that the destination directory exists, to improve the error code in case it doesn't. const QString destDir = dest.adjusted(QUrl::RemoveFilename).path(); QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(destDir).constData(), &buff) == -1) { error(KIO::ERR_SLAVE_DEFINED, i18n("The directory %1 does not exist anymore, so it is not possible to restore this item to its original location. " "You can either recreate that directory and use the restore operation again, or drag the item anywhere else to restore it.", destDir)); return; } copyOrMove(trashURL, dest, false /*overwrite*/, Move); } void TrashProtocol::rename(const QUrl &oldURL, const QUrl &newURL, KIO::JobFlags flags) { INIT_IMPL; qCDebug(KIO_TRASH) << "TrashProtocol::rename(): old=" << oldURL << " new=" << newURL << " overwrite=" << (flags & KIO::Overwrite); if (oldURL.scheme() == QLatin1String("trash") && newURL.scheme() == QLatin1String("trash")) { if (!isTopLevelEntry(oldURL) || !isTopLevelEntry(newURL)) { error(KIO::ERR_CANNOT_RENAME, oldURL.toString()); return; } int oldTrashId; QString oldFileId, oldRelativePath; bool oldOk = TrashImpl::parseURL(oldURL, oldTrashId, oldFileId, oldRelativePath); if (!oldOk) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", oldURL.toString())); return; } if (!oldRelativePath.isEmpty()) { error(KIO::ERR_CANNOT_RENAME, oldURL.toString()); return; } // Dolphin/KIO can't specify a trashid in the new URL so here path == filename //bool newOk = TrashImpl::parseURL(newURL, newTrashId, newFileId, newRelativePath); const QString newFileId = newURL.path().mid(1); if (newFileId.contains(QLatin1Char('/'))) { error(KIO::ERR_CANNOT_RENAME, oldURL.toString()); return; } bool ok = impl.moveInTrash(oldTrashId, oldFileId, newFileId); if (!ok) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } const QUrl finalUrl = TrashImpl::makeURL(oldTrashId, newFileId, QString()); org::kde::KDirNotify::emitFileRenamed(oldURL, finalUrl); finished(); return; } copyOrMove(oldURL, newURL, (flags & KIO::Overwrite), Move); } void TrashProtocol::copy(const QUrl &src, const QUrl &dest, int /*permissions*/, KIO::JobFlags flags) { INIT_IMPL; qCDebug(KIO_TRASH) << "TrashProtocol::copy(): " << src << " " << dest; if (src.scheme() == QLatin1String("trash") && dest.scheme() == QLatin1String("trash")) { error(KIO::ERR_UNSUPPORTED_ACTION, i18n("This file is already in the trash bin.")); return; } copyOrMove(src, dest, (flags & KIO::Overwrite), Copy); } void TrashProtocol::copyOrMove(const QUrl &src, const QUrl &dest, bool overwrite, CopyOrMove action) { if (src.scheme() == QLatin1String("trash") && dest.isLocalFile()) { // Extracting (e.g. via dnd). Ignore original location stored in info file. int trashId; QString fileId, relativePath; bool ok = TrashImpl::parseURL(src, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", src.toString())); return; } const QString destPath = dest.path(); if (QFile::exists(destPath)) { if (overwrite) { ok = QFile::remove(destPath); Q_ASSERT(ok); // ### TODO } else { error(KIO::ERR_FILE_ALREADY_EXIST, destPath); return; } } if (action == Move) { qCDebug(KIO_TRASH) << "calling moveFromTrash(" << destPath << " " << trashId << " " << fileId << ")"; ok = impl.moveFromTrash(destPath, trashId, fileId, relativePath); } else { // Copy qCDebug(KIO_TRASH) << "calling copyFromTrash(" << destPath << " " << trashId << " " << fileId << ")"; ok = impl.copyFromTrash(destPath, trashId, fileId, relativePath); } if (!ok) { error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { if (action == Move && relativePath.isEmpty()) { (void)impl.deleteInfo(trashId, fileId); } finished(); } return; } else if (src.isLocalFile() && dest.scheme() == QLatin1String("trash")) { qCDebug(KIO_TRASH) << "trashing a file" << src << dest; // Trashing a file // We detect the case where this isn't normal trashing, but // e.g. if kwrite tries to save (moving tempfile over destination) if (isTopLevelEntry(dest) && src.fileName() == dest.fileName()) { // new toplevel entry const QString srcPath = src.path(); // In theory we should use TrashImpl::parseURL to give the right filename to createInfo, // in case the trash URL didn't contain the same filename as srcPath. // But this can only happen with copyAs/moveAs, not available in the GUI // for the trash (New/... or Rename from iconview/listview). int trashId; QString fileId; if (!impl.createInfo(srcPath, trashId, fileId)) { error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { bool ok; if (action == Move) { qCDebug(KIO_TRASH) << "calling moveToTrash(" << srcPath << " " << trashId << " " << fileId << ")"; ok = impl.moveToTrash(srcPath, trashId, fileId); } else { // Copy qCDebug(KIO_TRASH) << "calling copyToTrash(" << srcPath << " " << trashId << " " << fileId << ")"; ok = impl.copyToTrash(srcPath, trashId, fileId); } if (!ok) { (void)impl.deleteInfo(trashId, fileId); error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { // Inform caller of the final URL. Used by konq_undo. const QUrl url = impl.makeURL(trashId, fileId, QString()); setMetaData(QLatin1String("trashURL-") + srcPath, url.url()); finished(); } } return; } else { qCDebug(KIO_TRASH) << "returning KIO::ERR_ACCESS_DENIED, it's not allowed to add a file to an existing trash directory"; // It's not allowed to add a file to an existing trash directory. error(KIO::ERR_ACCESS_DENIED, dest.toString()); return; } } else { error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Internal error in copyOrMove, should never happen")); } } void TrashProtocol::createTopLevelDirEntry(KIO::UDSEntry &entry) { entry.clear(); entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700); entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); entry.fastInsert(KIO::UDSEntry::UDS_USER, m_userName); entry.fastInsert(KIO::UDSEntry::UDS_GROUP, m_groupName); } void TrashProtocol::stat(const QUrl &url) { INIT_IMPL; const QString path = url.path(); if (path.isEmpty() || path == QLatin1String("/")) { // The root is "virtual" - it's not a single physical directory KIO::UDSEntry entry; createTopLevelDirEntry(entry); statEntry(entry); finished(); } else { int trashId; QString fileId, relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); if (!ok) { // ######## do we still need this? qCDebug(KIO_TRASH) << url << " looks fishy, returning does-not-exist"; // A URL like trash:/file simply means that CopyJob is trying to see if // the destination exists already (it made up the URL by itself). error(KIO::ERR_DOES_NOT_EXIST, url.toString()); //error( KIO::ERR_SLAVE_DEFINED, i18n( "Malformed URL %1" ).arg( url.toString() ) ); return; } qCDebug(KIO_TRASH) << "parsed" << url << "got" << trashId << fileId << relativePath; const QString filePath = impl.physicalPath(trashId, fileId, relativePath); if (filePath.isEmpty()) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } // For a toplevel file, use the fileId as display name (to hide the trashId) // For a file in a subdir, use the fileName as is. QString fileDisplayName = relativePath.isEmpty() ? fileId : url.fileName(); QUrl fileURL; if (url.path().length() > 1) { fileURL = url; } KIO::UDSEntry entry; TrashedFileInfo info; ok = impl.infoForFile(trashId, fileId, info); if (ok) { ok = createUDSEntry(filePath, fileDisplayName, fileURL.fileName(), entry, info); } if (!ok) { error(KIO::ERR_COULD_NOT_STAT, url.toString()); return; } statEntry(entry); finished(); } } void TrashProtocol::del(const QUrl &url, bool /*isfile*/) { INIT_IMPL; int trashId; QString fileId, relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", url.toString())); return; } ok = relativePath.isEmpty(); if (!ok) { error(KIO::ERR_ACCESS_DENIED, url.toString()); return; } ok = impl.del(trashId, fileId); if (!ok) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } finished(); } void TrashProtocol::listDir(const QUrl &url) { INIT_IMPL; qCDebug(KIO_TRASH) << "listdir: " << url; const QString path = url.path(); if (path.isEmpty() || path == QLatin1String("/")) { listRoot(); return; } int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", url.toString())); return; } //was: const QString physicalPath = impl.physicalPath( trashId, fileId, relativePath ); // Get info for deleted directory - the date of deletion and orig path will be used // for all the items in it, and we need the physicalPath. TrashedFileInfo info; ok = impl.infoForFile(trashId, fileId, info); if (!ok || info.physicalPath.isEmpty()) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } if (!relativePath.isEmpty()) { info.physicalPath += QLatin1Char('/') + relativePath; } // List subdir. Can't use kio_file here since we provide our own info... qCDebug(KIO_TRASH) << "listing " << info.physicalPath; const QStringList entryNames = impl.listDir(info.physicalPath); totalSize(entryNames.count()); KIO::UDSEntry entry; for (QStringList::const_iterator entryIt = entryNames.begin(), entryEnd = entryNames.end(); entryIt != entryEnd; ++entryIt) { const QString fileName = *entryIt; if (fileName == QLatin1String("..")) { continue; } const QString filePath = info.physicalPath + QLatin1Char('/') + fileName; // shouldn't be necessary //const QString url = TrashImpl::makeURL( trashId, fileId, relativePath + '/' + fileName ); entry.clear(); TrashedFileInfo infoForItem(info); infoForItem.origPath += QLatin1Char('/') + fileName; if (createUDSEntry(filePath, fileName, fileName, entry, infoForItem)) { listEntry(entry); } } entry.clear(); finished(); } bool TrashProtocol::createUDSEntry(const QString &physicalPath, const QString &displayFileName, const QString &internalFileName, KIO::UDSEntry &entry, const TrashedFileInfo &info) { QByteArray physicalPath_c = QFile::encodeName(physicalPath); QT_STATBUF buff; if (QT_LSTAT(physicalPath_c.constData(), &buff) == -1) { qCWarning(KIO_TRASH) << "couldn't stat " << physicalPath; return false; } if (S_ISLNK(buff.st_mode)) { char buffer2[ 1000 ]; int n = ::readlink(physicalPath_c.constData(), buffer2, 999); if (n != -1) { buffer2[ n ] = 0; } entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, QFile::decodeName(buffer2)); // Follow symlink // That makes sense in kio_file, but not in the trash, especially for the size // #136876 #if 0 if (KDE_stat(physicalPath_c, &buff) == -1) { // It is a link pointing to nowhere buff.st_mode = S_IFLNK | S_IRWXU | S_IRWXG | S_IRWXO; buff.st_mtime = 0; buff.st_atime = 0; buff.st_size = 0; } #endif } mode_t type = buff.st_mode & S_IFMT; // extract file type mode_t access = buff.st_mode & 07777; // extract permissions access &= 07555; // make it readonly, since it's in the trashcan Q_ASSERT(!internalFileName.isEmpty()); entry.fastInsert(KIO::UDSEntry::UDS_NAME, internalFileName); // internal filename, like "0-foo" entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, displayFileName); // user-visible filename, like "foo" entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, type); //if ( !url.isEmpty() ) // entry.insert( KIO::UDSEntry::UDS_URL, url ); QMimeDatabase db; QMimeType mt = db.mimeTypeForFile(physicalPath); if (mt.isValid()) { entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, mt.name()); } entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, access); entry.fastInsert(KIO::UDSEntry::UDS_SIZE, buff.st_size); entry.fastInsert(KIO::UDSEntry::UDS_USER, m_userName); // assumption entry.fastInsert(KIO::UDSEntry::UDS_GROUP, m_groupName); // assumption entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, buff.st_mtime); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, buff.st_atime); // ## or use it for deletion time? entry.fastInsert(KIO::UDSEntry::UDS_EXTRA, info.origPath); entry.fastInsert(KIO::UDSEntry::UDS_EXTRA + 1, info.deletionDate.toString(Qt::ISODate)); return true; } void TrashProtocol::listRoot() { INIT_IMPL; const TrashedFileInfoList lst = impl.list(); totalSize(lst.count()); KIO::UDSEntry entry; createTopLevelDirEntry(entry); listEntry(entry); for (TrashedFileInfoList::ConstIterator it = lst.begin(); it != lst.end(); ++it) { const QUrl url = TrashImpl::makeURL((*it).trashId, (*it).fileId, QString()); entry.clear(); const QString fileDisplayName = (*it).fileId; if (createUDSEntry((*it).physicalPath, fileDisplayName, url.fileName(), entry, *it)) { listEntry(entry); } } entry.clear(); finished(); } void TrashProtocol::special(const QByteArray &data) { INIT_IMPL; QDataStream stream(data); int cmd; stream >> cmd; switch (cmd) { case 1: if (impl.emptyTrash()) { finished(); } else { error(impl.lastErrorCode(), impl.lastErrorMessage()); } break; case 2: impl.migrateOldTrash(); finished(); break; case 3: { QUrl url; stream >> url; restore(url); break; } default: qCWarning(KIO_TRASH) << "Unknown command in special(): " << cmd; error(KIO::ERR_UNSUPPORTED_ACTION, QString::number(cmd)); break; } } void TrashProtocol::put(const QUrl &url, int /*permissions*/, KIO::JobFlags) { INIT_IMPL; qCDebug(KIO_TRASH) << "put: " << url; // create deleted file. We need to get the mtime and original location from metadata... // Maybe we can find the info file for url.fileName(), in case ::rename() was called first, and failed... error(KIO::ERR_ACCESS_DENIED, url.toString()); } void TrashProtocol::get(const QUrl &url) { INIT_IMPL; qCDebug(KIO_TRASH) << "get() : " << url; if (!url.isValid()) { //qCDebug(KIO_TRASH) << kBacktrace(); error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", url.url())); return; } if (url.path().length() <= 1) { error(KIO::ERR_IS_DIRECTORY, url.toString()); return; } int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); if (!ok) { error(KIO::ERR_SLAVE_DEFINED, i18n("Malformed URL %1", url.toString())); return; } const QString physicalPath = impl.physicalPath(trashId, fileId, relativePath); if (physicalPath.isEmpty()) { error(impl.lastErrorCode(), impl.lastErrorMessage()); return; } // Usually we run jobs in TrashImpl (for e.g. future kdedmodule) // But for this one we wouldn't use DCOP for every bit of data... QUrl fileURL = QUrl::fromLocalFile(physicalPath); KIO::TransferJob *job = KIO::get(fileURL, KIO::NoReload, KIO::HideProgressInfo); connect(job, &KIO::TransferJob::data, this, &TrashProtocol::slotData); connect(job, QOverload::of(&KIO::TransferJob::mimetype), this, &TrashProtocol::slotMimetype); connect(job, &KJob::result, this, &TrashProtocol::jobFinished); enterLoop(); } void TrashProtocol::slotData(KIO::Job *, const QByteArray &arr) { data(arr); } void TrashProtocol::slotMimetype(KIO::Job *, const QString &mt) { mimeType(mt); } void TrashProtocol::jobFinished(KJob *job) { if (job->error()) { error(job->error(), job->errorText()); } else { finished(); } emit leaveModality(); } #if 0 void TrashProtocol::mkdir(const QUrl &url, int /*permissions*/) { INIT_IMPL; // create info about deleted dir // ############ Problem: we don't know the original path. // Let's try to avoid this case (we should get to copy() instead, for local files) qCDebug(KIO_TRASH) << "mkdir: " << url; QString dir = url.adjusted(QUrl::RemoveFilename).path(); if (dir.length() <= 1) { // new toplevel entry // ## we should use TrashImpl::parseURL to give the right filename to createInfo int trashId; QString fileId; if (!impl.createInfo(url.path(), trashId, fileId)) { error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { if (!impl.mkdir(trashId, fileId, permissions)) { (void)impl.deleteInfo(trashId, fileId); error(impl.lastErrorCode(), impl.lastErrorMessage()); } else { finished(); } } } else { // Well it's not allowed to add a directory to an existing deleted directory. error(KIO::ERR_ACCESS_DENIED, url.toString()); } } #endif void TrashProtocol::virtual_hook(int id, void *data) { switch(id) { case SlaveBase::GetFileSystemFreeSpace: { QUrl *url = static_cast(data); fileSystemFreeSpace(*url); } break; default: SlaveBase::virtual_hook(id, data); } } void TrashProtocol::fileSystemFreeSpace(const QUrl &url) { qCDebug(KIO_TRASH) << "fileSystemFreeSpace:" << url; INIT_IMPL; TrashImpl::TrashSpaceInfo spaceInfo; if (!impl.trashSpaceInfo(url.path(), spaceInfo)) { error(KIO::ERR_COULD_NOT_STAT, url.toDisplayString()); return; } setMetaData(QStringLiteral("total"), QString::number(spaceInfo.totalSize)); setMetaData(QStringLiteral("available"), QString::number(spaceInfo.availableSize)); finished(); } #include "kio_trash.moc" diff --git a/src/ioslaves/trash/trashsizecache.cpp b/src/ioslaves/trash/trashsizecache.cpp index 05df213b..6df80d40 100644 --- a/src/ioslaves/trash/trashsizecache.cpp +++ b/src/ioslaves/trash/trashsizecache.cpp @@ -1,159 +1,158 @@ /* This file is part of the KDE project Copyright (C) 2009 Tobias Koenig Copyright (C) 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 "trashsizecache.h" #include "discspaceutil.h" #include "kiotrashdebug.h" #include // QT_LSTAT, QT_STAT, QT_STATBUF #include #include #include #include #include -#include TrashSizeCache::TrashSizeCache(const QString &path) : mTrashSizeCachePath(path + QLatin1String("/directorysizes")), mTrashPath(path) { //qCDebug(KIO_TRASH) << "CACHE:" << mTrashSizeCachePath; } void TrashSizeCache::add(const QString &directoryName, qulonglong directorySize) { //qCDebug(KIO_TRASH) << directoryName << directorySize; const QByteArray encodedDir = QFile::encodeName(directoryName).toPercentEncoding(); const QByteArray spaceAndDirAndNewline = ' ' + encodedDir + '\n'; QFile file(mTrashSizeCachePath); QSaveFile out(mTrashSizeCachePath); if (out.open(QIODevice::WriteOnly)) { if (file.open(QIODevice::ReadOnly)) { while (!file.atEnd()) { const QByteArray line = file.readLine(); if (line.endsWith(spaceAndDirAndNewline)) { // Already there! out.cancelWriting(); //qCDebug(KIO_TRASH) << "already there!"; return; } out.write(line); } } const QString fileInfoPath = mTrashPath + QLatin1String("/info/") + directoryName + QLatin1String(".trashinfo"); QDateTime mtime = QFileInfo(fileInfoPath).lastModified(); QByteArray newLine = QByteArray::number(directorySize) + ' ' + QByteArray::number(mtime.toMSecsSinceEpoch()) + spaceAndDirAndNewline; out.write(newLine); out.commit(); } //qCDebug(KIO_TRASH) << mTrashSizeCachePath << "exists:" << QFile::exists(mTrashSizeCachePath); } void TrashSizeCache::remove(const QString &directoryName) { //qCDebug(KIO_TRASH) << directoryName; const QByteArray encodedDir = QFile::encodeName(directoryName).toPercentEncoding(); const QByteArray spaceAndDirAndNewline = ' ' + encodedDir + '\n'; QFile file(mTrashSizeCachePath); QSaveFile out(mTrashSizeCachePath); if (file.open(QIODevice::ReadOnly) && out.open(QIODevice::WriteOnly)) { while (!file.atEnd()) { const QByteArray line = file.readLine(); if (line.endsWith(spaceAndDirAndNewline)) { // Found it -> skip it continue; } out.write(line); } } out.commit(); } void TrashSizeCache::clear() { QFile::remove(mTrashSizeCachePath); } struct CacheData { qulonglong size; qint64 mtime; }; qulonglong TrashSizeCache::calculateSize() { // First read the directorysizes cache into memory QFile file(mTrashSizeCachePath); typedef QHash DirCacheHash; DirCacheHash dirCache; if (file.open(QIODevice::ReadOnly)) { while (!file.atEnd()) { const QByteArray line = file.readLine(); const int firstSpace = line.indexOf(' '); const int secondSpace = line.indexOf(' ', firstSpace + 1); CacheData data; data.size = line.left(firstSpace).toULongLong(); // "012 4567 name" -> firstSpace=3, secondSpace=8, we want mid(4,4) data.mtime = line.mid(firstSpace + 1, secondSpace - firstSpace - 1).toLongLong(); dirCache.insert(line.mid(secondSpace + 1), data); } } // Iterate over the actual trashed files. // Orphan items (no .fileinfo) still take space. QDirIterator it(mTrashPath + QLatin1String("/files/"), QDirIterator::NoIteratorFlags); qulonglong sum = 0; while (it.hasNext()) { const QFileInfo file = it.next(); if (file.fileName() == QLatin1String(".") || file.fileName() == QLatin1String("..")) { continue; } if (file.isSymLink()) { // QFileInfo::size does not return the actual size of a symlink. #253776 QT_STATBUF buff; return static_cast(QT_LSTAT(QFile::encodeName(file.absoluteFilePath()).constData(), &buff) == 0 ? buff.st_size : 0); } else if (file.isFile()) { sum += file.size(); } else { bool usableCache = false; const QString fileId = file.fileName(); DirCacheHash::const_iterator it = dirCache.constFind(QFile::encodeName(fileId)); if (it != dirCache.constEnd()) { const CacheData &data = *it; const QString fileInfoPath = mTrashPath + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"); if (QFileInfo(fileInfoPath).lastModified().toMSecsSinceEpoch() == data.mtime) { sum += data.size; usableCache = true; } } if (!usableCache) { const qulonglong size = DiscSpaceUtil::sizeOfPath(file.absoluteFilePath()); sum += size; add(fileId, size); } } } return sum; } diff --git a/src/kcms/kio/kcookiesmanagement.cpp b/src/kcms/kio/kcookiesmanagement.cpp index 1861030c..332b516a 100644 --- a/src/kcms/kio/kcookiesmanagement.cpp +++ b/src/kcms/kio/kcookiesmanagement.cpp @@ -1,428 +1,423 @@ /** * 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 #include // KDE #include -#include #include #include -#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); 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/kcookiespolicyselectiondlg.cpp b/src/kcms/kio/kcookiespolicyselectiondlg.cpp index 766bd6d1..bab5a553 100644 --- a/src/kcms/kio/kcookiespolicyselectiondlg.cpp +++ b/src/kcms/kio/kcookiespolicyselectiondlg.cpp @@ -1,139 +1,138 @@ /** * Copyright (c) 2000- 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 "kcookiespolicyselectiondlg.h" // Qt #include #include #include #include // KDE #include #include -#include #include class DomainNameValidator : public QValidator { Q_OBJECT public: DomainNameValidator (QObject* parent) :QValidator(parent) { setObjectName(QStringLiteral("domainValidator")); } State validate (QString& input, int&) const override { if (input.isEmpty() || (input == QLatin1String("."))) { return Intermediate; } 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; } }; KCookiesPolicySelectionDlg::KCookiesPolicySelectionDlg (QWidget* parent, Qt::WindowFlags flags) : QDialog (parent, flags) , mOldPolicy(-1) , mButtonBox(nullptr) { QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->addWidget(mainWidget); mUi.setupUi(mainWidget); mUi.leDomain->setValidator(new DomainNameValidator (mUi.leDomain)); mUi.cbPolicy->setMinimumWidth(mUi.cbPolicy->fontMetrics().maxWidth() * 15); 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); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(mUi.leDomain, &QLineEdit::textEdited, this, &KCookiesPolicySelectionDlg::slotTextChanged); connect(mUi.cbPolicy, QOverload::of(&QComboBox::currentIndexChanged), this, &KCookiesPolicySelectionDlg::slotPolicyChanged); mUi.leDomain->setFocus(); } void KCookiesPolicySelectionDlg::setEnableHostEdit (bool state, const QString& host) { if (!host.isEmpty()) { mUi.leDomain->setText (host); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(state); } mUi.leDomain->setEnabled (state); } void KCookiesPolicySelectionDlg::setPolicy (int policy) { if (policy > -1 && policy <= static_cast (mUi.cbPolicy->count())) { const bool blocked = mUi.cbPolicy->blockSignals(true); mUi.cbPolicy->setCurrentIndex (policy - 1); mUi.cbPolicy->blockSignals(blocked); mOldPolicy = policy; } if (!mUi.leDomain->isEnabled()) mUi.cbPolicy->setFocus(); } int KCookiesPolicySelectionDlg::advice () const { return mUi.cbPolicy->currentIndex() + 1; } QString KCookiesPolicySelectionDlg::domain () const { return mUi.leDomain->text(); } void KCookiesPolicySelectionDlg::slotTextChanged (const QString& text) { mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (text.length() > 1); } void KCookiesPolicySelectionDlg::slotPolicyChanged(const QString& policyText) { const int policy = KCookieAdvice::strToAdvice(policyText); if (!mUi.leDomain->isEnabled()) { mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(policy != mOldPolicy); } else { slotTextChanged(policyText); } } #include "kcookiespolicyselectiondlg.moc" diff --git a/src/kcms/kio/main.cpp b/src/kcms/kio/main.cpp index 26cef61a..73b14ae8 100644 --- a/src/kcms/kio/main.cpp +++ b/src/kcms/kio/main.cpp @@ -1,46 +1,45 @@ // (c) Torben Weis 1998 // (c) David Faure 1998 /* * main.cpp for creating the konqueror kio kcm modules * * Copyright (C) 2000,2001,2009 Alexander Neundorf * * 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. */ // Qt -#include // KDE #include // Local #include "kcookiesmain.h" #include "netpref.h" #include "smbrodlg.h" #include "useragentdlg.h" #include "kproxydlg.h" #include "cache.h" K_PLUGIN_FACTORY(KioConfigFactory, registerPlugin(QStringLiteral("useragent")); registerPlugin(QStringLiteral("smb")); registerPlugin(QStringLiteral("netpref")); registerPlugin(QStringLiteral("proxy")); registerPlugin(QStringLiteral("cookie")); registerPlugin(QStringLiteral("cache")); ) #include "main.moc" diff --git a/src/kcms/kio/netpref.cpp b/src/kcms/kio/netpref.cpp index 40fb82b4..de22b977 100644 --- a/src/kcms/kio/netpref.cpp +++ b/src/kcms/kio/netpref.cpp @@ -1,181 +1,180 @@ // Own #include "netpref.h" // Qt #include #include #include // KDE #include #include #include -#include #include #include #include // Local #include "ksaveioconfig.h" #define MAX_TIMEOUT_VALUE 3600 K_PLUGIN_FACTORY_DECLARATION(KioConfigFactory) KIOPreferences::KIOPreferences(QWidget *parent, const QVariantList &) :KCModule(parent) { QVBoxLayout* mainLayout = new QVBoxLayout( this ); mainLayout->setContentsMargins(0, 0, 0, 0); gb_Timeout = new QGroupBox( i18n("Timeout Values"), this ); gb_Timeout->setWhatsThis( i18np("Here you can set timeout values. " "You might want to tweak them if your " "connection is very slow. The maximum " "allowed value is 1 second." , "Here you can set timeout values. " "You might want to tweak them if your " "connection is very slow. The maximum " "allowed value is %1 seconds.", MAX_TIMEOUT_VALUE)); mainLayout->addWidget( gb_Timeout ); QFormLayout* timeoutLayout = new QFormLayout(gb_Timeout); sb_socketRead = new KPluralHandlingSpinBox( this ); sb_socketRead->setSuffix( ki18np( " second", " seconds" ) ); connect(sb_socketRead, QOverload::of(&QSpinBox::valueChanged), this, &KIOPreferences::configChanged); timeoutLayout->addRow(i18n( "Soc&ket read:" ), sb_socketRead); sb_proxyConnect = new KPluralHandlingSpinBox( this ); sb_proxyConnect->setValue(0); sb_proxyConnect->setSuffix( ki18np( " second", " seconds" ) ); connect(sb_proxyConnect, QOverload::of(&QSpinBox::valueChanged), this, &KIOPreferences::configChanged); timeoutLayout->addRow(i18n( "Pro&xy connect:" ), sb_proxyConnect); sb_serverConnect = new KPluralHandlingSpinBox( this ); sb_serverConnect->setValue(0); sb_serverConnect->setSuffix( ki18np( " second", " seconds" ) ); connect(sb_serverConnect, QOverload::of(&QSpinBox::valueChanged), this, &KIOPreferences::configChanged); timeoutLayout->addRow(i18n("Server co&nnect:"), sb_serverConnect); sb_serverResponse = new KPluralHandlingSpinBox( this ); sb_serverResponse->setValue(0); sb_serverResponse->setSuffix( ki18np( " second", " seconds" ) ); connect(sb_serverResponse, QOverload::of(&QSpinBox::valueChanged), this, &KIOPreferences::configChanged); timeoutLayout->addRow(i18n("&Server response:"), sb_serverResponse); QGroupBox* gb_Global = new QGroupBox( i18n( "Global Options" ), this ); mainLayout->addWidget( gb_Global ); QVBoxLayout* globalLayout = new QVBoxLayout(gb_Global); cb_globalMarkPartial = new QCheckBox( i18n( "Mark &partially uploaded files" ), this ); cb_globalMarkPartial->setWhatsThis( i18n( "

Marks partially uploaded files " "through SMB, SFTP and other protocols." "

When this option is " "enabled, partially uploaded files " "will have a \".part\" extension. " "This extension will be removed " "once the transfer is complete.

") ); connect(cb_globalMarkPartial, &QAbstractButton::toggled, this, &KIOPreferences::configChanged); globalLayout->addWidget(cb_globalMarkPartial); gb_Ftp = new QGroupBox( i18n( "FTP Options" ), this ); mainLayout->addWidget( gb_Ftp ); QVBoxLayout* ftpLayout = new QVBoxLayout(gb_Ftp); cb_ftpEnablePasv = new QCheckBox( i18n( "Enable passive &mode (PASV)" ), this ); cb_ftpEnablePasv->setWhatsThis( i18n("Enables FTP's \"passive\" mode. " "This is required to allow FTP to " "work from behind firewalls.") ); connect(cb_ftpEnablePasv, &QAbstractButton::toggled, this, &KIOPreferences::configChanged); ftpLayout->addWidget(cb_ftpEnablePasv); cb_ftpMarkPartial = new QCheckBox( i18n( "Mark &partially uploaded files" ), this ); cb_ftpMarkPartial->setWhatsThis( i18n( "

Marks partially uploaded FTP " "files.

When this option is " "enabled, partially uploaded files " "will have a \".part\" extension. " "This extension will be removed " "once the transfer is complete.

") ); connect(cb_ftpMarkPartial, &QAbstractButton::toggled, this, &KIOPreferences::configChanged); ftpLayout->addWidget(cb_ftpMarkPartial); mainLayout->addStretch( 1 ); } KIOPreferences::~KIOPreferences() { } void KIOPreferences::load() { KProtocolManager proto; sb_socketRead->setRange( MIN_TIMEOUT_VALUE, MAX_TIMEOUT_VALUE ); sb_serverResponse->setRange( MIN_TIMEOUT_VALUE, MAX_TIMEOUT_VALUE ); sb_serverConnect->setRange( MIN_TIMEOUT_VALUE, MAX_TIMEOUT_VALUE ); sb_proxyConnect->setRange( MIN_TIMEOUT_VALUE, MAX_TIMEOUT_VALUE ); sb_socketRead->setValue( proto.readTimeout() ); sb_serverResponse->setValue( proto.responseTimeout() ); sb_serverConnect->setValue( proto.connectTimeout() ); sb_proxyConnect->setValue( proto.proxyConnectTimeout() ); cb_globalMarkPartial->setChecked( proto.markPartial() ); KConfig config( QStringLiteral("kio_ftprc"), KConfig::NoGlobals ); cb_ftpEnablePasv->setChecked( !config.group("").readEntry( "DisablePassiveMode", false ) ); cb_ftpMarkPartial->setChecked( config.group("").readEntry( "MarkPartial", true ) ); emit changed( false ); } void KIOPreferences::save() { KSaveIOConfig::setReadTimeout( sb_socketRead->value() ); KSaveIOConfig::setResponseTimeout( sb_serverResponse->value() ); KSaveIOConfig::setConnectTimeout( sb_serverConnect->value() ); KSaveIOConfig::setProxyConnectTimeout( sb_proxyConnect->value() ); KSaveIOConfig::setMarkPartial( cb_globalMarkPartial->isChecked() ); KConfig config(QStringLiteral("kio_ftprc"), KConfig::NoGlobals); config.group("").writeEntry( "DisablePassiveMode", !cb_ftpEnablePasv->isChecked() ); config.group("").writeEntry( "MarkPartial", cb_ftpMarkPartial->isChecked() ); config.sync(); KSaveIOConfig::updateRunningIOSlaves(this); emit changed( false ); } void KIOPreferences::defaults() { sb_socketRead->setValue( DEFAULT_READ_TIMEOUT ); sb_serverResponse->setValue( DEFAULT_RESPONSE_TIMEOUT ); sb_serverConnect->setValue( DEFAULT_CONNECT_TIMEOUT ); sb_proxyConnect->setValue( DEFAULT_PROXY_CONNECT_TIMEOUT ); cb_globalMarkPartial->setChecked( true ); cb_ftpEnablePasv->setChecked( true ); cb_ftpMarkPartial->setChecked( true ); emit changed(true); } QString KIOPreferences::quickHelp() const { return i18n("

Network Preferences

Here you can define" " the behavior of KDE programs when using Internet" " and network connections. If you experience timeouts" " or use a modem to connect to the Internet, you might" " want to adjust these settings." ); } diff --git a/src/kcms/kio/smbrodlg.cpp b/src/kcms/kio/smbrodlg.cpp index be45418c..12ee1140 100644 --- a/src/kcms/kio/smbrodlg.cpp +++ b/src/kcms/kio/smbrodlg.cpp @@ -1,188 +1,187 @@ /* This file is part of the KDE project Copyright (C) 2000 Alexander Neundorf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // TODO: remove me #undef QT_NO_CAST_FROM_ASCII // Own #include "smbrodlg.h" // Qt #include #include #include // KDE #include #include #include -#include #include #include //#include #include K_PLUGIN_FACTORY_DECLARATION(KioConfigFactory) SMBRoOptions::SMBRoOptions(QWidget *parent, const QVariantList &) : KCModule(parent) { QGridLayout *layout = new QGridLayout(this ); QLabel *label=new QLabel(i18n("These settings apply to network browsing only."),this); layout->addWidget(label,0,0, 1, 2 ); m_userLe=new QLineEdit(this); label=new QLabel(i18n("Default user name:"),this); label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); label->setBuddy( m_userLe ); layout->addWidget(label,1,0); layout->addWidget(m_userLe,1,1); m_passwordLe=new QLineEdit(this); m_passwordLe->setEchoMode(QLineEdit::Password); label=new QLabel(i18n("Default password:"),this); label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); label->setBuddy( m_passwordLe ); layout->addWidget(label,2,0); layout->addWidget(m_passwordLe,2,1); /* m_workgroupLe=new QLineEdit(this); label=new QLabel(m_workgroupLe,i18n("Workgroup:"),this); layout->addWidget(label,3,0); layout->addWidget(m_workgroupLe,3,1); m_showHiddenShares=new QCheckBox(i18n("Show hidden shares"),this); layout->addWidget(m_showHiddenShares,4,0, 1, 2 ); m_encodingList = new KComboBox( false, this ); QStringList _strList = KCharsets::charsets()->availableEncodingNames(); m_encodingList->addItems( _strList ); label = new QLabel( i18n( "MS Windows encoding:" ), this ); label->setBuddy( m_encodingList ); layout->addWidget( label, 3, 0 ); layout->addWidget( m_encodingList, 3, 1 ); */ layout->addWidget(new QWidget(this),4,0); // connect(m_showHiddenShares, SIGNAL(toggled(bool)), this, SLOT(changed())); connect(m_userLe, &QLineEdit::textChanged, this, &SMBRoOptions::changed); connect(m_passwordLe, &QLineEdit::textChanged, this, &SMBRoOptions::changed); // connect(m_workgroupLe, SIGNAL(textChanged(QString)), this, SLOT(changed())); // connect( m_encodingList, SIGNAL(activated(QString)), this , SLOT(changed()) ); layout->setRowStretch(4, 1); } SMBRoOptions::~SMBRoOptions() { } void SMBRoOptions::load() { KConfig *cfg = new KConfig(QStringLiteral("kioslaverc")); KConfigGroup group = cfg->group("Browser Settings/SMBro" ); m_userLe->setText(group.readEntry("User")); // m_workgroupLe->setText(group.readEntry("Workgroup")); // m_showHiddenShares->setChecked(group.readEntry("ShowHiddenShares", QVariant(false)).toBool()); //QStringList _strList = KCharsets::charsets()->availableEncodingNames(); //QString m_encoding = QTextCodec::codecForLocale()->name(); //m_encodingList->setCurrentIndex( _strList.indexOf( group.readEntry( "Encoding", m_encoding.toLower() ) ) ); // unscramble QString scrambled = group.readEntry( "Password" ); QString password; const int passwordLength = scrambled.length() / 3; password.reserve(passwordLength); for (int i=0; isetText(password); delete cfg; } void SMBRoOptions::save() { KConfig *cfg = new KConfig(QStringLiteral("kioslaverc")); KConfigGroup group = cfg->group("Browser Settings/SMBro" ); group.writeEntry( "User", m_userLe->text()); // group.writeEntry( "Workgroup", m_workgroupLe->text()); // group.writeEntry( "ShowHiddenShares", m_showHiddenShares->isChecked()); // group.writeEntry( "Encoding", m_encodingList->currentText() ); //taken from Nicola Brodu's smb ioslave //it's not really secure, but at //least better than storing the plain password QString password(m_passwordLe->text()); QString scrambled; for (int i=0; i> 10; unsigned int a2 = (num & 0x3E0) >> 5; unsigned int a3 = (num & 0x1F); scrambled += (char)(a1+'0'); scrambled += (char)(a2+'A'); scrambled += (char)(a3+'0'); } group.writeEntry( "Password", scrambled); delete cfg; } void SMBRoOptions::defaults() { m_userLe->setText(QString()); m_passwordLe->setText(QString()); // m_workgroupLe->setText(""); // m_showHiddenShares->setChecked(false); } void SMBRoOptions::changed() { emit KCModule::changed(true); } QString SMBRoOptions::quickHelp() const { return i18n("

Windows Shares

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

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

"); } diff --git a/src/kcms/kio/useragentselectordlg.cpp b/src/kcms/kio/useragentselectordlg.cpp index 1655c9bb..9c094656 100644 --- a/src/kcms/kio/useragentselectordlg.cpp +++ b/src/kcms/kio/useragentselectordlg.cpp @@ -1,160 +1,159 @@ /** * 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 #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); connect (mUi.aliasComboBox, QOverload::of(&QComboBox::activated), this, &UserAgentSelectorDlg::onAliasChanged); 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/kcms/webshortcuts/main.cpp b/src/kcms/webshortcuts/main.cpp index 8cbc7eb9..19b29544 100644 --- a/src/kcms/webshortcuts/main.cpp +++ b/src/kcms/webshortcuts/main.cpp @@ -1,128 +1,128 @@ /* * Copyright (c) 2000 Yves Arrouye * * 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 "main.h" // Qt #include -#include +#include // KDE #include #include #include #include #include #include K_PLUGIN_FACTORY(KURIFilterModuleFactory, registerPlugin();) KURIFilterModule::KURIFilterModule(QWidget *parent, const QVariantList &args) : KCModule(parent, args), m_widget(nullptr) { KAboutData *about = new KAboutData(QStringLiteral("kcm_webshortcuts"), i18n("Web Shortcuts"), QStringLiteral("0.1"), i18n("Configure enhanced browsing features"), KAboutLicense::GPL); setAboutData(about); KCModule::setButtons(KCModule::Buttons(KCModule::Default | KCModule::Apply | KCModule::Help)); filter = KUriFilter::self(); setQuickHelp(i18n("

Enhanced Browsing

In this module you can configure some enhanced browsing" " features of KDE. " "

Web Shortcuts

Web Shortcuts are a quick way of using Web search engines. For example, type \"duckduckgo:frobozz\"" " or \"dd:frobozz\" and your web browser will do a search on DuckDuckGo for \"frobozz\"." " Even easier: just press Alt+F2 (if you have not" " changed this keyboard shortcut) and enter the shortcut in the Run Command dialog.")); QVBoxLayout *layout = new QVBoxLayout(this); QMap helper; // Load the plugins. This saves a public method in KUriFilter just for this. QVector plugins = KPluginLoader::findPlugins(QStringLiteral("kf5/urifilters")); for (const KPluginMetaData &pluginMetaData : plugins) { KPluginFactory *factory = qobject_cast(pluginMetaData.instantiate()); if (factory) { KUriFilterPlugin *plugin = factory->create(nullptr); if (plugin) { KCModule *module = plugin->configModule(this, nullptr); if (module) { modules.append(module); helper.insert(plugin->configName(), module); connect(module, QOverload::of(&KCModule::changed), this, QOverload::of(&KCModule::changed)); } } } } if (modules.count() > 1) { QTabWidget *tab = new QTabWidget(this); QMap::iterator it2; for (it2 = helper.begin(); it2 != helper.end(); ++it2) { tab->addTab(it2.value(), it2.key()); } tab->setCurrentIndex(tab->indexOf(modules.first())); m_widget = tab; } else if (modules.count() == 1) { m_widget = modules.first(); if (m_widget->layout()) { m_widget->layout()->setContentsMargins(0, 0, 0, 0); } } if (m_widget) { layout->addWidget(m_widget); } } void KURIFilterModule::load() { // seems not to be necessary, since modules automatically call load() on show (uwolfer) // foreach( KCModule* module, modules ) // { // module->load(); // } } void KURIFilterModule::save() { foreach(KCModule * module, modules) { module->save(); } } void KURIFilterModule::defaults() { foreach(KCModule * module, modules) { module->defaults(); } } KURIFilterModule::~KURIFilterModule() { qDeleteAll(modules); } #include "main.moc" diff --git a/src/kioexec/main.cpp b/src/kioexec/main.cpp index 8fd9bb9b..548c1908 100644 --- a/src/kioexec/main.cpp +++ b/src/kioexec/main.cpp @@ -1,320 +1,318 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2000-2005 David Faure Copyright (C) 2001 Waldo Bastian 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // TODO: remove me #undef QT_NO_CAST_FROM_ASCII #include "main.h" #include "kio_version.h" #include "kioexecdinterface.h" #include #include -#include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 #include #endif static const char description[] = I18N_NOOP("KIO Exec - Opens remote files, watches modifications, asks for upload"); KIOExec::KIOExec(const QStringList &args, bool tempFiles, const QString &suggestedFileName) : mExited(false) , mTempFiles(tempFiles) , mUseDaemon(false) , mSuggestedFileName(suggestedFileName) , expectedCounter(0) , command(args.first()) , jobCounter(0) { qDebug() << "command=" << command << "args=" << args; for (int i = 1; i < args.count(); i++) { const QUrl urlArg = QUrl::fromUserInput(args.value(i)); if (!urlArg.isValid()) { KMessageBox::error(nullptr, i18n("Invalid URL: %1", args.value(i))); exit(1); } KIO::StatJob* mostlocal = KIO::mostLocalUrl(urlArg); bool b = mostlocal->exec(); if (!b) { KMessageBox::error(nullptr, i18n("File not found: %1", urlArg.toDisplayString())); exit(1); } Q_ASSERT(b); const QUrl url = mostlocal->mostLocalUrl(); //kDebug() << "url=" << url.url() << " filename=" << url.fileName(); // A local file, not an URL ? // => It is not encoded and not shell escaped, too. if (url.isLocalFile()) { FileInfo file; file.path = url.toLocalFile(); file.url = url; fileList.append(file); } else { // It is an URL if (!url.isValid()) { KMessageBox::error(nullptr, i18n("The URL %1\nis malformed" , url.url())); } else if (mTempFiles) { KMessageBox::error(nullptr, i18n("Remote URL %1\nnot allowed with --tempfiles switch" , url.toDisplayString())); } else { // We must fetch the file QString fileName = KIO::encodeFileName(url.fileName()); if (!suggestedFileName.isEmpty()) fileName = suggestedFileName; if (fileName.isEmpty()) fileName = QStringLiteral("unnamed"); // Build the destination filename, in ~/.cache/kioexec/krun/ // Unlike KDE-1.1, we put the filename at the end so that the extension is kept // (Some programs rely on it) QString krun_writable = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/krun/%1_%2/").arg(QCoreApplication::applicationPid()).arg(jobCounter++); QDir().mkpath(krun_writable); // error handling will be done by the job QString tmp = krun_writable + fileName; FileInfo file; file.path = tmp; file.url = url; fileList.append(file); expectedCounter++; const QUrl dest = QUrl::fromLocalFile(tmp); qDebug() << "Copying" << url << " to" << dest; KIO::Job *job = KIO::file_copy(url, dest); jobList.append(job); connect(job, &KJob::result, this, &KIOExec::slotResult); } } } if (mTempFiles) { slotRunApp(); return; } counter = 0; if (counter == expectedCounter) { slotResult(nullptr); } } void KIOExec::slotResult(KJob *job) { if (job) { KIO::FileCopyJob *copyJob = static_cast(job); const QString path = copyJob->destUrl().path(); if (job->error()) { // That error dialog would be queued, i.e. not immediate... //job->showErrorDialog(); if ((job->error() != KIO::ERR_USER_CANCELED)) KMessageBox::error(nullptr, job->errorString()); auto it = std::find_if(fileList.begin(), fileList.end(), [&path](const FileInfo &i) { return (i.path == path); }); if (it != fileList.end()) { fileList.erase(it); } else { qDebug() << path << " not found in list"; } } else { // Tell kioexecd to watch the file for changes. const QString dest = copyJob->srcUrl().toString(); qDebug() << "Telling kioexecd to watch path" << path << "dest" << dest; OrgKdeKIOExecdInterface kioexecd(QStringLiteral("org.kde.kioexecd"), QStringLiteral("/modules/kioexecd"), QDBusConnection::sessionBus()); kioexecd.watch(path, dest); mUseDaemon = !kioexecd.lastError().isValid(); if (!mUseDaemon) { qDebug() << "Not using kioexecd"; } } } counter++; if (counter < expectedCounter) { return; } qDebug() << "All files downloaded, will call slotRunApp shortly"; // We know we can run the app now - but let's finish the job properly first. QTimer::singleShot(0, this, &KIOExec::slotRunApp); jobList.clear(); } void KIOExec::slotRunApp() { if (fileList.isEmpty()) { qDebug() << "No files downloaded -> exiting"; mExited = true; QApplication::exit(1); return; } KService service(QStringLiteral("dummy"), command, QString()); QList list; list.reserve(fileList.size()); // Store modification times QList::Iterator it = fileList.begin(); for (; it != fileList.end() ; ++it) { QFileInfo info(QFile::encodeName(it->path)); it->time = info.lastModified(); QUrl url = QUrl::fromLocalFile(it->path); list << url; } KIO::DesktopExecParser execParser(service, list); QStringList params = execParser.resultingArguments(); qDebug() << "EXEC" << params.join(QLatin1Char(' ')); // propagate the startup identification to the started process KStartupInfoId id; QByteArray startupId; #if HAVE_X11 if (QX11Info::isPlatformX11()) { startupId = QX11Info::nextStartupId(); } #endif id.initId(startupId); id.setupStartupEnv(); QString exe(params.takeFirst()); const int exit_code = QProcess::execute(exe, params); KStartupInfo::resetStartupEnv(); qDebug() << "EXEC done"; // Test whether one of the files changed for (it = fileList.begin(); it != fileList.end(); ++it) { QString src = it->path; const QUrl dest = it->url; QFileInfo info(src); const bool uploadChanges = !mUseDaemon && !dest.isLocalFile(); if (info.exists() && (it->time != info.lastModified())) { if (mTempFiles) { if (KMessageBox::questionYesNo(nullptr, i18n("The supposedly temporary file\n%1\nhas been modified.\nDo you still want to delete it?", dest.toDisplayString(QUrl::PreferLocalFile)), i18n("File Changed"), KStandardGuiItem::del(), KGuiItem(i18n("Do Not Delete"))) != KMessageBox::Yes) continue; // don't delete the temp file } else if (uploadChanges) { // no upload when it's already a local file or kioexecd already did it. if (KMessageBox::questionYesNo(nullptr, i18n("The file\n%1\nhas been modified.\nDo you want to upload the changes?" , dest.toDisplayString()), i18n("File Changed"), KGuiItem(i18n("Upload")), KGuiItem(i18n("Do Not Upload"))) == KMessageBox::Yes) { qDebug() << "src='" << src << "' dest='" << dest << "'"; // Do it the synchronous way. KIO::CopyJob* job = KIO::copy(QUrl::fromLocalFile(src), dest); if (!job->exec()) { KMessageBox::error(nullptr, job->errorText()); continue; // don't delete the temp file } } } } if ((uploadChanges || mTempFiles) && exit_code == 0) { // Wait for a reasonable time so that even if the application forks on startup (like OOo or amarok) // it will have time to start up and read the file before it gets deleted. #130709. const int sleepSecs = 180; qDebug() << "sleeping for" << sleepSecs << "seconds before deleting file..."; QThread::currentThread()->sleep(sleepSecs); const QString parentDir = info.path(); qDebug() << sleepSecs << "seconds have passed, deleting" << info.filePath(); QFile(QFile::encodeName(src)).remove(); // NOTE: this is not necessarily a temporary directory. if (QDir().rmdir(parentDir)) { qDebug() << "Removed empty parent directory" << parentDir; } } } mExited = true; QApplication::exit(exit_code); } int main(int argc, char **argv) { QApplication app(argc, argv); KAboutData aboutData(QStringLiteral("kioexec"), i18n("KIOExec"), QStringLiteral(KIO_VERSION_STRING), i18n(description), KAboutLicense::GPL, i18n("(c) 1998-2000,2003 The KFM/Konqueror Developers")); aboutData.addAuthor(i18n("David Faure"), QString(), QStringLiteral("faure@kde.org")); aboutData.addAuthor(i18n("Stephan Kulow"), QString(), QStringLiteral("coolo@kde.org")); aboutData.addAuthor(i18n("Bernhard Rosenkraenzer"), QString(), QStringLiteral("bero@arklinux.org")); aboutData.addAuthor(i18n("Waldo Bastian"), QString(), QStringLiteral("bastian@kde.org")); aboutData.addAuthor(i18n("Oswald Buddenhagen"), QString(), QStringLiteral("ossi@kde.org")); KAboutData::setApplicationData(aboutData); KDBusService service(KDBusService::Multiple); QCommandLineParser parser; parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("tempfiles") , i18n("Treat URLs as local files and delete them afterwards"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("suggestedfilename"), i18n("Suggested file name for the downloaded file"), QStringLiteral("filename"))); parser.addPositionalArgument(QStringLiteral("command"), i18n("Command to execute")); parser.addPositionalArgument(QStringLiteral("urls"), i18n("URL(s) or local file(s) used for 'command'")); app.setQuitOnLastWindowClosed(false); aboutData.setupCommandLine(&parser); parser.process(app); aboutData.processCommandLine(&parser); if (parser.positionalArguments().count() < 1) { parser.showHelp(-1); return -1; } const bool tempfiles = parser.isSet(QStringLiteral("tempfiles")); const QString suggestedfilename = parser.value(QStringLiteral("suggestedfilename")); KIOExec exec(parser.positionalArguments(), tempfiles, suggestedfilename); // Don't go into the event loop if we already want to exit (#172197) if (exec.exited()) { return 0; } return app.exec(); } diff --git a/src/kioexec/main.h b/src/kioexec/main.h index 8e11874a..597c3e65 100644 --- a/src/kioexec/main.h +++ b/src/kioexec/main.h @@ -1,75 +1,74 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2000-2005 David Faure Copyright (C) 2001 Waldo Bastian 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIOEXEC_MAIN_H #define KIOEXEC_MAIN_H #include #include #include -#include #include #include namespace KIO { class Job; } class KJob; class QCommandLineParser; class KIOExec : public QObject { Q_OBJECT public: KIOExec(const QStringList &args, bool tempFiles, const QString &suggestedFileName); bool exited() const { return mExited; } public Q_SLOTS: void slotResult(KJob *); void slotRunApp(); protected: bool mExited; bool mTempFiles; bool mUseDaemon; QString mSuggestedFileName; int counter; int expectedCounter; QString command; struct FileInfo { QString path; QUrl url; QDateTime time; }; QList fileList; int jobCounter; QList jobList; }; #endif diff --git a/src/kioslave/kioslave.cpp b/src/kioslave/kioslave.cpp index 2b93a80d..52b3c837 100644 --- a/src/kioslave/kioslave.cpp +++ b/src/kioslave/kioslave.cpp @@ -1,139 +1,138 @@ /* * This file is part of the KDE libraries * Copyright (c) 1999-2000 Waldo Bastian * (c) 1999 Mario Weilguni * (c) 2001 Lubos Lunak * * 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 #include #include #include #include #include #include #include #if defined(Q_OS_WIN) || defined(Q_OS_MAC) #define USE_KPROCESS_FOR_KIOSLAVES #endif #ifdef USE_KPROCESS_FOR_KIOSLAVES -#include #include #include #ifdef Q_OS_WIN #include #include #endif #endif #ifndef Q_OS_WIN /* These are to link libkio even if 'smart' linker is used */ #include extern "C" KIO::AuthInfo *_kioslave_init_kio() { return new KIO::AuthInfo(); } #endif int main(int argc, char **argv) { if (argc < 5) { fprintf(stderr, "Usage: kioslave \n\nThis program is part of KDE.\n"); return 1; } #ifndef _WIN32_WCE setlocale(LC_ALL, ""); #endif QString libname = QFile::decodeName(argv[1]); if (libname.isEmpty()) { fprintf(stderr, "library path is empty.\n"); return 1; } // Use KPluginLoader to locate the library when using a relative path // But we need to use QLibrary to actually load it, because of resolve()! QString libpath = KPluginLoader::findPlugin(libname); if (libpath.isEmpty()) { fprintf(stderr, "could not locate %s, check QT_PLUGIN_PATH\n", qPrintable(libname)); return 1; } QLibrary lib(libpath); if (!lib.load()) { fprintf(stderr, "could not open %s: %s\n", qPrintable(libname), qPrintable(lib.errorString())); return 1; } QFunctionPointer sym = lib.resolve("kdemain"); if (!sym) { fprintf(stderr, "Could not find kdemain: %s\n", qPrintable(lib.errorString())); return 1; } #ifdef Q_OS_WIN // enter debugger in case debugging is activated QString slaveDebugWait(QString::fromLocal8Bit(qgetenv("KDE_SLAVE_DEBUG_WAIT"))); if (slaveDebugWait == QLatin1String("all") || slaveDebugWait == QString::fromLocal8Bit(argv[2])) { # ifdef Q_CC_MSVC // msvc debugger or windbg supports jit debugging, the latter requires setting up windbg jit with windbg -i DebugBreak(); # else // gdb does not support win32 jit debug support, so implement it by ourself WCHAR buf[1024]; GetModuleFileName(NULL, buf, 1024); QStringList params; params << QString::fromUtf16((const unsigned short *)buf); params << QString::number(GetCurrentProcessId()); QProcess::startDetached(QStringLiteral("gdb"), params); Sleep(1000); # endif } # if defined(Q_CC_MSVC) && !defined(_WIN32_WCE) else { QString slaveDebugPopup(QString::fromLocal8Bit(qgetenv("KDE_SLAVE_DEBUG_POPUP"))); if (slaveDebugPopup == QLatin1String("all") || slaveDebugPopup == QString::fromLocal8Bit(argv[2])) { // A workaround for OSes where DebugBreak() does not work in administrative mode (actually Vista with msvc 2k5) // - display a native message box so developer can attach the debugger to the KIO slave process and click OK. MessageBoxA(NULL, QStringLiteral("Please attach the debugger to process #%1 (%2)").arg(getpid()).arg(QString::fromLocal8Bit(argv[0])).toLatin1().constData(), QStringLiteral("\"%1\" KIO Slave Debugging").arg(QString::fromLocal8Bit(argv[2])).toLatin1().constData(), MB_OK | MB_ICONINFORMATION | MB_TASKMODAL); } } # endif #endif // Q_OS_WIN int (*func)(int, char *[]) = (int (*)(int, char *[])) sym; // We need argv[0] to remain /path/to/kioslave // so that applicationDirPath() is correct on non-Linux (no /proc) // and we want to skip argv[1] so the kioslave exe is transparent to kdemain. const int newArgc = argc - 1; QVarLengthArray newArgv(newArgc); newArgv[0] = argv[0]; for (int i = 1; i < newArgc; ++i) { newArgv[i] = argv[i+1]; } return func(newArgc, newArgv.data()); /* Launch! */ } diff --git a/src/kpac/script.cpp b/src/kpac/script.cpp index 24cc0a0c..581c234a 100644 --- a/src/kpac/script.cpp +++ b/src/kpac/script.cpp @@ -1,773 +1,770 @@ /* Copyright (c) 2003 Malte Starostik Copyright (c) 2011 Dawit Alemayehu This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "script.h" #include #include #include -#include -#include #include #include #include #include #include #include #include -#include #include #include #define QL1S(x) QLatin1String(x) namespace { static int findString(const QString &s, const char *const *values) { int index = 0; for (const char *const *p = values; *p; ++p, ++index) { if (s.compare(QLatin1String(*p), Qt::CaseInsensitive) == 0) { return index; } } return -1; } static const QDateTime getTime(QScriptContext *context) { const QString tz = context->argument(context->argumentCount() - 1).toString(); if (tz.compare(QLatin1String("gmt"), Qt::CaseInsensitive) == 0) { return QDateTime::currentDateTimeUtc(); } return QDateTime::currentDateTime(); } template static bool checkRange(T value, T min, T max) { return ((min <= max && value >= min && value <= max) || (min > max && (value <= min || value >= max))); } static bool isLocalHostAddress(const QHostAddress &address) { if (address == QHostAddress::LocalHost) { return true; } if (address == QHostAddress::LocalHostIPv6) { return true; } return false; } static bool isIPv6Address(const QHostAddress &address) { return address.protocol() == QAbstractSocket::IPv6Protocol; } static bool isIPv4Address(const QHostAddress &address) { return (address.protocol() == QAbstractSocket::IPv4Protocol); } static bool isSpecialAddress(const QHostAddress &address) { // Catch all the special addresses and return false. if (address == QHostAddress::Null) { return true; } if (address == QHostAddress::Any) { return true; } if (address == QHostAddress::AnyIPv6) { return true; } if (address == QHostAddress::Broadcast) { return true; } return false; } static bool addressLessThanComparison(const QHostAddress &addr1, const QHostAddress &addr2) { if (addr1.protocol() == QAbstractSocket::IPv4Protocol && addr2.protocol() == QAbstractSocket::IPv4Protocol) { return addr1.toIPv4Address() < addr2.toIPv4Address(); } if (addr1.protocol() == QAbstractSocket::IPv6Protocol && addr2.protocol() == QAbstractSocket::IPv6Protocol) { const Q_IPV6ADDR ipv6addr1 = addr1.toIPv6Address(); const Q_IPV6ADDR ipv6addr2 = addr2.toIPv6Address(); for (int i = 0; i < 16; ++i) { if (ipv6addr1[i] != ipv6addr2[i]) { return ((ipv6addr1[i] & 0xff) - (ipv6addr2[i] & 0xff)); } } } return false; } static QString addressListToString(const QList &addressList, const QHash &actualEntryMap) { QString result; Q_FOREACH (const QHostAddress &address, addressList) { if (!result.isEmpty()) { result += QLatin1Char(';'); } result += actualEntryMap.value(address.toString()); } return result; } class Address { public: struct Error {}; static Address resolve(const QString &host) { return Address(host); } QList addresses() const { return m_addressList; } QHostAddress address() const { if (m_addressList.isEmpty()) { return QHostAddress(); } return m_addressList.first(); } private: Address(const QString &host) { // Always try to see if it's already an IP first, to avoid Qt doing a // needless reverse lookup QHostAddress address(host); if (address.isNull()) { QHostInfo hostInfo = KIO::HostInfo::lookupCachedHostInfoFor(host); if (hostInfo.hostName().isEmpty() || hostInfo.error() != QHostInfo::NoError) { hostInfo = QHostInfo::fromName(host); KIO::HostInfo::cacheLookup(hostInfo); } m_addressList = hostInfo.addresses(); } else { m_addressList.clear(); m_addressList.append(address); } } QList m_addressList; }; // isPlainHostName(host) // @returns true if @p host doesn't contains a domain part QScriptValue IsPlainHostName(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 1) { return engine->undefinedValue(); } return engine->toScriptValue(context->argument(0).toString().indexOf(QLatin1Char('.')) == -1); } // dnsDomainIs(host, domain) // @returns true if the domain part of @p host matches @p domain QScriptValue DNSDomainIs(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 2) { return engine->undefinedValue(); } const QString host = context->argument(0).toString(); const QString domain = context->argument(1).toString(); return engine->toScriptValue(host.endsWith(domain, Qt::CaseInsensitive)); } // localHostOrDomainIs(host, fqdn) // @returns true if @p host is unqualified or equals @p fqdn QScriptValue LocalHostOrDomainIs(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 2) { return engine->undefinedValue(); } const QString host = context->argument(0).toString(); if (!host.contains(QLatin1Char('.'))) { return engine->toScriptValue(true); } const QString fqdn = context->argument(1).toString(); return engine->toScriptValue((host.compare(fqdn, Qt::CaseInsensitive) == 0)); } // isResolvable(host) // @returns true if host is resolvable to a IPv4 address. QScriptValue IsResolvable(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 1) { return engine->undefinedValue(); } try { const Address info = Address::resolve(context->argument(0).toString()); bool hasResolvableIPv4Address = false; Q_FOREACH (const QHostAddress &address, info.addresses()) { if (!isSpecialAddress(address) && isIPv4Address(address)) { hasResolvableIPv4Address = true; break; } } return engine->toScriptValue(hasResolvableIPv4Address); } catch (const Address::Error &) { return engine->toScriptValue(false); } } // isInNet(host, subnet, mask) // @returns true if the IPv4 address of host is within the specified subnet // and mask, false otherwise. QScriptValue IsInNet(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 3) { return engine->undefinedValue(); } try { const Address info = Address::resolve(context->argument(0).toString()); bool isInSubNet = false; const QString subnetStr = context->argument(1).toString() + QLatin1Char('/') + context->argument(2).toString(); const QPair subnet = QHostAddress::parseSubnet(subnetStr); Q_FOREACH (const QHostAddress &address, info.addresses()) { if (!isSpecialAddress(address) && isIPv4Address(address) && address.isInSubnet(subnet)) { isInSubNet = true; break; } } return engine->toScriptValue(isInSubNet); } catch (const Address::Error &) { return engine->toScriptValue(false); } } // dnsResolve(host) // @returns the IPv4 address for host or an empty string if host is not resolvable. QScriptValue DNSResolve(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 1) { return engine->undefinedValue(); } try { const Address info = Address::resolve(context->argument(0).toString()); QString resolvedAddress(QLatin1String("")); Q_FOREACH (const QHostAddress &address, info.addresses()) { if (!isSpecialAddress(address) && isIPv4Address(address)) { resolvedAddress = address.toString(); break; } } return engine->toScriptValue(resolvedAddress); } catch (const Address::Error &) { return engine->toScriptValue(QString(QLatin1String(""))); } } // myIpAddress() // @returns the local machine's IPv4 address. Note that this will return // the address for the first interfaces that match its criteria even if the // machine has multiple interfaces. QScriptValue MyIpAddress(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount()) { return engine->undefinedValue(); } QString ipAddress; const QList addresses = QNetworkInterface::allAddresses(); for (const QHostAddress& address : addresses) { if (isIPv4Address(address) && !isSpecialAddress(address) && !isLocalHostAddress(address)) { ipAddress = address.toString(); break; } } return engine->toScriptValue(ipAddress); } // dnsDomainLevels(host) // @returns the number of dots ('.') in @p host QScriptValue DNSDomainLevels(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 1) { return engine->undefinedValue(); } const QString host = context->argument(0).toString(); if (host.isNull()) { return engine->toScriptValue(0); } return engine->toScriptValue(host.count(QLatin1Char('.'))); } // shExpMatch(str, pattern) // @returns true if @p str matches the shell @p pattern QScriptValue ShExpMatch(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 2) { return engine->undefinedValue(); } QRegExp pattern(context->argument(1).toString(), Qt::CaseSensitive, QRegExp::Wildcard); return engine->toScriptValue(pattern.exactMatch(context->argument(0).toString())); } // weekdayRange(day [, "GMT" ]) // weekdayRange(day1, day2 [, "GMT" ]) // @returns true if the current day equals day or between day1 and day2 resp. // If the last argument is "GMT", GMT timezone is used, otherwise local time QScriptValue WeekdayRange(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() < 1 || context->argumentCount() > 3) { return engine->undefinedValue(); } static const char *const days[] = { "sun", "mon", "tue", "wed", "thu", "fri", "sat", nullptr }; const int d1 = findString(context->argument(0).toString(), days); if (d1 == -1) { return engine->undefinedValue(); } int d2 = findString(context->argument(1).toString(), days); if (d2 == -1) { d2 = d1; } // Adjust the days of week coming from QDateTime since it starts // counting with Monday as 1 and ends with Sunday as day 7. int dayOfWeek = getTime(context).date().dayOfWeek(); if (dayOfWeek == 7) { dayOfWeek = 0; } return engine->toScriptValue(checkRange(dayOfWeek, d1, d2)); } // dateRange(day [, "GMT" ]) // dateRange(day1, day2 [, "GMT" ]) // dateRange(month [, "GMT" ]) // dateRange(month1, month2 [, "GMT" ]) // dateRange(year [, "GMT" ]) // dateRange(year1, year2 [, "GMT" ]) // dateRange(day1, month1, day2, month2 [, "GMT" ]) // dateRange(month1, year1, month2, year2 [, "GMT" ]) // dateRange(day1, month1, year1, day2, month2, year2 [, "GMT" ]) // @returns true if the current date (GMT or local time according to // presence of "GMT" as last argument) is within the given range QScriptValue DateRange(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() < 1 || context->argumentCount() > 7) { return engine->undefinedValue(); } static const char *const months[] = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", nullptr }; QVector values; for (int i = 0; i < context->argumentCount(); ++i) { int value = -1; if (context->argument(i).isNumber()) { value = context->argument(i).toInt32(); } else { // QDate starts counting months from 1, so we add 1 here. value = findString(context->argument(i).toString(), months) + 1; } if (value > 0) { values.append(value); } else { break; } } const QDate now = getTime(context).date(); // day1, month1, year1, day2, month2, year2 if (values.size() == 6) { const QDate d1(values[2], values[1], values[0]); const QDate d2(values[5], values[4], values[3]); return engine->toScriptValue(checkRange(now, d1, d2)); } // day1, month1, day2, month2 else if (values.size() == 4 && values[ 1 ] < 13 && values[ 3 ] < 13) { const QDate d1(now.year(), values[1], values[0]); const QDate d2(now.year(), values[3], values[2]); return engine->toScriptValue(checkRange(now, d1, d2)); } // month1, year1, month2, year2 else if (values.size() == 4) { const QDate d1(values[1], values[0], now.day()); const QDate d2(values[3], values[2], now.day()); return engine->toScriptValue(checkRange(now, d1, d2)); } // year1, year2 else if (values.size() == 2 && values[0] >= 1000 && values[1] >= 1000) { return engine->toScriptValue(checkRange(now.year(), values[0], values[1])); } // day1, day2 else if (values.size() == 2 && context->argument(0).isNumber() && context->argument(1).isNumber()) { return engine->toScriptValue(checkRange(now.day(), values[0], values[1])); } // month1, month2 else if (values.size() == 2) { return engine->toScriptValue(checkRange(now.month(), values[0], values[1])); } // year else if (values.size() == 1 && values[ 0 ] >= 1000) { return engine->toScriptValue(checkRange(now.year(), values[0], values[0])); } // day else if (values.size() == 1 && context->argument(0).isNumber()) { return engine->toScriptValue(checkRange(now.day(), values[0], values[0])); } // month else if (values.size() == 1) { return engine->toScriptValue(checkRange(now.month(), values[0], values[0])); } return engine->undefinedValue(); } // timeRange(hour [, "GMT" ]) // timeRange(hour1, hour2 [, "GMT" ]) // timeRange(hour1, min1, hour2, min2 [, "GMT" ]) // timeRange(hour1, min1, sec1, hour2, min2, sec2 [, "GMT" ]) // @returns true if the current time (GMT or local based on presence // of "GMT" argument) is within the given range QScriptValue TimeRange(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() < 1 || context->argumentCount() > 7) { return engine->undefinedValue(); } QVector values; for (int i = 0; i < context->argumentCount(); ++i) { if (!context->argument(i).isNumber()) { break; } values.append(context->argument(i).toNumber()); } const QTime now = getTime(context).time(); // hour1, min1, sec1, hour2, min2, sec2 if (values.size() == 6) { const QTime t1(values[0], values[1], values[2]); const QTime t2(values[3], values[4], values[5]); return engine->toScriptValue(checkRange(now, t1, t2)); } // hour1, min1, hour2, min2 else if (values.size() == 4) { const QTime t1(values[0], values[1]); const QTime t2(values[2], values[3]); return engine->toScriptValue(checkRange(now, t1, t2)); } // hour1, hour2 else if (values.size() == 2) { return engine->toScriptValue(checkRange(now.hour(), values[0], values[1])); } // hour else if (values.size() == 1) { return engine->toScriptValue(checkRange(now.hour(), values[0], values[0])); } return engine->undefinedValue(); } /* * Implementation of Microsoft's IPv6 Extension for PAC * * Documentation: * http://msdn.microsoft.com/en-us/library/gg308477(v=vs.85).aspx * http://msdn.microsoft.com/en-us/library/gg308478(v=vs.85).aspx * http://msdn.microsoft.com/en-us/library/gg308474(v=vs.85).aspx * http://blogs.msdn.com/b/wndp/archive/2006/07/13/ipv6-pac-extensions-v0-9.aspx */ // isResolvableEx(host) // @returns true if host is resolvable to an IPv4 or IPv6 address. QScriptValue IsResolvableEx(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 1) { return engine->undefinedValue(); } try { const Address info = Address::resolve(context->argument(0).toString()); bool hasResolvableIPAddress = false; Q_FOREACH (const QHostAddress &address, info.addresses()) { if (isIPv4Address(address) || isIPv6Address(address)) { hasResolvableIPAddress = true; break; } } return engine->toScriptValue(hasResolvableIPAddress); } catch (const Address::Error &) { return engine->toScriptValue(false); } } // isInNetEx(ipAddress, ipPrefix ) // @returns true if ipAddress is within the specified ipPrefix. QScriptValue IsInNetEx(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 2) { return engine->undefinedValue(); } try { const Address info = Address::resolve(context->argument(0).toString()); bool isInSubNet = false; const QString subnetStr = context->argument(1).toString(); const QPair subnet = QHostAddress::parseSubnet(subnetStr); Q_FOREACH (const QHostAddress &address, info.addresses()) { if (isSpecialAddress(address)) { continue; } if (address.isInSubnet(subnet)) { isInSubNet = true; break; } } return engine->toScriptValue(isInSubNet); } catch (const Address::Error &) { return engine->toScriptValue(false); } } // dnsResolveEx(host) // @returns a semi-colon delimited string containing IPv6 and IPv4 addresses // for host or an empty string if host is not resolvable. QScriptValue DNSResolveEx(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 1) { return engine->undefinedValue(); } try { const Address info = Address::resolve(context->argument(0).toString()); QStringList addressList; QString resolvedAddress(QLatin1String("")); Q_FOREACH (const QHostAddress &address, info.addresses()) { if (!isSpecialAddress(address)) { addressList << address.toString(); } } if (!addressList.isEmpty()) { resolvedAddress = addressList.join(QLatin1Char(';')); } return engine->toScriptValue(resolvedAddress); } catch (const Address::Error &) { return engine->toScriptValue(QString(QLatin1String(""))); } } // myIpAddressEx() // @returns a semi-colon delimited string containing all IP addresses for localhost (IPv6 and/or IPv4), // or an empty string if unable to resolve localhost to an IP address. QScriptValue MyIpAddressEx(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount()) { return engine->undefinedValue(); } QStringList ipAddressList; const QList addresses = QNetworkInterface::allAddresses(); for (const QHostAddress& address : addresses) { if (!isSpecialAddress(address) && !isLocalHostAddress(address)) { ipAddressList << address.toString(); } } return engine->toScriptValue(ipAddressList.join(QLatin1Char(';'))); } // sortIpAddressList(ipAddressList) // @returns a sorted ipAddressList. If both IPv4 and IPv6 addresses are present in // the list. The sorted IPv6 addresses will precede the sorted IPv4 addresses. QScriptValue SortIpAddressList(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() != 1) { return engine->undefinedValue(); } QHash actualEntryMap; QList ipV4List, ipV6List; const QStringList ipAddressList = context->argument(0).toString().split(QLatin1Char(';')); for (const QString &ipAddress : ipAddressList) { QHostAddress address(ipAddress); switch (address.protocol()) { case QAbstractSocket::IPv4Protocol: ipV4List << address; actualEntryMap.insert(address.toString(), ipAddress); break; case QAbstractSocket::IPv6Protocol: ipV6List << address; actualEntryMap.insert(address.toString(), ipAddress); break; default: break; } } QString sortedAddress(QLatin1String("")); if (!ipV6List.isEmpty()) { std::sort(ipV6List.begin(), ipV6List.end(), addressLessThanComparison); sortedAddress += addressListToString(ipV6List, actualEntryMap); } if (!ipV4List.isEmpty()) { std::sort(ipV4List.begin(), ipV4List.end(), addressLessThanComparison); if (!sortedAddress.isEmpty()) { sortedAddress += QLatin1Char(';'); } sortedAddress += addressListToString(ipV4List, actualEntryMap); } return engine->toScriptValue(sortedAddress); } // getClientVersion // @return the version number of this engine for future extension. We too start // this at version 1.0. QScriptValue GetClientVersion(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount()) { return engine->undefinedValue(); } const QString version(QStringLiteral("1.0")); return engine->toScriptValue(version); } void registerFunctions(QScriptEngine *engine) { QScriptValue value = engine->globalObject(); value.setProperty(QStringLiteral("isPlainHostName"), engine->newFunction(IsPlainHostName)); value.setProperty(QStringLiteral("dnsDomainIs"), engine->newFunction(DNSDomainIs)); value.setProperty(QStringLiteral("localHostOrDomainIs"), engine->newFunction(LocalHostOrDomainIs)); value.setProperty(QStringLiteral("isResolvable"), engine->newFunction(IsResolvable)); value.setProperty(QStringLiteral("isInNet"), engine->newFunction(IsInNet)); value.setProperty(QStringLiteral("dnsResolve"), engine->newFunction(DNSResolve)); value.setProperty(QStringLiteral("myIpAddress"), engine->newFunction(MyIpAddress)); value.setProperty(QStringLiteral("dnsDomainLevels"), engine->newFunction(DNSDomainLevels)); value.setProperty(QStringLiteral("shExpMatch"), engine->newFunction(ShExpMatch)); value.setProperty(QStringLiteral("weekdayRange"), engine->newFunction(WeekdayRange)); value.setProperty(QStringLiteral("dateRange"), engine->newFunction(DateRange)); value.setProperty(QStringLiteral("timeRange"), engine->newFunction(TimeRange)); // Microsoft's IPv6 PAC Extensions... value.setProperty(QStringLiteral("isResolvableEx"), engine->newFunction(IsResolvableEx)); value.setProperty(QStringLiteral("isInNetEx"), engine->newFunction(IsInNetEx)); value.setProperty(QStringLiteral("dnsResolveEx"), engine->newFunction(DNSResolveEx)); value.setProperty(QStringLiteral("myIpAddressEx"), engine->newFunction(MyIpAddressEx)); value.setProperty(QStringLiteral("sortIpAddressList"), engine->newFunction(SortIpAddressList)); value.setProperty(QStringLiteral("getClientVersion"), engine->newFunction(GetClientVersion)); } } namespace KPAC { Script::Script(const QString &code) { m_engine = new QScriptEngine; registerFunctions(m_engine); QScriptProgram program(code); const QScriptValue result = m_engine->evaluate(program); if (m_engine->hasUncaughtException() || result.isError()) { throw Error(m_engine->uncaughtException().toString()); } } Script::~Script() { delete m_engine; } QString Script::evaluate(const QUrl &url) { QScriptValue func = m_engine->globalObject().property(QStringLiteral("FindProxyForURL")); if (!func.isValid()) { func = m_engine->globalObject().property(QStringLiteral("FindProxyForURLEx")); if (!func.isValid()) { throw Error(i18n("Could not find 'FindProxyForURL' or 'FindProxyForURLEx'")); return QString(); } } QUrl cleanUrl = url; cleanUrl.setUserInfo(QString()); if (cleanUrl.scheme() == QLatin1String("https")) { cleanUrl.setPath(QString()); cleanUrl.setQuery(QString()); } QScriptValueList args; args << cleanUrl.url(); args << cleanUrl.host(); QScriptValue result = func.call(QScriptValue(), args); if (result.isError()) { throw Error(i18n("Got an invalid reply when calling %1", func.toString())); } return result.toString(); } } diff --git a/src/kssld/kssld.cpp b/src/kssld/kssld.cpp index 24df30d3..9d3d4738 100644 --- a/src/kssld/kssld.cpp +++ b/src/kssld/kssld.cpp @@ -1,270 +1,269 @@ /* This file is part of the KDE libraries Copyright (c) 2007, 2008, 2010 Andreas Hartmetz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kssld.h" #include "ksslcertificatemanager.h" #include "kssld_adaptor.h" #include #include -#include #include #include #include K_PLUGIN_CLASS_WITH_JSON(KSSLD, "kssld.json") class KSSLDPrivate { public: KSSLDPrivate() : config(QStringLiteral("ksslcertificatemanager"), KConfig::SimpleConfig) { struct strErr { const char *str; KSslError::Error err; }; //hmmm, looks like these are all of the errors where it is possible to continue. const static strErr strError[] = { {"NoError", KSslError::NoError}, {"UnknownError", KSslError::UnknownError}, {"InvalidCertificateAuthority", KSslError::InvalidCertificateAuthorityCertificate}, {"InvalidCertificate", KSslError::InvalidCertificate}, {"CertificateSignatureFailed", KSslError::CertificateSignatureFailed}, {"SelfSignedCertificate", KSslError::SelfSignedCertificate}, {"RevokedCertificate", KSslError::RevokedCertificate}, {"InvalidCertificatePurpose", KSslError::InvalidCertificatePurpose}, {"RejectedCertificate", KSslError::RejectedCertificate}, {"UntrustedCertificate", KSslError::UntrustedCertificate}, {"ExpiredCertificate", KSslError::ExpiredCertificate}, {"HostNameMismatch", KSslError::HostNameMismatch} }; for (int i = 0; i < int(sizeof(strError) / sizeof(strErr)); i++) { QString s = QString::fromLatin1(strError[i].str); KSslError::Error e = strError[i].err; stringToSslError.insert(s, e); sslErrorToString.insert(e, s); } } KConfig config; QHash stringToSslError; QHash sslErrorToString; }; KSSLD::KSSLD(QObject *parent, const QVariantList &) : KDEDModule(parent), d(new KSSLDPrivate()) { new KSSLDAdaptor(this); pruneExpiredRules(); } KSSLD::~KSSLD() { delete d; } void KSSLD::setRule(const KSslCertificateRule &rule) { if (rule.hostName().isEmpty()) { return; } KConfigGroup group = d->config.group(rule.certificate().digest().toHex()); QStringList sl; QString dtString = QStringLiteral("ExpireUTC "); dtString.append(rule.expiryDateTime().toString(Qt::ISODate)); sl.append(dtString); if (rule.isRejected()) { sl.append(QStringLiteral("Reject")); } else { foreach (KSslError::Error e, rule.ignoredErrors()) { sl.append(d->sslErrorToString.value(e)); } } if (!group.hasKey("CertificatePEM")) { group.writeEntry("CertificatePEM", rule.certificate().toPem()); } #ifdef PARANOIA else if (group.readEntry("CertificatePEM") != rule.certificate().toPem()) { return; } #endif group.writeEntry(rule.hostName(), sl); group.sync(); } void KSSLD::clearRule(const KSslCertificateRule &rule) { clearRule(rule.certificate(), rule.hostName()); } void KSSLD::clearRule(const QSslCertificate &cert, const QString &hostName) { KConfigGroup group = d->config.group(cert.digest().toHex()); group.deleteEntry(hostName); if (group.keyList().size() < 2) { group.deleteGroup(); } group.sync(); } void KSSLD::pruneExpiredRules() { // expired rules are deleted when trying to load them, so we just try to load all rules. // be careful about iterating over KConfig(Group) while changing it foreach (const QString &groupName, d->config.groupList()) { QByteArray certDigest = groupName.toLatin1(); foreach (const QString &key, d->config.group(groupName).keyList()) { if (key == QLatin1String("CertificatePEM")) { continue; } KSslCertificateRule r = rule(QSslCertificate(certDigest), key); } } } // check a domain name with subdomains for well-formedness and count the dot-separated parts static QString normalizeSubdomains(const QString &hostName, int *namePartsCount) { QString ret; int partsCount = 0; bool wasPrevDot = true; // -> allow no dot at the beginning and count first name part const int length = hostName.length(); for (int i = 0; i < length; i++) { const QChar c = hostName.at(i); if (c == QLatin1Char('.')) { if (wasPrevDot || (i + 1 == hostName.length())) { // consecutive dots or a dot at the end are forbidden partsCount = 0; ret.clear(); break; } wasPrevDot = true; } else { if (wasPrevDot) { partsCount++; } wasPrevDot = false; } ret.append(c); } *namePartsCount = partsCount; return ret; } KSslCertificateRule KSSLD::rule(const QSslCertificate &cert, const QString &hostName) const { const QByteArray certDigest = cert.digest().toHex(); KConfigGroup group = d->config.group(certDigest); KSslCertificateRule ret(cert, hostName); bool foundHostName = false; int needlePartsCount; QString needle = normalizeSubdomains(hostName, &needlePartsCount); // Find a rule for the hostname, either... if (group.hasKey(needle)) { // directly (host, site.tld, a.site.tld etc) if (needlePartsCount >= 1) { foundHostName = true; } } else { // or with wildcards // "tld" <- "*." and "site.tld" <- "*.tld" are not valid matches, // "a.site.tld" <- "*.site.tld" is while (--needlePartsCount >= 2) { const int dotIndex = needle.indexOf(QLatin1Char('.')); Q_ASSERT(dotIndex > 0); // if this fails normalizeSubdomains() failed needle.remove(0, dotIndex - 1); needle[0] = QChar::fromLatin1('*'); if (group.hasKey(needle)) { foundHostName = true; break; } needle.remove(0, 2); // remove "*." } } if (!foundHostName) { //Don't make a rule with the failed wildcard pattern - use the original hostname. return KSslCertificateRule(cert, hostName); } //parse entry of the format "ExpireUTC , Reject" or //"ExpireUTC , HostNameMismatch, ExpiredCertificate, ..." QStringList sl = group.readEntry(needle, QStringList()); QDateTime expiryDt; // the rule is well-formed if it contains at least the expire date and one directive if (sl.size() >= 2) { QString dtString = sl.takeFirst(); if (dtString.startsWith(QLatin1String("ExpireUTC "))) { dtString.remove(0, 10/* length of "ExpireUTC " */); expiryDt = QDateTime::fromString(dtString, Qt::ISODate); } } if (!expiryDt.isValid() || expiryDt < QDateTime::currentDateTime()) { //the entry is malformed or expired so we remove it group.deleteEntry(needle); //the group is useless once only the CertificatePEM entry left if (group.keyList().size() < 2) { group.deleteGroup(); } return ret; } QList ignoredErrors; bool isRejected = false; for (const QString &s : qAsConst(sl)) { if (s == QLatin1String("Reject")) { isRejected = true; ignoredErrors.clear(); break; } if (!d->stringToSslError.contains(s)) { continue; } ignoredErrors.append(d->stringToSslError.value(s)); } //Everything is checked and we can make ret valid ret.setExpiryDateTime(expiryDt); ret.setRejected(isRejected); ret.setIgnoredErrors(ignoredErrors); return ret; } #include "moc_kssld.cpp" #include "moc_kssld_adaptor.cpp" #include "kssld.moc" diff --git a/src/tests/placesitemmodeltest.cpp b/src/tests/placesitemmodeltest.cpp index 6d86cb93..6efa3972 100644 --- a/src/tests/placesitemmodeltest.cpp +++ b/src/tests/placesitemmodeltest.cpp @@ -1,532 +1,531 @@ /*************************************************************************** * Copyright (C) 2017 by Renato Araujo Oliveira * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include #include #include #include #include #include -#include #include #include #include #include #include #include "panels/places/placesitemmodel.h" #include "panels/places/placesitem.h" #include "views/viewproperties.h" #include "kitemviews/kitemrange.h" Q_DECLARE_METATYPE(KItemRangeList) Q_DECLARE_METATYPE(PlacesItem::GroupType) #ifdef Q_OS_WIN //c:\ as root for windows #define KDE_ROOT_PATH "C:\\" #else #define KDE_ROOT_PATH "/" #endif static QString bookmarksFile() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/user-places.xbel"; } class PlacesItemModelTest : public QObject { Q_OBJECT private slots: void init(); void cleanup(); void initTestCase(); void cleanupTestCase(); void testModelSort(); void testModelMove(); void testGroups(); void testPlaceItem_data(); void testPlaceItem(); void testTearDownDevice(); void testDefaultViewProperties_data(); void testDefaultViewProperties(); void testClear(); void testHideItem(); void testSystemItems(); void testEditBookmark(); void testEditAfterCreation(); void testEditMetadata(); private: PlacesItemModel* m_model; QMap m_interfacesMap; void setBalooEnabled(bool enabled); int indexOf(const QUrl &url); QDBusInterface *fakeManager(); QDBusInterface *fakeDevice(const QString &udi); QStringList placesUrls() const; QStringList initialUrls() const; void createPlaceItem(const QString &text, const QUrl &url, const QString &icon); }; #define CHECK_PLACES_URLS(urls) \ QStringList tmp(urls); \ QStringList places = placesUrls(); \ while(!places.isEmpty()) { \ tmp.removeOne(places.takeFirst()); \ } \ if (!tmp.isEmpty()) { \ qWarning() << "Expected:" << urls; \ qWarning() << "Got:" << places; \ QCOMPARE(places, urls); \ } void PlacesItemModelTest::setBalooEnabled(bool enabled) { KConfig config(QStringLiteral("baloofilerc")); KConfigGroup basicSettings = config.group("Basic Settings"); basicSettings.writeEntry("Indexing-Enabled", enabled); config.sync(); } int PlacesItemModelTest::indexOf(const QUrl &url) { for (int r = 0; r < m_model->count(); r++) { if (m_model->placesItem(r)->url() == url) { return r; } } return -1; } QDBusInterface *PlacesItemModelTest::fakeManager() { return fakeDevice(QStringLiteral("/org/kde/solid/fakehw")); } QDBusInterface *PlacesItemModelTest::fakeDevice(const QString &udi) { if (m_interfacesMap.contains(udi)) { return m_interfacesMap[udi]; } QDBusInterface *iface = new QDBusInterface(QDBusConnection::sessionBus().baseService(), udi); m_interfacesMap[udi] = iface; return iface; } QStringList PlacesItemModelTest::placesUrls() const { QStringList urls; for (int row = 0; row < m_model->count(); ++row) { urls << m_model->placesItem(row)->url().toDisplayString(QUrl::PreferLocalFile); } return urls; } void PlacesItemModelTest::createPlaceItem(const QString &text, const QUrl &url, const QString &icon) { PlacesItem *item = m_model->createPlacesItem(text, url, icon); QSignalSpy itemsInsertedSpy(m_model, &PlacesItemModel::itemsInserted); m_model->appendItemToGroup(item); QTRY_COMPARE(itemsInsertedSpy.count(), 1); } void PlacesItemModelTest::init() { m_model = new PlacesItemModel(); // WORKAROUND: need to wait for bookmark to load, check: PlacesItemModel::updateBookmarks QTest::qWait(300); QCOMPARE(m_model->count(), 17); } void PlacesItemModelTest::cleanup() { delete m_model; m_model = nullptr; } void PlacesItemModelTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); const QString fakeHw = QFINDTESTDATA("fakecomputer.xml"); QVERIFY(!fakeHw.isEmpty()); qputenv("SOLID_FAKEHW", QFile::encodeName(fakeHw)); setBalooEnabled(true); const QString bookmarsFileName = bookmarksFile(); if (QFile::exists(bookmarsFileName)) { // Ensure we'll have a clean bookmark file to start QVERIFY(QFile::remove(bookmarsFileName)); } qRegisterMetaType(); } void PlacesItemModelTest::cleanupTestCase() { qDeleteAll(m_interfacesMap); QFile::remove(bookmarksFile()); } QStringList PlacesItemModelTest::initialUrls() const { QStringList urls; urls << QDir::homePath() << QStringLiteral("remote:/") << QStringLiteral(KDE_ROOT_PATH) << QStringLiteral("trash:/") << QStringLiteral("timeline:/today") << QStringLiteral("timeline:/yesterday") << QStringLiteral("timeline:/thismonth") << QStringLiteral("timeline:/lastmonth") << QStringLiteral("search:/documents") << QStringLiteral("search:/images") << QStringLiteral("search:/audio") << QStringLiteral("search:/videos") << QStringLiteral("/media/cdrom") << QStringLiteral("/foreign") << QStringLiteral("/media/XO-Y4") << QStringLiteral("/media/nfs") << QStringLiteral("/media/floppy0"); return urls; } void PlacesItemModelTest::testModelSort() { CHECK_PLACES_URLS(initialUrls()); } void PlacesItemModelTest::testModelMove() { QStringList urls = initialUrls(); KBookmarkManager *bookmarkManager = KBookmarkManager::managerForFile(bookmarksFile(), QStringLiteral("kfilePlaces")); KBookmarkGroup root = bookmarkManager->root(); KBookmark systemRoot = m_model->placesItem(1)->bookmark(); KBookmark last = m_model->placesItem(m_model->count() - 1)->bookmark(); // try to move the "root" path to the end of the list root.moveBookmark(systemRoot, last); bookmarkManager->emitChanged(root); // make sure that the items still grouped and the "root" item was moved to the end of places group instead urls.move(1, 2); CHECK_PLACES_URLS(urls); } void PlacesItemModelTest::testGroups() { const auto groups = m_model->groups(); QCOMPARE(groups.size(), 4); QCOMPARE(groups.at(0).first, 0); QCOMPARE(groups.at(0).second.toString(), QStringLiteral("Places")); QCOMPARE(groups.at(1).first, 4); QCOMPARE(groups.at(1).second.toString(), QStringLiteral("Recently Saved")); QCOMPARE(groups.at(2).first, 8); QCOMPARE(groups.at(2).second.toString(), QStringLiteral("Search For")); QCOMPARE(groups.at(3).first, 12); QCOMPARE(groups.at(3).second.toString(), QStringLiteral("Devices")); } void PlacesItemModelTest::testPlaceItem_data() { QTest::addColumn("url"); QTest::addColumn("expectedIsHidden"); QTest::addColumn("expectedIsSystemItem"); QTest::addColumn("expectedGroupType"); QTest::addColumn("expectedStorageSetupNeeded"); // places QTest::newRow("Places - Home") << QUrl::fromLocalFile(QDir::homePath()) << false << true << PlacesItem::PlacesType << false; // baloo -search QTest::newRow("Baloo - Documents") << QUrl("search:/documents") << false << true << PlacesItem::SearchForType << false; // baloo - timeline QTest::newRow("Baloo - Last Month") << QUrl("timeline:/lastmonth") << false << true << PlacesItem::RecentlySavedType << false; // devices QTest::newRow("Devices - Floppy") << QUrl("file:///media/floppy0") << false << false << PlacesItem::DevicesType << false; } void PlacesItemModelTest::testPlaceItem() { QFETCH(QUrl, url); QFETCH(bool, expectedIsHidden); QFETCH(bool, expectedIsSystemItem); QFETCH(PlacesItem::GroupType, expectedGroupType); QFETCH(bool, expectedStorageSetupNeeded); const int index = indexOf(url); PlacesItem *item = m_model->placesItem(index); QCOMPARE(item->url(), url); QCOMPARE(item->isHidden(), expectedIsHidden); QCOMPARE(item->isSystemItem(), expectedIsSystemItem); QCOMPARE(item->groupType(), expectedGroupType); QCOMPARE(item->storageSetupNeeded(), expectedStorageSetupNeeded); } void PlacesItemModelTest::testTearDownDevice() { const QUrl mediaUrl = QUrl::fromLocalFile(QStringLiteral("/media/XO-Y4")); int index = indexOf(mediaUrl); QVERIFY(index != -1); auto ejectAction = m_model->ejectAction(index); QVERIFY(!ejectAction); auto teardownAction = m_model->teardownAction(index); QVERIFY(teardownAction); QCOMPARE(m_model->count(), 17); QSignalSpy spyItemsRemoved(m_model, &PlacesItemModel::itemsRemoved); fakeManager()->call(QStringLiteral("unplug"), "/org/kde/solid/fakehw/volume_part1_size_993284096"); QTRY_COMPARE(m_model->count(), 16); QCOMPARE(spyItemsRemoved.count(), 1); const QList spyItemsRemovedArgs = spyItemsRemoved.takeFirst(); const KItemRangeList removedRange = spyItemsRemovedArgs.at(0).value(); QCOMPARE(removedRange.size(), 1); QCOMPARE(removedRange.first().index, index); QCOMPARE(removedRange.first().count, 1); QCOMPARE(indexOf(mediaUrl), -1); QSignalSpy spyItemsInserted(m_model, &PlacesItemModel::itemsInserted); fakeManager()->call(QStringLiteral("plug"), "/org/kde/solid/fakehw/volume_part1_size_993284096"); QTRY_COMPARE(m_model->count(), 17); QCOMPARE(spyItemsInserted.count(), 1); index = indexOf(mediaUrl); const QList args = spyItemsInserted.takeFirst(); const KItemRangeList insertedRange = args.at(0).value(); QCOMPARE(insertedRange.size(), 1); QCOMPARE(insertedRange.first().index, index); QCOMPARE(insertedRange.first().count, 1); } void PlacesItemModelTest::testDefaultViewProperties_data() { QTest::addColumn("url"); QTest::addColumn("expectedViewMode"); QTest::addColumn("expectedPreviewShow"); QTest::addColumn >("expectedVisibleRole"); // places QTest::newRow("Places - Home") << QUrl::fromLocalFile(QDir::homePath()) << DolphinView::IconsView << false << QList({"text"}); // baloo -search QTest::newRow("Baloo - Documents") << QUrl("search:/documents") << DolphinView::DetailsView << false << QList({"text", "path"}); // baloo - timeline QTest::newRow("Baloo - Last Month") << QUrl("timeline:/lastmonth") << DolphinView::DetailsView << false << QList({"text", "modificationtime"}); // devices QTest::newRow("Devices - Floppy") << QUrl("file:///media/floppy0") << DolphinView::IconsView << false << QList({"text"}); } void PlacesItemModelTest::testDefaultViewProperties() { QFETCH(QUrl, url); QFETCH(DolphinView::Mode, expectedViewMode); QFETCH(bool, expectedPreviewShow); QFETCH(QList, expectedVisibleRole); ViewProperties properties(m_model->convertedUrl(url)); QCOMPARE(properties.viewMode(), expectedViewMode); QCOMPARE(properties.previewsShown(), expectedPreviewShow); QCOMPARE(properties.visibleRoles(), expectedVisibleRole); } void PlacesItemModelTest::testClear() { QCOMPARE(m_model->count(), 17); m_model->clear(); QCOMPARE(m_model->count(), 0); QCOMPARE(m_model->hiddenCount(), 0); } void PlacesItemModelTest::testHideItem() { const QUrl mediaUrl = QUrl::fromLocalFile(QStringLiteral("/media/XO-Y4")); const int index = indexOf(mediaUrl); PlacesItem *item = m_model->placesItem(index); QSignalSpy spyItemsRemoved(m_model, &PlacesItemModel::itemsRemoved); QList spyItemsRemovedArgs; KItemRangeList removedRange; QSignalSpy spyItemsInserted(m_model, &PlacesItemModel::itemsInserted); QList spyItemsInsertedArgs; KItemRangeList insertedRange; QVERIFY(item); // hide an item item->setHidden(true); // check if items removed was fired QTRY_COMPARE(m_model->count(), 16); QCOMPARE(spyItemsRemoved.count(), 1); spyItemsRemovedArgs = spyItemsRemoved.takeFirst(); removedRange = spyItemsRemovedArgs.at(0).value(); QCOMPARE(removedRange.size(), 1); QCOMPARE(removedRange.first().index, index); QCOMPARE(removedRange.first().count, 1); // allow model to show hidden items m_model->setHiddenItemsShown(true); // check if the items inserted was fired spyItemsInsertedArgs = spyItemsInserted.takeFirst(); insertedRange = spyItemsInsertedArgs.at(0).value(); QCOMPARE(insertedRange.size(), 1); QCOMPARE(insertedRange.first().index, index); QCOMPARE(insertedRange.first().count, 1); // mark item as visible item = m_model->placesItem(index); item->setHidden(false); // mark model to hide invisible items m_model->setHiddenItemsShown(true); QTRY_COMPARE(m_model->count(), 17); } void PlacesItemModelTest::testSystemItems() { QCOMPARE(m_model->count(), 17); for (int r = 0; r < m_model->count(); r++) { QCOMPARE(m_model->placesItem(r)->isSystemItem(), !m_model->placesItem(r)->device().isValid()); } // create a new entry (non system item) PlacesItem *item = m_model->createPlacesItem(QStringLiteral("Temporary Dir"), QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)), QString()); QSignalSpy itemsInsertedSpy(m_model, &PlacesItemModel::itemsInserted); m_model->appendItemToGroup(item); // check if the new entry was created QTRY_COMPARE(itemsInsertedSpy.count(), 1); QList args = itemsInsertedSpy.takeFirst(); KItemRangeList range = args.at(0).value(); QCOMPARE(range.first().index, 4); QCOMPARE(range.first().count, 1); QVERIFY(!m_model->placesItem(4)->isSystemItem()); QCOMPARE(m_model->count(), 18); // remove new entry QSignalSpy itemsRemovedSpy(m_model, &PlacesItemModel::itemsRemoved); m_model->removeItem(4); m_model->saveBookmarks(); QTRY_COMPARE(itemsRemovedSpy.count(), 1); args = itemsRemovedSpy.takeFirst(); range = args.at(0).value(); QCOMPARE(range.first().index, 4); QCOMPARE(range.first().count, 1); QTRY_COMPARE(m_model->count(), 17); } void PlacesItemModelTest::testEditBookmark() { QScopedPointer other(new PlacesItemModel()); createPlaceItem(QStringLiteral("Temporary Dir"), QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)), QString()); QSignalSpy itemsChangedSply(m_model, &PlacesItemModel::itemsChanged); m_model->item(4)->setText(QStringLiteral("Renamed place")); m_model->saveBookmarks(); QTRY_COMPARE(itemsChangedSply.count(), 1); QList args = itemsChangedSply.takeFirst(); KItemRangeList range = args.at(0).value(); QCOMPARE(range.first().index, 4); QCOMPARE(range.first().count, 1); QSet roles = args.at(1).value >(); QCOMPARE(roles.size(), 1); QCOMPARE(*roles.begin(), QByteArrayLiteral("text")); QCOMPARE(m_model->item(4)->text(), QStringLiteral("Renamed place")); // check if the item was updated in the other model QTRY_COMPARE(other->item(4)->text(), QStringLiteral("Renamed place")); // remove new entry QSignalSpy itemsRemovedSpy(m_model, &PlacesItemModel::itemsRemoved); m_model->removeItem(4); m_model->saveBookmarks(); QTRY_COMPARE(itemsRemovedSpy.count(), 1); args = itemsRemovedSpy.takeFirst(); range = args.at(0).value(); QCOMPARE(range.first().index, 4); QCOMPARE(range.first().count, 1); QTRY_COMPARE(m_model->count(), 17); } void PlacesItemModelTest::testEditAfterCreation() { createPlaceItem(QStringLiteral("Temporary Dir"), QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)), QString()); PlacesItemModel *model = new PlacesItemModel(); QTRY_COMPARE(model->count(), m_model->count()); PlacesItem *item = m_model->placesItem(4); item->setText(QStringLiteral("Renamed place")); m_model->saveBookmarks(); QTRY_COMPARE(model->count(), m_model->count()); QTRY_COMPARE(model->placesItem(4)->text(), m_model->placesItem(4)->text()); QTRY_COMPARE(model->placesItem(4)->bookmark().metaDataItem(QStringLiteral("OnlyInApp")), m_model->placesItem(4)->bookmark().metaDataItem(QStringLiteral("OnlyInApp"))); QTRY_COMPARE(model->placesItem(4)->icon(), m_model->placesItem(4)->icon()); QTRY_COMPARE(model->placesItem(4)->url(), m_model->placesItem(4)->url()); m_model->removeItem(4); m_model->saveBookmarks(); QTRY_COMPARE(model->count(), m_model->count()); } void PlacesItemModelTest::testEditMetadata() { createPlaceItem(QStringLiteral("Temporary Dir"), QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)), QString()); PlacesItemModel *model = new PlacesItemModel(); QTRY_COMPARE(model->count(), m_model->count()); PlacesItem *item = m_model->placesItem(4); item->bookmark().setMetaDataItem(QStringLiteral("OnlyInApp"), KAboutData::applicationData().componentName()); m_model->saveBookmarks(); QTRY_COMPARE(model->count(), m_model->count()); QTRY_COMPARE(model->placesItem(4)->bookmark().metaDataItem(QStringLiteral("OnlyInApp")), KAboutData::applicationData().componentName()); QTRY_COMPARE(model->placesItem(4)->text(), m_model->placesItem(4)->text()); QTRY_COMPARE(model->placesItem(4)->bookmark().metaDataItem(QStringLiteral("OnlyInApp")), m_model->placesItem(4)->bookmark().metaDataItem(QStringLiteral("OnlyInApp"))); QTRY_COMPARE(model->placesItem(4)->icon(), m_model->placesItem(4)->icon()); QTRY_COMPARE(model->placesItem(4)->url(), m_model->placesItem(4)->url()); m_model->removeItem(4); m_model->saveBookmarks(); QTRY_COMPARE(model->count(), m_model->count()); } QTEST_MAIN(PlacesItemModelTest) #include "placesitemmodeltest.moc" diff --git a/src/urifilters/ikws/searchprovider.cpp b/src/urifilters/ikws/searchprovider.cpp index 9fb53d31..b4570b16 100644 --- a/src/urifilters/ikws/searchprovider.cpp +++ b/src/urifilters/ikws/searchprovider.cpp @@ -1,133 +1,132 @@ /* * Copyright (c) 2000 Malte Starostik * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "searchprovider.h" #include #include #include #include // KIO::iconNameForUrl #include -#include #include #include SearchProvider::SearchProvider(const QString &servicePath) : m_dirty(false) { setDesktopEntryName(QFileInfo(servicePath).baseName()); KDesktopFile parser(servicePath); setName(parser.readName()); KConfigGroup group(parser.desktopGroup()); setKeys(group.readEntry(QStringLiteral("Keys"), QStringList())); m_query = group.readEntry(QStringLiteral("Query")); m_charset = group.readEntry(QStringLiteral("Charset")); m_iconName = group.readEntry(QStringLiteral("Icon")); } SearchProvider::~SearchProvider() { } void SearchProvider::setName(const QString &name) { if (KUriFilterSearchProvider::name() == name) return; KUriFilterSearchProvider::setName(name); } void SearchProvider::setQuery(const QString &query) { if (m_query == query) return; m_query = query; } void SearchProvider::setKeys(const QStringList &keys) { if (KUriFilterSearchProvider::keys() == keys) return; KUriFilterSearchProvider::setKeys(keys); QString name = desktopEntryName(); if (!name.isEmpty()) return; // New provider. Set the desktopEntryName. // Take the longest search shortcut as filename, // if such a file already exists, append a number and increase it // until the name is unique for (const QString& key : keys) { if (key.length() > name.length()) name = key.toLower(); } const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/searchproviders/"); bool firstRun = true; while (true) { QString check(name); if (!firstRun) check += KRandom::randomString(4); const QString located = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/searchproviders/") + check + QLatin1String(".desktop")); if (located.isEmpty()) { name = check; break; } else if (located.startsWith(path)) { // If it's a deleted (hidden) entry, overwrite it if (KService(located).isDeleted()) break; } firstRun = false; } setDesktopEntryName(name); } void SearchProvider::setCharset(const QString &charset) { if (m_charset == charset) return; m_charset = charset; } QString SearchProvider::iconName() const { if (!m_iconName.isEmpty()) { return m_iconName; } return KIO::iconNameForUrl(QUrl(m_query)); } void SearchProvider::setDirty(bool dirty) { m_dirty = dirty; } diff --git a/src/urifilters/ikws/searchproviderregistry.cpp b/src/urifilters/ikws/searchproviderregistry.cpp index bf62edf3..1c8cc24e 100644 --- a/src/urifilters/ikws/searchproviderregistry.cpp +++ b/src/urifilters/ikws/searchproviderregistry.cpp @@ -1,86 +1,85 @@ /* * This file is part of the KDE project * Copyright 2017 David Faure * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #include "searchproviderregistry.h" #include "searchprovider.h" #include #include -#include SearchProviderRegistry::SearchProviderRegistry() { reload(); } SearchProviderRegistry::~SearchProviderRegistry() { qDeleteAll(m_searchProviders); } QStringList SearchProviderRegistry::directories() const { const QString testDir = QFile::decodeName(qgetenv("KIO_SEARCHPROVIDERS_DIR")); // for unittests if (!testDir.isEmpty()) return { testDir }; return QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kservices5/searchproviders/"), QStandardPaths::LocateDirectory); } void SearchProviderRegistry::reload() { m_searchProvidersByKey.clear(); m_searchProvidersByDesktopName.clear(); qDeleteAll(m_searchProviders); m_searchProviders.clear(); const QStringList servicesDirs = directories(); for (const QString &dirPath : servicesDirs) { QDir dir(dirPath); const auto files = dir.entryList({QStringLiteral("*.desktop")}, QDir::Files); for (const QString &file : files) { if (!m_searchProvidersByDesktopName.contains(file)) { const QString filePath = dir.path() + QLatin1Char('/') + file; auto *provider = new SearchProvider(filePath); m_searchProvidersByDesktopName.insert(file, provider); m_searchProviders.append(provider); const auto keys = provider->keys(); for (const QString &key : keys) { m_searchProvidersByKey.insert(key, provider); } } } } } QList SearchProviderRegistry::findAll() { return m_searchProviders; } SearchProvider* SearchProviderRegistry::findByKey(const QString& key) const { return m_searchProvidersByKey.value(key); } SearchProvider* SearchProviderRegistry::findByDesktopName(const QString &name) const { return m_searchProvidersByDesktopName.value(name + QLatin1String(".desktop")); } diff --git a/src/urifilters/localdomain/localdomainurifilter.cpp b/src/urifilters/localdomain/localdomainurifilter.cpp index 2eb88928..3685ac33 100644 --- a/src/urifilters/localdomain/localdomainurifilter.cpp +++ b/src/urifilters/localdomain/localdomainurifilter.cpp @@ -1,89 +1,88 @@ /* localdomainfilter.cpp This file is part of the KDE project Copyright (C) 2002 Lubos Lunak Copyright (C) 2010 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, see . */ #include "localdomainurifilter.h" #include #include -#include #include #include #define QL1C(x) QLatin1Char(x) #define QL1S(x) QLatin1String(x) #define HOSTPORT_PATTERN "[a-zA-Z0-9][a-zA-Z0-9+-]*(?:\\:[0-9]{1,5})?(?:/[\\w:@&=+$,-.!~*'()]*)*" namespace { QLoggingCategory category("org.kde.kurifilter-localdomain", QtWarningMsg); } /** * IMPORTANT: If you change anything here, make sure you run the kurifiltertest * regression test (this should be included as part of "make test"). */ LocalDomainUriFilter::LocalDomainUriFilter( QObject *parent, const QVariantList & /*args*/ ) :KUriFilterPlugin( QStringLiteral("localdomainurifilter"), parent ), m_hostPortPattern(QL1S(HOSTPORT_PATTERN)) { } bool LocalDomainUriFilter::filterUri(KUriFilterData& data) const { const QUrl url = data.uri(); const QString protocol = url.scheme(); // When checking for local domain just validate it is indeed a local domain, // but do not modify the hostname! See bug# if ((protocol.isEmpty() || !KProtocolInfo::isKnownProtocol(protocol)) && m_hostPortPattern.exactMatch(data.typedString())) { QString host (data.typedString().left(data.typedString().indexOf(QL1C('/')))); const int pos = host.indexOf(QL1C(':')); if (pos > -1) host.truncate(pos); // Remove port number if (exists(host)) { qCDebug(category) << "QHostInfo found a host called" << host; QString scheme (data.defaultUrlScheme()); if (scheme.isEmpty()) scheme = QStringLiteral("http://"); setFilteredUri(data, QUrl(scheme + data.typedString())); setUriType(data, KUriFilterData::NetProtocol); return true; } } return false; } bool LocalDomainUriFilter::exists(const QString& host) const { qCDebug(category) << "Checking if a host called" << host << "exists"; QHostInfo hostInfo = resolveName (host, 1500); return (hostInfo.error() == QHostInfo::NoError); } K_PLUGIN_CLASS_WITH_JSON(LocalDomainUriFilter, "localdomainurifilter.json") #include "localdomainurifilter.moc" diff --git a/src/widgets/accessmanager.cpp b/src/widgets/accessmanager.cpp index a3b7ed93..703858bd 100644 --- a/src/widgets/accessmanager.cpp +++ b/src/widgets/accessmanager.cpp @@ -1,571 +1,570 @@ /* * This file is part of the KDE project. * * Copyright (C) 2009 - 2012 Dawit Alemayehu * Copyright (C) 2008 - 2009 Urs Wolfer * Copyright (C) 2007 Trolltech ASA * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "accessmanager.h" #include "accessmanagerreply_p.h" #include "job.h" #include "kjobwidgets.h" #include "scheduler.h" #include "kio_widgets_debug.h" #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #define QL1S(x) QLatin1String(x) #define QL1C(x) QLatin1Char(x) static const QNetworkRequest::Attribute gSynchronousNetworkRequestAttribute = QNetworkRequest::SynchronousRequestAttribute; static qint64 sizeFromRequest(const QNetworkRequest &req) { const QVariant size = req.header(QNetworkRequest::ContentLengthHeader); if (!size.isValid()) { return -1; } bool ok = false; const qlonglong value = size.toLongLong(&ok); return (ok ? value : -1); } namespace KIO { class Q_DECL_HIDDEN AccessManager::AccessManagerPrivate { public: AccessManagerPrivate() : externalContentAllowed(true), emitReadyReadOnMetaDataChange(false), window(nullptr) {} void setMetaDataForRequest(QNetworkRequest request, KIO::MetaData &metaData); bool externalContentAllowed; bool emitReadyReadOnMetaDataChange; KIO::MetaData requestMetaData; KIO::MetaData sessionMetaData; QPointer window; }; namespace Integration { class Q_DECL_HIDDEN CookieJar::CookieJarPrivate { public: CookieJarPrivate() : windowId((WId) - 1), isEnabled(true), isStorageDisabled(false) {} WId windowId; bool isEnabled; bool isStorageDisabled; }; } } using namespace KIO; AccessManager::AccessManager(QObject *parent) : QNetworkAccessManager(parent), d(new AccessManager::AccessManagerPrivate()) { // KDE Cookiejar (KCookieJar) integration... setCookieJar(new KIO::Integration::CookieJar); } AccessManager::~AccessManager() { delete d; } void AccessManager::setExternalContentAllowed(bool allowed) { d->externalContentAllowed = allowed; } bool AccessManager::isExternalContentAllowed() const { return d->externalContentAllowed; } #ifndef KIOWIDGETS_NO_DEPRECATED void AccessManager::setCookieJarWindowId(WId id) { QWidget *window = QWidget::find(id); if (!window) { return; } KIO::Integration::CookieJar *jar = qobject_cast (cookieJar()); if (jar) { jar->setWindowId(id); } d->window = window->isWindow() ? window : window->window(); } #endif void AccessManager::setWindow(QWidget *widget) { if (!widget) { return; } d->window = widget->isWindow() ? widget : widget->window(); if (!d->window) { return; } KIO::Integration::CookieJar *jar = qobject_cast (cookieJar()); if (jar) { jar->setWindowId(d->window->winId()); } } #ifndef KIOWIDGETS_NO_DEPRECATED WId AccessManager::cookieJarWindowid() const { KIO::Integration::CookieJar *jar = qobject_cast (cookieJar()); if (jar) { return jar->windowId(); } return 0; } #endif QWidget *AccessManager::window() const { return d->window; } KIO::MetaData &AccessManager::requestMetaData() { return d->requestMetaData; } KIO::MetaData &AccessManager::sessionMetaData() { return d->sessionMetaData; } void AccessManager::putReplyOnHold(QNetworkReply *reply) { KDEPrivate::AccessManagerReply *r = qobject_cast(reply); if (!r) { return; } r->putOnHold(); } void AccessManager::setEmitReadyReadOnMetaDataChange(bool enable) { d->emitReadyReadOnMetaDataChange = enable; } QNetworkReply *AccessManager::createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData) { const QUrl reqUrl(req.url()); if (!d->externalContentAllowed && !KDEPrivate::AccessManagerReply::isLocalRequest(reqUrl) && reqUrl.scheme() != QL1S("data")) { //qDebug() << "Blocked: " << reqUrl; return new KDEPrivate::AccessManagerReply(op, req, QNetworkReply::ContentAccessDenied, i18n("Blocked request."), this); } // Check if the internal ignore content disposition header is set. const bool ignoreContentDisposition = req.hasRawHeader("x-kdewebkit-ignore-disposition"); // Retrieve the KIO meta data... KIO::MetaData metaData; d->setMetaDataForRequest(req, metaData); KIO::SimpleJob *kioJob = nullptr; switch (op) { case HeadOperation: { //qDebug() << "HeadOperation:" << reqUrl; kioJob = KIO::mimetype(reqUrl, KIO::HideProgressInfo); break; } case GetOperation: { //qDebug() << "GetOperation:" << reqUrl; if (!reqUrl.path().isEmpty() || reqUrl.host().isEmpty()) { kioJob = KIO::storedGet(reqUrl, KIO::NoReload, KIO::HideProgressInfo); } else { kioJob = KIO::stat(reqUrl, KIO::HideProgressInfo); } // WORKAROUND: Avoid the brain damaged stuff QtWebKit does when a POST // operation is redirected! See BR# 268694. metaData.remove(QStringLiteral("content-type")); // Remove the content-type from a GET/HEAD request! break; } case PutOperation: { //qDebug() << "PutOperation:" << reqUrl; if (outgoingData) { Q_ASSERT(outgoingData->isReadable()); StoredTransferJob* storedJob = KIO::storedPut(outgoingData, reqUrl, -1, KIO::HideProgressInfo); storedJob->setAsyncDataEnabled(outgoingData->isSequential()); QVariant len = req.header(QNetworkRequest::ContentLengthHeader); if (len.isValid()) { storedJob->setTotalSize(len.toInt()); } kioJob = storedJob; } else { kioJob = KIO::put(reqUrl, -1, KIO::HideProgressInfo); } break; } case PostOperation: { kioJob = KIO::storedHttpPost(outgoingData, reqUrl, sizeFromRequest(req), KIO::HideProgressInfo); if (!metaData.contains(QStringLiteral("content-type"))) { const QVariant header = req.header(QNetworkRequest::ContentTypeHeader); if (header.isValid()) { metaData.insert(QStringLiteral("content-type"), (QStringLiteral("Content-Type: ") + header.toString())); } else { metaData.insert(QStringLiteral("content-type"), QStringLiteral("Content-Type: application/x-www-form-urlencoded")); } } break; } case DeleteOperation: { //qDebug() << "DeleteOperation:" << reqUrl; kioJob = KIO::http_delete(reqUrl, KIO::HideProgressInfo); break; } case CustomOperation: { const QByteArray &method = req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray(); //qDebug() << "CustomOperation:" << reqUrl << "method:" << method << "outgoing data:" << outgoingData; if (method.isEmpty()) { return new KDEPrivate::AccessManagerReply(op, req, QNetworkReply::ProtocolUnknownError, i18n("Unknown HTTP verb."), this); } const qint64 size = sizeFromRequest(req); if (size > 0) { kioJob = KIO::http_post(reqUrl, outgoingData, size, KIO::HideProgressInfo); } else { kioJob = KIO::get(reqUrl, KIO::NoReload, KIO::HideProgressInfo); } metaData.insert(QStringLiteral("CustomHTTPMethod"), QString::fromUtf8(method)); break; } default: { qCWarning(KIO_WIDGETS) << "Unsupported KIO operation requested! Deferring to QNetworkAccessManager..."; return QNetworkAccessManager::createRequest(op, req, outgoingData); } } // Set the job priority switch (req.priority()) { case QNetworkRequest::HighPriority: KIO::Scheduler::setJobPriority(kioJob, -5); break; case QNetworkRequest::LowPriority: KIO::Scheduler::setJobPriority(kioJob, 5); break; default: break; } KDEPrivate::AccessManagerReply *reply; /* NOTE: Here we attempt to handle synchronous XHR requests. Unfortunately, due to the fact that QNAM is both synchronous and multi-thread while KIO is completely the opposite (asynchronous and not thread safe), the code below might cause crashes like the one reported in bug# 287778 (nested event loops are inherently dangerous). Unfortunately, all attempts to address the crash has so far failed due to the many regressions they caused, e.g. bug# 231932 and 297954. Hence, until a solution is found, we have to live with the side effects of creating nested event loops. */ if (req.attribute(gSynchronousNetworkRequestAttribute).toBool()) { KJobWidgets::setWindow(kioJob, d->window); kioJob->setRedirectionHandlingEnabled(true); if (kioJob->exec()) { QByteArray data; if (StoredTransferJob *storedJob = qobject_cast< KIO::StoredTransferJob * >(kioJob)) { data = storedJob->data(); } reply = new KDEPrivate::AccessManagerReply(op, req, data, kioJob->url(), kioJob->metaData(), this); //qDebug() << "Synchronous XHR:" << reply << reqUrl; } else { qCWarning(KIO_WIDGETS) << "Failed to create a synchronous XHR for" << reqUrl; qCWarning(KIO_WIDGETS) << "REASON:" << kioJob->errorString(); reply = new KDEPrivate::AccessManagerReply(op, req, QNetworkReply::UnknownNetworkError, kioJob->errorText(), this); } } else { // Set the window on the KIO ui delegate if (d->window) { KJobWidgets::setWindow(kioJob, d->window); } // Disable internal automatic redirection handling kioJob->setRedirectionHandlingEnabled(false); // Set the job priority switch (req.priority()) { case QNetworkRequest::HighPriority: KIO::Scheduler::setJobPriority(kioJob, -5); break; case QNetworkRequest::LowPriority: KIO::Scheduler::setJobPriority(kioJob, 5); break; default: break; } // Set the meta data for this job... kioJob->setMetaData(metaData); // Create the reply... reply = new KDEPrivate::AccessManagerReply(op, req, kioJob, d->emitReadyReadOnMetaDataChange, this); //qDebug() << reply << reqUrl; } if (ignoreContentDisposition && reply) { //qDebug() << "Content-Disposition WILL BE IGNORED!"; reply->setIgnoreContentDisposition(ignoreContentDisposition); } return reply; } static inline void moveMetaData(KIO::MetaData &metaData, const QString &metaDataKey, QNetworkRequest &request, const QByteArray &requestKey) { if (request.hasRawHeader(requestKey)) { metaData.insert(metaDataKey, QString::fromUtf8(request.rawHeader(requestKey))); request.setRawHeader(requestKey, QByteArray()); } } void AccessManager::AccessManagerPrivate::setMetaDataForRequest(QNetworkRequest request, KIO::MetaData &metaData) { // Add any meta data specified within request... QVariant userMetaData = request.attribute(static_cast(MetaData)); if (userMetaData.isValid() && userMetaData.type() == QVariant::Map) { metaData += userMetaData.toMap(); } metaData.insert(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true")); moveMetaData(metaData, QStringLiteral("UserAgent"), request, QByteArrayLiteral("User-Agent")); moveMetaData(metaData, QStringLiteral("accept"), request, QByteArrayLiteral("Accept")); moveMetaData(metaData, QStringLiteral("Charsets"), request, QByteArrayLiteral("Accept-Charset")); moveMetaData(metaData, QStringLiteral("Languages"), request, QByteArrayLiteral("Accept-Language")); moveMetaData(metaData, QStringLiteral("referrer"), request, QByteArrayLiteral("Referer")); //Don't try to correct spelling! moveMetaData(metaData, QStringLiteral("content-type"), request, QByteArrayLiteral("Content-Type")); if (request.attribute(QNetworkRequest::AuthenticationReuseAttribute) == QNetworkRequest::Manual) { metaData.insert(QStringLiteral("no-preemptive-auth-reuse"), QStringLiteral("true")); } request.setRawHeader("Content-Length", QByteArray()); request.setRawHeader("Connection", QByteArray()); request.setRawHeader("If-None-Match", QByteArray()); request.setRawHeader("If-Modified-Since", QByteArray()); request.setRawHeader("x-kdewebkit-ignore-disposition", QByteArray()); QStringList customHeaders; Q_FOREACH (const QByteArray &key, request.rawHeaderList()) { const QByteArray value = request.rawHeader(key); if (value.length()) { customHeaders << (QString::fromUtf8(key) + QLatin1String(": ") + QString::fromUtf8(value)); } } if (!customHeaders.isEmpty()) { metaData.insert(QStringLiteral("customHTTPHeader"), customHeaders.join(QStringLiteral("\r\n"))); } // Append per request meta data, if any... if (!requestMetaData.isEmpty()) { metaData += requestMetaData; // Clear per request meta data... requestMetaData.clear(); } // Append per session meta data, if any... if (!sessionMetaData.isEmpty()) { metaData += sessionMetaData; } } using namespace KIO::Integration; static QSsl::SslProtocol qSslProtocolFromString(const QString &str) { if (str.compare(QStringLiteral("SSLv3"), Qt::CaseInsensitive) == 0) { return QSsl::SslV3; } if (str.compare(QStringLiteral("SSLv2"), Qt::CaseInsensitive) == 0) { return QSsl::SslV2; } if (str.compare(QStringLiteral("TLSv1"), Qt::CaseInsensitive) == 0) { return QSsl::TlsV1_0; } return QSsl::AnyProtocol; } bool KIO::Integration::sslConfigFromMetaData(const KIO::MetaData &metadata, QSslConfiguration &sslconfig) { bool success = false; if (metadata.value(QStringLiteral("ssl_in_use")) == QStringLiteral("TRUE")) { const QSsl::SslProtocol sslProto = qSslProtocolFromString(metadata.value(QStringLiteral("ssl_protocol_version"))); QList cipherList; cipherList << QSslCipher(metadata.value(QStringLiteral("ssl_cipher_name")), sslProto); sslconfig.setCaCertificates(QSslCertificate::fromData(metadata.value(QStringLiteral("ssl_peer_chain")).toUtf8())); sslconfig.setCiphers(cipherList); sslconfig.setProtocol(sslProto); success = sslconfig.isNull(); } return success; } CookieJar::CookieJar(QObject *parent) : QNetworkCookieJar(parent), d(new CookieJar::CookieJarPrivate) { reparseConfiguration(); } CookieJar::~CookieJar() { delete d; } WId CookieJar::windowId() const { return d->windowId; } bool CookieJar::isCookieStorageDisabled() const { return d->isStorageDisabled; } QList CookieJar::cookiesForUrl(const QUrl &url) const { QList cookieList; if (!d->isEnabled) { return cookieList; } QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); QDBusReply reply = kcookiejar.call(QStringLiteral("findDOMCookies"), url.toString(QUrl::RemoveUserInfo), (qlonglong)d->windowId); if (!reply.isValid()) { qCWarning(KIO_WIDGETS) << "Unable to communicate with the cookiejar!"; return cookieList; } const QString cookieStr = reply.value(); const QStringList cookies = cookieStr.split(QStringLiteral("; "), QString::SkipEmptyParts); for (const QString &cookie : cookies) { const int index = cookie.indexOf(QL1C('=')); const QStringRef name = cookie.leftRef(index); const QStringRef value = cookie.rightRef((cookie.length() - index - 1)); cookieList << QNetworkCookie(name.toUtf8(), value.toUtf8()); //qDebug() << "cookie: name=" << name << ", value=" << value; } return cookieList; } bool CookieJar::setCookiesFromUrl(const QList &cookieList, const QUrl &url) { if (!d->isEnabled) { return false; } QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); for (const QNetworkCookie &cookie : cookieList) { QByteArray cookieHeader("Set-Cookie: "); if (d->isStorageDisabled && !cookie.isSessionCookie()) { QNetworkCookie sessionCookie(cookie); sessionCookie.setExpirationDate(QDateTime()); cookieHeader += sessionCookie.toRawForm(); } else { cookieHeader += cookie.toRawForm(); } kcookiejar.call(QStringLiteral("addCookies"), url.toString(QUrl::RemoveUserInfo), cookieHeader, (qlonglong)d->windowId); //qDebug() << "[" << d->windowId << "]" << cookieHeader << " from " << url; } return !kcookiejar.lastError().isValid(); } void CookieJar::setDisableCookieStorage(bool disable) { d->isStorageDisabled = disable; } void CookieJar::setWindowId(WId id) { d->windowId = id; } void CookieJar::reparseConfiguration() { KConfigGroup cfg = KSharedConfig::openConfig(QStringLiteral("kcookiejarrc"), KConfig::NoGlobals)->group("Cookie Policy"); d->isEnabled = cfg.readEntry("Cookies", true); } diff --git a/src/widgets/delegateanimationhandler.cpp b/src/widgets/delegateanimationhandler.cpp index f01a8b8f..9abf20b9 100644 --- a/src/widgets/delegateanimationhandler.cpp +++ b/src/widgets/delegateanimationhandler.cpp @@ -1,443 +1,442 @@ /* 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. */ #include "delegateanimationhandler_p.h" -#include #include #include #include #include #include #include "kdirmodel.h" #include #include "moc_delegateanimationhandler_p.cpp" namespace KIO { // Needed because state() is a protected method class ProtectedAccessor : public QAbstractItemView { Q_OBJECT public: bool draggingState() const { return state() == DraggingState; } }; // Debug output is disabled by default, use kdebugdialog to enable it //static int animationDebugArea() { static int s_area = KDebug::registerArea("kio (delegateanimationhandler)", false); // return s_area; } // --------------------------------------------------------------------------- CachedRendering::CachedRendering(QStyle::State state, const QSize &size, const QModelIndex &index, qreal devicePixelRatio) : state(state), regular(QPixmap(size*devicePixelRatio)), hover(QPixmap(size*devicePixelRatio)), valid(true), validityIndex(index) { regular.setDevicePixelRatio(devicePixelRatio); hover.setDevicePixelRatio(devicePixelRatio); regular.fill(Qt::transparent); hover.fill(Qt::transparent); if (index.model()) { connect(index.model(), &QAbstractItemModel::dataChanged, this, &CachedRendering::dataChanged); connect(index.model(), &QAbstractItemModel::modelReset, this, &CachedRendering::modelReset); } } void CachedRendering::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { if (validityIndex.row() >= topLeft.row() && validityIndex.column() >= topLeft.column() && validityIndex.row() <= bottomRight.row() && validityIndex.column() <= bottomRight.column()) { valid = false; } } void CachedRendering::modelReset() { valid = false; } // --------------------------------------------------------------------------- AnimationState::AnimationState(const QModelIndex &index) : index(index), direction(QTimeLine::Forward), animating(false), jobAnimation(false), progress(0.0), m_fadeProgress(1.0), m_jobAnimationAngle(0.0), renderCache(nullptr), fadeFromRenderCache(nullptr) { creationTime.start(); } AnimationState::~AnimationState() { delete renderCache; delete fadeFromRenderCache; } bool AnimationState::update() { const qreal runtime = (direction == QTimeLine::Forward ? 150 : 250); // milliseconds const qreal increment = 1000. / runtime / 1000.; const qreal delta = increment * time.restart(); if (direction == QTimeLine::Forward) { progress = qMin(qreal(1.0), progress + delta); animating = (progress < 1.0); } else { progress = qMax(qreal(0.0), progress - delta); animating = (progress > 0.0); } if (fadeFromRenderCache) { //Icon fading goes always forwards m_fadeProgress = qMin(qreal(1.0), m_fadeProgress + delta); animating |= (m_fadeProgress < 1.0); if (m_fadeProgress == 1) { setCachedRenderingFadeFrom(nullptr); } } if (jobAnimation) { m_jobAnimationAngle += 1.0; if (m_jobAnimationAngle == 360) { m_jobAnimationAngle = 0; } if (index.model()->data(index, KDirModel::HasJobRole).toBool()) { animating = true; //there is a job here still... return false; } else { animating = false; //there's no job here anymore, return true so we stop painting this. return true; } } else { return !animating; } } qreal AnimationState::hoverProgress() const { #ifndef M_PI_2 #define M_PI_2 1.57079632679489661923 #endif return qRound(255.0 * std::sin(progress * M_PI_2)) / 255.0; } qreal AnimationState::fadeProgress() const { return qRound(255.0 * std::sin(m_fadeProgress * M_PI_2)) / 255.0; } qreal AnimationState::jobAnimationAngle() const { return m_jobAnimationAngle; } bool AnimationState::hasJobAnimation() const { return jobAnimation; } void AnimationState::setJobAnimation(bool value) { jobAnimation = value; } // --------------------------------------------------------------------------- static const int switchIconInterval = 1000; ///@todo Eventually configurable interval? DelegateAnimationHandler::DelegateAnimationHandler(QObject *parent) : QObject(parent) { iconSequenceTimer.setSingleShot(true); iconSequenceTimer.setInterval(switchIconInterval); connect(&iconSequenceTimer, &QTimer::timeout, this, &DelegateAnimationHandler::sequenceTimerTimeout);; } DelegateAnimationHandler::~DelegateAnimationHandler() { timer.stop(); QMapIterator i(animationLists); while (i.hasNext()) { i.next(); qDeleteAll(*i.value()); delete i.value(); } animationLists.clear(); } void DelegateAnimationHandler::sequenceTimerTimeout() { QAbstractItemModel *model = const_cast(sequenceModelIndex.model()); QAbstractProxyModel *proxy = qobject_cast(model); QModelIndex index = sequenceModelIndex; if (proxy) { index = proxy->mapToSource(index); model = proxy->sourceModel(); } KDirModel *dirModel = dynamic_cast(model); if (dirModel) { //qDebug() << "requesting" << currentSequenceIndex; dirModel->requestSequenceIcon(index, currentSequenceIndex); iconSequenceTimer.start(); // Some upper-bound interval is needed, in case items are not generated } } void DelegateAnimationHandler::gotNewIcon(const QModelIndex &index) { Q_UNUSED(index); //qDebug() << currentSequenceIndex; if (sequenceModelIndex.isValid() && currentSequenceIndex) { iconSequenceTimer.start(); } // if(index ==sequenceModelIndex) //Leads to problems ++currentSequenceIndex; } void DelegateAnimationHandler::setSequenceIndex(int sequenceIndex) { //qDebug() << sequenceIndex; if (sequenceIndex > 0) { currentSequenceIndex = sequenceIndex; iconSequenceTimer.start(); } else { currentSequenceIndex = 0; sequenceTimerTimeout(); //Set the icon back to the standard one currentSequenceIndex = 0; //currentSequenceIndex was incremented, set it back to 0 iconSequenceTimer.stop(); } } void DelegateAnimationHandler::eventuallyStartIteration(const QModelIndex &index) { // if (KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects) { ///Think about it. if (sequenceModelIndex.isValid()) { setSequenceIndex(0); // Stop old iteration, and reset the icon for the old iteration } // Start sequence iteration sequenceModelIndex = index; setSequenceIndex(1); // } } AnimationState *DelegateAnimationHandler::animationState(const QStyleOption &option, const QModelIndex &index, const QAbstractItemView *view) { // We can't do animations reliably when an item is being dragged, since that // item will be drawn in two locations at the same time and hovered in one and // not the other. We can't tell them apart because they both have the same index. if (!view || static_cast(view)->draggingState()) { return nullptr; } AnimationState *state = findAnimationState(view, index); bool hover = option.state & QStyle::State_MouseOver; // If the cursor has entered an item if (!state && hover) { state = new AnimationState(index); addAnimationState(state, view); if (!fadeInAddTime.isValid() || (fadeInAddTime.isValid() && fadeInAddTime.elapsed() > 300)) { startAnimation(state); } else { state->animating = false; state->progress = 1.0; state->direction = QTimeLine::Forward; } fadeInAddTime.restart(); eventuallyStartIteration(index); } else if (state) { // If the cursor has exited an item if (!hover && (!state->animating || state->direction == QTimeLine::Forward)) { state->direction = QTimeLine::Backward; if (state->creationTime.elapsed() < 200) { state->progress = 0.0; } startAnimation(state); // Stop sequence iteration if (index == sequenceModelIndex) { setSequenceIndex(0); sequenceModelIndex = QPersistentModelIndex(); } } else if (hover && state->direction == QTimeLine::Backward) { // This is needed to handle the case where an item is dragged within // the view, and dropped in a different location. State_MouseOver will // initially not be set causing a "hover out" animation to start. // This reverses the direction as soon as we see the bit being set. state->direction = QTimeLine::Forward; if (!state->animating) { startAnimation(state); } eventuallyStartIteration(index); } } else if (!state && index.model()->data(index, KDirModel::HasJobRole).toBool()) { state = new AnimationState(index); addAnimationState(state, view); startAnimation(state); state->setJobAnimation(true); } return state; } AnimationState *DelegateAnimationHandler::findAnimationState(const QAbstractItemView *view, const QModelIndex &index) const { // Try to find a list of animation states for the view AnimationList *list = animationLists.value(view); if (list) { foreach (AnimationState *state, *list) if (state->index == index) { return state; } } return nullptr; } void DelegateAnimationHandler::addAnimationState(AnimationState *state, const QAbstractItemView *view) { AnimationList *list = animationLists.value(view); // If this is the first time we've seen this view if (!list) { connect(view, &QObject::destroyed, this, &DelegateAnimationHandler::viewDeleted); list = new AnimationList; animationLists.insert(view, list); } list->append(state); } void DelegateAnimationHandler::restartAnimation(AnimationState *state) { startAnimation(state); } void DelegateAnimationHandler::startAnimation(AnimationState *state) { state->time.start(); state->animating = true; if (!timer.isActive()) { timer.start(1000 / 30, this); // 30 fps } } int DelegateAnimationHandler::runAnimations(AnimationList *list, const QAbstractItemView *view) { int activeAnimations = 0; QRegion region; QMutableLinkedListIterator i(*list); while (i.hasNext()) { AnimationState *state = i.next(); if (!state->animating) { continue; } // We need to make sure the index is still valid, since it could be removed // while the animation is running. if (state->index.isValid()) { bool finished = state->update(); region += view->visualRect(state->index); if (!finished) { activeAnimations++; continue; } } // If the direction is Forward, the state object needs to stick around // after the animation has finished, so we know that we've already done // a "hover in" for the index. if (state->direction == QTimeLine::Backward || !state->index.isValid()) { delete state; i.remove(); } } // Trigger a repaint of the animated indexes if (!region.isEmpty()) { const_cast(view)->viewport()->update(region); } return activeAnimations; } void DelegateAnimationHandler::viewDeleted(QObject *view) { AnimationList *list = animationLists.take(static_cast(view)); qDeleteAll(*list); delete list; } void DelegateAnimationHandler::timerEvent(QTimerEvent *) { int activeAnimations = 0; AnimationListsIterator i(animationLists); while (i.hasNext()) { i.next(); AnimationList *list = i.value(); const QAbstractItemView *view = i.key(); activeAnimations += runAnimations(list, view); } if (activeAnimations == 0 && timer.isActive()) { timer.stop(); } } } #include "delegateanimationhandler.moc" diff --git a/src/widgets/dropjob.cpp b/src/widgets/dropjob.cpp index efe89fa1..dbc7aeed 100644 --- a/src/widgets/dropjob.cpp +++ b/src/widgets/dropjob.cpp @@ -1,579 +1,577 @@ /* This file is part of the KDE libraries Copyright (C) 2014 David Faure 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) version 3 or, at the discretion of KDE e.V. (which shall act as a proxy as in section 14 of the GPLv3), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser 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 "dropjob.h" #include "job_p.h" #include "pastejob.h" #include "pastejob_p.h" #include "jobuidelegate.h" #include "jobuidelegateextension.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 using namespace KIO; Q_DECLARE_METATYPE(Qt::DropAction) namespace KIO { class DropMenu; } class KIO::DropMenu : public QMenu { Q_OBJECT public: explicit DropMenu(QWidget *parent = nullptr); ~DropMenu(); void addCancelAction(); void addExtraActions(const QList &appActions, const QList &pluginActions); private: QList m_appActions; QList m_pluginActions; QAction *m_lastSeparator; QAction *m_extraActionsSeparator; QAction *m_cancelAction; }; class KIO::DropJobPrivate : public KIO::JobPrivate { public: DropJobPrivate(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags) : JobPrivate(), // Extract everything from the dropevent, since it will be deleted before the job starts m_mimeData(dropEvent->mimeData()), m_urls(KUrlMimeData::urlsFromMimeData(m_mimeData, KUrlMimeData::PreferLocalUrls, &m_metaData)), m_dropAction(dropEvent->dropAction()), m_relativePos(dropEvent->pos()), m_keyboardModifiers(dropEvent->keyboardModifiers()), m_destUrl(destUrl), m_destItem(KCoreDirLister::cachedItemForUrl(destUrl)), m_flags(flags), m_triggered(false) { // Check for the drop of a bookmark -> we want a Link action if (m_mimeData->hasFormat(QStringLiteral("application/x-xbel"))) { m_keyboardModifiers |= Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier); m_dropAction = Qt::LinkAction; } if (m_destItem.isNull() && m_destUrl.isLocalFile()) { m_destItem = KFileItem(m_destUrl); } if (!(m_flags & KIO::NoPrivilegeExecution)) { m_privilegeExecutionEnabled = true; switch (m_dropAction) { case Qt::CopyAction: m_operationType = Copy; break; case Qt::MoveAction: m_operationType = Move; break; case Qt::LinkAction: m_operationType = Symlink; break; default: m_operationType = Other; break; } } } bool destIsDirectory() const { if (!m_destItem.isNull()) { return m_destItem.isDir(); } // We support local dir, remote dir, local desktop file, local executable. // So for remote URLs, we just assume they point to a directory, the user will get an error from KIO::copy if not. return true; } void handleCopyToDirectory(); void handleDropToDesktopFile(); void handleDropToExecutable(); int determineDropAction(); void fillPopupMenu(KIO::DropMenu *popup); void addPluginActions(KIO::DropMenu *popup, const KFileItemListProperties &itemProps); void doCopyToDirectory(); const QMimeData *m_mimeData; const QList m_urls; QMap m_metaData; Qt::DropAction m_dropAction; QPoint m_relativePos; Qt::KeyboardModifiers m_keyboardModifiers; QUrl m_destUrl; KFileItem m_destItem; // null for remote URLs not found in the dirlister cache const JobFlags m_flags; QList m_appActions; QList m_pluginActions; bool m_triggered; // Tracks whether an action has been triggered in the popup menu. QSet m_menus; Q_DECLARE_PUBLIC(DropJob) void slotStart(); void slotTriggered(QAction *); void slotAboutToHide(); static inline DropJob *newJob(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags) { DropJob *job = new DropJob(*new DropJobPrivate(dropEvent, destUrl, flags)); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); // Note: never KIO::getJobTracker()->registerJob here. // We don't want a progress dialog during the copy/move/link popup, it would in fact close // the popup return job; } }; DropMenu::DropMenu(QWidget *parent) : QMenu(parent), m_extraActionsSeparator(nullptr) { m_cancelAction = new QAction(i18n("C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString(), this); m_cancelAction->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); m_lastSeparator = new QAction(this); m_lastSeparator->setSeparator(true); } DropMenu::~DropMenu() { } void DropMenu::addExtraActions(const QList &appActions, const QList &pluginActions) { removeAction(m_lastSeparator); removeAction(m_cancelAction); removeAction(m_extraActionsSeparator); for (QAction *action : qAsConst(m_appActions)) { removeAction(action); } for (QAction *action : qAsConst(m_pluginActions)) { removeAction(action); } m_appActions = appActions; m_pluginActions = pluginActions; if (!m_appActions.isEmpty() || !m_pluginActions.isEmpty()) { if (!m_extraActionsSeparator) { m_extraActionsSeparator = new QAction(this); m_extraActionsSeparator->setSeparator(true); } addAction(m_extraActionsSeparator); addActions(appActions); addActions(pluginActions); } addAction(m_lastSeparator); addAction(m_cancelAction); } DropJob::DropJob(DropJobPrivate &dd) : Job(dd) { QTimer::singleShot(0, this, SLOT(slotStart())); } DropJob::~DropJob() { } void DropJobPrivate::slotStart() { Q_Q(DropJob); if (!m_urls.isEmpty()) { if (destIsDirectory()) { handleCopyToDirectory(); } else { // local file const QString destFile = m_destUrl.toLocalFile(); if (KDesktopFile::isDesktopFile(destFile)) { handleDropToDesktopFile(); } else if (QFileInfo(destFile).isExecutable()) { handleDropToExecutable(); } else { // should not happen, if KDirModel::flags is correct q->setError(KIO::ERR_ACCESS_DENIED); q->emitResult(); } } } else { // Dropping raw data KIO::PasteJob *job = KIO::PasteJobPrivate::newJob(m_mimeData, m_destUrl, KIO::HideProgressInfo, false /*not clipboard*/); QObject::connect(job, &KIO::PasteJob::itemCreated, q, &KIO::DropJob::itemCreated); q->addSubjob(job); } } // Input: m_dropAction as set by Qt at the time of the drop event // Output: m_dropAction possibly modified // Returns a KIO error code, in case of error. int DropJobPrivate::determineDropAction() { Q_Q(DropJob); if (!KProtocolManager::supportsWriting(m_destUrl)) { return KIO::ERR_CANNOT_WRITE; } if (!m_destItem.isNull() && !m_destItem.isWritable() && (m_flags & KIO::NoPrivilegeExecution)) { return KIO::ERR_WRITE_ACCESS_DENIED; } bool allItemsAreFromTrash = true; bool containsTrashRoot = false; foreach (const QUrl &url, m_urls) { const bool local = url.isLocalFile(); if (!local /*optimization*/ && url.scheme() == QLatin1String("trash")) { if (url.path().isEmpty() || url.path() == QLatin1String("/")) { containsTrashRoot = true; } } else { allItemsAreFromTrash = false; } if (url.matches(m_destUrl, QUrl::StripTrailingSlash)) { return KIO::ERR_DROP_ON_ITSELF; } } const bool trashing = m_destUrl.scheme() == QLatin1String("trash"); if (trashing) { if (allItemsAreFromTrash) { qCDebug(KIO_WIDGETS) << "Dropping items from trash to trash"; return KIO::ERR_DROP_ON_ITSELF; } m_dropAction = Qt::MoveAction; if (!q->uiDelegateExtension()->askDeleteConfirmation(m_urls, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) { return KIO::ERR_USER_CANCELED; } return KJob::NoError; // ok } const bool implicitCopy = m_destUrl.scheme() == QLatin1String("stash"); if (implicitCopy) { m_dropAction = Qt::CopyAction; return KJob::NoError; // ok } if (containsTrashRoot) { // Dropping a link to the trash: don't move the full contents, just make a link (#319660) m_dropAction = Qt::LinkAction; return KJob::NoError; // ok } if (allItemsAreFromTrash) { // No point in asking copy/move/link when using dragging from the trash, just move the file out. m_dropAction = Qt::MoveAction; return KJob::NoError; // ok } if (m_keyboardModifiers & (Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier)) { // Qt determined m_dropAction from the modifiers already return KJob::NoError; // ok } // We need to ask the user with a popup menu. Let the caller know. return KIO::ERR_UNKNOWN; } void DropJobPrivate::fillPopupMenu(KIO::DropMenu *popup) { Q_Q(DropJob); // Check what the source can do // TODO: Determining the mimetype of the source URLs is difficult for remote URLs, // we would need to KIO::stat each URL in turn, asynchronously.... KFileItemList fileItems; fileItems.reserve(m_urls.size()); foreach (const QUrl &url, m_urls) { fileItems.append(KFileItem(url)); } const KFileItemListProperties itemProps(fileItems); emit q->popupMenuAboutToShow(itemProps); const bool sReading = itemProps.supportsReading(); const bool sDeleting = itemProps.supportsDeleting(); const bool sMoving = itemProps.supportsMoving(); QString seq = QKeySequence(Qt::ShiftModifier).toString(); Q_ASSERT(seq.endsWith(QLatin1Char('+'))); seq.chop(1); // chop superfluous '+' QAction* popupMoveAction = new QAction(i18n("&Move Here") + QLatin1Char('\t') + seq, popup); popupMoveAction->setIcon(QIcon::fromTheme(QStringLiteral("go-jump"))); popupMoveAction->setData(QVariant::fromValue(Qt::MoveAction)); seq = QKeySequence(Qt::ControlModifier).toString(); Q_ASSERT(seq.endsWith(QLatin1Char('+'))); seq.chop(1); QAction* popupCopyAction = new QAction(i18n("&Copy Here") + QLatin1Char('\t') + seq, popup); popupCopyAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); popupCopyAction->setData(QVariant::fromValue(Qt::CopyAction)); seq = QKeySequence(Qt::ControlModifier + Qt::ShiftModifier).toString(); Q_ASSERT(seq.endsWith(QLatin1Char('+'))); seq.chop(1); QAction* popupLinkAction = new QAction(i18n("&Link Here") + QLatin1Char('\t') + seq, popup); popupLinkAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-link"))); popupLinkAction->setData(QVariant::fromValue(Qt::LinkAction)); if (sMoving || (sReading && sDeleting)) { bool equalDestination = true; foreach (const QUrl &src, m_urls) { if (!m_destUrl.matches(src.adjusted(QUrl::RemoveFilename), QUrl::StripTrailingSlash)) { equalDestination = false; break; } } if (!equalDestination) { popup->addAction(popupMoveAction); } } if (sReading) { popup->addAction(popupCopyAction); } popup->addAction(popupLinkAction); addPluginActions(popup, itemProps); popup->addSeparator(); } void DropJobPrivate::addPluginActions(KIO::DropMenu *popup, const KFileItemListProperties &itemProps) { const QVector plugin_offers = KPluginLoader::findPlugins(QStringLiteral("kf5/kio_dnd")); foreach (const KPluginMetaData &service, plugin_offers) { KPluginFactory *factory = KPluginLoader(service.fileName()).factory(); if (factory) { KIO::DndPopupMenuPlugin *plugin = factory->create(); if (plugin) { const auto actions = plugin->setup(itemProps, m_destUrl); for (auto action : actions) { action->setParent(popup); } m_pluginActions += actions; } } } popup->addExtraActions(m_appActions, m_pluginActions); } void DropJob::setApplicationActions(const QList &actions) { Q_D(DropJob); d->m_appActions = actions; for (KIO::DropMenu *menu : qAsConst(d->m_menus)) { menu->addExtraActions(d->m_appActions, d->m_pluginActions); } } void DropJobPrivate::slotTriggered(QAction *action) { Q_Q(DropJob); if (m_appActions.contains(action) || m_pluginActions.contains(action)) { q->emitResult(); return; } const QVariant data = action->data(); if (!data.canConvert()) { q->setError(KIO::ERR_USER_CANCELED); q->emitResult(); return; } m_dropAction = data.value(); doCopyToDirectory(); } void DropJobPrivate::slotAboutToHide() { Q_Q(DropJob); // QMenu emits aboutToHide before triggered. // So we need to give the menu time in case it needs to emit triggered. // If it does, the cleanup will be done by slotTriggered. QTimer::singleShot(0, q, [=]() { if (!m_triggered) { q->setError(KIO::ERR_USER_CANCELED); q->emitResult(); } }); } void DropJobPrivate::handleCopyToDirectory() { Q_Q(DropJob); if (int error = determineDropAction()) { if (error == KIO::ERR_UNKNOWN) { auto window = KJobWidgets::window(q); KIO::DropMenu *menu = new KIO::DropMenu(window); QObject::connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); // If the user clicks outside the menu, it will be destroyed without emitting the triggered signal. QObject::connect(menu, &QMenu::aboutToHide, q, [this]() { slotAboutToHide(); }); fillPopupMenu(menu); QObject::connect(menu, &QMenu::triggered, q, [this](QAction* action) { m_triggered = true; slotTriggered(action); }); menu->popup(window ? window->mapToGlobal(m_relativePos) : QCursor::pos()); m_menus.insert(menu); QObject::connect(menu, &QObject::destroyed, q, [this, menu]() { m_menus.remove(menu); }); } else { q->setError(error); q->emitResult(); } } else { doCopyToDirectory(); } } void DropJobPrivate::doCopyToDirectory() { Q_Q(DropJob); KIO::CopyJob * job = nullptr; switch (m_dropAction) { case Qt::MoveAction: job = KIO::move(m_urls, m_destUrl, m_flags); KIO::FileUndoManager::self()->recordJob( m_destUrl.scheme() == QLatin1String("trash") ? KIO::FileUndoManager::Trash : KIO::FileUndoManager::Move, m_urls, m_destUrl, job); break; case Qt::CopyAction: job = KIO::copy(m_urls, m_destUrl, m_flags); KIO::FileUndoManager::self()->recordCopyJob(job); break; case Qt::LinkAction: job = KIO::link(m_urls, m_destUrl, m_flags); KIO::FileUndoManager::self()->recordCopyJob(job); break; default: qCWarning(KIO_WIDGETS) << "Unknown drop action" << int(m_dropAction); q->setError(KIO::ERR_UNSUPPORTED_ACTION); q->emitResult(); return; } Q_ASSERT(job); job->setParentJob(q); job->setMetaData(m_metaData); QObject::connect(job, &KIO::CopyJob::copyingDone, q, [q](KIO::Job*, const QUrl &, const QUrl &to) { emit q->itemCreated(to); }); QObject::connect(job, &KIO::CopyJob::copyingLinkDone, q, [q](KIO::Job*, const QUrl&, const QString&, const QUrl &to) { emit q->itemCreated(to); }); q->addSubjob(job); emit q->copyJobStarted(job); } void DropJobPrivate::handleDropToDesktopFile() { Q_Q(DropJob); const QString urlKey = QStringLiteral("URL"); const QString destFile = m_destUrl.toLocalFile(); const KDesktopFile desktopFile(destFile); const KConfigGroup desktopGroup = desktopFile.desktopGroup(); if (desktopFile.hasApplicationType()) { // Drop to application -> start app with urls as argument KService service(destFile); if (!KRun::runApplication(service, m_urls, KJobWidgets::window(q))) { q->setError(KIO::ERR_CANNOT_LAUNCH_PROCESS); q->setErrorText(destFile); } q->emitResult(); } else if (desktopFile.hasLinkType() && desktopGroup.hasKey(urlKey)) { // Drop to link -> adjust destination directory m_destUrl = QUrl::fromUserInput(desktopGroup.readPathEntry(urlKey, QString())); handleCopyToDirectory(); } else { if (desktopFile.hasDeviceType()) { qCWarning(KIO_WIDGETS) << "Not re-implemented; please email kde-frameworks-devel@kde.org if you need this."; // take code from libkonq's old konq_operations.cpp // for now, fallback } // Some other kind of .desktop file (service, servicetype...) q->setError(KIO::ERR_UNSUPPORTED_ACTION); q->emitResult(); } } void DropJobPrivate::handleDropToExecutable() { Q_Q(DropJob); // Launch executable for each of the files QStringList args; args.reserve(m_urls.size()); for (const QUrl &url : qAsConst(m_urls)) { args << url.toLocalFile(); // assume local files } QProcess::startDetached(m_destUrl.toLocalFile(), args); q->emitResult(); } void DropJob::slotResult(KJob *job) { if (job->error()) { KIO::Job::slotResult(job); // will set the error and emit result(this) return; } removeSubjob(job); emitResult(); } DropJob * KIO::drop(const QDropEvent *dropEvent, const QUrl &destUrl, JobFlags flags) { return DropJobPrivate::newJob(dropEvent, destUrl, flags); } #include "moc_dropjob.cpp" #include "dropjob.moc" diff --git a/src/widgets/fileundomanager_p.h b/src/widgets/fileundomanager_p.h index d4f0da9c..ba32ec90 100644 --- a/src/widgets/fileundomanager_p.h +++ b/src/widgets/fileundomanager_p.h @@ -1,179 +1,178 @@ /* This file is part of the KDE project Copyright (C) 2000 Simon Hausmann Copyright (C) 2006, 2008 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. */ #ifndef FILEUNDOMANAGER_P_H #define FILEUNDOMANAGER_P_H #include "fileundomanager.h" #include -#include #include class KJob; namespace KIO { class FileUndoManagerAdaptor; struct BasicOperation { typedef QList Stack; BasicOperation() { m_valid = false; } bool m_valid; bool m_renamed; enum Type { File, Link, Directory }; Type m_type: 2; QUrl m_src; QUrl m_dst; QString m_target; QDateTime m_mtime; }; class UndoCommand { public: typedef QList Stack; UndoCommand() { m_valid = false; } // TODO: is ::TRASH missing? bool isMoveCommand() const { return m_type == FileUndoManager::Move || m_type == FileUndoManager::Rename; } bool m_valid; FileUndoManager::CommandType m_type; BasicOperation::Stack m_opStack; QList m_src; QUrl m_dst; quint64 m_serialNumber; }; // This class listens to a job, collects info while it's running (for copyjobs) // and when the job terminates, on success, it calls addCommand in the undomanager. class CommandRecorder : public QObject { Q_OBJECT public: CommandRecorder(FileUndoManager::CommandType op, const QList &src, const QUrl &dst, KIO::Job *job); virtual ~CommandRecorder(); private Q_SLOTS: void slotResult(KJob *job); void slotCopyingDone(KIO::Job *, const QUrl &from, const QUrl &to, const QDateTime &, bool directory, bool renamed); void slotCopyingLinkDone(KIO::Job *, const QUrl &from, const QString &target, const QUrl &to); void slotDirectoryCreated(const QUrl &url); void slotBatchRenamingDone(const QUrl &from, const QUrl &to); private: UndoCommand m_cmd; }; enum UndoState { MAKINGDIRS = 0, MOVINGFILES, STATINGFILE, REMOVINGDIRS, REMOVINGLINKS }; // The private class is, exceptionally, a real QObject // so that it can be the class with the DBUS adaptor forwarding its signals. class FileUndoManagerPrivate : public QObject { Q_OBJECT public: explicit FileUndoManagerPrivate(FileUndoManager *qq); ~FileUndoManagerPrivate() { delete m_uiInterface; } void pushCommand(const UndoCommand &cmd); void addDirToUpdate(const QUrl &url); void stepMakingDirectories(); void stepMovingFiles(); void stepRemovingLinks(); void stepRemovingDirectories(); /// called by FileUndoManagerAdaptor QByteArray get() const; friend class UndoJob; /// called by UndoJob void stopUndo(bool step); friend class UndoCommandRecorder; /// called by UndoCommandRecorder void addCommand(const UndoCommand &cmd); bool m_lock; UndoCommand::Stack m_commands; UndoCommand m_current; KIO::Job *m_currentJob; UndoState m_undoState; QStack m_dirStack; QStack m_dirCleanupStack; QStack m_fileCleanupStack; // files and links QList m_dirsToUpdate; FileUndoManager::UiInterface *m_uiInterface; UndoJob *m_undoJob; quint64 m_nextCommandIndex; FileUndoManager * const q; // DBUS interface Q_SIGNALS: /// DBUS signal void push(const QByteArray &command); /// DBUS signal void pop(); /// DBUS signal void lock(); /// DBUS signal void unlock(); public Q_SLOTS: // Those four slots are connected to DBUS signals void slotPush(QByteArray); void slotPop(); void slotLock(); void slotUnlock(); void undoStep(); void slotResult(KJob *); }; } // namespace #endif /* FILEUNDOMANAGER_P_H */ diff --git a/src/widgets/imagefilter.cpp b/src/widgets/imagefilter.cpp index aaaa2707..14d80191 100644 --- a/src/widgets/imagefilter.cpp +++ b/src/widgets/imagefilter.cpp @@ -1,262 +1,261 @@ //krazy:exclude=copyright (email of Maxim is missing) /* This file is a part of the KDE project Copyright © 2006 Zack Rusin Copyright © 2006-2007, 2008 Fredrik Höglund The stack blur algorithm was invented by Mario Klingemann This implementation is based on the version in Anti-Grain Geometry Version 2.4, Copyright © 2002-2005 Maxim Shemanarev (http://www.antigrain.com) Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "imagefilter_p.h" #include #include #include -#include #include #include using namespace KIO; static const quint32 stack_blur8_mul[255] = { 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259 }; static const quint32 stack_blur8_shr[255] = { 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 }; inline static void blurHorizontal(QImage &image, unsigned int *stack, int div, int radius) { int stackindex; int stackstart; quint32 *const pixels = reinterpret_cast(image.bits()); quint32 pixel; int w = image.width(); int h = image.height(); int wm = w - 1; unsigned int mul_sum = stack_blur8_mul[radius]; unsigned int shr_sum = stack_blur8_shr[radius]; unsigned int sum, sum_in, sum_out; for (int y = 0; y < h; y++) { sum = 0; sum_in = 0; sum_out = 0; const int yw = y * w; pixel = pixels[yw]; for (int i = 0; i <= radius; i++) { stack[i] = qAlpha(pixel); sum += stack[i] * (i + 1); sum_out += stack[i]; } for (int i = 1; i <= radius; i++) { pixel = pixels[yw + qMin(i, wm)]; unsigned int *stackpix = &stack[i + radius]; *stackpix = qAlpha(pixel); sum += *stackpix * (radius + 1 - i); sum_in += *stackpix; } stackindex = radius; for (int x = 0, i = yw; x < w; x++) { pixels[i++] = (((sum * mul_sum) >> shr_sum) << 24) & 0xff000000; sum -= sum_out; stackstart = stackindex + div - radius; if (stackstart >= div) { stackstart -= div; } unsigned int *stackpix = &stack[stackstart]; sum_out -= *stackpix; pixel = pixels[yw + qMin(x + radius + 1, wm)]; *stackpix = qAlpha(pixel); sum_in += *stackpix; sum += sum_in; if (++stackindex >= div) { stackindex = 0; } stackpix = &stack[stackindex]; sum_out += *stackpix; sum_in -= *stackpix; } // for (x = 0, ...) } // for (y = 0, ...) } inline static void blurVertical(QImage &image, unsigned int *stack, int div, int radius) { int stackindex; int stackstart; quint32 *const pixels = reinterpret_cast(image.bits()); quint32 pixel; int w = image.width(); int h = image.height(); int hm = h - 1; int mul_sum = stack_blur8_mul[radius]; int shr_sum = stack_blur8_shr[radius]; unsigned int sum, sum_in, sum_out; for (int x = 0; x < w; x++) { sum = 0; sum_in = 0; sum_out = 0; pixel = pixels[x]; for (int i = 0; i <= radius; i++) { stack[i] = qAlpha(pixel); sum += stack[i] * (i + 1); sum_out += stack[i]; } for (int i = 1; i <= radius; i++) { pixel = pixels[qMin(i, hm) * w + x]; unsigned int *stackpix = &stack[i + radius]; *stackpix = qAlpha(pixel); sum += *stackpix * (radius + 1 - i); sum_in += *stackpix; } stackindex = radius; for (int y = 0, i = x; y < h; y++, i += w) { pixels[i] = (((sum * mul_sum) >> shr_sum) << 24) & 0xff000000; sum -= sum_out; stackstart = stackindex + div - radius; if (stackstart >= div) { stackstart -= div; } unsigned int *stackpix = &stack[stackstart]; sum_out -= *stackpix; pixel = pixels[qMin(y + radius + 1, hm) * w + x]; *stackpix = qAlpha(pixel); sum_in += *stackpix; sum += sum_in; if (++stackindex >= div) { stackindex = 0; } stackpix = &stack[stackindex]; sum_out += *stackpix; sum_in -= *stackpix; } // for (y = 0, ...) } // for (x = 0, ...) } static void stackBlur(QImage &image, float radius) { radius = qRound(radius); int div = int(radius * 2) + 1; unsigned int *stack = new unsigned int[div]; blurHorizontal(image, stack, div, radius); blurVertical(image, stack, div, radius); delete [] stack; } void ImageFilter::shadowBlur(QImage &image, float radius, const QColor &color) { if (radius < 0) { return; } if (radius > 0) { stackBlur(image, radius); } // Correct the color and opacity of the shadow QPainter p(&image); p.setCompositionMode(QPainter::CompositionMode_SourceIn); p.fillRect(image.rect(), color); } diff --git a/src/widgets/kacleditwidget.cpp b/src/widgets/kacleditwidget.cpp index f9f0492d..98115d48 100644 --- a/src/widgets/kacleditwidget.cpp +++ b/src/widgets/kacleditwidget.cpp @@ -1,1173 +1,1172 @@ /*************************************************************************** * Copyright (C) 2005 by Sean Harmer * * 2005 - 2007 Till Adam * * * * This program 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 program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "kacleditwidget.h" #include "kacleditwidget_p.h" #include "kio_widgets_debug.h" #if HAVE_POSIX_ACL #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #if HAVE_ACL_LIBACL_H # include #endif extern "C" { #include #include } #include class KACLEditWidget::KACLEditWidgetPrivate { public: KACLEditWidgetPrivate() { } // slots void _k_slotUpdateButtons(); KACLListView *m_listView; QPushButton *m_AddBtn; QPushButton *m_EditBtn; QPushButton *m_DelBtn; }; KACLEditWidget::KACLEditWidget(QWidget *parent) : QWidget(parent), d(new KACLEditWidgetPrivate) { QHBoxLayout *hbox = new QHBoxLayout(this); hbox->setContentsMargins(0, 0, 0, 0); d->m_listView = new KACLListView(this); hbox->addWidget(d->m_listView); connect(d->m_listView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(_k_slotUpdateButtons())); QVBoxLayout *vbox = new QVBoxLayout(); hbox->addLayout(vbox); d->m_AddBtn = new QPushButton(i18n("Add Entry..."), this); vbox->addWidget(d->m_AddBtn); d->m_AddBtn->setObjectName(QStringLiteral("add_entry_button")); connect(d->m_AddBtn, &QAbstractButton::clicked, d->m_listView, &KACLListView::slotAddEntry); d->m_EditBtn = new QPushButton(i18n("Edit Entry..."), this); vbox->addWidget(d->m_EditBtn); d->m_EditBtn->setObjectName(QStringLiteral("edit_entry_button")); connect(d->m_EditBtn, &QAbstractButton::clicked, d->m_listView, &KACLListView::slotEditEntry); d->m_DelBtn = new QPushButton(i18n("Delete Entry"), this); vbox->addWidget(d->m_DelBtn); d->m_DelBtn->setObjectName(QStringLiteral("delete_entry_button")); connect(d->m_DelBtn, &QAbstractButton::clicked, d->m_listView, &KACLListView::slotRemoveEntry); vbox->addItem(new QSpacerItem(10, 10, QSizePolicy::Fixed, QSizePolicy::Expanding)); d->_k_slotUpdateButtons(); } KACLEditWidget::~KACLEditWidget() { delete d; } void KACLEditWidget::KACLEditWidgetPrivate::_k_slotUpdateButtons() { bool atLeastOneIsNotDeletable = false; bool atLeastOneIsNotAllowedToChangeType = false; int selectedCount = 0; QList selected = m_listView->selectedItems(); QListIterator it(selected); while (it.hasNext()) { KACLListViewItem *item = static_cast(it.next()); ++selectedCount; if (!item->isDeletable()) { atLeastOneIsNotDeletable = true; } if (!item->isAllowedToChangeType()) { atLeastOneIsNotAllowedToChangeType = true; } } m_EditBtn->setEnabled(selectedCount && !atLeastOneIsNotAllowedToChangeType); m_DelBtn->setEnabled(selectedCount && !atLeastOneIsNotDeletable); } KACL KACLEditWidget::getACL() const { return d->m_listView->getACL(); } KACL KACLEditWidget::getDefaultACL() const { return d->m_listView->getDefaultACL(); } void KACLEditWidget::setACL(const KACL &acl) { d->m_listView->setACL(acl); } void KACLEditWidget::setDefaultACL(const KACL &acl) { d->m_listView->setDefaultACL(acl); } void KACLEditWidget::setAllowDefaults(bool value) { d->m_listView->setAllowDefaults(value); } KACLListViewItem::KACLListViewItem(QTreeWidget *parent, KACLListView::EntryType _type, unsigned short _value, bool defaults, const QString &_qualifier) : QTreeWidgetItem(parent), type(_type), value(_value), isDefault(defaults), qualifier(_qualifier), isPartial(false) { m_pACLListView = qobject_cast(parent); repaint(); } KACLListViewItem::~ KACLListViewItem() { } QString KACLListViewItem::key() const { QString key; if (!isDefault) { key = QLatin1Char('A'); } else { key = QLatin1Char('B'); } switch (type) { case KACLListView::User: key += QLatin1Char('A'); break; case KACLListView::Group: key += QLatin1Char('B'); break; case KACLListView::Others: key += QLatin1Char('C'); break; case KACLListView::Mask: key += QLatin1Char('D'); break; case KACLListView::NamedUser: key += QLatin1Char('E') + text(1); break; case KACLListView::NamedGroup: key += QLatin1Char('F') + text(1); break; default: key += text(0); break; } return key; } bool KACLListViewItem::operator< (const QTreeWidgetItem &other) const { return key() < static_cast(other).key(); } #if 0 void KACLListViewItem::paintCell(QPainter *p, const QColorGroup &cg, int column, int width, int alignment) { if (isDefault) { setForeground(QColor(0, 0, 255)); } if (isPartial) { QFont font = p->font(); font.setItalic(true); setForeground(QColor(100, 100, 100)); p->setFont(font); } QTreeWidgetItem::paintCell(p, mycg, column, width, alignment); KACLListViewItem *below = 0; if (itemBelow()) { below = static_cast(itemBelow()); } const bool lastUser = type == KACLListView::NamedUser && below && below->type == KACLListView::NamedGroup; const bool lastNonDefault = !isDefault && below && below->isDefault; if (type == KACLListView::Mask || lastUser || lastNonDefault) { p->setPen(QPen(Qt::gray, 0, Qt::DotLine)); if (type == KACLListView::Mask) { p->drawLine(0, 0, width - 1, 0); } p->drawLine(0, height() - 1, width - 1, height() - 1); } } #endif void KACLListViewItem::updatePermissionIcons() { unsigned int partialPerms = value; if (value & ACL_READ) { setIcon(2, QIcon::fromTheme(QStringLiteral("checkmark"))); } else if (partialPerms & ACL_READ) { setIcon(2, QIcon::fromTheme(QStringLiteral("checkmark-partial"))); } else { setIcon(2, QIcon()); } if (value & ACL_WRITE) { setIcon(3, QIcon::fromTheme(QStringLiteral("checkmark"))); } else if (partialPerms & ACL_WRITE) { setIcon(3, QIcon::fromTheme(QStringLiteral("checkmark-partial"))); } else { setIcon(3, QIcon()); } if (value & ACL_EXECUTE) { setIcon(4, QIcon::fromTheme(QStringLiteral("checkmark"))); } else if (partialPerms & ACL_EXECUTE) { setIcon(4, QIcon::fromTheme(QStringLiteral("checkmark-partial"))); } else { setIcon(4, QIcon()); } } void KACLListViewItem::repaint() { int idx = 0; QString text; QString icon; switch (type) { case KACLListView::User: default: text = i18nc("Unix permissions", "Owner"); icon = QStringLiteral("user-gray"); break; case KACLListView::Group: text = i18nc("UNIX permissions", "Owning Group"); icon = QStringLiteral("group-gray"); break; case KACLListView::Others: text = i18nc("UNIX permissions", "Others"); icon = QStringLiteral("users-other-gray"); break; case KACLListView::Mask: text = i18nc("UNIX permissions", "Mask"); icon = QStringLiteral("view-filter"); break; case KACLListView::NamedUser: text = i18nc("UNIX permissions", "Named User"); icon = QStringLiteral("user"); break; case KACLListView::NamedGroup: text = i18nc("UNIX permissions", "Others"); icon = QStringLiteral("users-other"); break; } setText(0, text); setIcon(0, QIcon::fromTheme(icon)); if (isDefault) { setText(0, i18n("Owner (Default)")); } setText(1, qualifier); // Set the icons for which of the perms are set updatePermissionIcons(); } void KACLListViewItem::calcEffectiveRights() { QString strEffective = QStringLiteral("---"); // Do we need to worry about the mask entry? It applies to named users, // owning group, and named groups if (m_pACLListView->hasMaskEntry() && (type == KACLListView::NamedUser || type == KACLListView::Group || type == KACLListView::NamedGroup) && !isDefault) { strEffective[0] = (m_pACLListView->maskPermissions() & value & ACL_READ) ? 'r' : '-'; strEffective[1] = (m_pACLListView->maskPermissions() & value & ACL_WRITE) ? 'w' : '-'; strEffective[2] = (m_pACLListView->maskPermissions() & value & ACL_EXECUTE) ? 'x' : '-'; /* // What about any partial perms? if ( maskPerms & partialPerms & ACL_READ || // Partial perms on entry maskPartialPerms & perms & ACL_READ || // Partial perms on mask maskPartialPerms & partialPerms & ACL_READ ) // Partial perms on mask and entry strEffective[0] = 'R'; if ( maskPerms & partialPerms & ACL_WRITE || // Partial perms on entry maskPartialPerms & perms & ACL_WRITE || // Partial perms on mask maskPartialPerms & partialPerms & ACL_WRITE ) // Partial perms on mask and entry strEffective[1] = 'W'; if ( maskPerms & partialPerms & ACL_EXECUTE || // Partial perms on entry maskPartialPerms & perms & ACL_EXECUTE || // Partial perms on mask maskPartialPerms & partialPerms & ACL_EXECUTE ) // Partial perms on mask and entry strEffective[2] = 'X'; */ } else { // No, the effective value are just the value in this entry strEffective[0] = (value & ACL_READ) ? 'r' : '-'; strEffective[1] = (value & ACL_WRITE) ? 'w' : '-'; strEffective[2] = (value & ACL_EXECUTE) ? 'x' : '-'; /* // What about any partial perms? if ( partialPerms & ACL_READ ) strEffective[0] = 'R'; if ( partialPerms & ACL_WRITE ) strEffective[1] = 'W'; if ( partialPerms & ACL_EXECUTE ) strEffective[2] = 'X'; */ } setText(5, strEffective); } bool KACLListViewItem::isDeletable() const { bool isMaskAndDeletable = false; if (type == KACLListView::Mask) { if (!isDefault && m_pACLListView->maskCanBeDeleted()) { isMaskAndDeletable = true; } else if (isDefault && m_pACLListView->defaultMaskCanBeDeleted()) { isMaskAndDeletable = true; } } return type != KACLListView::User && type != KACLListView::Group && type != KACLListView::Others && (type != KACLListView::Mask || isMaskAndDeletable); } bool KACLListViewItem::isAllowedToChangeType() const { return type != KACLListView::User && type != KACLListView::Group && type != KACLListView::Others && type != KACLListView::Mask; } void KACLListViewItem::togglePerm(acl_perm_t perm) { value ^= perm; // Toggle the perm if (type == KACLListView::Mask && !isDefault) { m_pACLListView->setMaskPermissions(value); } calcEffectiveRights(); updatePermissionIcons(); /* // If the perm is in the partial perms then remove it. i.e. Once // a user changes a partial perm it then applies to all selected files. if ( m_pEntry->m_partialPerms & perm ) m_pEntry->m_partialPerms ^= perm; m_pEntry->setPartialEntry( false ); // Make sure that all entries have their effective rights calculated if // we are changing the ACL_MASK entry. if ( type == Mask ) { m_pACLListView->setMaskPartialPermissions( m_pEntry->m_partialPerms ); m_pACLListView->setMaskPermissions( value ); m_pACLListView->calculateEffectiveRights(); } */ } EditACLEntryDialog::EditACLEntryDialog(KACLListView *listView, KACLListViewItem *item, const QStringList &users, const QStringList &groups, const QStringList &defaultUsers, const QStringList &defaultGroups, int allowedTypes, int allowedDefaultTypes, bool allowDefaults) : QDialog(listView), m_listView(listView), m_item(item), m_users(users), m_groups(groups), m_defaultUsers(defaultUsers), m_defaultGroups(defaultGroups), m_allowedTypes(allowedTypes), m_allowedDefaultTypes(allowedDefaultTypes), m_defaultCB(nullptr) { setObjectName(QStringLiteral("edit_entry_dialog")); setModal(true); setWindowTitle(i18n("Edit ACL Entry")); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); QGroupBox *gb = new QGroupBox(i18n("Entry Type"), this); QVBoxLayout *gbLayout = new QVBoxLayout(gb); m_buttonGroup = new QButtonGroup(this); if (allowDefaults) { m_defaultCB = new QCheckBox(i18n("Default for new files in this folder"), this); m_defaultCB->setObjectName(QStringLiteral("defaultCB")); mainLayout->addWidget(m_defaultCB); connect(m_defaultCB, &QAbstractButton::toggled, this, &EditACLEntryDialog::slotUpdateAllowedUsersAndGroups); connect(m_defaultCB, &QAbstractButton::toggled, this, &EditACLEntryDialog::slotUpdateAllowedTypes); } QRadioButton *ownerType = new QRadioButton(i18n("Owner"), gb); ownerType->setObjectName(QStringLiteral("ownerType")); gbLayout->addWidget(ownerType); m_buttonGroup->addButton(ownerType); m_buttonIds.insert(ownerType, KACLListView::User); QRadioButton *owningGroupType = new QRadioButton(i18n("Owning Group"), gb); owningGroupType->setObjectName(QStringLiteral("owningGroupType")); gbLayout->addWidget(owningGroupType); m_buttonGroup->addButton(owningGroupType); m_buttonIds.insert(owningGroupType, KACLListView::Group); QRadioButton *othersType = new QRadioButton(i18n("Others"), gb); othersType->setObjectName(QStringLiteral("othersType")); gbLayout->addWidget(othersType); m_buttonGroup->addButton(othersType); m_buttonIds.insert(othersType, KACLListView::Others); QRadioButton *maskType = new QRadioButton(i18n("Mask"), gb); maskType->setObjectName(QStringLiteral("maskType")); gbLayout->addWidget(maskType); m_buttonGroup->addButton(maskType); m_buttonIds.insert(maskType, KACLListView::Mask); QRadioButton *namedUserType = new QRadioButton(i18n("Named user"), gb); namedUserType->setObjectName(QStringLiteral("namesUserType")); gbLayout->addWidget(namedUserType); m_buttonGroup->addButton(namedUserType); m_buttonIds.insert(namedUserType, KACLListView::NamedUser); QRadioButton *namedGroupType = new QRadioButton(i18n("Named group"), gb); namedGroupType->setObjectName(QStringLiteral("namedGroupType")); gbLayout->addWidget(namedGroupType); m_buttonGroup->addButton(namedGroupType); m_buttonIds.insert(namedGroupType, KACLListView::NamedGroup); mainLayout->addWidget(gb); connect(m_buttonGroup, QOverload::of(&QButtonGroup::buttonClicked), this, &EditACLEntryDialog::slotSelectionChanged); m_widgetStack = new QStackedWidget(this); mainLayout->addWidget(m_widgetStack); // users box QWidget *usersBox = new QWidget(m_widgetStack); QHBoxLayout *usersLayout = new QHBoxLayout(usersBox); usersBox->setLayout(usersLayout); m_widgetStack->addWidget(usersBox); QLabel *usersLabel = new QLabel(i18n("User: "), usersBox); m_usersCombo = new KComboBox(usersBox); m_usersCombo->setEditable(false); m_usersCombo->setObjectName(QStringLiteral("users")); usersLabel->setBuddy(m_usersCombo); usersLayout->addWidget(usersLabel); usersLayout->addWidget(m_usersCombo); // groups box QWidget *groupsBox = new QWidget(m_widgetStack); QHBoxLayout *groupsLayout = new QHBoxLayout(usersBox); groupsBox->setLayout(groupsLayout); m_widgetStack->addWidget(groupsBox); QLabel *groupsLabel = new QLabel(i18n("Group: "), groupsBox); m_groupsCombo = new KComboBox(groupsBox); m_groupsCombo->setEditable(false); m_groupsCombo->setObjectName(QStringLiteral("groups")); groupsLabel->setBuddy(m_groupsCombo); groupsLayout->addWidget(groupsLabel); groupsLayout->addWidget(m_groupsCombo); if (m_item) { m_buttonIds.key(m_item->type)->setChecked(true); if (m_defaultCB) { m_defaultCB->setChecked(m_item->isDefault); } slotUpdateAllowedTypes(); slotSelectionChanged(m_buttonIds.key(m_item->type)); slotUpdateAllowedUsersAndGroups(); if (m_item->type == KACLListView::NamedUser) { m_usersCombo->setItemText(m_usersCombo->currentIndex(), m_item->qualifier); } else if (m_item->type == KACLListView::NamedGroup) { m_groupsCombo->setItemText(m_groupsCombo->currentIndex(), m_item->qualifier); } } else { // new entry, preselect "named user", arguably the most common one m_buttonIds.key(KACLListView::NamedUser)->setChecked(true); slotUpdateAllowedTypes(); slotSelectionChanged(m_buttonIds.key(KACLListView::NamedUser)); slotUpdateAllowedUsersAndGroups(); } QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &EditACLEntryDialog::slotOk); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); mainLayout->addWidget(buttonBox); adjustSize(); } void EditACLEntryDialog::slotUpdateAllowedTypes() { int allowedTypes = m_allowedTypes; if (m_defaultCB && m_defaultCB->isChecked()) { allowedTypes = m_allowedDefaultTypes; } for (int i = 1; i < KACLListView::AllTypes; i = i * 2) { if (allowedTypes & i) { m_buttonIds.key(i)->show(); } else { m_buttonIds.key(i)->hide(); } } } void EditACLEntryDialog::slotUpdateAllowedUsersAndGroups() { const QString oldUser = m_usersCombo->currentText(); const QString oldGroup = m_groupsCombo->currentText(); m_usersCombo->clear(); m_groupsCombo->clear(); if (m_defaultCB && m_defaultCB->isChecked()) { m_usersCombo->addItems(m_defaultUsers); if (m_defaultUsers.contains(oldUser)) { m_usersCombo->setItemText(m_usersCombo->currentIndex(), oldUser); } m_groupsCombo->addItems(m_defaultGroups); if (m_defaultGroups.contains(oldGroup)) { m_groupsCombo->setItemText(m_groupsCombo->currentIndex(), oldGroup); } } else { m_usersCombo->addItems(m_users); if (m_users.contains(oldUser)) { m_usersCombo->setItemText(m_usersCombo->currentIndex(), oldUser); } m_groupsCombo->addItems(m_groups); if (m_groups.contains(oldGroup)) { m_groupsCombo->setItemText(m_groupsCombo->currentIndex(), oldGroup); } } } void EditACLEntryDialog::slotOk() { KACLListView::EntryType type = static_cast(m_buttonIds[m_buttonGroup->checkedButton()]); qCWarning(KIO_WIDGETS) << "Type 2: " << type; QString qualifier; if (type == KACLListView::NamedUser) { qualifier = m_usersCombo->currentText(); } if (type == KACLListView::NamedGroup) { qualifier = m_groupsCombo->currentText(); } if (!m_item) { m_item = new KACLListViewItem(m_listView, type, ACL_READ | ACL_WRITE | ACL_EXECUTE, false, qualifier); } else { m_item->type = type; m_item->qualifier = qualifier; } if (m_defaultCB) { m_item->isDefault = m_defaultCB->isChecked(); } m_item->repaint(); QDialog::accept(); } void EditACLEntryDialog::slotSelectionChanged(QAbstractButton *button) { switch (m_buttonIds[ button ]) { case KACLListView::User: case KACLListView::Group: case KACLListView::Others: case KACLListView::Mask: m_widgetStack->setEnabled(false); break; case KACLListView::NamedUser: m_widgetStack->setEnabled(true); m_widgetStack->setCurrentIndex(0 /* User */); break; case KACLListView::NamedGroup: m_widgetStack->setEnabled(true); m_widgetStack->setCurrentIndex(1 /* Group */); break; default: break; } } KACLListView::KACLListView(QWidget *parent) : QTreeWidget(parent), m_hasMask(false), m_allowDefaults(false) { // Add the columns setColumnCount(6); const QStringList headers { i18n("Type"), i18n("Name"), i18nc("read permission", "r"), i18nc("write permission", "w"), i18nc("execute permission", "x"), i18n("Effective"), }; setHeaderLabels(headers); setSortingEnabled(false); setSelectionMode(QAbstractItemView::ExtendedSelection); header()->setSectionResizeMode(QHeaderView::ResizeToContents); setRootIsDecorated(false); // fill the lists of all legal users and groups struct passwd *user = nullptr; setpwent(); while ((user = getpwent()) != nullptr) { m_allUsers << QString::fromLatin1(user->pw_name); } endpwent(); struct group *gr = nullptr; setgrent(); while ((gr = getgrent()) != nullptr) { m_allGroups << QString::fromLatin1(gr->gr_name); } endgrent(); m_allUsers.sort(); m_allGroups.sort(); connect(this, &QTreeWidget::itemClicked, this, &KACLListView::slotItemClicked); connect(this, &KACLListView::itemDoubleClicked, this, &KACLListView::slotItemDoubleClicked); } KACLListView::~KACLListView() { } QStringList KACLListView::allowedUsers(bool defaults, KACLListViewItem *allowedItem) { QStringList allowedUsers = m_allUsers; QTreeWidgetItemIterator it(this); while (*it) { const KACLListViewItem *item = static_cast(*it); ++it; if (item->type != NamedUser || item->isDefault != defaults) { continue; } if (allowedItem && item == allowedItem && allowedItem->isDefault == defaults) { continue; } allowedUsers.removeAll(item->qualifier); } return allowedUsers; } QStringList KACLListView::allowedGroups(bool defaults, KACLListViewItem *allowedItem) { QStringList allowedGroups = m_allGroups; QTreeWidgetItemIterator it(this); while (*it) { const KACLListViewItem *item = static_cast(*it); ++it; if (item->type != NamedGroup || item->isDefault != defaults) { continue; } if (allowedItem && item == allowedItem && allowedItem->isDefault == defaults) { continue; } allowedGroups.removeAll(item->qualifier); } return allowedGroups; } void KACLListView::fillItemsFromACL(const KACL &pACL, bool defaults) { // clear out old entries of that ilk QTreeWidgetItemIterator it(this); while (KACLListViewItem *item = static_cast(*it)) { ++it; if (item->isDefault == defaults) { delete item; } } new KACLListViewItem(this, User, pACL.ownerPermissions(), defaults); new KACLListViewItem(this, Group, pACL.owningGroupPermissions(), defaults); new KACLListViewItem(this, Others, pACL.othersPermissions(), defaults); bool hasMask = false; unsigned short mask = pACL.maskPermissions(hasMask); if (hasMask) { new KACLListViewItem(this, Mask, mask, defaults); } // read all named user entries const ACLUserPermissionsList &userList = pACL.allUserPermissions(); ACLUserPermissionsConstIterator itu = userList.begin(); while (itu != userList.end()) { new KACLListViewItem(this, NamedUser, (*itu).second, defaults, (*itu).first); ++itu; } // and now all named groups const ACLUserPermissionsList &groupList = pACL.allGroupPermissions(); ACLUserPermissionsConstIterator itg = groupList.begin(); while (itg != groupList.end()) { new KACLListViewItem(this, NamedGroup, (*itg).second, defaults, (*itg).first); ++itg; } } void KACLListView::setACL(const KACL &acl) { if (!acl.isValid()) { return; } // Remove any entries left over from displaying a previous ACL m_ACL = acl; fillItemsFromACL(m_ACL); m_mask = acl.maskPermissions(m_hasMask); calculateEffectiveRights(); } void KACLListView::setDefaultACL(const KACL &acl) { if (!acl.isValid()) { return; } m_defaultACL = acl; fillItemsFromACL(m_defaultACL, true); calculateEffectiveRights(); } KACL KACLListView::itemsToACL(bool defaults) const { KACL newACL(0); bool atLeastOneEntry = false; ACLUserPermissionsList users; ACLGroupPermissionsList groups; QTreeWidgetItemIterator it(const_cast(this)); while (QTreeWidgetItem *qlvi = *it) { ++it; const KACLListViewItem *item = static_cast(qlvi); if (item->isDefault != defaults) { continue; } atLeastOneEntry = true; switch (item->type) { case User: newACL.setOwnerPermissions(item->value); break; case Group: newACL.setOwningGroupPermissions(item->value); break; case Others: newACL.setOthersPermissions(item->value); break; case Mask: newACL.setMaskPermissions(item->value); break; case NamedUser: users.append(qMakePair(item->text(1), item->value)); break; case NamedGroup: groups.append(qMakePair(item->text(1), item->value)); break; default: break; } } if (atLeastOneEntry) { newACL.setAllUserPermissions(users); newACL.setAllGroupPermissions(groups); if (newACL.isValid()) { return newACL; } } return KACL(); } KACL KACLListView::getACL() { return itemsToACL(false); } KACL KACLListView::getDefaultACL() { return itemsToACL(true); } void KACLListView::contentsMousePressEvent(QMouseEvent * /*e*/) { /* QTreeWidgetItem *clickedItem = itemAt( e->pos() ); if ( !clickedItem ) return; // if the click is on an as yet unselected item, select it first if ( !clickedItem->isSelected() ) QAbstractItemView::contentsMousePressEvent( e ); if ( !currentItem() ) return; int column = header()->sectionAt( e->x() ); acl_perm_t perm; switch ( column ) { case 2: perm = ACL_READ; break; case 3: perm = ACL_WRITE; break; case 4: perm = ACL_EXECUTE; break; default: return QTreeWidget::contentsMousePressEvent( e ); } KACLListViewItem* referenceItem = static_cast( clickedItem ); unsigned short referenceHadItSet = referenceItem->value & perm; QTreeWidgetItemIterator it( this ); while ( KACLListViewItem* item = static_cast( *it ) ) { ++it; if ( !item->isSelected() ) continue; // toggle those with the same value as the clicked item, leave the others if ( referenceHadItSet == ( item->value & perm ) ) item->togglePerm( perm ); } */ } void KACLListView::slotItemClicked(QTreeWidgetItem *pItem, int col) { if (!pItem) { return; } QTreeWidgetItemIterator it(this); while (KACLListViewItem *item = static_cast(*it)) { ++it; if (!item->isSelected()) { continue; } switch (col) { case 2: item->togglePerm(ACL_READ); break; case 3: item->togglePerm(ACL_WRITE); break; case 4: item->togglePerm(ACL_EXECUTE); break; default: ; // Do nothing } } /* // Has the user changed one of the required entries in a default ACL? if ( m_pACL->aclType() == ACL_TYPE_DEFAULT && ( col == 2 || col == 3 || col == 4 ) && ( pACLItem->entryType() == ACL_USER_OBJ || pACLItem->entryType() == ACL_GROUP_OBJ || pACLItem->entryType() == ACL_OTHER ) ) { // Mark the required entries as no longer being partial entries. // That is, they will get applied to all selected directories. KACLListViewItem* pUserObj = findACLEntryByType( this, ACL_USER_OBJ ); pUserObj->entry()->setPartialEntry( false ); KACLListViewItem* pGroupObj = findACLEntryByType( this, ACL_GROUP_OBJ ); pGroupObj->entry()->setPartialEntry( false ); KACLListViewItem* pOther = findACLEntryByType( this, ACL_OTHER ); pOther->entry()->setPartialEntry( false ); update(); } */ } void KACLListView::slotItemDoubleClicked(QTreeWidgetItem *item, int column) { if (!item) { return; } // avoid conflict with clicking to toggle permission if (column >= 2 && column <= 4) { return; } KACLListViewItem *aclListItem = static_cast(item); if (!aclListItem->isAllowedToChangeType()) { return; } setCurrentItem(item); slotEditEntry(); } void KACLListView::calculateEffectiveRights() { QTreeWidgetItemIterator it(this); KACLListViewItem *pItem; while ((pItem = dynamic_cast(*it)) != nullptr) { ++it; pItem->calcEffectiveRights(); } } unsigned short KACLListView::maskPermissions() const { return m_mask; } void KACLListView::setMaskPermissions(unsigned short maskPerms) { m_mask = maskPerms; calculateEffectiveRights(); } acl_perm_t KACLListView::maskPartialPermissions() const { // return m_pMaskEntry->m_partialPerms; return 0; } void KACLListView::setMaskPartialPermissions(acl_perm_t /*maskPartialPerms*/) { //m_pMaskEntry->m_partialPerms = maskPartialPerms; calculateEffectiveRights(); } bool KACLListView::hasDefaultEntries() const { QTreeWidgetItemIterator it(const_cast(this)); while (*it) { const KACLListViewItem *item = static_cast(*it); ++it; if (item->isDefault) { return true; } } return false; } const KACLListViewItem *KACLListView::findDefaultItemByType(EntryType type) const { return findItemByType(type, true); } const KACLListViewItem *KACLListView::findItemByType(EntryType type, bool defaults) const { QTreeWidgetItemIterator it(const_cast(this)); while (*it) { const KACLListViewItem *item = static_cast(*it); ++it; if (item->isDefault == defaults && item->type == type) { return item; } } return nullptr; } unsigned short KACLListView::calculateMaskValue(bool defaults) const { // KACL auto-adds the relevant maks entries, so we can simply query bool dummy; return itemsToACL(defaults).maskPermissions(dummy); } void KACLListView::slotAddEntry() { int allowedTypes = NamedUser | NamedGroup; if (!m_hasMask) { allowedTypes |= Mask; } int allowedDefaultTypes = NamedUser | NamedGroup; if (!findDefaultItemByType(Mask)) { allowedDefaultTypes |= Mask; } if (!hasDefaultEntries()) { allowedDefaultTypes |= User | Group; } EditACLEntryDialog dlg(this, nullptr, allowedUsers(false), allowedGroups(false), allowedUsers(true), allowedGroups(true), allowedTypes, allowedDefaultTypes, m_allowDefaults); dlg.exec(); KACLListViewItem *item = dlg.item(); if (!item) { return; // canceled } if (item->type == Mask && !item->isDefault) { m_hasMask = true; m_mask = item->value; } if (item->isDefault && !hasDefaultEntries()) { // first default entry, fill in what is needed if (item->type != User) { unsigned short v = findDefaultItemByType(User)->value; new KACLListViewItem(this, User, v, true); } if (item->type != Group) { unsigned short v = findDefaultItemByType(Group)->value; new KACLListViewItem(this, Group, v, true); } if (item->type != Others) { unsigned short v = findDefaultItemByType(Others)->value; new KACLListViewItem(this, Others, v, true); } } const KACLListViewItem *defaultMaskItem = findDefaultItemByType(Mask); if (item->isDefault && !defaultMaskItem) { unsigned short v = calculateMaskValue(true); new KACLListViewItem(this, Mask, v, true); } if (!item->isDefault && !m_hasMask && (item->type == Group || item->type == NamedUser || item->type == NamedGroup)) { // auto-add a mask entry unsigned short v = calculateMaskValue(false); new KACLListViewItem(this, Mask, v, false); m_hasMask = true; m_mask = v; } calculateEffectiveRights(); sortItems(sortColumn(), Qt::AscendingOrder); setCurrentItem(item); // QTreeWidget doesn't seem to emit, in this case, and we need to update // the buttons... if (topLevelItemCount() == 1) { emit currentItemChanged(item, item); } } void KACLListView::slotEditEntry() { QTreeWidgetItem *current = currentItem(); if (!current) { return; } KACLListViewItem *item = static_cast(current); int allowedTypes = item->type | NamedUser | NamedGroup; bool itemWasMask = item->type == Mask; if (!m_hasMask || itemWasMask) { allowedTypes |= Mask; } int allowedDefaultTypes = item->type | NamedUser | NamedGroup; if (!findDefaultItemByType(Mask)) { allowedDefaultTypes |= Mask; } if (!hasDefaultEntries()) { allowedDefaultTypes |= User | Group; } EditACLEntryDialog dlg(this, item, allowedUsers(false, item), allowedGroups(false, item), allowedUsers(true, item), allowedGroups(true, item), allowedTypes, allowedDefaultTypes, m_allowDefaults); dlg.exec(); if (itemWasMask && item->type != Mask) { m_hasMask = false; m_mask = 0; } if (!itemWasMask && item->type == Mask) { m_mask = item->value; m_hasMask = true; } calculateEffectiveRights(); sortItems(sortColumn(), Qt::AscendingOrder); } void KACLListView::slotRemoveEntry() { QTreeWidgetItemIterator it(this, QTreeWidgetItemIterator::Selected); while (*it) { KACLListViewItem *item = static_cast(*it); ++it; /* First check if it's a mask entry and if so, make sure that there is * either no name user or group entry, which means the mask can be * removed, or don't remove it, but reset it. That is allowed. */ if (item->type == Mask) { bool itemWasDefault = item->isDefault; if (!itemWasDefault && maskCanBeDeleted()) { m_hasMask = false; m_mask = 0; delete item; } else if (itemWasDefault && defaultMaskCanBeDeleted()) { delete item; } else { item->value = 0; item->repaint(); } if (!itemWasDefault) { calculateEffectiveRights(); } } else { // for the base permissions, disable them, which is what libacl does if (!item->isDefault && (item->type == User || item->type == Group || item->type == Others)) { item->value = 0; item->repaint(); } else { delete item; } } } } bool KACLListView::maskCanBeDeleted() const { return !findItemByType(NamedUser) && !findItemByType(NamedGroup); } bool KACLListView::defaultMaskCanBeDeleted() const { return !findDefaultItemByType(NamedUser) && !findDefaultItemByType(NamedGroup); } #include "moc_kacleditwidget.cpp" #include "moc_kacleditwidget_p.cpp" #endif diff --git a/src/widgets/kdirmodel.cpp b/src/widgets/kdirmodel.cpp index 6edc91a8..b531bf52 100644 --- a/src/widgets/kdirmodel.cpp +++ b/src/widgets/kdirmodel.cpp @@ -1,1311 +1,1310 @@ /* This file is part of the KDE project Copyright (C) 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 "kdirmodel.h" #include "kdirlister.h" #include "kfileitem.h" #include "kio_widgets_debug.h" #include #include #include #include #include "joburlcache_p.h" #include #include #include #include #include #include #include #include #include -#include #include #include #ifdef Q_OS_WIN #include #endif class KDirModelNode; class KDirModelDirNode; static QUrl cleanupUrl(const QUrl &url) { QUrl u = url; u.setPath(QDir::cleanPath(u.path())); // remove double slashes in the path, simplify "foo/." to "foo/", etc. u = u.adjusted(QUrl::StripTrailingSlash); // KDirLister does this too, so we remove the slash before comparing with the root node url. u.setQuery(QString()); u.setFragment(QString()); return u; } // We create our own tree behind the scenes to have fast lookup from an item to its parent, // and also to get the children of an item fast. class KDirModelNode { public: KDirModelNode(KDirModelDirNode *parent, const KFileItem &item) : m_item(item), m_parent(parent), m_preview() { } virtual ~KDirModelNode() { // Required, code will delete ptrs to this or a subclass. } // m_item is KFileItem() for the root item const KFileItem &item() const { return m_item; } void setItem(const KFileItem &item) { m_item = item; } KDirModelDirNode *parent() const { return m_parent; } // linear search int rowNumber() const; // O(n) QIcon preview() const { return m_preview; } void setPreview(const QPixmap &pix) { m_preview = QIcon(); m_preview.addPixmap(pix); } void setPreview(const QIcon &icn) { m_preview = icn; } private: KFileItem m_item; KDirModelDirNode *const m_parent; QIcon m_preview; }; // Specialization for directory nodes class KDirModelDirNode : public KDirModelNode { public: KDirModelDirNode(KDirModelDirNode *parent, const KFileItem &item) : KDirModelNode(parent, item), m_childNodes(), m_childCount(KDirModel::ChildCountUnknown), m_populated(false) {} ~KDirModelDirNode() override { qDeleteAll(m_childNodes); } QList m_childNodes; // owns the nodes // If we listed the directory, the child count is known. Otherwise it can be set via setChildCount. int childCount() const { return m_childNodes.isEmpty() ? m_childCount : m_childNodes.count(); } void setChildCount(int count) { m_childCount = count; } bool isPopulated() const { return m_populated; } void setPopulated(bool populated) { m_populated = populated; } bool isSlow() const { return item().isSlow(); } // For removing all child urls from the global hash. void collectAllChildUrls(QList &urls) const { urls.reserve(urls.size() + m_childNodes.size()); Q_FOREACH (KDirModelNode *node, m_childNodes) { const KFileItem &item = node->item(); urls.append(cleanupUrl(item.url())); if (item.isDir()) { static_cast(node)->collectAllChildUrls(urls); } } } private: int m_childCount: 31; bool m_populated: 1; }; int KDirModelNode::rowNumber() const { if (!m_parent) { return 0; } return m_parent->m_childNodes.indexOf(const_cast(this)); } //// class KDirModelPrivate { public: explicit KDirModelPrivate(KDirModel *model) : q(model), m_dirLister(nullptr), m_rootNode(new KDirModelDirNode(nullptr, KFileItem())), m_dropsAllowed(KDirModel::NoDrops), m_jobTransfersVisible(false) { } ~KDirModelPrivate() { delete m_rootNode; } void _k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &); void _k_slotCompleted(const QUrl &directoryUrl); void _k_slotDeleteItems(const KFileItemList &); void _k_slotRefreshItems(const QList > &); void _k_slotClear(); void _k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl); void _k_slotJobUrlsChanged(const QStringList &urlList); void clear() { delete m_rootNode; m_rootNode = new KDirModelDirNode(nullptr, KFileItem()); } // Emit expand for each parent and then return the // last known parent if there is no node for this url KDirModelNode *expandAllParentsUntil(const QUrl &url) const; // Return the node for a given url, using the hash. KDirModelNode *nodeForUrl(const QUrl &url) const; KDirModelNode *nodeForIndex(const QModelIndex &index) const; QModelIndex indexForNode(KDirModelNode *node, int rowNumber = -1 /*unknown*/) const; bool isDir(KDirModelNode *node) const { return (node == m_rootNode) || node->item().isDir(); } QUrl urlForNode(KDirModelNode *node) const { /** * Queries and fragments are removed from the URL, so that the URL of * child items really starts with the URL of the parent. * * For instance ksvn+http://url?rev=100 is the parent for ksvn+http://url/file?rev=100 * so we have to remove the query in both to be able to compare the URLs */ QUrl url(node == m_rootNode ? m_dirLister->url() : node->item().url()); if (url.hasQuery() || url.hasFragment()) { // avoid detach if not necessary. url.setQuery(QString()); url.setFragment(QString()); // kill ref (#171117) } return url; } void removeFromNodeHash(KDirModelNode *node, const QUrl &url); void clearAllPreviews(KDirModelDirNode *node); #ifndef NDEBUG void dump(); #endif KDirModel * const q; KDirLister *m_dirLister; KDirModelDirNode *m_rootNode; KDirModel::DropsAllowed m_dropsAllowed; bool m_jobTransfersVisible; // key = current known parent node (always a KDirModelDirNode but KDirModelNode is more convenient), // value = final url[s] being fetched QMap > m_urlsBeingFetched; QHash m_nodeHash; // global node hash: url -> node QStringList m_allCurrentDestUrls; //list of all dest urls that have jobs on them (e.g. copy, download) }; KDirModelNode *KDirModelPrivate::nodeForUrl(const QUrl &_url) const // O(1), well, O(length of url as a string) { QUrl url = cleanupUrl(_url); if (url == urlForNode(m_rootNode)) { return m_rootNode; } return m_nodeHash.value(url); } void KDirModelPrivate::removeFromNodeHash(KDirModelNode *node, const QUrl &url) { if (node->item().isDir()) { QList urls; static_cast(node)->collectAllChildUrls(urls); for (const QUrl &u : qAsConst(urls)) { m_nodeHash.remove(u); } } m_nodeHash.remove(cleanupUrl(url)); } KDirModelNode *KDirModelPrivate::expandAllParentsUntil(const QUrl &_url) const // O(depth) { QUrl url = cleanupUrl(_url); //qDebug() << url; QUrl nodeUrl = urlForNode(m_rootNode); if (url == nodeUrl) { return m_rootNode; } // Protocol mismatch? Don't even start comparing paths then. #171721 if (url.scheme() != nodeUrl.scheme()) { return nullptr; } const QString pathStr = url.path(); // no trailing slash KDirModelDirNode *dirNode = m_rootNode; if (!pathStr.startsWith(nodeUrl.path())) { return nullptr; } for (;;) { QString nodePath = nodeUrl.path(); if (!nodePath.endsWith(QLatin1Char('/'))) { nodePath += QLatin1Char('/'); } if (!pathStr.startsWith(nodePath)) { qCWarning(KIO_WIDGETS) << "The kioslave for" << url.scheme() << "violates the hierarchy structure:" << "I arrived at node" << nodePath << ", but" << pathStr << "does not start with that path."; return nullptr; } // E.g. pathStr is /a/b/c and nodePath is /a/. We want to find the node with url /a/b const int nextSlash = pathStr.indexOf(QLatin1Char('/'), nodePath.length()); const QString newPath = pathStr.left(nextSlash); // works even if nextSlash==-1 nodeUrl.setPath(newPath); nodeUrl = nodeUrl.adjusted(QUrl::StripTrailingSlash); // #172508 KDirModelNode *node = nodeForUrl(nodeUrl); if (!node) { //qDebug() << "child equal or starting with" << url << "not found"; // return last parent found: return dirNode; } emit q->expand(indexForNode(node)); //qDebug() << " nodeUrl=" << nodeUrl; if (nodeUrl == url) { //qDebug() << "Found node" << node << "for" << url; return node; } //qDebug() << "going into" << node->item().url(); Q_ASSERT(isDir(node)); dirNode = static_cast(node); } // NOTREACHED //return 0; } #ifndef NDEBUG void KDirModelPrivate::dump() { qCDebug(KIO_WIDGETS) << "Dumping contents of KDirModel" << q << "dirLister url:" << m_dirLister->url(); QHashIterator it(m_nodeHash); while (it.hasNext()) { it.next(); qDebug(KIO_WIDGETS) << it.key() << it.value(); } } #endif // node -> index. If rowNumber is set (or node is root): O(1). Otherwise: O(n). QModelIndex KDirModelPrivate::indexForNode(KDirModelNode *node, int rowNumber) const { if (node == m_rootNode) { return QModelIndex(); } Q_ASSERT(node->parent()); return q->createIndex(rowNumber == -1 ? node->rowNumber() : rowNumber, 0, node); } // index -> node. O(1) KDirModelNode *KDirModelPrivate::nodeForIndex(const QModelIndex &index) const { return index.isValid() ? static_cast(index.internalPointer()) : m_rootNode; } /* * This model wraps the data held by KDirLister. * * The internal pointer of the QModelIndex for a given file is the node for that file in our own tree. * E.g. index(2,0) returns a QModelIndex with row=2 internalPointer= * * Invalid parent index means root of the tree, m_rootNode */ #ifndef NDEBUG static QString debugIndex(const QModelIndex &index) { QString str; if (!index.isValid()) { str = QStringLiteral("[invalid index, i.e. root]"); } else { KDirModelNode *node = static_cast(index.internalPointer()); str = QLatin1String("[index for ") + node->item().url().toString(); if (index.column() > 0) { str += QLatin1String(", column ") + QString::number(index.column()); } str += QLatin1Char(']'); } return str; } #endif KDirModel::KDirModel(QObject *parent) : QAbstractItemModel(parent), d(new KDirModelPrivate(this)) { setDirLister(new KDirLister(this)); } KDirModel::~KDirModel() { delete d; } void KDirModel::setDirLister(KDirLister *dirLister) { if (d->m_dirLister) { d->clear(); delete d->m_dirLister; } d->m_dirLister = dirLister; d->m_dirLister->setParent(this); connect(d->m_dirLister, &KCoreDirLister::itemsAdded, this, [this](const QUrl &dirUrl, const KFileItemList &items){d->_k_slotNewItems(dirUrl, items);} ); connect(d->m_dirLister, static_cast(&KCoreDirLister::completed), this, [this](const QUrl &dirUrl){d->_k_slotCompleted(dirUrl);} ); connect(d->m_dirLister, &KCoreDirLister::itemsDeleted, this, [this](const KFileItemList &items){d->_k_slotDeleteItems(items);} ); connect(d->m_dirLister, &KCoreDirLister::refreshItems, this, [this](const QList > &items){d->_k_slotRefreshItems(items);} ); connect(d->m_dirLister, QOverload<>::of(&KCoreDirLister::clear), this, [this](){d->_k_slotClear();} ); connect(d->m_dirLister, QOverload::of(&KCoreDirLister::redirection), this, [this](const QUrl &oldUrl, const QUrl &newUrl){d->_k_slotRedirection(oldUrl, newUrl);} ); } Qt::DropActions KDirModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction | Qt::IgnoreAction; } KDirLister *KDirModel::dirLister() const { return d->m_dirLister; } void KDirModelPrivate::_k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &items) { //qDebug() << "directoryUrl=" << directoryUrl; KDirModelNode *result = nodeForUrl(directoryUrl); // O(depth) // If the directory containing the items wasn't found, then we have a big problem. // Are you calling KDirLister::openUrl(url,true,false)? Please use expandToUrl() instead. if (!result) { qCWarning(KIO_WIDGETS) << "Items emitted in directory" << directoryUrl << "but that directory isn't in KDirModel!" << "Root directory:" << urlForNode(m_rootNode); for (const KFileItem &item : items) { qDebug() << "Item:" << item.url(); } #ifndef NDEBUG dump(); #endif Q_ASSERT(result); } Q_ASSERT(isDir(result)); KDirModelDirNode *dirNode = static_cast(result); const QModelIndex index = indexForNode(dirNode); // O(n) const int newItemsCount = items.count(); const int newRowCount = dirNode->m_childNodes.count() + newItemsCount; #if 0 #ifndef NDEBUG // debugIndex only defined in debug mode //qDebug() << items.count() << "in" << directoryUrl << "index=" << debugIndex(index) << "newRowCount=" << newRowCount; #endif #endif q->beginInsertRows(index, newRowCount - newItemsCount, newRowCount - 1); // parent, first, last const QList urlsBeingFetched = m_urlsBeingFetched.value(dirNode); //qDebug() << "urlsBeingFetched for dir" << dirNode << directoryUrl << ":" << urlsBeingFetched; QList emitExpandFor; dirNode->m_childNodes.reserve(newRowCount); KFileItemList::const_iterator it = items.begin(); KFileItemList::const_iterator end = items.end(); for (; it != end; ++it) { const bool isDir = it->isDir(); KDirModelNode *node = isDir ? new KDirModelDirNode(dirNode, *it) : new KDirModelNode(dirNode, *it); #ifndef NDEBUG // Test code for possible duplication of items in the childnodes list, // not sure if/how it ever happened. //if (dirNode->m_childNodes.count() && // dirNode->m_childNodes.last()->item().name() == (*it).name()) { // qCWarning(KIO_WIDGETS) << "Already having" << (*it).name() << "in" << directoryUrl // << "url=" << dirNode->m_childNodes.last()->item().url(); // abort(); //} #endif dirNode->m_childNodes.append(node); const QUrl url = it->url(); m_nodeHash.insert(cleanupUrl(url), node); //qDebug() << url; if (!urlsBeingFetched.isEmpty()) { const QUrl dirUrl(url); foreach (const QUrl &urlFetched, urlsBeingFetched) { if (dirUrl.matches(urlFetched, QUrl::StripTrailingSlash) || dirUrl.isParentOf(urlFetched)) { //qDebug() << "Listing found" << dirUrl.url() << "which is a parent of fetched url" << urlFetched; const QModelIndex parentIndex = indexForNode(node, dirNode->m_childNodes.count() - 1); Q_ASSERT(parentIndex.isValid()); emitExpandFor.append(parentIndex); if (isDir && dirUrl != urlFetched) { q->fetchMore(parentIndex); m_urlsBeingFetched[node].append(urlFetched); } } } } } q->endInsertRows(); // Emit expand signal after rowsInserted signal has been emitted, // so that any proxy model will have updated its mapping already Q_FOREACH (const QModelIndex &idx, emitExpandFor) { emit q->expand(idx); } } void KDirModelPrivate::_k_slotCompleted(const QUrl &directoryUrl) { KDirModelNode *result = nodeForUrl(directoryUrl); // O(depth) Q_ASSERT(isDir(result)); KDirModelDirNode *dirNode = static_cast(result); m_urlsBeingFetched.remove(dirNode); } void KDirModelPrivate::_k_slotDeleteItems(const KFileItemList &items) { //qDebug() << items.count(); // I assume all items are from the same directory. // From KDirLister's code, this should be the case, except maybe emitChanges? const KFileItem item = items.first(); Q_ASSERT(!item.isNull()); QUrl url = item.url(); KDirModelNode *node = nodeForUrl(url); // O(depth) if (!node) { qCWarning(KIO_WIDGETS) << "No node found for item that was just removed:" << url; return; } KDirModelDirNode *dirNode = node->parent(); if (!dirNode) { return; } QModelIndex parentIndex = indexForNode(dirNode); // O(n) // Short path for deleting a single item if (items.count() == 1) { const int r = node->rowNumber(); q->beginRemoveRows(parentIndex, r, r); removeFromNodeHash(node, url); delete dirNode->m_childNodes.takeAt(r); q->endRemoveRows(); return; } // We need to make lists of consecutive row numbers, for the beginRemoveRows call. // Let's use a bit array where each bit represents a given child node. const int childCount = dirNode->m_childNodes.count(); QBitArray rowNumbers(childCount, false); for (const KFileItem &item : items) { if (!node) { // don't lookup the first item twice url = item.url(); node = nodeForUrl(url); if (!node) { qCWarning(KIO_WIDGETS) << "No node found for item that was just removed:" << url; continue; } if (!node->parent()) { // The root node has been deleted, but it was not first in the list 'items'. // see https://bugs.kde.org/show_bug.cgi?id=196695 return; } } rowNumbers.setBit(node->rowNumber(), 1); // O(n) removeFromNodeHash(node, url); node = nullptr; } int start = -1; int end = -1; bool lastVal = false; // Start from the end, otherwise all the row numbers are offset while we go for (int i = childCount - 1; i >= 0; --i) { const bool val = rowNumbers.testBit(i); if (!lastVal && val) { end = i; //qDebug() << "end=" << end; } if ((lastVal && !val) || (i == 0 && val)) { start = val ? i : i + 1; //qDebug() << "beginRemoveRows" << start << end; q->beginRemoveRows(parentIndex, start, end); for (int r = end; r >= start; --r) { // reverse because takeAt changes indexes ;) //qDebug() << "Removing from m_childNodes at" << r; delete dirNode->m_childNodes.takeAt(r); } q->endRemoveRows(); } lastVal = val; } } void KDirModelPrivate::_k_slotRefreshItems(const QList > &items) { QModelIndex topLeft, bottomRight; // Solution 1: we could emit dataChanged for one row (if items.size()==1) or all rows // Solution 2: more fine-grained, actually figure out the beginning and end rows. for (QList >::const_iterator fit = items.begin(), fend = items.end(); fit != fend; ++fit) { Q_ASSERT(!fit->first.isNull()); Q_ASSERT(!fit->second.isNull()); const QUrl oldUrl = fit->first.url(); const QUrl newUrl = fit->second.url(); KDirModelNode *node = nodeForUrl(oldUrl); // O(n); maybe we could look up to the parent only once //qDebug() << "in model for" << m_dirLister->url() << ":" << oldUrl << "->" << newUrl << "node=" << node; if (!node) { // not found [can happen when renaming a dir, redirection was emitted already] continue; } if (node != m_rootNode) { // we never set an item in the rootnode, we use m_dirLister->rootItem instead. bool hasNewNode = false; // A file became directory (well, it was overwritten) if (fit->first.isDir() != fit->second.isDir()) { //qDebug() << "DIR/FILE STATUS CHANGE"; const int r = node->rowNumber(); removeFromNodeHash(node, oldUrl); KDirModelDirNode *dirNode = node->parent(); delete dirNode->m_childNodes.takeAt(r); // i.e. "delete node" node = fit->second.isDir() ? new KDirModelDirNode(dirNode, fit->second) : new KDirModelNode(dirNode, fit->second); dirNode->m_childNodes.insert(r, node); // same position! hasNewNode = true; } else { node->setItem(fit->second); } if (oldUrl != newUrl || hasNewNode) { // What if a renamed dir had children? -> kdirlister takes care of emitting for each item //qDebug() << "Renaming" << oldUrl << "to" << newUrl << "in node hash"; m_nodeHash.remove(cleanupUrl(oldUrl)); m_nodeHash.insert(cleanupUrl(newUrl), node); } // Mimetype changed -> forget cached icon (e.g. from "cut", #164185 comment #13) if (fit->first.determineMimeType().name() != fit->second.determineMimeType().name()) { node->setPreview(QIcon()); } const QModelIndex index = indexForNode(node); if (!topLeft.isValid() || index.row() < topLeft.row()) { topLeft = index; } if (!bottomRight.isValid() || index.row() > bottomRight.row()) { bottomRight = index; } } } #ifndef NDEBUG // debugIndex only defined in debug mode //qDebug() << "dataChanged(" << debugIndex(topLeft) << " - " << debugIndex(bottomRight); Q_UNUSED(debugIndex(QModelIndex())); // fix compiler warning #endif bottomRight = bottomRight.sibling(bottomRight.row(), q->columnCount(QModelIndex()) - 1); emit q->dataChanged(topLeft, bottomRight); } // Called when a kioslave redirects (e.g. smb:/Workgroup -> smb://workgroup) // and when renaming a directory. void KDirModelPrivate::_k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl) { KDirModelNode *node = nodeForUrl(oldUrl); if (!node) { return; } m_nodeHash.remove(cleanupUrl(oldUrl)); m_nodeHash.insert(cleanupUrl(newUrl), node); // Ensure the node's URL is updated. In case of a listjob redirection // we won't get a refreshItem, and in case of renaming a directory // we'll get it too late (so the hash won't find the old url anymore). KFileItem item = node->item(); if (!item.isNull()) { // null if root item, #180156 item.setUrl(newUrl); node->setItem(item); } // The items inside the renamed directory have been handled before, // KDirLister took care of emitting refreshItem for each of them. } void KDirModelPrivate::_k_slotClear() { const int numRows = m_rootNode->m_childNodes.count(); if (numRows > 0) { q->beginRemoveRows(QModelIndex(), 0, numRows - 1); q->endRemoveRows(); } m_nodeHash.clear(); //emit layoutAboutToBeChanged(); clear(); //emit layoutChanged(); } void KDirModelPrivate::_k_slotJobUrlsChanged(const QStringList &urlList) { QStringList dirtyUrls; std::set_symmetric_difference(urlList.begin(), urlList.end(), m_allCurrentDestUrls.constBegin(), m_allCurrentDestUrls.constEnd(), std::back_inserter(dirtyUrls)); m_allCurrentDestUrls = urlList; for (const QString &dirtyUrl : qAsConst(dirtyUrls)) { if (KDirModelNode *node = nodeForUrl(QUrl(dirtyUrl))) { const QModelIndex idx = indexForNode(node); emit q->dataChanged(idx, idx, {KDirModel::HasJobRole}); } } } void KDirModelPrivate::clearAllPreviews(KDirModelDirNode *dirNode) { const int numRows = dirNode->m_childNodes.count(); if (numRows > 0) { KDirModelNode *lastNode = nullptr; for (KDirModelNode *node : qAsConst(dirNode->m_childNodes)) { node->setPreview(QIcon()); //node->setPreview(QIcon::fromTheme(node->item().iconName())); if (isDir(node)) { // recurse into child dirs clearAllPreviews(static_cast(node)); } lastNode = node; } emit q->dataChanged(indexForNode(dirNode->m_childNodes.at(0), 0), // O(1) indexForNode(lastNode, numRows - 1)); // O(1) } } void KDirModel::clearAllPreviews() { d->clearAllPreviews(d->m_rootNode); } void KDirModel::itemChanged(const QModelIndex &index) { // This method is really a itemMimeTypeChanged(), it's mostly called by KFilePreviewGenerator. // When the mimetype is determined, clear the old "preview" (could be // mimetype dependent like when cutting files, #164185) KDirModelNode *node = d->nodeForIndex(index); if (node) { node->setPreview(QIcon()); } #ifndef NDEBUG // debugIndex only defined in debug mode //qDebug() << "dataChanged(" << debugIndex(index); #endif emit dataChanged(index, index); } int KDirModel::columnCount(const QModelIndex &) const { return ColumnCount; } QVariant KDirModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { KDirModelNode *node = static_cast(index.internalPointer()); const KFileItem &item(node->item()); switch (role) { case Qt::DisplayRole: switch (index.column()) { case Name: return item.text(); case Size: return KIO::convertSize(item.size()); // size formatted as QString case ModifiedTime: { QDateTime dt = item.time(KFileItem::ModificationTime); return dt.toString(Qt::SystemLocaleShortDate); } case Permissions: return item.permissionsString(); case Owner: return item.user(); case Group: return item.group(); case Type: return item.mimeComment(); } break; case Qt::EditRole: switch (index.column()) { case Name: return item.text(); } break; case Qt::DecorationRole: if (index.column() == Name) { if (!node->preview().isNull()) { //qDebug() << item->url() << " preview found"; return node->preview(); } Q_ASSERT(!item.isNull()); //qDebug() << item->url() << " overlays=" << item->overlays(); return KDE::icon(item.iconName(), item.overlays()); } break; case Qt::TextAlignmentRole: if (index.column() == Size) { // use a right alignment for L2R and R2L languages const Qt::Alignment alignment = Qt::AlignRight | Qt::AlignVCenter; return int(alignment); } break; case Qt::ToolTipRole: return item.text(); case FileItemRole: return QVariant::fromValue(item); case ChildCountRole: if (!item.isDir()) { return ChildCountUnknown; } else { KDirModelDirNode *dirNode = static_cast(node); int count = dirNode->childCount(); if (count == ChildCountUnknown && item.isReadable() && !dirNode->isSlow()) { const QString path = item.localPath(); if (!path.isEmpty()) { // slow // QDir dir(path); // count = dir.entryList(QDir::AllEntries|QDir::NoDotAndDotDot|QDir::System).count(); #ifdef Q_OS_WIN QString s = path + QLatin1String("\\*.*"); s.replace(QLatin1Char('/'), QLatin1Char('\\')); count = 0; WIN32_FIND_DATA findData; HANDLE hFile = FindFirstFile((LPWSTR)s.utf16(), &findData); if (hFile != INVALID_HANDLE_VALUE) { do { if (!(findData.cFileName[0] == '.' && findData.cFileName[1] == '\0') && !(findData.cFileName[0] == '.' && findData.cFileName[1] == '.' && findData.cFileName[2] == '\0')) { ++count; } } while (FindNextFile(hFile, &findData) != 0); FindClose(hFile); } #else DIR *dir = QT_OPENDIR(QFile::encodeName(path).constData()); if (dir) { count = 0; QT_DIRENT *dirEntry = nullptr; while ((dirEntry = QT_READDIR(dir))) { if (dirEntry->d_name[0] == '.') { if (dirEntry->d_name[1] == '\0') { // skip "." continue; } if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') { // skip ".." continue; } } ++count; } QT_CLOSEDIR(dir); } #endif //qDebug() << "child count for " << path << ":" << count; dirNode->setChildCount(count); } } return count; } case HasJobRole: if (d->m_jobTransfersVisible && d->m_allCurrentDestUrls.isEmpty() == false) { KDirModelNode *node = d->nodeForIndex(index); const QString url = node->item().url().toString(); //return whether or not there are job dest urls visible in the view, so the delegate knows which ones to paint. return QVariant(d->m_allCurrentDestUrls.contains(url)); } } } return QVariant(); } void KDirModel::sort(int column, Qt::SortOrder order) { // Not implemented - we should probably use QSortFilterProxyModel instead. QAbstractItemModel::sort(column, order); } bool KDirModel::setData(const QModelIndex &index, const QVariant &value, int role) { switch (role) { case Qt::EditRole: if (index.column() == Name && value.type() == QVariant::String) { Q_ASSERT(index.isValid()); KDirModelNode *node = static_cast(index.internalPointer()); const KFileItem &item = node->item(); const QString newName = value.toString(); if (newName.isEmpty() || newName == item.text() || (newName == QLatin1String(".")) || (newName == QLatin1String(".."))) { return true; } QUrl newUrl = item.url().adjusted(QUrl::RemoveFilename); newUrl.setPath(newUrl.path() + KIO::encodeFileName(newName)); KIO::Job *job = KIO::rename(item.url(), newUrl, item.url().isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags); job->uiDelegate()->setAutoErrorHandlingEnabled(true); // undo handling KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Rename, QList() << item.url(), newUrl, job); return true; } break; case Qt::DecorationRole: if (index.column() == Name) { Q_ASSERT(index.isValid()); // Set new pixmap - e.g. preview KDirModelNode *node = static_cast(index.internalPointer()); //qDebug() << "setting icon for " << node->item()->url(); Q_ASSERT(node); if (value.type() == QVariant::Icon) { const QIcon icon(qvariant_cast(value)); node->setPreview(icon); } else if (value.type() == QVariant::Pixmap) { node->setPreview(qvariant_cast(value)); } emit dataChanged(index, index); return true; } break; default: break; } return false; } int KDirModel::rowCount(const QModelIndex &parent) const { KDirModelNode *node = d->nodeForIndex(parent); if (!node || !d->isDir(node)) { // #176555 return 0; } KDirModelDirNode *parentNode = static_cast(node); Q_ASSERT(parentNode); const int count = parentNode->m_childNodes.count(); #if 0 QStringList filenames; for (int i = 0; i < count; ++i) { filenames << d->urlForNode(parentNode->m_childNodes.at(i)).fileName(); } //qDebug() << "rowCount for " << d->urlForNode(parentNode) << ": " << count << filenames; #endif return count; } QModelIndex KDirModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } KDirModelNode *childNode = static_cast(index.internalPointer()); Q_ASSERT(childNode); KDirModelNode *parentNode = childNode->parent(); Q_ASSERT(parentNode); return d->indexForNode(parentNode); // O(n) } // Reimplemented to avoid the default implementation which calls parent // (O(n) for finding the parent's row number for nothing). This implementation is O(1). QModelIndex KDirModel::sibling(int row, int column, const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } KDirModelNode *oldChildNode = static_cast(index.internalPointer()); Q_ASSERT(oldChildNode); KDirModelNode *parentNode = oldChildNode->parent(); Q_ASSERT(parentNode); Q_ASSERT(d->isDir(parentNode)); KDirModelNode *childNode = static_cast(parentNode)->m_childNodes.value(row); // O(1) if (childNode) { return createIndex(row, column, childNode); } return QModelIndex(); } void KDirModel::requestSequenceIcon(const QModelIndex &index, int sequenceIndex) { emit needSequenceIcon(index, sequenceIndex); } void KDirModel::setJobTransfersVisible(bool value) { if (value) { d->m_jobTransfersVisible = true; connect(&JobUrlCache::instance(), SIGNAL(jobUrlsChanged(QStringList)), this, SLOT(_k_slotJobUrlsChanged(QStringList)), Qt::UniqueConnection); JobUrlCache::instance().requestJobUrlsChanged(); } else { disconnect(this, SLOT(_k_slotJobUrlsChanged(QStringList))); } } bool KDirModel::jobTransfersVisible() const { return d->m_jobTransfersVisible; } QList KDirModel::simplifiedUrlList(const QList &urls) { if (urls.isEmpty()) { return urls; } QList ret(urls); std::sort(ret.begin(), ret.end()); QList::iterator it = ret.begin(); QUrl url = *it; ++it; while (it != ret.end()) { if (url.isParentOf(*it) || url == *it) { it = ret.erase(it); } else { url = *it; ++it; } } return ret; } QStringList KDirModel::mimeTypes() const { return KUrlMimeData::mimeDataTypes(); } QMimeData *KDirModel::mimeData(const QModelIndexList &indexes) const { QList urls, mostLocalUrls; urls.reserve(indexes.size()); mostLocalUrls.reserve(indexes.size()); bool canUseMostLocalUrls = true; for (const QModelIndex &index : indexes) { const KFileItem &item = d->nodeForIndex(index)->item(); urls << item.url(); bool isLocal; mostLocalUrls << item.mostLocalUrl(isLocal); if (!isLocal) { canUseMostLocalUrls = false; } } QMimeData *data = new QMimeData(); const bool different = canUseMostLocalUrls && (mostLocalUrls != urls); urls = simplifiedUrlList(urls); if (different) { mostLocalUrls = simplifiedUrlList(mostLocalUrls); KUrlMimeData::setUrls(urls, mostLocalUrls, data); } else { data->setUrls(urls); } return data; } // Public API; not much point in calling it internally KFileItem KDirModel::itemForIndex(const QModelIndex &index) const { if (!index.isValid()) { return d->m_dirLister->rootItem(); } else { return static_cast(index.internalPointer())->item(); } } #ifndef KIOWIDGETS_NO_DEPRECATED QModelIndex KDirModel::indexForItem(const KFileItem *item) const { // Note that we can only use the URL here, not the pointer. // KFileItems can be copied. return indexForUrl(item->url()); // O(n) } #endif QModelIndex KDirModel::indexForItem(const KFileItem &item) const { // Note that we can only use the URL here, not the pointer. // KFileItems can be copied. return indexForUrl(item.url()); // O(n) } // url -> index. O(n) QModelIndex KDirModel::indexForUrl(const QUrl &url) const { KDirModelNode *node = d->nodeForUrl(url); // O(depth) if (!node) { //qDebug() << url << "not found"; return QModelIndex(); } return d->indexForNode(node); // O(n) } QModelIndex KDirModel::index(int row, int column, const QModelIndex &parent) const { KDirModelNode *parentNode = d->nodeForIndex(parent); // O(1) Q_ASSERT(parentNode); if (d->isDir(parentNode)) { KDirModelNode *childNode = static_cast(parentNode)->m_childNodes.value(row); // O(1) if (childNode) { return createIndex(row, column, childNode); } } return QModelIndex(); } QVariant KDirModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { switch (role) { case Qt::DisplayRole: switch (section) { case Name: return i18nc("@title:column", "Name"); case Size: return i18nc("@title:column", "Size"); case ModifiedTime: return i18nc("@title:column", "Date"); case Permissions: return i18nc("@title:column", "Permissions"); case Owner: return i18nc("@title:column", "Owner"); case Group: return i18nc("@title:column", "Group"); case Type: return i18nc("@title:column", "Type"); } } } return QVariant(); } bool KDirModel::hasChildren(const QModelIndex &parent) const { if (!parent.isValid()) { return true; } const KFileItem &parentItem = static_cast(parent.internalPointer())->item(); Q_ASSERT(!parentItem.isNull()); return parentItem.isDir(); } Qt::ItemFlags KDirModel::flags(const QModelIndex &index) const { Qt::ItemFlags f = Qt::ItemIsEnabled; if (index.column() == Name) { f |= Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; } // Allow dropping onto this item? if (d->m_dropsAllowed != NoDrops) { if (!index.isValid()) { if (d->m_dropsAllowed & DropOnDirectory) { f |= Qt::ItemIsDropEnabled; } } else { KFileItem item = itemForIndex(index); if (item.isNull()) { qCWarning(KIO_WIDGETS) << "Invalid item returned for index"; } else if (item.isDir()) { if (d->m_dropsAllowed & DropOnDirectory) { f |= Qt::ItemIsDropEnabled; } } else { // regular file item if (d->m_dropsAllowed & DropOnAnyFile) { f |= Qt::ItemIsDropEnabled; } else if (d->m_dropsAllowed & DropOnLocalExecutable) { if (!item.localPath().isEmpty()) { // Desktop file? if (item.determineMimeType().inherits(QStringLiteral("application/x-desktop"))) { f |= Qt::ItemIsDropEnabled; } // Executable, shell script ... ? else if (QFileInfo(item.localPath()).isExecutable()) { f |= Qt::ItemIsDropEnabled; } } } } } } return f; } bool KDirModel::canFetchMore(const QModelIndex &parent) const { if (!parent.isValid()) { return false; } // We now have a bool KDirModelNode::m_populated, // to avoid calling fetchMore more than once on empty dirs. // But this wastes memory, and how often does someone open and re-open an empty dir in a treeview? // Maybe we can ask KDirLister "have you listed already"? (to discuss with M. Brade) KDirModelNode *node = static_cast(parent.internalPointer()); const KFileItem &item = node->item(); return item.isDir() && !static_cast(node)->isPopulated() && static_cast(node)->m_childNodes.isEmpty(); } void KDirModel::fetchMore(const QModelIndex &parent) { if (!parent.isValid()) { return; } KDirModelNode *parentNode = static_cast(parent.internalPointer()); KFileItem parentItem = parentNode->item(); Q_ASSERT(!parentItem.isNull()); if (!parentItem.isDir()) { return; } KDirModelDirNode *dirNode = static_cast(parentNode); if (dirNode->isPopulated()) { return; } dirNode->setPopulated(true); const QUrl parentUrl = parentItem.url(); d->m_dirLister->openUrl(parentUrl, KDirLister::Keep); } bool KDirModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { // Not sure we want to implement any drop handling at this level, // but for sure the default QAbstractItemModel implementation makes no sense for a dir model. Q_UNUSED(data); Q_UNUSED(action); Q_UNUSED(row); Q_UNUSED(column); Q_UNUSED(parent); return false; } void KDirModel::setDropsAllowed(DropsAllowed dropsAllowed) { d->m_dropsAllowed = dropsAllowed; } void KDirModel::expandToUrl(const QUrl &url) { // emit expand for each parent and return last parent KDirModelNode *result = d->expandAllParentsUntil(url); // O(depth) //qDebug() << url << result; if (!result) { // doesn't seem related to our base url? return; } if (!(result->item().isNull()) && result->item().url() == url) { // We have it already, nothing to do //qDebug() << "have it already item=" <item()*/; return; } d->m_urlsBeingFetched[result].append(url); if (result == d->m_rootNode) { //qDebug() << "Remembering to emit expand after listing the root url"; // the root is fetched by default, so it must be currently being fetched return; } //qDebug() << "Remembering to emit expand after listing" << result->item().url(); // start a new fetch to look for the next level down the URL const QModelIndex parentIndex = d->indexForNode(result); // O(n) Q_ASSERT(parentIndex.isValid()); fetchMore(parentIndex); } bool KDirModel::insertRows(int, int, const QModelIndex &) { return false; } bool KDirModel::insertColumns(int, int, const QModelIndex &) { return false; } bool KDirModel::removeRows(int, int, const QModelIndex &) { return false; } bool KDirModel::removeColumns(int, int, const QModelIndex &) { return false; } #include "moc_kdirmodel.cpp" diff --git a/src/widgets/kdynamicjobtracker.cpp b/src/widgets/kdynamicjobtracker.cpp index 1cd62b39..d8a8063c 100644 --- a/src/widgets/kdynamicjobtracker.cpp +++ b/src/widgets/kdynamicjobtracker.cpp @@ -1,171 +1,169 @@ /* * Copyright 2008 by Rob Scheepmaker * Copyright 2010 Shaun Reich * * 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.1 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 St, Fifth Floor, * Boston, MA 02110-1301 USA */ #include "kdynamicjobtracker_p.h" #include "kio_widgets_debug.h" #include "kuiserver_interface.h" #include #include #include #include #include #include #include -#include #include -#include struct AllTrackers { KUiServerJobTracker *kuiserverTracker; KWidgetJobTracker *widgetTracker; }; class Q_DECL_HIDDEN KDynamicJobTracker::Private { public: Private() : kuiserverTracker(nullptr), widgetTracker(nullptr) { } ~Private() { delete kuiserverTracker; delete widgetTracker; } KUiServerJobTracker *kuiserverTracker; KWidgetJobTracker *widgetTracker; QMap trackers; }; KDynamicJobTracker::KDynamicJobTracker(QObject *parent) : KJobTrackerInterface(parent), d(new Private) { } KDynamicJobTracker::~KDynamicJobTracker() { delete d; } void KDynamicJobTracker::registerJob(KJob *job) { if (d->trackers.contains(job)) { return; } // only interested in finished() signal, // so catching ourselves instead of using KJobTrackerInterface::registerJob() connect(job, &KJob::finished, this, &KDynamicJobTracker::unregisterJob); const bool canHaveWidgets = (qobject_cast(qApp) != nullptr); // always add an entry, even with no trackers used at all, // so unregisterJob() will work as normal AllTrackers& trackers = d->trackers[job]; // do not try to query kuiserver if dbus is not available if (!QDBusConnection::sessionBus().interface()) { if (canHaveWidgets) { // fallback to widget tracker only! if (!d->widgetTracker) { d->widgetTracker = new KWidgetJobTracker(); } trackers.widgetTracker = d->widgetTracker; trackers.widgetTracker->registerJob(job); } else { trackers.widgetTracker = nullptr; } trackers.kuiserverTracker = nullptr; } else { if (!d->kuiserverTracker) { d->kuiserverTracker = new KUiServerJobTracker(); } trackers.kuiserverTracker = d->kuiserverTracker; trackers.kuiserverTracker->registerJob(job); trackers.widgetTracker = nullptr; if (canHaveWidgets) { org::kde::kuiserver interface(QStringLiteral("org.kde.kuiserver"), QStringLiteral("/JobViewServer"), QDBusConnection::sessionBus(), this); QDBusReply reply = interface.requiresJobTracker(); // If kuiserver isn't available or it tells us a job tracker is required // create a widget tracker. if (!reply.isValid() || reply.value()) { if (!d->widgetTracker) { d->widgetTracker = new KWidgetJobTracker(); } trackers.widgetTracker = d->widgetTracker; trackers.widgetTracker->registerJob(job); } } } } void KDynamicJobTracker::unregisterJob(KJob *job) { job->disconnect(this); QMap::Iterator it = d->trackers.find(job); if (it == d->trackers.end()) { qCWarning(KIO_WIDGETS) << "Tried to unregister a kio job that hasn't been registered."; return; } const AllTrackers& trackers = it.value(); KUiServerJobTracker *kuiserverTracker = trackers.kuiserverTracker; KWidgetJobTracker *widgetTracker = trackers.widgetTracker; if (kuiserverTracker) { kuiserverTracker->unregisterJob(job); } if (widgetTracker) { widgetTracker->unregisterJob(job); } d->trackers.erase(it); } Q_GLOBAL_STATIC(KDynamicJobTracker, globalJobTracker) // Simply linking to this library, creates a GUI job tracker for all KIO jobs static int registerDynamicJobTracker() { KIO::setJobTracker(globalJobTracker()); return 0; // something } Q_CONSTRUCTOR_FUNCTION(registerDynamicJobTracker) #include "moc_kdynamicjobtracker_p.cpp" diff --git a/src/widgets/kpropertiesdialog.cpp b/src/widgets/kpropertiesdialog.cpp index 5343e84c..23135fbc 100644 --- a/src/widgets/kpropertiesdialog.cpp +++ b/src/widgets/kpropertiesdialog.cpp @@ -1,3929 +1,3926 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (c) 1999, 2000 Preston Brown Copyright (c) 2000 Simon Hausmann Copyright (c) 2000 David Faure Copyright (c) 2003 Waldo Bastian This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /* * kpropertiesdialog.cpp * View/Edit Properties of files, locally or remotely * * some FilePermissionsPropsPlugin-changes by * Henner Zeller * some layout management by * Bertrand Leconte * the rest of the layout management, bug fixes, adaptation to libkio, * template feature by * David Faure * More layout, cleanups, and fixes by * Preston Brown * Plugin capability, cleanups and port to KDialog by * Simon Hausmann * KDesktopPropsPlugin by * Waldo Bastian */ #include "kpropertiesdialog.h" #include "kpropertiesdialog_p.h" #include "kio_widgets_debug.h" #include "../pathhelpers_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include -#include #include #include #include #include #include -#include -#include #include #include +#include #if HAVE_POSIX_ACL extern "C" { # include # include } #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_checksumswidget.h" #include "ui_kpropertiesdesktopbase.h" #include "ui_kpropertiesdesktopadvbase.h" #if HAVE_POSIX_ACL #include "kacleditwidget.h" #endif #include #include #ifdef Q_OS_WIN #include #include #include #ifdef __GNUC__ # warning TODO: port completely to win32 #endif #endif using namespace KDEPrivate; static QString nameFromFileName(QString nameStr) { if (nameStr.endsWith(QLatin1String(".desktop"))) { nameStr.chop(8); } if (nameStr.endsWith(QLatin1String(".kdelnk"))) { nameStr.chop(7); } // Make it human-readable (%2F => '/', ...) nameStr = KIO::decodeFileName(nameStr); return nameStr; } const mode_t KFilePermissionsPropsPlugin::fperm[3][4] = { {S_IRUSR, S_IWUSR, S_IXUSR, S_ISUID}, {S_IRGRP, S_IWGRP, S_IXGRP, S_ISGID}, {S_IROTH, S_IWOTH, S_IXOTH, S_ISVTX} }; class Q_DECL_HIDDEN KPropertiesDialog::KPropertiesDialogPrivate { public: explicit KPropertiesDialogPrivate(KPropertiesDialog *qq) : q(qq) , m_aborted(false) , fileSharePage(nullptr) { } ~KPropertiesDialogPrivate() { } /** * Common initialization for all constructors */ void init(); /** * Inserts all pages in the dialog. */ void insertPages(); KPropertiesDialog * const q; bool m_aborted; QWidget *fileSharePage; /** * The URL of the props dialog (when shown for only one file) */ QUrl m_singleUrl; /** * List of items this props dialog is shown for */ KFileItemList m_items; /** * For templates */ QString m_defaultName; QUrl m_currentDir; /** * List of all plugins inserted ( first one first ) */ QList m_pageList; }; KPropertiesDialog::KPropertiesDialog(const KFileItem &item, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(item.name()))); Q_ASSERT(!item.isNull()); d->m_items.append(item); d->m_singleUrl = item.url(); Q_ASSERT(!d->m_singleUrl.isEmpty()); d->init(); } KPropertiesDialog::KPropertiesDialog(const QString &title, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { setWindowTitle(i18n("Properties for %1", title)); d->init(); } KPropertiesDialog::KPropertiesDialog(const KFileItemList &_items, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { if (_items.count() > 1) { setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", _items.count())); } else { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_items.first().name()))); } Q_ASSERT(!_items.isEmpty()); d->m_singleUrl = _items.first().url(); Q_ASSERT(!d->m_singleUrl.isEmpty()); d->m_items = _items; d->init(); } KPropertiesDialog::KPropertiesDialog(const QUrl &_url, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_url.fileName()))); d->m_singleUrl = _url; KIO::StatJob *job = KIO::stat(_url); KJobWidgets::setWindow(job, parent); job->exec(); KIO::UDSEntry entry = job->statResult(); d->m_items.append(KFileItem(entry, _url)); d->init(); } KPropertiesDialog::KPropertiesDialog(const QList& urls, QWidget* parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { if (urls.count() > 1) { setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", urls.count())); } else { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(urls.first().fileName()))); } Q_ASSERT(!urls.isEmpty()); d->m_singleUrl = urls.first(); Q_ASSERT(!d->m_singleUrl.isEmpty()); d->m_items.reserve(urls.size()); for (const QUrl& url : urls) { KIO::StatJob *job = KIO::stat(url); KJobWidgets::setWindow(job, parent); job->exec(); KIO::UDSEntry entry = job->statResult(); d->m_items.append(KFileItem(entry, url)); } d->init(); } KPropertiesDialog::KPropertiesDialog(const QUrl &_tempUrl, const QUrl &_currentDir, const QString &_defaultName, QWidget *parent) : KPageDialog(parent), d(new KPropertiesDialogPrivate(this)) { setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_tempUrl.fileName()))); d->m_singleUrl = _tempUrl; d->m_defaultName = _defaultName; d->m_currentDir = _currentDir; Q_ASSERT(!d->m_singleUrl.isEmpty()); // Create the KFileItem for the _template_ file, in order to read from it. d->m_items.append(KFileItem(d->m_singleUrl)); d->init(); } #ifdef Q_OS_WIN bool showWin32FilePropertyDialog(const QString &fileName) { QString path_ = QDir::toNativeSeparators(QFileInfo(fileName).absoluteFilePath()); #ifndef _WIN32_WCE SHELLEXECUTEINFOW execInfo; #else SHELLEXECUTEINFO execInfo; #endif memset(&execInfo, 0, sizeof(execInfo)); execInfo.cbSize = sizeof(execInfo); #ifndef _WIN32_WCE execInfo.fMask = SEE_MASK_INVOKEIDLIST | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI; #else execInfo.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI; #endif const QString verb(QLatin1String("properties")); execInfo.lpVerb = (LPCWSTR)verb.utf16(); execInfo.lpFile = (LPCWSTR)path_.utf16(); #ifndef _WIN32_WCE return ShellExecuteExW(&execInfo); #else return ShellExecuteEx(&execInfo); //There is no native file property dialog in wince // return false; #endif } #endif bool KPropertiesDialog::showDialog(const KFileItem &item, QWidget *parent, bool modal) { // TODO: do we really want to show the win32 property dialog? // This means we lose metainfo, support for .desktop files, etc. (DF) #ifdef Q_OS_WIN QString localPath = item.localPath(); if (!localPath.isEmpty()) { return showWin32FilePropertyDialog(localPath); } #endif KPropertiesDialog *dlg = new KPropertiesDialog(item, parent); if (modal) { dlg->exec(); } else { dlg->show(); } return true; } bool KPropertiesDialog::showDialog(const QUrl &_url, QWidget *parent, bool modal) { #ifdef Q_OS_WIN if (_url.isLocalFile()) { return showWin32FilePropertyDialog(_url.toLocalFile()); } #endif KPropertiesDialog *dlg = new KPropertiesDialog(_url, parent); if (modal) { dlg->exec(); } else { dlg->show(); } return true; } bool KPropertiesDialog::showDialog(const KFileItemList &_items, QWidget *parent, bool modal) { if (_items.count() == 1) { const KFileItem item = _items.first(); if (item.entry().count() == 0 && item.localPath().isEmpty()) // this remote item wasn't listed by a slave // Let's stat to get more info on the file { return KPropertiesDialog::showDialog(item.url(), parent, modal); } else { return KPropertiesDialog::showDialog(_items.first(), parent, modal); } } KPropertiesDialog *dlg = new KPropertiesDialog(_items, parent); if (modal) { dlg->exec(); } else { dlg->show(); } return true; } bool KPropertiesDialog::showDialog(const QList &urls, QWidget* parent, bool modal) { KPropertiesDialog *dlg = new KPropertiesDialog(urls, parent); if (modal) { dlg->exec(); } else { dlg->show(); } return true; } void KPropertiesDialog::KPropertiesDialogPrivate::init() { q->setFaceType(KPageDialog::Tabbed); insertPages(); KConfigGroup group(KSharedConfig::openConfig(), "KPropertiesDialog"); KWindowConfig::restoreWindowSize(q->windowHandle(), group); } void KPropertiesDialog::showFileSharingPage() { if (d->fileSharePage) { // FIXME: this showFileSharingPage thingy looks broken! (tokoe) // showPage( pageIndex( d->fileSharePage)); } } void KPropertiesDialog::setFileSharingPage(QWidget *page) { d->fileSharePage = page; } void KPropertiesDialog::setFileNameReadOnly(bool ro) { foreach (KPropertiesDialogPlugin *it, d->m_pageList) { if (auto *filePropsPlugin = qobject_cast(it)) { filePropsPlugin->setFileNameReadOnly(ro); } else if (auto *urlPropsPlugin = qobject_cast(it)) { urlPropsPlugin->setFileNameReadOnly(ro); } } } KPropertiesDialog::~KPropertiesDialog() { qDeleteAll(d->m_pageList); delete d; KConfigGroup group(KSharedConfig::openConfig(), "KPropertiesDialog"); KWindowConfig::saveWindowSize(windowHandle(), group, KConfigBase::Persistent); } void KPropertiesDialog::insertPlugin(KPropertiesDialogPlugin *plugin) { connect(plugin, &KPropertiesDialogPlugin::changed, plugin, QOverload<>::of(&KPropertiesDialogPlugin::setDirty)); d->m_pageList.append(plugin); } QUrl KPropertiesDialog::url() const { return d->m_singleUrl; } KFileItem &KPropertiesDialog::item() { return d->m_items.first(); } KFileItemList KPropertiesDialog::items() const { return d->m_items; } QUrl KPropertiesDialog::currentDir() const { return d->m_currentDir; } QString KPropertiesDialog::defaultName() const { return d->m_defaultName; } bool KPropertiesDialog::canDisplay(const KFileItemList &_items) { // TODO: cache the result of those calls. Currently we parse .desktop files far too many times return KFilePropsPlugin::supports(_items) || KFilePermissionsPropsPlugin::supports(_items) || KDesktopPropsPlugin::supports(_items) || KUrlPropsPlugin::supports(_items) || KDevicePropsPlugin::supports(_items) /* || KPreviewPropsPlugin::supports( _items )*/; } void KPropertiesDialog::slotOk() { accept(); } void KPropertiesDialog::accept() { QList::const_iterator pageListIt; d->m_aborted = false; KFilePropsPlugin *filePropsPlugin = qobject_cast(d->m_pageList.first()); // If any page is dirty, then set the main one (KFilePropsPlugin) as // dirty too. This is what makes it possible to save changes to a global // desktop file into a local one. In other cases, it doesn't hurt. for (pageListIt = d->m_pageList.constBegin(); pageListIt != d->m_pageList.constEnd(); ++pageListIt) { if ((*pageListIt)->isDirty() && filePropsPlugin) { filePropsPlugin->setDirty(); break; } } // Apply the changes in the _normal_ order of the tabs now // This is because in case of renaming a file, KFilePropsPlugin will call // KPropertiesDialog::rename, so other tab will be ok with whatever order // BUT for file copied from templates, we need to do the renaming first ! for (pageListIt = d->m_pageList.constBegin(); pageListIt != d->m_pageList.constEnd() && !d->m_aborted; ++pageListIt) { if ((*pageListIt)->isDirty()) { // qDebug() << "applying changes for " << (*pageListIt)->metaObject()->className(); (*pageListIt)->applyChanges(); // applyChanges may change d->m_aborted. } else { // qDebug() << "skipping page " << (*pageListIt)->metaObject()->className(); } } if (!d->m_aborted && filePropsPlugin) { filePropsPlugin->postApplyChanges(); } if (!d->m_aborted) { emit applied(); emit propertiesClosed(); deleteLater(); // somewhat like Qt::WA_DeleteOnClose would do. KPageDialog::accept(); } // else, keep dialog open for user to fix the problem. } void KPropertiesDialog::slotCancel() { reject(); } void KPropertiesDialog::reject() { emit canceled(); emit propertiesClosed(); deleteLater(); KPageDialog::reject(); } void KPropertiesDialog::KPropertiesDialogPrivate::insertPages() { if (m_items.isEmpty()) { return; } if (KFilePropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KFilePropsPlugin(q); q->insertPlugin(p); } if (KFilePermissionsPropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KFilePermissionsPropsPlugin(q); q->insertPlugin(p); } if (KChecksumsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KChecksumsPlugin(q); q->insertPlugin(p); } if (KDesktopPropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KDesktopPropsPlugin(q); q->insertPlugin(p); } if (KUrlPropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KUrlPropsPlugin(q); q->insertPlugin(p); } if (KDevicePropsPlugin::supports(m_items)) { KPropertiesDialogPlugin *p = new KDevicePropsPlugin(q); q->insertPlugin(p); } // if ( KPreviewPropsPlugin::supports( m_items ) ) { // KPropertiesDialogPlugin *p = new KPreviewPropsPlugin(q); // q->insertPlugin(p); // } //plugins if (m_items.count() != 1) { return; } const KFileItem item = m_items.first(); const QString mimetype = item.mimetype(); if (mimetype.isEmpty()) { return; } QString query = QStringLiteral( "(((not exist [X-KDE-Protocol]) and (not exist [X-KDE-Protocols])) or ([X-KDE-Protocol] == '%1') or ('%1' in [X-KDE-Protocols]))" ).arg(item.url().scheme()); // qDebug() << "trader query: " << query; const KService::List offers = KMimeTypeTrader::self()->query(mimetype, QStringLiteral("KPropertiesDialog/Plugin"), query); for (const KService::Ptr &ptr : offers) { KPropertiesDialogPlugin *plugin = ptr->createInstance(q); if (!plugin) { continue; } plugin->setObjectName(ptr->name()); q->insertPlugin(plugin); } } void KPropertiesDialog::updateUrl(const QUrl &_newUrl) { Q_ASSERT(d->m_items.count() == 1); // qDebug() << "KPropertiesDialog::updateUrl (pre)" << _newUrl; QUrl newUrl = _newUrl; emit saveAs(d->m_singleUrl, newUrl); // qDebug() << "KPropertiesDialog::updateUrl (post)" << newUrl; d->m_singleUrl = newUrl; d->m_items.first().setUrl(newUrl); Q_ASSERT(!d->m_singleUrl.isEmpty()); // If we have an Desktop page, set it dirty, so that a full file is saved locally // Same for a URL page (because of the Name= hack) foreach (KPropertiesDialogPlugin *it, d->m_pageList) { if (qobject_cast(it) || qobject_cast(it)) { //qDebug() << "Setting page dirty"; it->setDirty(); break; } } } void KPropertiesDialog::rename(const QString &_name) { Q_ASSERT(d->m_items.count() == 1); // qDebug() << "KPropertiesDialog::rename " << _name; QUrl newUrl; // if we're creating from a template : use currentdir if (!d->m_currentDir.isEmpty()) { newUrl = d->m_currentDir; newUrl.setPath(concatPaths(newUrl.path(), _name)); } else { // It's a directory, so strip the trailing slash first newUrl = d->m_singleUrl.adjusted(QUrl::StripTrailingSlash); // Now change the filename newUrl = newUrl.adjusted(QUrl::RemoveFilename); // keep trailing slash newUrl.setPath(concatPaths(newUrl.path(), _name)); } updateUrl(newUrl); } void KPropertiesDialog::abortApplying() { d->m_aborted = true; } class Q_DECL_HIDDEN KPropertiesDialogPlugin::KPropertiesDialogPluginPrivate { public: KPropertiesDialogPluginPrivate() { } ~KPropertiesDialogPluginPrivate() { } bool m_bDirty; int fontHeight; }; KPropertiesDialogPlugin::KPropertiesDialogPlugin(KPropertiesDialog *_props) : QObject(_props), d(new KPropertiesDialogPluginPrivate) { properties = _props; d->fontHeight = 2 * properties->fontMetrics().height(); d->m_bDirty = false; } KPropertiesDialogPlugin::~KPropertiesDialogPlugin() { delete d; } #ifndef KIOWIDGETS_NO_DEPRECATED bool KPropertiesDialogPlugin::isDesktopFile(const KFileItem &_item) { return _item.isDesktopFile(); } #endif void KPropertiesDialogPlugin::setDirty(bool b) { d->m_bDirty = b; } void KPropertiesDialogPlugin::setDirty() { d->m_bDirty = true; } bool KPropertiesDialogPlugin::isDirty() const { return d->m_bDirty; } void KPropertiesDialogPlugin::applyChanges() { qCWarning(KIO_WIDGETS) << "applyChanges() not implemented in page !"; } int KPropertiesDialogPlugin::fontHeight() const { return d->fontHeight; } /////////////////////////////////////////////////////////////////////////////// class KFilePropsPlugin::KFilePropsPluginPrivate { public: KFilePropsPluginPrivate() { dirSizeJob = nullptr; dirSizeUpdateTimer = nullptr; m_lined = nullptr; m_capacityBar = nullptr; m_linkTargetLineEdit = nullptr; } ~KFilePropsPluginPrivate() { if (dirSizeJob) { dirSizeJob->kill(); } } KIO::DirectorySizeJob *dirSizeJob; QTimer *dirSizeUpdateTimer; QFrame *m_frame; bool bMultiple; bool bIconChanged; bool bKDesktopMode; bool bDesktopFile; KCapacityBar *m_capacityBar; QString mimeType; QString oldFileName; KLineEdit *m_lined; QLabel *m_fileNameLabel = nullptr; QGridLayout *m_grid = nullptr; QWidget *iconArea; QLabel *m_sizeLabel; QPushButton *m_sizeDetermineButton; QPushButton *m_sizeStopButton; KLineEdit *m_linkTargetLineEdit; QString m_sRelativePath; bool m_bFromTemplate; /** * The initial filename */ QString oldName; }; KFilePropsPlugin::KFilePropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KFilePropsPluginPrivate) { d->bMultiple = (properties->items().count() > 1); d->bIconChanged = false; d->bDesktopFile = KDesktopPropsPlugin::supports(properties->items()); // qDebug() << "KFilePropsPlugin::KFilePropsPlugin bMultiple=" << d->bMultiple; // We set this data from the first item, and we'll // check that the other items match against it, resetting when not. bool isLocal; const KFileItem item = properties->item(); QUrl url = item.mostLocalUrl(isLocal); bool isReallyLocal = item.url().isLocalFile(); bool bDesktopFile = item.isDesktopFile(); mode_t mode = item.mode(); bool hasDirs = item.isDir() && !item.isLink(); bool hasRoot = url.path() == QLatin1String("/"); QString iconStr = item.iconName(); QString directory = properties->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); QString protocol = properties->url().scheme(); d->bKDesktopMode = protocol == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop"); QString mimeComment = item.mimeComment(); d->mimeType = item.mimetype(); KIO::filesize_t totalSize = item.size(); QString magicMimeComment; QMimeDatabase db; if (isLocal) { QMimeType magicMimeType = db.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchContent); if (magicMimeType.isValid() && !magicMimeType.isDefault()) { magicMimeComment = magicMimeType.comment(); } } #ifdef Q_OS_WIN if (isReallyLocal) { directory = QDir::toNativeSeparators(directory.mid(1)); } #endif // Those things only apply to 'single file' mode QString filename; bool isTrash = false; d->m_bFromTemplate = false; // And those only to 'multiple' mode uint iDirCount = hasDirs ? 1 : 0; uint iFileCount = 1 - iDirCount; d->m_frame = new QFrame(); properties->addPage(d->m_frame, i18nc("@title:tab File properties", "&General")); QVBoxLayout *vbl = new QVBoxLayout(d->m_frame); vbl->setContentsMargins(0, 0, 0, 0); vbl->setObjectName(QStringLiteral("vbl")); QGridLayout *grid = new QGridLayout(); // unknown rows d->m_grid = grid; grid->setColumnStretch(0, 0); grid->setColumnStretch(1, 0); grid->setColumnStretch(2, 1); const int spacingHint = d->m_frame->style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); grid->addItem(new QSpacerItem(spacingHint, 0), 0, 1); vbl->addLayout(grid); int curRow = 0; if (!d->bMultiple) { QString path; if (!d->m_bFromTemplate) { isTrash = (properties->url().scheme() == QLatin1String("trash")); // Extract the full name, but without file: for local files path = properties->url().toDisplayString(QUrl::PreferLocalFile); } else { path = concatPaths(properties->currentDir().path(), properties->defaultName()); directory = properties->currentDir().toDisplayString(QUrl::PreferLocalFile); } if (d->bDesktopFile) { determineRelativePath(path); } // Extract the file name only filename = properties->defaultName(); if (filename.isEmpty()) { // no template const QFileInfo finfo(item.name()); // this gives support for UDS_NAME, e.g. for kio_trash or kio_system filename = finfo.fileName(); // Make sure only the file's name is displayed (#160964). } else { d->m_bFromTemplate = true; setDirty(); // to enforce that the copy happens } d->oldFileName = filename; // Make it human-readable filename = nameFromFileName(filename); if (d->bKDesktopMode && d->bDesktopFile) { KDesktopFile config(url.toLocalFile()); if (config.desktopGroup().hasKey("Name")) { filename = config.readName(); } } d->oldName = filename; } else { // Multiple items: see what they have in common const KFileItemList items = properties->items(); KFileItemList::const_iterator kit = items.begin(); const KFileItemList::const_iterator kend = items.end(); for (++kit /*no need to check the first one again*/; kit != kend; ++kit) { const QUrl url = (*kit).url(); // qDebug() << "KFilePropsPlugin::KFilePropsPlugin " << url.toDisplayString(); // The list of things we check here should match the variables defined // at the beginning of this method. if (url.isLocalFile() != isLocal) { isLocal = false; // not all local } if (bDesktopFile && (*kit).isDesktopFile() != bDesktopFile) { bDesktopFile = false; // not all desktop files } if ((*kit).mode() != mode) { mode = (mode_t)0; } if (KIO::iconNameForUrl(url) != iconStr) { iconStr = QStringLiteral("document-multiple"); } if (url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() != directory) { directory.clear(); } if (url.scheme() != protocol) { protocol.clear(); } if (!mimeComment.isNull() && (*kit).mimeComment() != mimeComment) { mimeComment.clear(); } if (isLocal && !magicMimeComment.isNull()) { QMimeType magicMimeType = db.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchContent); if (magicMimeType.isValid() && magicMimeType.comment() != magicMimeComment) { magicMimeComment.clear(); } } if (isLocal && url.path() == QLatin1String("/")) { hasRoot = true; } if ((*kit).isDir() && !(*kit).isLink()) { iDirCount++; hasDirs = true; } else { iFileCount++; totalSize += (*kit).size(); } } } if (!isReallyLocal && !protocol.isEmpty()) { directory += QLatin1String(" (") + protocol + QLatin1Char(')'); } if (!isTrash && (bDesktopFile || ((mode & QT_STAT_MASK) == QT_STAT_DIR)) && !d->bMultiple // not implemented for multiple && enableIconButton()) { // #56857 KIconButton *iconButton = new KIconButton(d->m_frame); int bsize = 66 + 2 * iconButton->style()->pixelMetric(QStyle::PM_ButtonMargin); iconButton->setFixedSize(bsize, bsize); iconButton->setIconSize(48); iconButton->setStrictIconSize(false); if (bDesktopFile && isLocal) { const KDesktopFile config(url.toLocalFile()); if (config.hasDeviceType()) { iconButton->setIconType(KIconLoader::Desktop, KIconLoader::Device); } else { iconButton->setIconType(KIconLoader::Desktop, KIconLoader::Application); } } else { iconButton->setIconType(KIconLoader::Desktop, KIconLoader::Place); } iconButton->setIcon(iconStr); d->iconArea = iconButton; connect(iconButton, &KIconButton::iconChanged, this, &KFilePropsPlugin::slotIconChanged); } else { QLabel *iconLabel = new QLabel(d->m_frame); iconLabel->setAlignment(Qt::AlignCenter); int bsize = 66 + 2 * iconLabel->style()->pixelMetric(QStyle::PM_ButtonMargin); iconLabel->setFixedSize(bsize, bsize); iconLabel->setPixmap(QIcon::fromTheme(iconStr).pixmap(48)); d->iconArea = iconLabel; } grid->addWidget(d->iconArea, curRow, 0, Qt::AlignCenter); KFileItemListProperties itemList(KFileItemList() << item); if (d->bMultiple || isTrash || hasRoot || !(d->m_bFromTemplate || itemList.supportsMoving())) { setFileNameReadOnly(true); if (d->bMultiple) { d->m_fileNameLabel->setText(KIO::itemsSummaryString(iFileCount + iDirCount, iFileCount, iDirCount, 0, false)); } } else { d->m_lined = new KLineEdit(d->m_frame); d->m_lined->setObjectName(QStringLiteral("KFilePropsPlugin::nameLineEdit")); d->m_lined->setText(filename); d->m_lined->setFocus(); // Enhanced rename: Don't highlight the file extension. QString extension = db.suffixForFileName(filename); if (!extension.isEmpty()) { d->m_lined->setSelection(0, filename.length() - extension.length() - 1); } else { int lastDot = filename.lastIndexOf(QLatin1Char('.')); if (lastDot > 0) { d->m_lined->setSelection(0, lastDot); } } connect(d->m_lined, &QLineEdit::textChanged, this, &KFilePropsPlugin::nameFileChanged); grid->addWidget(d->m_lined, curRow, 2); } ++curRow; KSeparator *sep = new KSeparator(Qt::Horizontal, d->m_frame); grid->addWidget(sep, curRow, 0, 1, 3); ++curRow; QLabel *l; if (!mimeComment.isEmpty() && !isTrash) { l = new QLabel(i18n("Type:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight | Qt::AlignTop); QFrame *box = new QFrame(d->m_frame); QVBoxLayout *boxLayout = new QVBoxLayout(box); boxLayout->setSpacing(2); // without that spacing the button literally “sticks” to the label ;) boxLayout->setContentsMargins(0, 0, 0, 0); l = new QLabel(mimeComment, box); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(box, curRow++, 2); QPushButton *button = new QPushButton(box); button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); // Minimum still makes the button grow to the entire layout width button->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); boxLayout->addWidget(l); boxLayout->addWidget(button); if (d->mimeType == QLatin1String("application/octet-stream")) { button->setText(i18n("Create New File Type")); } else { button->setText(i18n("File Type Options")); } connect(button, &QAbstractButton::clicked, this, &KFilePropsPlugin::slotEditFileType); if (!KAuthorized::authorizeAction(QStringLiteral("editfiletype"))) { button->hide(); } } if (!magicMimeComment.isEmpty() && magicMimeComment != mimeComment) { l = new QLabel(i18n("Contents:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(magicMimeComment, d->m_frame); grid->addWidget(l, curRow++, 2); } if (!directory.isEmpty()) { l = new QLabel(i18n("Location:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new KSqueezedTextLabel(directory, d->m_frame); // force the layout direction to be always LTR l->setLayoutDirection(Qt::LeftToRight); // but if we are in RTL mode, align the text to the right // otherwise the text is on the wrong side of the dialog if (properties->layoutDirection() == Qt::RightToLeft) { l->setAlignment(Qt::AlignRight); } l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(l, curRow++, 2); } l = new QLabel(i18n("Size:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight | Qt::AlignTop); d->m_sizeLabel = new QLabel(d->m_frame); d->m_sizeLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(d->m_sizeLabel, curRow++, 2); if (!hasDirs) { // Only files [and symlinks] d->m_sizeLabel->setText(QStringLiteral("%1 (%2)").arg(KIO::convertSize(totalSize), QLocale().toString(totalSize))); d->m_sizeDetermineButton = nullptr; d->m_sizeStopButton = nullptr; } else { // Directory QHBoxLayout *sizelay = new QHBoxLayout(); grid->addLayout(sizelay, curRow++, 2); // buttons d->m_sizeDetermineButton = new QPushButton(i18n("Calculate"), d->m_frame); d->m_sizeStopButton = new QPushButton(i18n("Stop"), d->m_frame); connect(d->m_sizeDetermineButton, &QAbstractButton::clicked, this, &KFilePropsPlugin::slotSizeDetermine); connect(d->m_sizeStopButton, &QAbstractButton::clicked, this, &KFilePropsPlugin::slotSizeStop); sizelay->addWidget(d->m_sizeDetermineButton, 0); sizelay->addWidget(d->m_sizeStopButton, 0); sizelay->addStretch(10); // so that the buttons don't grow horizontally // auto-launch for local dirs only, and not for '/' if (isLocal && !hasRoot) { d->m_sizeDetermineButton->setText(i18n("Refresh")); slotSizeDetermine(); } else { d->m_sizeStopButton->setEnabled(false); } } if (!d->bMultiple && item.isLink()) { l = new QLabel(i18n("Points to:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); d->m_linkTargetLineEdit = new KLineEdit(item.linkDest(), d->m_frame); grid->addWidget(d->m_linkTargetLineEdit, curRow++, 2); connect(d->m_linkTargetLineEdit, &QLineEdit::textChanged, this, QOverload<>::of(&KFilePropsPlugin::setDirty)); } if (!d->bMultiple) { // Dates for multiple don't make much sense... QDateTime dt = item.time(KFileItem::CreationTime); if (!dt.isNull()) { l = new QLabel(i18n("Created:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(dt.toString(Qt::DefaultLocaleLongDate), d->m_frame); grid->addWidget(l, curRow++, 2); } dt = item.time(KFileItem::ModificationTime); if (!dt.isNull()) { l = new QLabel(i18n("Modified:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(dt.toString(Qt::DefaultLocaleLongDate), d->m_frame); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(l, curRow++, 2); } dt = item.time(KFileItem::AccessTime); if (!dt.isNull()) { l = new QLabel(i18n("Accessed:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(dt.toString(Qt::DefaultLocaleLongDate), d->m_frame); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(l, curRow++, 2); } } if (hasDirs) { // only for directories sep = new KSeparator(Qt::Horizontal, d->m_frame); grid->addWidget(sep, curRow, 0, 1, 3); ++curRow; if (isLocal) { KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(url.toLocalFile()); l = new QLabel(i18n("File System:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(d->m_frame); grid->addWidget(l, curRow++, 2); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); l->setText(mp->mountType()); if (mp) { l = new QLabel(i18n("Mounted on:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new KSqueezedTextLabel(mp->mountPoint(), d->m_frame); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); grid->addWidget(l, curRow++, 2); l = new QLabel(i18n("Mounted from:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); l = new QLabel(mp->mountedFrom(), d->m_frame); grid->addWidget(l, curRow++, 2); l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); } } l = new QLabel(i18nc("Amount of used and available space on this device or partition", "Free space:"), d->m_frame); grid->addWidget(l, curRow, 0, Qt::AlignRight); d->m_capacityBar = new KCapacityBar(KCapacityBar::DrawTextOutline, d->m_frame); d->m_capacityBar->setText(i18nc("@info:status", "Unknown size")); grid->addWidget(d->m_capacityBar, curRow++, 2); KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(url); connect(job, &KIO::FileSystemFreeSpaceJob::result, this, &KFilePropsPlugin::slotFreeSpaceResult); } vbl->addStretch(1); } bool KFilePropsPlugin::enableIconButton() const { bool iconEnabled = false; const KFileItem item = properties->item(); // If the current item is a directory, check if it's writable, // so we can create/update a .directory // Current item is a file, same thing: check if it is writable if (item.isWritable()) { iconEnabled = true; } return iconEnabled; } // QString KFilePropsPlugin::tabName () const // { // return i18n ("&General"); // } void KFilePropsPlugin::setFileNameReadOnly(bool ro) { Q_ASSERT(ro); // false isn't supported if (ro && !d->m_fileNameLabel) { Q_ASSERT(!d->m_bFromTemplate); delete d->m_lined; d->m_lined = nullptr; d->m_fileNameLabel = new QLabel(d->m_frame); d->m_fileNameLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); d->m_fileNameLabel->setText(d->oldName); // will get overwritten if d->bMultiple d->m_grid->addWidget(d->m_fileNameLabel, 0, 2); } } void KFilePropsPlugin::slotEditFileType() { QString mime; if (d->mimeType == QLatin1String("application/octet-stream")) { const int pos = d->oldFileName.lastIndexOf(QLatin1Char('.')); if (pos != -1) { mime = QLatin1Char('*') + d->oldFileName.midRef(pos); } else { mime = QStringLiteral("*"); } } else { mime = d->mimeType; } KMimeTypeEditor::editMimeType(mime, properties->window()); } void KFilePropsPlugin::slotIconChanged() { d->bIconChanged = true; emit changed(); } void KFilePropsPlugin::nameFileChanged(const QString &text) { properties->buttonBox()->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); emit changed(); } static QString relativeAppsLocation(const QString &file) { const QString canonical = QFileInfo(file).canonicalFilePath(); Q_FOREACH (const QString &base, QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)) { QDir base_dir = QDir(base); if (base_dir.exists() && canonical.startsWith(base_dir.canonicalPath())) { return canonical.mid(base.length() + 1); } } return QString(); // return empty if the file is not in apps } void KFilePropsPlugin::determineRelativePath(const QString &path) { // now let's make it relative d->m_sRelativePath = relativeAppsLocation(path); } void KFilePropsPlugin::slotFreeSpaceResult(KIO::Job *job, KIO::filesize_t size, KIO::filesize_t available) { if (!job->error()) { const quint64 used = size - available; const int percentUsed = qRound(100.0 * qreal(used) / qreal(size)); d->m_capacityBar->setText( i18nc("Available space out of total partition size (percent used)", "%1 free of %2 (%3% used)", KIO::convertSize(available), KIO::convertSize(size), percentUsed)); d->m_capacityBar->setValue(percentUsed); } else { d->m_capacityBar->setText(i18nc("@info:status", "Unknown size")); d->m_capacityBar->setValue(0); } } void KFilePropsPlugin::slotDirSizeUpdate() { KIO::filesize_t totalSize = d->dirSizeJob->totalSize(); KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles(); KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs(); d->m_sizeLabel->setText( i18n("Calculating... %1 (%2)\n%3, %4", KIO::convertSize(totalSize), QLocale().toString(totalSize), i18np("1 file", "%1 files", totalFiles), i18np("1 sub-folder", "%1 sub-folders", totalSubdirs))); } void KFilePropsPlugin::slotDirSizeFinished(KJob *job) { if (job->error()) { d->m_sizeLabel->setText(job->errorString()); } else { KIO::filesize_t totalSize = d->dirSizeJob->totalSize(); KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles(); KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs(); d->m_sizeLabel->setText(QStringLiteral("%1 (%2)\n%3, %4") .arg(KIO::convertSize(totalSize), QLocale().toString(totalSize), i18np("1 file", "%1 files", totalFiles), i18np("1 sub-folder", "%1 sub-folders", totalSubdirs))); } d->m_sizeStopButton->setEnabled(false); // just in case you change something and try again :) d->m_sizeDetermineButton->setText(i18n("Refresh")); d->m_sizeDetermineButton->setEnabled(true); d->dirSizeJob = nullptr; delete d->dirSizeUpdateTimer; d->dirSizeUpdateTimer = nullptr; } void KFilePropsPlugin::slotSizeDetermine() { d->m_sizeLabel->setText(i18n("Calculating...")); // qDebug() << "properties->item()=" << properties->item() << "URL=" << properties->item().url(); d->dirSizeJob = KIO::directorySize(properties->items()); d->dirSizeUpdateTimer = new QTimer(this); connect(d->dirSizeUpdateTimer, &QTimer::timeout, this, &KFilePropsPlugin::slotDirSizeUpdate); d->dirSizeUpdateTimer->start(500); connect(d->dirSizeJob, &KJob::result, this, &KFilePropsPlugin::slotDirSizeFinished); d->m_sizeStopButton->setEnabled(true); d->m_sizeDetermineButton->setEnabled(false); // also update the "Free disk space" display if (d->m_capacityBar) { const KFileItem item = properties->item(); KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(item.url()); connect(job, &KIO::FileSystemFreeSpaceJob::result, this, &KFilePropsPlugin::slotFreeSpaceResult); } } void KFilePropsPlugin::slotSizeStop() { if (d->dirSizeJob) { KIO::filesize_t totalSize = d->dirSizeJob->totalSize(); d->m_sizeLabel->setText(i18n("At least %1", KIO::convertSize(totalSize))); d->dirSizeJob->kill(); d->dirSizeJob = nullptr; } if (d->dirSizeUpdateTimer) { d->dirSizeUpdateTimer->stop(); } d->m_sizeStopButton->setEnabled(false); d->m_sizeDetermineButton->setEnabled(true); } KFilePropsPlugin::~KFilePropsPlugin() { delete d; } bool KFilePropsPlugin::supports(const KFileItemList & /*_items*/) { return true; } void KFilePropsPlugin::applyChanges() { if (d->dirSizeJob) { slotSizeStop(); } // qDebug() << "KFilePropsPlugin::applyChanges"; if (d->m_lined) { QString n = d->m_lined->text(); // Remove trailing spaces (#4345) while (! n.isEmpty() && n[n.length() - 1].isSpace()) { n.chop(1); } if (n.isEmpty()) { KMessageBox::sorry(properties, i18n("The new file name is empty.")); properties->abortApplying(); return; } // Do we need to rename the file ? // qDebug() << "oldname = " << d->oldName; // qDebug() << "newname = " << n; if (d->oldName != n || d->m_bFromTemplate) { // true for any from-template file KIO::CopyJob *job = nullptr; QUrl oldurl = properties->url(); QString newFileName = KIO::encodeFileName(n); if (d->bDesktopFile && !newFileName.endsWith(QLatin1String(".desktop")) && !newFileName.endsWith(QLatin1String(".kdelnk"))) { newFileName += QLatin1String(".desktop"); } // Tell properties. Warning, this changes the result of properties->url() ! properties->rename(newFileName); // Update also relative path (for apps) if (!d->m_sRelativePath.isEmpty()) { determineRelativePath(properties->url().toLocalFile()); } // qDebug() << "New URL = " << properties->url(); // qDebug() << "old = " << oldurl.url(); // Don't remove the template !! if (!d->m_bFromTemplate) { // (normal renaming) job = KIO::moveAs(oldurl, properties->url()); } else { // Copying a template job = KIO::copyAs(oldurl, properties->url()); } connect(job, &KJob::result, this, &KFilePropsPlugin::slotCopyFinished); connect(job, &KIO::CopyJob::renamed, this, &KFilePropsPlugin::slotFileRenamed); // wait for job QEventLoop eventLoop; connect(this, &KFilePropsPlugin::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); return; } properties->updateUrl(properties->url()); // Update also relative path (for apps) if (!d->m_sRelativePath.isEmpty()) { determineRelativePath(properties->url().toLocalFile()); } } // No job, keep going slotCopyFinished(nullptr); } void KFilePropsPlugin::slotCopyFinished(KJob *job) { // qDebug() << "KFilePropsPlugin::slotCopyFinished"; if (job) { // allow apply() to return emit leaveModality(); if (job->error()) { job->uiDelegate()->showErrorMessage(); // Didn't work. Revert the URL to the old one properties->updateUrl(static_cast(job)->srcUrls().constFirst()); properties->abortApplying(); // Don't apply the changes to the wrong file ! return; } } Q_ASSERT(!properties->item().isNull()); Q_ASSERT(!properties->item().url().isEmpty()); // Save the file locally if (d->bDesktopFile && !d->m_sRelativePath.isEmpty()) { // qDebug() << "KFilePropsPlugin::slotCopyFinished " << d->m_sRelativePath; const QString newPath = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + d->m_sRelativePath; const QUrl newURL = QUrl::fromLocalFile(newPath); // qDebug() << "KFilePropsPlugin::slotCopyFinished path=" << newURL; properties->updateUrl(newURL); } if (d->bKDesktopMode && d->bDesktopFile) { // Renamed? Update Name field // Note: The desktop ioslave does this as well, but not when // the file is copied from a template. if (d->m_bFromTemplate) { KIO::StatJob *job = KIO::stat(properties->url()); job->exec(); KIO::UDSEntry entry = job->statResult(); KFileItem item(entry, properties->url()); KDesktopFile config(item.localPath()); KConfigGroup cg = config.desktopGroup(); QString nameStr = nameFromFileName(properties->url().fileName()); cg.writeEntry("Name", nameStr); cg.writeEntry("Name", nameStr, KConfigGroup::Persistent | KConfigGroup::Localized); } } if (d->m_linkTargetLineEdit && !d->bMultiple) { const KFileItem item = properties->item(); const QString newTarget = d->m_linkTargetLineEdit->text(); if (newTarget != item.linkDest()) { // qDebug() << "Updating target of symlink to" << newTarget; KIO::Job *job = KIO::symlink(newTarget, item.url(), KIO::Overwrite); job->uiDelegate()->setAutoErrorHandlingEnabled(true); job->exec(); } } // "Link to Application" templates need to be made executable // Instead of matching against a filename we check if the destination // is an Application now. if (d->m_bFromTemplate) { // destination is not necessarily local, use the src template KDesktopFile templateResult(static_cast(job)->srcUrls().constFirst().toLocalFile()); if (templateResult.hasApplicationType()) { // We can either stat the file and add the +x bit or use the larger chmod() job // with a umask designed to only touch u+x. This is only one KIO job, so let's // do that. KFileItem appLink(properties->item()); KFileItemList fileItemList; fileItemList << appLink; // first 0100 adds u+x, second 0100 only allows chmod to change u+x KIO::Job *chmodJob = KIO::chmod(fileItemList, 0100, 0100, QString(), QString(), KIO::HideProgressInfo); chmodJob->exec(); } } } void KFilePropsPlugin::applyIconChanges() { KIconButton *iconButton = qobject_cast(d->iconArea); if (!iconButton || !d->bIconChanged) { return; } // handle icon changes - only local files (or pseudo-local) for now // TODO: Use KTempFile and KIO::file_copy with overwrite = true QUrl url = properties->url(); KIO::StatJob *job = KIO::mostLocalUrl(url); KJobWidgets::setWindow(job, properties); job->exec(); url = job->mostLocalUrl(); if (url.isLocalFile()) { QString path; if ((properties->item().mode() & QT_STAT_MASK) == QT_STAT_DIR) { path = url.toLocalFile() + QLatin1String("/.directory"); // don't call updateUrl because the other tabs (i.e. permissions) // apply to the directory, not the .directory file. } else { path = url.toLocalFile(); } // Get the default image QMimeDatabase db; QString str = db.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchExtension).iconName(); // Is it another one than the default ? QString sIcon; if (str != iconButton->icon()) { sIcon = iconButton->icon(); } // (otherwise write empty value) // qDebug() << "**" << path << "**"; // If default icon and no .directory file -> don't create one if (!sIcon.isEmpty() || QFile::exists(path)) { KDesktopFile cfg(path); // qDebug() << "sIcon = " << (sIcon); // qDebug() << "str = " << (str); cfg.desktopGroup().writeEntry("Icon", sIcon); cfg.sync(); cfg.reparseConfiguration(); if (cfg.desktopGroup().readEntry("Icon") != sIcon) { KMessageBox::sorry(nullptr, i18n("Could not save properties. You do not " "have sufficient access to write to %1.", path)); } } } } void KFilePropsPlugin::slotFileRenamed(KIO::Job *, const QUrl &, const QUrl &newUrl) { // This is called in case of an existing local file during the copy/move operation, // if the user chooses Rename. properties->updateUrl(newUrl); } void KFilePropsPlugin::postApplyChanges() { // Save the icon only after applying the permissions changes (#46192) applyIconChanges(); const KFileItemList items = properties->items(); const QList lst = items.urlList(); org::kde::KDirNotify::emitFilesChanged(QList(lst)); } class KFilePermissionsPropsPlugin::KFilePermissionsPropsPluginPrivate { public: KFilePermissionsPropsPluginPrivate() { } ~KFilePermissionsPropsPluginPrivate() { } QFrame *m_frame; QCheckBox *cbRecursive; QLabel *explanationLabel; KComboBox *ownerPermCombo, *groupPermCombo, *othersPermCombo; QCheckBox *extraCheckbox; mode_t partialPermissions; KFilePermissionsPropsPlugin::PermissionsMode pmode; bool canChangePermissions; bool isIrregular; bool hasExtendedACL; KACL extendedACL; KACL defaultACL; bool fileSystemSupportsACLs; KComboBox *grpCombo; KLineEdit *usrEdit; KLineEdit *grpEdit; // Old permissions mode_t permissions; // Old group QString strGroup; // Old owner QString strOwner; }; #define UniOwner (S_IRUSR|S_IWUSR|S_IXUSR) #define UniGroup (S_IRGRP|S_IWGRP|S_IXGRP) #define UniOthers (S_IROTH|S_IWOTH|S_IXOTH) #define UniRead (S_IRUSR|S_IRGRP|S_IROTH) #define UniWrite (S_IWUSR|S_IWGRP|S_IWOTH) #define UniExec (S_IXUSR|S_IXGRP|S_IXOTH) #define UniSpecial (S_ISUID|S_ISGID|S_ISVTX) // synced with PermissionsTarget const mode_t KFilePermissionsPropsPlugin::permissionsMasks[3] = {UniOwner, UniGroup, UniOthers}; const mode_t KFilePermissionsPropsPlugin::standardPermissions[4] = { 0, UniRead, UniRead | UniWrite, (mode_t) - 1 }; // synced with PermissionsMode and standardPermissions const char *const KFilePermissionsPropsPlugin::permissionsTexts[4][4] = { { I18N_NOOP("No Access"), I18N_NOOP("Can Only View"), I18N_NOOP("Can View & Modify"), nullptr }, { I18N_NOOP("No Access"), I18N_NOOP("Can Only View Content"), I18N_NOOP("Can View & Modify Content"), nullptr }, { nullptr, nullptr, nullptr, nullptr}, // no texts for links { I18N_NOOP("No Access"), I18N_NOOP("Can Only View/Read Content"), I18N_NOOP("Can View/Read & Modify/Write"), nullptr } }; KFilePermissionsPropsPlugin::KFilePermissionsPropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KFilePermissionsPropsPluginPrivate) { d->cbRecursive = nullptr; d->grpCombo = nullptr; d->grpEdit = nullptr; d->usrEdit = nullptr; bool isLocal = properties->url().isLocalFile(); bool isTrash = (properties->url().scheme() == QLatin1String("trash")); KUser myself(KUser::UseEffectiveUID); const bool IamRoot = myself.isSuperUser(); const KFileItem item = properties->item(); bool isLink = item.isLink(); bool isDir = item.isDir(); // all dirs bool hasDir = item.isDir(); // at least one dir d->permissions = item.permissions(); // common permissions to all files d->partialPermissions = d->permissions; // permissions that only some files have (at first we take everything) d->isIrregular = isIrregular(d->permissions, isDir, isLink); d->strOwner = item.user(); d->strGroup = item.group(); d->hasExtendedACL = item.ACL().isExtended() || item.defaultACL().isValid(); d->extendedACL = item.ACL(); d->defaultACL = item.defaultACL(); d->fileSystemSupportsACLs = false; if (properties->items().count() > 1) { // Multiple items: see what they have in common const KFileItemList items = properties->items(); KFileItemList::const_iterator it = items.begin(); const KFileItemList::const_iterator kend = items.end(); for (++it /*no need to check the first one again*/; it != kend; ++it) { if (!d->isIrregular) d->isIrregular |= isIrregular((*it).permissions(), (*it).isDir() == isDir, (*it).isLink() == isLink); d->hasExtendedACL = d->hasExtendedACL || (*it).hasExtendedACL(); if ((*it).isLink() != isLink) { isLink = false; } if ((*it).isDir() != isDir) { isDir = false; } hasDir |= (*it).isDir(); if ((*it).permissions() != d->permissions) { d->permissions &= (*it).permissions(); d->partialPermissions |= (*it).permissions(); } if ((*it).user() != d->strOwner) { d->strOwner.clear(); } if ((*it).group() != d->strGroup) { d->strGroup.clear(); } } } if (isLink) { d->pmode = PermissionsOnlyLinks; } else if (isDir) { d->pmode = PermissionsOnlyDirs; } else if (hasDir) { d->pmode = PermissionsMixed; } else { d->pmode = PermissionsOnlyFiles; } // keep only what's not in the common permissions d->partialPermissions = d->partialPermissions & ~d->permissions; bool isMyFile = false; if (isLocal && !d->strOwner.isEmpty()) { // local files, and all owned by the same person if (myself.isValid()) { isMyFile = (d->strOwner == myself.loginName()); } else { qCWarning(KIO_WIDGETS) << "I don't exist ?! geteuid=" << KUserId::currentEffectiveUserId().toString(); } } else { //We don't know, for remote files, if they are ours or not. //So we let the user change permissions, and //KIO::chmod will tell, if he had no right to do it. isMyFile = true; } d->canChangePermissions = (isMyFile || IamRoot) && (!isLink); // create GUI d->m_frame = new QFrame(); properties->addPage(d->m_frame, i18n("&Permissions")); QBoxLayout *box = new QVBoxLayout(d->m_frame); box->setContentsMargins(0, 0, 0, 0); QWidget *l; QLabel *lbl; QGroupBox *gb; QGridLayout *gl; QPushButton *pbAdvancedPerm = nullptr; /* Group: Access Permissions */ gb = new QGroupBox(i18n("Access Permissions"), d->m_frame); box->addWidget(gb); gl = new QGridLayout(gb); gl->setColumnStretch(1, 1); l = d->explanationLabel = new QLabel(gb); if (isLink) d->explanationLabel->setText(i18np("This file is a link and does not have permissions.", "All files are links and do not have permissions.", properties->items().count())); else if (!d->canChangePermissions) { d->explanationLabel->setText(i18n("Only the owner can change permissions.")); } gl->addWidget(l, 0, 0, 1, 2); lbl = new QLabel(i18n("O&wner:"), gb); gl->addWidget(lbl, 1, 0, Qt::AlignRight); l = d->ownerPermCombo = new KComboBox(gb); lbl->setBuddy(l); gl->addWidget(l, 1, 1); connect(d->ownerPermCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); l->setWhatsThis(i18n("Specifies the actions that the owner is allowed to do.")); lbl = new QLabel(i18n("Gro&up:"), gb); gl->addWidget(lbl, 2, 0, Qt::AlignRight); l = d->groupPermCombo = new KComboBox(gb); lbl->setBuddy(l); gl->addWidget(l, 2, 1); connect(d->groupPermCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); l->setWhatsThis(i18n("Specifies the actions that the members of the group are allowed to do.")); lbl = new QLabel(i18n("O&thers:"), gb); gl->addWidget(lbl, 3, 0, Qt::AlignRight); l = d->othersPermCombo = new KComboBox(gb); lbl->setBuddy(l); gl->addWidget(l, 3, 1); connect(d->othersPermCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); l->setWhatsThis(i18n("Specifies the actions that all users, who are neither " "owner nor in the group, are allowed to do.")); if (!isLink) { l = d->extraCheckbox = new QCheckBox(hasDir ? i18n("Only own&er can rename and delete folder content") : i18n("Is &executable"), gb); connect(d->extraCheckbox, &QAbstractButton::clicked, this, &KPropertiesDialogPlugin::changed); gl->addWidget(l, 4, 1); l->setWhatsThis(hasDir ? i18n("Enable this option to allow only the folder's owner to " "delete or rename the contained files and folders. Other " "users can only add new files, which requires the 'Modify " "Content' permission.") : i18n("Enable this option to mark the file as executable. This only makes " "sense for programs and scripts. It is required when you want to " "execute them.")); QLayoutItem *spacer = new QSpacerItem(0, 20, QSizePolicy::Minimum, QSizePolicy::Expanding); gl->addItem(spacer, 5, 0, 1, 3); pbAdvancedPerm = new QPushButton(i18n("A&dvanced Permissions"), gb); gl->addWidget(pbAdvancedPerm, 6, 0, 1, 2, Qt::AlignRight); connect(pbAdvancedPerm, &QAbstractButton::clicked, this, &KFilePermissionsPropsPlugin::slotShowAdvancedPermissions); } else { d->extraCheckbox = nullptr; } /**** Group: Ownership ****/ gb = new QGroupBox(i18n("Ownership"), d->m_frame); box->addWidget(gb); gl = new QGridLayout(gb); gl->addItem(new QSpacerItem(0, 10), 0, 0); /*** Set Owner ***/ l = new QLabel(i18n("User:"), gb); gl->addWidget(l, 1, 0, Qt::AlignRight); /* GJ: Don't autocomplete more than 1000 users. This is a kind of random * value. Huge sites having 10.000+ user have a fair chance of using NIS, * (possibly) making this unacceptably slow. * OTOH, it is nice to offer this functionality for the standard user. */ int maxEntries = 1000; /* File owner: For root, offer a KLineEdit with autocompletion. * For a user, who can never chown() a file, offer a QLabel. */ if (IamRoot && isLocal) { d->usrEdit = new KLineEdit(gb); KCompletion *kcom = d->usrEdit->completionObject(); kcom->setOrder(KCompletion::Sorted); QStringList userNames = KUser::allUserNames(maxEntries); kcom->setItems(userNames); d->usrEdit->setCompletionMode((userNames.size() < maxEntries) ? KCompletion::CompletionAuto : KCompletion::CompletionNone); d->usrEdit->setText(d->strOwner); gl->addWidget(d->usrEdit, 1, 1); connect(d->usrEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); } else { l = new QLabel(d->strOwner, gb); gl->addWidget(l, 1, 1); } /*** Set Group ***/ KUser user(KUser::UseEffectiveUID); QStringList groupList = user.groupNames(); const bool isMyGroup = groupList.contains(d->strGroup); /* add the group the file currently belongs to .. * .. if it is not there already */ if (!isMyGroup) { groupList += d->strGroup; } l = new QLabel(i18n("Group:"), gb); gl->addWidget(l, 2, 0, Qt::AlignRight); /* Set group: if possible to change: * - Offer a KLineEdit for root, since he can change to any group. * - Offer a KComboBox for a normal user, since he can change to a fixed * (small) set of groups only. * If not changeable: offer a QLabel. */ if (IamRoot && isLocal) { d->grpEdit = new KLineEdit(gb); KCompletion *kcom = new KCompletion; kcom->setItems(groupList); d->grpEdit->setCompletionObject(kcom, true); d->grpEdit->setAutoDeleteCompletionObject(true); d->grpEdit->setCompletionMode(KCompletion::CompletionAuto); d->grpEdit->setText(d->strGroup); gl->addWidget(d->grpEdit, 2, 1); connect(d->grpEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); } else if ((groupList.count() > 1) && isMyFile && isLocal) { d->grpCombo = new KComboBox(gb); d->grpCombo->setObjectName(QStringLiteral("combogrouplist")); d->grpCombo->addItems(groupList); d->grpCombo->setCurrentIndex(groupList.indexOf(d->strGroup)); gl->addWidget(d->grpCombo, 2, 1); connect(d->grpCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); } else { l = new QLabel(d->strGroup, gb); gl->addWidget(l, 2, 1); } gl->setColumnStretch(2, 10); // "Apply recursive" checkbox if (hasDir && !isLink && !isTrash) { d->cbRecursive = new QCheckBox(i18n("Apply changes to all subfolders and their contents"), d->m_frame); connect(d->cbRecursive, &QAbstractButton::clicked, this, &KPropertiesDialogPlugin::changed); box->addWidget(d->cbRecursive); } updateAccessControls(); if (isTrash) { //don't allow to change properties for file into trash enableAccessControls(false); if (pbAdvancedPerm) { pbAdvancedPerm->setEnabled(false); } } box->addStretch(10); } #if HAVE_POSIX_ACL static bool fileSystemSupportsACL(const QByteArray &path) { bool fileSystemSupportsACLs = false; #ifdef Q_OS_FREEBSD struct statfs buf; fileSystemSupportsACLs = (statfs(path.data(), &buf) == 0) && (buf.f_flags & MNT_ACLS); #elif defined Q_OS_MACOS fileSystemSupportsACLs = getxattr(path.data(), "system.posix_acl_access", nullptr, 0, 0, XATTR_NOFOLLOW) >= 0 || errno == ENODATA; #else fileSystemSupportsACLs = getxattr(path.data(), "system.posix_acl_access", nullptr, 0) >= 0 || errno == ENODATA; #endif return fileSystemSupportsACLs; } #endif void KFilePermissionsPropsPlugin::slotShowAdvancedPermissions() { bool isDir = (d->pmode == PermissionsOnlyDirs) || (d->pmode == PermissionsMixed); QDialog dlg(properties); dlg.setModal(true); dlg.setWindowTitle(i18n("Advanced Permissions")); QLabel *l, *cl[3]; QGroupBox *gb; QGridLayout *gl; QVBoxLayout *vbox = new QVBoxLayout; dlg.setLayout(vbox); // Group: Access Permissions gb = new QGroupBox(i18n("Access Permissions"), &dlg); vbox->addWidget(gb); gl = new QGridLayout(gb); gl->addItem(new QSpacerItem(0, 10), 0, 0); QVector theNotSpecials; l = new QLabel(i18n("Class"), gb); gl->addWidget(l, 1, 0); theNotSpecials.append(l); QString readWhatsThis; QString readLabel; if (isDir) { readLabel = i18n("Show\nEntries"); readWhatsThis = i18n("This flag allows viewing the content of the folder."); } else { readLabel = i18n("Read"); readWhatsThis = i18n("The Read flag allows viewing the content of the file."); } QString writeWhatsThis; QString writeLabel; if (isDir) { writeLabel = i18n("Write\nEntries"); writeWhatsThis = i18n("This flag allows adding, renaming and deleting of files. " "Note that deleting and renaming can be limited using the Sticky flag."); } else { writeLabel = i18n("Write"); writeWhatsThis = i18n("The Write flag allows modifying the content of the file."); } QString execLabel; QString execWhatsThis; if (isDir) { execLabel = i18nc("Enter folder", "Enter"); execWhatsThis = i18n("Enable this flag to allow entering the folder."); } else { execLabel = i18n("Exec"); execWhatsThis = i18n("Enable this flag to allow executing the file as a program."); } // GJ: Add space between normal and special modes QSize size = l->sizeHint(); size.setWidth(size.width() + 15); l->setFixedSize(size); gl->addWidget(l, 1, 3); l = new QLabel(i18n("Special"), gb); gl->addWidget(l, 1, 4, 1, 1); QString specialWhatsThis; if (isDir) specialWhatsThis = i18n("Special flag. Valid for the whole folder, the exact " "meaning of the flag can be seen in the right hand column."); else specialWhatsThis = i18n("Special flag. The exact meaning of the flag can be seen " "in the right hand column."); l->setWhatsThis(specialWhatsThis); cl[0] = new QLabel(i18n("User"), gb); gl->addWidget(cl[0], 2, 0); theNotSpecials.append(cl[0]); cl[1] = new QLabel(i18n("Group"), gb); gl->addWidget(cl[1], 3, 0); theNotSpecials.append(cl[1]); cl[2] = new QLabel(i18n("Others"), gb); gl->addWidget(cl[2], 4, 0); theNotSpecials.append(cl[2]); QString setUidWhatsThis; if (isDir) setUidWhatsThis = i18n("If this flag is set, the owner of this folder will be " "the owner of all new files."); else setUidWhatsThis = i18n("If this file is an executable and the flag is set, it will " "be executed with the permissions of the owner."); QString setGidWhatsThis; if (isDir) setGidWhatsThis = i18n("If this flag is set, the group of this folder will be " "set for all new files."); else setGidWhatsThis = i18n("If this file is an executable and the flag is set, it will " "be executed with the permissions of the group."); QString stickyWhatsThis; if (isDir) stickyWhatsThis = i18n("If the Sticky flag is set on a folder, only the owner " "and root can delete or rename files. Otherwise everybody " "with write permissions can do this."); else stickyWhatsThis = i18n("The Sticky flag on a file is ignored on Linux, but may " "be used on some systems"); mode_t aPermissions, aPartialPermissions; mode_t dummy1, dummy2; if (!d->isIrregular) { switch (d->pmode) { case PermissionsOnlyFiles: getPermissionMasks(aPartialPermissions, dummy1, aPermissions, dummy2); break; case PermissionsOnlyDirs: case PermissionsMixed: getPermissionMasks(dummy1, aPartialPermissions, dummy2, aPermissions); break; case PermissionsOnlyLinks: aPermissions = UniRead | UniWrite | UniExec | UniSpecial; aPartialPermissions = 0; break; } } else { aPermissions = d->permissions; aPartialPermissions = d->partialPermissions; } // Draw Checkboxes QCheckBox *cba[3][4]; for (int row = 0; row < 3; ++row) { for (int col = 0; col < 4; ++col) { QCheckBox *cb = new QCheckBox(gb); if (col != 3) { theNotSpecials.append(cb); } cba[row][col] = cb; cb->setChecked(aPermissions & fperm[row][col]); if (aPartialPermissions & fperm[row][col]) { cb->setTristate(); cb->setCheckState(Qt::PartiallyChecked); } else if (d->cbRecursive && d->cbRecursive->isChecked()) { cb->setTristate(); } cb->setEnabled(d->canChangePermissions); gl->addWidget(cb, row + 2, col + 1); switch (col) { case 0: cb->setText(readLabel); cb->setWhatsThis(readWhatsThis); break; case 1: cb->setText(writeLabel); cb->setWhatsThis(writeWhatsThis); break; case 2: cb->setText(execLabel); cb->setWhatsThis(execWhatsThis); break; case 3: switch (row) { case 0: cb->setText(i18n("Set UID")); cb->setWhatsThis(setUidWhatsThis); break; case 1: cb->setText(i18n("Set GID")); cb->setWhatsThis(setGidWhatsThis); break; case 2: cb->setText(i18nc("File permission", "Sticky")); cb->setWhatsThis(stickyWhatsThis); break; } break; } } } gl->setColumnStretch(6, 10); #if HAVE_POSIX_ACL KACLEditWidget *extendedACLs = nullptr; // FIXME make it work with partial entries if (properties->items().count() == 1) { QByteArray path = QFile::encodeName(properties->item().url().toLocalFile()); d->fileSystemSupportsACLs = fileSystemSupportsACL(path); } if (d->fileSystemSupportsACLs) { std::for_each(theNotSpecials.begin(), theNotSpecials.end(), std::mem_fun(&QWidget::hide)); extendedACLs = new KACLEditWidget(&dlg); extendedACLs->setEnabled(d->canChangePermissions); vbox->addWidget(extendedACLs); if (d->extendedACL.isValid() && d->extendedACL.isExtended()) { extendedACLs->setACL(d->extendedACL); } else { extendedACLs->setACL(KACL(aPermissions)); } if (d->defaultACL.isValid()) { extendedACLs->setDefaultACL(d->defaultACL); } if (properties->items().constFirst().isDir()) { extendedACLs->setAllowDefaults(true); } } #endif QDialogButtonBox *buttonBox = new QDialogButtonBox(&dlg); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); vbox->addWidget(buttonBox); if (dlg.exec() != QDialog::Accepted) { return; } mode_t andPermissions = mode_t(~0); mode_t orPermissions = 0; for (int row = 0; row < 3; ++row) for (int col = 0; col < 4; ++col) { switch (cba[row][col]->checkState()) { case Qt::Checked: orPermissions |= fperm[row][col]; //fall through case Qt::Unchecked: andPermissions &= ~fperm[row][col]; break; case Qt::PartiallyChecked: break; } } d->isIrregular = false; const KFileItemList items = properties->items(); KFileItemList::const_iterator it = items.begin(); const KFileItemList::const_iterator kend = items.end(); for (; it != kend; ++it) { if (isIrregular(((*it).permissions() & andPermissions) | orPermissions, (*it).isDir(), (*it).isLink())) { d->isIrregular = true; break; } } d->permissions = orPermissions; d->partialPermissions = andPermissions; #if HAVE_POSIX_ACL // override with the acls, if present if (extendedACLs) { d->extendedACL = extendedACLs->getACL(); d->defaultACL = extendedACLs->getDefaultACL(); d->hasExtendedACL = d->extendedACL.isExtended() || d->defaultACL.isValid(); d->permissions = d->extendedACL.basePermissions(); d->permissions |= (andPermissions | orPermissions) & (S_ISUID | S_ISGID | S_ISVTX); } #endif updateAccessControls(); emit changed(); } // QString KFilePermissionsPropsPlugin::tabName () const // { // return i18n ("&Permissions"); // } KFilePermissionsPropsPlugin::~KFilePermissionsPropsPlugin() { delete d; } bool KFilePermissionsPropsPlugin::supports(const KFileItemList & /*_items*/) { return true; } // sets a combo box in the Access Control frame void KFilePermissionsPropsPlugin::setComboContent(QComboBox *combo, PermissionsTarget target, mode_t permissions, mode_t partial) { combo->clear(); if (d->isIrregular) { //#176876 return; } if (d->pmode == PermissionsOnlyLinks) { combo->addItem(i18n("Link")); combo->setCurrentIndex(0); return; } mode_t tMask = permissionsMasks[target]; int textIndex; for (textIndex = 0; standardPermissions[textIndex] != (mode_t) - 1; textIndex++) { if ((standardPermissions[textIndex]&tMask) == (permissions & tMask & (UniRead | UniWrite))) { break; } } Q_ASSERT(standardPermissions[textIndex] != (mode_t) - 1); // must not happen, would be irreglar for (int i = 0; permissionsTexts[(int)d->pmode][i]; i++) { combo->addItem(i18n(permissionsTexts[(int)d->pmode][i])); } if (partial & tMask & ~UniExec) { combo->addItem(i18n("Varying (No Change)")); combo->setCurrentIndex(3); } else { combo->setCurrentIndex(textIndex); } } // permissions are irregular if they cant be displayed in a combo box. bool KFilePermissionsPropsPlugin::isIrregular(mode_t permissions, bool isDir, bool isLink) { if (isLink) { // links are always ok return false; } mode_t p = permissions; if (p & (S_ISUID | S_ISGID)) { // setuid/setgid -> irregular return true; } if (isDir) { p &= ~S_ISVTX; // ignore sticky on dirs // check supported flag combinations mode_t p0 = p & UniOwner; if ((p0 != 0) && (p0 != (S_IRUSR | S_IXUSR)) && (p0 != UniOwner)) { return true; } p0 = p & UniGroup; if ((p0 != 0) && (p0 != (S_IRGRP | S_IXGRP)) && (p0 != UniGroup)) { return true; } p0 = p & UniOthers; if ((p0 != 0) && (p0 != (S_IROTH | S_IXOTH)) && (p0 != UniOthers)) { return true; } return false; } if (p & S_ISVTX) { // sticky on file -> irregular return true; } // check supported flag combinations mode_t p0 = p & UniOwner; bool usrXPossible = !p0; // true if this file could be an executable if (p0 & S_IXUSR) { if ((p0 == S_IXUSR) || (p0 == (S_IWUSR | S_IXUSR))) { return true; } usrXPossible = true; } else if (p0 == S_IWUSR) { return true; } p0 = p & UniGroup; bool grpXPossible = !p0; // true if this file could be an executable if (p0 & S_IXGRP) { if ((p0 == S_IXGRP) || (p0 == (S_IWGRP | S_IXGRP))) { return true; } grpXPossible = true; } else if (p0 == S_IWGRP) { return true; } if (p0 == 0) { grpXPossible = true; } p0 = p & UniOthers; bool othXPossible = !p0; // true if this file could be an executable if (p0 & S_IXOTH) { if ((p0 == S_IXOTH) || (p0 == (S_IWOTH | S_IXOTH))) { return true; } othXPossible = true; } else if (p0 == S_IWOTH) { return true; } // check that there either all targets are executable-compatible, or none return (p & UniExec) && !(usrXPossible && grpXPossible && othXPossible); } // enables/disabled the widgets in the Access Control frame void KFilePermissionsPropsPlugin::enableAccessControls(bool enable) { d->ownerPermCombo->setEnabled(enable); d->groupPermCombo->setEnabled(enable); d->othersPermCombo->setEnabled(enable); if (d->extraCheckbox) { d->extraCheckbox->setEnabled(enable); } if (d->cbRecursive) { d->cbRecursive->setEnabled(enable); } } // updates all widgets in the Access Control frame void KFilePermissionsPropsPlugin::updateAccessControls() { setComboContent(d->ownerPermCombo, PermissionsOwner, d->permissions, d->partialPermissions); setComboContent(d->groupPermCombo, PermissionsGroup, d->permissions, d->partialPermissions); setComboContent(d->othersPermCombo, PermissionsOthers, d->permissions, d->partialPermissions); switch (d->pmode) { case PermissionsOnlyLinks: enableAccessControls(false); break; case PermissionsOnlyFiles: enableAccessControls(d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL); if (d->canChangePermissions) d->explanationLabel->setText(d->isIrregular || d->hasExtendedACL ? i18np("This file uses advanced permissions", "These files use advanced permissions.", properties->items().count()) : QString()); if (d->partialPermissions & UniExec) { d->extraCheckbox->setTristate(); d->extraCheckbox->setCheckState(Qt::PartiallyChecked); } else { d->extraCheckbox->setTristate(false); d->extraCheckbox->setChecked(d->permissions & UniExec); } break; case PermissionsOnlyDirs: enableAccessControls(d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL); // if this is a dir, and we can change permissions, don't dis-allow // recursive, we can do that for ACL setting. if (d->cbRecursive) { d->cbRecursive->setEnabled(d->canChangePermissions && !d->isIrregular); } if (d->canChangePermissions) d->explanationLabel->setText(d->isIrregular || d->hasExtendedACL ? i18np("This folder uses advanced permissions.", "These folders use advanced permissions.", properties->items().count()) : QString()); if (d->partialPermissions & S_ISVTX) { d->extraCheckbox->setTristate(); d->extraCheckbox->setCheckState(Qt::PartiallyChecked); } else { d->extraCheckbox->setTristate(false); d->extraCheckbox->setChecked(d->permissions & S_ISVTX); } break; case PermissionsMixed: enableAccessControls(d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL); if (d->canChangePermissions) d->explanationLabel->setText(d->isIrregular || d->hasExtendedACL ? i18n("These files use advanced permissions.") : QString()); if (d->partialPermissions & S_ISVTX) { d->extraCheckbox->setTristate(); d->extraCheckbox->setCheckState(Qt::PartiallyChecked); } else { d->extraCheckbox->setTristate(false); d->extraCheckbox->setChecked(d->permissions & S_ISVTX); } break; } } // gets masks for files and dirs from the Access Control frame widgets void KFilePermissionsPropsPlugin::getPermissionMasks(mode_t &andFilePermissions, mode_t &andDirPermissions, mode_t &orFilePermissions, mode_t &orDirPermissions) { andFilePermissions = mode_t(~UniSpecial); andDirPermissions = mode_t(~(S_ISUID | S_ISGID)); orFilePermissions = 0; orDirPermissions = 0; if (d->isIrregular) { return; } mode_t m = standardPermissions[d->ownerPermCombo->currentIndex()]; if (m != (mode_t) - 1) { orFilePermissions |= m & UniOwner; if ((m & UniOwner) && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) { andFilePermissions &= ~(S_IRUSR | S_IWUSR); } else { andFilePermissions &= ~(S_IRUSR | S_IWUSR | S_IXUSR); if ((m & S_IRUSR) && (d->extraCheckbox->checkState() == Qt::Checked)) { orFilePermissions |= S_IXUSR; } } orDirPermissions |= m & UniOwner; if (m & S_IRUSR) { orDirPermissions |= S_IXUSR; } andDirPermissions &= ~(S_IRUSR | S_IWUSR | S_IXUSR); } m = standardPermissions[d->groupPermCombo->currentIndex()]; if (m != (mode_t) - 1) { orFilePermissions |= m & UniGroup; if ((m & UniGroup) && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) { andFilePermissions &= ~(S_IRGRP | S_IWGRP); } else { andFilePermissions &= ~(S_IRGRP | S_IWGRP | S_IXGRP); if ((m & S_IRGRP) && (d->extraCheckbox->checkState() == Qt::Checked)) { orFilePermissions |= S_IXGRP; } } orDirPermissions |= m & UniGroup; if (m & S_IRGRP) { orDirPermissions |= S_IXGRP; } andDirPermissions &= ~(S_IRGRP | S_IWGRP | S_IXGRP); } m = d->othersPermCombo->currentIndex() >= 0 ? standardPermissions[d->othersPermCombo->currentIndex()] : (mode_t) - 1; if (m != (mode_t) - 1) { orFilePermissions |= m & UniOthers; if ((m & UniOthers) && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) { andFilePermissions &= ~(S_IROTH | S_IWOTH); } else { andFilePermissions &= ~(S_IROTH | S_IWOTH | S_IXOTH); if ((m & S_IROTH) && (d->extraCheckbox->checkState() == Qt::Checked)) { orFilePermissions |= S_IXOTH; } } orDirPermissions |= m & UniOthers; if (m & S_IROTH) { orDirPermissions |= S_IXOTH; } andDirPermissions &= ~(S_IROTH | S_IWOTH | S_IXOTH); } if (((d->pmode == PermissionsMixed) || (d->pmode == PermissionsOnlyDirs)) && (d->extraCheckbox->checkState() != Qt::PartiallyChecked)) { andDirPermissions &= ~S_ISVTX; if (d->extraCheckbox->checkState() == Qt::Checked) { orDirPermissions |= S_ISVTX; } } } void KFilePermissionsPropsPlugin::applyChanges() { mode_t orFilePermissions; mode_t orDirPermissions; mode_t andFilePermissions; mode_t andDirPermissions; if (!d->canChangePermissions) { return; } if (!d->isIrregular) getPermissionMasks(andFilePermissions, andDirPermissions, orFilePermissions, orDirPermissions); else { orFilePermissions = d->permissions; andFilePermissions = d->partialPermissions; orDirPermissions = d->permissions; andDirPermissions = d->partialPermissions; } QString owner, group; if (d->usrEdit) { owner = d->usrEdit->text(); } if (d->grpEdit) { group = d->grpEdit->text(); } else if (d->grpCombo) { group = d->grpCombo->currentText(); } if (owner == d->strOwner) { owner.clear(); // no change } if (group == d->strGroup) { group.clear(); } bool recursive = d->cbRecursive && d->cbRecursive->isChecked(); bool permissionChange = false; KFileItemList files, dirs; const KFileItemList items = properties->items(); KFileItemList::const_iterator it = items.begin(); const KFileItemList::const_iterator kend = items.end(); for (; it != kend; ++it) { if ((*it).isDir()) { dirs.append(*it); if ((*it).permissions() != (((*it).permissions() & andDirPermissions) | orDirPermissions)) { permissionChange = true; } } else if ((*it).isFile()) { files.append(*it); if ((*it).permissions() != (((*it).permissions() & andFilePermissions) | orFilePermissions)) { permissionChange = true; } } } const bool ACLChange = (d->extendedACL != properties->item().ACL()); const bool defaultACLChange = (d->defaultACL != properties->item().defaultACL()); if (owner.isEmpty() && group.isEmpty() && !recursive && !permissionChange && !ACLChange && !defaultACLChange) { return; } KIO::Job *job; if (!files.isEmpty()) { job = KIO::chmod(files, orFilePermissions, ~andFilePermissions, owner, group, false); if (ACLChange && d->fileSystemSupportsACLs) { job->addMetaData(QStringLiteral("ACL_STRING"), d->extendedACL.isValid() ? d->extendedACL.asString() : QStringLiteral("ACL_DELETE")); } if (defaultACLChange && d->fileSystemSupportsACLs) { job->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), d->defaultACL.isValid() ? d->defaultACL.asString() : QStringLiteral("ACL_DELETE")); } connect(job, &KJob::result, this, &KFilePermissionsPropsPlugin::slotChmodResult); QEventLoop eventLoop; connect(this, &KFilePermissionsPropsPlugin::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } if (!dirs.isEmpty()) { job = KIO::chmod(dirs, orDirPermissions, ~andDirPermissions, owner, group, recursive); if (ACLChange && d->fileSystemSupportsACLs) { job->addMetaData(QStringLiteral("ACL_STRING"), d->extendedACL.isValid() ? d->extendedACL.asString() : QStringLiteral("ACL_DELETE")); } if (defaultACLChange && d->fileSystemSupportsACLs) { job->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), d->defaultACL.isValid() ? d->defaultACL.asString() : QStringLiteral("ACL_DELETE")); } connect(job, &KJob::result, this, &KFilePermissionsPropsPlugin::slotChmodResult); QEventLoop eventLoop; connect(this, &KFilePermissionsPropsPlugin::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } } void KFilePermissionsPropsPlugin::slotChmodResult(KJob *job) { // qDebug() << "KFilePermissionsPropsPlugin::slotChmodResult"; if (job->error()) { job->uiDelegate()->showErrorMessage(); } // allow apply() to return emit leaveModality(); } class KChecksumsPlugin::KChecksumsPluginPrivate { public: KChecksumsPluginPrivate() { } ~KChecksumsPluginPrivate() { } QWidget m_widget; Ui::ChecksumsWidget m_ui; QFileSystemWatcher fileWatcher; QString m_md5; QString m_sha1; QString m_sha256; }; KChecksumsPlugin::KChecksumsPlugin(KPropertiesDialog *dialog) : KPropertiesDialogPlugin(dialog), d(new KChecksumsPluginPrivate) { d->m_ui.setupUi(&d->m_widget); properties->addPage(&d->m_widget, i18nc("@title:tab", "C&hecksums")); d->m_ui.md5CopyButton->hide(); d->m_ui.sha1CopyButton->hide(); d->m_ui.sha256CopyButton->hide(); connect(d->m_ui.lineEdit, &QLineEdit::textChanged, this, [=](const QString &text) { slotVerifyChecksum(text.toLower()); }); connect(d->m_ui.md5Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowMd5); connect(d->m_ui.sha1Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowSha1); connect(d->m_ui.sha256Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowSha256); d->fileWatcher.addPath(properties->item().localPath()); connect(&d->fileWatcher, &QFileSystemWatcher::fileChanged, this, &KChecksumsPlugin::slotInvalidateCache); auto clipboard = QApplication::clipboard(); connect(d->m_ui.md5CopyButton, &QPushButton::clicked, this, [=]() { clipboard->setText(d->m_md5); }); connect(d->m_ui.sha1CopyButton, &QPushButton::clicked, this, [=]() { clipboard->setText(d->m_sha1); }); connect(d->m_ui.sha256CopyButton, &QPushButton::clicked, this, [=]() { clipboard->setText(d->m_sha256); }); connect(d->m_ui.pasteButton, &QPushButton::clicked, this, [=]() { d->m_ui.lineEdit->setText(clipboard->text()); }); setDefaultState(); } KChecksumsPlugin::~KChecksumsPlugin() { delete d; } bool KChecksumsPlugin::supports(const KFileItemList &items) { if (items.count() != 1) { return false; } const KFileItem item = items.first(); return item.isFile() && !item.localPath().isEmpty() && item.isReadable() && !item.isDesktopFile() && !item.isLink(); } void KChecksumsPlugin::slotInvalidateCache() { d->m_md5 = QString(); d->m_sha1 = QString(); d->m_sha256 = QString(); } void KChecksumsPlugin::slotShowMd5() { auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.md5Button, label); d->m_ui.md5Button->hide(); showChecksum(QCryptographicHash::Md5, label, d->m_ui.md5CopyButton); } void KChecksumsPlugin::slotShowSha1() { auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.sha1Button, label); d->m_ui.sha1Button->hide(); showChecksum(QCryptographicHash::Sha1, label, d->m_ui.sha1CopyButton); } void KChecksumsPlugin::slotShowSha256() { auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.sha256Button, label); d->m_ui.sha256Button->hide(); showChecksum(QCryptographicHash::Sha256, label, d->m_ui.sha256CopyButton); } void KChecksumsPlugin::slotVerifyChecksum(const QString &input) { auto algorithm = detectAlgorithm(input); // Input is not a supported hash algorithm. if (algorithm == QCryptographicHash::Md4) { if (input.isEmpty()) { setDefaultState(); } else { setInvalidChecksumState(); } return; } const QString checksum = cachedChecksum(algorithm); // Checksum already in cache. if (!checksum.isEmpty()) { const bool isMatch = (checksum == input); if (isMatch) { setMatchState(); } else { setMismatchState(); } return; } // Calculate checksum in another thread. auto futureWatcher = new QFutureWatcher(this); connect(futureWatcher, &QFutureWatcher::finished, this, [=]() { const QString checksum = futureWatcher->result(); futureWatcher->deleteLater(); cacheChecksum(checksum, algorithm); switch (algorithm) { case QCryptographicHash::Md5: slotShowMd5(); break; case QCryptographicHash::Sha1: slotShowSha1(); break; case QCryptographicHash::Sha256: slotShowSha256(); break; default: break; } const bool isMatch = (checksum == input); if (isMatch) { setMatchState(); } else { setMismatchState(); } }); // Notify the user about the background computation. setVerifyState(); auto future = QtConcurrent::run(&KChecksumsPlugin::computeChecksum, algorithm, properties->item().localPath()); futureWatcher->setFuture(future); } bool KChecksumsPlugin::isMd5(const QString &input) { QRegularExpression regex(QStringLiteral("^[a-f0-9]{32}$")); return regex.match(input).hasMatch(); } bool KChecksumsPlugin::isSha1(const QString &input) { QRegularExpression regex(QStringLiteral("^[a-f0-9]{40}$")); return regex.match(input).hasMatch(); } bool KChecksumsPlugin::isSha256(const QString &input) { QRegularExpression regex(QStringLiteral("^[a-f0-9]{64}$")); return regex.match(input).hasMatch(); } QString KChecksumsPlugin::computeChecksum(QCryptographicHash::Algorithm algorithm, const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return QString(); } QCryptographicHash hash(algorithm); hash.addData(&file); return QString::fromLatin1(hash.result().toHex()); } QCryptographicHash::Algorithm KChecksumsPlugin::detectAlgorithm(const QString &input) { if (isMd5(input)) { return QCryptographicHash::Md5; } if (isSha1(input)) { return QCryptographicHash::Sha1; } if (isSha256(input)) { return QCryptographicHash::Sha256; } // Md4 used as negative error code. return QCryptographicHash::Md4; } void KChecksumsPlugin::setDefaultState() { QColor defaultColor = d->m_widget.palette().color(QPalette::Base); QPalette palette = d->m_widget.palette(); palette.setColor(QPalette::Base, defaultColor); d->m_ui.feedbackLabel->hide(); d->m_ui.lineEdit->setPalette(palette); d->m_ui.lineEdit->setToolTip(QString()); } void KChecksumsPlugin::setInvalidChecksumState() { KColorScheme colorScheme(QPalette::Active, KColorScheme::View); QColor warningColor = colorScheme.background(KColorScheme::NegativeBackground).color(); QPalette palette = d->m_widget.palette(); palette.setColor(QPalette::Base, warningColor); d->m_ui.feedbackLabel->setText(i18n("Invalid checksum.")); d->m_ui.feedbackLabel->show(); d->m_ui.lineEdit->setPalette(palette); d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The given input is not a valid MD5, SHA1 or SHA256 checksum.")); } void KChecksumsPlugin::setMatchState() { KColorScheme colorScheme(QPalette::Active, KColorScheme::View); QColor positiveColor = colorScheme.background(KColorScheme::PositiveBackground).color(); QPalette palette = d->m_widget.palette(); palette.setColor(QPalette::Base, positiveColor); d->m_ui.feedbackLabel->setText(i18n("Checksums match.")); d->m_ui.feedbackLabel->show(); d->m_ui.lineEdit->setPalette(palette); d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The computed checksum and the expected checksum match.")); } void KChecksumsPlugin::setMismatchState() { KColorScheme colorScheme(QPalette::Active, KColorScheme::View); QColor warningColor = colorScheme.background(KColorScheme::NegativeBackground).color(); QPalette palette = d->m_widget.palette(); palette.setColor(QPalette::Base, warningColor); d->m_ui.feedbackLabel->setText(i18n("

Checksums do not match.

" "This may be due to a faulty download. Try re-downloading the file.
" "If the verification still fails, contact the source of the file.")); d->m_ui.feedbackLabel->show(); d->m_ui.lineEdit->setPalette(palette); d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The computed checksum and the expected checksum differ.")); } void KChecksumsPlugin::setVerifyState() { // Users can paste a checksum at any time, so reset to default. setDefaultState(); d->m_ui.feedbackLabel->setText(i18nc("notify the user about a computation in the background", "Verifying checksum...")); d->m_ui.feedbackLabel->show(); } void KChecksumsPlugin::showChecksum(QCryptographicHash::Algorithm algorithm, QLabel *label, QPushButton *copyButton) { const QString checksum = cachedChecksum(algorithm); // Checksum in cache, nothing else to do. if (!checksum.isEmpty()) { label->setText(checksum); return; } // Calculate checksum in another thread. auto futureWatcher = new QFutureWatcher(this); connect(futureWatcher, &QFutureWatcher::finished, this, [=]() { const QString checksum = futureWatcher->result(); futureWatcher->deleteLater(); label->setText(checksum); cacheChecksum(checksum, algorithm); copyButton->show(); }); auto future = QtConcurrent::run(&KChecksumsPlugin::computeChecksum, algorithm, properties->item().localPath()); futureWatcher->setFuture(future); } QString KChecksumsPlugin::cachedChecksum(QCryptographicHash::Algorithm algorithm) const { switch (algorithm) { case QCryptographicHash::Md5: return d->m_md5; case QCryptographicHash::Sha1: return d->m_sha1; case QCryptographicHash::Sha256: return d->m_sha256; default: break; } return QString(); } void KChecksumsPlugin::cacheChecksum(const QString &checksum, QCryptographicHash::Algorithm algorithm) { switch (algorithm) { case QCryptographicHash::Md5: d->m_md5 = checksum; break; case QCryptographicHash::Sha1: d->m_sha1 = checksum; break; case QCryptographicHash::Sha256: d->m_sha256 = checksum; break; default: return; } } class KUrlPropsPlugin::KUrlPropsPluginPrivate { public: KUrlPropsPluginPrivate() { } ~KUrlPropsPluginPrivate() { } QFrame *m_frame; KUrlRequester *URLEdit; QString URLStr; bool fileNameReadOnly = false; }; KUrlPropsPlugin::KUrlPropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KUrlPropsPluginPrivate) { d->m_frame = new QFrame(); properties->addPage(d->m_frame, i18n("U&RL")); QVBoxLayout *layout = new QVBoxLayout(d->m_frame); layout->setContentsMargins(0, 0, 0, 0); QLabel *l; l = new QLabel(d->m_frame); l->setObjectName(QStringLiteral("Label_1")); l->setText(i18n("URL:")); layout->addWidget(l, Qt::AlignRight); d->URLEdit = new KUrlRequester(d->m_frame); layout->addWidget(d->URLEdit); KIO::StatJob *job = KIO::mostLocalUrl(properties->url()); KJobWidgets::setWindow(job, properties); job->exec(); QUrl url = job->mostLocalUrl(); if (url.isLocalFile()) { QString path = url.toLocalFile(); QFile f(path); if (!f.open(QIODevice::ReadOnly)) { return; } f.close(); KDesktopFile config(path); const KConfigGroup dg = config.desktopGroup(); d->URLStr = dg.readPathEntry("URL", QString()); if (!d->URLStr.isEmpty()) { d->URLEdit->setUrl(QUrl(d->URLStr)); } } connect(d->URLEdit, &KUrlRequester::textChanged, this, &KPropertiesDialogPlugin::changed); layout->addStretch(1); } KUrlPropsPlugin::~KUrlPropsPlugin() { delete d; } void KUrlPropsPlugin::setFileNameReadOnly(bool ro) { d->fileNameReadOnly = ro; } // QString KUrlPropsPlugin::tabName () const // { // return i18n ("U&RL"); // } bool KUrlPropsPlugin::supports(const KFileItemList &_items) { if (_items.count() != 1) { return false; } const KFileItem item = _items.first(); // check if desktop file if (!item.isDesktopFile()) { return false; } // open file and check type bool isLocal; QUrl url = item.mostLocalUrl(isLocal); if (!isLocal) { return false; } KDesktopFile config(url.toLocalFile()); return config.hasLinkType(); } void KUrlPropsPlugin::applyChanges() { KIO::StatJob *job = KIO::mostLocalUrl(properties->url()); KJobWidgets::setWindow(job, properties); job->exec(); const QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { KMessageBox::sorry(nullptr, i18n("Could not save properties. Only entries on local file systems are supported.")); return; } QString path = url.toLocalFile(); QFile f(path); if (!f.open(QIODevice::ReadWrite)) { KMessageBox::sorry(nullptr, i18n("Could not save properties. You do not have " "sufficient access to write to %1.", path)); return; } f.close(); KDesktopFile config(path); KConfigGroup dg = config.desktopGroup(); dg.writeEntry("Type", QStringLiteral("Link")); dg.writePathEntry("URL", d->URLEdit->url().toString()); // Users can't create a Link .desktop file with a Name field, // but distributions can. Update the Name field in that case, // if the file name could have been changed. if (!d->fileNameReadOnly && dg.hasKey("Name")) { const QString nameStr = nameFromFileName(properties->url().fileName()); dg.writeEntry("Name", nameStr); dg.writeEntry("Name", nameStr, KConfigBase::Persistent | KConfigBase::Localized); } } /* ---------------------------------------------------- * * KDevicePropsPlugin * * -------------------------------------------------- */ class KDevicePropsPlugin::KDevicePropsPluginPrivate { public: KDevicePropsPluginPrivate() { } ~KDevicePropsPluginPrivate() { } bool isMounted() const { const QString dev = device->currentText(); return !dev.isEmpty() && KMountPoint::currentMountPoints().findByDevice(dev); } QFrame *m_frame; QStringList mountpointlist; QLabel *m_freeSpaceText; QLabel *m_freeSpaceLabel; QProgressBar *m_freeSpaceBar; KComboBox *device; QLabel *mountpoint; QCheckBox *readonly; QStringList m_devicelist; }; KDevicePropsPlugin::KDevicePropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KDevicePropsPluginPrivate) { d->m_frame = new QFrame(); properties->addPage(d->m_frame, i18n("De&vice")); QStringList devices; const KMountPoint::List mountPoints = KMountPoint::possibleMountPoints(); for (KMountPoint::List::ConstIterator it = mountPoints.begin(); it != mountPoints.end(); ++it) { const KMountPoint::Ptr mp = (*it); QString mountPoint = mp->mountPoint(); QString device = mp->mountedFrom(); // qDebug()<<"mountPoint :"<mountType() :"<mountType(); if ((mountPoint != QLatin1String("-")) && (mountPoint != QLatin1String("none")) && !mountPoint.isEmpty() && device != QLatin1String("none")) { devices.append(device + QLatin1String(" (") + mountPoint + QLatin1Char(')')); d->m_devicelist.append(device); d->mountpointlist.append(mountPoint); } } QGridLayout *layout = new QGridLayout(d->m_frame); layout->setContentsMargins(0, 0, 0, 0); layout->setColumnStretch(1, 1); QLabel *label; label = new QLabel(d->m_frame); label->setText(devices.count() == 0 ? i18n("Device (/dev/fd0):") : // old style i18n("Device:")); // new style (combobox) layout->addWidget(label, 0, 0, Qt::AlignRight); d->device = new KComboBox(d->m_frame); d->device->setObjectName(QStringLiteral("ComboBox_device")); d->device->setEditable(true); d->device->addItems(devices); layout->addWidget(d->device, 0, 1); connect(d->device, QOverload::of(&QComboBox::activated), this, &KDevicePropsPlugin::slotActivated); d->readonly = new QCheckBox(d->m_frame); d->readonly->setObjectName(QStringLiteral("CheckBox_readonly")); d->readonly->setText(i18n("Read only")); layout->addWidget(d->readonly, 1, 1); label = new QLabel(d->m_frame); label->setText(i18n("File system:")); layout->addWidget(label, 2, 0, Qt::AlignRight); QLabel *fileSystem = new QLabel(d->m_frame); layout->addWidget(fileSystem, 2, 1); label = new QLabel(d->m_frame); label->setText(devices.count() == 0 ? i18n("Mount point (/mnt/floppy):") : // old style i18n("Mount point:")); // new style (combobox) layout->addWidget(label, 3, 0, Qt::AlignRight); d->mountpoint = new QLabel(d->m_frame); d->mountpoint->setObjectName(QStringLiteral("LineEdit_mountpoint")); layout->addWidget(d->mountpoint, 3, 1); // show disk free d->m_freeSpaceText = new QLabel(i18nc("Amount of used and available space on this device or partition", "Free space:"), d->m_frame); layout->addWidget(d->m_freeSpaceText, 4, 0, Qt::AlignRight); d->m_freeSpaceLabel = new QLabel(d->m_frame); layout->addWidget(d->m_freeSpaceLabel, 4, 1); d->m_freeSpaceBar = new QProgressBar(d->m_frame); d->m_freeSpaceBar->setObjectName(QStringLiteral("freeSpaceBar")); layout->addWidget(d->m_freeSpaceBar, 5, 0, 1, 2); // we show it in the slot when we know the values d->m_freeSpaceText->hide(); d->m_freeSpaceLabel->hide(); d->m_freeSpaceBar->hide(); KSeparator *sep = new KSeparator(Qt::Horizontal, d->m_frame); layout->addWidget(sep, 6, 0, 1, 2); layout->setRowStretch(7, 1); KIO::StatJob *job = KIO::mostLocalUrl(_props->url()); KJobWidgets::setWindow(job, _props); job->exec(); QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { return; } QString path = url.toLocalFile(); QFile f(path); if (!f.open(QIODevice::ReadOnly)) { return; } f.close(); const KDesktopFile _config(path); const KConfigGroup config = _config.desktopGroup(); QString deviceStr = config.readEntry("Dev"); QString mountPointStr = config.readEntry("MountPoint"); bool ro = config.readEntry("ReadOnly", false); fileSystem->setText(config.readEntry("FSType")); d->device->setEditText(deviceStr); if (!deviceStr.isEmpty()) { // Set default options for this device (first matching entry) int index = d->m_devicelist.indexOf(deviceStr); if (index != -1) { //qDebug() << "found it" << index; slotActivated(index); } } if (!mountPointStr.isEmpty()) { d->mountpoint->setText(mountPointStr); updateInfo(); } d->readonly->setChecked(ro); connect(d->device, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); connect(d->device, &QComboBox::currentTextChanged, this, &KPropertiesDialogPlugin::changed); connect(d->readonly, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(d->device, &QComboBox::currentTextChanged, this, &KDevicePropsPlugin::slotDeviceChanged); } KDevicePropsPlugin::~KDevicePropsPlugin() { delete d; } // QString KDevicePropsPlugin::tabName () const // { // return i18n ("De&vice"); // } void KDevicePropsPlugin::updateInfo() { // we show it in the slot when we know the values d->m_freeSpaceText->hide(); d->m_freeSpaceLabel->hide(); d->m_freeSpaceBar->hide(); if (!d->mountpoint->text().isEmpty() && d->isMounted()) { KDiskFreeSpaceInfo info = KDiskFreeSpaceInfo::freeSpaceInfo(d->mountpoint->text()); slotFoundMountPoint(info.mountPoint(), info.size() / 1024, info.used() / 1024, info.available() / 1024); } } void KDevicePropsPlugin::slotActivated(int index) { // index can be more than the number of known devices, when the user types // a "custom" device. if (index < d->m_devicelist.count()) { // Update mountpoint so that it matches the device that was selected in the combo d->device->setEditText(d->m_devicelist[index]); d->mountpoint->setText(d->mountpointlist[index]); } updateInfo(); } void KDevicePropsPlugin::slotDeviceChanged() { // Update mountpoint so that it matches the typed device int index = d->m_devicelist.indexOf(d->device->currentText()); if (index != -1) { d->mountpoint->setText(d->mountpointlist[index]); } else { d->mountpoint->setText(QString()); } updateInfo(); } void KDevicePropsPlugin::slotFoundMountPoint(const QString &, quint64 kibSize, quint64 /*kibUsed*/, quint64 kibAvail) { d->m_freeSpaceText->show(); d->m_freeSpaceLabel->show(); const int percUsed = kibSize != 0 ? (100 - (int)(100.0 * kibAvail / kibSize)) : 100; d->m_freeSpaceLabel->setText( i18nc("Available space out of total partition size (percent used)", "%1 free of %2 (%3% used)", KIO::convertSizeFromKiB(kibAvail), KIO::convertSizeFromKiB(kibSize), percUsed)); d->m_freeSpaceBar->setRange(0, 100); d->m_freeSpaceBar->setValue(percUsed); d->m_freeSpaceBar->show(); } bool KDevicePropsPlugin::supports(const KFileItemList &_items) { if (_items.count() != 1) { return false; } const KFileItem item = _items.first(); // check if desktop file if (!item.isDesktopFile()) { return false; } // open file and check type bool isLocal; QUrl url = item.mostLocalUrl(isLocal); if (!isLocal) { return false; } KDesktopFile config(url.toLocalFile()); return config.hasDeviceType(); } void KDevicePropsPlugin::applyChanges() { KIO::StatJob *job = KIO::mostLocalUrl(properties->url()); KJobWidgets::setWindow(job, properties); job->exec(); const QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { return; } const QString path = url.toLocalFile(); QFile f(path); if (!f.open(QIODevice::ReadWrite)) { KMessageBox::sorry(nullptr, i18n("Could not save properties. You do not have sufficient " "access to write to %1.", path)); return; } f.close(); KDesktopFile _config(path); KConfigGroup config = _config.desktopGroup(); config.writeEntry("Type", QStringLiteral("FSDevice")); config.writeEntry("Dev", d->device->currentText()); config.writeEntry("MountPoint", d->mountpoint->text()); config.writeEntry("ReadOnly", d->readonly->isChecked()); config.sync(); } /* ---------------------------------------------------- * * KDesktopPropsPlugin * * -------------------------------------------------- */ class KDesktopPropsPlugin::KDesktopPropsPluginPrivate { public: KDesktopPropsPluginPrivate() : w(new Ui_KPropertiesDesktopBase) , m_frame(new QFrame()) { } ~KDesktopPropsPluginPrivate() { delete w; } Ui_KPropertiesDesktopBase *w; QWidget *m_frame; QString m_origCommandStr; QString m_terminalOptionStr; QString m_suidUserStr; QString m_dbusStartupType; QString m_dbusServiceName; QString m_origDesktopFile; bool m_terminalBool; bool m_suidBool; bool m_hasDiscreteGpuBool; bool m_runOnDiscreteGpuBool; bool m_startupBool; }; KDesktopPropsPlugin::KDesktopPropsPlugin(KPropertiesDialog *_props) : KPropertiesDialogPlugin(_props), d(new KDesktopPropsPluginPrivate) { QMimeDatabase db; d->w->setupUi(d->m_frame); properties->addPage(d->m_frame, i18n("&Application")); bool bKDesktopMode = properties->url().scheme() == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop"); if (bKDesktopMode) { // Hide Name entry d->w->nameEdit->hide(); d->w->nameLabel->hide(); } d->w->pathEdit->setMode(KFile::Directory | KFile::LocalOnly); d->w->pathEdit->lineEdit()->setAcceptDrops(false); connect(d->w->nameEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->genNameEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->commentEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->commandEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->pathEdit, &KUrlRequester::textChanged, this, &KPropertiesDialogPlugin::changed); connect(d->w->browseButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotBrowseExec); connect(d->w->addFiletypeButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotAddFiletype); connect(d->w->delFiletypeButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotDelFiletype); connect(d->w->advancedButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotAdvanced); enum DiscreteGpuCheck { NotChecked, Present, Absent }; static DiscreteGpuCheck s_gpuCheck = NotChecked; if (s_gpuCheck == NotChecked) { // Check whether we have a discrete gpu bool hasDiscreteGpu = false; QDBusInterface iface(QStringLiteral("org.kde.Solid.PowerManagement"), QStringLiteral("/org/kde/Solid/PowerManagement"), QStringLiteral("org.kde.Solid.PowerManagement"), QDBusConnection::sessionBus()); if (iface.isValid()) { QDBusReply reply = iface.call(QStringLiteral("hasDualGpu")); if (reply.isValid()) { hasDiscreteGpu = reply.value(); } } s_gpuCheck = hasDiscreteGpu ? Present : Absent; } d->m_hasDiscreteGpuBool = s_gpuCheck == Present; // now populate the page KIO::StatJob *job = KIO::mostLocalUrl(_props->url()); KJobWidgets::setWindow(job, _props); job->exec(); QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { return; } d->m_origDesktopFile = url.toLocalFile(); QFile f(d->m_origDesktopFile); if (!f.open(QIODevice::ReadOnly)) { return; } f.close(); KDesktopFile _config(d->m_origDesktopFile); KConfigGroup config = _config.desktopGroup(); QString nameStr = _config.readName(); QString genNameStr = _config.readGenericName(); QString commentStr = _config.readComment(); QString commandStr = config.readEntry("Exec", QString()); d->m_origCommandStr = commandStr; QString pathStr = config.readEntry("Path", QString()); // not readPathEntry, see kservice.cpp d->m_terminalBool = config.readEntry("Terminal", false); d->m_terminalOptionStr = config.readEntry("TerminalOptions"); d->m_suidBool = config.readEntry("X-KDE-SubstituteUID", false); d->m_suidUserStr = config.readEntry("X-KDE-Username"); if (d->m_hasDiscreteGpuBool) { d->m_runOnDiscreteGpuBool = config.readEntry("X-KDE-RunOnDiscreteGpu", false); } if (config.hasKey("StartupNotify")) { d->m_startupBool = config.readEntry("StartupNotify", true); } else { d->m_startupBool = config.readEntry("X-KDE-StartupNotify", true); } d->m_dbusStartupType = config.readEntry("X-DBUS-StartupType").toLower(); // ### should there be a GUI for this setting? // At least we're copying it over to the local file, to avoid side effects (#157853) d->m_dbusServiceName = config.readEntry("X-DBUS-ServiceName"); const QStringList mimeTypes = config.readXdgListEntry("MimeType"); if (nameStr.isEmpty() || bKDesktopMode) { // We'll use the file name if no name is specified // because we _need_ a Name for a valid file. // But let's do it in apply, not here, so that we pick up the right name. setDirty(); } if (!bKDesktopMode) { d->w->nameEdit->setText(nameStr); } d->w->genNameEdit->setText(genNameStr); d->w->commentEdit->setText(commentStr); d->w->commandEdit->setText(commandStr); d->w->pathEdit->lineEdit()->setText(pathStr); // was: d->w->filetypeList->setFullWidth(true); // d->w->filetypeList->header()->setStretchEnabled(true, d->w->filetypeList->columns()-1); for (QStringList::ConstIterator it = mimeTypes.begin(); it != mimeTypes.end();) { QMimeType p = db.mimeTypeForName(*it); ++it; QString preference; if (it != mimeTypes.end()) { bool numeric; (*it).toInt(&numeric); if (numeric) { preference = *it; ++it; } } if (p.isValid()) { QTreeWidgetItem *item = new QTreeWidgetItem(); item->setText(0, p.name()); item->setText(1, p.comment()); item->setText(2, preference); d->w->filetypeList->addTopLevelItem(item); } } d->w->filetypeList->resizeColumnToContents(0); } KDesktopPropsPlugin::~KDesktopPropsPlugin() { delete d; } void KDesktopPropsPlugin::slotAddFiletype() { QMimeDatabase db; KMimeTypeChooserDialog dlg(i18n("Add File Type for %1", properties->url().fileName()), i18n("Select one or more file types to add:"), QStringList(), // no preselected mimetypes QString(), QStringList(), KMimeTypeChooser::Comments | KMimeTypeChooser::Patterns, d->m_frame); if (dlg.exec() == QDialog::Accepted) { foreach (const QString &mimetype, dlg.chooser()->mimeTypes()) { QMimeType p = db.mimeTypeForName(mimetype); if (!p.isValid()) { continue; } bool found = false; int count = d->w->filetypeList->topLevelItemCount(); for (int i = 0; !found && i < count; ++i) { if (d->w->filetypeList->topLevelItem(i)->text(0) == mimetype) { found = true; } } if (!found) { QTreeWidgetItem *item = new QTreeWidgetItem(); item->setText(0, p.name()); item->setText(1, p.comment()); d->w->filetypeList->addTopLevelItem(item); } d->w->filetypeList->resizeColumnToContents(0); } } emit changed(); } void KDesktopPropsPlugin::slotDelFiletype() { QTreeWidgetItem *cur = d->w->filetypeList->currentItem(); if (cur) { delete cur; emit changed(); } } void KDesktopPropsPlugin::checkCommandChanged() { if (KIO::DesktopExecParser::executableName(d->w->commandEdit->text()) != KIO::DesktopExecParser::executableName(d->m_origCommandStr)) { d->m_origCommandStr = d->w->commandEdit->text(); d->m_dbusStartupType.clear(); // Reset d->m_dbusServiceName.clear(); } } void KDesktopPropsPlugin::applyChanges() { // qDebug(); KIO::StatJob *job = KIO::mostLocalUrl(properties->url()); KJobWidgets::setWindow(job, properties); job->exec(); const QUrl url = job->mostLocalUrl(); if (!url.isLocalFile()) { KMessageBox::sorry(nullptr, i18n("Could not save properties. Only entries on local file systems are supported.")); return; } const QString path(url.toLocalFile()); // make sure the directory exists QDir().mkpath(QFileInfo(path).absolutePath()); QFile f(path); if (!f.open(QIODevice::ReadWrite)) { KMessageBox::sorry(nullptr, i18n("Could not save properties. You do not have " "sufficient access to write to %1.", path)); return; } f.close(); // If the command is changed we reset certain settings that are strongly // coupled to the command. checkCommandChanged(); KDesktopFile origConfig(d->m_origDesktopFile); QScopedPointer _config(origConfig.copyTo(path)); KConfigGroup config = _config->desktopGroup(); config.writeEntry("Type", QStringLiteral("Application")); config.writeEntry("Comment", d->w->commentEdit->text()); config.writeEntry("Comment", d->w->commentEdit->text(), KConfigGroup::Persistent | KConfigGroup::Localized); // for compat config.writeEntry("GenericName", d->w->genNameEdit->text()); config.writeEntry("GenericName", d->w->genNameEdit->text(), KConfigGroup::Persistent | KConfigGroup::Localized); // for compat config.writeEntry("Exec", d->w->commandEdit->text()); config.writeEntry("Path", d->w->pathEdit->lineEdit()->text()); // not writePathEntry, see kservice.cpp // Write mimeTypes QStringList mimeTypes; int count = d->w->filetypeList->topLevelItemCount(); for (int i = 0; i < count; ++i) { QTreeWidgetItem *item = d->w->filetypeList->topLevelItem(i); QString preference = item->text(2); mimeTypes.append(item->text(0)); if (!preference.isEmpty()) { mimeTypes.append(preference); } } // qDebug() << mimeTypes; config.writeXdgListEntry("MimeType", mimeTypes); if (!d->w->nameEdit->isHidden()) { QString nameStr = d->w->nameEdit->text(); config.writeEntry("Name", nameStr); config.writeEntry("Name", nameStr, KConfigGroup::Persistent | KConfigGroup::Localized); } config.writeEntry("Terminal", d->m_terminalBool); config.writeEntry("TerminalOptions", d->m_terminalOptionStr); config.writeEntry("X-KDE-SubstituteUID", d->m_suidBool); config.writeEntry("X-KDE-Username", d->m_suidUserStr); if (d->m_hasDiscreteGpuBool) { config.writeEntry("X-KDE-RunOnDiscreteGpu", d->m_runOnDiscreteGpuBool); } config.writeEntry("StartupNotify", d->m_startupBool); config.writeEntry("X-DBUS-StartupType", d->m_dbusStartupType); config.writeEntry("X-DBUS-ServiceName", d->m_dbusServiceName); config.sync(); // KSycoca update needed? bool updateNeeded = !relativeAppsLocation(path).isEmpty(); if (updateNeeded) { KBuildSycocaProgressDialog::rebuildKSycoca(d->m_frame); } } void KDesktopPropsPlugin::slotBrowseExec() { QUrl f = QFileDialog::getOpenFileUrl(d->m_frame); if (f.isEmpty()) { return; } if (!f.isLocalFile()) { KMessageBox::sorry(d->m_frame, i18n("Only executables on local file systems are supported.")); return; } QString path = f.toLocalFile(); path = KShell::quoteArg(path); d->w->commandEdit->setText(path); } void KDesktopPropsPlugin::slotAdvanced() { QDialog dlg(d->m_frame); dlg.setObjectName(QStringLiteral("KPropertiesDesktopAdv")); dlg.setModal(true); dlg.setWindowTitle(i18n("Advanced Options for %1", properties->url().fileName())); Ui_KPropertiesDesktopAdvBase w; QWidget *mainWidget = new QWidget(&dlg); w.setupUi(mainWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(&dlg); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(mainWidget); layout->addWidget(buttonBox); dlg.setLayout(layout); // If the command is changed we reset certain settings that are strongly // coupled to the command. checkCommandChanged(); // check to see if we use konsole if not do not add the nocloseonexit // because we don't know how to do this on other terminal applications KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); bool terminalCloseBool = false; if (preferredTerminal == QLatin1String("konsole")) { terminalCloseBool = d->m_terminalOptionStr.contains(QStringLiteral("--noclose")); w.terminalCloseCheck->setChecked(terminalCloseBool); d->m_terminalOptionStr.remove(QStringLiteral("--noclose")); } else { w.terminalCloseCheck->hide(); } w.terminalCheck->setChecked(d->m_terminalBool); w.terminalEdit->setText(d->m_terminalOptionStr); w.terminalCloseCheck->setEnabled(d->m_terminalBool); w.terminalEdit->setEnabled(d->m_terminalBool); w.terminalEditLabel->setEnabled(d->m_terminalBool); w.suidCheck->setChecked(d->m_suidBool); w.suidEdit->setText(d->m_suidUserStr); w.suidEdit->setEnabled(d->m_suidBool); w.suidEditLabel->setEnabled(d->m_suidBool); if (d->m_hasDiscreteGpuBool) { w.discreteGpuCheck->setChecked(d->m_runOnDiscreteGpuBool); } else { w.discreteGpuGroupBox->hide(); } w.startupInfoCheck->setChecked(d->m_startupBool); if (d->m_dbusStartupType == QLatin1String("unique")) { w.dbusCombo->setCurrentIndex(2); } else if (d->m_dbusStartupType == QLatin1String("multi")) { w.dbusCombo->setCurrentIndex(1); } else if (d->m_dbusStartupType == QLatin1String("wait")) { w.dbusCombo->setCurrentIndex(3); } else { w.dbusCombo->setCurrentIndex(0); } // Provide username completion up to 1000 users. const int maxEntries = 1000; QStringList userNames = KUser::allUserNames(maxEntries); if (userNames.size() < maxEntries) { KCompletion *kcom = new KCompletion; kcom->setOrder(KCompletion::Sorted); w.suidEdit->setCompletionObject(kcom, true); w.suidEdit->setAutoDeleteCompletionObject(true); w.suidEdit->setCompletionMode(KCompletion::CompletionAuto); kcom->setItems(userNames); } connect(w.terminalEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(w.terminalCloseCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.terminalCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.suidCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.suidEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed); connect(w.discreteGpuCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.startupInfoCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed); connect(w.dbusCombo, QOverload::of(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed); if (dlg.exec() == QDialog::Accepted) { d->m_terminalOptionStr = w.terminalEdit->text().trimmed(); d->m_terminalBool = w.terminalCheck->isChecked(); d->m_suidBool = w.suidCheck->isChecked(); d->m_suidUserStr = w.suidEdit->text().trimmed(); if (d->m_hasDiscreteGpuBool) { d->m_runOnDiscreteGpuBool = w.discreteGpuCheck->isChecked(); } d->m_startupBool = w.startupInfoCheck->isChecked(); if (w.terminalCloseCheck->isChecked()) { d->m_terminalOptionStr.append(QLatin1String(" --noclose")); } switch (w.dbusCombo->currentIndex()) { case 1: d->m_dbusStartupType = QStringLiteral("multi"); break; case 2: d->m_dbusStartupType = QStringLiteral("unique"); break; case 3: d->m_dbusStartupType = QStringLiteral("wait"); break; default: d->m_dbusStartupType = QStringLiteral("none"); break; } } } bool KDesktopPropsPlugin::supports(const KFileItemList &_items) { if (_items.count() != 1) { return false; } const KFileItem item = _items.first(); // check if desktop file if (!item.isDesktopFile()) { return false; } // open file and check type bool isLocal; QUrl url = item.mostLocalUrl(isLocal); if (!isLocal) { return false; } KDesktopFile config(url.toLocalFile()); return config.hasApplicationType() && KAuthorized::authorize(QStringLiteral("run_desktop_files")) && KAuthorized::authorize(QStringLiteral("shell_access")); } #include "moc_kpropertiesdialog.cpp" #include "moc_kpropertiesdialog_p.cpp" diff --git a/src/widgets/krun.cpp b/src/widgets/krun.cpp index 4c7d2766..490ed81a 100644 --- a/src/widgets/krun.cpp +++ b/src/widgets/krun.cpp @@ -1,1682 +1,1678 @@ /* 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 "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 #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"); } // --------------------------------------------------------------------------- bool KRun::isExecutableFile(const QUrl &url, const QString &mimetype) { if (!url.isLocalFile()) { return false; } QFileInfo file(url.toLocalFile()); if (file.isExecutable()) { // Got a prospective file to run 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 true; } } return false; } 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; } } #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 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 (isExecutableFile(u, _mimetype)) { if (u.isLocalFile() && runExecutables) { if (KAuthorized::authorize(QStringLiteral("shell_access"))) { 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 { noAuth = true; } } else if (_mimetype == QLatin1String("application/x-executable")) { noRun = true; } } else if (isExecutable(_mimetype)) { if (!runExecutables) { noRun = true; } if (!KAuthorized::authorize(QStringLiteral("shell_access"))) { noAuth = 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() == QStringLiteral("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); } qint64 pid = KProcessRunner::run(proc, executable, id); 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 KProcessRunner::run(proc, bin, KStartupInfoId()); } // 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(QStringLiteral("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; } // 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 { // 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); updateGeometry(); } private: QPlainTextEdit *m_textEdit; }; // 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 makeFileExecutable(const QString &fileName) { // 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)) { qCWarning(KIO_WIDGETS) << "Error opening service" << fileName << desktopFile.errorString(); return false; } QByteArray header = desktopFile.peek(2); // First two chars of file if (header.size() == 0) { qCWarning(KIO_WIDGETS) << "Error inspecting service" << fileName << desktopFile.errorString(); return false; // Some kind of error } if (header != "#!") { // Add header QSaveFile saveFile; saveFile.setFileName(fileName); if (!saveFile.open(QIODevice::WriteOnly)) { qCWarning(KIO_WIDGETS) << "Unable to open replacement file for" << fileName << saveFile.errorString(); return false; } QByteArray shebang("#!/usr/bin/env xdg-open\n"); if (saveFile.write(shebang) != shebang.size()) { qCWarning(KIO_WIDGETS) << "Error occurred adding header for" << fileName << saveFile.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()) { qCWarning(KIO_WIDGETS) << "Unable to read service" << fileName << desktopFile.errorString(); saveFile.cancelWriting(); return false; } if (saveFile.write(desktopData) != desktopData.size()) { qCWarning(KIO_WIDGETS) << "Error copying service" << fileName << saveFile.errorString(); saveFile.cancelWriting(); return false; } desktopFile.close(); if (!saveFile.commit()) { // Figures.... qCWarning(KIO_WIDGETS) << "Error committing changes to service" << fileName << saveFile.errorString(); return false; } if (!desktopFile.open(QFile::ReadOnly)) { qCWarning(KIO_WIDGETS) << "Error re-opening service" << fileName << desktopFile.errorString(); return false; } } // Add header // 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 (!desktopFile.setPermissions(QFile::ExeUser | desktopFile.permissions())) { qCWarning(KIO_WIDGETS) << "Unable to change permissions for" << fileName << desktopFile.errorString(); return false; } // whew return true; } // 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 } SecureMessageDialog *baseDialog = new SecureMessageDialog(window); baseDialog->setWindowTitle(i18nc("Warning about executing unknown .desktop file", "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); // 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(); } QPlainTextEdit *textEdit = new QPlainTextEdit(baseWidget); textEdit->setPlainText(program); 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); int result = baseDialog->exec(); 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. if (!::makeFileExecutable(service.entryPath())) { QString serviceName = service.name(); if (serviceName.isEmpty()) { serviceName = service.genericName(); } KMessageBox::sorry( window, i18n("Unable to make the service %1 executable, aborting execution", serviceName) ); 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 &serviceType) { return (serviceType == QLatin1String("application/x-desktop") || serviceType == QLatin1String("application/x-executable") || /* See https://bugs.freedesktop.org/show_bug.cgi?id=97226 */ serviceType == QLatin1String("application/x-sharedlib") || serviceType == QLatin1String("application/x-ms-dos-executable") || serviceType == 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(); } /****************/ qint64 KProcessRunner::run(KProcess *p, const QString &executable, const KStartupInfoId &id) { return (new KProcessRunner(p, executable, id))->pid(); } 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?). slotProcessExited(255, process->exitStatus()); } 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 &error = i18n("Could not find the program '%1'", m_executable); if (qApp) { QTimer::singleShot(0, qApp, [=]() { QEventLoopLocker locker; KMessageBox::sorry(nullptr, error); }); } else { qWarning() << error; } } else { //qDebug() << process->readAllStandardError(); } } deleteLater(); } #include "moc_krun.cpp" #include "moc_krun_p.cpp" #include "krun.moc" diff --git a/src/widgets/kshellcompletion.cpp b/src/widgets/kshellcompletion.cpp index d82ba1c5..def505d4 100644 --- a/src/widgets/kshellcompletion.cpp +++ b/src/widgets/kshellcompletion.cpp @@ -1,320 +1,318 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Smith This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kshellcompletion.h" #include #include -#include -#include #include #include class KShellCompletionPrivate { public: KShellCompletionPrivate() : m_word_break_char(QLatin1Char(' ')) , m_quote_char1(QLatin1Char('\"')) , m_quote_char2(QLatin1Char('\'')) , m_escape_char(QLatin1Char('\\')) { } void splitText(const QString &text, QString &text_start, QString &text_compl) const; bool quoteText(QString *text, bool force, bool skip_last) const; QString unquote(const QString &text) const; QString m_text_start; // part of the text that was not completed QString m_text_compl; // part of the text that was completed (unchanged) QChar m_word_break_char; QChar m_quote_char1; QChar m_quote_char2; QChar m_escape_char; }; KShellCompletion::KShellCompletion() : KUrlCompletion(), d(new KShellCompletionPrivate) { } KShellCompletion::~KShellCompletion() { delete d; } /* * makeCompletion() * * Entry point for file name completion */ QString KShellCompletion::makeCompletion(const QString &text) { // Split text at the last unquoted space // d->splitText(text, d->m_text_start, d->m_text_compl); // Remove quotes from the text to be completed // QString tmp = d->unquote(d->m_text_compl); d->m_text_compl = tmp; // Do exe-completion if there was no unquoted space // bool is_exe_completion = true; for (int i = 0; i < d->m_text_start.length(); i++) { if (d->m_text_start[i] != d->m_word_break_char) { is_exe_completion = false; break; } } Mode mode = (is_exe_completion ? ExeCompletion : FileCompletion); setMode(mode); // Make completion on the last part of text // return KUrlCompletion::makeCompletion(d->m_text_compl); } /* * postProcessMatch, postProcessMatches * * Called by KCompletion before emitting match() and matches() * * Add add the part of the text that was not completed * Add quotes when needed */ void KShellCompletion::postProcessMatch(QString *match) const { KUrlCompletion::postProcessMatch(match); if (match->isNull()) { return; } if (match->endsWith(QLatin1Char('/'))) { d->quoteText(match, false, true); // don't quote the trailing '/' } else { d->quoteText(match, false, false); // quote the whole text } match->prepend(d->m_text_start); } void KShellCompletion::postProcessMatches(QStringList *matches) const { KUrlCompletion::postProcessMatches(matches); for (QStringList::Iterator it = matches->begin(); it != matches->end(); ++it) { if (!(*it).isNull()) { if ((*it).endsWith(QLatin1Char('/'))) { d->quoteText(&(*it), false, true); // don't quote trailing '/' } else { d->quoteText(&(*it), false, false); // quote the whole text } (*it).prepend(d->m_text_start); } } } void KShellCompletion::postProcessMatches(KCompletionMatches *matches) const { KUrlCompletion::postProcessMatches(matches); for (KCompletionMatches::Iterator it = matches->begin(); it != matches->end(); ++it) { if (!(*it).value().isNull()) { if ((*it).value().endsWith(QLatin1Char('/'))) { d->quoteText(&(*it).value(), false, true); // don't quote trailing '/' } else { d->quoteText(&(*it).value(), false, false); // quote the whole text } (*it).value().prepend(d->m_text_start); } } } /* * splitText * * Split text at the last unquoted space * * text_start = [out] text at the left, including the space * text_compl = [out] text at the right */ void KShellCompletionPrivate::splitText(const QString &text, QString &text_start, QString &text_compl) const { bool in_quote = false; bool escaped = false; QChar p_last_quote_char; int last_unquoted_space = -1; int end_space_len = 0; for (int pos = 0; pos < text.length(); pos++) { end_space_len = 0; if (escaped) { escaped = false; } else if (in_quote && text[pos] == p_last_quote_char) { in_quote = false; } else if (!in_quote && text[pos] == m_quote_char1) { p_last_quote_char = m_quote_char1; in_quote = true; } else if (!in_quote && text[pos] == m_quote_char2) { p_last_quote_char = m_quote_char2; in_quote = true; } else if (text[pos] == m_escape_char) { escaped = true; } else if (!in_quote && text[pos] == m_word_break_char) { end_space_len = 1; while (pos + 1 < text.length() && text[pos + 1] == m_word_break_char) { end_space_len++; pos++; } if (pos + 1 == text.length()) { break; } last_unquoted_space = pos; } } text_start = text.left(last_unquoted_space + 1); // the last part without trailing blanks text_compl = text.mid(last_unquoted_space + 1); } /* * quoteText() * * Add quotations to 'text' if needed or if 'force' = true * Returns true if quotes were added * * skip_last => ignore the last character (we add a space or '/' to all filenames) */ bool KShellCompletionPrivate::quoteText(QString *text, bool force, bool skip_last) const { int pos = 0; if (!force) { pos = text->indexOf(m_word_break_char); if (skip_last && (pos == (int)(text->length()) - 1)) { pos = -1; } } if (!force && pos == -1) { pos = text->indexOf(m_quote_char1); if (skip_last && (pos == (int)(text->length()) - 1)) { pos = -1; } } if (!force && pos == -1) { pos = text->indexOf(m_quote_char2); if (skip_last && (pos == (int)(text->length()) - 1)) { pos = -1; } } if (!force && pos == -1) { pos = text->indexOf(m_escape_char); if (skip_last && (pos == (int)(text->length()) - 1)) { pos = -1; } } if (force || (pos >= 0)) { // Escape \ in the string text->replace(m_escape_char, QString(m_escape_char) + m_escape_char); // Escape " in the string text->replace(m_quote_char1, QString(m_escape_char) + m_quote_char1); // " at the beginning text->insert(0, m_quote_char1); // " at the end if (skip_last) { text->insert(text->length() - 1, m_quote_char1); } else { text->insert(text->length(), m_quote_char1); } return true; } return false; } /* * unquote * * Remove quotes and return the result in a new string * */ QString KShellCompletionPrivate::unquote(const QString &text) const { bool in_quote = false; bool escaped = false; QChar p_last_quote_char; QString result; for (int pos = 0; pos < text.length(); pos++) { if (escaped) { escaped = false; result.insert(result.length(), text[pos]); } else if (in_quote && text[pos] == p_last_quote_char) { in_quote = false; } else if (!in_quote && text[pos] == m_quote_char1) { p_last_quote_char = m_quote_char1; in_quote = true; } else if (!in_quote && text[pos] == m_quote_char2) { p_last_quote_char = m_quote_char2; in_quote = true; } else if (text[pos] == m_escape_char) { escaped = true; result.insert(result.length(), text[pos]); } else { result.insert(result.length(), text[pos]); } } return result; } diff --git a/src/widgets/ksslinfodialog.cpp b/src/widgets/ksslinfodialog.cpp index 64385c89..ae1b35fb 100644 --- a/src/widgets/ksslinfodialog.cpp +++ b/src/widgets/ksslinfodialog.cpp @@ -1,242 +1,239 @@ /* This file is part of the KDE project * * Copyright (C) 2000,2001 George Staikos * Copyright (C) 2000 Malte Starostik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "ksslinfodialog.h" #include "ui_sslinfo.h" #include "ksslcertificatebox.h" -#include #include -#include -#include #include #include #include // BarIcon #include "ktcpsocket.h" class Q_DECL_HIDDEN KSslInfoDialog::KSslInfoDialogPrivate { public: QList certificateChain; QList > certificateErrors; bool isMainPartEncrypted; bool auxPartsEncrypted; Ui::SslInfo ui; KSslCertificateBox *subject; KSslCertificateBox *issuer; }; KSslInfoDialog::KSslInfoDialog(QWidget *parent) : QDialog(parent), d(new KSslInfoDialogPrivate) { setWindowTitle(i18n("KDE SSL Information")); setAttribute(Qt::WA_DeleteOnClose); QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); QWidget *mainWidget = new QWidget(this); d->ui.setupUi(mainWidget); layout->addWidget(mainWidget); d->subject = new KSslCertificateBox(d->ui.certParties); d->issuer = new KSslCertificateBox(d->ui.certParties); d->ui.certParties->addTab(d->subject, i18nc("The receiver of the SSL certificate", "Subject")); d->ui.certParties->addTab(d->issuer, i18nc("The authority that issued the SSL certificate", "Issuer")); d->isMainPartEncrypted = true; d->auxPartsEncrypted = true; updateWhichPartsEncrypted(); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->setStandardButtons(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(buttonBox); #if 0 if (QSslSocket::supportsSsl()) { if (d->m_secCon) { d->pixmap->setPixmap(BarIcon("security-high")); d->info->setText(i18n("Current connection is secured with SSL.")); } else { d->pixmap->setPixmap(BarIcon("security-low")); d->info->setText(i18n("Current connection is not secured with SSL.")); } } else { d->pixmap->setPixmap(BarIcon("security-low")); d->info->setText(i18n("SSL support is not available in this build of KDE.")); } #endif } KSslInfoDialog::~KSslInfoDialog() { delete d; } void KSslInfoDialog::setMainPartEncrypted(bool mainEncrypted) { d->isMainPartEncrypted = mainEncrypted; updateWhichPartsEncrypted(); } void KSslInfoDialog::setAuxiliaryPartsEncrypted(bool auxEncrypted) { d->auxPartsEncrypted = auxEncrypted; updateWhichPartsEncrypted(); } void KSslInfoDialog::updateWhichPartsEncrypted() { if (d->isMainPartEncrypted) { if (d->auxPartsEncrypted) { d->ui.encryptionIndicator->setPixmap(BarIcon(QStringLiteral("security-high"))); d->ui.explanation->setText(i18n("Current connection is secured with SSL.")); } else { d->ui.encryptionIndicator->setPixmap(BarIcon(QStringLiteral("security-medium"))); d->ui.explanation->setText(i18n("The main part of this document is secured " "with SSL, but some parts are not.")); } } else { if (d->auxPartsEncrypted) { d->ui.encryptionIndicator->setPixmap(BarIcon(QStringLiteral("security-medium"))); d->ui.explanation->setText(i18n("Some of this document is secured with SSL, " "but the main part is not.")); } else { d->ui.encryptionIndicator->setPixmap(BarIcon(QStringLiteral("security-low"))); d->ui.explanation->setText(i18n("Current connection is not secured with SSL.")); } } } void KSslInfoDialog::setSslInfo(const QList &certificateChain, const QString &ip, const QString &host, const QString &sslProtocol, const QString &cipher, int usedBits, int bits, const QList > &validationErrors) { d->certificateChain = certificateChain; d->certificateErrors = validationErrors; d->ui.certSelector->clear(); for (int i = 0; i < certificateChain.size(); i++) { const QSslCertificate &cert = certificateChain[i]; QString name; static const QSslCertificate::SubjectInfo si[] = { QSslCertificate::CommonName, QSslCertificate::Organization, QSslCertificate::OrganizationalUnitName }; for (int j = 0; j < 3 && name.isEmpty(); j++) { name = cert.subjectInfo(si[j]).join(QStringLiteral(", ")); } d->ui.certSelector->addItem(name); } if (certificateChain.size() < 2) { d->ui.certSelector->setEnabled(false); } connect(d->ui.certSelector, QOverload::of(&QComboBox::currentIndexChanged), this, &KSslInfoDialog::displayFromChain); if (d->certificateChain.isEmpty()) { d->certificateChain.append(QSslCertificate()); } displayFromChain(0); d->ui.ip->setText(ip); d->ui.address->setText(host); d->ui.sslVersion->setText(sslProtocol); const QStringList cipherInfo = cipher.split(QLatin1Char('\n'), QString::SkipEmptyParts); if (cipherInfo.size() >= 4) { d->ui.encryption->setText(i18nc("%1, using %2 bits of a %3 bit key", "%1, %2 %3", cipherInfo[0], i18ncp("Part of: %1, using %2 bits of a %3 bit key", "using %1 bit", "using %1 bits", usedBits), i18ncp("Part of: %1, using %2 bits of a %3 bit key", "of a %1 bit key", "of a %1 bit key", bits))); d->ui.details->setText(QStringLiteral("Auth = %1, Kx = %2, MAC = %3") .arg(cipherInfo[1], cipherInfo[2], cipherInfo[3])); } else { d->ui.encryption->setText(QString()); d->ui.details->setText(QString()); } } void KSslInfoDialog::displayFromChain(int i) { const QSslCertificate &cert = d->certificateChain[i]; QString trusted; if (!d->certificateErrors[i].isEmpty()) { trusted = i18nc("The certificate is not trusted", "NO, there were errors:"); foreach (KSslError::Error e, d->certificateErrors[i]) { KSslError classError(e); trusted += QLatin1Char('\n') + classError.errorString(); } } else { trusted = i18nc("The certificate is trusted", "Yes"); } d->ui.trusted->setText(trusted); QString vp = i18nc("%1 is the effective date of the certificate, %2 is the expiry date", "%1 to %2", cert.effectiveDate().toString(), cert.expiryDate().toString()); d->ui.validityPeriod->setText(vp); d->ui.serial->setText(QString::fromUtf8(cert.serialNumber())); d->ui.digest->setText(QString::fromUtf8(cert.digest().toHex())); d->ui.sha1Digest->setText(QString::fromUtf8(cert.digest(QCryptographicHash::Sha1).toHex())); d->subject->setCertificate(cert, KSslCertificateBox::Subject); d->issuer->setCertificate(cert, KSslCertificateBox::Issuer); } //static QList > KSslInfoDialog::errorsFromString(const QString &es) { const QStringList sl = es.split(QLatin1Char('\n'), QString::KeepEmptyParts); QList > ret; ret.reserve(sl.size()); for (const QString &s : sl) { QList certErrors; const QStringList sl2 = s.split(QLatin1Char('\t'), QString::SkipEmptyParts); for (const QString &s2 : sl2) { bool didConvert; KSslError::Error error = static_cast(s2.toInt(&didConvert)); if (didConvert) { certErrors.append(error); } } ret.append(certErrors); } return ret; } diff --git a/src/widgets/kurifilter.cpp b/src/widgets/kurifilter.cpp index e580c6e2..f91a62e0 100644 --- a/src/widgets/kurifilter.cpp +++ b/src/widgets/kurifilter.cpp @@ -1,696 +1,695 @@ /* This file is part of the KDE libraries * Copyright (C) 2000 Yves Arrouye * Copyright (C) 2000,2010 Dawit Alemayehu * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. **/ #include "kurifilter.h" #include "hostinfo.h" #include #include #include #include #include -#include #include #include typedef QList KUriFilterPluginList; typedef QMap SearchProviderMap; static QString lookupIconNameFor(const QUrl &url, KUriFilterData::UriTypes type) { QString iconName; switch (type) { case KUriFilterData::NetProtocol: iconName = KIO::iconNameForUrl(url); break; case KUriFilterData::Executable: { QString exeName = url.path(); exeName.remove(0, exeName.lastIndexOf(QLatin1Char('/')) + 1); // strip path if given KService::Ptr service = KService::serviceByDesktopName(exeName); if (service && service->icon() != QLatin1String("unknown")) { iconName = service->icon(); } // Try to find an icon with the same name as the binary (useful for non-kde apps) // Use iconPath rather than loadIcon() as the latter uses QPixmap (not threadsafe) else if (!KIconLoader::global()->iconPath(exeName, KIconLoader::NoGroup, true).isNull()) { iconName = exeName; } else // not found, use default { iconName = QStringLiteral("system-run"); } break; } case KUriFilterData::Help: { iconName = QStringLiteral("khelpcenter"); break; } case KUriFilterData::Shell: { iconName = QStringLiteral("konsole"); break; } case KUriFilterData::Error: case KUriFilterData::Blocked: { iconName = QStringLiteral("error"); break; } default: break; } return iconName; } class Q_DECL_HIDDEN KUriFilterSearchProvider::KUriFilterSearchProviderPrivate { public: KUriFilterSearchProviderPrivate() {} KUriFilterSearchProviderPrivate(const KUriFilterSearchProviderPrivate &other) : desktopEntryName(other.desktopEntryName), iconName(other.iconName), name(other.name), keys(other.keys) {} QString desktopEntryName; QString iconName; QString name; QStringList keys; }; KUriFilterSearchProvider::KUriFilterSearchProvider() : d(new KUriFilterSearchProvider::KUriFilterSearchProviderPrivate) { } KUriFilterSearchProvider::KUriFilterSearchProvider(const KUriFilterSearchProvider &other) : d(new KUriFilterSearchProvider::KUriFilterSearchProviderPrivate(*(other.d))) { } KUriFilterSearchProvider::~KUriFilterSearchProvider() { delete d; } QString KUriFilterSearchProvider::desktopEntryName() const { return d->desktopEntryName; } QString KUriFilterSearchProvider::iconName() const { return d->iconName; } QString KUriFilterSearchProvider::name() const { return d->name; } QStringList KUriFilterSearchProvider::keys() const { return d->keys; } QString KUriFilterSearchProvider::defaultKey() const { if (d->keys.isEmpty()) { return QString(); } return d->keys.first(); } KUriFilterSearchProvider &KUriFilterSearchProvider::operator=(const KUriFilterSearchProvider &other) { d->desktopEntryName = other.d->desktopEntryName; d->iconName = other.d->iconName; d->keys = other.d->keys; d->name = other.d->name; return *this; } void KUriFilterSearchProvider::setDesktopEntryName(const QString &desktopEntryName) { d->desktopEntryName = desktopEntryName; } void KUriFilterSearchProvider::setIconName(const QString &iconName) { d->iconName = iconName; } void KUriFilterSearchProvider::setName(const QString &name) { d->name = name; } void KUriFilterSearchProvider::setKeys(const QStringList &keys) { d->keys = keys; } class KUriFilterDataPrivate { public: explicit KUriFilterDataPrivate(const QUrl &u, const QString &typedUrl) : checkForExecs(true), wasModified(true), uriType(KUriFilterData::Unknown), searchFilterOptions(KUriFilterData::SearchFilterOptionNone), url(u.adjusted(QUrl::NormalizePathSegments)), typedString(typedUrl) { } ~KUriFilterDataPrivate() { } void setData(const QUrl &u, const QString &typedUrl) { checkForExecs = true; wasModified = true; uriType = KUriFilterData::Unknown; searchFilterOptions = KUriFilterData::SearchFilterOptionNone; url = u.adjusted(QUrl::NormalizePathSegments); typedString = typedUrl; errMsg.clear(); iconName.clear(); absPath.clear(); args.clear(); searchTerm.clear(); searchProvider.clear(); searchTermSeparator = QChar(); alternateDefaultSearchProvider.clear(); alternateSearchProviders.clear(); searchProviderMap.clear(); defaultUrlScheme.clear(); } KUriFilterDataPrivate(KUriFilterDataPrivate *data) { wasModified = data->wasModified; checkForExecs = data->checkForExecs; uriType = data->uriType; searchFilterOptions = data->searchFilterOptions; url = data->url; typedString = data->typedString; errMsg = data->errMsg; iconName = data->iconName; absPath = data->absPath; args = data->args; searchTerm = data->searchTerm; searchTermSeparator = data->searchTermSeparator; searchProvider = data->searchProvider; alternateDefaultSearchProvider = data->alternateDefaultSearchProvider; alternateSearchProviders = data->alternateSearchProviders; searchProviderMap = data->searchProviderMap; defaultUrlScheme = data->defaultUrlScheme; } bool checkForExecs; bool wasModified; KUriFilterData::UriTypes uriType; KUriFilterData::SearchFilterOptions searchFilterOptions; QUrl url; QString typedString; QString errMsg; QString iconName; QString absPath; QString args; QString searchTerm; QString searchProvider; QString alternateDefaultSearchProvider; QString defaultUrlScheme; QChar searchTermSeparator; QStringList alternateSearchProviders; QStringList searchProviderList; SearchProviderMap searchProviderMap; }; KUriFilterData::KUriFilterData() : d(new KUriFilterDataPrivate(QUrl(), QString())) { } KUriFilterData::KUriFilterData(const QUrl &url) : d(new KUriFilterDataPrivate(url, url.toString())) { } KUriFilterData::KUriFilterData(const QString &url) : d(new KUriFilterDataPrivate(QUrl::fromUserInput(url), url)) { } KUriFilterData::KUriFilterData(const KUriFilterData &other) : d(new KUriFilterDataPrivate(other.d)) { } KUriFilterData::~KUriFilterData() { delete d; } QUrl KUriFilterData::uri() const { return d->url; } QString KUriFilterData::errorMsg() const { return d->errMsg; } KUriFilterData::UriTypes KUriFilterData::uriType() const { return d->uriType; } QString KUriFilterData::absolutePath() const { return d->absPath; } bool KUriFilterData::hasAbsolutePath() const { return !d->absPath.isEmpty(); } QString KUriFilterData::argsAndOptions() const { return d->args; } bool KUriFilterData::hasArgsAndOptions() const { return !d->args.isEmpty(); } bool KUriFilterData::checkForExecutables() const { return d->checkForExecs; } QString KUriFilterData::typedString() const { return d->typedString; } QString KUriFilterData::searchTerm() const { return d->searchTerm; } QChar KUriFilterData::searchTermSeparator() const { return d->searchTermSeparator; } QString KUriFilterData::searchProvider() const { return d->searchProvider; } QStringList KUriFilterData::preferredSearchProviders() const { return d->searchProviderList; } KUriFilterSearchProvider KUriFilterData::queryForSearchProvider(const QString &provider) const { const KUriFilterSearchProvider *searchProvider = d->searchProviderMap.value(provider); if (searchProvider) { return *(searchProvider); } return KUriFilterSearchProvider(); } QString KUriFilterData::queryForPreferredSearchProvider(const QString &provider) const { const KUriFilterSearchProvider *searchProvider = d->searchProviderMap.value(provider); if (searchProvider) { return (searchProvider->defaultKey() % searchTermSeparator() % searchTerm()); } return QString(); } QStringList KUriFilterData::allQueriesForSearchProvider(const QString &provider) const { const KUriFilterSearchProvider *searchProvider = d->searchProviderMap.value(provider); if (searchProvider) { return searchProvider->keys(); } return QStringList(); } QString KUriFilterData::iconNameForPreferredSearchProvider(const QString &provider) const { const KUriFilterSearchProvider *searchProvider = d->searchProviderMap.value(provider); if (searchProvider) { return searchProvider->iconName(); } return QString(); } QStringList KUriFilterData::alternateSearchProviders() const { return d->alternateSearchProviders; } QString KUriFilterData::alternateDefaultSearchProvider() const { return d->alternateDefaultSearchProvider; } QString KUriFilterData::defaultUrlScheme() const { return d->defaultUrlScheme; } KUriFilterData::SearchFilterOptions KUriFilterData::searchFilteringOptions() const { return d->searchFilterOptions; } QString KUriFilterData::iconName() { if (d->wasModified) { d->iconName = lookupIconNameFor(d->url, d->uriType); d->wasModified = false; } return d->iconName; } void KUriFilterData::setData(const QUrl &url) { d->setData(url, url.toString()); } void KUriFilterData::setData(const QString &url) { d->setData(QUrl(url), url); } bool KUriFilterData::setAbsolutePath(const QString &absPath) { // Since a malformed URL could possibly be a relative // URL we tag it as a possible local resource... if ((d->url.scheme().isEmpty() || d->url.isLocalFile())) { d->absPath = absPath; return true; } return false; } void KUriFilterData::setCheckForExecutables(bool check) { d->checkForExecs = check; } void KUriFilterData::setAlternateSearchProviders(const QStringList &providers) { d->alternateSearchProviders = providers; } void KUriFilterData::setAlternateDefaultSearchProvider(const QString &provider) { d->alternateDefaultSearchProvider = provider; } void KUriFilterData::setDefaultUrlScheme(const QString &scheme) { d->defaultUrlScheme = scheme; } void KUriFilterData::setSearchFilteringOptions(SearchFilterOptions options) { d->searchFilterOptions = options; } KUriFilterData &KUriFilterData::operator=(const QUrl &url) { d->setData(url, url.toString()); return *this; } KUriFilterData &KUriFilterData::operator=(const QString &url) { d->setData(QUrl(url), url); return *this; } /************************* KUriFilterPlugin ******************************/ KUriFilterPlugin::KUriFilterPlugin(const QString &name, QObject *parent) : QObject(parent), d(nullptr) { setObjectName(name); } // KF6 TODO //KUriFilterPlugin::~KUriFilterPlugin() //{ //} KCModule *KUriFilterPlugin::configModule(QWidget *, const char *) const { return nullptr; } QString KUriFilterPlugin::configName() const { return objectName(); } void KUriFilterPlugin::setFilteredUri(KUriFilterData &data, const QUrl &uri) const { data.d->url = uri.adjusted(QUrl::NormalizePathSegments); data.d->wasModified = true; //qDebug() << "Got filtered to:" << uri; } void KUriFilterPlugin::setErrorMsg(KUriFilterData &data, const QString &errmsg) const { data.d->errMsg = errmsg; } void KUriFilterPlugin::setUriType(KUriFilterData &data, KUriFilterData::UriTypes type) const { data.d->uriType = type; data.d->wasModified = true; } void KUriFilterPlugin::setArguments(KUriFilterData &data, const QString &args) const { data.d->args = args; } void KUriFilterPlugin::setSearchProvider(KUriFilterData &data, const QString &provider, const QString &term, const QChar &separator) const { data.d->searchProvider = provider; data.d->searchTerm = term; data.d->searchTermSeparator = separator; } void KUriFilterPlugin::setSearchProviders(KUriFilterData &data, const QList &providers) const { data.d->searchProviderList.reserve(data.d->searchProviderList.size() + providers.size()); for (KUriFilterSearchProvider *searchProvider : providers) { data.d->searchProviderList << searchProvider->name(); data.d->searchProviderMap.insert(searchProvider->name(), searchProvider); } } QString KUriFilterPlugin::iconNameFor(const QUrl &url, KUriFilterData::UriTypes type) const { return lookupIconNameFor(url, type); } QHostInfo KUriFilterPlugin::resolveName(const QString &hostname, unsigned long timeout) const { return KIO::HostInfo::lookupHost(hostname, timeout); } /******************************* KUriFilter ******************************/ class KUriFilterPrivate { public: KUriFilterPrivate() {} ~KUriFilterPrivate() { qDeleteAll(pluginList); pluginList.clear(); } QVector pluginList; }; class KUriFilterSingleton { public: KUriFilter instance; }; Q_GLOBAL_STATIC(KUriFilterSingleton, m_self) KUriFilter *KUriFilter::self() { return &m_self()->instance; } KUriFilter::KUriFilter() : d(new KUriFilterPrivate()) { loadPlugins(); } KUriFilter::~KUriFilter() { delete d; } bool KUriFilter::filterUri(KUriFilterData &data, const QStringList &filters) { bool filtered = false; for (KUriFilterPlugin *plugin : qAsConst(d->pluginList)) { // If no specific filters were requested, iterate through all the plugins. // Otherwise, only use available filters. if (filters.isEmpty() || filters.contains(plugin->objectName())) { if (plugin->filterUri(data)) { filtered = true; } } } return filtered; } bool KUriFilter::filterUri(QUrl &uri, const QStringList &filters) { KUriFilterData data(uri); bool filtered = filterUri(data, filters); if (filtered) { uri = data.uri(); } return filtered; } bool KUriFilter::filterUri(QString &uri, const QStringList &filters) { KUriFilterData data(uri); bool filtered = filterUri(data, filters); if (filtered) { uri = data.uri().toString(); } return filtered; } QUrl KUriFilter::filteredUri(const QUrl &uri, const QStringList &filters) { KUriFilterData data(uri); filterUri(data, filters); return data.uri(); } QString KUriFilter::filteredUri(const QString &uri, const QStringList &filters) { KUriFilterData data(uri); filterUri(data, filters); return data.uri().toString(); } #ifndef KIOWIDGETS_NO_DEPRECATED bool KUriFilter::filterSearchUri(KUriFilterData &data) { return filterSearchUri(data, (NormalTextFilter | WebShortcutFilter)); } #endif bool KUriFilter::filterSearchUri(KUriFilterData &data, SearchFilterTypes types) { QStringList filters; if (types & WebShortcutFilter) { filters << QStringLiteral("kurisearchfilter"); } if (types & NormalTextFilter) { filters << QStringLiteral("kuriikwsfilter"); } return filterUri(data, filters); } QStringList KUriFilter::pluginNames() const { QStringList res; res.reserve(d->pluginList.size()); std::transform(d->pluginList.constBegin(), d->pluginList.constEnd(), std::back_inserter(res), [](const KUriFilterPlugin *plugin) { return plugin->objectName(); }); return res; } void KUriFilter::loadPlugins() { QVector plugins = KPluginLoader::findPlugins(QStringLiteral("kf5/urifilters")); const QString prefKey = QStringLiteral("X-KDE-InitialPreference"); // Sort the plugins by order of priority std::sort(plugins.begin(), plugins.end(), [prefKey](const KPluginMetaData &a, const KPluginMetaData &b) { return a.rawData().value(prefKey).toInt() > b.rawData().value(prefKey).toInt(); }); QStringList pluginNames; pluginNames.reserve(plugins.count()); for (const KPluginMetaData &pluginMetaData : plugins) { const QString fileName = pluginMetaData.fileName().section(QLatin1Char('/'), -1); if (!pluginNames.contains(fileName)) { pluginNames << fileName; KPluginFactory *factory = qobject_cast(pluginMetaData.instantiate()); if (factory) { KUriFilterPlugin *plugin = factory->create(nullptr); if (plugin) { d->pluginList << plugin; } } } } } diff --git a/src/widgets/kurifiltersearchprovideractions.cpp b/src/widgets/kurifiltersearchprovideractions.cpp index 21ed1d33..138f1292 100644 --- a/src/widgets/kurifiltersearchprovideractions.cpp +++ b/src/widgets/kurifiltersearchprovideractions.cpp @@ -1,125 +1,124 @@ /* Copyright (c) 2015 Montel Laurent 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 "kurifiltersearchprovideractions.h" #include #include #include #include #include #include #include -#include using namespace KIO; class KIO::WebShortcutsMenuManagerPrivate { public: WebShortcutsMenuManagerPrivate() { } QString mSelectedText; }; KUriFilterSearchProviderActions::KUriFilterSearchProviderActions(QObject *parent) : QObject(parent), d(new KIO::WebShortcutsMenuManagerPrivate) { } KUriFilterSearchProviderActions::~KUriFilterSearchProviderActions() { delete d; } QString KUriFilterSearchProviderActions::selectedText() const { return d->mSelectedText; } void KUriFilterSearchProviderActions::setSelectedText(const QString &selectedText) { d->mSelectedText = selectedText; } void KUriFilterSearchProviderActions::slotConfigureWebShortcuts() { KToolInvocation::kdeinitExec(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts")); } void KUriFilterSearchProviderActions::addWebShortcutsToMenu(QMenu *menu) { if (d->mSelectedText.isEmpty()) { return; } const QString searchText = d->mSelectedText.simplified(); if (searchText.isEmpty()) { return; } KUriFilterData filterData(searchText); filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly); if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) { const QStringList searchProviders = filterData.preferredSearchProviders(); if (!searchProviders.isEmpty()) { QMenu *webShortcutsMenu = new QMenu(menu); webShortcutsMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts"))); const QString squeezedText = KStringHandler::rsqueeze(searchText, 21); webShortcutsMenu->setTitle(i18n("Search for '%1' with", squeezedText)); QActionGroup *actionGroup = new QActionGroup(this); connect(actionGroup, &QActionGroup::triggered, this, &KUriFilterSearchProviderActions::slotHandleWebShortcutAction); Q_FOREACH (const QString &searchProvider, searchProviders) { QAction *action = new QAction(i18nc("@action:inmenu Search for with", "%1", searchProvider), webShortcutsMenu); action->setIcon(QIcon::fromTheme(filterData.iconNameForPreferredSearchProvider(searchProvider))); action->setData(filterData.queryForPreferredSearchProvider(searchProvider)); webShortcutsMenu->addAction(action); actionGroup->addAction(action); } if (!QStandardPaths::findExecutable(QStringLiteral("kcmshell5")).isEmpty()) { webShortcutsMenu->addSeparator(); QAction *action = new QAction(i18n("Configure Web Shortcuts..."), webShortcutsMenu); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); connect(action, &QAction::triggered, this, &KUriFilterSearchProviderActions::slotConfigureWebShortcuts); webShortcutsMenu->addAction(action); } menu->addMenu(webShortcutsMenu); } } } void KUriFilterSearchProviderActions::slotHandleWebShortcutAction(QAction *action) { KUriFilterData filterData(action->data().toString()); if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::WebShortcutFilter)) { QDesktopServices::openUrl(filterData.uri()); } } diff --git a/src/widgets/kurlcompletion.cpp b/src/widgets/kurlcompletion.cpp index 7066795e..4c92eee6 100644 --- a/src/widgets/kurlcompletion.cpp +++ b/src/widgets/kurlcompletion.cpp @@ -1,1561 +1,1558 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Smith Copyright (C) 2004 Scott Wheeler This class was inspired by a previous KUrlCompletion by Henner Zeller This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kurlcompletion.h" #include "../pathhelpers_p.h" #include #include #include -#include #include #include -#include #include #include #include -#include #include #include #include #include #include #include // QT_LSTAT, QT_STAT, QT_STATBUF #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #else #include #include #endif static bool expandTilde(QString &); static bool expandEnv(QString &); static QString unescape(const QString &text); // Permission mask for files that are executable by // user, group or other #define MODE_EXE (S_IXUSR | S_IXGRP | S_IXOTH) // Constants for types of completion enum ComplType {CTNone = 0, CTEnv, CTUser, CTMan, CTExe, CTFile, CTUrl, CTInfo}; class CompletionThread; // Ensure that we don't end up with "//". static QUrl addPathToUrl(const QUrl &url, const QString &relPath) { QUrl u(url); u.setPath(concatPaths(url.path(), relPath)); return u; } static QBasicAtomicInt s_waitDuration = Q_BASIC_ATOMIC_INITIALIZER(-1); static int initialWaitDuration() { if (s_waitDuration.load() == -1) { const QByteArray envVar = qgetenv("KURLCOMPLETION_WAIT"); if (envVar.isEmpty()) { s_waitDuration = 200; // default: 200 ms } else { s_waitDuration = envVar.toInt(); } } return s_waitDuration; } /////////////////////////////////////////////////////// /////////////////////////////////////////////////////// // KUrlCompletionPrivate // class KUrlCompletionPrivate { public: explicit KUrlCompletionPrivate(KUrlCompletion *parent) : q(parent), url_auto_completion(true), userListThread(nullptr), dirListThread(nullptr) { } ~KUrlCompletionPrivate(); void _k_slotEntries(KIO::Job *, const KIO::UDSEntryList &); void _k_slotIOFinished(KJob *); void slotCompletionThreadDone(QThread *thread, const QStringList &matches); class MyURL; bool userCompletion(const MyURL &url, QString *match); bool envCompletion(const MyURL &url, QString *match); bool exeCompletion(const MyURL &url, QString *match); bool fileCompletion(const MyURL &url, QString *match); bool urlCompletion(const MyURL &url, QString *match); bool isAutoCompletion(); // List the next dir in m_dirs QString listDirectories(const QStringList &, const QString &, bool only_exe = false, bool only_dir = false, bool no_hidden = false, bool stat_files = true); void listUrls(const QList &urls, const QString &filter = QString(), bool only_exe = false, bool no_hidden = false); void addMatches(const QStringList &); QString finished(); void init(); void setListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false); bool isListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false); KUrlCompletion * const q; QList list_urls; bool onlyLocalProto; // urlCompletion() in Auto/Popup mode? bool url_auto_completion; // Append '/' to directories in Popup mode? // Doing that stat's all files and is slower bool popup_append_slash; // Keep track of currently listed files to avoid reading them again bool last_no_hidden; QString last_path_listed; QString last_file_listed; QString last_prepend; ComplType last_compl_type; QUrl cwd; // "current directory" = base dir for completion KUrlCompletion::Mode mode; // ExeCompletion, FileCompletion, DirCompletion bool replace_env; bool replace_home; bool complete_url; // if true completing a URL (i.e. 'prepend' is a URL), otherwise a path KIO::ListJob *list_job; // kio job to list directories QString prepend; // text to prepend to listed items QString compl_text; // text to pass on to KCompletion // Filters for files read with kio bool list_urls_only_exe; // true = only list executables bool list_urls_no_hidden; QString list_urls_filter; // filter for listed files CompletionThread *userListThread; CompletionThread *dirListThread; QStringList mimeTypeFilters; }; class CompletionThread : public QThread { Q_OBJECT protected: CompletionThread(KUrlCompletionPrivate *receiver) : QThread(), m_prepend(receiver->prepend), m_complete_url(receiver->complete_url), m_terminationRequested(false) {} public: void requestTermination() { if (!isFinished()) { qCDebug(KIO_WIDGETS) << "stopping thread" << this; } m_terminationRequested.store(true); wait(); } QStringList matches() const { QMutexLocker locker(&m_mutex); return m_matches; } Q_SIGNALS: void completionThreadDone(QThread *thread, const QStringList &matches); protected: void addMatch(const QString &match) { QMutexLocker locker(&m_mutex); m_matches.append(match); } bool terminationRequested() const { return m_terminationRequested.load(); } void done() { if (!terminationRequested()) { qCDebug(KIO_WIDGETS) << "done, emitting signal with" << m_matches.count() << "matches"; emit completionThreadDone(this, m_matches); } } const QString m_prepend; const bool m_complete_url; // if true completing a URL (i.e. 'm_prepend' is a URL), otherwise a path private: mutable QMutex m_mutex; // protects m_matches QStringList m_matches; // written by secondary thread, read by the matches() method QAtomicInt m_terminationRequested; // used as a bool }; /** * A simple thread that fetches a list of tilde-completions and returns this * to the caller via the completionThreadDone signal. */ class UserListThread : public CompletionThread { Q_OBJECT public: UserListThread(KUrlCompletionPrivate *receiver) : CompletionThread(receiver) {} protected: void run() override { #ifndef Q_OS_ANDROID const QChar tilde = QLatin1Char('~'); // we don't need to handle prepend here, right? ~user is always at pos 0 assert(m_prepend.isEmpty()); #pragma message("TODO: add KUser::allUserNames() with a std::function shouldTerminate parameter") #ifndef Q_OS_WIN struct passwd *pw; ::setpwent(); while ((pw = ::getpwent()) && !terminationRequested()) { addMatch(tilde + QString::fromLocal8Bit(pw->pw_name)); } ::endpwent(); #else //currently terminationRequested is ignored on Windows const QStringList allUsers = KUser::allUserNames(); for (const QString& s : allUsers) { addMatch(tilde + s); } #endif addMatch(QString(tilde)); #endif done(); } }; class DirectoryListThread : public CompletionThread { Q_OBJECT public: DirectoryListThread(KUrlCompletionPrivate *receiver, const QStringList &dirList, const QString &filter, const QStringList &mimeTypeFilters, bool onlyExe, bool onlyDir, bool noHidden, bool appendSlashToDir) : CompletionThread(receiver), m_dirList(dirList), m_filter(filter), m_mimeTypeFilters(mimeTypeFilters), m_onlyExe(onlyExe), m_onlyDir(onlyDir), m_noHidden(noHidden), m_appendSlashToDir(appendSlashToDir) {} void run() override; private: QStringList m_dirList; QString m_filter; QStringList m_mimeTypeFilters; bool m_onlyExe; bool m_onlyDir; bool m_noHidden; bool m_appendSlashToDir; }; void DirectoryListThread::run() { //qDebug() << "Entered DirectoryListThread::run(), m_filter=" << m_filter << ", m_onlyExe=" << m_onlyExe << ", m_onlyDir=" << m_onlyDir << ", m_appendSlashToDir=" << m_appendSlashToDir << ", m_dirList.size()=" << m_dirList.size(); QDir::Filters iterator_filter = (m_noHidden ? QDir::Filter(0) : QDir::Hidden) | QDir::Readable | QDir::NoDotAndDotDot; if (m_onlyExe) { iterator_filter |= (QDir::Dirs | QDir::Files | QDir::Executable); } else if (m_onlyDir) { iterator_filter |= QDir::Dirs; } else { iterator_filter |= (QDir::Dirs | QDir::Files); } QMimeDatabase mimeTypes; const QStringList::const_iterator end = m_dirList.constEnd(); for (QStringList::const_iterator it = m_dirList.constBegin(); it != end && !terminationRequested(); ++it) { //qDebug() << "Scanning directory" << *it; QDirIterator current_dir_iterator(*it, iterator_filter); while (current_dir_iterator.hasNext() && !terminationRequested()) { current_dir_iterator.next(); QFileInfo file_info = current_dir_iterator.fileInfo(); const QString file_name = file_info.fileName(); //qDebug() << "Found" << file_name; if (!m_filter.isEmpty() && !file_name.startsWith(m_filter)) { continue; } if (!m_mimeTypeFilters.isEmpty() && !file_info.isDir()) { auto mimeType = mimeTypes.mimeTypeForFile(file_info); if (!m_mimeTypeFilters.contains(mimeType.name())) { continue; } } QString toAppend = file_name; // Add '/' to directories if (m_appendSlashToDir && file_info.isDir()) { toAppend.append(QLatin1Char('/')); } if (m_complete_url) { QUrl info(m_prepend); info = addPathToUrl(info, toAppend); addMatch(info.toDisplayString()); } else { addMatch(m_prepend + toAppend); } } } done(); } KUrlCompletionPrivate::~KUrlCompletionPrivate() { } /////////////////////////////////////////////////////// /////////////////////////////////////////////////////// // MyURL - wrapper for QUrl with some different functionality // class KUrlCompletionPrivate::MyURL { public: MyURL(const QString &url, const QUrl &cwd); MyURL(const MyURL &url); ~MyURL(); QUrl kurl() const { return m_kurl; } bool isLocalFile() const { return m_kurl.isLocalFile(); } QString scheme() const { return m_kurl.scheme(); } // The directory with a trailing '/' QString dir() const { return m_kurl.adjusted(QUrl::RemoveFilename).path(); } QString file() const { return m_kurl.fileName(); } // The initial, unparsed, url, as a string. QString url() const { return m_url; } // Is the initial string a URL, or just a path (whether absolute or relative) bool isURL() const { return m_isURL; } void filter(bool replace_user_dir, bool replace_env); private: void init(const QString &url, const QUrl &cwd); QUrl m_kurl; QString m_url; bool m_isURL; }; KUrlCompletionPrivate::MyURL::MyURL(const QString &_url, const QUrl &cwd) { init(_url, cwd); } KUrlCompletionPrivate::MyURL::MyURL(const MyURL &_url) : m_kurl(_url.m_kurl) { m_url = _url.m_url; m_isURL = _url.m_isURL; } void KUrlCompletionPrivate::MyURL::init(const QString &_url, const QUrl &cwd) { // Save the original text m_url = _url; // Non-const copy QString url_copy = _url; // Special shortcuts for "man:" and "info:" if (url_copy.startsWith(QLatin1Char('#'))) { if (url_copy.length() > 1 && url_copy.at(1) == QLatin1Char('#')) { url_copy.replace(0, 2, QStringLiteral("info:")); } else { url_copy.replace(0, 1, QStringLiteral("man:")); } } // Look for a protocol in 'url' QRegExp protocol_regex = QRegExp(QStringLiteral("^(?![A-Za-z]:)[^/\\s\\\\]*:")); // Assume "file:" or whatever is given by 'cwd' if there is // no protocol. (QUrl does this only for absolute paths) if (protocol_regex.indexIn(url_copy) == 0) { m_kurl = QUrl(url_copy); m_isURL = true; } else { // relative path or ~ or $something m_isURL = false; if (!QDir::isRelativePath(url_copy) || url_copy.startsWith(QLatin1Char('~')) || url_copy.startsWith(QLatin1Char('$'))) { m_kurl = QUrl::fromLocalFile(url_copy); } else { // Relative path if (cwd.isEmpty()) { m_kurl = QUrl(url_copy); } else { m_kurl = cwd; m_kurl.setPath(concatPaths(m_kurl.path(), url_copy)); } } } } KUrlCompletionPrivate::MyURL::~MyURL() { } void KUrlCompletionPrivate::MyURL::filter(bool replace_user_dir, bool replace_env) { QString d = dir() + file(); if (replace_user_dir) { expandTilde(d); } if (replace_env) { expandEnv(d); } m_kurl.setPath(d); } /////////////////////////////////////////////////////// /////////////////////////////////////////////////////// // KUrlCompletion // KUrlCompletion::KUrlCompletion() : KCompletion(), d(new KUrlCompletionPrivate(this)) { d->init(); } KUrlCompletion::KUrlCompletion(Mode _mode) : KCompletion(), d(new KUrlCompletionPrivate(this)) { d->init(); setMode(_mode); } KUrlCompletion::~KUrlCompletion() { stop(); delete d; } void KUrlCompletionPrivate::init() { cwd = QUrl::fromLocalFile(QDir::homePath()); replace_home = true; replace_env = true; last_no_hidden = false; last_compl_type = CTNone; list_job = nullptr; mode = KUrlCompletion::FileCompletion; // Read settings KConfigGroup cg(KSharedConfig::openConfig(), "URLCompletion"); url_auto_completion = cg.readEntry("alwaysAutoComplete", true); popup_append_slash = cg.readEntry("popupAppendSlash", true); onlyLocalProto = cg.readEntry("LocalProtocolsOnly", false); q->setIgnoreCase(true); } void KUrlCompletion::setDir(const QUrl &dir) { d->cwd = dir; } QUrl KUrlCompletion::dir() const { return d->cwd; } KUrlCompletion::Mode KUrlCompletion::mode() const { return d->mode; } void KUrlCompletion::setMode(Mode _mode) { d->mode = _mode; } bool KUrlCompletion::replaceEnv() const { return d->replace_env; } void KUrlCompletion::setReplaceEnv(bool replace) { d->replace_env = replace; } bool KUrlCompletion::replaceHome() const { return d->replace_home; } void KUrlCompletion::setReplaceHome(bool replace) { d->replace_home = replace; } /* * makeCompletion() * * Entry point for file name completion */ QString KUrlCompletion::makeCompletion(const QString &text) { qCDebug(KIO_WIDGETS) << text << "d->cwd=" << d->cwd; KUrlCompletionPrivate::MyURL url(text, d->cwd); d->compl_text = text; // Set d->prepend to the original URL, with the filename [and ref/query] stripped. // This is what gets prepended to the directory-listing matches. if (url.isURL()) { QUrl directoryUrl(url.kurl()); directoryUrl.setQuery(QString()); directoryUrl.setFragment(QString()); directoryUrl.setPath(url.dir()); d->prepend = directoryUrl.toString(); } else { d->prepend = text.left(text.length() - url.file().length()); } d->complete_url = url.isURL(); QString aMatch; // Environment variables // if (d->replace_env && d->envCompletion(url, &aMatch)) { return aMatch; } // User directories // if (d->replace_home && d->userCompletion(url, &aMatch)) { return aMatch; } // Replace user directories and variables url.filter(d->replace_home, d->replace_env); //qDebug() << "Filtered: proto=" << url.scheme() // << ", dir=" << url.dir() // << ", file=" << url.file() // << ", kurl url=" << *url.kurl(); if (d->mode == ExeCompletion) { // Executables // if (d->exeCompletion(url, &aMatch)) { return aMatch; } // KRun can run "man:" and "info:" etc. so why not treat them // as executables... if (d->urlCompletion(url, &aMatch)) { return aMatch; } } else { // Local files, directories // if (d->fileCompletion(url, &aMatch)) { return aMatch; } // All other... // if (d->urlCompletion(url, &aMatch)) { return aMatch; } } d->setListedUrl(CTNone); stop(); return QString(); } /* * finished * * Go on and call KCompletion. * Called when all matches have been added */ QString KUrlCompletionPrivate::finished() { if (last_compl_type == CTInfo) { return q->KCompletion::makeCompletion(compl_text.toLower()); } else { return q->KCompletion::makeCompletion(compl_text); } } /* * isRunning * * Return true if either a KIO job or a thread is running */ bool KUrlCompletion::isRunning() const { return d->list_job || (d->dirListThread && !d->dirListThread->isFinished()) || (d->userListThread && !d->userListThread->isFinished()); } /* * stop * * Stop and delete a running KIO job or the DirLister */ void KUrlCompletion::stop() { if (d->list_job) { d->list_job->kill(); d->list_job = nullptr; } if (d->dirListThread) { d->dirListThread->requestTermination(); delete d->dirListThread; d->dirListThread = nullptr; } if (d->userListThread) { d->userListThread->requestTermination(); delete d->userListThread; d->userListThread = nullptr; } } /* * Keep track of the last listed directory */ void KUrlCompletionPrivate::setListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden) { last_compl_type = complType; last_path_listed = directory; last_file_listed = filter; last_no_hidden = no_hidden; last_prepend = prepend; } bool KUrlCompletionPrivate::isListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden) { return last_compl_type == complType && (last_path_listed == directory || (directory.isEmpty() && last_path_listed.isEmpty())) && (filter.startsWith(last_file_listed) || (filter.isEmpty() && last_file_listed.isEmpty())) && last_no_hidden == no_hidden && last_prepend == prepend; // e.g. relative path vs absolute } /* * isAutoCompletion * * Returns true if completion mode is Auto or Popup */ bool KUrlCompletionPrivate::isAutoCompletion() { return q->completionMode() == KCompletion::CompletionAuto || q->completionMode() == KCompletion::CompletionPopup || q->completionMode() == KCompletion::CompletionMan || q->completionMode() == KCompletion::CompletionPopupAuto; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // User directories // bool KUrlCompletionPrivate::userCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (url.scheme() != QLatin1String("file") || !url.dir().isEmpty() || !url.file().startsWith(QLatin1Char('~')) || !prepend.isEmpty()) { return false; } if (!isListedUrl(CTUser)) { q->stop(); q->clear(); setListedUrl(CTUser); Q_ASSERT(!userListThread); // caller called stop() userListThread = new UserListThread(this); QObject::connect(userListThread, &CompletionThread::completionThreadDone, q, [this](QThread *thread, const QStringList &matches){ slotCompletionThreadDone(thread, matches); }); userListThread->start(); // If the thread finishes quickly make sure that the results // are added to the first matching case. userListThread->wait(initialWaitDuration()); const QStringList l = userListThread->matches(); addMatches(l); } *pMatch = finished(); return true; } ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// // Environment variables // bool KUrlCompletionPrivate::envCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (url.file().isEmpty() || url.file().at(0) != QLatin1Char('$')) { return false; } if (!isListedUrl(CTEnv)) { q->stop(); q->clear(); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); const QStringList keys = env.keys(); QStringList l; l.reserve(keys.size()); for (const QString &key : keys) { l.append(prepend + QLatin1Char('$') + key); } addMatches(l); } setListedUrl(CTEnv); *pMatch = finished(); return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // Executables // bool KUrlCompletionPrivate::exeCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (!url.isLocalFile()) { return false; } QString directory = unescape(url.dir()); // remove escapes // Find directories to search for completions, either // // 1. complete path given in url // 2. current directory (d->cwd) // 3. $PATH // 4. no directory at all QStringList dirList; if (!url.file().isEmpty()) { // $PATH dirList = QString::fromLocal8Bit(qgetenv("PATH")).split( QDir::listSeparator(), QString::SkipEmptyParts); QStringList::Iterator it = dirList.begin(); for (; it != dirList.end(); ++it) { it->append(QLatin1Char('/')); } } else if (!QDir::isRelativePath(directory)) { // complete path in url dirList.append(directory); } else if (!directory.isEmpty() && !cwd.isEmpty()) { // current directory dirList.append(cwd.toLocalFile() + QLatin1Char('/') + directory); } // No hidden files unless the user types "." bool no_hidden_files = url.file().isEmpty() || url.file().at(0) != QLatin1Char('.'); // List files if needed // if (!isListedUrl(CTExe, directory, url.file(), no_hidden_files)) { q->stop(); q->clear(); setListedUrl(CTExe, directory, url.file(), no_hidden_files); *pMatch = listDirectories(dirList, url.file(), true, false, no_hidden_files); } else { *pMatch = finished(); } return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // Local files // bool KUrlCompletionPrivate::fileCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (!url.isLocalFile()) { return false; } QString directory = unescape(url.dir()); if (url.url() == QLatin1String("..")) { *pMatch = QStringLiteral(".."); return true; } //qDebug() << "fileCompletion" << url << "dir=" << dir; // Find directories to search for completions, either // // 1. complete path given in url // 2. current directory (d->cwd) // 3. no directory at all QStringList dirList; if (!QDir::isRelativePath(directory)) { // complete path in url dirList.append(directory); } else if (!cwd.isEmpty()) { // current directory QString dirToAdd = cwd.toLocalFile(); if (!directory.isEmpty()) { if (!dirToAdd.endsWith(QLatin1Char('/'))) { dirToAdd.append(QLatin1Char('/')); } dirToAdd.append(directory); } dirList.append(dirToAdd); } // No hidden files unless the user types "." bool no_hidden_files = !url.file().startsWith(QLatin1Char('.')); // List files if needed // if (!isListedUrl(CTFile, directory, QString(), no_hidden_files)) { q->stop(); q->clear(); setListedUrl(CTFile, directory, QString(), no_hidden_files); // Append '/' to directories in Popup mode? bool append_slash = (popup_append_slash && (q->completionMode() == KCompletion::CompletionPopup || q->completionMode() == KCompletion::CompletionPopupAuto)); bool only_dir = (mode == KUrlCompletion::DirCompletion); *pMatch = listDirectories(dirList, QString(), false, only_dir, no_hidden_files, append_slash); } else { *pMatch = finished(); } return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // URLs not handled elsewhere... // static bool isLocalProtocol(const QString &protocol) { return (KProtocolInfo::protocolClass(protocol) == QLatin1String(":local")); } bool KUrlCompletionPrivate::urlCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { //qDebug() << *url.kurl(); if (onlyLocalProto && isLocalProtocol(url.scheme())) { return false; } // Use d->cwd as base url in case url is not absolute QUrl url_dir = url.kurl(); if (url_dir.isRelative() && !cwd.isEmpty()) { // Create an URL with the directory to be listed url_dir = cwd.resolved(url_dir); } // url is malformed if (!url_dir.isValid() || url.scheme().isEmpty()) { return false; } // non local urls if (!isLocalProtocol(url.scheme())) { // url does not specify host if (url_dir.host().isEmpty()) { return false; } // url does not specify a valid directory if (url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty()) { return false; } // automatic completion is disabled if (isAutoCompletion() && !url_auto_completion) { return false; } } // url handler doesn't support listing if (!KProtocolManager::supportsListing(url_dir)) { return false; } // Remove escapes const QString directory = unescape(url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); url_dir.setPath(directory); // List files if needed // if (!isListedUrl(CTUrl, directory, url.file())) { q->stop(); q->clear(); setListedUrl(CTUrl, directory, QString()); QList url_list; url_list.append(url_dir); listUrls(url_list, QString(), false); pMatch->clear(); } else if (!q->isRunning()) { *pMatch = finished(); } else { pMatch->clear(); } return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // Directory and URL listing // /* * addMatches * * Called to add matches to KCompletion */ void KUrlCompletionPrivate::addMatches(const QStringList &matchList) { q->insertItems(matchList); } /* * listDirectories * * List files starting with 'filter' in the given directories, * either using DirLister or listURLs() * * In either case, addMatches() is called with the listed * files, and eventually finished() when the listing is done * * Returns the match if available, or QString() if * DirLister timed out or using kio */ QString KUrlCompletionPrivate::listDirectories( const QStringList &dirList, const QString &filter, bool only_exe, bool only_dir, bool no_hidden, bool append_slash_to_dir) { assert(!q->isRunning()); if (qEnvironmentVariableIsEmpty("KURLCOMPLETION_LOCAL_KIO")) { qCDebug(KIO_WIDGETS) << "Listing directories:" << dirList << "with filter=" << filter << "using thread"; // Don't use KIO QStringList dirs; QStringList::ConstIterator end = dirList.constEnd(); for (QStringList::ConstIterator it = dirList.constBegin(); it != end; ++it) { QUrl url = QUrl::fromLocalFile(*it); if (KUrlAuthorized::authorizeUrlAction(QStringLiteral("list"), QUrl(), url)) { dirs.append(*it); } } Q_ASSERT(!dirListThread); // caller called stop() dirListThread = new DirectoryListThread(this, dirs, filter, mimeTypeFilters, only_exe, only_dir, no_hidden, append_slash_to_dir); QObject::connect(dirListThread, &CompletionThread::completionThreadDone, q, [this](QThread *thread, const QStringList &matches){ slotCompletionThreadDone(thread, matches); }); dirListThread->start(); dirListThread->wait(initialWaitDuration()); qCDebug(KIO_WIDGETS) << "Adding initial matches:" << dirListThread->matches(); addMatches(dirListThread->matches()); return finished(); } // Use KIO //qDebug() << "Listing (listDirectories):" << dirList << "with KIO"; QList url_list; QStringList::ConstIterator it = dirList.constBegin(); QStringList::ConstIterator end = dirList.constEnd(); url_list.reserve(dirList.size()); for (; it != end; ++it) { url_list.append(QUrl(*it)); } listUrls(url_list, filter, only_exe, no_hidden); // Will call addMatches() and finished() return QString(); } /* * listURLs * * Use KIO to list the given urls * * addMatches() is called with the listed files * finished() is called when the listing is done */ void KUrlCompletionPrivate::listUrls( const QList &urls, const QString &filter, bool only_exe, bool no_hidden) { assert(list_urls.isEmpty()); assert(list_job == nullptr); list_urls = urls; list_urls_filter = filter; list_urls_only_exe = only_exe; list_urls_no_hidden = no_hidden; //qDebug() << "Listing URLs:" << *urls[0] << ",..."; // Start it off by calling _k_slotIOFinished // // This will start a new list job as long as there // are urls in d->list_urls // _k_slotIOFinished(nullptr); } /* * _k_slotEntries * * Receive files listed by KIO and call addMatches() */ void KUrlCompletionPrivate::_k_slotEntries(KIO::Job *, const KIO::UDSEntryList &entries) { QStringList matchList; KIO::UDSEntryList::ConstIterator it = entries.constBegin(); const KIO::UDSEntryList::ConstIterator end = entries.constEnd(); QString filter = list_urls_filter; int filter_len = filter.length(); // Iterate over all files // for (; it != end; ++it) { const KIO::UDSEntry &entry = *it; const QString url = entry.stringValue(KIO::UDSEntry::UDS_URL); QString entry_name; if (!url.isEmpty()) { //qDebug() << "url:" << url; entry_name = QUrl(url).fileName(); } else { entry_name = entry.stringValue(KIO::UDSEntry::UDS_NAME); } //qDebug() << "name:" << name; if ((!entry_name.isEmpty() && entry_name.at(0) == QLatin1Char('.')) && (list_urls_no_hidden || entry_name.length() == 1 || (entry_name.length() == 2 && entry_name.at(1) == QLatin1Char('.')))) { continue; } const bool isDir = entry.isDir(); if (mode == KUrlCompletion::DirCompletion && !isDir) { continue; } if (filter_len != 0 && entry_name.leftRef(filter_len) != filter) { continue; } if (!mimeTypeFilters.isEmpty() && !isDir && !mimeTypeFilters.contains(entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE))) { continue; } QString toAppend = entry_name; if (isDir) { toAppend.append(QLatin1Char('/')); } if (!list_urls_only_exe || (entry.numberValue(KIO::UDSEntry::UDS_ACCESS) & MODE_EXE) // true if executable ) { if (complete_url) { QUrl url(prepend); url = addPathToUrl(url, toAppend); matchList.append(url.toDisplayString()); } else { matchList.append(prepend + toAppend); } } } addMatches(matchList); } /* * _k_slotIOFinished * * Called when a KIO job is finished. * * Start a new list job if there are still urls in * list_urls, otherwise call finished() */ void KUrlCompletionPrivate::_k_slotIOFinished(KJob *job) { assert(job == list_job); Q_UNUSED(job) if (list_urls.isEmpty()) { list_job = nullptr; finished(); // will call KCompletion::makeCompletion() } else { QUrl kurl(list_urls.takeFirst()); // list_urls.removeAll( kurl ); //qDebug() << "Start KIO::listDir" << kurl; list_job = KIO::listDir(kurl, KIO::HideProgressInfo); list_job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); assert(list_job); q->connect(list_job, SIGNAL(result(KJob*)), SLOT(_k_slotIOFinished(KJob*))); q->connect(list_job, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), SLOT(_k_slotEntries(KIO::Job*,KIO::UDSEntryList))); } } /////////////////////////////////////////////////// /////////////////////////////////////////////////// /* * postProcessMatch, postProcessMatches * * Called by KCompletion before emitting match() and matches() * * Append '/' to directories for file completion. This is * done here to avoid stat()'ing a lot of files */ void KUrlCompletion::postProcessMatch(QString *pMatch) const { //qDebug() << *pMatch; if (!pMatch->isEmpty() && pMatch->startsWith(QLatin1String("file:"))) { // Add '/' to directories in file completion mode // unless it has already been done if (d->last_compl_type == CTFile && pMatch->at(pMatch->length() - 1) != QLatin1Char('/')) { QString copy = QUrl(*pMatch).toLocalFile(); expandTilde(copy); expandEnv(copy); if (QDir::isRelativePath(copy)) { copy.prepend(d->cwd.toLocalFile() + QLatin1Char('/')); } //qDebug() << "stat'ing" << copy; QByteArray file = QFile::encodeName(copy); QT_STATBUF sbuff; if (QT_STAT(file.constData(), &sbuff) == 0) { if ((sbuff.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { pMatch->append(QLatin1Char('/')); } } else { //qDebug() << "Could not stat file" << copy; } } } } void KUrlCompletion::postProcessMatches(QStringList * /*matches*/) const { // Maybe '/' should be added to directories here as in // postProcessMatch() but it would slow things down // when there are a lot of matches... } void KUrlCompletion::postProcessMatches(KCompletionMatches * /*matches*/) const { // Maybe '/' should be added to directories here as in // postProcessMatch() but it would slow things down // when there are a lot of matches... } // no longer used, KF6 TODO: remove this method void KUrlCompletion::customEvent(QEvent *e) { KCompletion::customEvent(e); } void KUrlCompletionPrivate::slotCompletionThreadDone(QThread *thread, const QStringList &matches) { if (thread != userListThread && thread != dirListThread) { qCDebug(KIO_WIDGETS) << "got" << matches.count() << "outdated matches"; return; } qCDebug(KIO_WIDGETS) << "got" << matches.count() << "matches at end of thread"; q->setItems(matches); if (userListThread == thread) { thread->wait(); delete thread; userListThread = nullptr; } if (dirListThread == thread) { thread->wait(); delete thread; dirListThread = nullptr; } finished(); // will call KCompletion::makeCompletion() } // static QString KUrlCompletion::replacedPath(const QString &text, bool replaceHome, bool replaceEnv) { if (text.isEmpty()) { return text; } KUrlCompletionPrivate::MyURL url(text, QUrl()); // no need to replace something of our current cwd if (!url.kurl().isLocalFile()) { return text; } url.filter(replaceHome, replaceEnv); return url.dir() + url.file(); } QString KUrlCompletion::replacedPath(const QString &text) const { return replacedPath(text, d->replace_home, d->replace_env); } void KUrlCompletion::setMimeTypeFilters(const QStringList &mimeTypeFilters) { d->mimeTypeFilters = mimeTypeFilters; } QStringList KUrlCompletion::mimeTypeFilters() const { return d->mimeTypeFilters; } ///////////////////////////////////////////////////////// ///////////////////////////////////////////////////////// // Static functions /* * expandEnv * * Expand environment variables in text. Escaped '$' are ignored. * Return true if expansion was made. */ static bool expandEnv(QString &text) { // Find all environment variables beginning with '$' // int pos = 0; bool expanded = false; while ((pos = text.indexOf(QLatin1Char('$'), pos)) != -1) { // Skip escaped '$' // if (pos > 0 && text.at(pos - 1) == QLatin1Char('\\')) { pos++; } // Variable found => expand // else { // Find the end of the variable = next '/' or ' ' // int pos2 = text.indexOf(QLatin1Char(' '), pos + 1); int pos_tmp = text.indexOf(QLatin1Char('/'), pos + 1); if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) { pos2 = pos_tmp; } if (pos2 == -1) { pos2 = text.length(); } // Replace if the variable is terminated by '/' or ' ' // and defined // if (pos2 >= 0) { int len = pos2 - pos; const QStringRef key = text.midRef(pos + 1, len - 1); QString value = QString::fromLocal8Bit(qgetenv(key.toLocal8Bit().constData())); if (!value.isEmpty()) { expanded = true; text.replace(pos, len, value); pos = pos + value.length(); } else { pos = pos2; } } } } return expanded; } /* * expandTilde * * Replace "~user" with the users home directory * Return true if expansion was made. */ static bool expandTilde(QString &text) { if (text.isEmpty() || (text.at(0) != QLatin1Char('~'))) { return false; } bool expanded = false; // Find the end of the user name = next '/' or ' ' // int pos2 = text.indexOf(QLatin1Char(' '), 1); int pos_tmp = text.indexOf(QLatin1Char('/'), 1); if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) { pos2 = pos_tmp; } if (pos2 == -1) { pos2 = text.length(); } // Replace ~user if the user name is terminated by '/' or ' ' // if (pos2 >= 0) { QString userName = text.mid(1, pos2 - 1); QString dir; // A single ~ is replaced with $HOME // if (userName.isEmpty()) { dir = QDir::homePath(); } // ~user is replaced with the dir from passwd // else { KUser user(userName); dir = user.homeDir(); } if (!dir.isEmpty()) { expanded = true; text.replace(0, pos2, dir); } } return expanded; } /* * unescape * * Remove escapes and return the result in a new string * */ static QString unescape(const QString &text) { QString result; for (int pos = 0; pos < text.length(); pos++) if (text.at(pos) != QLatin1Char('\\')) { result.insert(result.length(), text.at(pos)); } return result; } #include "moc_kurlcompletion.cpp" #include "kurlcompletion.moc" diff --git a/src/widgets/kurlrequester.cpp b/src/widgets/kurlrequester.cpp index 4d18b8fa..776fcdd3 100644 --- a/src/widgets/kurlrequester.cpp +++ b/src/widgets/kurlrequester.cpp @@ -1,718 +1,716 @@ /* This file is part of the KDE libraries Copyright (C) 1999,2000,2001 Carsten Pfeiffer Copyright (C) 2013 Teo Mrnjavac This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kurlrequester.h" #include "kio_widgets_debug.h" #include "../pathhelpers_p.h" #include #include #include #include #include #include #include #include -#include #include #include #include #include #include -#include #include #include class KUrlDragPushButton : public QPushButton { Q_OBJECT public: explicit KUrlDragPushButton(QWidget *parent) : QPushButton(parent) { new DragDecorator(this); } ~KUrlDragPushButton() {} void setURL(const QUrl &url) { m_urls.clear(); m_urls.append(url); } private: class DragDecorator : public KDragWidgetDecoratorBase { public: explicit DragDecorator(KUrlDragPushButton *button) : KDragWidgetDecoratorBase(button), m_button(button) {} protected: QDrag *dragObject() override { if (m_button->m_urls.isEmpty()) { return nullptr; } QDrag *drag = new QDrag(m_button); QMimeData *mimeData = new QMimeData; mimeData->setUrls(m_button->m_urls); drag->setMimeData(mimeData); return drag; } private: KUrlDragPushButton *m_button; }; QList m_urls; }; class Q_DECL_HIDDEN KUrlRequester::KUrlRequesterPrivate { public: explicit KUrlRequesterPrivate(KUrlRequester *parent) : m_fileDialogModeWasDirAndFile(false), m_parent(parent), edit(nullptr), combo(nullptr), fileDialogMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly), fileDialogAcceptMode(QFileDialog::AcceptOpen) { } ~KUrlRequesterPrivate() { delete myCompletion; delete myFileDialog; } void init(); void setText(const QString &text) { if (combo) { if (combo->isEditable()) { combo->setEditText(text); } else { int i = combo->findText(text); if (i == -1) { combo->addItem(text); combo->setCurrentIndex(combo->count() - 1); } else { combo->setCurrentIndex(i); } } } else { edit->setText(text); } } void connectSignals(KUrlRequester *receiver) { if (combo) { connect(combo, &QComboBox::currentTextChanged, receiver, &KUrlRequester::textChanged); connect(combo, &QComboBox::editTextChanged, receiver, &KUrlRequester::textEdited); connect(combo, QOverload<>::of(&KComboBox::returnPressed), receiver, QOverload<>::of(&KUrlRequester::returnPressed)); connect(combo, QOverload::of(&KComboBox::returnPressed), receiver, QOverload::of(&KUrlRequester::returnPressed)); } else if (edit) { connect(edit, &QLineEdit::textChanged, receiver, &KUrlRequester::textChanged); connect(edit, &QLineEdit::textEdited, receiver, &KUrlRequester::textEdited); connect(edit, QOverload<>::of(&QLineEdit::returnPressed), receiver, QOverload<>::of(&KUrlRequester::returnPressed)); if (auto kline = qobject_cast(edit)) { connect(kline, QOverload::of(&KLineEdit::returnPressed), receiver, QOverload::of(&KUrlRequester::returnPressed)); } } } void setCompletionObject(KCompletion *comp) { if (combo) { combo->setCompletionObject(comp); } else { edit->setCompletionObject(comp); } } void updateCompletionStartDir(const QUrl &newStartDir) { myCompletion->setDir(newStartDir); } QString text() const { return combo ? combo->currentText() : edit->text(); } /** * replaces ~user or $FOO, if necessary * if text() is a relative path, make it absolute using startDir() */ QUrl url() const { const QString txt = text(); KUrlCompletion *comp; if (combo) { comp = qobject_cast(combo->completionObject()); } else { comp = qobject_cast(edit->completionObject()); } QString enteredPath; if (comp) enteredPath = comp->replacedPath(txt); else enteredPath = txt; if (QDir::isAbsolutePath(enteredPath)) { return QUrl::fromLocalFile(enteredPath); } const QUrl enteredUrl = QUrl(enteredPath); // absolute or relative if (enteredUrl.isRelative() && !txt.isEmpty()) { QUrl finalUrl(m_startDir); finalUrl.setPath(concatPaths(finalUrl.path(), enteredPath)); return finalUrl; } else { return enteredUrl; } } static void applyFileMode(QFileDialog *dlg, KFile::Modes m, QFileDialog::AcceptMode acceptMode) { QFileDialog::FileMode fileMode; bool dirsOnly = false; if (m & KFile::Directory) { fileMode = QFileDialog::Directory; if ((m & KFile::File) == 0 && (m & KFile::Files) == 0) { dirsOnly = true; } } else if (m & KFile::Files && m & KFile::ExistingOnly) { fileMode = QFileDialog::ExistingFiles; } else if (m & KFile::File && m & KFile::ExistingOnly) { fileMode = QFileDialog::ExistingFile; } else { fileMode = QFileDialog::AnyFile; } dlg->setFileMode(fileMode); dlg->setAcceptMode(acceptMode); dlg->setOption(QFileDialog::ShowDirsOnly, dirsOnly); } // Converts from "*.foo *.bar|Comment" to "Comment (*.foo *.bar)" QStringList kToQFilters(const QString &filters) const { QStringList qFilters = filters.split(QLatin1Char('\n'), QString::SkipEmptyParts); for (QStringList::iterator it = qFilters.begin(); it != qFilters.end(); ++it) { int sep = it->indexOf(QLatin1Char('|')); const QStringRef globs = it->leftRef(sep); const QStringRef desc = it->midRef(sep + 1); *it = desc + QLatin1String(" (") + globs + QLatin1Char(')'); } return qFilters; } QUrl getDirFromFileDialog(const QUrl &openUrl) const { return QFileDialog::getExistingDirectoryUrl(m_parent, QString(), openUrl, QFileDialog::ShowDirsOnly); } void createFileDialog() { //Creates the fileDialog if it doesn't exist yet QFileDialog *dlg = m_parent->fileDialog(); if (!url().isEmpty() && !url().isRelative()) { QUrl u(url()); // If we won't be able to list it (e.g. http), then don't try :) if (KProtocolManager::supportsListing(u)) { dlg->selectUrl(u); } } else { dlg->setDirectoryUrl(m_startDir); } dlg->setAcceptMode(fileDialogAcceptMode); //Update the file dialog window modality if (dlg->windowModality() != fileDialogModality) { dlg->setWindowModality(fileDialogModality); } if (fileDialogModality == Qt::NonModal) { dlg->show(); } else { dlg->exec(); } } // slots void _k_slotUpdateUrl(); void _k_slotOpenDialog(); void _k_slotFileDialogAccepted(); QUrl m_startDir; bool m_startDirCustomized; bool m_fileDialogModeWasDirAndFile; KUrlRequester * const m_parent; // TODO: rename to 'q' KLineEdit *edit; KComboBox *combo; KFile::Modes fileDialogMode; QFileDialog::AcceptMode fileDialogAcceptMode; QString fileDialogFilter; QStringList mimeTypeFilters; KEditListWidget::CustomEditor editor; KUrlDragPushButton *myButton; QFileDialog *myFileDialog; KUrlCompletion *myCompletion; Qt::WindowModality fileDialogModality; }; KUrlRequester::KUrlRequester(QWidget *editWidget, QWidget *parent) : QWidget(parent), d(new KUrlRequesterPrivate(this)) { // must have this as parent editWidget->setParent(this); d->combo = qobject_cast(editWidget); d->edit = qobject_cast(editWidget); if (d->edit) { d->edit->setClearButtonEnabled(true); } d->init(); } KUrlRequester::KUrlRequester(QWidget *parent) : QWidget(parent), d(new KUrlRequesterPrivate(this)) { d->init(); } KUrlRequester::KUrlRequester(const QUrl &url, QWidget *parent) : QWidget(parent), d(new KUrlRequesterPrivate(this)) { d->init(); setUrl(url); } KUrlRequester::~KUrlRequester() { delete d; } void KUrlRequester::KUrlRequesterPrivate::init() { myFileDialog = nullptr; fileDialogModality = Qt::ApplicationModal; if (!combo && !edit) { edit = new KLineEdit(m_parent); edit->setClearButtonEnabled(true); } QWidget *widget = combo ? static_cast(combo) : static_cast(edit); QHBoxLayout *topLayout = new QHBoxLayout(m_parent); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(-1); // use default spacing topLayout->addWidget(widget); myButton = new KUrlDragPushButton(m_parent); myButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); int buttonSize = myButton->sizeHint().expandedTo(widget->sizeHint()).height(); myButton->setFixedSize(buttonSize, buttonSize); myButton->setToolTip(i18n("Open file dialog")); connect(myButton, SIGNAL(pressed()), m_parent, SLOT(_k_slotUpdateUrl())); widget->installEventFilter(m_parent); m_parent->setFocusProxy(widget); m_parent->setFocusPolicy(Qt::StrongFocus); topLayout->addWidget(myButton); connectSignals(m_parent); connect(myButton, SIGNAL(clicked()), m_parent, SLOT(_k_slotOpenDialog())); m_startDir = QUrl::fromLocalFile(QDir::currentPath()); m_startDirCustomized = false; myCompletion = new KUrlCompletion(); updateCompletionStartDir(m_startDir); setCompletionObject(myCompletion); QAction *openAction = new QAction(m_parent); openAction->setShortcut(QKeySequence::Open); m_parent->connect(openAction, SIGNAL(triggered(bool)), SLOT(_k_slotOpenDialog())); } void KUrlRequester::setUrl(const QUrl &url) { d->setText(url.toDisplayString(QUrl::PreferLocalFile)); } #ifndef KIOWIDGETS_NO_DEPRECATED void KUrlRequester::setPath(const QString &path) { d->setText(path); } #endif void KUrlRequester::setText(const QString &text) { d->setText(text); } void KUrlRequester::setStartDir(const QUrl &startDir) { d->m_startDir = startDir; d->m_startDirCustomized = true; d->updateCompletionStartDir(startDir); } void KUrlRequester::changeEvent(QEvent *e) { if (e->type() == QEvent::WindowTitleChange) { if (d->myFileDialog) { d->myFileDialog->setWindowTitle(windowTitle()); } } QWidget::changeEvent(e); } QUrl KUrlRequester::url() const { return d->url(); } QUrl KUrlRequester::startDir() const { return d->m_startDir; } QString KUrlRequester::text() const { return d->text(); } void KUrlRequester::KUrlRequesterPrivate::_k_slotOpenDialog() { if (myFileDialog) if (myFileDialog->isVisible()) { //The file dialog is already being shown, raise it and exit myFileDialog->raise(); myFileDialog->activateWindow(); return; } if (!m_fileDialogModeWasDirAndFile && (((fileDialogMode & KFile::Directory) && !(fileDialogMode & KFile::File)) || /* catch possible fileDialog()->setMode( KFile::Directory ) changes */ (myFileDialog && (myFileDialog->fileMode() == QFileDialog::Directory && myFileDialog->testOption(QFileDialog::ShowDirsOnly))))) { const QUrl openUrl = (!m_parent->url().isEmpty() && !m_parent->url().isRelative()) ? m_parent->url() : m_startDir; /* FIXME We need a new abstract interface for using KDirSelectDialog in a non-modal way */ QUrl newUrl; if (fileDialogMode & KFile::LocalOnly) { newUrl = QFileDialog::getExistingDirectoryUrl(m_parent, QString(), openUrl, QFileDialog::ShowDirsOnly, QStringList() << QStringLiteral("file")); } else { newUrl = getDirFromFileDialog(openUrl); } if (newUrl.isValid()) { m_parent->setUrl(newUrl); emit m_parent->urlSelected(url()); } } else { emit m_parent->openFileDialog(m_parent); if (((fileDialogMode & KFile::Directory) && (fileDialogMode & KFile::File)) || m_fileDialogModeWasDirAndFile) { QMenu *dirOrFileMenu = new QMenu(); QAction *fileAction = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("File")); QAction *dirAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Directory")); dirOrFileMenu->addAction(fileAction); dirOrFileMenu->addAction(dirAction); connect(fileAction, &QAction::triggered, [this]() { fileDialogMode = KFile::File; applyFileMode(m_parent->fileDialog(), fileDialogMode, fileDialogAcceptMode); m_fileDialogModeWasDirAndFile = true; createFileDialog(); }); connect(dirAction, &QAction::triggered, [this]() { fileDialogMode = KFile::Directory; applyFileMode(m_parent->fileDialog(), fileDialogMode, fileDialogAcceptMode); m_fileDialogModeWasDirAndFile = true; createFileDialog(); }); dirOrFileMenu->exec(m_parent->mapToGlobal(QPoint(m_parent->width(), m_parent->height()))); return; } createFileDialog(); } } void KUrlRequester::KUrlRequesterPrivate::_k_slotFileDialogAccepted() { if (!myFileDialog) { return; } const QUrl newUrl = myFileDialog->selectedUrls().constFirst(); if (newUrl.isValid()) { m_parent->setUrl(newUrl); emit m_parent->urlSelected(url()); // remember url as defaultStartDir and update startdir for autocompletion if (newUrl.isLocalFile() && !m_startDirCustomized) { m_startDir = newUrl.adjusted(QUrl::RemoveFilename); updateCompletionStartDir(m_startDir); } } } void KUrlRequester::setMode(KFile::Modes mode) { Q_ASSERT((mode & KFile::Files) == 0); d->fileDialogMode = mode; if ((mode & KFile::Directory) && !(mode & KFile::File)) { d->myCompletion->setMode(KUrlCompletion::DirCompletion); } if (d->myFileDialog) { d->applyFileMode(d->myFileDialog, mode, d->fileDialogAcceptMode); } } KFile::Modes KUrlRequester::mode() const { return d->fileDialogMode; } void KUrlRequester::setAcceptMode(QFileDialog::AcceptMode mode) { d->fileDialogAcceptMode = mode; if (d->myFileDialog) { d->applyFileMode(d->myFileDialog, d->fileDialogMode, mode); } } QFileDialog::AcceptMode KUrlRequester::acceptMode() const { return d->fileDialogAcceptMode; } void KUrlRequester::setFilter(const QString &filter) { d->fileDialogFilter = filter; if (d->myFileDialog) { d->myFileDialog->setNameFilters(d->kToQFilters(d->fileDialogFilter)); } } QString KUrlRequester::filter() const { return d->fileDialogFilter; } void KUrlRequester::setMimeTypeFilters(const QStringList &mimeTypes) { d->mimeTypeFilters = mimeTypes; if (d->myFileDialog) { d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters); } d->myCompletion->setMimeTypeFilters(d->mimeTypeFilters); } QStringList KUrlRequester::mimeTypeFilters() const { return d->mimeTypeFilters; } #ifndef KIOWIDGETS_NO_DEPRECATED QFileDialog *KUrlRequester::fileDialog() const { if (d->myFileDialog && ( (d->myFileDialog->fileMode() == QFileDialog::Directory && !(d->fileDialogMode & KFile::Directory)) || (d->myFileDialog->fileMode() != QFileDialog::Directory && (d->fileDialogMode & KFile::Directory)))) { delete d->myFileDialog; d->myFileDialog = nullptr; } if (!d->myFileDialog) { d->myFileDialog = new QFileDialog(window(), windowTitle()); if (!d->mimeTypeFilters.isEmpty()) { d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters); } else { d->myFileDialog->setNameFilters(d->kToQFilters(d->fileDialogFilter)); } d->applyFileMode(d->myFileDialog, d->fileDialogMode, d->fileDialogAcceptMode); d->myFileDialog->setWindowModality(d->fileDialogModality); connect(d->myFileDialog, SIGNAL(accepted()), SLOT(_k_slotFileDialogAccepted())); } return d->myFileDialog; } #endif void KUrlRequester::clear() { d->setText(QString()); } KLineEdit *KUrlRequester::lineEdit() const { return d->edit; } KComboBox *KUrlRequester::comboBox() const { return d->combo; } void KUrlRequester::KUrlRequesterPrivate::_k_slotUpdateUrl() { const QUrl visibleUrl = url(); QUrl u = visibleUrl; if (visibleUrl.isRelative()) { u = QUrl::fromLocalFile(QDir::currentPath() + QLatin1Char('/')).resolved(visibleUrl); } myButton->setURL(u); } bool KUrlRequester::eventFilter(QObject *obj, QEvent *ev) { if ((d->edit == obj) || (d->combo == obj)) { if ((ev->type() == QEvent::FocusIn) || (ev->type() == QEvent::FocusOut)) // Forward focusin/focusout events to the urlrequester; needed by file form element in khtml { QApplication::sendEvent(this, ev); } } return QWidget::eventFilter(obj, ev); } QPushButton *KUrlRequester::button() const { return d->myButton; } KUrlCompletion *KUrlRequester::completionObject() const { return d->myCompletion; } #ifndef KIOWIDGETS_NO_DEPRECATED void KUrlRequester::setClickMessage(const QString &msg) { setPlaceholderText(msg); } #endif void KUrlRequester::setPlaceholderText(const QString &msg) { if (d->edit) { d->edit->setPlaceholderText(msg); } } #ifndef KIOWIDGETS_NO_DEPRECATED QString KUrlRequester::clickMessage() const { return placeholderText(); } #endif QString KUrlRequester::placeholderText() const { if (d->edit) { return d->edit->placeholderText(); } else { return QString(); } } Qt::WindowModality KUrlRequester::fileDialogModality() const { return d->fileDialogModality; } void KUrlRequester::setFileDialogModality(Qt::WindowModality modality) { d->fileDialogModality = modality; } const KEditListWidget::CustomEditor &KUrlRequester::customEditor() { setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed)); KLineEdit *edit = d->edit; if (!edit && d->combo) { edit = qobject_cast(d->combo->lineEdit()); } #ifndef NDEBUG if (!edit) { qCWarning(KIO_WIDGETS) << "KUrlRequester's lineedit is not a KLineEdit!??\n"; } #endif d->editor.setRepresentationWidget(this); d->editor.setLineEdit(edit); return d->editor; } KUrlComboRequester::KUrlComboRequester(QWidget *parent) : KUrlRequester(new KComboBox(false), parent), d(nullptr) { } #include "moc_kurlrequester.cpp" #include "kurlrequester.moc" diff --git a/src/widgets/kurlrequesterdialog.cpp b/src/widgets/kurlrequesterdialog.cpp index 0c14e27a..b7ec26ae 100644 --- a/src/widgets/kurlrequesterdialog.cpp +++ b/src/widgets/kurlrequesterdialog.cpp @@ -1,143 +1,143 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Wilco Greven 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 "kurlrequesterdialog.h" #include #include -#include #include +#include #include #include #include #include #include class KUrlRequesterDialogPrivate { public: explicit KUrlRequesterDialogPrivate(KUrlRequesterDialog *qq) : q(qq) { } KUrlRequesterDialog * const q; void initDialog(const QString &text, const QUrl &url); // slots void _k_slotTextChanged(const QString &); KUrlRequester *urlRequester; QDialogButtonBox *buttonBox; }; KUrlRequesterDialog::KUrlRequesterDialog(const QUrl &urlName, QWidget *parent) : QDialog(parent), d(new KUrlRequesterDialogPrivate(this)) { d->initDialog(i18n("Location:"), urlName); } KUrlRequesterDialog::KUrlRequesterDialog(const QUrl &urlName, const QString &_text, QWidget *parent) : QDialog(parent), d(new KUrlRequesterDialogPrivate(this)) { d->initDialog(_text, urlName); } KUrlRequesterDialog::~KUrlRequesterDialog() { delete d; } void KUrlRequesterDialogPrivate::initDialog(const QString &text, const QUrl &urlName) { QVBoxLayout *topLayout = new QVBoxLayout; q->setLayout(topLayout); QLabel *label = new QLabel(text, q); topLayout->addWidget(label); urlRequester = new KUrlRequester(urlName, q); urlRequester->setMinimumWidth(urlRequester->sizeHint().width() * 3); topLayout->addWidget(urlRequester); urlRequester->setFocus(); QObject::connect(urlRequester->lineEdit(), SIGNAL(textChanged(QString)), q, SLOT(_k_slotTextChanged(QString))); /* KFile::Mode mode = static_cast( KFile::File | KFile::ExistingOnly ); urlRequester_->setMode( mode ); */ buttonBox = new QDialogButtonBox(q); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QObject::connect(buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept); QObject::connect(buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); topLayout->addWidget(buttonBox); _k_slotTextChanged(urlName.toString()); } void KUrlRequesterDialogPrivate::_k_slotTextChanged(const QString &text) { bool state = !text.trimmed().isEmpty(); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(state); } QUrl KUrlRequesterDialog::selectedUrl() const { if (result() == QDialog::Accepted) { return d->urlRequester->url(); } else { return QUrl(); } } QUrl KUrlRequesterDialog::getUrl(const QUrl &dir, QWidget *parent, const QString &caption) { KUrlRequesterDialog dlg(dir, parent); dlg.setWindowTitle(caption.isEmpty() ? i18n("Open") : caption); dlg.exec(); const QUrl &url = dlg.selectedUrl(); if (url.isValid()) { KRecentDocument::add(url); } return url; } #ifndef KIOWIDGETS_NO_DEPRECATED QFileDialog *KUrlRequesterDialog::fileDialog() { return d->urlRequester->fileDialog(); } #endif KUrlRequester *KUrlRequesterDialog::urlRequester() { return d->urlRequester; } #include "moc_kurlrequesterdialog.cpp" diff --git a/src/widgets/openfilemanagerwindowjob.cpp b/src/widgets/openfilemanagerwindowjob.cpp index 610ed550..370da68d 100644 --- a/src/widgets/openfilemanagerwindowjob.cpp +++ b/src/widgets/openfilemanagerwindowjob.cpp @@ -1,157 +1,156 @@ /* This file is part of the KDE libraries Copyright (C) 2016 Kai Uwe Broulik 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 "openfilemanagerwindowjob.h" #include "openfilemanagerwindowjob_p.h" #include #include #include #include -#include #include #include namespace KIO { class OpenFileManagerWindowJobPrivate { public: OpenFileManagerWindowJobPrivate() : strategy(nullptr) { } ~OpenFileManagerWindowJobPrivate() { delete strategy; } QList highlightUrls; QByteArray startupId; AbstractOpenFileManagerWindowStrategy *strategy; }; OpenFileManagerWindowJob::OpenFileManagerWindowJob(QObject *parent) : KJob(parent) , d(new OpenFileManagerWindowJobPrivate()) { #ifdef Q_OS_LINUX d->strategy = new OpenFileManagerWindowDBusStrategy(this); #else d->strategy = new OpenFileManagerWindowKRunStrategy(this); #endif } OpenFileManagerWindowJob::~OpenFileManagerWindowJob() { delete d; } QList OpenFileManagerWindowJob::highlightUrls() const { return d->highlightUrls; } void OpenFileManagerWindowJob::setHighlightUrls(const QList &highlightUrls) { d->highlightUrls = highlightUrls; } QByteArray OpenFileManagerWindowJob::startupId() const { return d->startupId; } void OpenFileManagerWindowJob::setStartupId(const QByteArray &startupId) { d->startupId = startupId; } void OpenFileManagerWindowJob::start() { if (d->highlightUrls.isEmpty()) { setError(NoValidUrlsError); emitResult(); return; } d->strategy->start(d->highlightUrls, d->startupId); } OpenFileManagerWindowJob *highlightInFileManager(const QList &urls, const QByteArray &asn) { auto *job = new OpenFileManagerWindowJob(); job->setHighlightUrls(urls); job->setStartupId(asn); job->start(); return job; } void OpenFileManagerWindowDBusStrategy::start(const QList &urls, const QByteArray &asn) { // see the spec at: https://www.freedesktop.org/wiki/Specifications/file-manager-interface/ QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.FileManager1"), QStringLiteral("/org/freedesktop/FileManager1"), QStringLiteral("org.freedesktop.FileManager1"), QStringLiteral("ShowItems")); msg << QUrl::toStringList(urls) << QString::fromUtf8(asn); QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, job); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, job, [=](QDBusPendingCallWatcher *watcher) { QDBusPendingReply reply = *watcher; watcher->deleteLater(); if (reply.isError()) { // Try the KRun strategy as fallback, also calls emitResult inside OpenFileManagerWindowKRunStrategy kRunStrategy(job); kRunStrategy.start(urls, asn); return; } emitResultProxy(); }); } void OpenFileManagerWindowKRunStrategy::start(const QList &urls, const QByteArray &asn) { if (!KRun::runUrl(urls.value(0).adjusted(QUrl::RemoveFilename), QStringLiteral("inode/directory"), KJobWidgets::window(job), // window KRun::RunFlags(), QString(), // suggested file name asn)) { emitResultProxy(OpenFileManagerWindowJob::LaunchFailedError); return; } emitResultProxy(); } } // namespace KIO diff --git a/src/widgets/pastedialog.cpp b/src/widgets/pastedialog.cpp index beea2b8c..a47cc5ce 100644 --- a/src/widgets/pastedialog.cpp +++ b/src/widgets/pastedialog.cpp @@ -1,97 +1,97 @@ /* This file is part of the KDE libraries Copyright (C) 2005 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 "pastedialog_p.h" #include #include #include #include #include -#include #include #include +#include KIO::PasteDialog::PasteDialog(const QString &caption, const QString &label, const QString &value, const QStringList &items, QWidget *parent, bool clipboard) : QDialog(parent) { setWindowTitle(caption); setModal(true); QVBoxLayout *topLayout = new QVBoxLayout; setLayout(topLayout); QFrame *frame = new QFrame(this); topLayout->addWidget(frame); QVBoxLayout *layout = new QVBoxLayout(frame); m_label = new QLabel(label, frame); layout->addWidget(m_label); m_lineEdit = new QLineEdit(value, frame); layout->addWidget(m_lineEdit); m_lineEdit->setFocus(); m_label->setBuddy(m_lineEdit); layout->addWidget(new QLabel(i18n("Data format:"), frame)); m_comboBox = new KComboBox(frame); m_comboBox->addItems(items); layout->addWidget(m_comboBox); layout->addStretch(); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); topLayout->addWidget(buttonBox); //connect( m_lineEdit, SIGNAL(textChanged(QString)), // SLOT(slotEditTextChanged(QString)) ); //connect( this, SIGNAL(user1Clicked()), m_lineEdit, SLOT(clear()) ); //slotEditTextChanged( value ); setMinimumWidth(350); m_clipboardChanged = false; if (clipboard) connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &PasteDialog::slotClipboardDataChanged); } void KIO::PasteDialog::slotClipboardDataChanged() { m_clipboardChanged = true; } QString KIO::PasteDialog::lineEditText() const { return m_lineEdit->text(); } int KIO::PasteDialog::comboItem() const { return m_comboBox->currentIndex(); } diff --git a/src/widgets/pastejob.cpp b/src/widgets/pastejob.cpp index 3b5d87ba..70ba7976 100644 --- a/src/widgets/pastejob.cpp +++ b/src/widgets/pastejob.cpp @@ -1,106 +1,104 @@ /* This file is part of the KDE libraries Copyright (C) 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 "pastejob.h" #include "pastejob_p.h" #include "paste.h" -#include -#include #include #include #include #include #include #include #include using namespace KIO; extern KIO::Job *pasteMimeDataImpl(const QMimeData *mimeData, const QUrl &destUrl, const QString &dialogText, QWidget *widget, bool clipboard); PasteJob::PasteJob(PasteJobPrivate &dd) : Job(dd) { QTimer::singleShot(0, this, SLOT(slotStart())); } PasteJob::~PasteJob() { } void PasteJobPrivate::slotStart() { Q_Q(PasteJob); const bool move = KIO::isClipboardDataCut(m_mimeData); KIO::Job *job = nullptr; if (m_mimeData->hasUrls()) { const QList urls = KUrlMimeData::urlsFromMimeData(m_mimeData, KUrlMimeData::PreferLocalUrls); if (!urls.isEmpty()) { KIO::CopyJob *copyJob; if (move) { copyJob = KIO::move(urls, m_destDir, m_flags); } else { copyJob = KIO::copy(urls, m_destDir, m_flags); } QObject::connect(copyJob, SIGNAL(copyingDone(KIO::Job*,QUrl,QUrl,QDateTime,bool,bool)), q, SLOT(slotCopyingDone(KIO::Job*,QUrl,QUrl))); QObject::connect(copyJob, SIGNAL(copyingLinkDone(KIO::Job*,QUrl,QString,QUrl)), q, SLOT(slotCopyingLinkDone(KIO::Job*,QUrl,QString,QUrl))); KIO::FileUndoManager::self()->recordJob(move ? KIO::FileUndoManager::Move : KIO::FileUndoManager::Copy, QList(), m_destDir, copyJob); job = copyJob; } } else { const QString dialogText = m_clipboard ? i18n("Filename for clipboard content:") : i18n("Filename for dropped contents:"); job = pasteMimeDataImpl(m_mimeData, m_destDir, dialogText, KJobWidgets::window(q), m_clipboard); if (KIO::SimpleJob* simpleJob = qobject_cast(job)) { KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Put, QList(), simpleJob->url(), job); } } if (job) { q->addSubjob(job); } else { q->setError(KIO::ERR_NO_CONTENT); q->emitResult(); } } void PasteJob::slotResult(KJob *job) { if (job->error()) { KIO::Job::slotResult(job); // will set the error and emit result(this) return; } KIO::SimpleJob *simpleJob = qobject_cast(job); if (simpleJob) { emit itemCreated(simpleJob->url()); } removeSubjob(job); emitResult(); } PasteJob * KIO::paste(const QMimeData *mimeData, const QUrl &destDir, JobFlags flags) { return PasteJobPrivate::newJob(mimeData, destDir, flags, true /*clipboard*/); } #include "moc_pastejob.cpp" diff --git a/src/widgets/renamedialog.cpp b/src/widgets/renamedialog.cpp index d12d1b9d..d6d01a83 100644 --- a/src/widgets/renamedialog.cpp +++ b/src/widgets/renamedialog.cpp @@ -1,676 +1,674 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow 1999 - 2008 David Faure 2001, 2006 Holger Freyther This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kio/renamedialog.h" #include "kio_widgets_debug.h" #include "../pathhelpers_p.h" #include #include #include -#include #include -#include #include -#include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #if 0 #include #endif #include #include using namespace KIO; static QLabel *createLabel(QWidget *parent, const QString &text, bool containerTitle = false) { QLabel *label = new QLabel(parent); if (containerTitle) { QFont font = label->font(); font.setBold(true); label->setFont(font); } label->setAlignment(Qt::AlignHCenter); label->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); label->setText(text); return label; } static QLabel *createDateLabel(QWidget *parent, const KFileItem &item) { const QString text = i18n("Date: %1", item.timeString(KFileItem::ModificationTime)); return createLabel(parent, text); } static QLabel *createSizeLabel(QWidget *parent, const KFileItem &item) { const QString text = i18n("Size: %1", KIO::convertSize(item.size())); return createLabel(parent, text); } static KSqueezedTextLabel *createSqueezedLabel(QWidget *parent, const QString &text) { KSqueezedTextLabel *label = new KSqueezedTextLabel(text, parent); label->setAlignment(Qt::AlignHCenter); label->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); return label; } /** @internal */ class Q_DECL_HIDDEN RenameDialog::RenameDialogPrivate { public: RenameDialogPrivate() { bCancel = nullptr; bRename = bSkip = bOverwrite = nullptr; bResume = bSuggestNewName = nullptr; bApplyAll = nullptr; m_pLineEdit = nullptr; m_srcPendingPreview = false; m_destPendingPreview = false; m_srcPreview = nullptr; m_destPreview = nullptr; m_srcArea = nullptr; m_destArea = nullptr; } void setRenameBoxText(const QString &fileName) { // sets the text in file name line edit box, selecting the filename (but not the extension if there is one). QMimeDatabase db; const QString extension = db.suffixForFileName(fileName); m_pLineEdit->setText(fileName); if (!extension.isEmpty()) { const int selectionLength = fileName.length() - extension.length() - 1; m_pLineEdit->setSelection(0, selectionLength); } else { m_pLineEdit->selectAll(); } } QPushButton *bCancel; QPushButton *bRename; QPushButton *bSkip; QPushButton *bOverwrite; QPushButton *bResume; QPushButton *bSuggestNewName; QCheckBox *bApplyAll; QLineEdit *m_pLineEdit; QUrl src; QUrl dest; bool m_srcPendingPreview; bool m_destPendingPreview; QLabel *m_srcPreview; QLabel *m_destPreview; QScrollArea *m_srcArea; QScrollArea *m_destArea; KFileItem srcItem; KFileItem destItem; }; RenameDialog::RenameDialog(QWidget *parent, const QString &_caption, const QUrl &_src, const QUrl &_dest, RenameDialog_Options _options, KIO::filesize_t sizeSrc, KIO::filesize_t sizeDest, const QDateTime &ctimeSrc, const QDateTime &ctimeDest, const QDateTime &mtimeSrc, const QDateTime &mtimeDest) : QDialog(parent), d(new RenameDialogPrivate) { setObjectName(QStringLiteral("KIO::RenameDialog")); d->src = _src; d->dest = _dest; setWindowTitle(_caption); d->bCancel = new QPushButton(this); KGuiItem::assign(d->bCancel, KStandardGuiItem::cancel()); connect(d->bCancel, &QAbstractButton::clicked, this, &RenameDialog::cancelPressed); if (_options & RenameDialog_MultipleItems) { d->bApplyAll = new QCheckBox(i18n("Appl&y to All"), this); d->bApplyAll->setToolTip((_options & RenameDialog_IsDirectory) ? i18n("When this is checked the button pressed will be applied to all subsequent folder conflicts for the remainder of the current job.\nUnless you press Skip you will still be prompted in case of a conflict with an existing file in the directory.") : i18n("When this is checked the button pressed will be applied to all subsequent conflicts for the remainder of the current job.")); connect(d->bApplyAll, &QAbstractButton::clicked, this, &RenameDialog::applyAllPressed); } if (!(_options & RenameDialog_NoRename)) { d->bRename = new QPushButton(i18n("&Rename"), this); d->bRename->setEnabled(false); d->bSuggestNewName = new QPushButton(i18n("Suggest New &Name"), this); connect(d->bSuggestNewName, &QAbstractButton::clicked, this, &RenameDialog::suggestNewNamePressed); connect(d->bRename, &QAbstractButton::clicked, this, &RenameDialog::renamePressed); } if ((_options & RenameDialog_MultipleItems) && (_options & RenameDialog_Skip)) { d->bSkip = new QPushButton(i18n("&Skip"), this); d->bSkip->setToolTip((_options & RenameDialog_IsDirectory) ? i18n("Do not copy or move this folder, skip to the next item instead") : i18n("Do not copy or move this file, skip to the next item instead")); connect(d->bSkip, &QAbstractButton::clicked, this, &RenameDialog::skipPressed); } if (_options & RenameDialog_Overwrite) { const QString text = (_options & RenameDialog_IsDirectory) ? i18nc("Write files into an existing folder", "&Write Into") : i18n("&Overwrite"); d->bOverwrite = new QPushButton(text, this); d->bOverwrite->setToolTip(i18n("Files and folders will be copied into the existing directory, alongside its existing contents.\nYou will be prompted again in case of a conflict with an existing file in the directory.")); connect(d->bOverwrite, &QAbstractButton::clicked, this, &RenameDialog::overwritePressed); } if (_options & RenameDialog_Resume) { d->bResume = new QPushButton(i18n("&Resume"), this); connect(d->bResume, &QAbstractButton::clicked, this, &RenameDialog::resumePressed); } QVBoxLayout *pLayout = new QVBoxLayout(this); pLayout->addStrut(400); // makes dlg at least that wide // User tries to overwrite a file with itself ? if (_options & RenameDialog_OverwriteItself) { QLabel *lb = new QLabel(i18n("This action would overwrite '%1' with itself.\n" "Please enter a new file name:", KStringHandler::csqueeze(d->src.toDisplayString(QUrl::PreferLocalFile), 100)), this); lb->setTextFormat(Qt::PlainText); d->bRename->setText(i18n("C&ontinue")); pLayout->addWidget(lb); } else if (_options & RenameDialog_Overwrite) { if (d->src.isLocalFile()) { d->srcItem = KFileItem(d->src); } else { UDSEntry srcUds; srcUds.fastInsert(UDSEntry::UDS_NAME, d->src.fileName()); srcUds.fastInsert(UDSEntry::UDS_MODIFICATION_TIME, mtimeSrc.toMSecsSinceEpoch() / 1000); srcUds.fastInsert(UDSEntry::UDS_CREATION_TIME, ctimeSrc.toMSecsSinceEpoch() / 1000); srcUds.fastInsert(UDSEntry::UDS_SIZE, sizeSrc); d->srcItem = KFileItem(srcUds, d->src); } if (d->dest.isLocalFile()) { d->destItem = KFileItem(d->dest); } else { UDSEntry destUds; destUds.fastInsert(UDSEntry::UDS_NAME, d->dest.fileName()); destUds.fastInsert(UDSEntry::UDS_MODIFICATION_TIME, mtimeDest.toMSecsSinceEpoch() / 1000); destUds.fastInsert(UDSEntry::UDS_CREATION_TIME, ctimeDest.toMSecsSinceEpoch() / 1000); destUds.fastInsert(UDSEntry::UDS_SIZE, sizeDest); d->destItem = KFileItem(destUds, d->dest); } d->m_srcPreview = createLabel(parent, QString()); d->m_destPreview = createLabel(parent, QString()); d->m_srcPreview->setMinimumHeight(KIconLoader::SizeEnormous); d->m_destPreview->setMinimumHeight(KIconLoader::SizeEnormous); d->m_srcPreview->setAlignment(Qt::AlignCenter); d->m_destPreview->setAlignment(Qt::AlignCenter); d->m_srcPendingPreview = true; d->m_destPendingPreview = true; // widget d->m_srcArea = createContainerLayout(parent, d->srcItem, d->m_srcPreview); d->m_destArea = createContainerLayout(parent, d->destItem, d->m_destPreview); connect(d->m_srcArea->verticalScrollBar(), &QAbstractSlider::valueChanged, d->m_destArea->verticalScrollBar(), &QAbstractSlider::setValue); connect(d->m_destArea->verticalScrollBar(), &QAbstractSlider::valueChanged, d->m_srcArea->verticalScrollBar(), &QAbstractSlider::setValue); connect(d->m_srcArea->horizontalScrollBar(), &QAbstractSlider::valueChanged, d->m_destArea->horizontalScrollBar(), &QAbstractSlider::setValue); connect(d->m_destArea->horizontalScrollBar(), &QAbstractSlider::valueChanged, d->m_srcArea->horizontalScrollBar(), &QAbstractSlider::setValue); // create layout QGridLayout *gridLayout = new QGridLayout(); pLayout->addLayout(gridLayout); int gridRow = 0; QLabel *titleLabel = new QLabel(i18n("This action will overwrite the destination."), this); gridLayout->addWidget(titleLabel, gridRow, 0, 1, 2); // takes the complete first line if (mtimeDest > mtimeSrc) { QLabel *warningLabel = new QLabel(i18n("Warning, the destination is more recent."), this); gridLayout->addWidget(warningLabel, ++gridRow, 0, 1, 2); } gridLayout->setRowMinimumHeight(++gridRow, 15); // spacer QLabel *srcTitle = createLabel(parent, i18n("Source"), true); gridLayout->addWidget(srcTitle, ++gridRow, 0); QLabel *destTitle = createLabel(parent, i18n("Destination"), true); gridLayout->addWidget(destTitle, gridRow, 1); QLabel *srcUrlLabel = createSqueezedLabel(parent, d->src.toDisplayString(QUrl::PreferLocalFile)); srcUrlLabel->setTextFormat(Qt::PlainText); gridLayout->addWidget(srcUrlLabel, ++gridRow, 0); QLabel *destUrlLabel = createSqueezedLabel(parent, d->dest.toDisplayString(QUrl::PreferLocalFile)); destUrlLabel->setTextFormat(Qt::PlainText); gridLayout->addWidget(destUrlLabel, gridRow, 1); // The labels containing previews or icons gridLayout->addWidget(d->m_srcArea, ++gridRow, 0); gridLayout->addWidget(d->m_destArea, gridRow, 1); QLabel *srcDateLabel = createDateLabel(parent, d->srcItem); gridLayout->addWidget(srcDateLabel, ++gridRow, 0); QLabel *destDateLabel = createDateLabel(parent, d->destItem); gridLayout->addWidget(destDateLabel, gridRow, 1); QLabel *srcSizeLabel = createSizeLabel(parent, d->srcItem); gridLayout->addWidget(srcSizeLabel, ++gridRow, 0); QLabel *destSizeLabel = createSizeLabel(parent, d->destItem); gridLayout->addWidget(destSizeLabel, gridRow, 1); } else { // This is the case where we don't want to allow overwriting, the existing // file must be preserved (e.g. when renaming). QString sentence1; if (mtimeDest < mtimeSrc) { sentence1 = i18n("An older item named '%1' already exists.", d->dest.toDisplayString(QUrl::PreferLocalFile)); } else if (mtimeDest == mtimeSrc) { sentence1 = i18n("A similar file named '%1' already exists.", d->dest.toDisplayString(QUrl::PreferLocalFile)); } else { sentence1 = i18n("A more recent item named '%1' already exists.", d->dest.toDisplayString(QUrl::PreferLocalFile)); } QLabel *lb = new KSqueezedTextLabel(sentence1, this); lb->setTextFormat(Qt::PlainText); pLayout->addWidget(lb); } if (!(_options & RenameDialog_OverwriteItself) && !(_options & RenameDialog_NoRename)) { if (_options & RenameDialog_Overwrite) { pLayout->addSpacing(15); // spacer } QLabel *lb2 = new QLabel(i18n("Rename:"), this); pLayout->addWidget(lb2); } QHBoxLayout *layout2 = new QHBoxLayout(); pLayout->addLayout(layout2); d->m_pLineEdit = new QLineEdit(this); layout2->addWidget(d->m_pLineEdit); if (d->bRename) { const QString fileName = d->dest.fileName(); d->setRenameBoxText(KIO::decodeFileName(fileName)); connect(d->m_pLineEdit, &QLineEdit::textChanged, this, &RenameDialog::enableRenameButton); d->m_pLineEdit->setFocus(); } else { d->m_pLineEdit->hide(); } if (d->bSuggestNewName) { layout2->addWidget(d->bSuggestNewName); setTabOrder(d->m_pLineEdit, d->bSuggestNewName); } KSeparator *separator = new KSeparator(this); pLayout->addWidget(separator); QHBoxLayout *layout = new QHBoxLayout(); pLayout->addLayout(layout); layout->addStretch(1); if (d->bApplyAll) { layout->addWidget(d->bApplyAll); setTabOrder(d->bApplyAll, d->bCancel); } if (d->bRename) { layout->addWidget(d->bRename); setTabOrder(d->bRename, d->bCancel); } if (d->bSkip) { layout->addWidget(d->bSkip); setTabOrder(d->bSkip, d->bCancel); } if (d->bOverwrite) { layout->addWidget(d->bOverwrite); setTabOrder(d->bOverwrite, d->bCancel); } if (d->bResume) { layout->addWidget(d->bResume); setTabOrder(d->bResume, d->bCancel); } d->bCancel->setDefault(true); layout->addWidget(d->bCancel); resize(sizeHint()); #if 1 // without kfilemetadata // don't wait for kfilemetadata, but wait until the layouting is done if (_options & RenameDialog_Overwrite) { QMetaObject::invokeMethod(this, "resizePanels", Qt::QueuedConnection); } #endif } RenameDialog::~RenameDialog() { delete d; } void RenameDialog::enableRenameButton(const QString &newDest) { if (newDest != KIO::decodeFileName(d->dest.fileName()) && !newDest.isEmpty()) { d->bRename->setEnabled(true); d->bRename->setDefault(true); if (d->bOverwrite) { d->bOverwrite->setEnabled(false); // prevent confusion (#83114) } } else { d->bRename->setEnabled(false); if (d->bOverwrite) { d->bOverwrite->setEnabled(true); } } } QUrl RenameDialog::newDestUrl() { const QString fileName = d->m_pLineEdit->text(); QUrl newDest = d->dest.adjusted(QUrl::RemoveFilename); // keeps trailing slash newDest.setPath(newDest.path() + KIO::encodeFileName(fileName)); return newDest; } QUrl RenameDialog::autoDestUrl() const { const QUrl destDirectory = d->dest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); const QString newName = KFileUtils::suggestName(destDirectory, d->dest.fileName()); QUrl newDest(destDirectory); newDest.setPath(concatPaths(newDest.path(), newName)); return newDest; } void RenameDialog::cancelPressed() { done(Result_Cancel); } // Rename void RenameDialog::renamePressed() { if (d->m_pLineEdit->text().isEmpty()) { return; } if (d->bApplyAll && d->bApplyAll->isChecked()) { done(Result_AutoRename); } else { const QUrl u = newDestUrl(); if (!u.isValid()) { KMessageBox::error(this, i18n("Malformed URL\n%1", u.errorString())); qCWarning(KIO_WIDGETS) << u.errorString(); return; } done(Result_Rename); } } #ifndef KIOWIDGETS_NO_DEPRECATED QString RenameDialog::suggestName(const QUrl &baseURL, const QString &oldName) { return KFileUtils::suggestName(baseURL, oldName); } #endif // Propose button clicked void RenameDialog::suggestNewNamePressed() { /* no name to play with */ if (d->m_pLineEdit->text().isEmpty()) { return; } QUrl destDirectory = d->dest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); d->setRenameBoxText(KFileUtils::suggestName(destDirectory, d->m_pLineEdit->text())); } void RenameDialog::skipPressed() { if (d->bApplyAll && d->bApplyAll->isChecked()) { done(Result_AutoSkip); } else { done(Result_Skip); } } void RenameDialog::autoSkipPressed() { done(Result_AutoSkip); } void RenameDialog::overwritePressed() { if (d->bApplyAll && d->bApplyAll->isChecked()) { done(Result_OverwriteAll); } else { done(Result_Overwrite); } } void RenameDialog::overwriteAllPressed() { done(Result_OverwriteAll); } void RenameDialog::resumePressed() { if (d->bApplyAll && d->bApplyAll->isChecked()) { done(Result_ResumeAll); } else { done(Result_Resume); } } void RenameDialog::resumeAllPressed() { done(Result_ResumeAll); } void RenameDialog::applyAllPressed() { if (d->bApplyAll && d->bApplyAll->isChecked()) { d->m_pLineEdit->setText(KIO::decodeFileName(d->dest.fileName())); d->m_pLineEdit->setEnabled(false); if (d->bRename) { d->bRename->setEnabled(true); } if (d->bSuggestNewName) { d->bSuggestNewName->setEnabled(false); } } else { d->m_pLineEdit->setEnabled(true); if (d->bRename) { d->bRename->setEnabled(false); } if (d->bSuggestNewName) { d->bSuggestNewName->setEnabled(true); } } } void RenameDialog::showSrcIcon(const KFileItem &fileitem) { // The preview job failed, show a standard file icon. d->m_srcPendingPreview = false; const int size = d->m_srcPreview->height(); const QPixmap pix = KIconLoader::global()->loadMimeTypeIcon(fileitem.iconName(), KIconLoader::Desktop, size); d->m_srcPreview->setPixmap(pix); } void RenameDialog::showDestIcon(const KFileItem &fileitem) { // The preview job failed, show a standard file icon. d->m_destPendingPreview = false; const int size = d->m_destPreview->height(); const QPixmap pix = KIconLoader::global()->loadMimeTypeIcon(fileitem.iconName(), KIconLoader::Desktop, size); d->m_destPreview->setPixmap(pix); } void RenameDialog::showSrcPreview(const KFileItem &fileitem, const QPixmap &pixmap) { Q_UNUSED(fileitem); if (d->m_srcPendingPreview) { d->m_srcPreview->setPixmap(pixmap); d->m_srcPendingPreview = false; } } void RenameDialog::showDestPreview(const KFileItem &fileitem, const QPixmap &pixmap) { Q_UNUSED(fileitem); if (d->m_destPendingPreview) { d->m_destPreview->setPixmap(pixmap); d->m_destPendingPreview = false; } } void RenameDialog::resizePanels() { Q_ASSERT(d->m_srcArea != nullptr); Q_ASSERT(d->m_destArea != nullptr); Q_ASSERT(d->m_srcPreview != nullptr); Q_ASSERT(d->m_destPreview != nullptr); // using QDesktopWidget geometry as Kephal isn't accessible here in kdelibs const QSize screenSize = QApplication::desktop()->availableGeometry(this).size(); QSize halfSize = d->m_srcArea->widget()->sizeHint().expandedTo(d->m_destArea->widget()->sizeHint()); const QSize currentSize = d->m_srcArea->size().expandedTo(d->m_destArea->size()); const int maxHeightPossible = screenSize.height() - (size().height() - currentSize.height()); QSize maxHalfSize = QSize(screenSize.width() / qreal(2.1), maxHeightPossible * qreal(0.9)); if (halfSize.height() > maxHalfSize.height() && halfSize.width() <= maxHalfSize.width() + d->m_srcArea->verticalScrollBar()->width()) { halfSize.rwidth() += d->m_srcArea->verticalScrollBar()->width(); maxHalfSize.rwidth() += d->m_srcArea->verticalScrollBar()->width(); } d->m_srcArea->setMinimumSize(halfSize.boundedTo(maxHalfSize)); d->m_destArea->setMinimumSize(halfSize.boundedTo(maxHalfSize)); KIO::PreviewJob *srcJob = KIO::filePreview(KFileItemList() << d->srcItem, QSize(d->m_srcPreview->width() * qreal(0.9), d->m_srcPreview->height())); srcJob->setScaleType(KIO::PreviewJob::Unscaled); KIO::PreviewJob *destJob = KIO::filePreview(KFileItemList() << d->destItem, QSize(d->m_destPreview->width() * qreal(0.9), d->m_destPreview->height())); destJob->setScaleType(KIO::PreviewJob::Unscaled); connect(srcJob, &PreviewJob::gotPreview, this, &RenameDialog::showSrcPreview); connect(destJob, &PreviewJob::gotPreview, this, &RenameDialog::showDestPreview); connect(srcJob, &PreviewJob::failed, this, &RenameDialog::showSrcIcon); connect(destJob, &PreviewJob::failed, this, &RenameDialog::showDestIcon); } QScrollArea *RenameDialog::createContainerLayout(QWidget *parent, const KFileItem &item, QLabel *preview) { KFileItemList itemList; itemList << item; #if 0 // PENDING // KFileMetaDataWidget was deprecated for a Nepomuk widget, which is itself deprecated... // If we still want metadata shown, we need a plugin that fetches data from KFileMetaData::ExtractorCollection KFileMetaDataWidget *metaWidget = new KFileMetaDataWidget(this); metaWidget->setReadOnly(true); metaWidget->setItems(itemList); // ### This is going to call resizePanels twice! Need to split it up to do preview job only once on each side connect(metaWidget, SIGNAL(metaDataRequestFinished(KFileItemList)), this, SLOT(resizePanels())); #endif // Encapsulate the MetaDataWidgets inside a container with stretch at the bottom. // This prevents that the meta data widgets get vertically stretched // in the case where the height of m_metaDataArea > m_metaDataWidget. QWidget *widgetContainer = new QWidget(parent); QVBoxLayout *containerLayout = new QVBoxLayout(widgetContainer); containerLayout->setContentsMargins(0, 0, 0, 0); containerLayout->setSpacing(0); containerLayout->addWidget(preview); #if 0 // PENDING containerLayout->addWidget(metaWidget); #endif containerLayout->addStretch(1); QScrollArea *metaDataArea = new QScrollArea(parent); metaDataArea->setWidget(widgetContainer); metaDataArea->setWidgetResizable(true); metaDataArea->setFrameShape(QFrame::NoFrame); return metaDataArea; } diff --git a/src/widgets/skipdialog.cpp b/src/widgets/skipdialog.cpp index 340e06f8..d176440b 100644 --- a/src/widgets/skipdialog.cpp +++ b/src/widgets/skipdialog.cpp @@ -1,91 +1,91 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kio/skipdialog.h" #include #include #include #include #include -#include #include +#include #include #include using namespace KIO; SkipDialog::SkipDialog(QWidget *parent, KIO::SkipDialog_Options options, const QString &_error_text) : QDialog(parent), d(nullptr) { setWindowTitle(i18n("Information")); QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); layout->addWidget(new QLabel(_error_text, this)); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); layout->addWidget(buttonBox); QPushButton *retryButton = new QPushButton(i18n("Retry")); connect(retryButton, &QAbstractButton::clicked, this, &SkipDialog::retryPressed); buttonBox->addButton(retryButton, QDialogButtonBox::ActionRole); if (options & SkipDialog_MultipleItems) { QPushButton *skipButton = new QPushButton(i18n("Skip")); connect(skipButton, &QAbstractButton::clicked, this, &SkipDialog::skipPressed); buttonBox->addButton(skipButton, QDialogButtonBox::ActionRole); QPushButton *autoSkipButton = new QPushButton(i18n("AutoSkip")); connect(autoSkipButton, &QAbstractButton::clicked, this, &SkipDialog::autoSkipPressed); buttonBox->addButton(autoSkipButton, QDialogButtonBox::ActionRole); } buttonBox->addButton(QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::rejected, this, &SkipDialog::cancelPressed); resize(sizeHint()); } SkipDialog::~SkipDialog() { } void SkipDialog::cancelPressed() { done(KIO::Result_Cancel); } void SkipDialog::skipPressed() { done(KIO::Result_Skip); } void SkipDialog::autoSkipPressed() { done(KIO::Result_AutoSkip); } void SkipDialog::retryPressed() { done(KIO::Result_Retry); } diff --git a/tests/kdirlistertest_gui.cpp b/tests/kdirlistertest_gui.cpp index 034ad536..6cc1e779 100644 --- a/tests/kdirlistertest_gui.cpp +++ b/tests/kdirlistertest_gui.cpp @@ -1,165 +1,165 @@ /* This file is part of the KDE desktop environment Copyright (C) 2001, 2002 Michael Brade This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kdirlistertest_gui.h" #include -#include #include #include +#include #include #include KDirListerTest::KDirListerTest(QWidget *parent) : QWidget(parent) { lister = new KDirLister(this); debug = new PrintSignals; QVBoxLayout *layout = new QVBoxLayout(this); QPushButton *startH = new QPushButton(QStringLiteral("Start listing Home"), this); QPushButton *startR = new QPushButton(QStringLiteral("Start listing Root"), this); QPushButton *test = new QPushButton(QStringLiteral("Many"), this); QPushButton *startT = new QPushButton(QStringLiteral("tarfile"), this); layout->addWidget(startH); layout->addWidget(startR); layout->addWidget(startT); layout->addWidget(test); resize(layout->sizeHint()); connect(startR, &QAbstractButton::clicked, this, &KDirListerTest::startRoot); connect(startH, &QAbstractButton::clicked, this, &KDirListerTest::startHome); connect(startT, &QAbstractButton::clicked, this, &KDirListerTest::startTar); connect(test, &QAbstractButton::clicked, this, &KDirListerTest::test); connect(lister, &KCoreDirLister::started, debug, &PrintSignals::started); connect(lister, SIGNAL(completed()), debug, SLOT(completed())); connect(lister, SIGNAL(completed(QUrl)), debug, SLOT(completed(QUrl))); connect(lister, SIGNAL(canceled()), debug, SLOT(canceled())); connect(lister, SIGNAL(canceled(QUrl)), debug, SLOT(canceled(QUrl))); connect(lister, SIGNAL(redirection(QUrl)), debug, SLOT(redirection(QUrl))); connect(lister, SIGNAL(redirection(QUrl,QUrl)), debug, SLOT(redirection(QUrl,QUrl))); connect(lister, SIGNAL(clear()), debug, SLOT(clear())); connect(lister, &KCoreDirLister::newItems, debug, &PrintSignals::newItems); connect(lister, &KCoreDirLister::itemsFilteredByMime, debug, &PrintSignals::itemsFilteredByMime); connect(lister, &KCoreDirLister::itemsDeleted, debug, &PrintSignals::itemsDeleted); connect(lister, &KCoreDirLister::refreshItems, debug, &PrintSignals::refreshItems); connect(lister, &KCoreDirLister::infoMessage, debug, &PrintSignals::infoMessage); connect(lister, &KCoreDirLister::percent, debug, &PrintSignals::percent); connect(lister, &KCoreDirLister::totalSize, debug, &PrintSignals::totalSize); connect(lister, &KCoreDirLister::processedSize, debug, &PrintSignals::processedSize); connect(lister, &KCoreDirLister::speed, debug, &PrintSignals::speed); connect(lister, SIGNAL(completed()), this, SLOT(completed())); } KDirListerTest::~KDirListerTest() { } void KDirListerTest::startHome() { QUrl home = QUrl::fromLocalFile(QDir::homePath()); lister->openUrl(home, KDirLister::NoFlags); // lister->stop(); } void KDirListerTest::startRoot() { QUrl root = QUrl::fromLocalFile(QDir::rootPath()); lister->openUrl(root, KDirLister::Keep | KDirLister::Reload); // lister->stop( root ); } void KDirListerTest::startTar() { QUrl root = QUrl::fromLocalFile(QDir::homePath() + "/aclocal_1.tgz"); lister->openUrl(root, KDirLister::Keep | KDirLister::Reload); // lister->stop( root ); } void KDirListerTest::test() { QUrl home = QUrl::fromLocalFile(QDir::homePath()); QUrl root = QUrl::fromLocalFile(QDir::rootPath()); #ifdef Q_OS_WIN lister->openUrl(home, KDirLister::Keep); lister->openUrl(root, KDirLister::Keep | KDirLister::Reload); #else /* lister->openUrl( home, KDirLister::Keep ); lister->openUrl( root, KDirLister::Keep | KDirLister::Reload ); lister->openUrl( QUrl::fromLocalFile("file:/etc"), KDirLister::Keep | KDirLister::Reload ); lister->openUrl( root, KDirLister::Keep | KDirLister::Reload ); lister->openUrl( QUrl::fromLocalFile("file:/dev"), KDirLister::Keep | KDirLister::Reload ); lister->openUrl( QUrl::fromLocalFile("file:/tmp"), KDirLister::Keep | KDirLister::Reload ); lister->openUrl( QUrl::fromLocalFile("file:/usr/include"), KDirLister::Keep | KDirLister::Reload ); lister->updateDirectory( QUrl::fromLocalFile("file:/usr/include") ); lister->updateDirectory( QUrl::fromLocalFile("file:/usr/include") ); lister->openUrl( QUrl::fromLocalFile("file:/usr/"), KDirLister::Keep | KDirLister::Reload ); */ lister->openUrl(QUrl::fromLocalFile(QStringLiteral("/dev")), KDirLister::Keep | KDirLister::Reload); #endif } void KDirListerTest::completed() { if (lister->url().toLocalFile() == QDir::rootPath()) { const KFileItem item = lister->findByUrl(QUrl::fromLocalFile(QDir::tempPath())); if (!item.isNull()) { qDebug() << "Found " << QDir::tempPath() << ": " << item.name(); } else { qWarning() << QDir::tempPath() << " not found! Bug in findByURL?"; } } } int main(int argc, char *argv[]) { QApplication::setApplicationName(QStringLiteral("kdirlistertest")); QApplication app(argc, argv); KDirListerTest *test = new KDirListerTest(nullptr); test->show(); return app.exec(); } #include "moc_kdirlistertest_gui.cpp" diff --git a/tests/kdirmodeltest_gui.cpp b/tests/kdirmodeltest_gui.cpp index 1d4170fd..ea542378 100644 --- a/tests/kdirmodeltest_gui.cpp +++ b/tests/kdirmodeltest_gui.cpp @@ -1,110 +1,108 @@ /* * Copyright (C) 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 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 #include #include #include #include #include -#include -#include #include // Test controller for making the view open up while expandToUrl lists subdirs class TreeController : public QObject { Q_OBJECT public: explicit TreeController(QTreeView *view, KDirModel *model) : QObject(view), m_treeView(view), m_model(model) { connect(model, &KDirModel::expand, this, &TreeController::slotExpand); } private Q_SLOTS: void slotExpand(const QModelIndex &index) { KFileItem item = m_model->itemForIndex(index); qDebug() << "slotListingCompleted" << item.url(); m_treeView->setExpanded(index, true); // The scrollTo call doesn't seem to work. // We probably need to delay this until everything's listed and layouted... m_treeView->scrollTo(index); } private: QTreeView *m_treeView; KDirModel *m_model; }; int main(int argc, char **argv) { //options.add("+[directory ...]", qi18n("Directory(ies) to model")); QApplication a(argc, argv); KDirModel *dirmodel = new KDirModel(nullptr); dirmodel->dirLister()->setDelayedMimeTypes(true); #if 1 QTreeView *treeView = new QTreeView(nullptr); treeView->setModel(dirmodel); treeView->setUniformRowHeights(true); // makes visualRect() much faster treeView->resize(500, 500); treeView->show(); treeView->setItemDelegate(new KFileItemDelegate(treeView)); #endif #if 0 QListView *listView = new QListView(0); listView->setModel(dirmodel); listView->setUniformItemSizes(true); // true in list mode, not in icon mode. listView->show(); listView->setItemDelegate(new KFileItemDelegate(listView)); #endif #if 1 QListView *iconView = new QListView(nullptr); iconView->setModel(dirmodel); iconView->setSelectionMode(QListView::ExtendedSelection); iconView->setViewMode(QListView::IconMode); iconView->show(); iconView->setItemDelegate(new KFileItemDelegate(iconView)); #endif if (argc <= 1) { dirmodel->dirLister()->openUrl(QUrl::fromLocalFile(QStringLiteral("/"))); const QUrl url = QUrl::fromLocalFile(QStringLiteral("/usr/share/applications/kde")); dirmodel->expandToUrl(url); new TreeController(treeView, dirmodel); } const int count = QCoreApplication::arguments().count() - 1; for (int i = 0; i < count; i++) { QUrl u = QUrl::fromUserInput(QCoreApplication::arguments().at(i + 1)); qDebug() << "Adding: " << u; dirmodel->dirLister()->openUrl(u, KDirLister::Keep); } return a.exec(); } #include "kdirmodeltest_gui.moc" diff --git a/tests/kfilewidgettest_gui.cpp b/tests/kfilewidgettest_gui.cpp index 749f8cdc..cb1541f4 100644 --- a/tests/kfilewidgettest_gui.cpp +++ b/tests/kfilewidgettest_gui.cpp @@ -1,38 +1,37 @@ /* This file is part of the KDE libraries Copyright (C) 2015 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 int main(int argc, char **argv) { QApplication app(argc, argv); KFileWidget* fileWidget = new KFileWidget(QUrl(QStringLiteral("kfiledialog:///OpenDialog")), nullptr); fileWidget->setMode(KFile::Files | KFile::ExistingOnly); fileWidget->setAttribute(Qt::WA_DeleteOnClose); fileWidget->show(); QObject::connect(fileWidget, &KFileWidget::destroyed, &app, &QApplication::quit); return app.exec(); } diff --git a/tests/kopenwithtest.cpp b/tests/kopenwithtest.cpp index 1bddb95f..53e38653 100644 --- a/tests/kopenwithtest.cpp +++ b/tests/kopenwithtest.cpp @@ -1,68 +1,65 @@ /* This file is part of the KDE libraries Copyright (C) 2002 Dirk Mueller Copyright (C) 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 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 int main(int argc, char **argv) { QApplication::setApplicationName(QStringLiteral("kopenwithdialogtest")); QApplication app(argc, argv); QList list; list += QUrl(QStringLiteral("file:///tmp/testfile.txt")); // Test with one URL KOpenWithDialog *dlg = new KOpenWithDialog(list, QString(), QString()); if (dlg->exec()) { qDebug() << "Dialog ended successfully\ntext: " << dlg->text(); } else { qDebug() << "Dialog was canceled."; } delete dlg; // Test with two URLs list += QUrl(QStringLiteral("http://www.kde.org/index.html")); dlg = new KOpenWithDialog(list, QString(), QString(), nullptr); if (dlg->exec()) { qDebug() << "Dialog ended successfully\ntext: " << dlg->text(); } else { qDebug() << "Dialog was canceled."; } delete dlg; // Test with a mimetype QString mimetype = QStringLiteral("text/plain"); dlg = new KOpenWithDialog(mimetype, QStringLiteral("kwrite"), nullptr); if (dlg->exec()) { qDebug() << "Dialog ended successfully\ntext: " << dlg->text(); } else { qDebug() << "Dialog was canceled."; } delete dlg; return 0; } diff --git a/tests/kurlnavigatortest_gui.cpp b/tests/kurlnavigatortest_gui.cpp index db28c5f1..f0307fda 100644 --- a/tests/kurlnavigatortest_gui.cpp +++ b/tests/kurlnavigatortest_gui.cpp @@ -1,39 +1,37 @@ /* This file is part of the KDE libraries Copyright (C) 2015 David Edmundson 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 int main(int argc, char **argv) { QApplication::setApplicationName(QStringLiteral("kurlnavigatortest")); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); QApplication app(argc, argv); KUrlNavigator urlNavigator(new KFilePlacesModel, QUrl::fromLocalFile(QDir::homePath()), nullptr); urlNavigator.show(); return app.exec(); }