kurlcompletion.cpp
00001 /* -*- indent-tabs-mode: t; tab-width: 4; c-basic-offset:4 -*- 00002 00003 This file is part of the KDE libraries 00004 Copyright (C) 2000 David Smith <dsmith@algonet.se> 00005 Copyright (C) 2004 Scott Wheeler <wheeler@kde.org> 00006 00007 This class was inspired by a previous KURLCompletion by 00008 Henner Zeller <zeller@think.de> 00009 00010 This library is free software; you can redistribute it and/or 00011 modify it under the terms of the GNU Library General Public 00012 License as published by the Free Software Foundation; either 00013 version 2 of the License, or (at your option) any later version. 00014 00015 This library is distributed in the hope that it will be useful, 00016 but WITHOUT ANY WARRANTY; without even the implied warranty of 00017 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00018 Library General Public License for more details. 00019 00020 You should have received a copy of the GNU Library General Public License 00021 along with this library; see the file COPYING.LIB. If not, write to 00022 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 00023 Boston, MA 02110-1301, USA. 00024 */ 00025 00026 #include <config.h> 00027 #include <stdlib.h> 00028 #include <assert.h> 00029 #include <limits.h> 00030 00031 #include <qstring.h> 00032 #include <qstringlist.h> 00033 #include <qvaluelist.h> 00034 #include <qregexp.h> 00035 #include <qtimer.h> 00036 #include <qdir.h> 00037 #include <qfile.h> 00038 #include <qtextstream.h> 00039 #include <qdeepcopy.h> 00040 #include <qthread.h> 00041 00042 #include <kapplication.h> 00043 #include <kdebug.h> 00044 #include <kcompletion.h> 00045 #include <kurl.h> 00046 #include <kio/jobclasses.h> 00047 #include <kio/job.h> 00048 #include <kprotocolinfo.h> 00049 #include <kconfig.h> 00050 #include <kglobal.h> 00051 #include <klocale.h> 00052 #include <kde_file.h> 00053 00054 #include <sys/types.h> 00055 #include <dirent.h> 00056 #include <unistd.h> 00057 #include <sys/stat.h> 00058 #include <pwd.h> 00059 #include <time.h> 00060 #include <sys/param.h> 00061 00062 #include "kurlcompletion.h" 00063 00064 static bool expandTilde(QString &); 00065 static bool expandEnv(QString &); 00066 00067 static QString unescape(const QString &text); 00068 00069 // Permission mask for files that are executable by 00070 // user, group or other 00071 #define MODE_EXE (S_IXUSR | S_IXGRP | S_IXOTH) 00072 00073 // Constants for types of completion 00074 enum ComplType {CTNone=0, CTEnv, CTUser, CTMan, CTExe, CTFile, CTUrl, CTInfo}; 00075 00076 class CompletionThread; 00077 00083 class CompletionMatchEvent : public QCustomEvent 00084 { 00085 public: 00086 CompletionMatchEvent( CompletionThread *thread ) : 00087 QCustomEvent( uniqueType() ), 00088 m_completionThread( thread ) 00089 {} 00090 00091 CompletionThread *completionThread() const { return m_completionThread; } 00092 static int uniqueType() { return User + 61080; } 00093 00094 private: 00095 CompletionThread *m_completionThread; 00096 }; 00097 00098 class CompletionThread : public QThread 00099 { 00100 protected: 00101 CompletionThread( KURLCompletion *receiver ) : 00102 QThread(), 00103 m_receiver( receiver ), 00104 m_terminationRequested( false ) 00105 {} 00106 00107 public: 00108 void requestTermination() { m_terminationRequested = true; } 00109 QDeepCopy<QStringList> matches() const { return m_matches; } 00110 00111 protected: 00112 void addMatch( const QString &match ) { m_matches.append( match ); } 00113 bool terminationRequested() const { return m_terminationRequested; } 00114 void done() 00115 { 00116 if ( !m_terminationRequested ) 00117 kapp->postEvent( m_receiver, new CompletionMatchEvent( this ) ); 00118 else 00119 delete this; 00120 } 00121 00122 private: 00123 KURLCompletion *m_receiver; 00124 QStringList m_matches; 00125 bool m_terminationRequested; 00126 }; 00127 00133 class UserListThread : public CompletionThread 00134 { 00135 public: 00136 UserListThread( KURLCompletion *receiver ) : 00137 CompletionThread( receiver ) 00138 {} 00139 00140 protected: 00141 virtual void run() 00142 { 00143 static const QChar tilde = '~'; 00144 00145 struct passwd *pw; 00146 while ( ( pw = ::getpwent() ) && !terminationRequested() ) 00147 addMatch( tilde + QString::fromLocal8Bit( pw->pw_name ) ); 00148 00149 ::endpwent(); 00150 00151 addMatch( tilde ); 00152 00153 done(); 00154 } 00155 }; 00156 00157 class DirectoryListThread : public CompletionThread 00158 { 00159 public: 00160 DirectoryListThread( KURLCompletion *receiver, 00161 const QStringList &dirList, 00162 const QString &filter, 00163 bool onlyExe, 00164 bool onlyDir, 00165 bool noHidden, 00166 bool appendSlashToDir ) : 00167 CompletionThread( receiver ), 00168 m_dirList( QDeepCopy<QStringList>( dirList ) ), 00169 m_filter( QDeepCopy<QString>( filter ) ), 00170 m_onlyExe( onlyExe ), 00171 m_onlyDir( onlyDir ), 00172 m_noHidden( noHidden ), 00173 m_appendSlashToDir( appendSlashToDir ) 00174 {} 00175 00176 virtual void run(); 00177 00178 private: 00179 QStringList m_dirList; 00180 QString m_filter; 00181 bool m_onlyExe; 00182 bool m_onlyDir; 00183 bool m_noHidden; 00184 bool m_appendSlashToDir; 00185 }; 00186 00187 void DirectoryListThread::run() 00188 { 00189 // Thread safety notes: 00190 // 00191 // There very possibly may be thread safety issues here, but I've done a check 00192 // of all of the things that would seem to be problematic. Here are a few 00193 // things that I have checked to be safe here (some used indirectly): 00194 // 00195 // QDir::currentDirPath(), QDir::setCurrent(), QFile::decodeName(), QFile::encodeName() 00196 // QString::fromLocal8Bit(), QString::local8Bit(), QTextCodec::codecForLocale() 00197 // 00198 // Also see (for POSIX functions): 00199 // http://www.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_09.html 00200 00201 DIR *dir = 0; 00202 00203 for ( QStringList::ConstIterator it = m_dirList.begin(); 00204 it != m_dirList.end() && !terminationRequested(); 00205 ++it ) 00206 { 00207 // Open the next directory 00208 00209 if ( !dir ) { 00210 dir = ::opendir( QFile::encodeName( *it ) ); 00211 if ( ! dir ) { 00212 kdDebug() << "Failed to open dir: " << *it << endl; 00213 done(); 00214 return; 00215 } 00216 } 00217 00218 // A trick from KIO that helps performance by a little bit: 00219 // chdir to the directroy so we won't have to deal with full paths 00220 // with stat() 00221 00222 QString path = QDir::currentDirPath(); 00223 QDir::setCurrent( *it ); 00224 00225 // Loop through all directory entries 00226 // Solaris and IRIX dirent structures do not allocate space for d_name. On 00227 // systems that do (HP-UX, Linux, Tru64 UNIX), we overallocate space but 00228 // that's ok. 00229 #ifndef HAVE_READDIR_R 00230 struct dirent *dirEntry = 0; 00231 while ( !terminationRequested() && 00232 (dirEntry = ::readdir( dir))) 00233 #else 00234 struct dirent *dirPosition = (struct dirent *) malloc( sizeof( struct dirent ) + MAXPATHLEN + 1 ); 00235 struct dirent *dirEntry = 0; 00236 while ( !terminationRequested() && 00237 ::readdir_r( dir, dirPosition, &dirEntry ) == 0 && dirEntry ) 00238 #endif 00239 00240 { 00241 // Skip hidden files if m_noHidden is true 00242 00243 if ( dirEntry->d_name[0] == '.' && m_noHidden ) 00244 continue; 00245 00246 // Skip "." 00247 00248 if ( dirEntry->d_name[0] == '.' && dirEntry->d_name[1] == '\0' ) 00249 continue; 00250 00251 // Skip ".." 00252 00253 if ( dirEntry->d_name[0] == '.' && dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0' ) 00254 continue; 00255 00256 QString file = QFile::decodeName( dirEntry->d_name ); 00257 00258 if ( m_filter.isEmpty() || file.startsWith( m_filter ) ) { 00259 00260 if ( m_onlyExe || m_onlyDir || m_appendSlashToDir ) { 00261 KDE_struct_stat sbuff; 00262 00263 if ( KDE_stat( dirEntry->d_name, &sbuff ) == 0 ) { 00264 00265 // Verify executable 00266 00267 if ( m_onlyExe && ( sbuff.st_mode & MODE_EXE ) == 0 ) 00268 continue; 00269 00270 // Verify directory 00271 00272 if ( m_onlyDir && !S_ISDIR( sbuff.st_mode ) ) 00273 continue; 00274 00275 // Add '/' to directories 00276 00277 if ( m_appendSlashToDir && S_ISDIR( sbuff.st_mode ) ) 00278 file.append( '/' ); 00279 00280 } 00281 else { 00282 kdDebug() << "Could not stat file " << file << endl; 00283 continue; 00284 } 00285 } 00286 00287 addMatch( file ); 00288 } 00289 } 00290 00291 // chdir to the original directory 00292 00293 QDir::setCurrent( path ); 00294 00295 ::closedir( dir ); 00296 dir = 0; 00297 #ifdef HAVE_READDIR_R 00298 free( dirPosition ); 00299 #endif 00300 } 00301 00302 done(); 00303 } 00304 00307 // MyURL - wrapper for KURL with some different functionality 00308 // 00309 00310 class KURLCompletion::MyURL 00311 { 00312 public: 00313 MyURL(const QString &url, const QString &cwd); 00314 MyURL(const MyURL &url); 00315 ~MyURL(); 00316 00317 KURL *kurl() const { return m_kurl; } 00318 00319 QString protocol() const { return m_kurl->protocol(); } 00320 // The directory with a trailing '/' 00321 QString dir() const { return m_kurl->directory(false, false); } 00322 QString file() const { return m_kurl->fileName(false); } 00323 00324 // The initial, unparsed, url, as a string. 00325 QString url() const { return m_url; } 00326 00327 // Is the initial string a URL, or just a path (whether absolute or relative) 00328 bool isURL() const { return m_isURL; } 00329 00330 void filter( bool replace_user_dir, bool replace_env ); 00331 00332 private: 00333 void init(const QString &url, const QString &cwd); 00334 00335 KURL *m_kurl; 00336 QString m_url; 00337 bool m_isURL; 00338 }; 00339 00340 KURLCompletion::MyURL::MyURL(const QString &url, const QString &cwd) 00341 { 00342 init(url, cwd); 00343 } 00344 00345 KURLCompletion::MyURL::MyURL(const MyURL &url) 00346 { 00347 m_kurl = new KURL( *(url.m_kurl) ); 00348 m_url = url.m_url; 00349 m_isURL = url.m_isURL; 00350 } 00351 00352 void KURLCompletion::MyURL::init(const QString &url, const QString &cwd) 00353 { 00354 // Save the original text 00355 m_url = url; 00356 00357 // Non-const copy 00358 QString url_copy = url; 00359 00360 // Special shortcuts for "man:" and "info:" 00361 if ( url_copy[0] == '#' ) { 00362 if ( url_copy[1] == '#' ) 00363 url_copy.replace( 0, 2, QString("info:") ); 00364 else 00365 url_copy.replace( 0, 1, QString("man:") ); 00366 } 00367 00368 // Look for a protocol in 'url' 00369 QRegExp protocol_regex = QRegExp( "^[^/\\s\\\\]*:" ); 00370 00371 // Assume "file:" or whatever is given by 'cwd' if there is 00372 // no protocol. (KURL does this only for absoute paths) 00373 if ( protocol_regex.search( url_copy ) == 0 ) 00374 { 00375 m_kurl = new KURL( url_copy ); 00376 m_isURL = true; 00377 } 00378 else // relative path or ~ or $something 00379 { 00380 m_isURL = false; 00381 if ( cwd.isEmpty() ) 00382 { 00383 m_kurl = new KURL(); 00384 if ( !QDir::isRelativePath(url_copy) || url_copy[0] == '$' || url_copy[0] == '~' ) 00385 m_kurl->setPath( url_copy ); 00386 else 00387 *m_kurl = url_copy; 00388 } 00389 else 00390 { 00391 KURL base = KURL::fromPathOrURL( cwd ); 00392 base.adjustPath(+1); 00393 00394 if ( !QDir::isRelativePath(url_copy) || url_copy[0] == '~' || url_copy[0] == '$' ) 00395 { 00396 m_kurl = new KURL(); 00397 m_kurl->setPath( url_copy ); 00398 } 00399 else // relative path 00400 { 00401 //m_kurl = new KURL( base, url_copy ); 00402 m_kurl = new KURL( base ); 00403 m_kurl->addPath( url_copy ); 00404 } 00405 } 00406 } 00407 } 00408 00409 KURLCompletion::MyURL::~MyURL() 00410 { 00411 delete m_kurl; 00412 } 00413 00414 void KURLCompletion::MyURL::filter( bool replace_user_dir, bool replace_env ) 00415 { 00416 QString d = dir() + file(); 00417 if ( replace_user_dir ) expandTilde( d ); 00418 if ( replace_env ) expandEnv( d ); 00419 m_kurl->setPath( d ); 00420 } 00421 00424 // KURLCompletionPrivate 00425 // 00426 class KURLCompletionPrivate 00427 { 00428 public: 00429 KURLCompletionPrivate() : url_auto_completion(true), 00430 userListThread(0), 00431 dirListThread(0) {} 00432 ~KURLCompletionPrivate(); 00433 00434 QValueList<KURL*> list_urls; 00435 00436 bool onlyLocalProto; 00437 00438 // urlCompletion() in Auto/Popup mode? 00439 bool url_auto_completion; 00440 00441 // Append '/' to directories in Popup mode? 00442 // Doing that stat's all files and is slower 00443 bool popup_append_slash; 00444 00445 // Keep track of currently listed files to avoid reading them again 00446 QString last_path_listed; 00447 QString last_file_listed; 00448 QString last_prepend; 00449 int last_compl_type; 00450 int last_no_hidden; 00451 00452 QString cwd; // "current directory" = base dir for completion 00453 00454 KURLCompletion::Mode mode; // ExeCompletion, FileCompletion, DirCompletion 00455 bool replace_env; 00456 bool replace_home; 00457 bool complete_url; // if true completing a URL (i.e. 'prepend' is a URL), otherwise a path 00458 00459 KIO::ListJob *list_job; // kio job to list directories 00460 00461 QString prepend; // text to prepend to listed items 00462 QString compl_text; // text to pass on to KCompletion 00463 00464 // Filters for files read with kio 00465 bool list_urls_only_exe; // true = only list executables 00466 bool list_urls_no_hidden; 00467 QString list_urls_filter; // filter for listed files 00468 00469 CompletionThread *userListThread; 00470 CompletionThread *dirListThread; 00471 }; 00472 00473 KURLCompletionPrivate::~KURLCompletionPrivate() 00474 { 00475 if ( userListThread ) 00476 userListThread->requestTermination(); 00477 if ( dirListThread ) 00478 dirListThread->requestTermination(); 00479 } 00480 00483 // KURLCompletion 00484 // 00485 00486 KURLCompletion::KURLCompletion() : KCompletion() 00487 { 00488 init(); 00489 } 00490 00491 00492 KURLCompletion::KURLCompletion( Mode mode ) : KCompletion() 00493 { 00494 init(); 00495 setMode ( mode ); 00496 } 00497 00498 KURLCompletion::~KURLCompletion() 00499 { 00500 stop(); 00501 delete d; 00502 } 00503 00504 00505 void KURLCompletion::init() 00506 { 00507 d = new KURLCompletionPrivate; 00508 00509 d->cwd = QDir::homeDirPath(); 00510 00511 d->replace_home = true; 00512 d->replace_env = true; 00513 d->last_no_hidden = false; 00514 d->last_compl_type = 0; 00515 d->list_job = 0L; 00516 d->mode = KURLCompletion::FileCompletion; 00517 00518 // Read settings 00519 KConfig *c = KGlobal::config(); 00520 KConfigGroupSaver cgs( c, "URLCompletion" ); 00521 00522 d->url_auto_completion = c->readBoolEntry("alwaysAutoComplete", true); 00523 d->popup_append_slash = c->readBoolEntry("popupAppendSlash", true); 00524 d->onlyLocalProto = c->readBoolEntry("LocalProtocolsOnly", false); 00525 } 00526 00527 void KURLCompletion::setDir(const QString &dir) 00528 { 00529 d->cwd = dir; 00530 } 00531 00532 QString KURLCompletion::dir() const 00533 { 00534 return d->cwd; 00535 } 00536 00537 KURLCompletion::Mode KURLCompletion::mode() const 00538 { 00539 return d->mode; 00540 } 00541 00542 void KURLCompletion::setMode( Mode mode ) 00543 { 00544 d->mode = mode; 00545 } 00546 00547 bool KURLCompletion::replaceEnv() const 00548 { 00549 return d->replace_env; 00550 } 00551 00552 void KURLCompletion::setReplaceEnv( bool replace ) 00553 { 00554 d->replace_env = replace; 00555 } 00556 00557 bool KURLCompletion::replaceHome() const 00558 { 00559 return d->replace_home; 00560 } 00561 00562 void KURLCompletion::setReplaceHome( bool replace ) 00563 { 00564 d->replace_home = replace; 00565 } 00566 00567 /* 00568 * makeCompletion() 00569 * 00570 * Entry point for file name completion 00571 */ 00572 QString KURLCompletion::makeCompletion(const QString &text) 00573 { 00574 //kdDebug() << "KURLCompletion::makeCompletion: " << text << " d->cwd=" << d->cwd << endl; 00575 00576 MyURL url(text, d->cwd); 00577 00578 d->compl_text = text; 00579 00580 // Set d->prepend to the original URL, with the filename [and ref/query] stripped. 00581 // This is what gets prepended to the directory-listing matches. 00582 int toRemove = url.file().length() - url.kurl()->query().length(); 00583 if ( url.kurl()->hasRef() ) 00584 toRemove += url.kurl()->ref().length() + 1; 00585 d->prepend = text.left( text.length() - toRemove ); 00586 d->complete_url = url.isURL(); 00587 00588 QString match; 00589 00590 // Environment variables 00591 // 00592 if ( d->replace_env && envCompletion( url, &match ) ) 00593 return match; 00594 00595 // User directories 00596 // 00597 if ( d->replace_home && userCompletion( url, &match ) ) 00598 return match; 00599 00600 // Replace user directories and variables 00601 url.filter( d->replace_home, d->replace_env ); 00602 00603 //kdDebug() << "Filtered: proto=" << url.protocol() 00604 // << ", dir=" << url.dir() 00605 // << ", file=" << url.file() 00606 // << ", kurl url=" << *url.kurl() << endl; 00607 00608 if ( d->mode == ExeCompletion ) { 00609 // Executables 00610 // 00611 if ( exeCompletion( url, &match ) ) 00612 return match; 00613 00614 // KRun can run "man:" and "info:" etc. so why not treat them 00615 // as executables... 00616 00617 if ( urlCompletion( url, &match ) ) 00618 return match; 00619 } 00620 else { 00621 // Local files, directories 00622 // 00623 if ( fileCompletion( url, &match ) ) 00624 return match; 00625 00626 // All other... 00627 // 00628 if ( urlCompletion( url, &match ) ) 00629 return match; 00630 } 00631 00632 setListedURL( CTNone ); 00633 stop(); 00634 00635 return QString::null; 00636 } 00637 00638 /* 00639 * finished 00640 * 00641 * Go on and call KCompletion. 00642 * Called when all matches have been added 00643 */ 00644 QString KURLCompletion::finished() 00645 { 00646 if ( d->last_compl_type == CTInfo ) 00647 return KCompletion::makeCompletion( d->compl_text.lower() ); 00648 else 00649 return KCompletion::makeCompletion( d->compl_text ); 00650 } 00651 00652 /* 00653 * isRunning 00654 * 00655 * Return true if either a KIO job or the DirLister 00656 * is running 00657 */ 00658 bool KURLCompletion::isRunning() const 00659 { 00660 return d->list_job || (d->dirListThread && !d->dirListThread->finished()); 00661 } 00662 00663 /* 00664 * stop 00665 * 00666 * Stop and delete a running KIO job or the DirLister 00667 */ 00668 void KURLCompletion::stop() 00669 { 00670 if ( d->list_job ) { 00671 d->list_job->kill(); 00672 d->list_job = 0L; 00673 } 00674 00675 if ( !d->list_urls.isEmpty() ) { 00676 QValueList<KURL*>::Iterator it = d->list_urls.begin(); 00677 for ( ; it != d->list_urls.end(); it++ ) 00678 delete (*it); 00679 d->list_urls.clear(); 00680 } 00681 00682 if ( d->dirListThread ) { 00683 d->dirListThread->requestTermination(); 00684 d->dirListThread = 0; 00685 } 00686 } 00687 00688 /* 00689 * Keep track of the last listed directory 00690 */ 00691 void KURLCompletion::setListedURL( int complType, 00692 const QString& dir, 00693 const QString& filter, 00694 bool no_hidden ) 00695 { 00696 d->last_compl_type = complType; 00697 d->last_path_listed = dir; 00698 d->last_file_listed = filter; 00699 d->last_no_hidden = (int)no_hidden; 00700 d->last_prepend = d->prepend; 00701 } 00702 00703 bool KURLCompletion::isListedURL( int complType, 00704 const QString& dir, 00705 const QString& filter, 00706 bool no_hidden ) 00707 { 00708 return d->last_compl_type == complType 00709 && ( d->last_path_listed == dir 00710 || (dir.isEmpty() && d->last_path_listed.isEmpty()) ) 00711 && ( filter.startsWith(d->last_file_listed) 00712 || (filter.isEmpty() && d->last_file_listed.isEmpty()) ) 00713 && d->last_no_hidden == (int)no_hidden 00714 && d->last_prepend == d->prepend; // e.g. relative path vs absolute 00715 } 00716 00717 /* 00718 * isAutoCompletion 00719 * 00720 * Returns true if completion mode is Auto or Popup 00721 */ 00722 bool KURLCompletion::isAutoCompletion() 00723 { 00724 return completionMode() == KGlobalSettings::CompletionAuto 00725 || completionMode() == KGlobalSettings::CompletionPopup 00726 || completionMode() == KGlobalSettings::CompletionMan 00727 || completionMode() == KGlobalSettings::CompletionPopupAuto; 00728 } 00731 // User directories 00732 // 00733 00734 bool KURLCompletion::userCompletion(const MyURL &url, QString *match) 00735 { 00736 if ( url.protocol() != "file" 00737 || !url.dir().isEmpty() 00738 || url.file().at(0) != '~' ) 00739 return false; 00740 00741 if ( !isListedURL( CTUser ) ) { 00742 stop(); 00743 clear(); 00744 00745 if ( !d->userListThread ) { 00746 d->userListThread = new UserListThread( this ); 00747 d->userListThread->start(); 00748 00749 // If the thread finishes quickly make sure that the results 00750 // are added to the first matching case. 00751 00752 d->userListThread->wait( 200 ); 00753 QStringList l = d->userListThread->matches(); 00754 addMatches( l ); 00755 } 00756 } 00757 *match = finished(); 00758 return true; 00759 } 00760 00763 // Environment variables 00764 // 00765 00766 extern char **environ; // Array of environment variables 00767 00768 bool KURLCompletion::envCompletion(const MyURL &url, QString *match) 00769 { 00770 if ( url.file().at(0) != '$' ) 00771 return false; 00772 00773 if ( !isListedURL( CTEnv ) ) { 00774 stop(); 00775 clear(); 00776 00777 char **env = environ; 00778 00779 QString dollar = QString("$"); 00780 00781 QStringList l; 00782 00783 while ( *env ) { 00784 QString s = QString::fromLocal8Bit( *env ); 00785 00786 int pos = s.find('='); 00787 00788 if ( pos == -1 ) 00789 pos = s.length(); 00790 00791 if ( pos > 0 ) 00792 l.append( dollar + s.left(pos) ); 00793 00794 env++; 00795 } 00796 00797 addMatches( l ); 00798 } 00799 00800 setListedURL( CTEnv ); 00801 00802 *match = finished(); 00803 return true; 00804 } 00805 00808 // Executables 00809 // 00810 00811 bool KURLCompletion::exeCompletion(const MyURL &url, QString *match) 00812 { 00813 if ( url.protocol() != "file" ) 00814 return false; 00815 00816 QString dir = url.dir(); 00817 00818 dir = unescape( dir ); // remove escapes 00819 00820 // Find directories to search for completions, either 00821 // 00822 // 1. complete path given in url 00823 // 2. current directory (d->cwd) 00824 // 3. $PATH 00825 // 4. no directory at all 00826 00827 QStringList dirList; 00828 00829 if ( !QDir::isRelativePath(dir) ) { 00830 // complete path in url 00831 dirList.append( dir ); 00832 } 00833 else if ( !dir.isEmpty() && !d->cwd.isEmpty() ) { 00834 // current directory 00835 dirList.append( d->cwd + '/' + dir ); 00836 } 00837 else if ( !url.file().isEmpty() ) { 00838 // $PATH 00839 dirList = QStringList::split(KPATH_SEPARATOR, 00840 QString::fromLocal8Bit(::getenv("PATH"))); 00841 00842 QStringList::Iterator it = dirList.begin(); 00843 00844 for ( ; it != dirList.end(); it++ ) 00845 (*it).append('/'); 00846 } 00847 00848 // No hidden files unless the user types "." 00849 bool no_hidden_files = url.file().at(0) != '.'; 00850 00851 // List files if needed 00852 // 00853 if ( !isListedURL( CTExe, dir, url.file(), no_hidden_files ) ) 00854 { 00855 stop(); 00856 clear(); 00857 00858 setListedURL( CTExe, dir, url.file(), no_hidden_files ); 00859 00860 *match = listDirectories( dirList, url.file(), true, false, no_hidden_files ); 00861 } 00862 else if ( !isRunning() ) { 00863 *match = finished(); 00864 } 00865 else { 00866 if ( d->dirListThread ) 00867 setListedURL( CTExe, dir, url.file(), no_hidden_files ); 00868 *match = QString::null; 00869 } 00870 00871 return true; 00872 } 00873 00876 // Local files 00877 // 00878 00879 bool KURLCompletion::fileCompletion(const MyURL &url, QString *match) 00880 { 00881 if ( url.protocol() != "file" ) 00882 return false; 00883 00884 QString dir = url.dir(); 00885 00886 if (url.url()[0] == '.') 00887 { 00888 if (url.url().length() == 1) 00889 { 00890 *match = 00891 ( completionMode() == KGlobalSettings::CompletionMan )? "." : ".."; 00892 return true; 00893 } 00894 if (url.url().length() == 2 && url.url()[1]=='.') 00895 { 00896 *match=".."; 00897 return true; 00898 } 00899 } 00900 00901 //kdDebug() << "fileCompletion " << url.url() << " dir=" << dir << endl; 00902 00903 dir = unescape( dir ); // remove escapes 00904 00905 // Find directories to search for completions, either 00906 // 00907 // 1. complete path given in url 00908 // 2. current directory (d->cwd) 00909 // 3. no directory at all 00910 00911 QStringList dirList; 00912 00913 if ( !QDir::isRelativePath(dir) ) { 00914 // complete path in url 00915 dirList.append( dir ); 00916 } 00917 else if ( !d->cwd.isEmpty() ) { 00918 // current directory 00919 dirList.append( d->cwd + '/' + dir ); 00920 } 00921 00922 // No hidden files unless the user types "." 00923 bool no_hidden_files = ( url.file().at(0) != '.' ); 00924 00925 // List files if needed 00926 // 00927 if ( !isListedURL( CTFile, dir, "", no_hidden_files ) ) 00928 { 00929 stop(); 00930 clear(); 00931 00932 setListedURL( CTFile, dir, "", no_hidden_files ); 00933 00934 // Append '/' to directories in Popup mode? 00935 bool append_slash = ( d->popup_append_slash 00936 && (completionMode() == KGlobalSettings::CompletionPopup || 00937 completionMode() == KGlobalSettings::CompletionPopupAuto ) ); 00938 00939 bool only_dir = ( d->mode == DirCompletion ); 00940 00941 *match = listDirectories( dirList, "", false, only_dir, no_hidden_files, 00942 append_slash ); 00943 } 00944 else if ( !isRunning() ) { 00945 *match = finished(); 00946 } 00947 else { 00948 *match = QString::null; 00949 } 00950 00951 return true; 00952 } 00953 00956 // URLs not handled elsewhere... 00957 // 00958 00959 bool KURLCompletion::urlCompletion(const MyURL &url, QString *match) 00960 { 00961 //kdDebug() << "urlCompletion: url = " << *url.kurl() << endl; 00962 if (d->onlyLocalProto && KProtocolInfo::protocolClass(url.protocol()) != ":local") 00963 return false; 00964 00965 // Use d->cwd as base url in case url is not absolute 00966 KURL url_cwd = KURL::fromPathOrURL( d->cwd ); 00967 00968 // Create an URL with the directory to be listed 00969 KURL url_dir( url_cwd, url.kurl()->url() ); 00970 00971 // Don't try url completion if 00972 // 1. malformed url 00973 // 2. protocol that doesn't have listDir() 00974 // 3. there is no directory (e.g. "ftp://ftp.kd" shouldn't do anything) 00975 // 4. auto or popup completion mode depending on settings 00976 00977 bool man_or_info = ( url_dir.protocol() == QString("man") 00978 || url_dir.protocol() == QString("info") ); 00979 00980 if ( !url_dir.isValid() 00981 || !KProtocolInfo::supportsListing( url_dir ) 00982 || ( !man_or_info 00983 && ( url_dir.directory(false,false).isEmpty() 00984 || ( isAutoCompletion() 00985 && !d->url_auto_completion ) ) ) ) { 00986 return false; 00987 } 00988 00989 url_dir.setFileName(""); // not really nesseccary, but clear the filename anyway... 00990 00991 // Remove escapes 00992 QString dir = url_dir.directory( false, false ); 00993 00994 dir = unescape( dir ); 00995 00996 url_dir.setPath( dir ); 00997 00998 // List files if needed 00999 // 01000 if ( !isListedURL( CTUrl, url_dir.prettyURL(), url.file() ) ) 01001 { 01002 stop(); 01003 clear(); 01004 01005 setListedURL( CTUrl, url_dir.prettyURL(), "" ); 01006 01007 QValueList<KURL*> url_list; 01008 url_list.append( new KURL( url_dir ) ); 01009 01010 listURLs( url_list, "", false ); 01011 01012 *match = QString::null; 01013 } 01014 else if ( !isRunning() ) { 01015 *match = finished(); 01016 } 01017 else { 01018 *match = QString::null; 01019 } 01020 01021 return true; 01022 } 01023 01026 // Directory and URL listing 01027 // 01028 01029 /* 01030 * addMatches 01031 * 01032 * Called to add matches to KCompletion 01033 */ 01034 void KURLCompletion::addMatches( const QStringList &matches ) 01035 { 01036 QStringList::ConstIterator it = matches.begin(); 01037 QStringList::ConstIterator end = matches.end(); 01038 01039 if ( d->complete_url ) 01040 for ( ; it != end; it++ ) 01041 addItem( d->prepend + KURL::encode_string(*it)); 01042 else 01043 for ( ; it != end; it++ ) 01044 addItem( d->prepend + (*it)); 01045 } 01046 01047 /* 01048 * listDirectories 01049 * 01050 * List files starting with 'filter' in the given directories, 01051 * either using DirLister or listURLs() 01052 * 01053 * In either case, addMatches() is called with the listed 01054 * files, and eventually finished() when the listing is done 01055 * 01056 * Returns the match if available, or QString::null if 01057 * DirLister timed out or using kio 01058 */ 01059 QString KURLCompletion::listDirectories( 01060 const QStringList &dirList, 01061 const QString &filter, 01062 bool only_exe, 01063 bool only_dir, 01064 bool no_hidden, 01065 bool append_slash_to_dir) 01066 { 01067 assert( !isRunning() ); 01068 01069 if ( !::getenv("KURLCOMPLETION_LOCAL_KIO") ) { 01070 01071 //kdDebug() << "Listing (listDirectories): " << dirList << " filter=" << filter << " without KIO" << endl; 01072 01073 // Don't use KIO 01074 01075 if ( d->dirListThread ) 01076 d->dirListThread->requestTermination(); 01077 01078 QStringList dirs; 01079 01080 for ( QStringList::ConstIterator it = dirList.begin(); 01081 it != dirList.end(); 01082 ++it ) 01083 { 01084 KURL url; 01085 url.setPath(*it); 01086 if ( kapp->authorizeURLAction( "list", KURL(), url ) ) 01087 dirs.append( *it ); 01088 } 01089 01090 d->dirListThread = new DirectoryListThread( this, dirs, filter, only_exe, only_dir, 01091 no_hidden, append_slash_to_dir ); 01092 d->dirListThread->start(); 01093 d->dirListThread->wait( 200 ); 01094 addMatches( d->dirListThread->matches() ); 01095 01096 return finished(); 01097 } 01098 else { 01099 01100 // Use KIO 01101 //kdDebug() << "Listing (listDirectories): " << dirList << " with KIO" << endl; 01102 01103 QValueList<KURL*> url_list; 01104 01105 QStringList::ConstIterator it = dirList.begin(); 01106 01107 for ( ; it != dirList.end(); it++ ) 01108 url_list.append( new KURL(*it) ); 01109 01110 listURLs( url_list, filter, only_exe, no_hidden ); 01111 // Will call addMatches() and finished() 01112 01113 return QString::null; 01114 } 01115 } 01116 01117 /* 01118 * listURLs 01119 * 01120 * Use KIO to list the given urls 01121 * 01122 * addMatches() is called with the listed files 01123 * finished() is called when the listing is done 01124 */ 01125 void KURLCompletion::listURLs( 01126 const QValueList<KURL *> &urls, 01127 const QString &filter, 01128 bool only_exe, 01129 bool no_hidden ) 01130 { 01131 assert( d->list_urls.isEmpty() ); 01132 assert( d->list_job == 0L ); 01133 01134 d->list_urls = urls; 01135 d->list_urls_filter = filter; 01136 d->list_urls_only_exe = only_exe; 01137 d->list_urls_no_hidden = no_hidden; 01138 01139 // kdDebug() << "Listing URLs: " << urls[0]->prettyURL() << ",..." << endl; 01140 01141 // Start it off by calling slotIOFinished 01142 // 01143 // This will start a new list job as long as there 01144 // are urls in d->list_urls 01145 // 01146 slotIOFinished(0L); 01147 } 01148 01149 /* 01150 * slotEntries 01151 * 01152 * Receive files listed by KIO and call addMatches() 01153 */ 01154 void KURLCompletion::slotEntries(KIO::Job*, const KIO::UDSEntryList& entries) 01155 { 01156 QStringList matches; 01157 01158 KIO::UDSEntryListConstIterator it = entries.begin(); 01159 KIO::UDSEntryListConstIterator end = entries.end(); 01160 01161 QString filter = d->list_urls_filter; 01162 01163 int filter_len = filter.length(); 01164 01165 // Iterate over all files 01166 // 01167 for (; it != end; ++it) { 01168 QString name; 01169 QString url; 01170 bool is_exe = false; 01171 bool is_dir = false; 01172 01173 KIO::UDSEntry e = *it; 01174 KIO::UDSEntry::ConstIterator it_2 = e.begin(); 01175 01176 for( ; it_2 != e.end(); it_2++ ) { 01177 switch ( (*it_2).m_uds ) { 01178 case KIO::UDS_NAME: 01179 name = (*it_2).m_str; 01180 break; 01181 case KIO::UDS_ACCESS: 01182 is_exe = ((*it_2).m_long & MODE_EXE) != 0; 01183 break; 01184 case KIO::UDS_FILE_TYPE: 01185 is_dir = ((*it_2).m_long & S_IFDIR) != 0; 01186 break; 01187 case KIO::UDS_URL: 01188 url = (*it_2).m_str; 01189 break; 01190 } 01191 } 01192 01193 if (!url.isEmpty()) { 01194 // kdDebug() << "KURLCompletion::slotEntries url: " << url << endl; 01195 name = KURL(url).fileName(); 01196 } 01197 01198 // kdDebug() << "KURLCompletion::slotEntries name: " << name << endl; 01199 01200 if ( name[0] == '.' && 01201 ( d->list_urls_no_hidden || 01202 name.length() == 1 || 01203 ( name.length() == 2 && name[1] == '.' ) ) ) 01204 continue; 01205 01206 if ( d->mode == DirCompletion && !is_dir ) 01207 continue; 01208 01209 if ( filter_len == 0 || name.left(filter_len) == filter ) { 01210 if ( is_dir ) 01211 name.append( '/' ); 01212 01213 if ( is_exe || !d->list_urls_only_exe ) 01214 matches.append( name ); 01215 } 01216 } 01217 01218 addMatches( matches ); 01219 } 01220 01221 /* 01222 * slotIOFinished 01223 * 01224 * Called when a KIO job is finished. 01225 * 01226 * Start a new list job if there are still urls in 01227 * d->list_urls, otherwise call finished() 01228 */ 01229 void KURLCompletion::slotIOFinished( KIO::Job * job ) 01230 { 01231 // kdDebug() << "slotIOFinished() " << endl; 01232 01233 assert( job == d->list_job ); 01234 01235 if ( d->list_urls.isEmpty() ) { 01236 01237 d->list_job = 0L; 01238 01239 finished(); // will call KCompletion::makeCompletion() 01240 01241 } 01242 else { 01243 01244 KURL *kurl = d->list_urls.first(); 01245 01246 d->list_urls.remove( kurl ); 01247 01248 // kdDebug() << "Start KIO: " << kurl->prettyURL() << endl; 01249 01250 d->list_job = KIO::listDir( *kurl, false ); 01251 d->list_job->addMetaData("no-auth-prompt", "true"); 01252 01253 assert( d->list_job ); 01254 01255 connect( d->list_job, 01256 SIGNAL(result(KIO::Job*)), 01257 SLOT(slotIOFinished(KIO::Job*)) ); 01258 01259 connect( d->list_job, 01260 SIGNAL( entries( KIO::Job*, const KIO::UDSEntryList&)), 01261 SLOT( slotEntries( KIO::Job*, const KIO::UDSEntryList&)) ); 01262 01263 delete kurl; 01264 } 01265 } 01266 01269 01270 /* 01271 * postProcessMatch, postProcessMatches 01272 * 01273 * Called by KCompletion before emitting match() and matches() 01274 * 01275 * Append '/' to directories for file completion. This is 01276 * done here to avoid stat()'ing a lot of files 01277 */ 01278 void KURLCompletion::postProcessMatch( QString *match ) const 01279 { 01280 // kdDebug() << "KURLCompletion::postProcess: " << *match << endl; 01281 01282 if ( !match->isEmpty() ) { 01283 01284 // Add '/' to directories in file completion mode 01285 // unless it has already been done 01286 if ( d->last_compl_type == CTFile ) 01287 adjustMatch( *match ); 01288 } 01289 } 01290 01291 void KURLCompletion::adjustMatch( QString& match ) const 01292 { 01293 if ( match.at( match.length()-1 ) != '/' ) 01294 { 01295 QString copy; 01296 01297 if ( match.startsWith( QString("file:") ) ) 01298 copy = KURL(match).path(); 01299 else 01300 copy = match; 01301 01302 expandTilde( copy ); 01303 expandEnv( copy ); 01304 if ( QDir::isRelativePath(copy) ) 01305 copy.prepend( d->cwd + '/' ); 01306 01307 // kdDebug() << "postProcess: stating " << copy << endl; 01308 01309 KDE_struct_stat sbuff; 01310 01311 QCString file = QFile::encodeName( copy ); 01312 01313 if ( KDE_stat( (const char*)file, &sbuff ) == 0 ) { 01314 if ( S_ISDIR ( sbuff.st_mode ) ) 01315 match.append( '/' ); 01316 } 01317 else { 01318 kdDebug() << "Could not stat file " << copy << endl; 01319 } 01320 } 01321 } 01322 01323 void KURLCompletion::postProcessMatches( QStringList * matches ) const 01324 { 01325 if ( !matches->isEmpty() && d->last_compl_type == CTFile ) { 01326 QStringList::Iterator it = matches->begin(); 01327 for (; it != matches->end(); ++it ) { 01328 adjustMatch( (*it) ); 01329 } 01330 } 01331 } 01332 01333 void KURLCompletion::postProcessMatches( KCompletionMatches * matches ) const 01334 { 01335 if ( !matches->isEmpty() && d->last_compl_type == CTFile ) { 01336 KCompletionMatches::Iterator it = matches->begin(); 01337 for (; it != matches->end(); ++it ) { 01338 adjustMatch( (*it).value() ); 01339 } 01340 } 01341 } 01342 01343 void KURLCompletion::customEvent(QCustomEvent *e) 01344 { 01345 if ( e->type() == CompletionMatchEvent::uniqueType() ) { 01346 01347 CompletionMatchEvent *event = static_cast<CompletionMatchEvent *>( e ); 01348 01349 event->completionThread()->wait(); 01350 01351 if ( !isListedURL( CTUser ) ) { 01352 stop(); 01353 clear(); 01354 addMatches( event->completionThread()->matches() ); 01355 } 01356 01357 setListedURL( CTUser ); 01358 01359 if ( d->userListThread == event->completionThread() ) 01360 d->userListThread = 0; 01361 01362 if ( d->dirListThread == event->completionThread() ) 01363 d->dirListThread = 0; 01364 01365 delete event->completionThread(); 01366 } 01367 } 01368 01369 // static 01370 QString KURLCompletion::replacedPath( const QString& text, bool replaceHome, bool replaceEnv ) 01371 { 01372 if ( text.isEmpty() ) 01373 return text; 01374 01375 MyURL url( text, QString::null ); // no need to replace something of our current cwd 01376 if ( !url.kurl()->isLocalFile() ) 01377 return text; 01378 01379 url.filter( replaceHome, replaceEnv ); 01380 return url.dir() + url.file(); 01381 } 01382 01383 01384 QString KURLCompletion::replacedPath( const QString& text ) 01385 { 01386 return replacedPath( text, d->replace_home, d->replace_env ); 01387 } 01388 01391 // Static functions 01392 01393 /* 01394 * expandEnv 01395 * 01396 * Expand environment variables in text. Escaped '$' are ignored. 01397 * Return true if expansion was made. 01398 */ 01399 static bool expandEnv( QString &text ) 01400 { 01401 // Find all environment variables beginning with '$' 01402 // 01403 int pos = 0; 01404 01405 bool expanded = false; 01406 01407 while ( (pos = text.find('$', pos)) != -1 ) { 01408 01409 // Skip escaped '$' 01410 // 01411 if ( text[pos-1] == '\\' ) { 01412 pos++; 01413 } 01414 // Variable found => expand 01415 // 01416 else { 01417 // Find the end of the variable = next '/' or ' ' 01418 // 01419 int pos2 = text.find( ' ', pos+1 ); 01420 int pos_tmp = text.find( '/', pos+1 ); 01421 01422 if ( pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2) ) 01423 pos2 = pos_tmp; 01424 01425 if ( pos2 == -1 ) 01426 pos2 = text.length(); 01427 01428 // Replace if the variable is terminated by '/' or ' ' 01429 // and defined 01430 // 01431 if ( pos2 >= 0 ) { 01432 int len = pos2 - pos; 01433 QString key = text.mid( pos+1, len-1); 01434 QString value = 01435 QString::fromLocal8Bit( ::getenv(key.local8Bit()) ); 01436 01437 if ( !value.isEmpty() ) { 01438 expanded = true; 01439 text.replace( pos, len, value ); 01440 pos = pos + value.length(); 01441 } 01442 else { 01443 pos = pos2; 01444 } 01445 } 01446 } 01447 } 01448 01449 return expanded; 01450 } 01451 01452 /* 01453 * expandTilde 01454 * 01455 * Replace "~user" with the users home directory 01456 * Return true if expansion was made. 01457 */ 01458 static bool expandTilde(QString &text) 01459 { 01460 if ( text[0] != '~' ) 01461 return false; 01462 01463 bool expanded = false; 01464 01465 // Find the end of the user name = next '/' or ' ' 01466 // 01467 int pos2 = text.find( ' ', 1 ); 01468 int pos_tmp = text.find( '/', 1 ); 01469 01470 if ( pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2) ) 01471 pos2 = pos_tmp; 01472 01473 if ( pos2 == -1 ) 01474 pos2 = text.length(); 01475 01476 // Replace ~user if the user name is terminated by '/' or ' ' 01477 // 01478 if ( pos2 >= 0 ) { 01479 01480 QString user = text.mid( 1, pos2-1 ); 01481 QString dir; 01482 01483 // A single ~ is replaced with $HOME 01484 // 01485 if ( user.isEmpty() ) { 01486 dir = QDir::homeDirPath(); 01487 } 01488 // ~user is replaced with the dir from passwd 01489 // 01490 else { 01491 struct passwd *pw = ::getpwnam( user.local8Bit() ); 01492 01493 if ( pw ) 01494 dir = QFile::decodeName( pw->pw_dir ); 01495 01496 ::endpwent(); 01497 } 01498 01499 if ( !dir.isEmpty() ) { 01500 expanded = true; 01501 text.replace(0, pos2, dir); 01502 } 01503 } 01504 01505 return expanded; 01506 } 01507 01508 /* 01509 * unescape 01510 * 01511 * Remove escapes and return the result in a new string 01512 * 01513 */ 01514 static QString unescape(const QString &text) 01515 { 01516 QString result; 01517 01518 for (uint pos = 0; pos < text.length(); pos++) 01519 if ( text[pos] != '\\' ) 01520 result.insert( result.length(), text[pos] ); 01521 01522 return result; 01523 } 01524 01525 void KURLCompletion::virtual_hook( int id, void* data ) 01526 { KCompletion::virtual_hook( id, data ); } 01527 01528 #include "kurlcompletion.moc" 01529