/* This file is part of the KDE libraries Copyright (c) 1999 Preston Brown Copyright (c) 1997-1999 Matthias Kalle Dalheimer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #ifdef HAVE_SYS_MMAN_H #include #endif #include #ifdef HAVE_SYS_STAT_H #include #endif #include #include #include #include #include #include #include #include "kconfigbackend.h" #include "kconfigbase.h" #include #include #include #include #include #include #include #include extern bool checkAccess(const QString& pathname, int mode); /* translate escaped escape sequences to their actual values. */ static QCString printableToString(const char *str, int l) { // Strip leading white-space. while((l>0) && ((*str == ' ') || (*str == '\t') || (*str == '\r'))) { str++; l--; } // Strip trailing white-space. while((l>0) && ((str[l-1] == ' ') || (str[l-1] == '\t') || (str[l-1] == '\r'))) { l--; } QCString result(l + 1); char *r = result.data(); for(int i = 0; i < l;i++, str++) { if (*str == '\\') { i++, str++; if (i >= l) // End of line. (Line ends with single slash) { *r++ = '\\'; break; } switch(*str) { case 's': *r++ = ' '; break; case 't': *r++ = '\t'; break; case 'n': *r++ = '\n'; break; case 'r': *r++ = '\r'; break; case '\\': *r++ = '\\'; break; default: *r++ = '\\'; *r++ = *str; } } else { *r++ = *str; } } result.truncate(r-result.data()); return result; } static QCString stringToPrintable(const QCString& str){ QCString result(str.length()*2); // Maximum 2x as long as source string register char *r = result.data(); register char *s = str.data(); if (!s) return QCString(""); // Escape leading space if (*s == ' ') { *r++ = '\\'; *r++ = 's'; s++; } if (*s) { while(*s) { if (*s == '\n') { *r++ = '\\'; *r++ = 'n'; } else if (*s == '\t') { *r++ = '\\'; *r++ = 't'; } else if (*s == '\r') { *r++ = '\\'; *r++ = 'r'; } else if (*s == '\\') { *r++ = '\\'; *r++ = '\\'; } else { *r++ = *s; } s++; } // Escape trailing space if (*(r-1) == ' ') { *(r-1) = '\\'; *r++ = 's'; } } result.truncate(r - result.data()); return result; } static QCString decodeGroup(const char*s, int l) { QCString result(l); register char *r = result.data(); l--; // Correct for trailing \0 while(l) { if ((*s == '[') && (l > 1)) { if ((*(s+1) == '[')) { l--; s++; } } if ((*s == ']') && (l > 1)) { if ((*(s+1) == ']')) { l--; s++; } } *r++ = *s++; l--; } result.truncate(r - result.data()); return result; } static QCString encodeGroup(const QCString &str) { int l = str.length(); QCString result(l*2+1); register char *r = result.data(); register char *s = str.data(); while(l) { if ((*s == '[') || (*s == ']')) *r++ = *s; *r++ = *s++; l--; } result.truncate(r - result.data()); return result; } static QCString encodeKey(const char* key) { QCString newKey(key); newKey.replace('[', "%5b"); newKey.replace(']', "%5d"); return newKey; } static QCString decodeKey(const char* key) { QCString newKey(key); newKey.replace("%5b", "["); newKey.replace("%5d", "]"); return newKey; } class KConfigBackEnd::KConfigBackEndPrivate { public: QDateTime localLastModified; uint localLastSize; KLockFile::Ptr localLockFile; KLockFile::Ptr globalLockFile; }; void KConfigBackEnd::changeFileName(const QString &_fileName, const char * _resType, bool _useKDEGlobals) { mfileName = _fileName; resType = _resType; useKDEGlobals = _useKDEGlobals; if (mfileName.isEmpty()) mLocalFileName = QString::null; else if (!QDir::isRelativePath(mfileName)) mLocalFileName = mfileName; else mLocalFileName = KGlobal::dirs()->saveLocation(resType) + mfileName; if (useKDEGlobals) mGlobalFileName = KGlobal::dirs()->saveLocation("config") + QString::fromLatin1("kdeglobals"); else mGlobalFileName = QString::null; d->localLastModified = QDateTime(); d->localLastSize = 0; d->localLockFile = 0; d->globalLockFile = 0; } KLockFile::Ptr KConfigBackEnd::lockFile(bool bGlobal) { if (bGlobal) { if (d->globalLockFile) return d->globalLockFile; if (!mGlobalFileName.isEmpty()) { d->globalLockFile = new KLockFile(mGlobalFileName+".lock"); return d->globalLockFile; } } else { if (d->localLockFile) return d->localLockFile; if (!mLocalFileName.isEmpty()) { d->localLockFile = new KLockFile(mLocalFileName+".lock"); return d->localLockFile; } } return 0; } KConfigBackEnd::KConfigBackEnd(KConfigBase *_config, const QString &_fileName, const char * _resType, bool _useKDEGlobals) : pConfig(_config), bFileImmutable(false), mConfigState(KConfigBase::NoAccess), mFileMode(-1) { d = new KConfigBackEndPrivate; changeFileName(_fileName, _resType, _useKDEGlobals); } KConfigBackEnd::~KConfigBackEnd() { delete d; } void KConfigBackEnd::setFileWriteMode(int mode) { mFileMode = mode; } bool KConfigINIBackEnd::parseConfigFiles() { // Check if we can write to the local file. mConfigState = KConfigBase::ReadOnly; if (!mLocalFileName.isEmpty() && !pConfig->isReadOnly()) { if (checkAccess(mLocalFileName, W_OK)) { mConfigState = KConfigBase::ReadWrite; } else { // Create the containing dir, maybe it wasn't there KURL path; path.setPath(mLocalFileName); QString dir=path.directory(); KStandardDirs::makeDir(dir); if (checkAccess(mLocalFileName, W_OK)) { mConfigState = KConfigBase::ReadWrite; } } QFileInfo info(mLocalFileName); d->localLastModified = info.lastModified(); d->localLastSize = info.size(); } // Parse all desired files from the least to the most specific. bFileImmutable = false; // Parse the general config files if (useKDEGlobals) { QStringList kdercs = KGlobal::dirs()-> findAllResources("config", QString::fromLatin1("kdeglobals")); #ifdef Q_WS_WIN QString etc_kderc = QFile::decodeName( QCString(getenv("WINDIR")) + "\\kderc" ); #else QString etc_kderc = QString::fromLatin1("/etc/kderc"); #endif if (checkAccess(etc_kderc, R_OK)) kdercs += etc_kderc; kdercs += KGlobal::dirs()-> findAllResources("config", QString::fromLatin1("system.kdeglobals")); QStringList::ConstIterator it; for (it = kdercs.fromLast(); it != kdercs.end(); --it) { QFile aConfigFile( *it ); if (!aConfigFile.open( IO_ReadOnly )) continue; parseSingleConfigFile( aConfigFile, 0L, true, (*it != mGlobalFileName) ); aConfigFile.close(); if (bFileImmutable) break; } } bool bReadFile = !mfileName.isEmpty(); while(bReadFile) { bReadFile = false; QString bootLanguage; if (useKDEGlobals && localeString.isEmpty() && !KGlobal::_locale) { // Boot strap language bootLanguage = KLocale::_initLanguage(pConfig); setLocaleString(bootLanguage.utf8()); } bFileImmutable = false; QStringList list; if ( !QDir::isRelativePath(mfileName) ) list << mfileName; else list = KGlobal::dirs()->findAllResources(resType, mfileName); QStringList::ConstIterator it; for (it = list.fromLast(); it != list.end(); --it) { QFile aConfigFile( *it ); // we can already be sure that this file exists bool bIsLocal = (*it == mLocalFileName); if (aConfigFile.open( IO_ReadOnly )) { parseSingleConfigFile( aConfigFile, 0L, false, !bIsLocal ); aConfigFile.close(); if (bFileImmutable) break; } } if (KGlobal::dirs()->isRestrictedResource(resType, mfileName)) bFileImmutable = true; QString currentLanguage; if (!bootLanguage.isEmpty()) { currentLanguage = KLocale::_initLanguage(pConfig); // If the file changed the language, we need to read the file again // with the new language setting. if (bootLanguage != currentLanguage) { bReadFile = true; setLocaleString(currentLanguage.utf8()); } } } if (bFileImmutable) mConfigState = KConfigBase::ReadOnly; return true; } #ifdef HAVE_MMAP #ifdef SIGBUS static sigjmp_buf mmap_jmpbuf; struct sigaction mmap_old_sigact; extern "C" { static void mmap_sigbus_handler(int) { siglongjmp (mmap_jmpbuf, 1); } } #endif #endif extern bool kde_kiosk_exception; void KConfigINIBackEnd::parseSingleConfigFile(QFile &rFile, KEntryMap *pWriteBackMap, bool bGlobal, bool bDefault) { const char *s; // May get clobbered by sigsetjump, but we don't use them afterwards. const char *eof; // May get clobbered by sigsetjump, but we don't use them afterwards. QByteArray data; if (!rFile.isOpen()) // come back, if you have real work for us ;-> return; //using kdDebug() here leads to an infinite loop //remove this for the release, aleXXX //qWarning("Parsing %s, global = %s default = %s", // rFile.name().latin1(), bGlobal ? "true" : "false", bDefault ? "true" : "false"); QCString aCurrentGroup(""); unsigned int ll = localeString.length(); #ifdef HAVE_MMAP static volatile const char *map; map = ( const char* ) mmap(0, rFile.size(), PROT_READ, MAP_PRIVATE, rFile.handle(), 0); if ( map != MAP_FAILED ) { s = (const char*) map; eof = s + rFile.size(); #ifdef SIGBUS struct sigaction act; act.sa_handler = mmap_sigbus_handler; sigemptyset( &act.sa_mask ); #ifdef SA_ONESHOT act.sa_flags = SA_ONESHOT; #else act.sa_flags = SA_RESETHAND; #endif sigaction( SIGBUS, &act, &mmap_old_sigact ); if (sigsetjmp (mmap_jmpbuf, 1)) { qWarning("SIGBUS while reading %s", rFile.name().latin1()); munmap(( char* )map, rFile.size()); sigaction (SIGBUS, &mmap_old_sigact, 0); return; } #endif } else #endif { rFile.at(0); data = rFile.readAll(); s = data.data(); eof = s + data.size(); } bool fileOptionImmutable = false; bool groupOptionImmutable = false; bool groupSkip = false; int line = 0; for(; s < eof; s++) { line++; while((s < eof) && isspace(*s) && (*s != '\n')) s++; //skip leading whitespace, shouldn't happen too often //skip empty lines, lines starting with # if ((s < eof) && ((*s == '\n') || (*s == '#'))) { sktoeol: //skip till end-of-line while ((s < eof) && (*s != '\n')) s++; continue; // Empty or comment or no keyword } const char *startLine = s; if (*s == '[') //group { // In a group [[ and ]] have a special meaning while ((s < eof) && (*s != '\n')) { if (*s == ']') { if ((s+1 < eof) && (*(s+1) == ']')) s++; // Skip "]]" else break; } s++; // Search till end of group } const char *e = s; while ((s < eof) && (*s != '\n')) s++; // Search till end of line / end of file if ((e >= eof) || (*e != ']')) { fprintf(stderr, "Invalid group header at %s:%d\n", rFile.name().latin1(), line); continue; } // group found; get the group name by taking everything in // between the brackets if ((e-startLine == 3) && (startLine[1] == '$') && (startLine[2] == 'i')) { if (!kde_kiosk_exception) fileOptionImmutable = true; continue; } aCurrentGroup = decodeGroup(startLine + 1, e - startLine); //cout<<"found group ["<lookupData(groupKey); groupSkip = entry.bImmutable; if (groupSkip && !bDefault) continue; entry.bImmutable |= groupOptionImmutable; pConfig->putData(groupKey, entry, false); if (pWriteBackMap) { // add the special group key indicator (*pWriteBackMap)[groupKey] = entry; } continue; } if (groupSkip && !bDefault) goto sktoeol; // Skip entry bool optionImmutable = groupOptionImmutable; bool optionDeleted = false; bool optionExpand = false; const char *endOfKey = 0, *locale = 0, *elocale = 0; for (; (s < eof) && (*s != '\n'); s++) { if (*s == '=') //find the equal sign { if (!endOfKey) endOfKey = s; goto haveeq; } if (*s == '[') //find the locale or options. { const char *option; const char *eoption; endOfKey = s; option = ++s; for (;; s++) { if ((s >= eof) || (*s == '\n') || (*s == '=')) { fprintf(stderr, "Invalid entry (missing ']') at %s:%d\n", rFile.name().latin1(), line); goto sktoeol; } if (*s == ']') break; } eoption = s; if (*option != '$') { // Locale if (locale) { fprintf(stderr, "Invalid entry (second locale!?) at %s:%d\n", rFile.name().latin1(), line); goto sktoeol; } locale = option; elocale = eoption; } else { // Option while (option < eoption) { option++; if ((*option == 'i') && !kde_kiosk_exception) optionImmutable = true; else if (*option == 'e') optionExpand = true; else if (*option == 'd') { optionDeleted = true; goto haveeq; } else if (*option == ']') break; } } } } fprintf(stderr, "Invalid entry (missing '=') at %s:%d\n", rFile.name().latin1(), line); continue; haveeq: for (endOfKey--; ; endOfKey--) { if (endOfKey < startLine) { fprintf(stderr, "Invalid entry (empty key) at %s:%d\n", rFile.name().latin1(), line); goto sktoeol; } if (!isspace(*endOfKey)) break; } const char *st = ++s; while ((s < eof) && (*s != '\n')) s++; // Search till end of line / end of file if (locale) { unsigned int cl = static_cast(elocale - locale); if ((ll != cl) || memcmp(locale, localeString.data(), ll)) { // backward compatibility. C == en_US if ( cl != 1 || ll != 5 || *locale != 'C' || memcmp(localeString.data(), "en_US", 5)) { //cout<<"mismatched locale '"<insert(aEntryKey, aEntry); } else { // directly insert value into config object // no need to specify localization; if the key we just // retrieved was localized already, no need to localize it again. pConfig->putData(aEntryKey, aEntry, false); } } if (fileOptionImmutable) bFileImmutable = true; #ifdef HAVE_MMAP if (map) { munmap(( char* )map, rFile.size()); #ifdef SIGBUS sigaction (SIGBUS, &mmap_old_sigact, 0); #endif } #endif } void KConfigINIBackEnd::sync(bool bMerge) { // write-sync is only necessary if there are dirty entries if (!pConfig->isDirty()) return; bool bEntriesLeft = true; // find out the file to write to (most specific writable file) // try local app-specific file first if (!mfileName.isEmpty()) { // Create the containing dir if needed if ((resType!="config") && !QDir::isRelativePath(mLocalFileName)) { KURL path; path.setPath(mLocalFileName); QString dir=path.directory(); KStandardDirs::makeDir(dir); } // Can we allow the write? We can, if the program // doesn't run SUID. But if it runs SUID, we must // check if the user would be allowed to write if // it wasn't SUID. if (checkAccess(mLocalFileName, W_OK)) { // File is writable KLockFile::Ptr lf; bool mergeLocalFile = bMerge; // Check if the file has been updated since. if (mergeLocalFile) { lf = lockFile(false); // Lock file for local file if (lf && lf->isLocked()) lf = 0; // Already locked, we don't need to lock/unlock again if (lf) { lf->lock( KLockFile::LockForce ); // But what if the locking failed? Ignore it for now... } QFileInfo info(mLocalFileName); if ((d->localLastSize == info.size()) && (d->localLastModified == info.lastModified())) { // Not changed, don't merge. mergeLocalFile = false; } else { // Changed... d->localLastModified = QDateTime(); d->localLastSize = 0; } } bEntriesLeft = writeConfigFile( mLocalFileName, false, mergeLocalFile ); // Only if we didn't have to merge anything can we use our in-memory state // the next time around. Otherwise the config-file may contain entries // that are different from our in-memory state which means we will have to // do a merge from then on. // We do not automatically update the in-memory state with the on-disk // state when writing the config to disk. We only do so when // KCOnfig::reparseConfiguration() is called. // For KDE 4.0 we may wish to reconsider that. if (!mergeLocalFile) { QFileInfo info(mLocalFileName); d->localLastModified = info.lastModified(); d->localLastSize = info.size(); } if (lf) lf->unlock(); } } // only write out entries to the kdeglobals file if there are any // entries marked global (indicated by bEntriesLeft) and // the useKDEGlobals flag is set. if (bEntriesLeft && useKDEGlobals) { // can we allow the write? (see above) if (checkAccess ( mGlobalFileName, W_OK )) { KLockFile::Ptr lf = lockFile(true); // Lock file for global file if (lf && lf->isLocked()) lf = 0; // Already locked, we don't need to lock/unlock again if (lf) { lf->lock( KLockFile::LockForce ); // But what if the locking failed? Ignore it for now... } writeConfigFile( mGlobalFileName, true, bMerge ); // Always merge if (lf) lf->unlock(); } } } static void writeEntries(FILE *pStream, const KEntryMap& entryMap, bool defaultGroup, bool &firstEntry, const QCString &localeString) { // now write out all other groups. QCString currentGroup; for (KEntryMapConstIterator aIt = entryMap.begin(); aIt != entryMap.end(); ++aIt) { const KEntryKey &key = aIt.key(); // Either proces the default group or all others if ((key.mGroup != "") == defaultGroup) continue; // Skip // Skip default values and group headers. if ((key.bDefault) || key.mKey.isEmpty()) continue; // Skip const KEntry ¤tEntry = *aIt; KEntryMapConstIterator aTestIt = aIt; ++aTestIt; bool hasDefault = (aTestIt != entryMap.end()); if (hasDefault) { const KEntryKey &defaultKey = aTestIt.key(); if ((!defaultKey.bDefault) || (defaultKey.mKey != key.mKey) || (defaultKey.mGroup != key.mGroup) || (defaultKey.bLocal != key.bLocal)) hasDefault = false; } if (hasDefault) { // Entry had a default value if ((currentEntry.mValue == (*aTestIt).mValue) && (currentEntry.bDeleted == (*aTestIt).bDeleted)) continue; // Same as default, don't write. } else { // Entry had no default value. if (currentEntry.bDeleted) continue; // Don't write deleted entries if there is no default. } if (!defaultGroup && (currentGroup != key.mGroup)) { if (!firstEntry) fprintf(pStream, "\n"); currentGroup = key.mGroup; fprintf(pStream, "[%s]\n", encodeGroup(currentGroup).data()); } firstEntry = false; // it is data for a group fputs(encodeKey(key.mKey.data()), pStream); // Key if ( currentEntry.bNLS ) { fputc('[', pStream); fputs(localeString.data(), pStream); fputc(']', pStream); } if (currentEntry.bDeleted) { fputs("[$d]\n", pStream); // Deleted } else { if (currentEntry.bImmutable || currentEntry.bExpand) { fputc('[', pStream); fputc('$', pStream); if (currentEntry.bImmutable) fputc('i', pStream); if (currentEntry.bExpand) fputc('e', pStream); fputc(']', pStream); } fputc('=', pStream); fputs(stringToPrintable(currentEntry.mValue).data(), pStream); fputc('\n', pStream); } } // for loop } bool KConfigINIBackEnd::getEntryMap(KEntryMap &aTempMap, bool bGlobal, QFile *mergeFile) { bool bEntriesLeft = false; bFileImmutable = false; // Read entries from disk if (mergeFile && mergeFile->open(IO_ReadOnly)) { // fill the temporary structure with entries from the file parseSingleConfigFile(*mergeFile, &aTempMap, bGlobal, false ); if (bFileImmutable) // File has become immutable on disk return bEntriesLeft; } KEntryMap aMap = pConfig->internalEntryMap(); // augment this structure with the dirty entries from the config object for (KEntryMapIterator aIt = aMap.begin(); aIt != aMap.end(); ++aIt) { const KEntry ¤tEntry = *aIt; if(aIt.key().bDefault) { aTempMap.replace(aIt.key(), currentEntry); continue; } if (mergeFile && !currentEntry.bDirty) continue; // only write back entries that have the same // "globality" as the file if (currentEntry.bGlobal != bGlobal) { // wrong "globality" - might have to be saved later bEntriesLeft = true; continue; } // put this entry from the config object into the // temporary map, possibly replacing an existing entry KEntryMapIterator aIt2 = aTempMap.find(aIt.key()); if (aIt2 != aTempMap.end() && (*aIt2).bImmutable) continue; // Bail out if the on-disk entry is immutable aTempMap.insert(aIt.key(), currentEntry, true); } // loop return bEntriesLeft; } /* antlarr: KDE 4.0: make the first parameter "const QString &" */ bool KConfigINIBackEnd::writeConfigFile(QString filename, bool bGlobal, bool bMerge) { // is the config object read-only? if (pConfig->isReadOnly()) return true; // pretend we wrote it KEntryMap aTempMap; QFile *mergeFile = (bMerge ? new QFile(filename) : 0); bool bEntriesLeft = getEntryMap(aTempMap, bGlobal, mergeFile); delete mergeFile; if (bFileImmutable) return true; // pretend we wrote it // OK now the temporary map should be full of ALL entries. // write it out to disk. // Check if file exists: int fileMode = -1; bool createNew = true; KDE_struct_stat buf; if (KDE_stat(QFile::encodeName(filename), &buf) == 0) { if (buf.st_uid == getuid()) { // Preserve file mode if file exists and is owned by user. fileMode = buf.st_mode & 0777; } else { // File is not owned by user: // Don't create new file but write to existing file instead. createNew = false; } } KSaveFile *pConfigFile = 0; FILE *pStream = 0; if (createNew) { pConfigFile = new KSaveFile( filename, 0600 ); if (pConfigFile->status() != 0) { delete pConfigFile; return bEntriesLeft; } if (!bGlobal && (fileMode == -1)) fileMode = mFileMode; if (fileMode != -1) { fchmod(pConfigFile->handle(), fileMode); } pStream = pConfigFile->fstream(); } else { // Open existing file. // We use open() to ensure that we call without O_CREAT. int fd = KDE_open( QFile::encodeName(filename), O_WRONLY | O_TRUNC ); if (fd < 0) { return bEntriesLeft; } pStream = KDE_fdopen( fd, "w"); if (!pStream) { close(fd); return bEntriesLeft; } } writeEntries(pStream, aTempMap); if (pConfigFile) { bool bEmptyFile = (ftell(pStream) == 0); if ( bEmptyFile && ((fileMode == -1) || (fileMode == 0600)) ) { // File is empty and doesn't have special permissions: delete it. ::unlink(QFile::encodeName(filename)); pConfigFile->abort(); } else { // Normal case: Close the file pConfigFile->close(); } delete pConfigFile; } else { fclose(pStream); } return bEntriesLeft; } void KConfigINIBackEnd::writeEntries(FILE *pStream, const KEntryMap &aTempMap) { bool firstEntry = true; // Write default group ::writeEntries(pStream, aTempMap, true, firstEntry, localeString); // Write all other groups ::writeEntries(pStream, aTempMap, false, firstEntry, localeString); } void KConfigBackEnd::virtual_hook( int, void* ) { /*BASE::virtual_hook( id, data );*/ } void KConfigINIBackEnd::virtual_hook( int id, void* data ) { KConfigBackEnd::virtual_hook( id, data ); } bool KConfigBackEnd::checkConfigFilesWritable(bool warnUser) { // WARNING: Do NOT use the event loop as it may not exist at this time. bool allWritable = true; QString errorMsg; if ( !mLocalFileName.isEmpty() && !bFileImmutable && !checkAccess(mLocalFileName,W_OK) ) { errorMsg = i18n("Will not save configuration.\n"); allWritable = false; errorMsg += i18n("Configuration file \"%1\" not writable.\n").arg(mLocalFileName); } // We do not have an immutability flag for kdeglobals. However, making kdeglobals mutable while making // the local config file immutable is senseless. if ( !mGlobalFileName.isEmpty() && useKDEGlobals && !bFileImmutable && !checkAccess(mGlobalFileName,W_OK) ) { if ( errorMsg.isEmpty() ) errorMsg = i18n("Will not save configuration.\n"); errorMsg += i18n("Configuration file \"%1\" not writable.\n").arg(mGlobalFileName); allWritable = false; } if (warnUser && !allWritable) { // Note: We don't ask the user if we should not ask this question again because we can't save the answer. errorMsg += i18n("Please contact your system administrator."); QString cmdToExec = KStandardDirs::findExe(QString("kdialog")); KApplication *app = kapp; if (!cmdToExec.isEmpty() && app) { KProcess lprocess; lprocess << cmdToExec << "--title" << app->instanceName() << "--msgbox" << errorMsg.local8Bit(); lprocess.start( KProcess::Block ); } } return allWritable; }