1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 David Smith <dsmith@algonet.se>
4 SPDX-FileCopyrightText: 2004 Scott Wheeler <wheeler@kde.org>
5
6 This class was inspired by a previous KUrlCompletion by
7 SPDX-FileContributor: Henner Zeller <zeller@think.de>
8
9 SPDX-License-Identifier: LGPL-2.0-or-later
10*/
11
12#include "kurlcompletion.h"
13#include "../utils_p.h"
14#include <assert.h>
15#include <limits.h>
16#include <stdlib.h>
17
18#include <QCollator>
19#include <QDebug>
20#include <QDir>
21#include <QDirIterator>
22#include <QFile>
23#include <QMimeDatabase>
24#include <QMutex>
25#include <QProcessEnvironment>
26#include <QRegularExpression>
27#include <QThread>
28#include <QUrl>
29#include <qplatformdefs.h> // QT_LSTAT, QT_STAT, QT_STATBUF
30
31#include <KConfig>
32#include <KConfigGroup>
33#include <KSharedConfig>
34#include <KUser>
35
36#include <kio/listjob.h>
37#include <kio_widgets_debug.h>
38#include <kioglobal_p.h>
39#include <kprotocolmanager.h>
40#include <kurlauthorized.h>
41
42#include <time.h>
43
44#ifdef Q_OS_WIN
45#include <qt_windows.h>
46#else
47#include <pwd.h>
48#include <sys/param.h>
49#endif
50
51static bool expandTilde(QString &);
52static bool expandEnv(QString &);
53
54static QString unescape(const QString &text);
55
56// Permission mask for files that are executable by
57// user, group or other
58static constexpr mode_t s_modeExe = S_IXUSR | S_IXGRP | S_IXOTH;
59
60// Constants for types of completion
61enum ComplType { CTNone = 0, CTEnv, CTUser, CTMan, CTExe, CTFile, CTUrl, CTInfo };
62
63class CompletionThread;
64
65// Ensure that we don't end up with "//".
66static void addPathToUrl(QUrl &url, const QString &relPath)
67{
68 url.setPath(path: Utils::concatPaths(path1: url.path(), path2: relPath));
69}
70
71static QBasicAtomicInt s_waitDuration = Q_BASIC_ATOMIC_INITIALIZER(-1);
72
73static int initialWaitDuration()
74{
75 if (s_waitDuration.loadRelaxed() == -1) {
76 const QByteArray envVar = qgetenv(varName: "KURLCOMPLETION_WAIT");
77 if (envVar.isEmpty()) {
78 s_waitDuration = 200; // default: 200 ms
79 } else {
80 s_waitDuration = envVar.toInt();
81 }
82 }
83 return s_waitDuration;
84}
85
86// For local paths we use our custom comparer function that ignores the trailing slash character
87static void sortLocalPaths(QStringList &list)
88{
89 QCollator c;
90 c.setCaseSensitivity(Qt::CaseSensitive);
91 std::sort(first: list.begin(), last: list.end(), comp: [c](const QString &a, const QString &b) {
92 return c.compare(s1: a.endsWith(QStringLiteral("/")) ? a.chopped(n: 1) : a, s2: b.endsWith(QStringLiteral("/")) ? b.chopped(n: 1) : b) < 0;
93 });
94}
95
96///////////////////////////////////////////////////////
97///////////////////////////////////////////////////////
98// KUrlCompletionPrivate
99//
100class KUrlCompletionPrivate
101{
102public:
103 explicit KUrlCompletionPrivate(KUrlCompletion *qq, KUrlCompletion::Mode m)
104 : q(qq)
105 , cwd(QUrl::fromLocalFile(localfile: QDir::homePath()))
106 , mode(m)
107 {
108 // Read settings
109 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("URLCompletion"));
110 url_auto_completion = cg.readEntry(key: "alwaysAutoComplete", defaultValue: true);
111 popup_append_slash = cg.readEntry(key: "popupAppendSlash", defaultValue: true);
112 onlyLocalProto = cg.readEntry(key: "LocalProtocolsOnly", defaultValue: false);
113
114 q->setIgnoreCase(true);
115 }
116
117 ~KUrlCompletionPrivate();
118
119 void slotEntries(KIO::Job *, const KIO::UDSEntryList &);
120 void slotIOFinished(KJob *);
121 void slotCompletionThreadDone(QThread *thread, const QStringList &matches);
122
123 class MyURL;
124 bool userCompletion(const MyURL &url, QString *match);
125 bool envCompletion(const MyURL &url, QString *match);
126 bool exeCompletion(const MyURL &url, QString *match);
127 bool fileCompletion(const MyURL &url, QString *match);
128 bool urlCompletion(const MyURL &url, QString *match);
129
130 bool isAutoCompletion();
131
132 // List the next dir in m_dirs
133 QString listDirectories(const QStringList &, const QString &, bool only_exe = false, bool only_dir = false, bool no_hidden = false, bool stat_files = true);
134
135 void listUrls(const QList<QUrl> &urls, const QString &filter = QString(), bool only_exe = false, bool no_hidden = false);
136
137 void addMatches(const QStringList &);
138 QString finished();
139
140 void init();
141
142 void setListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false);
143
144 bool isListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false);
145
146 KUrlCompletion *const q;
147 QList<QUrl> list_urls;
148
149 bool onlyLocalProto = false;
150
151 // urlCompletion() in Auto/Popup mode?
152 bool url_auto_completion = true;
153
154 // Append '/' to directories in Popup mode?
155 // Doing that stat's all files and is slower
156 bool popup_append_slash = true;
157
158 // Keep track of currently listed files to avoid reading them again
159 bool last_no_hidden = false;
160 QString last_path_listed;
161 QString last_file_listed;
162 QString last_prepend;
163 ComplType last_compl_type = CTNone;
164
165 QUrl cwd; // "current directory" = base dir for completion
166
167 KUrlCompletion::Mode mode = KUrlCompletion::FileCompletion;
168 bool replace_env = true;
169 bool replace_home = true;
170 bool complete_url; // if true completing a URL (i.e. 'prepend' is a URL), otherwise a path
171
172 KIO::ListJob *list_job = nullptr; // kio job to list directories
173
174 QString prepend; // text to prepend to listed items
175 QString compl_text; // text to pass on to KCompletion
176
177 // Filters for files read with kio
178 bool list_urls_only_exe; // true = only list executables
179 bool list_urls_no_hidden;
180 QString list_urls_filter; // filter for listed files
181
182 CompletionThread *userListThread = nullptr;
183 CompletionThread *dirListThread = nullptr;
184
185 QStringList mimeTypeFilters;
186};
187
188class CompletionThread : public QThread
189{
190 Q_OBJECT
191protected:
192 CompletionThread(KUrlCompletionPrivate *receiver)
193 : QThread()
194 , m_prepend(receiver->prepend)
195 , m_complete_url(receiver->complete_url)
196 , m_terminationRequested(false)
197 {
198 }
199
200public:
201 void requestTermination()
202 {
203 if (!isFinished()) {
204 qCDebug(KIO_WIDGETS) << "stopping thread" << this;
205 }
206 m_terminationRequested.storeRelaxed(newValue: true);
207 wait();
208 }
209
210 QStringList matches() const
211 {
212 QMutexLocker locker(&m_mutex);
213 return m_matches;
214 }
215
216Q_SIGNALS:
217 void completionThreadDone(QThread *thread, const QStringList &matches);
218
219protected:
220 void addMatch(const QString &match)
221 {
222 QMutexLocker locker(&m_mutex);
223 m_matches.append(t: match);
224 }
225 bool terminationRequested() const
226 {
227 return m_terminationRequested.loadRelaxed();
228 }
229 void done()
230 {
231 if (!terminationRequested()) {
232 qCDebug(KIO_WIDGETS) << "done, emitting signal with" << m_matches.count() << "matches";
233 Q_EMIT completionThreadDone(thread: this, matches: m_matches);
234 }
235 }
236
237 const QString m_prepend;
238 const bool m_complete_url; // if true completing a URL (i.e. 'm_prepend' is a URL), otherwise a path
239
240private:
241 mutable QMutex m_mutex; // protects m_matches
242 QStringList m_matches; // written by secondary thread, read by the matches() method
243 QAtomicInt m_terminationRequested; // used as a bool
244};
245
246/**
247 * A simple thread that fetches a list of tilde-completions and returns this
248 * to the caller via the completionThreadDone signal.
249 */
250
251class UserListThread : public CompletionThread
252{
253 Q_OBJECT
254public:
255 UserListThread(KUrlCompletionPrivate *receiver)
256 : CompletionThread(receiver)
257 {
258 }
259
260protected:
261 void run() override
262 {
263#ifndef Q_OS_ANDROID
264 const QChar tilde = QLatin1Char('~');
265
266 // we don't need to handle prepend here, right? ~user is always at pos 0
267 assert(m_prepend.isEmpty());
268#ifndef Q_OS_WIN
269 struct passwd *pw;
270 ::setpwent();
271 while ((pw = ::getpwent()) && !terminationRequested()) {
272 addMatch(match: tilde + QString::fromLocal8Bit(ba: pw->pw_name));
273 }
274 ::endpwent();
275#else
276 // TODO: add KUser::allUserNames() with a std::function<bool()> shouldTerminate parameter
277 // currently terminationRequested is ignored on Windows
278 const QStringList allUsers = KUser::allUserNames();
279 for (const QString &s : allUsers) {
280 addMatch(tilde + s);
281 }
282#endif
283 addMatch(match: QString(tilde));
284#endif
285 done();
286 }
287};
288
289class DirectoryListThread : public CompletionThread
290{
291 Q_OBJECT
292public:
293 DirectoryListThread(KUrlCompletionPrivate *receiver,
294 const QStringList &dirList,
295 const QString &filter,
296 const QStringList &mimeTypeFilters,
297 bool onlyExe,
298 bool onlyDir,
299 bool noHidden,
300 bool appendSlashToDir)
301 : CompletionThread(receiver)
302 , m_dirList(dirList)
303 , m_filter(filter)
304 , m_mimeTypeFilters(mimeTypeFilters)
305 , m_onlyExe(onlyExe)
306 , m_onlyDir(onlyDir)
307 , m_noHidden(noHidden)
308 , m_appendSlashToDir(appendSlashToDir)
309 {
310 }
311
312 void run() override;
313
314private:
315 QStringList m_dirList;
316 QString m_filter;
317 QStringList m_mimeTypeFilters;
318 bool m_onlyExe;
319 bool m_onlyDir;
320 bool m_noHidden;
321 bool m_appendSlashToDir;
322};
323
324void DirectoryListThread::run()
325{
326 // qDebug() << "Entered DirectoryListThread::run(), m_filter=" << m_filter << ", m_onlyExe=" << m_onlyExe << ", m_onlyDir=" << m_onlyDir << ",
327 // m_appendSlashToDir=" << m_appendSlashToDir << ", m_dirList.size()=" << m_dirList.size();
328
329 QDir::Filters iterator_filter = (m_noHidden ? QDir::Filter(0) : QDir::Hidden) | QDir::Readable | QDir::NoDotAndDotDot;
330 if (m_onlyExe) {
331 iterator_filter |= (QDir::Dirs | QDir::Files | QDir::Executable);
332 } else if (m_onlyDir) {
333 iterator_filter |= QDir::Dirs;
334 } else {
335 iterator_filter |= (QDir::Dirs | QDir::Files);
336 }
337
338 QMimeDatabase mimeTypes;
339
340 for (const QString &dir : std::as_const(t&: m_dirList)) {
341 if (terminationRequested()) {
342 break;
343 }
344
345 // qDebug() << "Scanning directory" << dir;
346
347 QDirIterator current_dir_iterator(dir, iterator_filter);
348
349 while (current_dir_iterator.hasNext() && !terminationRequested()) {
350 current_dir_iterator.next();
351
352 QFileInfo item_info = current_dir_iterator.fileInfo();
353 QString item_name = item_info.fileName();
354
355 // qDebug() << "Found" << file_name;
356
357 if (!m_filter.isEmpty() && !item_name.startsWith(s: m_filter)) {
358 continue;
359 }
360
361 if (!m_mimeTypeFilters.isEmpty() && !item_info.isDir()) {
362 auto mimeType = mimeTypes.mimeTypeForFile(fileInfo: item_info);
363 if (!m_mimeTypeFilters.contains(str: mimeType.name())) {
364 continue;
365 }
366 }
367
368 // Add '/' to directories
369 if (m_appendSlashToDir && item_info.isDir()) {
370 Utils::appendSlash(path&: item_name);
371 }
372
373 if (m_complete_url) {
374 QUrl url(m_prepend);
375 addPathToUrl(url, relPath: item_name);
376 addMatch(match: url.toDisplayString());
377 } else {
378 item_name.prepend(s: m_prepend);
379 addMatch(match: item_name);
380 }
381 }
382 }
383
384 done();
385}
386
387KUrlCompletionPrivate::~KUrlCompletionPrivate()
388{
389}
390
391///////////////////////////////////////////////////////
392///////////////////////////////////////////////////////
393// MyURL - wrapper for QUrl with some different functionality
394//
395
396class KUrlCompletionPrivate::MyURL
397{
398public:
399 MyURL(const QString &url, const QUrl &cwd);
400 MyURL(const MyURL &url);
401 ~MyURL();
402
403 QUrl kurl() const
404 {
405 return m_kurl;
406 }
407
408 bool isLocalFile() const
409 {
410 return m_kurl.isLocalFile();
411 }
412 QString scheme() const
413 {
414 return m_kurl.scheme();
415 }
416 // The directory with a trailing '/'
417 QString dir() const
418 {
419 return m_kurl.adjusted(options: QUrl::RemoveFilename).path();
420 }
421 QString file() const
422 {
423 return m_kurl.fileName();
424 }
425
426 // The initial, unparsed, url, as a string.
427 QString url() const
428 {
429 return m_url;
430 }
431
432 // Is the initial string a URL, or just a path (whether absolute or relative)
433 bool isURL() const
434 {
435 return m_isURL;
436 }
437
438 void filter(bool replace_user_dir, bool replace_env);
439
440private:
441 void init(const QString &url, const QUrl &cwd);
442
443 QUrl m_kurl;
444 QString m_url;
445 bool m_isURL;
446};
447
448KUrlCompletionPrivate::MyURL::MyURL(const QString &_url, const QUrl &cwd)
449{
450 init(url: _url, cwd);
451}
452
453KUrlCompletionPrivate::MyURL::MyURL(const MyURL &_url)
454 : m_kurl(_url.m_kurl)
455{
456 m_url = _url.m_url;
457 m_isURL = _url.m_isURL;
458}
459
460void KUrlCompletionPrivate::MyURL::init(const QString &_url, const QUrl &cwd)
461{
462 // Save the original text
463 m_url = _url;
464
465 // Non-const copy
466 QString url_copy = _url;
467
468 // Special shortcuts for "man:" and "info:"
469 if (url_copy.startsWith(c: QLatin1Char('#'))) {
470 if (url_copy.length() > 1 && url_copy.at(i: 1) == QLatin1Char('#')) {
471 url_copy.replace(i: 0, len: 2, QStringLiteral("info:"));
472 } else {
473 url_copy.replace(i: 0, len: 1, QStringLiteral("man:"));
474 }
475 }
476
477 // Look for a protocol in 'url'
478 const QRegularExpression protocol_regex(QStringLiteral("^(?![A-Za-z]:)[^/\\s\\\\]*:"));
479
480 // Assume "file:" or whatever is given by 'cwd' if there is
481 // no protocol. (QUrl does this only for absolute paths)
482 if (protocol_regex.match(subject: url_copy).hasMatch()) {
483 m_kurl = QUrl(url_copy);
484 m_isURL = true;
485 } else { // relative path or ~ or $something
486 m_isURL = false;
487 if (Utils::isAbsoluteLocalPath(path: url_copy) || url_copy.startsWith(c: QLatin1Char('~')) || url_copy.startsWith(c: QLatin1Char('$'))) {
488 m_kurl = QUrl::fromLocalFile(localfile: url_copy);
489 } else {
490 // Relative path
491 if (cwd.isEmpty()) {
492 m_kurl = QUrl(url_copy);
493 } else {
494 m_kurl = cwd;
495 m_kurl.setPath(path: Utils::concatPaths(path1: m_kurl.path(), path2: url_copy));
496 }
497 }
498 }
499}
500
501KUrlCompletionPrivate::MyURL::~MyURL()
502{
503}
504
505void KUrlCompletionPrivate::MyURL::filter(bool replace_user_dir, bool replace_env)
506{
507 QString d = dir() + file();
508 if (replace_user_dir) {
509 expandTilde(d);
510 }
511 if (replace_env) {
512 expandEnv(d);
513 }
514 m_kurl.setPath(path: d);
515}
516
517///////////////////////////////////////////////////////
518///////////////////////////////////////////////////////
519// KUrlCompletion
520//
521
522KUrlCompletion::KUrlCompletion()
523 : KUrlCompletion(FileCompletion)
524{
525}
526
527KUrlCompletion::KUrlCompletion(Mode _mode)
528 : KCompletion()
529 , d(new KUrlCompletionPrivate(this, _mode))
530{
531}
532
533KUrlCompletion::~KUrlCompletion()
534{
535 stop();
536}
537
538void KUrlCompletion::setDir(const QUrl &dir)
539{
540 d->cwd = dir;
541}
542
543QUrl KUrlCompletion::dir() const
544{
545 return d->cwd;
546}
547
548KUrlCompletion::Mode KUrlCompletion::mode() const
549{
550 return d->mode;
551}
552
553void KUrlCompletion::setMode(Mode _mode)
554{
555 d->mode = _mode;
556}
557
558bool KUrlCompletion::replaceEnv() const
559{
560 return d->replace_env;
561}
562
563void KUrlCompletion::setReplaceEnv(bool replace)
564{
565 d->replace_env = replace;
566}
567
568bool KUrlCompletion::replaceHome() const
569{
570 return d->replace_home;
571}
572
573void KUrlCompletion::setReplaceHome(bool replace)
574{
575 d->replace_home = replace;
576}
577
578/*
579 * makeCompletion()
580 *
581 * Entry point for file name completion
582 */
583QString KUrlCompletion::makeCompletion(const QString &text)
584{
585 qCDebug(KIO_WIDGETS) << text << "d->cwd=" << d->cwd;
586
587 KUrlCompletionPrivate::MyURL url(text, d->cwd);
588
589 d->compl_text = text;
590
591 // Set d->prepend to the original URL, with the filename [and ref/query] stripped.
592 // This is what gets prepended to the directory-listing matches.
593 if (url.isURL()) {
594 QUrl directoryUrl(url.kurl());
595 directoryUrl.setQuery(query: QString());
596 directoryUrl.setFragment(fragment: QString());
597 directoryUrl.setPath(path: url.dir());
598 d->prepend = directoryUrl.toString();
599 } else {
600 d->prepend = text.left(n: text.length() - url.file().length());
601 }
602
603 d->complete_url = url.isURL();
604
605 // We use our custom sorter function if we are completing local paths
606 setSorterFunction(!d->complete_url ? sortLocalPaths : nullptr);
607
608 // If we typed an exact path to a directory, we block autosuggestion
609 // or else it would autosuggest the first child dir.
610 setShouldAutoSuggest(d->complete_url || (url.dir() != text));
611
612 QString aMatch;
613
614 // Environment variables
615 //
616 if (d->replace_env && d->envCompletion(url, match: &aMatch)) {
617 return aMatch;
618 }
619
620 // User directories
621 //
622 if (d->replace_home && d->userCompletion(url, match: &aMatch)) {
623 return aMatch;
624 }
625
626 // Replace user directories and variables
627 url.filter(replace_user_dir: d->replace_home, replace_env: d->replace_env);
628
629 // qDebug() << "Filtered: proto=" << url.scheme()
630 // << ", dir=" << url.dir()
631 // << ", file=" << url.file()
632 // << ", kurl url=" << *url.kurl();
633
634 if (d->mode == ExeCompletion) {
635 // Executables
636 //
637 if (d->exeCompletion(url, match: &aMatch)) {
638 return aMatch;
639 }
640
641 // KRun can run "man:" and "info:" etc. so why not treat them
642 // as executables...
643
644 if (d->urlCompletion(url, match: &aMatch)) {
645 return aMatch;
646 }
647 } else {
648 // Local files, directories
649 //
650 if (d->fileCompletion(url, match: &aMatch)) {
651 return aMatch;
652 }
653
654 // All other...
655 //
656 if (d->urlCompletion(url, match: &aMatch)) {
657 return aMatch;
658 }
659 }
660
661 d->setListedUrl(compl_type: CTNone);
662 stop();
663
664 return QString();
665}
666
667/*
668 * finished
669 *
670 * Go on and call KCompletion.
671 * Called when all matches have been added
672 */
673QString KUrlCompletionPrivate::finished()
674{
675 if (last_compl_type == CTInfo) {
676 return q->KCompletion::makeCompletion(string: compl_text.toLower());
677 } else {
678 return q->KCompletion::makeCompletion(string: compl_text);
679 }
680}
681
682/*
683 * isRunning
684 *
685 * Return true if either a KIO job or a thread is running
686 */
687bool KUrlCompletion::isRunning() const
688{
689 return d->list_job || (d->dirListThread && !d->dirListThread->isFinished()) || (d->userListThread && !d->userListThread->isFinished());
690}
691
692/*
693 * stop
694 *
695 * Stop and delete a running KIO job or the DirLister
696 */
697void KUrlCompletion::stop()
698{
699 if (d->list_job) {
700 d->list_job->kill();
701 d->list_job = nullptr;
702 }
703
704 if (d->dirListThread) {
705 d->dirListThread->requestTermination();
706 delete d->dirListThread;
707 d->dirListThread = nullptr;
708 }
709
710 if (d->userListThread) {
711 d->userListThread->requestTermination();
712 delete d->userListThread;
713 d->userListThread = nullptr;
714 }
715}
716
717/*
718 * Keep track of the last listed directory
719 */
720void KUrlCompletionPrivate::setListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden)
721{
722 last_compl_type = complType;
723 last_path_listed = directory;
724 last_file_listed = filter;
725 last_no_hidden = no_hidden;
726 last_prepend = prepend;
727}
728
729bool KUrlCompletionPrivate::isListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden)
730{
731 /* clang-format off */
732 return last_compl_type == complType
733 && (last_path_listed == directory || (directory.isEmpty() && last_path_listed.isEmpty()))
734 && (filter.startsWith(s: last_file_listed) || (filter.isEmpty() && last_file_listed.isEmpty()))
735 && last_no_hidden == no_hidden
736 && last_prepend == prepend; // e.g. relative path vs absolute
737 /* clang-format on */
738}
739
740/*
741 * isAutoCompletion
742 *
743 * Returns true if completion mode is Auto or Popup
744 */
745bool KUrlCompletionPrivate::isAutoCompletion()
746{
747 /* clang-format off */
748 return q->completionMode() == KCompletion::CompletionAuto
749 || q->completionMode() == KCompletion::CompletionPopup
750 || q->completionMode() == KCompletion::CompletionMan
751 || q->completionMode() == KCompletion::CompletionPopupAuto;
752 /* clang-format on */
753}
754//////////////////////////////////////////////////
755//////////////////////////////////////////////////
756// User directories
757//
758
759bool KUrlCompletionPrivate::userCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
760{
761 if (url.scheme() != QLatin1String("file") || !url.dir().isEmpty() || !url.file().startsWith(c: QLatin1Char('~')) || !prepend.isEmpty()) {
762 return false;
763 }
764
765 if (!isListedUrl(complType: CTUser)) {
766 q->stop();
767 q->clear();
768 setListedUrl(complType: CTUser);
769
770 Q_ASSERT(!userListThread); // caller called stop()
771 userListThread = new UserListThread(this);
772 QObject::connect(sender: userListThread, signal: &CompletionThread::completionThreadDone, context: q, slot: [this](QThread *thread, const QStringList &matches) {
773 slotCompletionThreadDone(thread, matches);
774 });
775 userListThread->start();
776
777 // If the thread finishes quickly make sure that the results
778 // are added to the first matching case.
779
780 userListThread->wait(time: initialWaitDuration());
781 const QStringList l = userListThread->matches();
782 addMatches(l);
783 }
784 *pMatch = finished();
785 return true;
786}
787
788/////////////////////////////////////////////////////
789/////////////////////////////////////////////////////
790// Environment variables
791//
792
793bool KUrlCompletionPrivate::envCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
794{
795 if (url.file().isEmpty() || url.file().at(i: 0) != QLatin1Char('$')) {
796 return false;
797 }
798
799 if (!isListedUrl(complType: CTEnv)) {
800 q->stop();
801 q->clear();
802
803 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
804 const QStringList keys = env.keys();
805
806 QStringList l;
807 l.reserve(asize: keys.size());
808 for (const QString &key : keys) {
809 l.append(t: prepend + QLatin1Char('$') + key);
810 }
811
812 addMatches(l);
813 }
814
815 setListedUrl(complType: CTEnv);
816
817 *pMatch = finished();
818 return true;
819}
820
821//////////////////////////////////////////////////
822//////////////////////////////////////////////////
823// Executables
824//
825
826bool KUrlCompletionPrivate::exeCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
827{
828 if (!url.isLocalFile()) {
829 return false;
830 }
831
832 QString directory = unescape(text: url.dir()); // remove escapes
833
834 // Find directories to search for completions, either
835 //
836 // 1. complete path given in url
837 // 2. current directory (d->cwd)
838 // 3. $PATH
839 // 4. no directory at all
840
841 QStringList dirList;
842
843 if (!url.file().isEmpty()) {
844 // $PATH
845 dirList = QString::fromLocal8Bit(ba: qgetenv(varName: "PATH")).split(sep: QDir::listSeparator(), behavior: Qt::SkipEmptyParts);
846
847 QStringList::Iterator it = dirList.begin();
848
849 for (; it != dirList.end(); ++it) {
850 it->append(c: QLatin1Char('/'));
851 }
852 } else if (Utils::isAbsoluteLocalPath(path: directory)) {
853 // complete path in url
854 dirList.append(t: directory);
855 } else if (!directory.isEmpty() && !cwd.isEmpty()) {
856 // current directory
857 dirList.append(t: cwd.toLocalFile() + QLatin1Char('/') + directory);
858 }
859
860 // No hidden files unless the user types "."
861 bool no_hidden_files = url.file().isEmpty() || url.file().at(i: 0) != QLatin1Char('.');
862
863 // List files if needed
864 //
865 if (!isListedUrl(complType: CTExe, directory, filter: url.file(), no_hidden: no_hidden_files)) {
866 q->stop();
867 q->clear();
868
869 setListedUrl(complType: CTExe, directory, filter: url.file(), no_hidden: no_hidden_files);
870
871 *pMatch = listDirectories(dirList, url.file(), only_exe: true, only_dir: false, no_hidden: no_hidden_files);
872 } else {
873 *pMatch = finished();
874 }
875
876 return true;
877}
878
879//////////////////////////////////////////////////
880//////////////////////////////////////////////////
881// Local files
882//
883
884bool KUrlCompletionPrivate::fileCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
885{
886 if (!url.isLocalFile()) {
887 return false;
888 }
889
890 QString directory = unescape(text: url.dir());
891
892 if (url.url() == QLatin1String("..")) {
893 *pMatch = QStringLiteral("..");
894 return true;
895 }
896
897 // qDebug() << "fileCompletion" << url << "dir=" << dir;
898
899 // Find directories to search for completions, either
900 //
901 // 1. complete path given in url
902 // 2. current directory (d->cwd)
903 // 3. no directory at all
904
905 QStringList dirList;
906
907 if (Utils::isAbsoluteLocalPath(path: directory)) {
908 // complete path in url
909 dirList.append(t: directory);
910 } else if (!cwd.isEmpty()) {
911 // current directory
912 QString dirToAdd = cwd.toLocalFile();
913 if (!directory.isEmpty()) {
914 Utils::appendSlash(path&: dirToAdd);
915 dirToAdd += directory;
916 }
917 dirList.append(t: dirToAdd);
918 }
919
920 // No hidden files unless the user types "."
921 bool no_hidden_files = !url.file().startsWith(c: QLatin1Char('.'));
922
923 // List files if needed
924 //
925 if (!isListedUrl(complType: CTFile, directory, filter: QString(), no_hidden: no_hidden_files)) {
926 q->stop();
927 q->clear();
928
929 setListedUrl(complType: CTFile, directory, filter: QString(), no_hidden: no_hidden_files);
930
931 // Append '/' to directories in Popup mode?
932 bool append_slash =
933 (popup_append_slash && (q->completionMode() == KCompletion::CompletionPopup || q->completionMode() == KCompletion::CompletionPopupAuto));
934
935 bool only_dir = (mode == KUrlCompletion::DirCompletion);
936
937 *pMatch = listDirectories(dirList, QString(), only_exe: false, only_dir, no_hidden: no_hidden_files, stat_files: append_slash);
938 } else {
939 *pMatch = finished();
940 }
941
942 return true;
943}
944
945//////////////////////////////////////////////////
946//////////////////////////////////////////////////
947// URLs not handled elsewhere...
948//
949
950static bool isLocalProtocol(const QString &protocol)
951{
952 return (KProtocolInfo::protocolClass(protocol) == QLatin1String(":local"));
953}
954
955bool KUrlCompletionPrivate::urlCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
956{
957 // qDebug() << *url.kurl();
958 if (onlyLocalProto && isLocalProtocol(protocol: url.scheme())) {
959 return false;
960 }
961
962 // Use d->cwd as base url in case url is not absolute
963 QUrl url_dir = url.kurl();
964 if (url_dir.isRelative() && !cwd.isEmpty()) {
965 // Create an URL with the directory to be listed
966 url_dir = cwd.resolved(relative: url_dir);
967 }
968
969 // url is malformed
970 if (!url_dir.isValid() || url.scheme().isEmpty()) {
971 return false;
972 }
973
974 // non local urls
975 if (!isLocalProtocol(protocol: url.scheme())) {
976 // url does not specify host
977 if (url_dir.host().isEmpty()) {
978 return false;
979 }
980
981 // url does not specify a valid directory
982 if (url_dir.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty()) {
983 return false;
984 }
985
986 // automatic completion is disabled
987 if (isAutoCompletion() && !url_auto_completion) {
988 return false;
989 }
990 }
991
992 // url handler doesn't support listing
993 if (!KProtocolManager::supportsListing(url: url_dir)) {
994 return false;
995 }
996
997 // Remove escapes
998 const QString directory = unescape(text: url_dir.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash).path());
999 url_dir.setPath(path: directory);
1000
1001 // List files if needed
1002 //
1003 if (!isListedUrl(complType: CTUrl, directory, filter: url.file())) {
1004 q->stop();
1005 q->clear();
1006
1007 setListedUrl(complType: CTUrl, directory, filter: QString());
1008
1009 QList<QUrl> url_list;
1010 url_list.append(t: url_dir);
1011
1012 listUrls(urls: url_list, filter: QString(), only_exe: false);
1013
1014 pMatch->clear();
1015 } else if (!q->isRunning()) {
1016 *pMatch = finished();
1017 } else {
1018 pMatch->clear();
1019 }
1020
1021 return true;
1022}
1023
1024//////////////////////////////////////////////////
1025//////////////////////////////////////////////////
1026// Directory and URL listing
1027//
1028
1029/*
1030 * addMatches
1031 *
1032 * Called to add matches to KCompletion
1033 */
1034void KUrlCompletionPrivate::addMatches(const QStringList &matchList)
1035{
1036 q->insertItems(items: matchList);
1037}
1038
1039/*
1040 * listDirectories
1041 *
1042 * List files starting with 'filter' in the given directories,
1043 * either using DirLister or listURLs()
1044 *
1045 * In either case, addMatches() is called with the listed
1046 * files, and eventually finished() when the listing is done
1047 *
1048 * Returns the match if available, or QString() if
1049 * DirLister timed out or using kio
1050 */
1051QString KUrlCompletionPrivate::listDirectories(const QStringList &dirList,
1052 const QString &filter,
1053 bool only_exe,
1054 bool only_dir,
1055 bool no_hidden,
1056 bool append_slash_to_dir)
1057{
1058 assert(!q->isRunning());
1059
1060 if (qEnvironmentVariableIsEmpty(varName: "KURLCOMPLETION_LOCAL_KIO")) {
1061 qCDebug(KIO_WIDGETS) << "Listing directories:" << dirList << "with filter=" << filter << "using thread";
1062
1063 // Don't use KIO
1064
1065 QStringList dirs;
1066
1067 QStringList::ConstIterator end = dirList.constEnd();
1068 for (QStringList::ConstIterator it = dirList.constBegin(); it != end; ++it) {
1069 QUrl url = QUrl::fromLocalFile(localfile: *it);
1070 if (KUrlAuthorized::authorizeUrlAction(QStringLiteral("list"), baseUrl: QUrl(), destUrl: url)) {
1071 dirs.append(t: *it);
1072 }
1073 }
1074
1075 Q_ASSERT(!dirListThread); // caller called stop()
1076 dirListThread = new DirectoryListThread(this, dirs, filter, mimeTypeFilters, only_exe, only_dir, no_hidden, append_slash_to_dir);
1077 QObject::connect(sender: dirListThread, signal: &CompletionThread::completionThreadDone, context: q, slot: [this](QThread *thread, const QStringList &matches) {
1078 slotCompletionThreadDone(thread, matches);
1079 });
1080 dirListThread->start();
1081 dirListThread->wait(time: initialWaitDuration());
1082 qCDebug(KIO_WIDGETS) << "Adding initial matches:" << dirListThread->matches();
1083 addMatches(matchList: dirListThread->matches());
1084
1085 return finished();
1086 }
1087
1088 // Use KIO
1089 // qDebug() << "Listing (listDirectories):" << dirList << "with KIO";
1090
1091 QList<QUrl> url_list;
1092
1093 QStringList::ConstIterator it = dirList.constBegin();
1094 QStringList::ConstIterator end = dirList.constEnd();
1095
1096 url_list.reserve(asize: dirList.size());
1097 for (; it != end; ++it) {
1098 url_list.append(t: QUrl(*it));
1099 }
1100
1101 listUrls(urls: url_list, filter, only_exe, no_hidden);
1102 // Will call addMatches() and finished()
1103
1104 return QString();
1105}
1106
1107/*
1108 * listURLs
1109 *
1110 * Use KIO to list the given urls
1111 *
1112 * addMatches() is called with the listed files
1113 * finished() is called when the listing is done
1114 */
1115void KUrlCompletionPrivate::listUrls(const QList<QUrl> &urls, const QString &filter, bool only_exe, bool no_hidden)
1116{
1117 assert(list_urls.isEmpty());
1118 assert(list_job == nullptr);
1119
1120 list_urls = urls;
1121 list_urls_filter = filter;
1122 list_urls_only_exe = only_exe;
1123 list_urls_no_hidden = no_hidden;
1124
1125 // qDebug() << "Listing URLs:" << *urls[0] << ",...";
1126
1127 // Start it off by calling slotIOFinished
1128 //
1129 // This will start a new list job as long as there
1130 // are urls in d->list_urls
1131 //
1132 slotIOFinished(nullptr);
1133}
1134
1135/*
1136 * slotEntries
1137 *
1138 * Receive files listed by KIO and call addMatches()
1139 */
1140void KUrlCompletionPrivate::slotEntries(KIO::Job *, const KIO::UDSEntryList &entries)
1141{
1142 QStringList matchList;
1143
1144 const QString filter = list_urls_filter;
1145 const int filter_len = filter.length();
1146
1147 // Iterate over all files
1148 for (const auto &entry : entries) {
1149 const QString udsUrl = entry.stringValue(field: KIO::UDSEntry::UDS_URL);
1150
1151 QString entry_name;
1152 if (!udsUrl.isEmpty()) {
1153 // qDebug() << "url:" << url;
1154 entry_name = QUrl(udsUrl).fileName();
1155 } else {
1156 entry_name = entry.stringValue(field: KIO::UDSEntry::UDS_NAME);
1157 }
1158
1159 // This can happen with kdeconnect://deviceId as a completion for kdeconnect:/,
1160 // there's no fileName [and the UDS_NAME is unrelated, can't use that].
1161 // This code doesn't support completing hostnames anyway (see addPathToUrl below).
1162 if (entry_name.isEmpty()) {
1163 continue;
1164 }
1165
1166 if (entry_name.at(i: 0) == QLatin1Char('.')
1167 && (list_urls_no_hidden || entry_name.length() == 1 || (entry_name.length() == 2 && entry_name.at(i: 1) == QLatin1Char('.')))) {
1168 continue;
1169 }
1170
1171 const bool isDir = entry.isDir();
1172
1173 if (mode == KUrlCompletion::DirCompletion && !isDir) {
1174 continue;
1175 }
1176
1177 if (filter_len != 0 && QStringView(entry_name).left(n: filter_len) != filter) {
1178 continue;
1179 }
1180
1181 if (!mimeTypeFilters.isEmpty() && !isDir && !mimeTypeFilters.contains(str: entry.stringValue(field: KIO::UDSEntry::UDS_MIME_TYPE))) {
1182 continue;
1183 }
1184
1185 QString toAppend = entry_name;
1186
1187 if (isDir) {
1188 toAppend.append(c: QLatin1Char('/'));
1189 }
1190
1191 if (!list_urls_only_exe || (entry.numberValue(field: KIO::UDSEntry::UDS_ACCESS) & s_modeExe) // true if executable
1192 ) {
1193 if (complete_url) {
1194 QUrl url(prepend);
1195 addPathToUrl(url, relPath: toAppend);
1196 matchList.append(t: url.toDisplayString());
1197 } else {
1198 matchList.append(t: prepend + toAppend);
1199 }
1200 }
1201 }
1202
1203 addMatches(matchList);
1204}
1205
1206/*
1207 * slotIOFinished
1208 *
1209 * Called when a KIO job is finished.
1210 *
1211 * Start a new list job if there are still urls in
1212 * list_urls, otherwise call finished()
1213 */
1214void KUrlCompletionPrivate::slotIOFinished(KJob *job)
1215{
1216 assert(job == list_job);
1217 Q_UNUSED(job)
1218
1219 if (list_urls.isEmpty()) {
1220 list_job = nullptr;
1221
1222 finished(); // will call KCompletion::makeCompletion()
1223
1224 } else {
1225 QUrl kurl(list_urls.takeFirst());
1226
1227 // list_urls.removeAll( kurl );
1228
1229 // qDebug() << "Start KIO::listDir" << kurl;
1230
1231 list_job = KIO::listDir(url: kurl, flags: KIO::HideProgressInfo);
1232 list_job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
1233
1234 assert(list_job);
1235
1236 q->connect(sender: list_job, signal: &KJob::result, context: q, slot: [this](KJob *job) {
1237 slotIOFinished(job);
1238 });
1239
1240 q->connect(sender: list_job, signal: &KIO::ListJob::entries, context: q, slot: [this](KIO::Job *job, const KIO::UDSEntryList &list) {
1241 slotEntries(job, entries: list);
1242 });
1243 }
1244}
1245
1246///////////////////////////////////////////////////
1247///////////////////////////////////////////////////
1248
1249/*
1250 * postProcessMatch, postProcessMatches
1251 *
1252 * Called by KCompletion before emitting match() and matches()
1253 *
1254 * Append '/' to directories for file completion. This is
1255 * done here to avoid stat()'ing a lot of files
1256 */
1257void KUrlCompletion::postProcessMatch(QString *pMatch) const
1258{
1259 // qDebug() << *pMatch;
1260
1261 if (!pMatch->isEmpty() && pMatch->startsWith(s: QLatin1String("file:"))) {
1262 // Add '/' to directories in file completion mode
1263 // unless it has already been done
1264 if (d->last_compl_type == CTFile && pMatch->at(i: pMatch->length() - 1) != QLatin1Char('/')) {
1265 QString copy = QUrl(*pMatch).toLocalFile();
1266 expandTilde(copy);
1267 expandEnv(copy);
1268 if (!Utils::isAbsoluteLocalPath(path: copy)) {
1269 copy.prepend(s: d->cwd.toLocalFile() + QLatin1Char('/'));
1270 }
1271
1272 // qDebug() << "stat'ing" << copy;
1273
1274 QByteArray file = QFile::encodeName(fileName: copy);
1275
1276 QT_STATBUF sbuff;
1277 if (QT_STAT(file: file.constData(), buf: &sbuff) == 0) {
1278 if (Utils::isDirMask(mode: sbuff.st_mode)) {
1279 pMatch->append(c: QLatin1Char('/'));
1280 }
1281 } else {
1282 // qDebug() << "Could not stat file" << copy;
1283 }
1284 }
1285 }
1286}
1287
1288void KUrlCompletion::postProcessMatches(QStringList * /*matches*/) const
1289{
1290 // Maybe '/' should be added to directories here as in
1291 // postProcessMatch() but it would slow things down
1292 // when there are a lot of matches...
1293}
1294
1295void KUrlCompletion::postProcessMatches(KCompletionMatches * /*matches*/) const
1296{
1297 // Maybe '/' should be added to directories here as in
1298 // postProcessMatch() but it would slow things down
1299 // when there are a lot of matches...
1300}
1301
1302void KUrlCompletionPrivate::slotCompletionThreadDone(QThread *thread, const QStringList &matches)
1303{
1304 if (thread != userListThread && thread != dirListThread) {
1305 qCDebug(KIO_WIDGETS) << "got" << matches.count() << "outdated matches";
1306 return;
1307 }
1308
1309 qCDebug(KIO_WIDGETS) << "got" << matches.count() << "matches at end of thread";
1310 q->setItems(matches);
1311
1312 if (userListThread == thread) {
1313 thread->wait();
1314 delete thread;
1315 userListThread = nullptr;
1316 } else if (dirListThread == thread) {
1317 thread->wait();
1318 delete thread;
1319 dirListThread = nullptr;
1320 }
1321 finished(); // will call KCompletion::makeCompletion()
1322}
1323
1324// static
1325QString KUrlCompletion::replacedPath(const QString &text, bool replaceHome, bool replaceEnv)
1326{
1327 if (text.isEmpty()) {
1328 return text;
1329 }
1330
1331 KUrlCompletionPrivate::MyURL url(text, QUrl()); // no need to replace something of our current cwd
1332 if (!url.kurl().isLocalFile()) {
1333 return text;
1334 }
1335
1336 url.filter(replace_user_dir: replaceHome, replace_env: replaceEnv);
1337 return url.dir() + url.file();
1338}
1339
1340QString KUrlCompletion::replacedPath(const QString &text) const
1341{
1342 return replacedPath(text, replaceHome: d->replace_home, replaceEnv: d->replace_env);
1343}
1344
1345void KUrlCompletion::setMimeTypeFilters(const QStringList &mimeTypeFilters)
1346{
1347 d->mimeTypeFilters = mimeTypeFilters;
1348}
1349
1350QStringList KUrlCompletion::mimeTypeFilters() const
1351{
1352 return d->mimeTypeFilters;
1353}
1354
1355/////////////////////////////////////////////////////////
1356/////////////////////////////////////////////////////////
1357// Static functions
1358
1359/*
1360 * expandEnv
1361 *
1362 * Expand environment variables in text. Escaped '$' are ignored.
1363 * Return true if expansion was made.
1364 */
1365static bool expandEnv(QString &text)
1366{
1367 // Find all environment variables beginning with '$'
1368 //
1369 int pos = 0;
1370
1371 bool expanded = false;
1372
1373 while ((pos = text.indexOf(c: QLatin1Char('$'), from: pos)) != -1) {
1374 // Skip escaped '$'
1375 //
1376 if (pos > 0 && text.at(i: pos - 1) == QLatin1Char('\\')) {
1377 pos++;
1378 }
1379 // Variable found => expand
1380 //
1381 else {
1382 // Find the end of the variable = next '/' or ' '
1383 //
1384 int pos2 = text.indexOf(c: QLatin1Char(' '), from: pos + 1);
1385 int pos_tmp = text.indexOf(c: QLatin1Char('/'), from: pos + 1);
1386
1387 if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) {
1388 pos2 = pos_tmp;
1389 }
1390
1391 if (pos2 == -1) {
1392 pos2 = text.length();
1393 }
1394
1395 // Replace if the variable is terminated by '/' or ' '
1396 // and defined
1397 //
1398 if (pos2 >= 0) {
1399 const int len = pos2 - pos;
1400 const QStringView key = QStringView(text).mid(pos: pos + 1, n: len - 1);
1401 const QString value = QString::fromLocal8Bit(ba: qgetenv(varName: key.toLocal8Bit().constData()));
1402
1403 if (!value.isEmpty()) {
1404 expanded = true;
1405 text.replace(i: pos, len, after: value);
1406 pos = pos + value.length();
1407 } else {
1408 pos = pos2;
1409 }
1410 }
1411 }
1412 }
1413
1414 return expanded;
1415}
1416
1417/*
1418 * expandTilde
1419 *
1420 * Replace "~user" with the users home directory
1421 * Return true if expansion was made.
1422 */
1423static bool expandTilde(QString &text)
1424{
1425 if (text.isEmpty() || (text.at(i: 0) != QLatin1Char('~'))) {
1426 return false;
1427 }
1428
1429 bool expanded = false;
1430
1431 // Find the end of the user name = next '/' or ' '
1432 //
1433 int pos2 = text.indexOf(c: QLatin1Char(' '), from: 1);
1434 int pos_tmp = text.indexOf(c: QLatin1Char('/'), from: 1);
1435
1436 if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) {
1437 pos2 = pos_tmp;
1438 }
1439
1440 if (pos2 == -1) {
1441 pos2 = text.length();
1442 }
1443
1444 // Replace ~user if the user name is terminated by '/' or ' '
1445 //
1446 if (pos2 >= 0) {
1447 QString userName = text.mid(position: 1, n: pos2 - 1);
1448 QString dir;
1449
1450 // A single ~ is replaced with $HOME
1451 //
1452 if (userName.isEmpty()) {
1453 dir = QDir::homePath();
1454 }
1455 // ~user is replaced with the dir from passwd
1456 //
1457 else {
1458 KUser user(userName);
1459 dir = user.homeDir();
1460 }
1461
1462 if (!dir.isEmpty()) {
1463 expanded = true;
1464 text.replace(i: 0, len: pos2, after: dir);
1465 }
1466 }
1467
1468 return expanded;
1469}
1470
1471/*
1472 * unescape
1473 *
1474 * Remove escapes and return the result in a new string
1475 *
1476 */
1477static QString unescape(const QString &text)
1478{
1479 QString result;
1480 result.reserve(asize: text.size());
1481
1482 for (const QChar ch : text) {
1483 if (ch != QLatin1Char('\\')) {
1484 result.append(c: ch);
1485 }
1486 }
1487
1488 return result;
1489}
1490
1491#include "kurlcompletion.moc"
1492#include "moc_kurlcompletion.cpp"
1493

source code of kio/src/widgets/kurlcompletion.cpp