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

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