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 | |
51 | static bool expandTilde(QString &); |
52 | static bool expandEnv(QString &); |
53 | |
54 | static QString unescape(const QString &text); |
55 | |
56 | // Permission mask for files that are executable by |
57 | // user, group or other |
58 | static constexpr mode_t s_modeExe = S_IXUSR | S_IXGRP | S_IXOTH; |
59 | |
60 | // Constants for types of completion |
61 | enum ComplType { CTNone = 0, CTEnv, CTUser, CTMan, CTExe, CTFile, CTUrl, CTInfo }; |
62 | |
63 | class CompletionThread; |
64 | |
65 | // Ensure that we don't end up with "//". |
66 | static void addPathToUrl(QUrl &url, const QString &relPath) |
67 | { |
68 | url.setPath(path: Utils::concatPaths(path1: url.path(), path2: relPath)); |
69 | } |
70 | |
71 | static QBasicAtomicInt s_waitDuration = Q_BASIC_ATOMIC_INITIALIZER(-1); |
72 | |
73 | static 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 |
87 | static 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 | // |
100 | class KUrlCompletionPrivate |
101 | { |
102 | public: |
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 = 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 | |
188 | class CompletionThread : public QThread |
189 | { |
190 | Q_OBJECT |
191 | protected: |
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 | |
200 | public: |
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 | |
216 | Q_SIGNALS: |
217 | void completionThreadDone(QThread *thread, const QStringList &matches); |
218 | |
219 | protected: |
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 | |
240 | private: |
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 | |
251 | class UserListThread : public CompletionThread |
252 | { |
253 | Q_OBJECT |
254 | public: |
255 | UserListThread(KUrlCompletionPrivate *receiver) |
256 | : CompletionThread(receiver) |
257 | { |
258 | } |
259 | |
260 | protected: |
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 | |
289 | class DirectoryListThread : public CompletionThread |
290 | { |
291 | Q_OBJECT |
292 | public: |
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 | |
314 | private: |
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 | |
324 | void 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 | |
387 | KUrlCompletionPrivate::~KUrlCompletionPrivate() |
388 | { |
389 | } |
390 | |
391 | /////////////////////////////////////////////////////// |
392 | /////////////////////////////////////////////////////// |
393 | // MyURL - wrapper for QUrl with some different functionality |
394 | // |
395 | |
396 | class KUrlCompletionPrivate::MyURL |
397 | { |
398 | public: |
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 | |
440 | private: |
441 | void init(const QString &url, const QUrl &cwd); |
442 | |
443 | QUrl m_kurl; |
444 | QString m_url; |
445 | bool m_isURL; |
446 | }; |
447 | |
448 | KUrlCompletionPrivate::MyURL::MyURL(const QString &_url, const QUrl &cwd) |
449 | { |
450 | init(url: _url, cwd); |
451 | } |
452 | |
453 | KUrlCompletionPrivate::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 | |
460 | void 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 | |
501 | KUrlCompletionPrivate::MyURL::~MyURL() |
502 | { |
503 | } |
504 | |
505 | void 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 | |
522 | KUrlCompletion::KUrlCompletion() |
523 | : KUrlCompletion(FileCompletion) |
524 | { |
525 | } |
526 | |
527 | KUrlCompletion::KUrlCompletion(Mode _mode) |
528 | : KCompletion() |
529 | , d(new KUrlCompletionPrivate(this, _mode)) |
530 | { |
531 | } |
532 | |
533 | KUrlCompletion::~KUrlCompletion() |
534 | { |
535 | stop(); |
536 | } |
537 | |
538 | void KUrlCompletion::setDir(const QUrl &dir) |
539 | { |
540 | d->cwd = dir; |
541 | } |
542 | |
543 | QUrl KUrlCompletion::dir() const |
544 | { |
545 | return d->cwd; |
546 | } |
547 | |
548 | KUrlCompletion::Mode KUrlCompletion::mode() const |
549 | { |
550 | return d->mode; |
551 | } |
552 | |
553 | void KUrlCompletion::setMode(Mode _mode) |
554 | { |
555 | d->mode = _mode; |
556 | } |
557 | |
558 | bool KUrlCompletion::replaceEnv() const |
559 | { |
560 | return d->replace_env; |
561 | } |
562 | |
563 | void KUrlCompletion::setReplaceEnv(bool replace) |
564 | { |
565 | d->replace_env = replace; |
566 | } |
567 | |
568 | bool KUrlCompletion::replaceHome() const |
569 | { |
570 | return d->replace_home; |
571 | } |
572 | |
573 | void KUrlCompletion::setReplaceHome(bool replace) |
574 | { |
575 | d->replace_home = replace; |
576 | } |
577 | |
578 | /* |
579 | * makeCompletion() |
580 | * |
581 | * Entry point for file name completion |
582 | */ |
583 | QString 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 | */ |
673 | QString 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 | */ |
687 | bool 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 | */ |
697 | void 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 | */ |
720 | void 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 | |
729 | bool 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 | */ |
745 | bool 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 | |
759 | bool 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 | |
793 | bool 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 | |
826 | bool 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 | |
884 | bool 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 | |
950 | static bool isLocalProtocol(const QString &protocol) |
951 | { |
952 | return (KProtocolInfo::protocolClass(protocol) == QLatin1String(":local" )); |
953 | } |
954 | |
955 | bool 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 | */ |
1034 | void 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 | */ |
1051 | QString 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 | */ |
1115 | void 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 | */ |
1140 | void 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 | */ |
1214 | void 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 | */ |
1257 | void 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 | |
1288 | void 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 | |
1295 | void 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 | |
1302 | void 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 |
1325 | QString 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 | |
1340 | QString KUrlCompletion::replacedPath(const QString &text) const |
1341 | { |
1342 | return replacedPath(text, replaceHome: d->replace_home, replaceEnv: d->replace_env); |
1343 | } |
1344 | |
1345 | void KUrlCompletion::setMimeTypeFilters(const QStringList &mimeTypeFilters) |
1346 | { |
1347 | d->mimeTypeFilters = mimeTypeFilters; |
1348 | } |
1349 | |
1350 | QStringList 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 | */ |
1365 | static 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 | */ |
1423 | static 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 | */ |
1477 | static 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 | |