/* This file is part of the KDE libraries SPDX-FileCopyrightText: 2000 David Smith SPDX-FileCopyrightText: 2004 Scott Wheeler This class was inspired by a previous KUrlCompletion by SPDX-FileContributor: Henner Zeller SPDX-License-Identifier: LGPL-2.0-or-later */ #include "kurlcompletion.h" #include "../utils_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // QT_LSTAT, QT_STAT, QT_STATBUF #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #else #include #include #endif static bool expandTilde(QString &); static bool expandEnv(QString &); static QString unescape(const QString &text); // Permission mask for files that are executable by // user, group or other static constexpr mode_t s_modeExe = S_IXUSR | S_IXGRP | S_IXOTH; // Constants for types of completion enum ComplType { CTNone = 0, CTEnv, CTUser, CTMan, CTExe, CTFile, CTUrl, CTInfo }; class CompletionThread; // Ensure that we don't end up with "//". static void addPathToUrl(QUrl &url, const QString &relPath) { url.setPath(Utils::concatPaths(url.path(), relPath)); } static QBasicAtomicInt s_waitDuration = Q_BASIC_ATOMIC_INITIALIZER(-1); static int initialWaitDuration() { if (s_waitDuration.loadRelaxed() == -1) { const QByteArray envVar = qgetenv("KURLCOMPLETION_WAIT"); if (envVar.isEmpty()) { s_waitDuration = 200; // default: 200 ms } else { s_waitDuration = envVar.toInt(); } } return s_waitDuration; } // For local paths we use our custom comparer function that ignores the trailing slash character static void sortLocalPaths(QStringList &list) { QCollator c; c.setCaseSensitivity(Qt::CaseSensitive); std::sort(list.begin(), list.end(), [c](const QString &a, const QString &b) { return c.compare(a.endsWith(QStringLiteral("/")) ? a.chopped(1) : a, b.endsWith(QStringLiteral("/")) ? b.chopped(1) : b) < 0; }); } /////////////////////////////////////////////////////// /////////////////////////////////////////////////////// // KUrlCompletionPrivate // class KUrlCompletionPrivate { public: explicit KUrlCompletionPrivate(KUrlCompletion *qq, KUrlCompletion::Mode m) : q(qq) , cwd(QUrl::fromLocalFile(QDir::homePath())) , mode(m) { // Read settings KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("URLCompletion")); url_auto_completion = cg.readEntry("alwaysAutoComplete", true); popup_append_slash = cg.readEntry("popupAppendSlash", true); onlyLocalProto = cg.readEntry("LocalProtocolsOnly", false); q->setIgnoreCase(true); } ~KUrlCompletionPrivate(); void slotEntries(KIO::Job *, const KIO::UDSEntryList &); void slotIOFinished(KJob *); void slotCompletionThreadDone(QThread *thread, const QStringList &matches); class MyURL; bool userCompletion(const MyURL &url, QString *match); bool envCompletion(const MyURL &url, QString *match); bool exeCompletion(const MyURL &url, QString *match); bool fileCompletion(const MyURL &url, QString *match); bool urlCompletion(const MyURL &url, QString *match); bool isAutoCompletion(); // List the next dir in m_dirs QString listDirectories(const QStringList &, const QString &, bool only_exe = false, bool only_dir = false, bool no_hidden = false, bool stat_files = true); void listUrls(const QList &urls, const QString &filter = QString(), bool only_exe = false, bool no_hidden = false); void addMatches(const QStringList &); QString finished(); void init(); void setListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false); bool isListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false); KUrlCompletion *const q; QList list_urls; bool onlyLocalProto = false; // urlCompletion() in Auto/Popup mode? bool url_auto_completion = true; // Append '/' to directories in Popup mode? // Doing that stat's all files and is slower bool popup_append_slash = true; // Keep track of currently listed files to avoid reading them again bool last_no_hidden = false; QString last_path_listed; QString last_file_listed; QString last_prepend; ComplType last_compl_type = CTNone; QUrl cwd; // "current directory" = base dir for completion KUrlCompletion::Mode mode = KUrlCompletion::FileCompletion; bool replace_env = true; bool replace_home = true; bool complete_url; // if true completing a URL (i.e. 'prepend' is a URL), otherwise a path KIO::ListJob *list_job = nullptr; // kio job to list directories QString prepend; // text to prepend to listed items QString compl_text; // text to pass on to KCompletion // Filters for files read with kio bool list_urls_only_exe; // true = only list executables bool list_urls_no_hidden; QString list_urls_filter; // filter for listed files CompletionThread *userListThread = nullptr; CompletionThread *dirListThread = nullptr; QStringList mimeTypeFilters; }; class CompletionThread : public QThread { Q_OBJECT protected: CompletionThread(KUrlCompletionPrivate *receiver) : QThread() , m_prepend(receiver->prepend) , m_complete_url(receiver->complete_url) , m_terminationRequested(false) { } public: void requestTermination() { if (!isFinished()) { qCDebug(KIO_WIDGETS) << "stopping thread" << this; } m_terminationRequested.storeRelaxed(true); wait(); } QStringList matches() const { QMutexLocker locker(&m_mutex); return m_matches; } Q_SIGNALS: void completionThreadDone(QThread *thread, const QStringList &matches); protected: void addMatch(const QString &match) { QMutexLocker locker(&m_mutex); m_matches.append(match); } bool terminationRequested() const { return m_terminationRequested.loadRelaxed(); } void done() { if (!terminationRequested()) { qCDebug(KIO_WIDGETS) << "done, emitting signal with" << m_matches.count() << "matches"; Q_EMIT completionThreadDone(this, m_matches); } } const QString m_prepend; const bool m_complete_url; // if true completing a URL (i.e. 'm_prepend' is a URL), otherwise a path private: mutable QMutex m_mutex; // protects m_matches QStringList m_matches; // written by secondary thread, read by the matches() method QAtomicInt m_terminationRequested; // used as a bool }; /** * A simple thread that fetches a list of tilde-completions and returns this * to the caller via the completionThreadDone signal. */ class UserListThread : public CompletionThread { Q_OBJECT public: UserListThread(KUrlCompletionPrivate *receiver) : CompletionThread(receiver) { } protected: void run() override { #ifndef Q_OS_ANDROID const QChar tilde = QLatin1Char('~'); // we don't need to handle prepend here, right? ~user is always at pos 0 assert(m_prepend.isEmpty()); #ifndef Q_OS_WIN struct passwd *pw; ::setpwent(); while ((pw = ::getpwent()) && !terminationRequested()) { addMatch(tilde + QString::fromLocal8Bit(pw->pw_name)); } ::endpwent(); #else // TODO: add KUser::allUserNames() with a std::function shouldTerminate parameter // currently terminationRequested is ignored on Windows const QStringList allUsers = KUser::allUserNames(); for (const QString &s : allUsers) { addMatch(tilde + s); } #endif addMatch(QString(tilde)); #endif done(); } }; class DirectoryListThread : public CompletionThread { Q_OBJECT public: DirectoryListThread(KUrlCompletionPrivate *receiver, const QStringList &dirList, const QString &filter, const QStringList &mimeTypeFilters, bool onlyExe, bool onlyDir, bool noHidden, bool appendSlashToDir) : CompletionThread(receiver) , m_dirList(dirList) , m_filter(filter) , m_mimeTypeFilters(mimeTypeFilters) , m_onlyExe(onlyExe) , m_onlyDir(onlyDir) , m_noHidden(noHidden) , m_appendSlashToDir(appendSlashToDir) { } void run() override; private: QStringList m_dirList; QString m_filter; QStringList m_mimeTypeFilters; bool m_onlyExe; bool m_onlyDir; bool m_noHidden; bool m_appendSlashToDir; }; void DirectoryListThread::run() { // qDebug() << "Entered DirectoryListThread::run(), m_filter=" << m_filter << ", m_onlyExe=" << m_onlyExe << ", m_onlyDir=" << m_onlyDir << ", // m_appendSlashToDir=" << m_appendSlashToDir << ", m_dirList.size()=" << m_dirList.size(); QDir::Filters iterator_filter = (m_noHidden ? QDir::Filter(0) : QDir::Hidden) | QDir::Readable | QDir::NoDotAndDotDot; if (m_onlyExe) { iterator_filter |= (QDir::Dirs | QDir::Files | QDir::Executable); } else if (m_onlyDir) { iterator_filter |= QDir::Dirs; } else { iterator_filter |= (QDir::Dirs | QDir::Files); } QMimeDatabase mimeTypes; for (const QString &dir : std::as_const(m_dirList)) { if (terminationRequested()) { break; } // qDebug() << "Scanning directory" << dir; QDirIterator current_dir_iterator(dir, iterator_filter); while (current_dir_iterator.hasNext() && !terminationRequested()) { current_dir_iterator.next(); QFileInfo item_info = current_dir_iterator.fileInfo(); QString item_name = item_info.fileName(); // qDebug() << "Found" << file_name; if (!m_filter.isEmpty() && !item_name.startsWith(m_filter)) { continue; } if (!m_mimeTypeFilters.isEmpty() && !item_info.isDir()) { auto mimeType = mimeTypes.mimeTypeForFile(item_info); if (!m_mimeTypeFilters.contains(mimeType.name())) { continue; } } // Add '/' to directories if (m_appendSlashToDir && item_info.isDir()) { Utils::appendSlash(item_name); } if (m_complete_url) { QUrl url(m_prepend); addPathToUrl(url, item_name); addMatch(url.toDisplayString()); } else { item_name.prepend(m_prepend); addMatch(item_name); } } } done(); } KUrlCompletionPrivate::~KUrlCompletionPrivate() { } /////////////////////////////////////////////////////// /////////////////////////////////////////////////////// // MyURL - wrapper for QUrl with some different functionality // class KUrlCompletionPrivate::MyURL { public: MyURL(const QString &url, const QUrl &cwd); MyURL(const MyURL &url); ~MyURL(); QUrl kurl() const { return m_kurl; } bool isLocalFile() const { return m_kurl.isLocalFile(); } QString scheme() const { return m_kurl.scheme(); } // The directory with a trailing '/' QString dir() const { return m_kurl.adjusted(QUrl::RemoveFilename).path(); } QString file() const { return m_kurl.fileName(); } // The initial, unparsed, url, as a string. QString url() const { return m_url; } // Is the initial string a URL, or just a path (whether absolute or relative) bool isURL() const { return m_isURL; } void filter(bool replace_user_dir, bool replace_env); private: void init(const QString &url, const QUrl &cwd); QUrl m_kurl; QString m_url; bool m_isURL; }; KUrlCompletionPrivate::MyURL::MyURL(const QString &_url, const QUrl &cwd) { init(_url, cwd); } KUrlCompletionPrivate::MyURL::MyURL(const MyURL &_url) : m_kurl(_url.m_kurl) { m_url = _url.m_url; m_isURL = _url.m_isURL; } void KUrlCompletionPrivate::MyURL::init(const QString &_url, const QUrl &cwd) { // Save the original text m_url = _url; // Non-const copy QString url_copy = _url; // Special shortcuts for "man:" and "info:" if (url_copy.startsWith(QLatin1Char('#'))) { if (url_copy.length() > 1 && url_copy.at(1) == QLatin1Char('#')) { url_copy.replace(0, 2, QStringLiteral("info:")); } else { url_copy.replace(0, 1, QStringLiteral("man:")); } } // Look for a protocol in 'url' const QRegularExpression protocol_regex(QStringLiteral("^(?![A-Za-z]:)[^/\\s\\\\]*:")); // Assume "file:" or whatever is given by 'cwd' if there is // no protocol. (QUrl does this only for absolute paths) if (protocol_regex.match(url_copy).hasMatch()) { m_kurl = QUrl(url_copy); m_isURL = true; } else { // relative path or ~ or $something m_isURL = false; if (Utils::isAbsoluteLocalPath(url_copy) || url_copy.startsWith(QLatin1Char('~')) || url_copy.startsWith(QLatin1Char('$'))) { m_kurl = QUrl::fromLocalFile(url_copy); } else { // Relative path if (cwd.isEmpty()) { m_kurl = QUrl(url_copy); } else { m_kurl = cwd; m_kurl.setPath(Utils::concatPaths(m_kurl.path(), url_copy)); } } } } KUrlCompletionPrivate::MyURL::~MyURL() { } void KUrlCompletionPrivate::MyURL::filter(bool replace_user_dir, bool replace_env) { QString d = dir() + file(); if (replace_user_dir) { expandTilde(d); } if (replace_env) { expandEnv(d); } m_kurl.setPath(d); } /////////////////////////////////////////////////////// /////////////////////////////////////////////////////// // KUrlCompletion // KUrlCompletion::KUrlCompletion() : KUrlCompletion(FileCompletion) { } KUrlCompletion::KUrlCompletion(Mode _mode) : KCompletion() , d(new KUrlCompletionPrivate(this, _mode)) { } KUrlCompletion::~KUrlCompletion() { stop(); } void KUrlCompletion::setDir(const QUrl &dir) { d->cwd = dir; } QUrl KUrlCompletion::dir() const { return d->cwd; } KUrlCompletion::Mode KUrlCompletion::mode() const { return d->mode; } void KUrlCompletion::setMode(Mode _mode) { d->mode = _mode; } bool KUrlCompletion::replaceEnv() const { return d->replace_env; } void KUrlCompletion::setReplaceEnv(bool replace) { d->replace_env = replace; } bool KUrlCompletion::replaceHome() const { return d->replace_home; } void KUrlCompletion::setReplaceHome(bool replace) { d->replace_home = replace; } /* * makeCompletion() * * Entry point for file name completion */ QString KUrlCompletion::makeCompletion(const QString &text) { qCDebug(KIO_WIDGETS) << text << "d->cwd=" << d->cwd; KUrlCompletionPrivate::MyURL url(text, d->cwd); d->compl_text = text; // Set d->prepend to the original URL, with the filename [and ref/query] stripped. // This is what gets prepended to the directory-listing matches. if (url.isURL()) { QUrl directoryUrl(url.kurl()); directoryUrl.setQuery(QString()); directoryUrl.setFragment(QString()); directoryUrl.setPath(url.dir()); d->prepend = directoryUrl.toString(); } else { d->prepend = text.left(text.length() - url.file().length()); } d->complete_url = url.isURL(); // We use our custom sorter function if we are completing local paths setSorterFunction(!d->complete_url ? sortLocalPaths : nullptr); // If we typed an exact path to a directory, we block autosuggestion // or else it would autosuggest the first child dir. setShouldAutoSuggest(d->complete_url || (url.dir() != text)); QString aMatch; // Environment variables // if (d->replace_env && d->envCompletion(url, &aMatch)) { return aMatch; } // User directories // if (d->replace_home && d->userCompletion(url, &aMatch)) { return aMatch; } // Replace user directories and variables url.filter(d->replace_home, d->replace_env); // qDebug() << "Filtered: proto=" << url.scheme() // << ", dir=" << url.dir() // << ", file=" << url.file() // << ", kurl url=" << *url.kurl(); if (d->mode == ExeCompletion) { // Executables // if (d->exeCompletion(url, &aMatch)) { return aMatch; } // KRun can run "man:" and "info:" etc. so why not treat them // as executables... if (d->urlCompletion(url, &aMatch)) { return aMatch; } } else { // Local files, directories // if (d->fileCompletion(url, &aMatch)) { return aMatch; } // All other... // if (d->urlCompletion(url, &aMatch)) { return aMatch; } } d->setListedUrl(CTNone); stop(); return QString(); } /* * finished * * Go on and call KCompletion. * Called when all matches have been added */ QString KUrlCompletionPrivate::finished() { if (last_compl_type == CTInfo) { return q->KCompletion::makeCompletion(compl_text.toLower()); } else { return q->KCompletion::makeCompletion(compl_text); } } /* * isRunning * * Return true if either a KIO job or a thread is running */ bool KUrlCompletion::isRunning() const { return d->list_job || (d->dirListThread && !d->dirListThread->isFinished()) || (d->userListThread && !d->userListThread->isFinished()); } /* * stop * * Stop and delete a running KIO job or the DirLister */ void KUrlCompletion::stop() { if (d->list_job) { d->list_job->kill(); d->list_job = nullptr; } if (d->dirListThread) { d->dirListThread->requestTermination(); delete d->dirListThread; d->dirListThread = nullptr; } if (d->userListThread) { d->userListThread->requestTermination(); delete d->userListThread; d->userListThread = nullptr; } } /* * Keep track of the last listed directory */ void KUrlCompletionPrivate::setListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden) { last_compl_type = complType; last_path_listed = directory; last_file_listed = filter; last_no_hidden = no_hidden; last_prepend = prepend; } bool KUrlCompletionPrivate::isListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden) { /* clang-format off */ return last_compl_type == complType && (last_path_listed == directory || (directory.isEmpty() && last_path_listed.isEmpty())) && (filter.startsWith(last_file_listed) || (filter.isEmpty() && last_file_listed.isEmpty())) && last_no_hidden == no_hidden && last_prepend == prepend; // e.g. relative path vs absolute /* clang-format on */ } /* * isAutoCompletion * * Returns true if completion mode is Auto or Popup */ bool KUrlCompletionPrivate::isAutoCompletion() { /* clang-format off */ return q->completionMode() == KCompletion::CompletionAuto || q->completionMode() == KCompletion::CompletionPopup || q->completionMode() == KCompletion::CompletionMan || q->completionMode() == KCompletion::CompletionPopupAuto; /* clang-format on */ } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // User directories // bool KUrlCompletionPrivate::userCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (url.scheme() != QLatin1String("file") || !url.dir().isEmpty() || !url.file().startsWith(QLatin1Char('~')) || !prepend.isEmpty()) { return false; } if (!isListedUrl(CTUser)) { q->stop(); q->clear(); setListedUrl(CTUser); Q_ASSERT(!userListThread); // caller called stop() userListThread = new UserListThread(this); QObject::connect(userListThread, &CompletionThread::completionThreadDone, q, [this](QThread *thread, const QStringList &matches) { slotCompletionThreadDone(thread, matches); }); userListThread->start(); // If the thread finishes quickly make sure that the results // are added to the first matching case. userListThread->wait(initialWaitDuration()); const QStringList l = userListThread->matches(); addMatches(l); } *pMatch = finished(); return true; } ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// // Environment variables // bool KUrlCompletionPrivate::envCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (url.file().isEmpty() || url.file().at(0) != QLatin1Char('$')) { return false; } if (!isListedUrl(CTEnv)) { q->stop(); q->clear(); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); const QStringList keys = env.keys(); QStringList l; l.reserve(keys.size()); for (const QString &key : keys) { l.append(prepend + QLatin1Char('$') + key); } addMatches(l); } setListedUrl(CTEnv); *pMatch = finished(); return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // Executables // bool KUrlCompletionPrivate::exeCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (!url.isLocalFile()) { return false; } QString directory = unescape(url.dir()); // remove escapes // Find directories to search for completions, either // // 1. complete path given in url // 2. current directory (d->cwd) // 3. $PATH // 4. no directory at all QStringList dirList; if (!url.file().isEmpty()) { // $PATH dirList = QString::fromLocal8Bit(qgetenv("PATH")).split(QDir::listSeparator(), Qt::SkipEmptyParts); QStringList::Iterator it = dirList.begin(); for (; it != dirList.end(); ++it) { it->append(QLatin1Char('/')); } } else if (Utils::isAbsoluteLocalPath(directory)) { // complete path in url dirList.append(directory); } else if (!directory.isEmpty() && !cwd.isEmpty()) { // current directory dirList.append(cwd.toLocalFile() + QLatin1Char('/') + directory); } // No hidden files unless the user types "." bool no_hidden_files = url.file().isEmpty() || url.file().at(0) != QLatin1Char('.'); // List files if needed // if (!isListedUrl(CTExe, directory, url.file(), no_hidden_files)) { q->stop(); q->clear(); setListedUrl(CTExe, directory, url.file(), no_hidden_files); *pMatch = listDirectories(dirList, url.file(), true, false, no_hidden_files); } else { *pMatch = finished(); } return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // Local files // bool KUrlCompletionPrivate::fileCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { if (!url.isLocalFile()) { return false; } QString directory = unescape(url.dir()); if (url.url() == QLatin1String("..")) { *pMatch = QStringLiteral(".."); return true; } // qDebug() << "fileCompletion" << url << "dir=" << dir; // Find directories to search for completions, either // // 1. complete path given in url // 2. current directory (d->cwd) // 3. no directory at all QStringList dirList; if (Utils::isAbsoluteLocalPath(directory)) { // complete path in url dirList.append(directory); } else if (!cwd.isEmpty()) { // current directory QString dirToAdd = cwd.toLocalFile(); if (!directory.isEmpty()) { Utils::appendSlash(dirToAdd); dirToAdd += directory; } dirList.append(dirToAdd); } // No hidden files unless the user types "." bool no_hidden_files = !url.file().startsWith(QLatin1Char('.')); // List files if needed // if (!isListedUrl(CTFile, directory, QString(), no_hidden_files)) { q->stop(); q->clear(); setListedUrl(CTFile, directory, QString(), no_hidden_files); // Append '/' to directories in Popup mode? bool append_slash = (popup_append_slash && (q->completionMode() == KCompletion::CompletionPopup || q->completionMode() == KCompletion::CompletionPopupAuto)); bool only_dir = (mode == KUrlCompletion::DirCompletion); *pMatch = listDirectories(dirList, QString(), false, only_dir, no_hidden_files, append_slash); } else { *pMatch = finished(); } return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // URLs not handled elsewhere... // static bool isLocalProtocol(const QString &protocol) { return (KProtocolInfo::protocolClass(protocol) == QLatin1String(":local")); } bool KUrlCompletionPrivate::urlCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) { // qDebug() << *url.kurl(); if (onlyLocalProto && isLocalProtocol(url.scheme())) { return false; } // Use d->cwd as base url in case url is not absolute QUrl url_dir = url.kurl(); if (url_dir.isRelative() && !cwd.isEmpty()) { // Create an URL with the directory to be listed url_dir = cwd.resolved(url_dir); } // url is malformed if (!url_dir.isValid() || url.scheme().isEmpty()) { return false; } // non local urls if (!isLocalProtocol(url.scheme())) { // url does not specify host if (url_dir.host().isEmpty()) { return false; } // url does not specify a valid directory if (url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty()) { return false; } // automatic completion is disabled if (isAutoCompletion() && !url_auto_completion) { return false; } } // url handler doesn't support listing if (!KProtocolManager::supportsListing(url_dir)) { return false; } // Remove escapes const QString directory = unescape(url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); url_dir.setPath(directory); // List files if needed // if (!isListedUrl(CTUrl, directory, url.file())) { q->stop(); q->clear(); setListedUrl(CTUrl, directory, QString()); QList url_list; url_list.append(url_dir); listUrls(url_list, QString(), false); pMatch->clear(); } else if (!q->isRunning()) { *pMatch = finished(); } else { pMatch->clear(); } return true; } ////////////////////////////////////////////////// ////////////////////////////////////////////////// // Directory and URL listing // /* * addMatches * * Called to add matches to KCompletion */ void KUrlCompletionPrivate::addMatches(const QStringList &matchList) { q->insertItems(matchList); } /* * listDirectories * * List files starting with 'filter' in the given directories, * either using DirLister or listURLs() * * In either case, addMatches() is called with the listed * files, and eventually finished() when the listing is done * * Returns the match if available, or QString() if * DirLister timed out or using kio */ QString KUrlCompletionPrivate::listDirectories(const QStringList &dirList, const QString &filter, bool only_exe, bool only_dir, bool no_hidden, bool append_slash_to_dir) { assert(!q->isRunning()); if (qEnvironmentVariableIsEmpty("KURLCOMPLETION_LOCAL_KIO")) { qCDebug(KIO_WIDGETS) << "Listing directories:" << dirList << "with filter=" << filter << "using thread"; // Don't use KIO QStringList dirs; QStringList::ConstIterator end = dirList.constEnd(); for (QStringList::ConstIterator it = dirList.constBegin(); it != end; ++it) { QUrl url = QUrl::fromLocalFile(*it); if (KUrlAuthorized::authorizeUrlAction(QStringLiteral("list"), QUrl(), url)) { dirs.append(*it); } } Q_ASSERT(!dirListThread); // caller called stop() dirListThread = new DirectoryListThread(this, dirs, filter, mimeTypeFilters, only_exe, only_dir, no_hidden, append_slash_to_dir); QObject::connect(dirListThread, &CompletionThread::completionThreadDone, q, [this](QThread *thread, const QStringList &matches) { slotCompletionThreadDone(thread, matches); }); dirListThread->start(); dirListThread->wait(initialWaitDuration()); qCDebug(KIO_WIDGETS) << "Adding initial matches:" << dirListThread->matches(); addMatches(dirListThread->matches()); return finished(); } // Use KIO // qDebug() << "Listing (listDirectories):" << dirList << "with KIO"; QList url_list; QStringList::ConstIterator it = dirList.constBegin(); QStringList::ConstIterator end = dirList.constEnd(); url_list.reserve(dirList.size()); for (; it != end; ++it) { url_list.append(QUrl(*it)); } listUrls(url_list, filter, only_exe, no_hidden); // Will call addMatches() and finished() return QString(); } /* * listURLs * * Use KIO to list the given urls * * addMatches() is called with the listed files * finished() is called when the listing is done */ void KUrlCompletionPrivate::listUrls(const QList &urls, const QString &filter, bool only_exe, bool no_hidden) { assert(list_urls.isEmpty()); assert(list_job == nullptr); list_urls = urls; list_urls_filter = filter; list_urls_only_exe = only_exe; list_urls_no_hidden = no_hidden; // qDebug() << "Listing URLs:" << *urls[0] << ",..."; // Start it off by calling slotIOFinished // // This will start a new list job as long as there // are urls in d->list_urls // slotIOFinished(nullptr); } /* * slotEntries * * Receive files listed by KIO and call addMatches() */ void KUrlCompletionPrivate::slotEntries(KIO::Job *, const KIO::UDSEntryList &entries) { QStringList matchList; const QString filter = list_urls_filter; const int filter_len = filter.length(); // Iterate over all files for (const auto &entry : entries) { const QString udsUrl = entry.stringValue(KIO::UDSEntry::UDS_URL); QString entry_name; if (!udsUrl.isEmpty()) { // qDebug() << "url:" << url; entry_name = QUrl(udsUrl).fileName(); } else { entry_name = entry.stringValue(KIO::UDSEntry::UDS_NAME); } // This can happen with kdeconnect://deviceId as a completion for kdeconnect:/, // there's no fileName [and the UDS_NAME is unrelated, can't use that]. // This code doesn't support completing hostnames anyway (see addPathToUrl below). if (entry_name.isEmpty()) { continue; } if (entry_name.at(0) == QLatin1Char('.') && (list_urls_no_hidden || entry_name.length() == 1 || (entry_name.length() == 2 && entry_name.at(1) == QLatin1Char('.')))) { continue; } const bool isDir = entry.isDir(); if (mode == KUrlCompletion::DirCompletion && !isDir) { continue; } if (filter_len != 0 && QStringView(entry_name).left(filter_len) != filter) { continue; } if (!mimeTypeFilters.isEmpty() && !isDir && !mimeTypeFilters.contains(entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE))) { continue; } QString toAppend = entry_name; if (isDir) { toAppend.append(QLatin1Char('/')); } if (!list_urls_only_exe || (entry.numberValue(KIO::UDSEntry::UDS_ACCESS) & s_modeExe) // true if executable ) { if (complete_url) { QUrl url(prepend); addPathToUrl(url, toAppend); matchList.append(url.toDisplayString()); } else { matchList.append(prepend + toAppend); } } } addMatches(matchList); } /* * slotIOFinished * * Called when a KIO job is finished. * * Start a new list job if there are still urls in * list_urls, otherwise call finished() */ void KUrlCompletionPrivate::slotIOFinished(KJob *job) { assert(job == list_job); Q_UNUSED(job) if (list_urls.isEmpty()) { list_job = nullptr; finished(); // will call KCompletion::makeCompletion() } else { QUrl kurl(list_urls.takeFirst()); // list_urls.removeAll( kurl ); // qDebug() << "Start KIO::listDir" << kurl; list_job = KIO::listDir(kurl, KIO::HideProgressInfo); list_job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); assert(list_job); q->connect(list_job, &KJob::result, q, [this](KJob *job) { slotIOFinished(job); }); q->connect(list_job, &KIO::ListJob::entries, q, [this](KIO::Job *job, const KIO::UDSEntryList &list) { slotEntries(job, list); }); } } /////////////////////////////////////////////////// /////////////////////////////////////////////////// /* * postProcessMatch, postProcessMatches * * Called by KCompletion before emitting match() and matches() * * Append '/' to directories for file completion. This is * done here to avoid stat()'ing a lot of files */ void KUrlCompletion::postProcessMatch(QString *pMatch) const { // qDebug() << *pMatch; if (!pMatch->isEmpty() && pMatch->startsWith(QLatin1String("file:"))) { // Add '/' to directories in file completion mode // unless it has already been done if (d->last_compl_type == CTFile && pMatch->at(pMatch->length() - 1) != QLatin1Char('/')) { QString copy = QUrl(*pMatch).toLocalFile(); expandTilde(copy); expandEnv(copy); if (!Utils::isAbsoluteLocalPath(copy)) { copy.prepend(d->cwd.toLocalFile() + QLatin1Char('/')); } // qDebug() << "stat'ing" << copy; QByteArray file = QFile::encodeName(copy); QT_STATBUF sbuff; if (QT_STAT(file.constData(), &sbuff) == 0) { if (Utils::isDirMask(sbuff.st_mode)) { pMatch->append(QLatin1Char('/')); } } else { // qDebug() << "Could not stat file" << copy; } } } } void KUrlCompletion::postProcessMatches(QStringList * /*matches*/) const { // Maybe '/' should be added to directories here as in // postProcessMatch() but it would slow things down // when there are a lot of matches... } void KUrlCompletion::postProcessMatches(KCompletionMatches * /*matches*/) const { // Maybe '/' should be added to directories here as in // postProcessMatch() but it would slow things down // when there are a lot of matches... } void KUrlCompletionPrivate::slotCompletionThreadDone(QThread *thread, const QStringList &matches) { if (thread != userListThread && thread != dirListThread) { qCDebug(KIO_WIDGETS) << "got" << matches.count() << "outdated matches"; return; } qCDebug(KIO_WIDGETS) << "got" << matches.count() << "matches at end of thread"; q->setItems(matches); if (userListThread == thread) { thread->wait(); delete thread; userListThread = nullptr; } else if (dirListThread == thread) { thread->wait(); delete thread; dirListThread = nullptr; } finished(); // will call KCompletion::makeCompletion() } // static QString KUrlCompletion::replacedPath(const QString &text, bool replaceHome, bool replaceEnv) { if (text.isEmpty()) { return text; } KUrlCompletionPrivate::MyURL url(text, QUrl()); // no need to replace something of our current cwd if (!url.kurl().isLocalFile()) { return text; } url.filter(replaceHome, replaceEnv); return url.dir() + url.file(); } QString KUrlCompletion::replacedPath(const QString &text) const { return replacedPath(text, d->replace_home, d->replace_env); } void KUrlCompletion::setMimeTypeFilters(const QStringList &mimeTypeFilters) { d->mimeTypeFilters = mimeTypeFilters; } QStringList KUrlCompletion::mimeTypeFilters() const { return d->mimeTypeFilters; } ///////////////////////////////////////////////////////// ///////////////////////////////////////////////////////// // Static functions /* * expandEnv * * Expand environment variables in text. Escaped '$' are ignored. * Return true if expansion was made. */ static bool expandEnv(QString &text) { // Find all environment variables beginning with '$' // int pos = 0; bool expanded = false; while ((pos = text.indexOf(QLatin1Char('$'), pos)) != -1) { // Skip escaped '$' // if (pos > 0 && text.at(pos - 1) == QLatin1Char('\\')) { pos++; } // Variable found => expand // else { // Find the end of the variable = next '/' or ' ' // int pos2 = text.indexOf(QLatin1Char(' '), pos + 1); int pos_tmp = text.indexOf(QLatin1Char('/'), pos + 1); if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) { pos2 = pos_tmp; } if (pos2 == -1) { pos2 = text.length(); } // Replace if the variable is terminated by '/' or ' ' // and defined // if (pos2 >= 0) { const int len = pos2 - pos; const QStringView key = QStringView(text).mid(pos + 1, len - 1); const QString value = QString::fromLocal8Bit(qgetenv(key.toLocal8Bit().constData())); if (!value.isEmpty()) { expanded = true; text.replace(pos, len, value); pos = pos + value.length(); } else { pos = pos2; } } } } return expanded; } /* * expandTilde * * Replace "~user" with the users home directory * Return true if expansion was made. */ static bool expandTilde(QString &text) { if (text.isEmpty() || (text.at(0) != QLatin1Char('~'))) { return false; } bool expanded = false; // Find the end of the user name = next '/' or ' ' // int pos2 = text.indexOf(QLatin1Char(' '), 1); int pos_tmp = text.indexOf(QLatin1Char('/'), 1); if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) { pos2 = pos_tmp; } if (pos2 == -1) { pos2 = text.length(); } // Replace ~user if the user name is terminated by '/' or ' ' // if (pos2 >= 0) { QString userName = text.mid(1, pos2 - 1); QString dir; // A single ~ is replaced with $HOME // if (userName.isEmpty()) { dir = QDir::homePath(); } // ~user is replaced with the dir from passwd // else { KUser user(userName); dir = user.homeDir(); } if (!dir.isEmpty()) { expanded = true; text.replace(0, pos2, dir); } } return expanded; } /* * unescape * * Remove escapes and return the result in a new string * */ static QString unescape(const QString &text) { QString result; result.reserve(text.size()); for (const QChar ch : text) { if (ch != QLatin1Char('\\')) { result.append(ch); } } return result; } #include "kurlcompletion.moc" #include "moc_kurlcompletion.cpp"