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 { |
62 | CTNone = 0, |
63 | CTEnv, |
64 | CTUser, |
65 | CTMan, |
66 | CTExe, |
67 | CTFile, |
68 | CTUrl, |
69 | CTInfo |
70 | }; |
71 | |
72 | class CompletionThread; |
73 | |
74 | // Ensure that we don't end up with "//". |
75 | static void addPathToUrl(QUrl &url, const QString &relPath) |
76 | { |
77 | url.setPath(path: Utils::concatPaths(path1: url.path(), path2: relPath)); |
78 | } |
79 | |
80 | static QBasicAtomicInt s_waitDuration = Q_BASIC_ATOMIC_INITIALIZER(-1); |
81 | |
82 | static 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 |
96 | static 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 | // |
109 | class KUrlCompletionPrivate |
110 | { |
111 | public: |
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 = 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 | |
197 | class CompletionThread : public QThread |
198 | { |
199 | Q_OBJECT |
200 | protected: |
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 | |
209 | public: |
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 | |
225 | Q_SIGNALS: |
226 | void completionThreadDone(QThread *thread, const QStringList &matches); |
227 | |
228 | protected: |
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 | |
249 | private: |
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 | */ |
259 | class UserListThread : public CompletionThread |
260 | { |
261 | Q_OBJECT |
262 | public: |
263 | UserListThread(KUrlCompletionPrivate *receiver) |
264 | : CompletionThread(receiver) |
265 | { |
266 | } |
267 | |
268 | protected: |
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 | |
297 | class DirectoryListThread : public CompletionThread |
298 | { |
299 | Q_OBJECT |
300 | public: |
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 | |
322 | private: |
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 | |
332 | void 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 | |
395 | KUrlCompletionPrivate::~KUrlCompletionPrivate() |
396 | { |
397 | } |
398 | |
399 | /////////////////////////////////////////////////////// |
400 | /////////////////////////////////////////////////////// |
401 | // MyURL - wrapper for QUrl with some different functionality |
402 | // |
403 | |
404 | class KUrlCompletionPrivate::MyURL |
405 | { |
406 | public: |
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 | |
448 | private: |
449 | void init(const QString &url, const QUrl &cwd); |
450 | |
451 | QUrl m_kurl; |
452 | QString m_url; |
453 | bool m_isURL; |
454 | }; |
455 | |
456 | KUrlCompletionPrivate::MyURL::MyURL(const QString &_url, const QUrl &cwd) |
457 | { |
458 | init(url: _url, cwd); |
459 | } |
460 | |
461 | KUrlCompletionPrivate::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 | |
468 | void 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 | |
509 | KUrlCompletionPrivate::MyURL::~MyURL() |
510 | { |
511 | } |
512 | |
513 | void 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 | |
530 | KUrlCompletion::KUrlCompletion() |
531 | : KUrlCompletion(FileCompletion) |
532 | { |
533 | } |
534 | |
535 | KUrlCompletion::KUrlCompletion(Mode _mode) |
536 | : KCompletion() |
537 | , d(new KUrlCompletionPrivate(this, _mode)) |
538 | { |
539 | } |
540 | |
541 | KUrlCompletion::~KUrlCompletion() |
542 | { |
543 | stop(); |
544 | } |
545 | |
546 | void KUrlCompletion::setDir(const QUrl &dir) |
547 | { |
548 | d->cwd = dir; |
549 | } |
550 | |
551 | QUrl KUrlCompletion::dir() const |
552 | { |
553 | return d->cwd; |
554 | } |
555 | |
556 | KUrlCompletion::Mode KUrlCompletion::mode() const |
557 | { |
558 | return d->mode; |
559 | } |
560 | |
561 | void KUrlCompletion::setMode(Mode _mode) |
562 | { |
563 | d->mode = _mode; |
564 | } |
565 | |
566 | bool KUrlCompletion::replaceEnv() const |
567 | { |
568 | return d->replace_env; |
569 | } |
570 | |
571 | void KUrlCompletion::setReplaceEnv(bool replace) |
572 | { |
573 | d->replace_env = replace; |
574 | } |
575 | |
576 | bool KUrlCompletion::replaceHome() const |
577 | { |
578 | return d->replace_home; |
579 | } |
580 | |
581 | void KUrlCompletion::setReplaceHome(bool replace) |
582 | { |
583 | d->replace_home = replace; |
584 | } |
585 | |
586 | /* |
587 | * makeCompletion() |
588 | * |
589 | * Entry point for file name completion |
590 | */ |
591 | QString 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 | */ |
681 | QString 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 | */ |
695 | bool 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 | */ |
705 | void 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 | */ |
728 | void 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 | |
737 | bool 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 | */ |
753 | bool 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 | |
767 | bool 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 | |
801 | bool 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 | |
834 | bool 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 | |
892 | bool 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 | |
958 | static bool isLocalProtocol(const QString &protocol) |
959 | { |
960 | return (KProtocolInfo::protocolClass(protocol) == QLatin1String(":local" )); |
961 | } |
962 | |
963 | bool 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 | */ |
1042 | void 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 | */ |
1059 | QString 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 | */ |
1123 | void 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 | */ |
1148 | void 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 | */ |
1222 | void 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 | */ |
1265 | void 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 | |
1296 | void 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 | |
1303 | void 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 | |
1310 | void 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 |
1333 | QString 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 | |
1348 | QString KUrlCompletion::replacedPath(const QString &text) const |
1349 | { |
1350 | return replacedPath(text, replaceHome: d->replace_home, replaceEnv: d->replace_env); |
1351 | } |
1352 | |
1353 | void KUrlCompletion::setMimeTypeFilters(const QStringList &mimeTypeFilters) |
1354 | { |
1355 | d->mimeTypeFilters = mimeTypeFilters; |
1356 | } |
1357 | |
1358 | QStringList 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 | */ |
1373 | static 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 | */ |
1431 | static 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 | */ |
1485 | static 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 | |