1 /***************************************************************************
2                           KraftDB.cpp  -
3                              -------------------
4     begin                : Die Feb 3 2004
5     copyright            : (C) 2004 by Klaas Freitag
6     email                : freitag@kde.org
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 #include <QDebug>
18 
19 #include <QFile>
20 #include <QSqlQuery>
21 #include <QStringList>
22 #include <QRegExp>
23 #include <QTextStream>
24 #include <QSqlError>
25 #include <QDir>
26 #include <QDebug>
27 #include <QDomDocument>
28 #include <QDomElement>
29 #include <QTimer>
30 
31 #include "version.h"
32 #include "kraftdb.h"
33 #include "doctype.h"
34 #include "dbids.h"
35 #include "defaultprovider.h"
36 #include "archiveman.h"
37 #include "documentsaverdb.h"
38 
Q_GLOBAL_STATIC(KraftDB,mSelf)39 Q_GLOBAL_STATIC(KraftDB, mSelf)
40 
41 
42 SqlCommand::SqlCommand()
43 {
44 
45 }
46 
SqlCommand(const QString & cmd,const QString & msg,bool mayfail)47 SqlCommand::SqlCommand(const QString& cmd, const QString& msg, bool mayfail)
48     : mSql(cmd),
49       mMessage(msg),
50       mMayFail(mayfail)
51 {
52     if( !mMessage.isEmpty() && !mMessage.endsWith(';') ) {
53         mMessage.append(';');
54     }
55     if( !mSql.isEmpty() && !mSql.endsWith(';') ) {
56         mSql.append(';');
57     }
58 }
59 
message()60 QString SqlCommand::message()
61 {
62     return mMessage;
63 }
64 
command()65 QString SqlCommand::command()
66 {
67     return mSql;
68 }
69 
mayfail()70 bool SqlCommand::mayfail()
71 {
72     return mMayFail;
73 }
74 
75 // ============================
76 
SqlCommandList()77 SqlCommandList::SqlCommandList()
78     :QList<SqlCommand>(),
79       _number(0)
80 {
81 
82 }
83 
setNumber(int no)84 void SqlCommandList::setNumber(int no)
85 {
86     _number = no;
87 }
88 
number()89 int SqlCommandList::number()
90 {
91     return _number;
92 }
93 
setMetaAddDocTypeList(QList<MetaDocTypeAdd> list)94 void SqlCommandList::setMetaAddDocTypeList( QList<MetaDocTypeAdd> list )
95 {
96     _docTypeMetaList = list;
97 }
98 
metaAddDocTypeList() const99 QList<MetaDocTypeAdd> SqlCommandList::metaAddDocTypeList() const
100 {
101     return _docTypeMetaList;
102 }
103 // ==========================================================================
104 
KraftDB()105 KraftDB::KraftDB()
106     :QObject (), mParent(nullptr),
107       mSuccess( true ),
108       EuroTag( QString::fromLatin1( "%EURO" ) ),
109       mInitDialog(nullptr),
110       _amountOfDocs(-1),
111       _amountOfArchs(-1),
112       _emitDBChangeSignal(true)
113 {
114     // Attention: Before setup assistant rewrite, dbConnect() was called here.
115     // Keep that in mind, maybe the auto connect to the DB now misses somewhere.
116     // dbConnect();
117 }
118 
dbConnect(const QString & driver,const QString & dbName,const QString & dbUser,const QString & dbHost,const QString & dbPasswd)119 bool KraftDB::dbConnect( const QString& driver, const QString& dbName,
120                          const QString& dbUser, const QString& dbHost,
121                          const QString& dbPasswd )
122 {
123     mSuccess = true;
124 
125     mDatabaseDriver = driver;
126     mDatabaseName = dbName;
127 
128     if( mDatabaseDriver.isEmpty() ) {
129         // qDebug () << "Database Driver is not specified, check katalog settings";
130         mSuccess = false;
131         return false;
132     } else {
133         // qDebug () << "Using database Driver " << mDatabaseDriver;
134     }
135 
136     QStringList list = QSqlDatabase::drivers();
137     if( list.size() == 0 ) {
138         // qDebug () << "Database Drivers could not be loaded." << endl;
139         mSuccess = false ;
140     } else {
141         if( list.indexOf( mDatabaseDriver ) == -1 ) {
142             // qDebug () << "Database Driver " << mDatabaseDriver << " could not be loaded!" << endl;
143             mSuccess = false;
144         }
145     }
146 
147     if( mSuccess && m_db.isValid() ) {
148         m_db.close();
149     }
150 
151     if( mSuccess ) {
152         m_db = QSqlDatabase::addDatabase( mDatabaseDriver );
153 
154         if ( ! m_db.isValid() || m_db.isOpenError() )
155         {
156             qDebug() <<  "Failed to connect to the database driver: "
157                       << m_db.lastError().text() << endl;
158             mSuccess = false;
159         }
160     }
161 
162     if ( mSuccess ) {
163         int re = 0;
164         if(mDatabaseDriver == "QMYSQL") {
165             int port = -1; // use the default port so far
166             // FIXME: get port from user interface
167             // qDebug () << "Try to open MySQL database " << name << endl;
168             re = checkConnect( dbHost, dbName , dbUser, dbPasswd, port);
169         } else if(mDatabaseDriver == "QSQLITE") {
170             // SqlLite only requires a valid file name which comes in as Database Name
171             // qDebug () << "Try to open SqLite database " << name << endl;
172             re = checkConnect( QString(), dbName, QString(), QString(), -1);
173         }
174         if ( re == 0 ) {
175             // Database successfully opened; we can now issue SQL commands.
176             // qDebug () << "** Database opened successfully" << endl;
177         } else {
178             // qDebug () << "## Could not open database" << endl;
179             mSuccess = false;
180         }
181     }
182 
183     bool detectChanges {true};
184 
185     connect( &_timer, &QTimer::timeout, this, &KraftDB::slotCheckDocDatabaseChanged);
186 
187     if (mSuccess && detectChanges) {
188         _timer.start(10*1000);
189     }
190     return mSuccess;
191 }
192 
self()193 KraftDB *KraftDB::self()
194 {
195     return mSelf;
196 }
197 
close()198 void KraftDB::close()
199 {
200     QString name = m_db.connectionName();
201     // qDebug () << "Database connection name to close: " << name;
202 
203     m_db.close();
204 }
205 
isSqlite()206 bool KraftDB::isSqlite()
207 {
208     const QString dbDriver = qtDriver().toUpper();
209 
210     return (dbDriver.startsWith("QSQLITE"));
211 }
212 
checkConnect(const QString & host,const QString & dbName,const QString & user,const QString & pwd,int port)213 int KraftDB::checkConnect( const QString& host, const QString& dbName,
214                            const QString& user, const QString& pwd, int port )
215 {
216     mDatabaseName = dbName;
217     // works for both mysql and sqlite if the filename for sqlite comes in
218     // as parameter two
219     if ( dbName.isEmpty() || !(m_db.isValid()) ) return false;
220     m_db.setHostName( host );
221     m_db.setDatabaseName( dbName );
222     m_db.setUserName( user );
223     m_db.setPassword( pwd );
224     if( port > -1 ) {
225         m_db.setPort(port);
226     }
227     int re = 0;
228 
229     m_db.open();
230     if ( m_db.isOpenError() ) {
231          qDebug () << "ERR opening the db: " << m_db.lastError().text() <<
232                     ", type is " << m_db.lastError().type() << endl;
233         re = m_db.lastError().type();
234     }
235     return re;
236 }
237 
lastError()238 QSqlError KraftDB::lastError()
239 {
240     return m_db.lastError();
241 }
242 
getLastInsertID()243 dbID KraftDB::getLastInsertID()
244 {
245     if(! ( m_db.isValid()) ) return 0;
246 
247     QSqlQuery query;
248     if( mDatabaseDriver.toLower() == "qmysql" ) {
249         query.prepare("SELECT LAST_INSERT_ID()");
250         query.exec();
251     } else if( mDatabaseDriver.toLower() == "qsqlite") {
252         query.prepare( "SELECT last_insert_rowid()");
253         query.exec();
254     } else {
255         // qDebug () << "############# FATAL ERROR: Unknown database driver " << mDatabaseDriver;
256     }
257     int id = -1;
258 
259     if( query.next() ) {
260         id = query.value(0).toInt();
261     } else {
262         // qDebug () << "############# FATAL ERROR: Query for last insert id is invalid!";
263     }
264     // qDebug () << "Last Insert ID: " << id;
265     return dbID(id);
266 }
267 
databaseName() const268 QString KraftDB::databaseName() const
269 {
270     return mDatabaseName;
271 }
272 
databaseExists()273 bool KraftDB::databaseExists()
274 {
275     bool re = false;
276 
277     if(!m_db.isOpen()) {
278         m_db.open();
279     }
280     if(m_db.isOpen()) {
281         const QStringList t = m_db.tables();
282         re = t.contains( "kraftsystem");
283     }
284     return re;
285 }
286 
setSchemaVersion(const QString & versionStr)287 void KraftDB::setSchemaVersion( const QString& versionStr )
288 {
289     QSqlQuery q;
290     q.prepare( "UPDATE kraftsystem SET dbSchemaVersion=:id" );
291     q.bindValue(":id", versionStr );
292     q.exec();
293 }
294 
parseCommandFile(int currentVersion)295 SqlCommandList KraftDB::parseCommandFile( int currentVersion )
296 {
297     SqlCommandList list;
298     const QString& file = QString("%1_dbmigrate.sql").arg(currentVersion);
299     list = parseCommandFile(file);
300     list.setMetaAddDocTypeList( parseMetaFile(currentVersion) );
301     list.setNumber(currentVersion);
302     return list;
303 }
304 
parseCommandFile(const QString & file)305 SqlCommandList KraftDB::parseCommandFile( const QString& file )
306 {
307     QString sqlFile;
308     QString env = QString::fromUtf8( qgetenv( "KRAFT_HOME" ) );
309     if( !env.isEmpty() && env.right(1) != QDir::separator () ) {
310         env += QDir::separator ();
311     }
312 
313     QString driverPrefix = "mysql"; // Default on mysql
314     if( mDatabaseDriver.toLower() == "qsqlite") {
315         driverPrefix = "sqlite3";
316     }
317 
318     // qDebug() << "XXXXXXXXXX: " << stdDirs.resourceDirs("data");
319 
320     if( env.isEmpty() ) {
321         // Environment-Variable is empty, search in KDE paths
322         QString fragment = QString("kraft/dbmigrate/%1/%2").arg(driverPrefix).arg(file );
323         sqlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, fragment );
324         // qDebug () << "Searching for this fragment: " << fragment;
325         // search in dbcreate as well.
326         if ( sqlFile.isEmpty() ) {
327             fragment = QString("kraft/dbinit/%1/%2").arg(driverPrefix).arg(file );
328             // qDebug () << "Also searching in " << fragment;
329             sqlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, fragment );
330         }
331     } else {
332         // read from environment variable path
333         QString envPath = QString( "%1/database/%2/%3").arg(env).arg(driverPrefix).arg(file);
334         // qDebug () << "Environment variable KRAFT_HOME set, searching for DB setup files in " << envPath;
335         if( QFile::exists( envPath ) ) {
336             sqlFile = envPath;
337         } else if( QFile::exists( QString( "%1/database/%2/migration/%3").arg(env).arg(driverPrefix).arg(file ) ) ){
338             sqlFile = QString( "%1/database/%2/migration/%3").arg(env).arg(driverPrefix).arg(file );
339         }
340     }
341 
342     SqlCommandList retList;
343 
344     if ( ! sqlFile.isEmpty() ) {
345         // qDebug () << "Opening migration file " << sqlFile << endl;
346 
347         QFile f( sqlFile );
348         if ( !f.exists() ) {
349             qDebug() << "FATAL: File" << sqlFile << "does not exist!";
350         }
351         if ( !f.open( QIODevice::ReadOnly ) ) {
352             qDebug () << "FATAL: Could not open " << sqlFile << endl;
353         } else {
354             QTextStream ts( &f );
355             ts.setCodec("UTF-8");
356 
357             QString allSql = ts.readAll(); //Not sure of this one!
358             QStringList sqlList = allSql.split(";");
359 
360             QRegExp reg( "\\s*(#|--)\\s*message:? ?(.*)\\s*\\n" );
361             QRegExp failreg( "\\s*(#|--)\\s*mayfail\\s*\\n" );
362             reg.setMinimal( true );
363 
364             QListIterator<QString> it(sqlList);
365 
366             while( it.hasNext() ) {
367                 QString msg, command;
368 
369                 QString sqlFragment = it.next().trimmed();
370 
371                 int pos = reg.indexIn( sqlFragment.toLower(),  0 );
372                 if ( pos > -1 ) {
373                     msg = reg.cap( 2 );
374                     // qDebug() << "SQL-Commands-Parser: Msg: >" << msg << "<" << endl;
375                 }
376 
377                 bool mayfail = false;
378                 pos = failreg.indexIn( sqlFragment.toLower(), 0 );
379                 if( pos > -1 ) {
380                     mayfail = true;
381                 }
382                 bool clean = false;
383                 while( ! clean ) {
384                     if(  sqlFragment.startsWith("#") || sqlFragment.startsWith("--") ) {
385                         // remove the comment line.
386                         int newLinePos = sqlFragment.indexOf('\n');
387                         // qDebug() << "Found newline in <" << sqlFragment << ">:" << newLinePos;
388                         if(newLinePos > 0) {
389                             sqlFragment = sqlFragment.remove( 0, 1+sqlFragment.indexOf('\n') );
390                         } else {
391                             sqlFragment = QString();
392                         }
393                         // qDebug() << "Left over SQL Fragment:" << sqlFragment;
394                     } else {
395                         clean = true;
396                     }
397                 }
398 
399                 if( !sqlFragment.isEmpty() ) {
400 
401                     if( sqlFragment.startsWith( "CREATE TRIGGER", Qt::CaseInsensitive )) {
402                         // Triggers contain a ; which scares the parser. In case of triggers we pull
403                         // the next item in the list which should be the END; keyword.
404                         command = sqlFragment + ";";
405                         if( it.hasNext())
406                             command += it.next();
407                     } else {
408                         // ordinary command, we take it as it is.
409                         command = sqlFragment;
410                     }
411                     if( !command.isEmpty() ) {
412                         retList.append( SqlCommand( command, msg, mayfail ) );
413                     }
414                 }
415             }
416         }
417     } else {
418         qDebug () << "ERR: Can not find sql file " << file;
419     }
420 
421 
422 
423     return retList;
424 }
425 
parseMetaFile(int currentVersion)426 QList<MetaDocTypeAdd> KraftDB::parseMetaFile( int currentVersion )
427 {
428     const QString fileName = QString("%1_meta.xml").arg(currentVersion);
429 
430     QString env = QString::fromUtf8( qgetenv( "KRAFT_HOME" ) );
431     if( !env.isEmpty() && env.right(1) != QDir::separator () ) {
432         env += QDir::separator ();
433     }
434     QString xmlFile;
435     if( !env.isEmpty() ) {
436         xmlFile = env + QLatin1String("database/meta/") + fileName;
437     } else {
438         const QString fragment = QString("kraft/meta/") + fileName;
439         xmlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, fragment );
440     }
441     QFile f( xmlFile );
442     MetaXMLParser parser;
443     if( f.exists() ) {
444         if ( !f.open( QIODevice::ReadOnly ) ) {
445             qDebug () << "FATAL: Could not open " << xmlFile << endl;
446         } else {
447             QTextStream ts( &f );
448             ts.setCodec("UTF-8");
449             parser.parse( &f );
450         }
451     } else {
452         qDebug() << "XML Metafile" << xmlFile << "does not exist!";
453     }
454 
455     return parser.metaDocTypeAddList();
456 }
457 
processSqlCommands(const SqlCommandList & commands)458 int KraftDB::processSqlCommands( const SqlCommandList& commands )
459 {
460     int cnt = 0;
461 
462     // first do the doctype definitions
463     QList<MetaDocTypeAdd> newDocTypes = commands.metaAddDocTypeList();
464 
465     // loop over all doctypes first, later loop again to create the followers.
466     // The followers might reference each other and thus must exist.
467     for( auto newDocType : newDocTypes ) {
468        const QString name = newDocType.name();
469        DocType type(name, true);
470 
471        for( QString attr : newDocType._attribs.keys() ) {
472            type.setAttribute(attr, newDocType._attribs[attr]);
473        }
474        type.save();
475     }
476 
477     // now loop again to process the followers
478     for( auto newDocType : newDocTypes ) {
479         const QString name = newDocType.name();
480         if( newDocType._follower.count() > 0 ) {
481             DocType type(name, true);
482             type.setAllFollowers(newDocType._follower);
483             type.save();
484         }
485     }
486 
487     foreach( SqlCommand cmd, commands ) {
488         if( !cmd.message().isEmpty() ) {
489             emit statusMessage( cmd.message() );
490         }
491 
492         if( !cmd.command().isEmpty() ) {
493             bool res = true;
494             QSqlQuery q;
495             q.clear();
496             res = q.exec(cmd.command()) || cmd.mayfail();
497 
498             if ( res ) {
499                 // qDebug () << "Successful SQL Command: " << cmd.command() << endl;
500                 cnt ++;
501             } else {
502                 QSqlError err = q.lastError();
503                 res = false;
504                 qDebug () << "###### Failed SQL Command " << cmd.command() << ": " << err.text() << endl;
505             }
506             q.clear();
507             emit processedSqlCommand( res );
508 
509         }
510     }
511     return cnt;
512 }
513 
requiredSchemaVersion()514 int KraftDB::requiredSchemaVersion()
515 {
516     return KRAFT_REQUIRED_SCHEMA_VERSION;
517 }
518 
currentSchemaVersion()519 int KraftDB::currentSchemaVersion()
520 {
521     QSqlQuery query;
522 
523     query.exec("SELECT dbschemaversion FROM kraftsystem"); //We'll retrieve every record
524 
525     int re = -1;
526     if ( query.next() ) {
527         re = query.value(0).toInt();
528     }
529     return re;
530 }
531 
qtDriver()532 QString KraftDB::qtDriver()
533 {
534     return mDatabaseDriver;
535 }
536 
currentTimeStamp(const QDateTime & dt)537 QString KraftDB::currentTimeStamp( const QDateTime& dt )
538 {
539     QString dateStr;
540     if( dt.isValid() ) {
541         dateStr = dt.toString(Qt::ISODate);
542     } else {
543         dateStr = QDateTime::currentDateTime().toString(Qt::ISODate);
544     }
545     return dateStr;
546 }
547 
mysqlEuroEncode(const QString & str) const548 QString KraftDB::mysqlEuroEncode( const QString& str ) const
549 {
550     QChar euro( 0x20ac );
551     QString restr( str );
552     return restr.replace( euro, EuroTag );
553 }
554 
mysqlEuroDecode(const QString & str) const555 QString KraftDB::mysqlEuroDecode( const QString& str ) const
556 {
557     QChar euro( 0x20ac );
558     QString restr( str );
559     return restr.replace( EuroTag, euro );
560 }
561 
wordList(const QString & selector,StringMap replaceMap)562 QStringList KraftDB::wordList( const QString& selector, StringMap replaceMap )
563 {
564     QStringList re;
565     QSqlQuery query;
566 
567     query.prepare("SELECT category, word FROM wordLists WHERE category=:cat");
568     query.bindValue(":cat", selector);
569     query.exec();
570     while ( query.next() ) {
571         re << replaceTagsInWord( query.value(1).toString(), replaceMap );
572     }
573     re.sort();
574     return re;
575 }
576 
replaceTagsInWord(const QString & w,StringMap replaceMap) const577 QString KraftDB::replaceTagsInWord( const QString& w, StringMap replaceMap ) const
578 {
579     QString re( w );
580 
581     QMap<int, QStringList> reMap;
582     StringMap::Iterator it;
583     for ( it = replaceMap.begin(); it != replaceMap.end(); ++it ) {
584         reMap[it.key().length()] << it.key();
585     }
586 
587     QMap<int, QStringList>::Iterator reIt;
588     for ( reIt = reMap.end(); reIt != reMap.begin(); ) {
589         --reIt;
590         QStringList keys = reIt.value();
591         // qDebug () << "PP: " << keys;
592         for ( QStringList::Iterator dtIt = keys.begin(); dtIt != keys.end(); ++dtIt ) {
593             QString repKey = *dtIt;
594             re.replace( repKey, replaceMap[repKey] );
595         }
596     }
597 
598     // qDebug () << "Adding to wordlist <" << re << ">";
599 
600     return re;
601 }
602 
writeWordList(const QString & listName,const QStringList & list)603 void KraftDB::writeWordList( const QString& listName, const QStringList& list )
604 {
605     // qDebug () << "Saving " << list[0] << " into list " << listName << endl;
606     QSqlQuery  qd;
607     qd.prepare( "DELETE FROM wordLists WHERE category=:catName" );
608     qd.bindValue( ":catName", listName );
609     qd.exec();
610 
611     QSqlQuery qi;
612     qi.prepare( "INSERT INTO wordLists (category, word) VALUES( :category, :entry )" );
613 
614     qi.bindValue( ":category", listName );
615     for ( QStringList::ConstIterator it = list.begin(); it != list.end(); ++it ) {
616         qi.bindValue( ":entry", *it );
617         qi.exec();
618     }
619 }
620 
checkTableExistsSqlite(const QString & name,const QStringList & lookupCols)621 bool KraftDB::checkTableExistsSqlite(const QString& name, const QStringList& lookupCols)
622 {
623     const QString query = QString("PRAGMA table_info(%1)").arg(name);
624     QSqlQuery q(query);
625     QStringList cols = lookupCols;
626 
627     q.exec();
628     QSqlError err = q.lastError();
629     if( err.isValid() ) {
630         qDebug() << "Error: " << err.text();
631     }
632 
633     while( q.next() ) {
634         const QString colName = q.value(1).toString();
635         qDebug() << "checking colum" << colName;
636         cols.removeAll(colName);
637     }
638     return cols.isEmpty();
639 }
640 
~KraftDB()641 KraftDB::~KraftDB()
642 {
643 }
644 
slotCheckDocDatabaseChanged()645 void KraftDB::slotCheckDocDatabaseChanged()
646 {
647     bool changed{false};
648     {
649         QSqlQuery q("SELECT count(*) FROM document");
650 
651         q.exec();
652         QSqlError err = q.lastError();
653         if( err.isValid() ) {
654             qDebug() << "Error: " << err.text();
655             return;
656         }
657 
658         if ( q.next() ) {
659             bool ok;
660             int cnt = q.value(0).toInt(&ok);
661 
662             if (_amountOfDocs != -1 && cnt != _amountOfDocs ) {
663                 qDebug() << "Docs from" << _amountOfDocs << "to" << cnt;
664                 changed = true;
665             }
666             _amountOfDocs = cnt;
667         }
668     }
669     {
670         QSqlQuery qArch("SELECT count(*) FROM archdoc");
671 
672         qArch.exec();
673         QSqlError err = qArch.lastError();
674         if( err.isValid() ) {
675             qDebug() << "Error: " << err.text();
676             return;
677         }
678 
679         if ( qArch.next() ) {
680             bool ok;
681             int cnt = qArch.value(0).toInt(&ok);
682 
683             if (_amountOfArchs != -1 && cnt != _amountOfArchs) {
684                 qDebug() << "Arched docs from" << _amountOfArchs << "to" << cnt;
685                 changed = true;
686             }
687             _amountOfArchs = cnt;
688         }
689     }
690     if (changed && _emitDBChangeSignal) emit docDatabaseChanged();
691 }
692 
archiveDocument(KraftDoc * docPtr)693 dbID KraftDB::archiveDocument( KraftDoc *docPtr )
694 {
695     dbID archID = ArchiveMan::self()->archiveDocument( docPtr );
696 
697     if (archID.isOk()) {
698         _emitDBChangeSignal = false; // block sending of the signal
699         slotCheckDocDatabaseChanged();
700         _emitDBChangeSignal = true;
701     }
702 
703     return archID;
704 }
705 
loadDocument(const QString & id,KraftDoc * docPtr)706 void KraftDB::loadDocument(const QString& id, KraftDoc *docPtr)
707 {
708     DocumentSaverDB loader;
709 
710     loader.load(id, docPtr);
711 }
712 
saveDocument(KraftDoc * docPtr)713 bool KraftDB::saveDocument(KraftDoc *docPtr)
714 {
715     bool res {false};
716     DocumentSaverDB saver;
717 
718     if (docPtr) {
719         res  = saver.saveDocument(docPtr);
720         if (res) {
721             _emitDBChangeSignal = false; // block sending of the signal
722             slotCheckDocDatabaseChanged();
723             _emitDBChangeSignal = true;
724         }
725     }
726 
727     return res;
728 }
729 
730