diff --git a/src/core/ksslcertificatemanager.cpp b/src/core/ksslcertificatemanager.cpp index dd85f6ee..d374b41c 100644 --- a/src/core/ksslcertificatemanager.cpp +++ b/src/core/ksslcertificatemanager.cpp @@ -1,498 +1,498 @@ /* This file is part of the KDE project * * Copyright (C) 2007, 2008, 2010 Andreas Hartmetz * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "ksslcertificatemanager.h" #include "ksslcertificatemanager_p.h" #include "ktcpsocket.h" #include "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. foreach (KSslError::Error e, errors) if (!isErrorIgnored(e)) { d->ignoredErrors.append(e); } } void KSslCertificateRule::setIgnoredErrors(const QList &errors) { QList el; foreach (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; foreach (KSslError::Error error, errors) { if (!isErrorIgnored(error)) { ret.append(error); } } return ret; } QList KSslCertificateRule::filterErrors(const QList &errors) const { QList ret; foreach (const KSslError &error, errors) { if (!isErrorIgnored(error.error())) { ret.append(error); } } return ret; } //////////////////////////////////////////////////////////////////// static QList deduplicate(const QList &certs) { QSet digests; QList ret; foreach (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 + QStringLiteral("*"), QSsl::Pem, QRegExp::Wildcard)); foreach (const QSslCertificate &cert, 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(); - qSort(in.begin(), in.end(), certLessThan); - qSort(old.begin(), old.end(), certLessThan); + 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 + "*", QSsl::Pem, QRegExp::Wildcard)) { ret += KSslCaCertificate(cert, KSslCaCertificate::UserStore, false); } KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig); KConfigGroup group = config.group("Blacklist of CA Certificates"); for (int i = 0; i < ret.size(); i++) { if (group.hasKey(ret[i].certHash.constData())) { ret[i].isBlacklisted = true; //qDebug() << "is blacklisted"; } } return ret; } bool KSslCertificateManagerPrivate::updateCertificateBlacklisted(const KSslCaCertificate &cert) { return setCertificateBlacklisted(cert.certHash, cert.isBlacklisted); } bool KSslCertificateManagerPrivate::setCertificateBlacklisted(const QByteArray &certHash, bool isBlacklisted) { //qDebug() << Q_FUNC_INFO << isBlacklisted; KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig); KConfigGroup group = config.group("Blacklist of CA Certificates"); if (isBlacklisted) { // TODO check against certificate list ? group.writeEntry(certHash.constData(), QString()); } else { if (!group.hasKey(certHash.constData())) { return false; } group.deleteEntry(certHash.constData()); } return true; } class KSslCertificateManagerContainer { public: KSslCertificateManager sslCertificateManager; }; Q_GLOBAL_STATIC(KSslCertificateManagerContainer, g_instance) KSslCertificateManager::KSslCertificateManager() : d(new KSslCertificateManagerPrivate()) { } KSslCertificateManager::~KSslCertificateManager() { delete d; } //static KSslCertificateManager *KSslCertificateManager::self() { return &g_instance()->sslCertificateManager; } void KSslCertificateManager::setRule(const KSslCertificateRule &rule) { d->iface->setRule(rule); } void KSslCertificateManager::clearRule(const KSslCertificateRule &rule) { d->iface->clearRule(rule); } void KSslCertificateManager::clearRule(const QSslCertificate &cert, const QString &hostName) { d->iface->clearRule(cert, hostName); } KSslCertificateRule KSslCertificateManager::rule(const QSslCertificate &cert, const QString &hostName) const { return d->iface->rule(cert, hostName); } QList KSslCertificateManager::caCertificates() const { QMutexLocker certLocker(&d->certListMutex); if (!d->isCertListLoaded) { d->loadDefaultCaCertificates(); } return d->defaultCaCertificates; } //static QList KSslCertificateManager::nonIgnorableErrors(const QList &/*e*/) { QList ret; // ### add filtering here... return ret; } //static QList KSslCertificateManager::nonIgnorableErrors(const QList &/*e*/) { QList ret; // ### add filtering here... return ret; } QList _allKsslCaCertificates(KSslCertificateManager *cm) { return KSslCertificateManagerPrivate::get(cm)->allCertificates(); } void _setAllKsslCaCertificates(KSslCertificateManager *cm, const QList &certsIn) { KSslCertificateManagerPrivate::get(cm)->setAllCertificates(certsIn); } #include "moc_kssld_interface.cpp" diff --git a/src/filewidgets/kurlnavigatorbutton.cpp b/src/filewidgets/kurlnavigatorbutton.cpp index 8bc659a8..cd73a78a 100644 --- a/src/filewidgets/kurlnavigatorbutton.cpp +++ b/src/filewidgets/kurlnavigatorbutton.cpp @@ -1,697 +1,697 @@ /***************************************************************************** * Copyright (C) 2006 by Peter Penz * * Copyright (C) 2006 by Aaron J. Seigo * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public License * * along with this library; see the file COPYING.LIB. If not, write to * * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301, USA. * *****************************************************************************/ #include "kurlnavigatorbutton_p.h" #include "kurlnavigator.h" #include "kurlnavigatormenu_p.h" #include "kdirsortfilterproxymodel.h" #include "../pathhelpers_p.h" #include #include #include #include #include #include #include #include #include namespace KDEPrivate { QPointer KUrlNavigatorButton::m_subDirsMenu; KUrlNavigatorButton::KUrlNavigatorButton(const QUrl &url, QWidget *parent) : KUrlNavigatorButtonBase(parent), m_hoverArrow(false), m_pendingTextChange(false), m_replaceButton(false), m_showMnemonic(false), m_wheelSteps(0), m_url(url), m_subDir(), m_openSubDirsTimer(nullptr), m_subDirsJob(nullptr) { setAcceptDrops(true); setUrl(url); setMouseTracking(true); m_openSubDirsTimer = new QTimer(this); m_openSubDirsTimer->setSingleShot(true); m_openSubDirsTimer->setInterval(300); connect(m_openSubDirsTimer, SIGNAL(timeout()), this, SLOT(startSubDirsJob())); connect(this, SIGNAL(pressed()), this, SLOT(requestSubDirs())); } KUrlNavigatorButton::~KUrlNavigatorButton() { } void KUrlNavigatorButton::setUrl(const QUrl &url) { m_url = url; // Doing a text-resolving with KIO::stat() for all non-local // URLs leads to problems for protocols where a limit is given for // the number of parallel connections. A black-list // is given where KIO::stat() should not be used: static const QSet protocolBlacklist = QSet() << QStringLiteral("nfs") << QStringLiteral("fish") << QStringLiteral("ftp") << QStringLiteral("sftp") << QStringLiteral("smb") << QStringLiteral("webdav"); const bool startTextResolving = m_url.isValid() && !m_url.isLocalFile() && !protocolBlacklist.contains(m_url.scheme()); if (startTextResolving) { m_pendingTextChange = true; KIO::StatJob *job = KIO::stat(m_url, KIO::HideProgressInfo); connect(job, SIGNAL(result(KJob*)), this, SLOT(statFinished(KJob*))); emit startedTextResolving(); } else { setText(m_url.fileName().replace('&', QLatin1String("&&"))); } } QUrl KUrlNavigatorButton::url() const { return m_url; } void KUrlNavigatorButton::setText(const QString &text) { QString adjustedText = text; if (adjustedText.isEmpty()) { adjustedText = m_url.scheme(); } // Assure that the button always consists of one line adjustedText.remove(QLatin1Char('\n')); KUrlNavigatorButtonBase::setText(adjustedText); updateMinimumWidth(); // Assure that statFinished() does not overwrite a text that has been // set by a client of the URL navigator button m_pendingTextChange = false; } void KUrlNavigatorButton::setActiveSubDirectory(const QString &subDir) { m_subDir = subDir; // We use a different (bold) font on active, so the size hint changes updateGeometry(); update(); } QString KUrlNavigatorButton::activeSubDirectory() const { return m_subDir; } QSize KUrlNavigatorButton::sizeHint() const { QFont adjustedFont(font()); adjustedFont.setBold(m_subDir.isEmpty()); // the minimum size is textWidth + arrowWidth() + 2 * BorderWidth; for the // preferred size we add the BorderWidth 2 times again for having an uncluttered look const int width = QFontMetrics(adjustedFont).width(plainText()) + arrowWidth() + 4 * BorderWidth; return QSize(width, KUrlNavigatorButtonBase::sizeHint().height()); } void KUrlNavigatorButton::setShowMnemonic(bool show) { if (m_showMnemonic != show) { m_showMnemonic = show; update(); } } bool KUrlNavigatorButton::showMnemonic() const { return m_showMnemonic; } void KUrlNavigatorButton::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); QFont adjustedFont(font()); adjustedFont.setBold(m_subDir.isEmpty()); painter.setFont(adjustedFont); int buttonWidth = width(); int preferredWidth = sizeHint().width(); if (preferredWidth < minimumWidth()) { preferredWidth = minimumWidth(); } if (buttonWidth > preferredWidth) { buttonWidth = preferredWidth; } const int buttonHeight = height(); const QColor fgColor = foregroundColor(); drawHoverBackground(&painter); int textLeft = 0; int textWidth = buttonWidth; const bool leftToRight = (layoutDirection() == Qt::LeftToRight); if (!m_subDir.isEmpty()) { // draw arrow const int arrowSize = arrowWidth(); const int arrowX = leftToRight ? (buttonWidth - arrowSize) - BorderWidth : BorderWidth; const int arrowY = (buttonHeight - arrowSize) / 2; QStyleOption option; option.initFrom(this); option.rect = QRect(arrowX, arrowY, arrowSize, arrowSize); option.palette = palette(); option.palette.setColor(QPalette::Text, fgColor); option.palette.setColor(QPalette::WindowText, fgColor); option.palette.setColor(QPalette::ButtonText, fgColor); if (m_hoverArrow) { // highlight the background of the arrow to indicate that the directories // popup can be opened by a mouse click QColor hoverColor = palette().color(QPalette::HighlightedText); hoverColor.setAlpha(96); painter.setPen(Qt::NoPen); painter.setBrush(hoverColor); int hoverX = arrowX; if (!leftToRight) { hoverX -= BorderWidth; } painter.drawRect(QRect(hoverX, 0, arrowSize + BorderWidth, buttonHeight)); } if (leftToRight) { style()->drawPrimitive(QStyle::PE_IndicatorArrowRight, &option, &painter, this); } else { style()->drawPrimitive(QStyle::PE_IndicatorArrowLeft, &option, &painter, this); textLeft += arrowSize + 2 * BorderWidth; } textWidth -= arrowSize + 2 * BorderWidth; } painter.setPen(fgColor); const bool clipped = isTextClipped(); const QRect textRect(textLeft, 0, textWidth, buttonHeight); if (clipped) { QColor bgColor = fgColor; bgColor.setAlpha(0); QLinearGradient gradient(textRect.topLeft(), textRect.topRight()); if (leftToRight) { gradient.setColorAt(0.8, fgColor); gradient.setColorAt(1.0, bgColor); } else { gradient.setColorAt(0.0, bgColor); gradient.setColorAt(0.2, fgColor); } QPen pen; pen.setBrush(QBrush(gradient)); painter.setPen(pen); } int textFlags = clipped ? Qt::AlignVCenter : Qt::AlignCenter; if (m_showMnemonic) { textFlags |= Qt::TextShowMnemonic; painter.drawText(textRect, textFlags, text()); } else { painter.drawText(textRect, textFlags, plainText()); } } void KUrlNavigatorButton::enterEvent(QEvent *event) { KUrlNavigatorButtonBase::enterEvent(event); // if the text is clipped due to a small window width, the text should // be shown as tooltip if (isTextClipped()) { setToolTip(plainText()); } } void KUrlNavigatorButton::leaveEvent(QEvent *event) { KUrlNavigatorButtonBase::leaveEvent(event); setToolTip(QString()); if (m_hoverArrow) { m_hoverArrow = false; update(); } } void KUrlNavigatorButton::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Enter: case Qt::Key_Return: emit clicked(m_url, Qt::LeftButton); break; case Qt::Key_Down: case Qt::Key_Space: startSubDirsJob(); break; default: KUrlNavigatorButtonBase::keyPressEvent(event); } } void KUrlNavigatorButton::dropEvent(QDropEvent *event) { if (event->mimeData()->hasUrls()) { setDisplayHintEnabled(DraggedHint, true); emit urlsDropped(m_url, event); setDisplayHintEnabled(DraggedHint, false); update(); } } void KUrlNavigatorButton::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls()) { setDisplayHintEnabled(DraggedHint, true); event->acceptProposedAction(); update(); } } void KUrlNavigatorButton::dragMoveEvent(QDragMoveEvent *event) { QRect rect = event->answerRect(); if (isAboveArrow(rect.center().x())) { m_hoverArrow = true; update(); if (m_subDirsMenu == nullptr) { requestSubDirs(); } else if (m_subDirsMenu->parent() != this) { m_subDirsMenu->close(); m_subDirsMenu->deleteLater(); m_subDirsMenu = nullptr; requestSubDirs(); } } else { if (m_openSubDirsTimer->isActive()) { cancelSubDirsRequest(); } delete m_subDirsMenu; m_subDirsMenu = nullptr; m_hoverArrow = false; update(); } } void KUrlNavigatorButton::dragLeaveEvent(QDragLeaveEvent *event) { KUrlNavigatorButtonBase::dragLeaveEvent(event); m_hoverArrow = false; setDisplayHintEnabled(DraggedHint, false); update(); } void KUrlNavigatorButton::mousePressEvent(QMouseEvent *event) { if (isAboveArrow(event->x()) && (event->button() == Qt::LeftButton)) { // the mouse is pressed above the [>] button startSubDirsJob(); } KUrlNavigatorButtonBase::mousePressEvent(event); } void KUrlNavigatorButton::mouseReleaseEvent(QMouseEvent *event) { if (!isAboveArrow(event->x()) || (event->button() != Qt::LeftButton)) { // the mouse has been released above the text area and not // above the [>] button emit clicked(m_url, event->button()); cancelSubDirsRequest(); } KUrlNavigatorButtonBase::mouseReleaseEvent(event); } void KUrlNavigatorButton::mouseMoveEvent(QMouseEvent *event) { KUrlNavigatorButtonBase::mouseMoveEvent(event); const bool hoverArrow = isAboveArrow(event->x()); if (hoverArrow != m_hoverArrow) { m_hoverArrow = hoverArrow; update(); } } void KUrlNavigatorButton::wheelEvent(QWheelEvent *event) { if (event->orientation() == Qt::Vertical) { m_wheelSteps = event->delta() / 120; m_replaceButton = true; startSubDirsJob(); } KUrlNavigatorButtonBase::wheelEvent(event); } void KUrlNavigatorButton::requestSubDirs() { if (!m_openSubDirsTimer->isActive() && (m_subDirsJob == nullptr)) { m_openSubDirsTimer->start(); } } void KUrlNavigatorButton::startSubDirsJob() { if (m_subDirsJob != nullptr) { return; } const QUrl url = m_replaceButton ? KIO::upUrl(m_url) : m_url; m_subDirsJob = KIO::listDir(url, KIO::HideProgressInfo, false /*no hidden files*/); m_subDirs.clear(); // just to be ++safe connect(m_subDirsJob, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), this, SLOT(addEntriesToSubDirs(KIO::Job*,KIO::UDSEntryList))); if (m_replaceButton) { connect(m_subDirsJob, SIGNAL(result(KJob*)), this, SLOT(replaceButton(KJob*))); } else { connect(m_subDirsJob, SIGNAL(result(KJob*)), this, SLOT(openSubDirsMenu(KJob*))); } } void KUrlNavigatorButton::addEntriesToSubDirs(KIO::Job *job, const KIO::UDSEntryList &entries) { Q_ASSERT(job == m_subDirsJob); Q_UNUSED(job); foreach (const KIO::UDSEntry &entry, entries) { if (entry.isDir()) { const QString name = entry.stringValue(KIO::UDSEntry::UDS_NAME); QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); if (displayName.isEmpty()) { displayName = name; } if ((name != QLatin1String(".")) && (name != QLatin1String(".."))) { m_subDirs.append(qMakePair(name, displayName)); } } } } void KUrlNavigatorButton::urlsDropped(QAction *action, QDropEvent *event) { const int result = action->data().toInt(); QUrl url(m_url); url.setPath(concatPaths(url.path(), m_subDirs.at(result).first)); urlsDropped(url, event); } void KUrlNavigatorButton::slotMenuActionClicked(QAction *action, Qt::MouseButton button) { const int result = action->data().toInt(); QUrl url(m_url); url.setPath(concatPaths(url.path(), m_subDirs.at(result).first)); emit clicked(url, button); } void KUrlNavigatorButton::statFinished(KJob *job) { if (m_pendingTextChange) { m_pendingTextChange = false; const KIO::UDSEntry entry = static_cast(job)->statResult(); QString name = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); if (name.isEmpty()) { name = m_url.fileName(); } setText(name); emit finishedTextResolving(); } } /** * Helper class for openSubDirsMenu */ class NaturalLessThan { public: NaturalLessThan() { m_collator.setCaseSensitivity(Qt::CaseInsensitive); m_collator.setNumericMode(true); } bool operator()(const QPair &s1, const QPair &s2) { return m_collator.compare(s1.first, s2.first) < 0; } private: QCollator m_collator; }; void KUrlNavigatorButton::openSubDirsMenu(KJob *job) { Q_ASSERT(job == m_subDirsJob); m_subDirsJob = nullptr; if (job->error() || m_subDirs.isEmpty()) { // clear listing return; } NaturalLessThan nlt; - qSort(m_subDirs.begin(), m_subDirs.end(), nlt); + std::sort(m_subDirs.begin(), m_subDirs.end(), nlt); setDisplayHintEnabled(PopupActiveHint, true); update(); // ensure the button is drawn highlighted if (m_subDirsMenu != nullptr) { m_subDirsMenu->close(); m_subDirsMenu->deleteLater(); m_subDirsMenu = nullptr; } m_subDirsMenu = new KUrlNavigatorMenu(this); initMenu(m_subDirsMenu, 0); const bool leftToRight = (layoutDirection() == Qt::LeftToRight); const int popupX = leftToRight ? width() - arrowWidth() - BorderWidth : 0; const QPoint popupPos = parentWidget()->mapToGlobal(geometry().bottomLeft() + QPoint(popupX, 0)); QPointer guard(this); m_subDirsMenu->exec(popupPos); // If 'this' has been deleted in the menu's nested event loop, we have to return // immediatedely because any access to a member variable might cause a crash. if (!guard) { return; } m_subDirs.clear(); delete m_subDirsMenu; m_subDirsMenu = nullptr; setDisplayHintEnabled(PopupActiveHint, false); } void KUrlNavigatorButton::replaceButton(KJob *job) { Q_ASSERT(job == m_subDirsJob); m_subDirsJob = nullptr; m_replaceButton = false; if (job->error() || m_subDirs.isEmpty()) { return; } NaturalLessThan nlt; - qSort(m_subDirs.begin(), m_subDirs.end(), nlt); + std::sort(m_subDirs.begin(), m_subDirs.end(), nlt); // Get index of the directory that is shown currently in the button const QString currentDir = m_url.fileName(); int currentIndex = 0; const int subDirsCount = m_subDirs.count(); while (currentIndex < subDirsCount) { if (m_subDirs[currentIndex].first == currentDir) { break; } ++currentIndex; } // Adjust the index by respecting the wheel steps and // trigger a replacing of the button content int targetIndex = currentIndex - m_wheelSteps; if (targetIndex < 0) { targetIndex = 0; } else if (targetIndex >= subDirsCount) { targetIndex = subDirsCount - 1; } QUrl url(KIO::upUrl(m_url)); url.setPath(concatPaths(url.path(), m_subDirs[targetIndex].first)); emit clicked(url, Qt::LeftButton); m_subDirs.clear(); } void KUrlNavigatorButton::cancelSubDirsRequest() { m_openSubDirsTimer->stop(); if (m_subDirsJob != nullptr) { m_subDirsJob->kill(); m_subDirsJob = nullptr; } } QString KUrlNavigatorButton::plainText() const { // Replace all "&&" by '&' and remove all single // '&' characters const QString source = text(); const int sourceLength = source.length(); QString dest; dest.reserve(sourceLength); int sourceIndex = 0; int destIndex = 0; while (sourceIndex < sourceLength) { if (source.at(sourceIndex) == QLatin1Char('&')) { ++sourceIndex; if (sourceIndex >= sourceLength) { break; } } dest[destIndex] = source.at(sourceIndex); ++sourceIndex; ++destIndex; } return dest; } int KUrlNavigatorButton::arrowWidth() const { // if there isn't arrow then return 0 int width = 0; if (!m_subDir.isEmpty()) { width = height() / 2; if (width < 4) { width = 4; } } return width; } bool KUrlNavigatorButton::isAboveArrow(int x) const { const bool leftToRight = (layoutDirection() == Qt::LeftToRight); return leftToRight ? (x >= width() - arrowWidth()) : (x < arrowWidth()); } bool KUrlNavigatorButton::isTextClipped() const { int availableWidth = width() - 2 * BorderWidth; if (!m_subDir.isEmpty()) { availableWidth -= arrowWidth() - BorderWidth; } QFont adjustedFont(font()); adjustedFont.setBold(m_subDir.isEmpty()); return QFontMetrics(adjustedFont).width(plainText()) >= availableWidth; } void KUrlNavigatorButton::updateMinimumWidth() { const int oldMinWidth = minimumWidth(); int minWidth = sizeHint().width(); if (minWidth < 40) { minWidth = 40; } else if (minWidth > 150) { // don't let an overlong path name waste all the URL navigator space minWidth = 150; } if (oldMinWidth != minWidth) { setMinimumWidth(minWidth); } } void KUrlNavigatorButton::initMenu(KUrlNavigatorMenu *menu, int startIndex) { connect(menu, SIGNAL(mouseButtonClicked(QAction*, Qt::MouseButton)), this, SLOT(slotMenuActionClicked(QAction*, Qt::MouseButton))); connect(menu, SIGNAL(urlsDropped(QAction*,QDropEvent*)), this, SLOT(urlsDropped(QAction*,QDropEvent*))); menu->setLayoutDirection(Qt::LeftToRight); const int maxIndex = startIndex + 30; // Don't show more than 30 items in a menu const int lastIndex = qMin(m_subDirs.count() - 1, maxIndex); for (int i = startIndex; i <= lastIndex; ++i) { const QString subDirName = m_subDirs[i].first; const QString subDirDisplayName = m_subDirs[i].second; QString text = KStringHandler::csqueeze(subDirDisplayName, 60); text.replace(QLatin1Char('&'), QLatin1String("&&")); QAction *action = new QAction(text, this); if (m_subDir == subDirName) { QFont font(action->font()); font.setBold(true); action->setFont(font); } action->setData(i); menu->addAction(action); } if (m_subDirs.count() > maxIndex) { // If too much items are shown, move them into a sub menu menu->addSeparator(); KUrlNavigatorMenu *subDirsMenu = new KUrlNavigatorMenu(menu); subDirsMenu->setTitle(i18nc("@action:inmenu", "More")); initMenu(subDirsMenu, maxIndex); menu->addMenu(subDirsMenu); } } } // namespace KDEPrivate #include "moc_kurlnavigatorbutton_p.cpp" diff --git a/src/ioslaves/http/http_cache_cleaner.cpp b/src/ioslaves/http/http_cache_cleaner.cpp index c22c692f..5093b10c 100644 --- a/src/ioslaves/http/http_cache_cleaner.cpp +++ b/src/ioslaves/http/http_cache_cleaner.cpp @@ -1,850 +1,850 @@ /* This file is part of KDE Copyright (C) 1999-2000 Waldo Bastian (bastian@kde.org) Copyright (C) 2009 Andreas Hartmetz (ahartmetz@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ //---------------------------------------------------------------------------- // // KDE HTTP Cache cleanup tool #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QDateTime g_currentDate; int g_maxCacheAge; qint64 g_maxCacheSize; static const char appFullName[] = "org.kio5.kio_http_cache_cleaner"; static const char appName[] = "kio_http_cache_cleaner"; // !START OF SYNC! // Keep the following in sync with the cache code in http.cpp static const int s_hashedUrlBits = 160; // this number should always be divisible by eight static const int s_hashedUrlNibbles = s_hashedUrlBits / 4; static const int s_hashedUrlBytes = s_hashedUrlBits / 8; static const char version[] = "A\n"; // never instantiated, on-disk / wire format only struct SerializedCacheFileInfo { // from http.cpp quint8 version[2]; quint8 compression; // for now fixed to 0 quint8 reserved; // for now; also alignment static const int useCountOffset = 4; qint32 useCount; qint64 servedDate; qint64 lastModifiedDate; qint64 expireDate; qint32 bytesCached; static const int size = 36; QString url; QString etag; QString mimeType; QStringList responseHeaders; // including status response like "HTTP 200 OK" }; struct MiniCacheFileInfo { // data from cache entry file, or from scoreboard file qint32 useCount; // from filesystem QDateTime lastUsedDate; qint64 sizeOnDisk; // we want to delete the least "useful" files and we'll have to sort a list for that... bool operator<(const MiniCacheFileInfo &other) const; void debugPrint() const { // qDebug() << "useCount:" << useCount // << "\nlastUsedDate:" << lastUsedDate.toString(Qt::ISODate) // << "\nsizeOnDisk:" << sizeOnDisk << '\n'; } }; struct CacheFileInfo : MiniCacheFileInfo { quint8 version[2]; quint8 compression; // for now fixed to 0 quint8 reserved; // for now; also alignment QDateTime servedDate; QDateTime lastModifiedDate; QDateTime expireDate; qint32 bytesCached; QString baseName; QString url; QString etag; QString mimeType; QStringList responseHeaders; // including status response like "HTTP 200 OK" void prettyPrint() const { QTextStream out(stdout, QIODevice::WriteOnly); out << "File " << baseName << " version " << version[0] << version[1]; out << "\n cached bytes " << bytesCached << " useCount " << useCount; out << "\n servedDate " << servedDate.toString(Qt::ISODate); out << "\n lastModifiedDate " << lastModifiedDate.toString(Qt::ISODate); out << "\n expireDate " << expireDate.toString(Qt::ISODate); out << "\n entity tag " << etag; out << "\n encoded URL " << url; out << "\n mimetype " << mimeType; out << "\nResponse headers follow...\n"; Q_FOREACH (const QString &h, responseHeaders) { out << h << '\n'; } } }; bool MiniCacheFileInfo::operator<(const MiniCacheFileInfo &other) const { const int thisUseful = useCount / qMax(lastUsedDate.secsTo(g_currentDate), qint64(1)); const int otherUseful = other.useCount / qMax(other.lastUsedDate.secsTo(g_currentDate), qint64(1)); return thisUseful < otherUseful; } bool CacheFileInfoPtrLessThan(const CacheFileInfo *cf1, const CacheFileInfo *cf2) { return *cf1 < *cf2; } enum OperationMode { CleanCache = 0, DeleteCache, FileInfo }; static bool readBinaryHeader(const QByteArray &d, CacheFileInfo *fi) { if (d.size() < SerializedCacheFileInfo::size) { // qDebug() << "readBinaryHeader(): file too small?"; return false; } QDataStream stream(d); stream.setVersion(QDataStream::Qt_4_5); stream >> fi->version[0]; stream >> fi->version[1]; if (fi->version[0] != version[0] || fi->version[1] != version[1]) { // qDebug() << "readBinaryHeader(): wrong magic bytes"; return false; } stream >> fi->compression; stream >> fi->reserved; stream >> fi->useCount; stream >> fi->servedDate; stream >> fi->lastModifiedDate; stream >> fi->expireDate; stream >> fi->bytesCached; return true; } static QString filenameFromUrl(const QByteArray &url) { QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(url); return QString::fromLatin1(hash.result().toHex()); } static QString cacheDir() { return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/kio_http"; } static QString filePath(const QString &baseName) { QString cacheDirName = cacheDir(); if (!cacheDirName.endsWith('/')) { cacheDirName.append('/'); } return cacheDirName + baseName; } static bool readLineChecked(QIODevice *dev, QByteArray *line) { *line = dev->readLine(8192); // if nothing read or the line didn't fit into 8192 bytes(!) if (line->isEmpty() || !line->endsWith('\n')) { return false; } // we don't actually want the newline! line->chop(1); return true; } static bool readTextHeader(QFile *file, CacheFileInfo *fi, OperationMode mode) { bool ok = true; QByteArray readBuf; ok = ok && readLineChecked(file, &readBuf); fi->url = QString::fromLatin1(readBuf); if (filenameFromUrl(readBuf) != QFileInfo(*file).baseName()) { // qDebug() << "You have witnessed a very improbable hash collision!"; return false; } // only read the necessary info for cache cleaning. Saves time and (more importantly) memory. if (mode != FileInfo) { return true; } ok = ok && readLineChecked(file, &readBuf); fi->etag = QString::fromLatin1(readBuf); ok = ok && readLineChecked(file, &readBuf); fi->mimeType = QString::fromLatin1(readBuf); // read as long as no error and no empty line found while (true) { ok = ok && readLineChecked(file, &readBuf); if (ok && !readBuf.isEmpty()) { fi->responseHeaders.append(QString::fromLatin1(readBuf)); } else { break; } } return ok; // it may still be false ;) } // TODO common include file with http.cpp? enum CacheCleanerCommand { InvalidCommand = 0, CreateFileNotificationCommand, UpdateFileCommand }; static bool readCacheFile(const QString &baseName, CacheFileInfo *fi, OperationMode mode) { QFile file(filePath(baseName)); if (!file.open(QIODevice::ReadOnly)) { return false; } fi->baseName = baseName; QByteArray header = file.read(SerializedCacheFileInfo::size); // do *not* modify/delete the file if we're in file info mode. if (!(readBinaryHeader(header, fi) && readTextHeader(&file, fi, mode)) && mode != FileInfo) { // qDebug() << "read(Text|Binary)Header() returned false, deleting file" << baseName; file.remove(); return false; } // get meta-information from the filesystem QFileInfo fileInfo(file); fi->lastUsedDate = fileInfo.lastModified(); fi->sizeOnDisk = fileInfo.size(); return true; } class Scoreboard; class CacheIndex { public: explicit CacheIndex(const QString &baseName) { QByteArray ba = baseName.toLatin1(); const int sz = ba.size(); const char *input = ba.constData(); Q_ASSERT(sz == s_hashedUrlNibbles); int translated = 0; for (int i = 0; i < sz; i++) { int c = input[i]; if (c >= '0' && c <= '9') { translated |= c - '0'; } else if (c >= 'a' && c <= 'f') { translated |= c - 'a' + 10; } else { Q_ASSERT(false); } if (i & 1) { // odd index m_index[i >> 1] = translated; translated = 0; } else { translated = translated << 4; } } computeHash(); } bool operator==(const CacheIndex &other) const { const bool isEqual = memcmp(m_index, other.m_index, s_hashedUrlBytes) == 0; if (isEqual) { Q_ASSERT(m_hash == other.m_hash); } return isEqual; } private: explicit CacheIndex(const QByteArray &index) { Q_ASSERT(index.length() >= s_hashedUrlBytes); memcpy(m_index, index.constData(), s_hashedUrlBytes); computeHash(); } void computeHash() { uint hash = 0; const int ints = s_hashedUrlBytes / sizeof(uint); for (int i = 0; i < ints; i++) { hash ^= reinterpret_cast(&m_index[0])[i]; } if (const int bytesLeft = s_hashedUrlBytes % sizeof(uint)) { // dead code until a new url hash algorithm or architecture with sizeof(uint) != 4 appears. // we have the luxury of ignoring endianness because the hash is never written to disk. // just merge the bits into the hash in some way. const int offset = ints * sizeof(uint); for (int i = 0; i < bytesLeft; i++) { hash ^= static_cast(m_index[offset + i]) << (i * 8); } } m_hash = hash; } friend uint qHash(const CacheIndex &); friend class Scoreboard; quint8 m_index[s_hashedUrlBytes]; // packed binary version of the hexadecimal name uint m_hash; }; uint qHash(const CacheIndex &ci) { return ci.m_hash; } static CacheCleanerCommand readCommand(const QByteArray &cmd, CacheFileInfo *fi) { readBinaryHeader(cmd, fi); QDataStream stream(cmd); stream.skipRawData(SerializedCacheFileInfo::size); quint32 ret; stream >> ret; QByteArray baseName; baseName.resize(s_hashedUrlNibbles); stream.readRawData(baseName.data(), s_hashedUrlNibbles); Q_ASSERT(stream.atEnd()); fi->baseName = QString::fromLatin1(baseName); Q_ASSERT(ret == CreateFileNotificationCommand || ret == UpdateFileCommand); return static_cast(ret); } // never istantiated, on-disk format only struct ScoreboardEntry { // from scoreboard file quint8 index[s_hashedUrlBytes]; static const int indexSize = s_hashedUrlBytes; qint32 useCount; // from scoreboard file, but compared with filesystem to see if scoreboard has current data qint64 lastUsedDate; qint32 sizeOnDisk; static const int size = 36; // we want to delete the least "useful" files and we'll have to sort a list for that... bool operator<(const MiniCacheFileInfo &other) const; }; class Scoreboard { public: Scoreboard() { // read in the scoreboard... QFile sboard(filePath(QStringLiteral("scoreboard"))); if (sboard.open(QIODevice::ReadOnly)) { while (true) { QByteArray baIndex = sboard.read(ScoreboardEntry::indexSize); QByteArray baRest = sboard.read(ScoreboardEntry::size - ScoreboardEntry::indexSize); if (baIndex.size() + baRest.size() != ScoreboardEntry::size) { break; } const QString entryBasename = QString::fromLatin1(baIndex.toHex()); MiniCacheFileInfo mcfi; if (readAndValidateMcfi(baRest, entryBasename, &mcfi)) { m_scoreboard.insert(CacheIndex(baIndex), mcfi); } } } } void writeOut() { // write out the scoreboard QFile sboard(filePath(QStringLiteral("scoreboard"))); if (!sboard.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return; } QDataStream stream(&sboard); QHash::ConstIterator it = m_scoreboard.constBegin(); for (; it != m_scoreboard.constEnd(); ++it) { const char *indexData = reinterpret_cast(it.key().m_index); stream.writeRawData(indexData, s_hashedUrlBytes); stream << it.value().useCount; stream << it.value().lastUsedDate; stream << it.value().sizeOnDisk; } } bool fillInfo(const QString &baseName, MiniCacheFileInfo *mcfi) { QHash::ConstIterator it = m_scoreboard.constFind(CacheIndex(baseName)); if (it == m_scoreboard.constEnd()) { return false; } *mcfi = it.value(); return true; } qint64 runCommand(const QByteArray &cmd) { // execute the command; return number of bytes if a new file was created, zero otherwise. Q_ASSERT(cmd.size() == 80); CacheFileInfo fi; const CacheCleanerCommand ccc = readCommand(cmd, &fi); QString fileName = filePath(fi.baseName); switch (ccc) { case CreateFileNotificationCommand: // qDebug() << "CreateNotificationCommand for" << fi.baseName; if (!readBinaryHeader(cmd, &fi)) { return 0; } break; case UpdateFileCommand: { // qDebug() << "UpdateFileCommand for" << fi.baseName; QFile file(fileName); file.open(QIODevice::ReadWrite); CacheFileInfo fiFromDisk; QByteArray header = file.read(SerializedCacheFileInfo::size); if (!readBinaryHeader(header, &fiFromDisk) || fiFromDisk.bytesCached != fi.bytesCached) { return 0; } // adjust the use count, to make sure that we actually count up. (slaves read the file // asynchronously...) const quint32 newUseCount = fiFromDisk.useCount + 1; QByteArray newHeader = cmd.mid(0, SerializedCacheFileInfo::size); { QDataStream stream(&newHeader, QIODevice::WriteOnly); stream.skipRawData(SerializedCacheFileInfo::useCountOffset); stream << newUseCount; } file.seek(0); file.write(newHeader); file.close(); if (!readBinaryHeader(newHeader, &fi)) { return 0; } break; } default: // qDebug() << "received invalid command"; return 0; } QFileInfo fileInfo(fileName); fi.lastUsedDate = fileInfo.lastModified(); fi.sizeOnDisk = fileInfo.size(); fi.debugPrint(); // a CacheFileInfo is-a MiniCacheFileInfo which enables the following assignment... add(fi); // finally, return cache dir growth (only relevant if a file was actually created!) return ccc == CreateFileNotificationCommand ? fi.sizeOnDisk : 0; } void add(const CacheFileInfo &fi) { m_scoreboard[CacheIndex(fi.baseName)] = fi; } void remove(const QString &basename) { m_scoreboard.remove(CacheIndex(basename)); } // keep memory usage reasonably low - otherwise entries of nonexistent files don't hurt. void maybeRemoveStaleEntries(const QList &fiList) { // don't bother when there are a few bogus entries if (m_scoreboard.count() < fiList.count() + 100) { return; } // qDebug() << "we have too many fake/stale entries, cleaning up..."; QSet realFiles; Q_FOREACH (CacheFileInfo *fi, fiList) { realFiles.insert(CacheIndex(fi->baseName)); } QHash::Iterator it = m_scoreboard.begin(); while (it != m_scoreboard.end()) { if (realFiles.contains(it.key())) { ++it; } else { it = m_scoreboard.erase(it); } } } private: bool readAndValidateMcfi(const QByteArray &rawData, const QString &basename, MiniCacheFileInfo *mcfi) { QDataStream stream(rawData); stream >> mcfi->useCount; // check those against filesystem stream >> mcfi->lastUsedDate; stream >> mcfi->sizeOnDisk; QFileInfo fileInfo(filePath(basename)); if (!fileInfo.exists()) { return false; } bool ok = true; ok = ok && fileInfo.lastModified() == mcfi->lastUsedDate; ok = ok && fileInfo.size() == mcfi->sizeOnDisk; if (!ok) { // size or last-modified date not consistent with entry file; reload useCount // note that avoiding to open the file is the whole purpose of the scoreboard - we only // open the file if we really have to. QFile entryFile(fileInfo.absoluteFilePath()); if (!entryFile.open(QIODevice::ReadOnly)) { return false; } if (entryFile.size() < SerializedCacheFileInfo::size) { return false; } QDataStream stream(&entryFile); stream.skipRawData(SerializedCacheFileInfo::useCountOffset); stream >> mcfi->useCount; mcfi->lastUsedDate = fileInfo.lastModified(); mcfi->sizeOnDisk = fileInfo.size(); ok = true; } return ok; } QHash m_scoreboard; }; // Keep the above in sync with the cache code in http.cpp // !END OF SYNC! // remove files and directories used by earlier versions of the HTTP cache. static void removeOldFiles() { const char *oldDirs = "0abcdefghijklmnopqrstuvwxyz"; const int n = strlen(oldDirs); QDir cacheRootDir(filePath(QString())); for (int i = 0; i < n; i++) { QString dirName = QString::fromLatin1(&oldDirs[i], 1); // delete files in directory... Q_FOREACH (const QString &baseName, QDir(filePath(dirName)).entryList()) { QFile::remove(filePath(dirName + '/' + baseName)); } // delete the (now hopefully empty!) directory itself cacheRootDir.rmdir(dirName); } QFile::remove(filePath(QStringLiteral("cleaned"))); } class CacheCleaner { public: CacheCleaner(const QDir &cacheDir) : m_totalSizeOnDisk(0) { // qDebug(); m_fileNameList = cacheDir.entryList(); } // Delete some of the files that need to be deleted. Return true when done, false otherwise. // This makes interleaved cleaning / serving ioslaves possible. bool processSlice(Scoreboard *scoreboard = nullptr) { QTime t; t.start(); // phase one: gather information about cache files if (!m_fileNameList.isEmpty()) { while (t.elapsed() < 100 && !m_fileNameList.isEmpty()) { QString baseName = m_fileNameList.takeFirst(); // check if the filename is of the $s_hashedUrlNibbles letters, 0...f type if (baseName.length() < s_hashedUrlNibbles) { continue; } bool nameOk = true; for (int i = 0; i < s_hashedUrlNibbles && nameOk; i++) { QChar c = baseName[i]; nameOk = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); } if (!nameOk) { continue; } if (baseName.length() > s_hashedUrlNibbles) { if (QFileInfo(filePath(baseName)).lastModified().secsTo(g_currentDate) > 15 * 60) { // it looks like a temporary file that hasn't been touched in > 15 minutes... QFile::remove(filePath(baseName)); } // the temporary file might still be written to, leave it alone continue; } CacheFileInfo *fi = new CacheFileInfo(); fi->baseName = baseName; bool gotInfo = false; if (scoreboard) { gotInfo = scoreboard->fillInfo(baseName, fi); } if (!gotInfo) { gotInfo = readCacheFile(baseName, fi, CleanCache); if (gotInfo && scoreboard) { scoreboard->add(*fi); } } if (gotInfo) { m_fiList.append(fi); m_totalSizeOnDisk += fi->sizeOnDisk; } else { delete fi; } } // qDebug() << "total size of cache files is" << m_totalSizeOnDisk; if (m_fileNameList.isEmpty()) { // final step of phase one - qSort(m_fiList.begin(), m_fiList.end(), CacheFileInfoPtrLessThan); + std::sort(m_fiList.begin(), m_fiList.end(), CacheFileInfoPtrLessThan); } return false; } // phase two: delete files until cache is under maximum allowed size // TODO: delete files larger than allowed for a single file while (t.elapsed() < 100) { if (m_totalSizeOnDisk <= g_maxCacheSize || m_fiList.isEmpty()) { // qDebug() << "total size of cache files after cleaning is" << m_totalSizeOnDisk; if (scoreboard) { scoreboard->maybeRemoveStaleEntries(m_fiList); scoreboard->writeOut(); } qDeleteAll(m_fiList); m_fiList.clear(); return true; } CacheFileInfo *fi = m_fiList.takeFirst(); QString filename = filePath(fi->baseName); if (QFile::remove(filename)) { m_totalSizeOnDisk -= fi->sizeOnDisk; if (scoreboard) { scoreboard->remove(fi->baseName); } } delete fi; } return false; } private: QStringList m_fileNameList; QList m_fiList; qint64 m_totalSizeOnDisk; }; int main(int argc, char **argv) { QCoreApplication app(argc, argv); app.setApplicationVersion(QStringLiteral("5.0")); KLocalizedString::setApplicationDomain("kio5"); QCommandLineParser parser; parser.addVersionOption(); parser.setApplicationDescription(QCoreApplication::translate("main", "KDE HTTP cache maintenance tool")); parser.addHelpOption(); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("clear-all"), QCoreApplication::translate("main", "Empty the cache"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("file-info"), QCoreApplication::translate("main", "Display information about cache file"), QStringLiteral("filename"))); parser.process(app); OperationMode mode = CleanCache; if (parser.isSet(QStringLiteral("clear-all"))) { mode = DeleteCache; } else if (parser.isSet(QStringLiteral("file-info"))) { mode = FileInfo; } // file info mode: no scanning of directories, just output info and exit. if (mode == FileInfo) { CacheFileInfo fi; if (!readCacheFile(parser.value(QStringLiteral("file-info")), &fi, mode)) { return 1; } fi.prettyPrint(); return 0; } // make sure we're the only running instance of the cleaner service if (mode == CleanCache) { if (!QDBusConnection::sessionBus().isConnected()) { QDBusError error(QDBusConnection::sessionBus().lastError()); fprintf(stderr, "%s: Could not connect to D-Bus! (%s: %s)\n", appName, qPrintable(error.name()), qPrintable(error.message())); return 1; } if (!QDBusConnection::sessionBus().registerService(appFullName)) { fprintf(stderr, "%s: Already running!\n", appName); return 0; } } g_currentDate = QDateTime::currentDateTime(); g_maxCacheAge = KProtocolManager::maxCacheAge(); g_maxCacheSize = mode == DeleteCache ? -1 : KProtocolManager::maxCacheSize() * 1024; QString cacheDirName = cacheDir(); QDir().mkpath(cacheDirName); QDir cacheDir(cacheDirName); if (!cacheDir.exists()) { fprintf(stderr, "%s: '%s' does not exist.\n", appName, qPrintable(cacheDirName)); return 0; } removeOldFiles(); if (mode == DeleteCache) { QTime t; t.start(); cacheDir.refresh(); //qDebug() << "time to refresh the cacheDir QDir:" << t.elapsed(); CacheCleaner cleaner(cacheDir); while (!cleaner.processSlice()) { } return 0; } QLocalServer lServer; QString socketFileName = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QLatin1Char('/') + "kio_http_cache_cleaner"; // we need to create the file by opening the socket, otherwise it won't work QFile::remove(socketFileName); if (!lServer.listen(socketFileName)) { qWarning() << "Error listening on" << socketFileName; } QList sockets; qint64 newBytesCounter = LLONG_MAX; // force cleaner run on startup Scoreboard scoreboard; CacheCleaner *cleaner = nullptr; while (true) { g_currentDate = QDateTime::currentDateTime(); if (cleaner) { QCoreApplication::processEvents(QEventLoop::AllEvents, 100); } else { // We will not immediately know when a socket was disconnected. Causes: // - WaitForMoreEvents does not make processEvents() return when a socket disconnects // - WaitForMoreEvents *and* a timeout is not possible. QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); } if (!lServer.isListening()) { return 1; } lServer.waitForNewConnection(1); while (QLocalSocket *sock = lServer.nextPendingConnection()) { sock->waitForConnected(); sockets.append(sock); } for (int i = 0; i < sockets.size(); i++) { QLocalSocket *sock = sockets[i]; if (sock->state() != QLocalSocket::ConnectedState) { if (sock->state() != QLocalSocket::UnconnectedState) { sock->waitForDisconnected(); } delete sock; sockets.removeAll(sock); i--; continue; } sock->waitForReadyRead(0); while (true) { QByteArray recv = sock->read(80); if (recv.isEmpty()) { break; } Q_ASSERT(recv.size() == 80); newBytesCounter += scoreboard.runCommand(recv); } } // interleave cleaning with serving ioslaves to reduce "garbage collection pauses" if (cleaner) { if (cleaner->processSlice(&scoreboard)) { // that was the last slice, done delete cleaner; cleaner = nullptr; } } else if (newBytesCounter > (g_maxCacheSize / 8)) { cacheDir.refresh(); cleaner = new CacheCleaner(cacheDir); newBytesCounter = 0; } } return 0; } diff --git a/src/kpac/script.cpp b/src/kpac/script.cpp index 2485c54d..57105770 100644 --- a/src/kpac/script.cpp +++ b/src/kpac/script.cpp @@ -1,776 +1,776 @@ /* 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; const QString lower = s.toLower(); 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; QString subnetStr = context->argument(1).toString(); subnetStr += QLatin1Char('/'); subnetStr += 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(); Q_FOREACH (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(QStringLiteral(";")); } 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(); Q_FOREACH (const QHostAddress& address, addresses) { if (!isSpecialAddress(address) && !isLocalHostAddress(address)) { ipAddressList << address.toString(); } } return engine->toScriptValue(ipAddressList.join(QStringLiteral(";"))); } // 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(';')); Q_FOREACH (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()) { - qSort(ipV6List.begin(), ipV6List.end(), addressLessThanComparison); + std::sort(ipV6List.begin(), ipV6List.end(), addressLessThanComparison); sortedAddress += addressListToString(ipV6List, actualEntryMap); } if (!ipV4List.isEmpty()) { - qSort(ipV4List.begin(), ipV4List.end(), addressLessThanComparison); + 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/kpasswdserver/kpasswdserver.cpp b/src/kpasswdserver/kpasswdserver.cpp index 7032ad1b..28b35e49 100644 --- a/src/kpasswdserver/kpasswdserver.cpp +++ b/src/kpasswdserver/kpasswdserver.cpp @@ -1,1123 +1,1123 @@ /* This file is part of the KDE Password Server Copyright (C) 2002 Waldo Bastian (bastian@kde.org) Copyright (C) 2005 David Faure (faure@kde.org) Copyright (C) 2012 Dawit Alemayehu (adawit@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. */ //---------------------------------------------------------------------------- // // KDE Password Server #include "kpasswdserver.h" #include "kpasswdserveradaptor.h" #include #include #include #include #include #ifdef HAVE_KF5WALLET #include #endif #include #include #include static QLoggingCategory category("org.kde.kio.kpasswdserver"); #define AUTHINFO_EXTRAFIELD_DOMAIN QStringLiteral("domain") #define AUTHINFO_EXTRAFIELD_ANONYMOUS QStringLiteral("anonymous") #define AUTHINFO_EXTRAFIELD_BYPASS_CACHE_AND_KWALLET QStringLiteral("bypass-cache-and-kwallet") #define AUTHINFO_EXTRAFIELD_SKIP_CACHING_ON_QUERY QStringLiteral("skip-caching-on-query") #define AUTHINFO_EXTRAFIELD_HIDE_USERNAME_INPUT QStringLiteral("hide-username-line") static qlonglong getRequestId() { static qlonglong nextRequestId = 0; return nextRequestId++; } bool KPasswdServer::AuthInfoContainer::Sorter::operator ()(AuthInfoContainer* n1, AuthInfoContainer* n2) const { if (!n1 || !n2) return 0; const int l1 = n1->directory.length(); const int l2 = n2->directory.length(); return l1 < l2; } KPasswdServer::KPasswdServer(QObject* parent, const QList&) : KDEDModule(parent) { KIO::AuthInfo::registerMetaTypes(); m_seqNr = 0; m_wallet = nullptr; m_walletDisabled = false; KPasswdServerAdaptor *adaptor = new KPasswdServerAdaptor(this); // connect signals to the adaptor connect(this, SIGNAL(checkAuthInfoAsyncResult(qlonglong,qlonglong,KIO::AuthInfo)), adaptor, SIGNAL(checkAuthInfoAsyncResult(qlonglong,qlonglong,KIO::AuthInfo))); connect(this, SIGNAL(queryAuthInfoAsyncResult(qlonglong,qlonglong,KIO::AuthInfo)), adaptor, SIGNAL(queryAuthInfoAsyncResult(qlonglong,qlonglong,KIO::AuthInfo))); connect(this, SIGNAL(windowUnregistered(qlonglong)), this, SLOT(removeAuthForWindowId(qlonglong))); connect(KWindowSystem::self(), SIGNAL(windowRemoved(WId)), this, SLOT(windowRemoved(WId))); } KPasswdServer::~KPasswdServer() { // TODO: what about clients waiting for requests? will they just // notice kpasswdserver is gone from the dbus? qDeleteAll(m_authPending); qDeleteAll(m_authWait); qDeleteAll(m_authDict); qDeleteAll(m_authInProgress); qDeleteAll(m_authRetryInProgress); #ifdef HAVE_KF5WALLET delete m_wallet; #endif } #ifdef HAVE_KF5WALLET // Helper - returns the wallet key to use for read/store/checking for existence. static QString makeWalletKey( const QString& key, const QString& realm ) { return realm.isEmpty() ? key : key + '-' + realm; } // Helper for storeInWallet/readFromWallet static QString makeMapKey( const char* key, int entryNumber ) { QString str = QLatin1String( key ); if ( entryNumber > 1 ) str += '-' + QString::number( entryNumber ); return str; } static bool storeInWallet( KWallet::Wallet* wallet, const QString& key, const KIO::AuthInfo &info ) { if ( !wallet->hasFolder( KWallet::Wallet::PasswordFolder() ) ) if ( !wallet->createFolder( KWallet::Wallet::PasswordFolder() ) ) return false; wallet->setFolder( KWallet::Wallet::PasswordFolder() ); // Before saving, check if there's already an entry with this login. // If so, replace it (with the new password). Otherwise, add a new entry. typedef QMap Map; int entryNumber = 1; Map map; QString walletKey = makeWalletKey( key, info.realmValue ); qCDebug(category) << "walletKey =" << walletKey << " reading existing map"; if ( wallet->readMap( walletKey, map ) == 0 ) { Map::ConstIterator end = map.constEnd(); Map::ConstIterator it = map.constFind( QStringLiteral("login") ); while ( it != end ) { if ( it.value() == info.username ) { break; // OK, overwrite this entry } it = map.constFind( QStringLiteral( "login-" ) + QString::number( ++entryNumber ) ); } // If no entry was found, create a new entry - entryNumber is set already. } const QString loginKey = makeMapKey( "login", entryNumber ); const QString passwordKey = makeMapKey( "password", entryNumber ); qCDebug(category) << "writing to " << loginKey << "," << passwordKey; // note the overwrite=true by default map.insert( loginKey, info.username ); map.insert( passwordKey, info.password ); wallet->writeMap( walletKey, map ); return true; } static bool readFromWallet( KWallet::Wallet* wallet, const QString& key, const QString& realm, QString& username, QString& password, bool userReadOnly, QMap& knownLogins ) { //qCDebug(category) << "key =" << key << " username =" << username << " password =" /*<< password*/ << " userReadOnly =" << userReadOnly << " realm =" << realm; if ( wallet->hasFolder( KWallet::Wallet::PasswordFolder() ) ) { wallet->setFolder( KWallet::Wallet::PasswordFolder() ); QMap map; if ( wallet->readMap( makeWalletKey( key, realm ), map ) == 0 ) { typedef QMap Map; int entryNumber = 1; Map::ConstIterator end = map.constEnd(); Map::ConstIterator it = map.constFind( QStringLiteral("login") ); while ( it != end ) { //qCDebug(category) << "found " << it.key() << "=" << it.value(); Map::ConstIterator pwdIter = map.constFind( makeMapKey( "password", entryNumber ) ); if ( pwdIter != end ) { if ( it.value() == username ) password = pwdIter.value(); knownLogins.insert( it.value(), pwdIter.value() ); } it = map.constFind( QStringLiteral( "login-" ) + QString::number( ++entryNumber ) ); } //qCDebug(category) << knownLogins.count() << " known logins"; if ( !userReadOnly && !knownLogins.isEmpty() && username.isEmpty() ) { // Pick one, any one... username = knownLogins.begin().key(); password = knownLogins.begin().value(); //qCDebug(category) << "picked the first one:" << username; } return true; } } return false; } #endif bool KPasswdServer::hasPendingQuery(const QString &key, const KIO::AuthInfo &info) { const QString path2 (info.url.path().left(info.url.path().indexOf('/')+1)); Q_FOREACH(const Request *request, m_authPending) { if (request->key != key) { continue; } if (info.verifyPath) { const QString path1 (request->info.url.path().left(info.url.path().indexOf('/')+1)); if (!path2.startsWith(path1)) { continue; } } return true; } return false; } // deprecated method, not used anymore. TODO KF6: REMOVE QByteArray KPasswdServer::checkAuthInfo(const QByteArray &data, qlonglong windowId, qlonglong usertime) { KIO::AuthInfo info; QDataStream stream(data); stream >> info; if (usertime != 0) { KUserTimestamp::updateUserTimestamp(usertime); } // if the check depends on a pending query, delay it // until that query is finished. const QString key (createCacheKey(info)); if (hasPendingQuery(key, info)) { setDelayedReply(true); Request *pendingCheck = new Request; pendingCheck->isAsync = false; if (calledFromDBus()) { pendingCheck->transaction = message(); } pendingCheck->key = key; pendingCheck->info = info; m_authWait.append(pendingCheck); return data; // return value will be ignored } // qCDebug(category) << "key =" << key << "user =" << info.username << "windowId =" << windowId; const AuthInfoContainer *result = findAuthInfoItem(key, info); if (!result || result->isCanceled) { #ifdef HAVE_KF5WALLET if (!result && !m_walletDisabled && (info.username.isEmpty() || info.password.isEmpty()) && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(), KWallet::Wallet::PasswordFolder(), makeWalletKey(key, info.realmValue))) { QMap knownLogins; if (openWallet(windowId)) { if (readFromWallet(m_wallet, key, info.realmValue, info.username, info.password, info.readOnly, knownLogins)) { info.setModified(true); // fall through } } } else { info.setModified(false); } #else info.setModified(false); #endif } else { qCDebug(category) << "Found cached authentication for" << key; updateAuthExpire(key, result, windowId, false); copyAuthInfo(result, info); } QByteArray data2; QDataStream stream2(&data2, QIODevice::WriteOnly); stream2 << info; return data2; } qlonglong KPasswdServer::checkAuthInfoAsync(KIO::AuthInfo info, qlonglong windowId, qlonglong usertime) { if (usertime != 0) { KUserTimestamp::updateUserTimestamp(usertime); } // send the request id back to the client qlonglong requestId = getRequestId(); qCDebug(category) << "User =" << info.username << ", WindowId =" << windowId; if (calledFromDBus()) { QDBusMessage reply(message().createReply(requestId)); QDBusConnection::sessionBus().send(reply); } // if the check depends on a pending query, delay it // until that query is finished. const QString key (createCacheKey(info)); if (hasPendingQuery(key, info)) { Request *pendingCheck = new Request; pendingCheck->isAsync = true; pendingCheck->requestId = requestId; pendingCheck->key = key; pendingCheck->info = info; m_authWait.append(pendingCheck); return 0; // ignored as we already sent a reply } const AuthInfoContainer *result = findAuthInfoItem(key, info); if (!result || result->isCanceled) { #ifdef HAVE_KF5WALLET if (!result && !m_walletDisabled && (info.username.isEmpty() || info.password.isEmpty()) && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(), KWallet::Wallet::PasswordFolder(), makeWalletKey(key, info.realmValue))) { QMap knownLogins; if (openWallet(windowId)) { if (readFromWallet(m_wallet, key, info.realmValue, info.username, info.password, info.readOnly, knownLogins)) { info.setModified(true); // fall through } } } else { info.setModified(false); } #else info.setModified(false); #endif } else { // qCDebug(category) << "Found cached authentication for" << key; updateAuthExpire(key, result, windowId, false); copyAuthInfo(result, info); } emit checkAuthInfoAsyncResult(requestId, m_seqNr, info); return 0; // ignored } // deprecated method, not used anymore. TODO KF6: REMOVE QByteArray KPasswdServer::queryAuthInfo(const QByteArray &data, const QString &errorMsg, qlonglong windowId, qlonglong seqNr, qlonglong usertime) { KIO::AuthInfo info; QDataStream stream(data); stream >> info; qCDebug(category) << "User =" << info.username << ", WindowId =" << windowId << "seqNr =" << seqNr << ", errorMsg =" << errorMsg; if ( !info.password.isEmpty() ) { // should we really allow the caller to pre-fill the password? qCDebug(category) << "password was set by caller"; } if (usertime != 0) { KUserTimestamp::updateUserTimestamp(usertime); } const QString key (createCacheKey(info)); Request *request = new Request; setDelayedReply(true); request->isAsync = false; request->transaction = message(); request->key = key; request->info = info; request->windowId = windowId; request->seqNr = seqNr; if (errorMsg == QLatin1String("")) { request->errorMsg.clear(); request->prompt = false; } else { request->errorMsg = errorMsg; request->prompt = true; } m_authPending.append(request); if (m_authPending.count() == 1) QTimer::singleShot(0, this, SLOT(processRequest())); return QByteArray(); // return value is going to be ignored } qlonglong KPasswdServer::queryAuthInfoAsync(const KIO::AuthInfo &info, const QString &errorMsg, qlonglong windowId, qlonglong seqNr, qlonglong usertime) { qCDebug(category) << "User =" << info.username << ", WindowId =" << windowId << "seqNr =" << seqNr << ", errorMsg =" << errorMsg; if (!info.password.isEmpty()) { qCDebug(category) << "password was set by caller"; } if (usertime != 0) { KUserTimestamp::updateUserTimestamp(usertime); } const QString key (createCacheKey(info)); Request *request = new Request; request->isAsync = true; request->requestId = getRequestId(); request->key = key; request->info = info; request->windowId = windowId; request->seqNr = seqNr; if (errorMsg == QLatin1String("")) { request->errorMsg.clear(); request->prompt = false; } else { request->errorMsg = errorMsg; request->prompt = true; } m_authPending.append(request); if (m_authPending.count() == 1) { QTimer::singleShot(0, this, SLOT(processRequest())); } return request->requestId; } void KPasswdServer::addAuthInfo(const KIO::AuthInfo &info, qlonglong windowId) { qCDebug(category) << "User =" << info.username << ", Realm =" << info.realmValue << ", WindowId =" << windowId; const QString key (createCacheKey(info)); m_seqNr++; #ifdef HAVE_KF5WALLET if (!m_walletDisabled && openWallet(windowId) && storeInWallet(m_wallet, key, info)) { // Since storing the password in the wallet succeeded, make sure the // password information is stored in memory only for the duration the // windows associated with it are still around. KIO::AuthInfo authToken (info); authToken.keepPassword = false; addAuthInfoItem(key, authToken, windowId, m_seqNr, false); return; } #endif addAuthInfoItem(key, info, windowId, m_seqNr, false); } // deprecated method, not used anymore. TODO KF6: REMOVE void KPasswdServer::addAuthInfo(const QByteArray &data, qlonglong windowId) { KIO::AuthInfo info; QDataStream stream(data); stream >> info; addAuthInfo(info, windowId); } void KPasswdServer::removeAuthInfo(const QString& host, const QString& protocol, const QString& user) { qCDebug(category) << protocol << host << user; QHashIterator< QString, AuthInfoContainerList* > dictIterator(m_authDict); while (dictIterator.hasNext()) { dictIterator.next(); AuthInfoContainerList *authList = dictIterator.value(); if (!authList) continue; Q_FOREACH(AuthInfoContainer *current, *authList) { qCDebug(category) << "Evaluating: " << current->info.url.scheme() << current->info.url.host() << current->info.username; if (current->info.url.scheme() == protocol && current->info.url.host() == host && (current->info.username == user || user.isEmpty())) { qCDebug(category) << "Removing this entry"; removeAuthInfoItem(dictIterator.key(), current->info); } } } } #ifdef HAVE_KF5WALLET bool KPasswdServer::openWallet( qlonglong windowId ) { if ( m_wallet && !m_wallet->isOpen() ) { // forced closed delete m_wallet; m_wallet = nullptr; } if ( !m_wallet ) m_wallet = KWallet::Wallet::openWallet( KWallet::Wallet::NetworkWallet(), (WId)(windowId)); return m_wallet != nullptr; } #endif void KPasswdServer::processRequest() { if (m_authPending.isEmpty()) { return; } QScopedPointer request (m_authPending.takeFirst()); // Prevent multiple prompts originating from the same window or the same // key (server address). const QString windowIdStr = QString::number(request->windowId); if (m_authPrompted.contains(windowIdStr) || m_authPrompted.contains(request->key)) { m_authPending.prepend(request.take()); // put it back. return; } m_authPrompted.append(windowIdStr); m_authPrompted.append(request->key); KIO::AuthInfo &info = request->info; // NOTE: If info.username is empty and info.url.userName() is not, set // info.username to info.url.userName() to ensure proper caching. See // note passwordDialogDone. if (info.username.isEmpty() && !info.url.userName().isEmpty()) { info.username = info.url.userName(); } const bool bypassCacheAndKWallet = info.getExtraField(AUTHINFO_EXTRAFIELD_BYPASS_CACHE_AND_KWALLET).toBool(); const AuthInfoContainer *result = findAuthInfoItem(request->key, request->info); qCDebug(category) << "key=" << request->key << ", user=" << info.username << "seqNr: request=" << request->seqNr << ", result=" << (result ? result->seqNr : -1); if (!bypassCacheAndKWallet && result && (request->seqNr < result->seqNr)) { qCDebug(category) << "auto retry!"; if (result->isCanceled) { info.setModified(false); } else { updateAuthExpire(request->key, result, request->windowId, false); copyAuthInfo(result, info); } } else { m_seqNr++; if (result && !request->errorMsg.isEmpty()) { QString prompt (request->errorMsg.trimmed()); prompt += QLatin1Char('\n'); prompt += i18n("Do you want to retry?"); QDialog* dlg = new QDialog; connect(dlg, SIGNAL(finished(int)), this, SLOT(retryDialogDone(int))); connect(this, SIGNAL(destroyed(QObject*)), dlg, SLOT(deleteLater())); dlg->setWindowTitle(i18n("Retry Authentication")); dlg->setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-password"))); dlg->setObjectName(QStringLiteral("warningOKCancel")); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Yes|QDialogButtonBox::Cancel); buttonBox->button(QDialogButtonBox::Yes)->setText(i18nc("@action:button filter-continue", "Retry")); KMessageBox::createKMessageBox(dlg, buttonBox, QMessageBox::Warning, prompt, QStringList(), QString(), nullptr, (KMessageBox::Notify | KMessageBox::NoExec)); #ifndef Q_WS_WIN KWindowSystem::setMainWindow(dlg, request->windowId); #else KWindowSystem::setMainWindow(dlg, (HWND)(long)request->windowId); #endif qCDebug(category) << "Calling open on retry dialog" << dlg; m_authRetryInProgress.insert(dlg, request.take()); dlg->open(); return; } if (request->prompt) { showPasswordDialog(request.take()); return; } else { if (!bypassCacheAndKWallet && request->prompt) { addAuthInfoItem(request->key, info, 0, m_seqNr, true); } info.setModified( false ); } } sendResponse(request.data()); } QString KPasswdServer::createCacheKey( const KIO::AuthInfo &info ) { if( !info.url.isValid() ) { // Note that a null key will break findAuthInfoItem later on... qCWarning(category) << "createCacheKey: invalid URL " << info.url ; return QString(); } // Generate the basic key sequence. QString key = info.url.scheme(); key += '-'; if (!info.url.userName().isEmpty()) { key += info.url.userName(); key += '@'; } key += info.url.host(); int port = info.url.port(); if( port ) { key += ':'; key += QString::number(port); } return key; } void KPasswdServer::copyAuthInfo(const AuthInfoContainer *i, KIO::AuthInfo& info) { info = i->info; info.setModified(true); } const KPasswdServer::AuthInfoContainer * KPasswdServer::findAuthInfoItem(const QString &key, const KIO::AuthInfo &info) { // qCDebug(category) << "key=" << key << ", user=" << info.username; AuthInfoContainerList *authList = m_authDict.value(key); if (authList) { QString path2 = info.url.path().left(info.url.path().indexOf('/')+1); Q_FOREACH(AuthInfoContainer *current, *authList) { if (current->expire == AuthInfoContainer::expTime && static_cast(time(nullptr)) > current->expireTime) { authList->removeOne(current); delete current; continue; } if (info.verifyPath) { QString path1 = current->directory; if (path2.startsWith(path1) && (info.username.isEmpty() || info.username == current->info.username)) return current; } else { if (current->info.realmValue == info.realmValue && (info.username.isEmpty() || info.username == current->info.username)) return current; // TODO: Update directory info, } } } return nullptr; } void KPasswdServer::removeAuthInfoItem(const QString &key, const KIO::AuthInfo &info) { AuthInfoContainerList *authList = m_authDict.value(key); if (!authList) return; Q_FOREACH(AuthInfoContainer *current, *authList) { if (current->info.realmValue == info.realmValue) { authList->removeOne(current); delete current; } } if (authList->isEmpty()) { delete m_authDict.take(key); } } void KPasswdServer::addAuthInfoItem(const QString &key, const KIO::AuthInfo &info, qlonglong windowId, qlonglong seqNr, bool canceled) { qCDebug(category) << "key=" << key << "window-id=" << windowId << "username=" << info.username << "realm=" << info.realmValue << "seqNr=" << seqNr << "keepPassword?" << info.keepPassword << "canceled?" << canceled; AuthInfoContainerList *authList = m_authDict.value(key); if (!authList) { authList = new AuthInfoContainerList; m_authDict.insert(key, authList); } AuthInfoContainer *authItem = nullptr; Q_FOREACH(AuthInfoContainer* current, *authList) { if (current->info.realmValue == info.realmValue) { authList->removeAll(current); authItem = current; break; } } if (!authItem) { qCDebug(category) << "Creating AuthInfoContainer"; authItem = new AuthInfoContainer; authItem->expire = AuthInfoContainer::expTime; } authItem->info = info; authItem->directory = info.url.path().left(info.url.path().indexOf('/')+1); authItem->seqNr = seqNr; authItem->isCanceled = canceled; updateAuthExpire(key, authItem, windowId, (info.keepPassword && !canceled)); // Insert into list, keep the list sorted "longest path" first. authList->append(authItem); - qSort(authList->begin(), authList->end(), AuthInfoContainer::Sorter()); + std::sort(authList->begin(), authList->end(), AuthInfoContainer::Sorter()); } void KPasswdServer::updateAuthExpire(const QString &key, const AuthInfoContainer *auth, qlonglong windowId, bool keep) { AuthInfoContainer *current = const_cast(auth); Q_ASSERT(current); qCDebug(category) << "key=" << key << "expire=" << current->expire << "window-id=" << windowId << "keep=" << keep; if (keep && !windowId) { current->expire = AuthInfoContainer::expNever; } else if (windowId && (current->expire != AuthInfoContainer::expNever)) { current->expire = AuthInfoContainer::expWindowClose; if (!current->windowList.contains(windowId)) current->windowList.append(windowId); } else if (current->expire == AuthInfoContainer::expTime) { current->expireTime = time(nullptr) + 10; } // Update mWindowIdList if (windowId) { QStringList& keysChanged = mWindowIdList[windowId]; // find or insert if (!keysChanged.contains(key)) keysChanged.append(key); } } void KPasswdServer::removeAuthForWindowId(qlonglong windowId) { const QStringList keysChanged = mWindowIdList.value(windowId); foreach (const QString &key, keysChanged) { AuthInfoContainerList *authList = m_authDict.value(key); if (!authList) continue; QMutableListIterator it (*authList); while (it.hasNext()) { AuthInfoContainer* current = it.next(); if (current->expire == AuthInfoContainer::expWindowClose) { if (current->windowList.removeAll(windowId) && current->windowList.isEmpty()) { delete current; it.remove(); } } } } } void KPasswdServer::showPasswordDialog (KPasswdServer::Request* request) { KIO::AuthInfo &info = request->info; QString username = info.username; QString password = info.password; bool hasWalletData = false; QMap knownLogins; #ifdef HAVE_KF5WALLET const bool bypassCacheAndKWallet = info.getExtraField(AUTHINFO_EXTRAFIELD_BYPASS_CACHE_AND_KWALLET).toBool(); if ( !bypassCacheAndKWallet && ( username.isEmpty() || password.isEmpty() ) && !m_walletDisabled && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(), KWallet::Wallet::PasswordFolder(), makeWalletKey( request->key, info.realmValue )) ) { // no login+pass provided, check if kwallet has one if ( openWallet( request->windowId ) ) hasWalletData = readFromWallet( m_wallet, request->key, info.realmValue, username, password, info.readOnly, knownLogins ); } #endif // assemble dialog-flags KPasswordDialog::KPasswordDialogFlags dialogFlags; if (info.getExtraField(AUTHINFO_EXTRAFIELD_DOMAIN).isValid()) { dialogFlags |= KPasswordDialog::ShowDomainLine; if (info.getExtraFieldFlags(AUTHINFO_EXTRAFIELD_DOMAIN) & KIO::AuthInfo::ExtraFieldReadOnly) { dialogFlags |= KPasswordDialog::DomainReadOnly; } } if (info.getExtraField(AUTHINFO_EXTRAFIELD_ANONYMOUS).isValid()) { dialogFlags |= KPasswordDialog::ShowAnonymousLoginCheckBox; } if (!info.getExtraField(AUTHINFO_EXTRAFIELD_HIDE_USERNAME_INPUT).toBool()) { dialogFlags |= KPasswordDialog::ShowUsernameLine; } #ifdef HAVE_KF5WALLET // If wallet is not enabled and the caller explicitly requested for it, // do not show the keep password checkbox. if (info.keepPassword && KWallet::Wallet::isEnabled()) dialogFlags |= KPasswordDialog::ShowKeepPassword; #endif // instantiate dialog #ifndef Q_WS_WIN qCDebug(category) << "Widget for" << request->windowId << QWidget::find(request->windowId); #else qCDebug(category) << "Widget for" << request->windowId << QWidget::find((HWND)request->windowId); #endif KPasswordDialog* dlg = new KPasswordDialog(nullptr, dialogFlags); connect(dlg, SIGNAL(finished(int)), this, SLOT(passwordDialogDone(int))); connect(this, SIGNAL(destroyed(QObject*)), dlg, SLOT(deleteLater())); dlg->setPrompt(info.prompt); dlg->setUsername(username); if (info.caption.isEmpty()) dlg->setWindowTitle( i18n("Authentication Dialog") ); else dlg->setWindowTitle( info.caption ); if ( !info.comment.isEmpty() ) dlg->addCommentLine( info.commentLabel, info.comment ); if ( !password.isEmpty() ) dlg->setPassword( password ); if (info.readOnly) dlg->setUsernameReadOnly( true ); else dlg->setKnownLogins( knownLogins ); if (hasWalletData) dlg->setKeepPassword( true ); if (info.getExtraField(AUTHINFO_EXTRAFIELD_DOMAIN).isValid ()) dlg->setDomain(info.getExtraField(AUTHINFO_EXTRAFIELD_DOMAIN).toString()); if (info.getExtraField(AUTHINFO_EXTRAFIELD_ANONYMOUS).isValid () && password.isEmpty() && username.isEmpty()) dlg->setAnonymousMode(info.getExtraField(AUTHINFO_EXTRAFIELD_ANONYMOUS).toBool()); #ifndef Q_OS_MACOS #ifndef Q_WS_WIN KWindowSystem::setMainWindow(dlg, request->windowId); #else KWindowSystem::setMainWindow(dlg, (HWND)request->windowId); #endif #else KWindowSystem::forceActiveWindow(dlg->winId(), 0); #endif qCDebug(category) << "Showing password dialog" << dlg << ", window-id=" << request->windowId; m_authInProgress.insert(dlg, request); dlg->open(); } void KPasswdServer::sendResponse (KPasswdServer::Request* request) { Q_ASSERT(request); if (!request) { return; } qCDebug(category) << "key=" << request->key; if (request->isAsync) { emit queryAuthInfoAsyncResult(request->requestId, m_seqNr, request->info); } else { QByteArray replyData; QDataStream stream2(&replyData, QIODevice::WriteOnly); stream2 << request->info; QDBusConnection::sessionBus().send(request->transaction.createReply(QVariantList() << replyData << m_seqNr)); } // Check all requests in the wait queue. Request *waitRequest; QMutableListIterator it(m_authWait); while (it.hasNext()) { waitRequest = it.next(); if (!hasPendingQuery(waitRequest->key, waitRequest->info)) { const AuthInfoContainer *result = findAuthInfoItem(waitRequest->key, waitRequest->info); QByteArray replyData; QDataStream stream2(&replyData, QIODevice::WriteOnly); KIO::AuthInfo rcinfo; if (!result || result->isCanceled) { waitRequest->info.setModified(false); stream2 << waitRequest->info; } else { updateAuthExpire(waitRequest->key, result, waitRequest->windowId, false); copyAuthInfo(result, rcinfo); stream2 << rcinfo; } if (waitRequest->isAsync) { emit checkAuthInfoAsyncResult(waitRequest->requestId, m_seqNr, rcinfo); } else { QDBusConnection::sessionBus().send(waitRequest->transaction.createReply(QVariantList() << replyData << m_seqNr)); } delete waitRequest; it.remove(); } } // Re-enable password request processing for the current window id again. m_authPrompted.removeAll(QString::number(request->windowId)); m_authPrompted.removeAll(request->key); if (m_authPending.count()) QTimer::singleShot(0, this, SLOT(processRequest())); } void KPasswdServer::passwordDialogDone(int result) { KPasswordDialog* dlg = qobject_cast(sender()); Q_ASSERT(dlg); QScopedPointer request (m_authInProgress.take(dlg)); Q_ASSERT(request); // request should never be NULL. if (request) { KIO::AuthInfo& info = request->info; const bool bypassCacheAndKWallet = info.getExtraField(AUTHINFO_EXTRAFIELD_BYPASS_CACHE_AND_KWALLET).toBool(); qCDebug(category) << "dialog result=" << result << ", bypassCacheAndKWallet?" << bypassCacheAndKWallet; if (dlg && result == QDialog::Accepted) { Q_ASSERT(dlg); info.username = dlg->username(); info.password = dlg->password(); info.keepPassword = dlg->keepPassword(); if (info.getExtraField(AUTHINFO_EXTRAFIELD_DOMAIN).isValid ()) info.setExtraField(AUTHINFO_EXTRAFIELD_DOMAIN, dlg->domain()); if (info.getExtraField(AUTHINFO_EXTRAFIELD_ANONYMOUS).isValid ()) info.setExtraField(AUTHINFO_EXTRAFIELD_ANONYMOUS, dlg->anonymousMode()); // When the user checks "keep password", that means: // * if the wallet is enabled, store it there for long-term, and in kpasswdserver // only for the duration of the window (#92928) // * otherwise store in kpasswdserver for the duration of the KDE session. if (!bypassCacheAndKWallet) { /* NOTE: The following code changes the key under which the auth info is stored in memory if the request url contains a username. e.g. "ftp://user@localhost", but the user changes that username in the password dialog. Since the key generated to store the credential contains the username from the request URL, the key must be updated on such changes. Otherwise, the key will not be found on subsequent requests and the user will be end up being prompted over and over to re-enter the password unnecessarily. */ if (!info.url.userName().isEmpty() && info.username != info.url.userName()) { const QString oldKey(request->key); removeAuthInfoItem(oldKey, info); info.url.setUserName(info.username); request->key = createCacheKey(info); updateCachedRequestKey(m_authPending, oldKey, request->key); updateCachedRequestKey(m_authWait, oldKey, request->key); } #ifdef HAVE_KF5WALLET const bool skipAutoCaching = info.getExtraField(AUTHINFO_EXTRAFIELD_SKIP_CACHING_ON_QUERY).toBool(); if (!skipAutoCaching && info.keepPassword && openWallet(request->windowId)) { if ( storeInWallet( m_wallet, request->key, info ) ) // password is in wallet, don't keep it in memory after window is closed info.keepPassword = false; } #endif addAuthInfoItem(request->key, info, request->windowId, m_seqNr, false); } info.setModified( true ); } else { if (!bypassCacheAndKWallet && request->prompt) { addAuthInfoItem(request->key, info, 0, m_seqNr, true); } info.setModified( false ); } sendResponse(request.data()); } dlg->deleteLater(); } void KPasswdServer::retryDialogDone(int result) { QDialog* dlg = qobject_cast(sender()); Q_ASSERT(dlg); QScopedPointer request (m_authRetryInProgress.take(dlg)); Q_ASSERT(request); if (request) { if (result == QDialogButtonBox::Yes) { showPasswordDialog(request.take()); } else { // NOTE: If the user simply cancels the retry dialog, we remove the // credential stored under this key because the original attempt to // use it has failed. Otherwise, the failed credential would be cached // and used subsequently. // // TODO: decide whether it should be removed from the wallet too. KIO::AuthInfo& info = request->info; removeAuthInfoItem(request->key, request->info); info.setModified(false); sendResponse(request.data()); } } } void KPasswdServer::windowRemoved (WId id) { bool foundMatch = false; if (!m_authInProgress.isEmpty()) { const qlonglong windowId = (qlonglong)(id); QMutableHashIterator it (m_authInProgress); while (it.hasNext()) { it.next(); if (it.value()->windowId == windowId) { Request* request = it.value(); QObject* obj = it.key(); it.remove(); m_authPrompted.removeAll(QString::number(request->windowId)); m_authPrompted.removeAll(request->key); delete obj; delete request; foundMatch = true; } } } if (!foundMatch && !m_authRetryInProgress.isEmpty()) { const qlonglong windowId = (qlonglong)(id); QMutableHashIterator it (m_authRetryInProgress); while (it.hasNext()) { it.next(); if (it.value()->windowId == windowId) { Request* request = it.value(); QObject* obj = it.key(); it.remove(); delete obj; delete request; } } } } void KPasswdServer::updateCachedRequestKey (QList& list, const QString& oldKey, const QString& newKey) { QListIterator it (list); while (it.hasNext()) { Request* r = it.next(); if (r->key == oldKey) { r->key = newKey; } } } diff --git a/src/widgets/kdirmodel.cpp b/src/widgets/kdirmodel.cpp index 8f23d997..66c24ee5 100644 --- a/src/widgets/kdirmodel.cpp +++ b/src/widgets/kdirmodel.cpp @@ -1,1298 +1,1298 @@ /* 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 #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) {} virtual ~KDirModelDirNode() Q_DECL_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 { 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: 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_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 *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); Q_FOREACH (const QUrl &u, 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('/')) { nodePath += '/'; } 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('/', 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 = "[index for " + node->item().url().toString(); if (index.column() > 0) { str += ", column " + QString::number(index.column()); } str += ']'; } 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, SIGNAL(itemsAdded(QUrl,KFileItemList)), this, SLOT(_k_slotNewItems(QUrl,KFileItemList))); connect(d->m_dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(_k_slotDeleteItems(KFileItemList))); connect(d->m_dirLister, SIGNAL(refreshItems(QList >)), this, SLOT(_k_slotRefreshItems(QList >))); connect(d->m_dirLister, SIGNAL(clear()), this, SLOT(_k_slotClear())); connect(d->m_dirLister, SIGNAL(redirection(QUrl,QUrl)), this, SLOT(_k_slotRedirection(QUrl,QUrl))); } 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); Q_FOREACH (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; 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); } } } } } m_urlsBeingFetched.remove(dirNode); 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_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); Q_FOREACH (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 : 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 : 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('/', '\\'); 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)); 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. return 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(); } static bool lessThan(const QUrl &left, const QUrl &right) { return left.toString().compare(right.toString()) < 0; } 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.count()) { return urls; } QList ret(urls); - qSort(ret.begin(), ret.end(), lessThan); + std::sort(ret.begin(), ret.end(), lessThan); 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; bool canUseMostLocalUrls = true; foreach (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/kfileitemactions.cpp b/src/widgets/kfileitemactions.cpp index a036ba6f..567f6cd4 100644 --- a/src/widgets/kfileitemactions.cpp +++ b/src/widgets/kfileitemactions.cpp @@ -1,813 +1,813 @@ /* This file is part of the KDE project Copyright (C) 1998-2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfileitemactions.h" #include "kfileitemactions_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool KIOSKAuthorizedAction(const KConfigGroup &cfg) { if (!cfg.hasKey("X-KDE-AuthorizeAction")) { return true; } const QStringList list = cfg.readEntry("X-KDE-AuthorizeAction", QStringList()); for (QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { if (!KAuthorized::authorize((*it).trimmed())) { return false; } } return true; } static bool mimeTypeListContains(const QStringList &list, const KFileItem &item) { const QString itemMimeType = item.mimetype(); foreach (const QString &i, list) { if (i == itemMimeType || i == QLatin1String("all/all")) { return true; } if (item.isFile() && (i == QLatin1String("allfiles") || i == QLatin1String("all/allfiles") || i == QLatin1String("application/octet-stream"))) { return true; } if (item.currentMimeType().inherits(i)) { return true; } const int iSlashPos = i.indexOf('/'); Q_ASSERT(i > nullptr); const QStringRef iSubType = i.midRef(iSlashPos+1); if (iSubType == "*") { const int itemSlashPos = itemMimeType.indexOf('/'); Q_ASSERT(itemSlashPos > 0); const QStringRef iTopLevelType = i.midRef(0, iSlashPos); const QStringRef itemTopLevelType = itemMimeType.midRef(0, itemSlashPos); if (itemTopLevelType == iTopLevelType) { return true; } } } return false; } // This helper class stores the .desktop-file actions and the servicemenus // in order to support X-KDE-Priority and X-KDE-Submenu. namespace KIO { class PopupServices { public: ServiceList &selectList(const QString &priority, const QString &submenuName); ServiceList builtin; ServiceList user, userToplevel, userPriority; QMap userSubmenus, userToplevelSubmenus, userPrioritySubmenus; }; ServiceList &PopupServices::selectList(const QString &priority, const QString &submenuName) { // we use the categories .desktop entry to define submenus // if none is defined, we just pop it in the main menu if (submenuName.isEmpty()) { if (priority == QLatin1String("TopLevel")) { return userToplevel; } else if (priority == QLatin1String("Important")) { return userPriority; } } else if (priority == QLatin1String("TopLevel")) { return userToplevelSubmenus[submenuName]; } else if (priority == QLatin1String("Important")) { return userPrioritySubmenus[submenuName]; } else { return userSubmenus[submenuName]; } return user; } } // namespace //// KFileItemActionsPrivate::KFileItemActionsPrivate(KFileItemActions *qq) : QObject(), q(qq), m_executeServiceActionGroup(static_cast(nullptr)), m_runApplicationActionGroup(static_cast(nullptr)), m_parentWidget(nullptr), m_config(QStringLiteral("kservicemenurc"), KConfig::NoGlobals) { QObject::connect(&m_executeServiceActionGroup, SIGNAL(triggered(QAction*)), this, SLOT(slotExecuteService(QAction*))); QObject::connect(&m_runApplicationActionGroup, SIGNAL(triggered(QAction*)), this, SLOT(slotRunApplication(QAction*))); } KFileItemActionsPrivate::~KFileItemActionsPrivate() { } int KFileItemActionsPrivate::insertServicesSubmenus(const QMap &submenus, QMenu *menu, bool isBuiltin) { int count = 0; QMap::ConstIterator it; for (it = submenus.begin(); it != submenus.end(); ++it) { if (it.value().isEmpty()) { //avoid empty sub-menus continue; } QMenu *actionSubmenu = new QMenu(menu); actionSubmenu->setTitle(it.key()); actionSubmenu->menuAction()->setObjectName(QStringLiteral("services_submenu")); // for the unittest menu->addMenu(actionSubmenu); count += insertServices(it.value(), actionSubmenu, isBuiltin); } return count; } int KFileItemActionsPrivate::insertServices(const ServiceList &list, QMenu *menu, bool isBuiltin) { int count = 0; ServiceList::const_iterator it = list.begin(); for (; it != list.end(); ++it) { if ((*it).isSeparator()) { const QList actions = menu->actions(); if (!actions.isEmpty() && !actions.last()->isSeparator()) { menu->addSeparator(); } continue; } if (isBuiltin || !(*it).noDisplay()) { QAction *act = new QAction(q); act->setObjectName(QStringLiteral("menuaction")); // for the unittest QString text = (*it).text(); text.replace('&', QLatin1String("&&")); act->setText(text); if (!(*it).icon().isEmpty()) { act->setIcon(QIcon::fromTheme((*it).icon())); } act->setData(QVariant::fromValue(*it)); m_executeServiceActionGroup.addAction(act); menu->addAction(act); // Add to toplevel menu ++count; } } return count; } void KFileItemActionsPrivate::slotExecuteService(QAction *act) { KServiceAction serviceAction = act->data().value(); if (KAuthorized::authorizeAction(serviceAction.name())) { KDesktopFileActions::executeService(m_props.urlList(), serviceAction); } } //// KFileItemActions::KFileItemActions(QObject *parent) : QObject(parent), d(new KFileItemActionsPrivate(this)) { } KFileItemActions::~KFileItemActions() { delete d; } void KFileItemActions::setItemListProperties(const KFileItemListProperties &itemListProperties) { d->m_props = itemListProperties; d->m_mimeTypeList.clear(); const KFileItemList items = d->m_props.items(); KFileItemList::const_iterator kit = items.constBegin(); const KFileItemList::const_iterator kend = items.constEnd(); for (; kit != kend; ++kit) { if (!d->m_mimeTypeList.contains((*kit).mimetype())) { d->m_mimeTypeList << (*kit).mimetype(); } } } int KFileItemActions::addServiceActionsTo(QMenu *mainMenu) { const KFileItemList items = d->m_props.items(); const KFileItem firstItem = items.first(); const QString protocol = firstItem.url().scheme(); // assumed to be the same for all items const bool isLocal = !firstItem.localPath().isEmpty(); const bool isSingleLocal = items.count() == 1 && isLocal; const QList urlList = d->m_props.urlList(); KIO::PopupServices s; // 1 - Look for builtin and user-defined services if (isSingleLocal && (d->m_props.mimeType() == QLatin1String("application/x-desktop") || // .desktop file d->m_props.mimeType() == QLatin1String("inode/blockdevice"))) { // dev file // get builtin services, like mount/unmount const QString path = firstItem.localPath(); s.builtin = KDesktopFileActions::builtinServices(QUrl::fromLocalFile(path)); KDesktopFile desktopFile(path); KConfigGroup cfg = desktopFile.desktopGroup(); const QString priority = cfg.readEntry("X-KDE-Priority"); const QString submenuName = cfg.readEntry("X-KDE-Submenu"); #if 0 if (cfg.readEntry("Type") == "Link") { d->m_url = cfg.readEntry("URL"); // TODO: Do we want to make all the actions apply on the target // of the .desktop file instead of the .desktop file itself? } #endif ServiceList &list = s.selectList(priority, submenuName); list = KDesktopFileActions::userDefinedServices(path, desktopFile, true /*isLocal*/); } // 2 - Look for "servicemenus" bindings (user-defined services) // first check the .directory if this is a directory if (d->m_props.isDirectory() && isSingleLocal) { QString dotDirectoryFile = QUrl::fromLocalFile(firstItem.localPath()).path().append("/.directory"); if (QFile::exists(dotDirectoryFile)) { const KDesktopFile desktopFile(dotDirectoryFile); const KConfigGroup cfg = desktopFile.desktopGroup(); if (KIOSKAuthorizedAction(cfg)) { const QString priority = cfg.readEntry("X-KDE-Priority"); const QString submenuName = cfg.readEntry("X-KDE-Submenu"); ServiceList &list = s.selectList(priority, submenuName); list += KDesktopFileActions::userDefinedServices(dotDirectoryFile, desktopFile, true); } } } const KConfigGroup showGroup = d->m_config.group("Show"); const QString commonMimeType = d->m_props.mimeType(); const QString commonMimeGroup = d->m_props.mimeGroup(); const QMimeDatabase db; const QMimeType mimeTypePtr = commonMimeType.isEmpty() ? QMimeType() : db.mimeTypeForName(commonMimeType); const KService::List entries = KServiceTypeTrader::self()->query(QStringLiteral("KonqPopupMenu/Plugin")); KService::List::const_iterator eEnd = entries.end(); for (KService::List::const_iterator it2 = entries.begin(); it2 != eEnd; ++it2) { QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + (*it2)->entryPath()); KDesktopFile desktopFile(file); const KConfigGroup cfg = desktopFile.desktopGroup(); if (!KIOSKAuthorizedAction(cfg)) { continue; } if (cfg.hasKey("X-KDE-ShowIfRunning")) { const QString app = cfg.readEntry("X-KDE-ShowIfRunning"); if (QDBusConnection::sessionBus().interface()->isServiceRegistered(app)) { continue; } } if (cfg.hasKey("X-KDE-ShowIfDBusCall")) { QString calldata = cfg.readEntry("X-KDE-ShowIfDBusCall"); const QStringList parts = calldata.split(' '); const QString &app = parts.at(0); const QString &obj = parts.at(1); QString interface = parts.at(2); QString method; int pos = interface.lastIndexOf(QLatin1Char('.')); if (pos != -1) { method = interface.mid(pos + 1); interface.truncate(pos); } //if (!QDBus::sessionBus().busService()->nameHasOwner(app)) // continue; //app does not exist so cannot send call QDBusMessage reply = QDBusInterface(app, obj, interface). call(method, QUrl::toStringList(urlList)); if (reply.arguments().count() < 1 || reply.arguments().at(0).type() != QVariant::Bool || !reply.arguments().at(0).toBool()) { continue; } } if (cfg.hasKey("X-KDE-Protocol")) { const QString theProtocol = cfg.readEntry("X-KDE-Protocol"); if (theProtocol.startsWith('!')) { const QString excludedProtocol = theProtocol.mid(1); if (excludedProtocol == protocol) { continue; } } else if (protocol != theProtocol) { continue; } } else if (cfg.hasKey("X-KDE-Protocols")) { const QStringList protocols = cfg.readEntry("X-KDE-Protocols", QStringList()); if (!protocols.contains(protocol)) { continue; } } else if (protocol == QLatin1String("trash")) { // Require servicemenus for the trash to ask for protocol=trash explicitly. // Trashed files aren't supposed to be available for actions. // One might want a servicemenu for trash.desktop itself though. continue; } if (cfg.hasKey("X-KDE-Require")) { const QStringList capabilities = cfg.readEntry("X-KDE-Require", QStringList()); if (capabilities.contains(QStringLiteral("Write")) && !d->m_props.supportsWriting()) { continue; } } if (cfg.hasKey("X-KDE-RequiredNumberOfUrls")) { const QStringList requiredNumberOfUrls = cfg.readEntry("X-KDE-RequiredNumberOfUrls", QStringList()); bool matchesAtLeastOneCriterion = false; for (const QString &criterion : requiredNumberOfUrls) { const int number = criterion.toInt(); if (number < 1) { continue; } if (urlList.count() == number) { matchesAtLeastOneCriterion = true; break; } } if (!matchesAtLeastOneCriterion) { continue; } } if (cfg.hasKey("Actions") || cfg.hasKey("X-KDE-GetActionMenu")) { // Like KService, we support ServiceTypes, X-KDE-ServiceTypes, and MimeType. const QStringList types = cfg.readEntry("ServiceTypes", QStringList()) << cfg.readEntry("X-KDE-ServiceTypes", QStringList()) << cfg.readXdgListEntry("MimeType"); if (types.isEmpty()) { continue; } const QStringList excludeTypes = cfg.readEntry("ExcludeServiceTypes", QStringList()); const bool ok = std::all_of(items.constBegin(), items.constEnd(), [&types, &excludeTypes, this](const KFileItem &i) { const QString mimetype = i.mimetype(); return mimeTypeListContains(types, i) && !mimeTypeListContains(excludeTypes, i); }); if (ok) { const QString priority = cfg.readEntry("X-KDE-Priority"); const QString submenuName = cfg.readEntry("X-KDE-Submenu"); ServiceList &list = s.selectList(priority, submenuName); const ServiceList userServices = KDesktopFileActions::userDefinedServices(*(*it2), isLocal, urlList); foreach (const KServiceAction &action, userServices) { if (showGroup.readEntry(action.name(), true)) { list += action; } } } } } QMenu *actionMenu = mainMenu; int userItemCount = 0; if (s.user.count() + s.userSubmenus.count() + s.userPriority.count() + s.userPrioritySubmenus.count() > 1) { // we have more than one item, so let's make a submenu actionMenu = new QMenu(i18nc("@title:menu", "&Actions"), mainMenu); actionMenu->menuAction()->setObjectName(QStringLiteral("actions_submenu")); // for the unittest mainMenu->addMenu(actionMenu); } userItemCount += d->insertServicesSubmenus(s.userPrioritySubmenus, actionMenu, false); userItemCount += d->insertServices(s.userPriority, actionMenu, false); // see if we need to put a separator between our priority items and our regular items if (userItemCount > 0 && (s.user.count() > 0 || s.userSubmenus.count() > 0 || s.builtin.count() > 0) && !actionMenu->actions().last()->isSeparator()) { actionMenu->addSeparator(); } userItemCount += d->insertServicesSubmenus(s.userSubmenus, actionMenu, false); userItemCount += d->insertServices(s.user, actionMenu, false); userItemCount += d->insertServices(s.builtin, mainMenu, true); userItemCount += d->insertServicesSubmenus(s.userToplevelSubmenus, mainMenu, false); userItemCount += d->insertServices(s.userToplevel, mainMenu, false); return userItemCount; } int KFileItemActions::addPluginActionsTo(QMenu *mainMenu) { QStringList addedPlugins; const QString commonMimeType = d->m_props.mimeType(); const QString commonMimeGroup = d->m_props.mimeGroup(); const QMimeDatabase db; int itemCount = 0; const KConfigGroup showGroup = d->m_config.group("Show"); const KService::List fileItemPlugins = KMimeTypeTrader::self()->query(commonMimeType, QStringLiteral("KFileItemAction/Plugin"), QStringLiteral("exist Library")); for(const auto &service : fileItemPlugins) { if (!showGroup.readEntry(service->desktopEntryName(), true)) { // The plugin has been disabled continue; } KAbstractFileItemActionPlugin *abstractPlugin = service->createInstance(); if (abstractPlugin) { abstractPlugin->setParent(mainMenu); auto actions = abstractPlugin->actions(d->m_props, d->m_parentWidget); itemCount += actions.count(); mainMenu->addActions(actions); addedPlugins.append(service->desktopEntryName()); } } const auto jsonPlugins = KPluginLoader::findPlugins(QStringLiteral("kf5/kfileitemaction"), [&db, commonMimeType](const KPluginMetaData& metaData) { if (!metaData.serviceTypes().contains(QStringLiteral("KFileItemAction/Plugin"))) { return false; } auto mimeType = db.mimeTypeForName(commonMimeType); foreach (const auto& supportedMimeType, metaData.mimeTypes()) { if (mimeType.inherits(supportedMimeType)) { return true; } } return false; }); foreach (const auto& jsonMetadata, jsonPlugins) { // The plugin has been disabled if (!showGroup.readEntry(jsonMetadata.pluginId(), true)) { continue; } // The plugin also has a .desktop file and has already been added. if (addedPlugins.contains(jsonMetadata.pluginId())) { continue; } KPluginFactory *factory = KPluginLoader(jsonMetadata.fileName()).factory(); if (!factory) { continue; } KAbstractFileItemActionPlugin* abstractPlugin = factory->create(); if (abstractPlugin) { abstractPlugin->setParent(this); auto actions = abstractPlugin->actions(d->m_props, d->m_parentWidget); itemCount += actions.count(); mainMenu->addActions(actions); addedPlugins.append(jsonMetadata.pluginId()); } } return itemCount; } // static KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList, const QString &traderConstraint) { if (!KAuthorized::authorizeAction(QStringLiteral("openwith")) || mimeTypeList.isEmpty()) { return KService::List(); } const KService::List firstOffers = KMimeTypeTrader::self()->query(mimeTypeList.first(), QStringLiteral("Application"), traderConstraint); QList rankings; QStringList serviceList; // This section does two things. First, it determines which services are common to all the given mimetypes. // Second, it ranks them based on their preference level in the associated applications list. // The more often a service appear near the front of the list, the LOWER its score. for (int i = 0; i < firstOffers.count(); ++i) { KFileItemActionsPrivate::ServiceRank tempRank; tempRank.service = firstOffers[i]; tempRank.score = i; rankings << tempRank; serviceList << tempRank.service->storageId(); } for (int j = 1; j < mimeTypeList.count(); ++j) { QStringList subservice; // list of services that support this mimetype const KService::List offers = KMimeTypeTrader::self()->query(mimeTypeList[j], QStringLiteral("Application"), traderConstraint); for (int i = 0; i != offers.count(); ++i) { const QString serviceId = offers[i]->storageId(); subservice << serviceId; const int idPos = serviceList.indexOf(serviceId); if (idPos != -1) { rankings[idPos].score += i; } // else: we ignore the services that didn't support the previous mimetypes } // Remove services which supported the previous mimetypes but don't support this one for (int i = 0; i < serviceList.count(); ++i) { if (!subservice.contains(serviceList[i])) { serviceList.removeAt(i); rankings.removeAt(i); --i; } } // Nothing left -> there is no common application for these mimetypes if (rankings.isEmpty()) { return KService::List(); } } - qSort(rankings.begin(), rankings.end(), KFileItemActionsPrivate::lessRank); + std::sort(rankings.begin(), rankings.end(), KFileItemActionsPrivate::lessRank); KService::List result; Q_FOREACH (const KFileItemActionsPrivate::ServiceRank &tempRank, rankings) { result << tempRank.service; } return result; } // KMimeTypeTrader::preferredService doesn't take a constraint static KService::Ptr preferredService(const QString &mimeType, const QString &constraint) { const KService::List services = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("Application"), constraint); return !services.isEmpty() ? services.first() : KService::Ptr(); } void KFileItemActions::addOpenWithActionsTo(QMenu *topMenu, const QString &traderConstraint) { if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) { return; } d->m_traderConstraint = traderConstraint; KService::List offers = associatedApplications(d->m_mimeTypeList, traderConstraint); //// Ok, we have everything, now insert const KFileItemList items = d->m_props.items(); const KFileItem firstItem = items.first(); const bool isLocal = firstItem.url().isLocalFile(); // "Open With..." for folders is really not very useful, especially for remote folders. // (media:/something, or trash:/, or ftp://...) if (!d->m_props.isDirectory() || isLocal) { if (!topMenu->actions().isEmpty()) { topMenu->addSeparator(); } QAction *runAct = new QAction(this); QString runActionName; const QStringList serviceIdList = d->listPreferredServiceIds(d->m_mimeTypeList, traderConstraint); //qDebug() << "serviceIdList=" << serviceIdList; // When selecting files with multiple mimetypes, offer either "open with " // or a generic (if there are any apps associated). if (d->m_mimeTypeList.count() > 1 && !serviceIdList.isEmpty() && !(serviceIdList.count() == 1 && serviceIdList.first().isEmpty())) { // empty means "no apps associated" if (serviceIdList.count() == 1) { const KService::Ptr app = preferredService(d->m_mimeTypeList.first(), traderConstraint); runActionName = i18n("&Open with %1", app->name()); runAct->setIcon(QIcon::fromTheme(app->icon())); // Remove that app from the offers list (#242731) for (int i = 0; i < offers.count(); ++i) { if (offers[i]->storageId() == app->storageId()) { offers.removeAt(i); break; } } } else { runActionName = i18n("&Open"); } runAct->setText(runActionName); d->m_traderConstraint = traderConstraint; d->m_fileOpenList = d->m_props.items(); QObject::connect(runAct, SIGNAL(triggered()), d, SLOT(slotRunPreferredApplications())); topMenu->addAction(runAct); } if (!offers.isEmpty()) { QMenu *menu = topMenu; if (offers.count() > 1) { // submenu 'open with' menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu); menu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // for the unittest topMenu->addMenu(menu); } //qDebug() << offers.count() << "offers" << topMenu << menu; KService::List::ConstIterator it = offers.constBegin(); for (; it != offers.constEnd(); it++) { QAction *act = d->createAppAction(*it, // no submenu -> prefix single offer menu == topMenu); menu->addAction(act); } QString openWithActionName; if (menu != topMenu) { // submenu menu->addSeparator(); openWithActionName = i18nc("@action:inmenu Open With", "&Other..."); } else { openWithActionName = i18nc("@title:menu", "&Open With..."); } QAction *openWithAct = new QAction(this); openWithAct->setText(openWithActionName); openWithAct->setObjectName(QStringLiteral("openwith_browse")); // for the unittest QObject::connect(openWithAct, SIGNAL(triggered()), d, SLOT(slotOpenWithDialog())); menu->addAction(openWithAct); } else { // no app offers -> Open With... QAction *act = new QAction(this); act->setText(i18nc("@title:menu", "&Open With...")); act->setObjectName(QStringLiteral("openwith")); // for the unittest QObject::connect(act, SIGNAL(triggered()), d, SLOT(slotOpenWithDialog())); topMenu->addAction(act); } } } void KFileItemActionsPrivate::slotRunPreferredApplications() { const KFileItemList fileItems = m_fileOpenList; const QStringList mimeTypeList = listMimeTypes(fileItems); const QStringList serviceIdList = listPreferredServiceIds(mimeTypeList, m_traderConstraint); foreach (const QString& serviceId, serviceIdList) { KFileItemList serviceItems; foreach (const KFileItem &item, fileItems) { const KService::Ptr serv = preferredService(item.mimetype(), m_traderConstraint); const QString preferredServiceId = serv ? serv->storageId() : QString(); if (preferredServiceId == serviceId) { serviceItems << item; } } if (serviceId.isEmpty()) { // empty means: no associated app for this mimetype openWithByMime(serviceItems); continue; } const KService::Ptr servicePtr = KService::serviceByStorageId(serviceId); if (!servicePtr) { KRun::displayOpenWithDialog(serviceItems.urlList(), m_parentWidget); continue; } KRun::runService(*servicePtr, serviceItems.urlList(), m_parentWidget); } } void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList, const QString &traderConstraint) { d->m_fileOpenList = fileOpenList; d->m_traderConstraint = traderConstraint; d->slotRunPreferredApplications(); } void KFileItemActionsPrivate::openWithByMime(const KFileItemList &fileItems) { const QStringList mimeTypeList = listMimeTypes(fileItems); foreach (const QString& mimeType, mimeTypeList) { KFileItemList mimeItems; foreach (const KFileItem &item, fileItems) { if (item.mimetype() == mimeType) { mimeItems << item; } } KRun::displayOpenWithDialog(mimeItems.urlList(), m_parentWidget); } } void KFileItemActionsPrivate::slotRunApplication(QAction *act) { // Is it an application, from one of the "Open With" actions KService::Ptr app = act->data().value(); Q_ASSERT(app); if (app) { KRun::runService(*app, m_props.urlList(), m_parentWidget); } } void KFileItemActionsPrivate::slotOpenWithDialog() { // The item 'Other...' or 'Open With...' has been selected emit q->openWithDialogAboutToBeShown(); KRun::displayOpenWithDialog(m_props.urlList(), m_parentWidget); } QStringList KFileItemActionsPrivate::listMimeTypes(const KFileItemList &items) { QStringList mimeTypeList; foreach (const KFileItem &item, items) { if (!mimeTypeList.contains(item.mimetype())) { mimeTypeList << item.mimetype(); } } return mimeTypeList; } QStringList KFileItemActionsPrivate::listPreferredServiceIds(const QStringList &mimeTypeList, const QString &traderConstraint) { QStringList serviceIdList; Q_FOREACH (const QString &mimeType, mimeTypeList) { const KService::Ptr serv = preferredService(mimeType, traderConstraint); const QString newOffer = serv ? serv->storageId() : QString(); serviceIdList << newOffer; } serviceIdList.removeDuplicates(); return serviceIdList; } QAction *KFileItemActionsPrivate::createAppAction(const KService::Ptr &service, bool singleOffer) { QString actionName(service->name().replace('&', QLatin1String("&&"))); if (singleOffer) { actionName = i18n("Open &with %1", actionName); } else { actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName); } QAction *act = new QAction(q); act->setObjectName(QStringLiteral("openwith")); // for the unittest act->setIcon(QIcon::fromTheme(service->icon())); act->setText(actionName); act->setData(QVariant::fromValue(service)); m_runApplicationActionGroup.addAction(act); return act; } QAction *KFileItemActions::preferredOpenWithAction(const QString &traderConstraint) { const KService::List offers = associatedApplications(d->m_mimeTypeList, traderConstraint); if (offers.isEmpty()) { return nullptr; } return d->createAppAction(offers.first(), true); } void KFileItemActions::setParentWidget(QWidget *widget) { d->m_parentWidget = widget; }