File indexing completed on 2024-04-28 16:30:31

0001 /***************************************************************************
0002  * SPDX-FileCopyrightText: 2022 S. MANKOWSKI stephane@mankowski.fr
0003  * SPDX-FileCopyrightText: 2022 G. DE BURE support@mankowski.fr
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  ***************************************************************************/
0006 /** @file
0007  * This file implements classes SKGDocument.
0008  *
0009  * @author Stephane MANKOWSKI / Guillaume DE BURE
0010  */
0011 #include "skgdocument.h"
0012 
0013 #include <kcolorscheme.h>
0014 
0015 #include <qapplication.h>
0016 #ifdef SKG_DBUS
0017 #include <qdbusconnection.h>
0018 #endif
0019 #include <qdir.h>
0020 #include <qfile.h>
0021 #include <qhash.h>
0022 #include <qicon.h>
0023 #include <qjsondocument.h>
0024 #include <qprocess.h>
0025 #include <qregularexpression.h>
0026 #include <qsqldatabase.h>
0027 #include <qsqldriver.h>
0028 #include <qsqlerror.h>
0029 #include <qsqlquery.h>
0030 #include <qtconcurrentrun.h>
0031 #include <qthread.h>
0032 #include <qurl.h>
0033 #include <quuid.h>
0034 #include <qvariant.h>
0035 
0036 #include <sqlite3.h>
0037 #include <cmath>
0038 
0039 #include "skgdocumentprivate.h"
0040 #include "skgerror.h"
0041 #include "skgpropertyobject.h"
0042 #include "skgreport.h"
0043 #include "skgservices.h"
0044 #include "skgtraces.h"
0045 #include "skgtransactionmng.h"
0046 
0047 #define SQLDRIVERNAME QStringLiteral("SKGSQLCIPHER")
0048 
0049 /**
0050  * Custom sqlite function.
0051  */
0052 static void sleepFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0053 {
0054     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0055     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0056 
0057     if (Q_LIKELY(data1)) {
0058         auto s = SKGServices::stringToInt(QString(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar)));
0059         QThread::sleep(s);
0060 
0061         sqlite3_result_text(context, "OK", 2, SQLITE_TRANSIENT);
0062     }
0063 }
0064 
0065 /**
0066  * Custom sqlite function.
0067  */
0068 static void periodFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0069 {
0070     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0071     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0072     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0073     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0074 
0075     if (Q_LIKELY(data1 && data2)) {
0076         QDate date = SKGServices::stringToTime(QString(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar))).date();
0077         QString format = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar)).toUpper();
0078         QString period = SKGServices::dateToPeriod(date, format);
0079         QByteArray output = period.toUtf8();
0080         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0081     }
0082 }
0083 
0084 /**
0085  * Custom sqlite function.
0086  */
0087 static void formattedDateFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0088 {
0089     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0090     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0091     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0092     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0093 
0094     if (Q_LIKELY(data1 && data2)) {
0095         QString string(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
0096         QString format = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar));
0097 
0098         QString date = QDate::fromString(string, QStringLiteral("yyyy-MM-dd")).toString(format);
0099 
0100         QByteArray output = date.toUtf8();
0101         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0102     }
0103 }
0104 
0105 /**
0106  * Custom sqlite function.
0107  */
0108 static void dateFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0109 {
0110     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0111     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0112     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0113     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0114 
0115     if (Q_LIKELY(data1 && data2)) {
0116         QString string(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
0117         QString format = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar));
0118 
0119         QString date = SKGServices::dateToSqlString(string, format).trimmed();
0120         if (date.isEmpty()) {
0121             date = QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd"));
0122         }
0123 
0124         QByteArray output = date.toUtf8();
0125         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0126     }
0127 }
0128 
0129 /**
0130  * Custom sqlite function.
0131  */
0132 static void weekYearFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0133 {
0134     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0135     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0136 
0137     if (Q_LIKELY(data1)) {
0138         QString string(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
0139 
0140         QString date = SKGServices::dateToPeriod(SKGServices::stringToTime(string).date(), QStringLiteral("W"));
0141 
0142         QByteArray output = date.toUtf8();
0143         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0144     }
0145 }
0146 
0147 /**
0148  * Custom sqlite function.
0149  */
0150 static void currencyFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0151 {
0152     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0153     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0154     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0155     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0156 
0157     if (Q_LIKELY(data1 && data2)) {
0158         double string = SKGServices::stringToDouble(QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar)));
0159         QString symbol = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar));
0160 
0161         QString currency = SKGServices::toCurrencyString(string, symbol);
0162 
0163         QByteArray output = currency.toUtf8();
0164         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0165     }
0166 }
0167 
0168 /**
0169  * Custom sqlite function.
0170  */
0171 static void xorFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0172 {
0173     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0174     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0175     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0176     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0177 
0178     if (Q_LIKELY(data1 && data2)) {
0179         auto string = QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar)).toUtf8();
0180         auto key = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar)).toUtf8();
0181 
0182         if (string.startsWith(QByteArray("# "))) {
0183             // Decrypt
0184             string = QByteArray::fromHex(string.right(string.length() - 2));
0185             QByteArray estring;
0186             for (int i = 0; i < string.size(); ++i) {
0187                 estring += static_cast<char>(string[i] ^ key[i % key.size()]);
0188             }
0189             QByteArray output = estring;
0190             sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0191         } else {
0192             // Encrypt
0193             QByteArray estring;
0194             for (int i = 0; i < string.size(); ++i) {
0195                 estring += static_cast<char>(string[i] ^ key[i % key.size()]);
0196             }
0197             QByteArray output = "# " + estring.toHex();
0198             sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0199         }
0200     }
0201 }
0202 
0203 static void xordoubleFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0204 {
0205     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0206     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0207     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0208     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0209 
0210     if (Q_LIKELY(data1 && data2)) {
0211         auto d = SKGServices::stringToDouble(QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar)).toUtf8());
0212         auto key = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar)).toUtf8();
0213         int kk = 0;
0214         for (int i = 0; i < key.size(); ++i) {
0215             kk += key[i];
0216         }
0217         QByteArray output = SKGServices::doubleToString(static_cast<double>(kk) - d).toUtf8();
0218         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0219     }
0220 }
0221 
0222 /**
0223  * Custom sqlite function.
0224  */
0225 static void wordFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0226 {
0227     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0228     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0229     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0230     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0231 
0232     if (Q_LIKELY(data1 && data2)) {
0233         QString string1(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
0234         string1 = string1.simplified();
0235         QRegularExpression re(QStringLiteral("(\\w+)"));
0236         QRegularExpressionMatchIterator i = re.globalMatch(string1);
0237         QStringList list;
0238         while (i.hasNext()) {
0239             QRegularExpressionMatch match = i.next();
0240             QString word = match.captured(1);
0241             list << word;
0242         }
0243 
0244         int pos = SKGServices::stringToInt(QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar)));
0245         if (pos == 0) {
0246             pos = 1;
0247         } else if (pos > list.count()) {
0248             pos = list.count();
0249         } else if (pos < -list.count()) {
0250             pos = 1;
0251         } else if (pos < 0) {
0252             pos = list.count() + pos + 1;
0253         }
0254 
0255         QByteArray output = list[pos - 1].toUtf8();
0256 
0257         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0258     }
0259 }
0260 
0261 /**
0262  * Custom sqlite function.
0263  */
0264 static void wildcardFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0265 {
0266     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0267     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0268     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0269     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0270 
0271     if (Q_LIKELY(data1 && data2)) {
0272         QString string1(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
0273         QString string2 = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar));
0274 
0275         QRegularExpression pattern(QRegularExpression::anchoredPattern(QRegularExpression::wildcardToRegularExpression(string1)), QRegularExpression::CaseInsensitiveOption);
0276         if (pattern.isValid()) {
0277             sqlite3_result_int(context, static_cast<int>(pattern.match(string2).hasMatch()));
0278         } else {
0279             sqlite3_result_error(context, pattern.errorString().toUtf8().constData(), -1);
0280         }
0281     }
0282 }
0283 
0284 /**
0285  * Custom sqlite function.
0286  */
0287 static void regexpFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0288 {
0289     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0290     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0291     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0292     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0293 
0294     if (Q_LIKELY(data1 && data2)) {
0295         QString string1(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
0296         QString string2 = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar));
0297 
0298         QRegularExpression pattern(QRegularExpression::anchoredPattern(string1), QRegularExpression::CaseInsensitiveOption);
0299         if (pattern.isValid()) {
0300             sqlite3_result_int(context, static_cast<int>(pattern.match(string2).hasMatch()));
0301         } else {
0302             sqlite3_result_error(context, pattern.errorString().toUtf8().constData(), -1);
0303         }
0304     }
0305 }
0306 
0307 /**
0308  * Custom sqlite function.
0309  */
0310 static void regexpCaptureFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0311 {
0312     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0313     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0314     int len2 = sqlite3_value_bytes16(argv[ 1 ]);
0315     const void* data2 = sqlite3_value_text16(argv[ 1 ]);
0316     int len3 = sqlite3_value_bytes16(argv[ 2 ]);
0317     const void* data3 = sqlite3_value_text16(argv[ 2 ]);
0318     if (Q_LIKELY(data1 && data2 && data3)) {
0319         int pos = SKGServices::stringToInt(QString::fromRawData(reinterpret_cast<const QChar*>(data3), len3 / sizeof(QChar)));
0320 
0321         QString string1(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
0322         QString string2 = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar));
0323 
0324         QRegularExpression pattern(string1, QRegularExpression::CaseInsensitiveOption);
0325         if (pattern.isValid()) {
0326             auto match = pattern.match(string2);
0327             QByteArray output = match.capturedTexts().value(pos).toUtf8();
0328             sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0329         } else {
0330             sqlite3_result_error(context, pattern.errorString().toUtf8().constData(), -1);
0331         }
0332     }
0333 }
0334 
0335 /**
0336  * Custom sqlite function.
0337  */
0338 static void upperFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0339 {
0340     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0341     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0342 
0343     if (Q_LIKELY(data1)) {
0344         QByteArray output = QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar)).toUpper().toUtf8();
0345         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0346     }
0347 }
0348 
0349 /**
0350  * Custom sqlite function.
0351  */
0352 static void nextFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0353 {
0354     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0355     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0356 
0357     if (Q_LIKELY(data1)) {
0358         QByteArray output = SKGServices::getNextString(QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar))).toUtf8();
0359         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0360     }
0361 }
0362 
0363 /**
0364  * Custom sqlite function.
0365  */
0366 static void lowerFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0367 {
0368     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0369     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0370 
0371     if (Q_LIKELY(data1)) {
0372         QByteArray output = QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar)).toLower().toUtf8();
0373         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0374     }
0375 }
0376 
0377 /**
0378  * Custom sqlite function.
0379  */
0380 static void capitalizeFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
0381 {
0382     int len1 = sqlite3_value_bytes16(argv[ 0 ]);
0383     const void* data1 = sqlite3_value_text16(argv[ 0 ]);
0384 
0385     if (Q_LIKELY(data1)) {
0386         QString str = QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
0387         QByteArray output = (str.at(0).toUpper() + str.mid(1).toLower()).toUtf8();
0388         sqlite3_result_text(context, output.constData(), output.size(), SQLITE_TRANSIENT);
0389     }
0390 }
0391 
0392 static SKGError addSqliteAddon(QSqlDatabase* iDb)
0393 {
0394     SKGError err;
0395     auto* sqlite_handle = iDb->driver()->handle().value<sqlite3*>();
0396     if (sqlite_handle != nullptr) {
0397         sqlite3_create_function(sqlite_handle, "REGEXP", 2, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &regexpFunction, nullptr, nullptr);
0398         sqlite3_create_function(sqlite_handle, "REGEXPCAPTURE", 3, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &regexpCaptureFunction, nullptr, nullptr);
0399         sqlite3_create_function(sqlite_handle, "WILDCARD", 2, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &wildcardFunction, nullptr, nullptr);
0400         sqlite3_create_function(sqlite_handle, "WORD", 2, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &wordFunction, nullptr, nullptr);
0401         sqlite3_create_function(sqlite_handle, "TODATE", 2, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &dateFunction, nullptr, nullptr);
0402         sqlite3_create_function(sqlite_handle, "TOWEEKYEAR", 1, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &weekYearFunction, nullptr, nullptr);
0403         sqlite3_create_function(sqlite_handle, "TOFORMATTEDDATE", 2, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &formattedDateFunction, nullptr, nullptr);
0404         sqlite3_create_function(sqlite_handle, "PERIOD", 2, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &periodFunction, nullptr, nullptr);
0405         sqlite3_create_function(sqlite_handle, "SLEEP", 1, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &sleepFunction, nullptr, nullptr);
0406         sqlite3_create_function(sqlite_handle, "TOCURRENCY", 2, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &currencyFunction, nullptr, nullptr);
0407         sqlite3_create_function(sqlite_handle, "XOR", 2, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &xorFunction, nullptr, nullptr);
0408         sqlite3_create_function(sqlite_handle, "XORD", 2, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &xordoubleFunction, nullptr, nullptr);
0409         sqlite3_create_function(sqlite_handle, "UPPER", 1, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &upperFunction, nullptr, nullptr);
0410         sqlite3_create_function(sqlite_handle, "LOWER", 1, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &lowerFunction, nullptr, nullptr);
0411         sqlite3_create_function(sqlite_handle, "NEXT", 1, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &nextFunction, nullptr, nullptr);
0412         int rc = sqlite3_create_function(sqlite_handle, "CAPITALIZE", 1, SQLITE_UTF16 | SQLITE_DETERMINISTIC, nullptr, &capitalizeFunction, nullptr, nullptr);
0413         if (rc != SQLITE_OK) {
0414             err = SKGError(SQLLITEERROR + rc, QStringLiteral("sqlite3_create_function failed"));
0415         }
0416     } else {
0417         SKGTRACE << "WARNING: Custom sqlite functions not added" << SKGENDL;
0418     }
0419     return err;
0420 }
0421 
0422 SKGDocument::SKGDocument()
0423     :  d(new SKGDocumentPrivate())
0424 {
0425     SKGTRACEINFUNC(10)
0426     // Set the QThreadPool
0427     // QThreadPool::globalInstance()->setMaxThreadCount(3*QThread::idealThreadCount());
0428 
0429     // DBUS registration
0430 #ifdef SKG_DBUS
0431     QDBusConnection dbus = QDBusConnection::sessionBus();
0432     dbus.registerObject(QStringLiteral("/skg/skgdocument"), this, QDBusConnection::ExportAllContents);
0433     dbus.registerService(QStringLiteral("org.skg"));
0434 #endif
0435 
0436     // Initialisation of undoable tables
0437     SKGListNotUndoable.push_back(QStringLiteral("T.doctransaction"));
0438     SKGListNotUndoable.push_back(QStringLiteral("T.doctransactionitem"));
0439     SKGListNotUndoable.push_back(QStringLiteral("T.doctransactionmsg"));
0440 
0441     // Database unique identifier
0442     ++SKGDocumentPrivate::m_databaseUniqueIdentifier;
0443     d->m_databaseIdentifier = "SKGDATABASE_" % SKGServices::intToString(SKGDocumentPrivate::m_databaseUniqueIdentifier);
0444 
0445     // Initialisation of backup file parameters
0446     setBackupParameters(QLatin1String(""), QStringLiteral(".old"));
0447 
0448     // 320157 vvvv
0449     // Disable OS lock
0450     sqlite3_vfs* vfs = sqlite3_vfs_find("unix-none");
0451     if (Q_LIKELY(vfs)) {
0452         sqlite3_vfs_register(vfs, 1);
0453     } else {
0454         SKGTRACE << "WARNING: Impossible to use the 'unix-none' vfs mode of sqlite3. Use:'" << sqlite3_vfs_find(nullptr)->zName << "'" << SKGENDL;
0455     }
0456     // 320157 ^^^^
0457 }
0458 
0459 SKGDocument::~SKGDocument()
0460 {
0461     SKGTRACEINFUNC(10)
0462     close();
0463     d->m_progressFunction = nullptr;
0464     d->m_progressData = nullptr;
0465     d->m_checkFunctions.clear();
0466 
0467     for (auto w  : qAsConst(d->m_watchers)) {
0468         delete w;
0469     }
0470     d->m_watchers.clear();
0471 
0472     delete d->m_cacheSql;
0473     d->m_cacheSql = nullptr;
0474 
0475     delete d;
0476     d = nullptr;
0477 }
0478 
0479 QString SKGDocument::getUniqueIdentifier() const
0480 {
0481     return d->m_uniqueIdentifier;
0482 }
0483 
0484 QString SKGDocument::getDatabaseIdentifier() const
0485 {
0486     return d->m_databaseIdentifier;
0487 }
0488 
0489 SKGError SKGDocument::setProgressCallback(FuncProgress iProgressFunction, void* iData)
0490 {
0491     d->m_progressFunction = iProgressFunction;
0492     d->m_progressData = iData;
0493     return SKGError();
0494 }
0495 
0496 SKGError SKGDocument::addEndOfTransactionCheck(SKGError(*iCheckFunction)(SKGDocument*))
0497 {
0498     d->m_checkFunctions.append(iCheckFunction);
0499     return SKGError();
0500 }
0501 
0502 SKGError SKGDocument::stepForward(int iPosition, const QString& iText)
0503 {
0504     SKGError err;
0505 
0506     // Increase the step for the last transaction
0507     if (Q_LIKELY(getDepthTransaction())) {
0508         d->m_posStepForTransaction.pop_back();
0509         d->m_posStepForTransaction.push_back(iPosition);
0510     }
0511 
0512     // Check if a callback function exists
0513     if (Q_LIKELY(d->m_progressFunction)) {
0514         // YES ==> compute
0515         double min = 0;
0516         double max = 100;
0517 
0518         bool emitevent = true;
0519         auto nbIt = d->m_nbStepForTransaction.constBegin();
0520         auto posIt = d->m_posStepForTransaction.constBegin();
0521         for (; emitevent && nbIt != d->m_nbStepForTransaction.constEnd(); ++nbIt) {
0522             int p = *posIt;
0523             int n = *nbIt;
0524             if (Q_UNLIKELY(p < 0 || p > n)) {
0525                 p = n;
0526             }
0527 
0528             if (Q_LIKELY(n != 0)) {
0529                 double pmin = min;
0530                 double pmax = max;
0531                 min = pmin + (pmax - pmin) * (static_cast<double>(p) / static_cast<double>(n));
0532                 max = pmin + (pmax - pmin) * (static_cast<double>(p + 1) / static_cast<double>(n));
0533                 if (Q_UNLIKELY(max > 100)) {
0534                     max = 100;
0535                 }
0536             } else {
0537                 emitevent = false;
0538             }
0539 
0540             ++posIt;
0541         }
0542 
0543         int posPercent = rint(min);
0544 
0545         // Call the call back
0546         if (emitevent) {
0547             d->m_inProgress = true;
0548             QString text;
0549             qint64 time = QDateTime::currentMSecsSinceEpoch() - d->m_timeBeginTransaction;
0550             if (Q_UNLIKELY(time >  3000)) {
0551                 text = iText;
0552                 if (text.isEmpty()) {
0553                     text = d->m_nameForTransaction.at(d->m_nameForTransaction.count() - 1);
0554                 }
0555             }
0556             if (Q_LIKELY(d->m_progressFunction(posPercent, time, text, d->m_progressData) != 0)) {
0557                 err.setReturnCode(ERR_ABORT).setMessage(i18nc("User interrupted something that Skrooge was performing", "The current transaction has been interrupted"));
0558 
0559                 // Remove all untransactionnal messaged
0560                 m_unTransactionnalMessages.clear();
0561             }
0562             d->m_inProgress = false;
0563         }
0564     }
0565     return err;
0566 }
0567 
0568 SKGError SKGDocument::beginTransaction(const QString& iName, int iNbStep, const QDateTime& iDate, bool iRefreshViews)
0569 {
0570     SKGError err;
0571     SKGTRACEINFUNCRC(5, err)
0572     SKGTRACEL(10) << "Input parameter [name]=[" << iName << "]  [nb step]=[" << iNbStep << "]  [refresh]=[" << (iRefreshViews ? QStringLiteral("Y") : QStringLiteral("N")) << ']' << SKGENDL;
0573     bool overrideCursor = false;
0574     if (d->m_nbStepForTransaction.isEmpty()) {
0575         // Open SQLtransaction
0576         err = executeSqliteOrder(QStringLiteral("BEGIN;"));
0577         IFOK(err) {
0578             overrideCursor = true;
0579 
0580             // Create undo redo transaction
0581             err = executeSqliteOrder(QStringLiteral("insert into doctransaction (d_date, t_name, i_parent") %
0582                                      (!iRefreshViews ? ", t_refreshviews" : "") %
0583                                      ") values "
0584                                      "('" % SKGServices::timeToString(iDate) %
0585                                      "','" % SKGServices::stringToSqlString(iName) %
0586                                      "', " % SKGServices::intToString(getTransactionToProcess(SKGDocument::UNDO)) %
0587                                      (!iRefreshViews ? ",'N'" : "") %
0588                                      ");");
0589             addValueInCache(QStringLiteral("SKG_REFRESH_VIEW"), (iRefreshViews ? QStringLiteral("Y") : QStringLiteral("N")));
0590             d->m_currentTransaction = getTransactionToProcess(SKGDocument::UNDO);
0591             d->m_timeBeginTransaction = QDateTime::currentMSecsSinceEpoch();
0592         }
0593     } else {
0594         // A transaction already exists
0595         // Check if the child transaction is a opened in the progress callback
0596         if (d->m_inProgress) {
0597             err.setReturnCode(ERR_FAIL).setMessage(i18nc("Something went wrong with SQL transactions", "A transaction cannot be started during execution of another one"));
0598         }
0599     }
0600     IFOK(err) {
0601         d->m_nbStepForTransaction.push_back(iNbStep);
0602         d->m_posStepForTransaction.push_back(iNbStep);
0603         QString n = iName;
0604         n = n.remove(QStringLiteral("#INTERNAL#"));
0605         if (n.isEmpty() && !d->m_nameForTransaction.isEmpty()) {
0606             n = d->m_nameForTransaction.at(d->m_nameForTransaction.count() - 1);
0607         }
0608         d->m_nameForTransaction.push_back(n);
0609 
0610         if (iNbStep > 0) {
0611             err = stepForward(0);
0612         }
0613     } else {
0614         executeSqliteOrder(QStringLiteral("ROLLBACK;"));
0615     }
0616 
0617     if (Q_LIKELY(overrideCursor && !err && qobject_cast<QGuiApplication*>(qApp) != nullptr)) {  // clazy:excludeall=unneeded-cast
0618         QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
0619     }
0620 
0621     return err;
0622 }
0623 
0624 SKGError SKGDocument::checkExistingTransaction() const
0625 {
0626     SKGError err;
0627     if (d->m_nbStepForTransaction.isEmpty()) {
0628         err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "A transaction must be opened to do this action"));
0629     }
0630     return err;
0631 }
0632 
0633 SKGError SKGDocument::endTransaction(bool succeedded)
0634 {
0635     SKGError err;
0636     SKGError errOverwritten;
0637     SKGTRACEINFUNCRC(5, err)
0638     if (Q_UNLIKELY(d->m_nbStepForTransaction.empty())) {
0639         err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "Closing transaction failed because too many transactions ended"));
0640     } else {
0641         stepForward(d->m_nbStepForTransaction.at(d->m_nbStepForTransaction.count() - 1));  // =100%
0642         if (Q_LIKELY(d->m_nbStepForTransaction.size())) {  // This test is needed. It is a security in some cases.
0643             d->m_nbStepForTransaction.pop_back();
0644             d->m_posStepForTransaction.pop_back();
0645             d->m_nameForTransaction.pop_back();
0646         }
0647         QString currentTransactionString = SKGServices::intToString(getCurrentTransaction());
0648 
0649         if (d->m_nbStepForTransaction.empty()) {
0650             QStringList listModifiedTables;
0651 
0652             // Check
0653             if (succeedded) {
0654                 auto cachepointer = d->m_cache;
0655                 d->m_cache = QHash<QString, QString>();
0656 
0657                 for (auto check : qAsConst(d->m_checkFunctions)) {
0658                     errOverwritten = check(this);
0659                     IFKO(errOverwritten) {
0660                         succeedded = false;
0661                         SKGTRACEL(5) << "Transaction cancelled by a check" << SKGENDL;
0662                         break;
0663                     }
0664                 }
0665 
0666                 d->m_cache = cachepointer;
0667             }
0668 
0669             if (succeedded) {
0670                 // Link items on current transaction
0671                 IFOK(err) {
0672                     err = executeSqliteOrder("UPDATE doctransactionitem set rd_doctransaction_id=" % currentTransactionString % " WHERE rd_doctransaction_id=0;");
0673                 }
0674 
0675                 // Optimization of the current transaction
0676                 IFOK(err) {
0677                     SKGStringListList listTmp;
0678                     err = executeSelectSqliteOrder("SELECT count(1) FROM doctransactionitem where rd_doctransaction_id=" % currentTransactionString, listTmp);
0679                     IFOK(err) {
0680                         int nbItem = SKGServices::stringToInt(listTmp.at(1).at(0));
0681                         if (nbItem == 0) {
0682                             // Optimization is needed
0683                             // Get non hidden messages
0684                             SKGMessageList messages;
0685                             getMessages(getCurrentTransaction(), messages, false);
0686 
0687                             // Delete current transaction
0688                             err = executeSqliteOrder("DELETE FROM doctransaction WHERE id=" % currentTransactionString);
0689 
0690                             int nb = messages.count();
0691                             for (int i = 0; i < nb; ++i) {
0692                                 m_unTransactionnalMessages.push_back(messages.at(i));
0693                             }
0694                         }
0695                     }
0696                 }
0697 
0698                 // Optimization 2: remove duplicate orders
0699                 IFOK(err) {
0700                     QString wc = "DELETE FROM doctransactionitem WHERE id IN "
0701                                  "(SELECT a.id FROM doctransactionitem a INDEXED BY idx_doctransactionitem_optimization, doctransactionitem b INDEXED BY idx_doctransactionitem_optimization "
0702                                  "WHERE a.rd_doctransaction_id=" % currentTransactionString % " AND b.rd_doctransaction_id=" % currentTransactionString %
0703                                  " AND a.i_object_id=b.i_object_id AND a.t_object_table=b.t_object_table AND b.t_action=a.t_action AND b.t_sqlorder=a.t_sqlorder AND a.id>b.id );";
0704                     err = executeSqliteOrder(wc);
0705                 }
0706 
0707                 // Get current transaction information to be able to emit envent in case of SKG_UNDO_MAX_DEPTH=0
0708                 IFOK(err) {
0709                     err = this->getDistinctValues(QStringLiteral("doctransactionitem"),
0710                                                   QStringLiteral("t_object_table"),
0711                                                   "rd_doctransaction_id=" % currentTransactionString,
0712                                                   listModifiedTables);
0713                 }
0714 
0715                 // Remove oldest transaction
0716                 IFOK(err) {
0717                     QString maxdepthstring = getParameter(QStringLiteral("SKG_UNDO_MAX_DEPTH"));
0718                     if (maxdepthstring.isEmpty()) {
0719                         maxdepthstring = QStringLiteral("-1");
0720                     }
0721                     int maxdepth = SKGServices::stringToInt(maxdepthstring);
0722                     if (maxdepth >= 0) {
0723                         err = executeSqliteOrder("delete from doctransaction where id in (select id from doctransaction limit max(0,((select count(1) from doctransaction)-(" % maxdepthstring % "))))");
0724                     }
0725                 }
0726 
0727                 // Remove SKGDocument::REDO transactions if we are not in a undo / redo transaction
0728                 if (d->m_inundoRedoTransaction == 0) {
0729                     int i = 0;
0730                     while (((i = getTransactionToProcess(SKGDocument::REDO)) != 0) && !err) {
0731                         err = executeSqliteOrder("delete from doctransaction where id=" % SKGServices::intToString(i));
0732                     }
0733                 }
0734 
0735                 // Commit the transaction
0736                 IFOK(err) {
0737                     err = executeSqliteOrder(QStringLiteral("COMMIT;"));
0738                 }
0739             }
0740 
0741             // clean cache sql (must be done before event emit)
0742             d->m_cacheSql->clear();
0743 
0744             if (!succeedded || err) {
0745                 // Rollback the transaction
0746                 SKGError err2 = executeSqliteOrder(QStringLiteral("ROLLBACK;"));
0747                 // delete the transaction
0748                 IFOKDO(err2, executeSqliteOrder("delete from doctransaction where id=" % currentTransactionString))
0749 
0750                 if (err2) {
0751                     err.addError(err2.getReturnCode(), err2.getMessage());
0752                 }
0753             } else {
0754                 // For better performance, events are submitted only for the first recursive undo
0755                 if (Q_UNLIKELY(d->m_inundoRedoTransaction <= 1)) {
0756                     // Is it a light transaction?
0757                     bool lightTransaction = (getCachedValue(QStringLiteral("SKG_REFRESH_VIEW")) != QStringLiteral("Y"));
0758 
0759                     // Emit modification events
0760                     QStringList tablesRefreshed;
0761                     tablesRefreshed.reserve(listModifiedTables.count());
0762                     for (const auto& table : qAsConst(listModifiedTables)) {
0763                         Q_EMIT tableModified(table, getCurrentTransaction(), lightTransaction);
0764                         tablesRefreshed.push_back(table);
0765                     }
0766 
0767                     // Remove temporary transaction if needed
0768                     IFOKDO(err, executeSqliteOrder(QStringLiteral("delete from doctransaction where t_name LIKE '#INTERNAL#%';")))
0769 
0770                     Q_EMIT tableModified(QStringLiteral("doctransaction"), getCurrentTransaction(), lightTransaction);
0771                     Q_EMIT tableModified(QStringLiteral("doctransactionitem"), getCurrentTransaction(), lightTransaction);
0772 
0773                     // WARNING: list is modified during treatement
0774                     for (int i = 0; !err && i < listModifiedTables.count(); ++i) {
0775                         QString table = listModifiedTables.at(i);
0776                         QStringList toAdd = getImpactedViews(table);
0777                         int nbToAdd = toAdd.count();
0778                         for (int j = 0; !err &&  j < nbToAdd; ++j) {
0779                             const QString& toAddTable = toAdd.at(j);
0780                             if (!listModifiedTables.contains(toAddTable)) {
0781                                 // Compute materialized view of modified table
0782                                 if (!lightTransaction) {
0783                                     err = computeMaterializedViews(toAddTable);
0784                                 }
0785                                 listModifiedTables.push_back(toAddTable);
0786                             }
0787                         }
0788                     }
0789 
0790                     // Emit events
0791                     for (int i = tablesRefreshed.count(); i < listModifiedTables.count(); ++i) {
0792                         Q_EMIT tableModified(listModifiedTables.at(i), 0, lightTransaction);
0793                     }
0794 
0795                     Q_EMIT transactionSuccessfullyEnded(getCurrentTransaction());
0796                 }
0797             }
0798 
0799             // clean cache
0800             d->m_cache.clear();
0801 
0802             d->m_currentTransaction = 0;
0803 
0804             if (Q_LIKELY(qobject_cast<QGuiApplication*>(qApp) != nullptr)) {   // clazy:excludeall=unneeded-cast
0805                 QApplication::restoreOverrideCursor();
0806             }
0807         }
0808     }
0809 
0810     IFOK(err) {
0811         err = errOverwritten;
0812     }
0813     return err;
0814 }
0815 
0816 SKGError SKGDocument::removeAllTransactions()
0817 {
0818     SKGError err;
0819     SKGTRACEINFUNCRC(10, err)
0820     // Check if a transaction is still opened
0821     err = checkExistingTransaction();
0822     IFOK(err) err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "Remove of transactions is forbidden inside a transaction"));
0823     else {
0824         err = SKGDocument::beginTransaction(QStringLiteral("#INTERNAL#"));
0825         IFOKDO(err, executeSqliteOrder(QStringLiteral("delete from doctransaction")))
0826         SKGENDTRANSACTION(this,  err)
0827 
0828         // Force the save
0829         d->m_lastSavedTransaction = -1;
0830     }
0831     return err;
0832 }
0833 
0834 SKGError SKGDocument::computeMaterializedViews(const QString& iTable) const
0835 {
0836     SKGError err;
0837     SKGTRACEINFUNCRC(5, err)
0838 
0839     // Compute additional where clause
0840     QStringList tables;
0841     if (d->m_MaterializedViews.contains(iTable)) {
0842         tables = d->m_MaterializedViews[iTable];
0843     } else {
0844         QString wc;
0845         if (!iTable.isEmpty()) {
0846             QString t = iTable;
0847             if (t.startsWith(QLatin1String("v_"))) {
0848                 t.replace(0, 2, QStringLiteral("vm_"));
0849             }
0850             wc = " AND name='" % t % '\'';
0851         }
0852 
0853         // Get list of materialized table
0854         err = getDistinctValues(QStringLiteral("sqlite_master"), QStringLiteral("name"), "type='table' AND name LIKE 'vm_%' " % wc, tables);
0855         d->m_MaterializedViews[iTable] = tables;
0856     }
0857 
0858     // Refresh tables
0859     int nb = tables.count();
0860     for (int i = 0; !err && i < nb; ++i) {
0861         const QString& table = tables.at(i);
0862         QString view = table;
0863         view.replace(0, 3, QStringLiteral("v_"));
0864 
0865         // Remove previous table
0866         {
0867             SKGTRACEINRC(5, "SKGDocument::computeMaterializedViews-drop-" % table, err)
0868             err = executeSqliteOrder("DROP TABLE IF EXISTS " % table);
0869         }
0870         {
0871             // Recreate table
0872             SKGTRACEINRC(5, "SKGDocument::computeMaterializedViews-create-" % table, err)
0873             IFOKDO(err, executeSqliteOrder("CREATE TABLE " % table % " AS SELECT * FROM " % view))
0874         }
0875     }
0876 
0877     return err;
0878 }
0879 
0880 SKGError SKGDocument::sendMessage(const QString& iMessage, MessageType iMessageType, const QString& iAction)
0881 {
0882     SKGError err;
0883     SKGTRACEINFUNCRC(10, err)
0884     // Associate message with transaction
0885     if (!checkExistingTransaction()) {
0886         SKGObjectBase msg(this, QStringLiteral("doctransactionmsg"));
0887         err = msg.setAttribute(QStringLiteral("rd_doctransaction_id"), SKGServices::intToString(getCurrentTransaction()));
0888         IFOKDO(err, msg.setAttribute(QStringLiteral("t_message"), iMessage))
0889         IFOKDO(err, msg.setAttribute(QStringLiteral("t_type"), iMessageType == SKGDocument::Positive ? QStringLiteral("P") :
0890                                      iMessageType == SKGDocument::Information ? QStringLiteral("I") :
0891                                      iMessageType == SKGDocument::Warning ? QStringLiteral("W") :
0892                                      iMessageType == SKGDocument::Error ? QStringLiteral("E") : QStringLiteral("H")))
0893         IFOKDO(err, msg.save())
0894     }
0895 
0896     if (checkExistingTransaction() || !iAction.isEmpty()) {
0897         // Addition message in global variable in case of no transaction opened
0898         bool found = false;
0899         for (const auto& m : qAsConst(m_unTransactionnalMessages)) {
0900             if (m.Text == iMessage) {
0901                 found = true;
0902             }
0903         }
0904         if (iMessageType != SKGDocument::Hidden && !found) {
0905             SKGMessage m;
0906             m.Text = iMessage;
0907             m.Type = iMessageType;
0908             m.Action = iAction;
0909             m_unTransactionnalMessages.push_back(m);
0910         }
0911     }
0912     return err;
0913 }
0914 
0915 SKGError SKGDocument::removeMessages(int iIdTransaction)
0916 {
0917     SKGError err;
0918     SKGTRACEINFUNCRC(10, err)
0919 
0920     if (!checkExistingTransaction()) {
0921         err = executeSqliteOrder("DELETE FROM doctransactionmsg WHERE rd_doctransaction_id=" % SKGServices::intToString(iIdTransaction));
0922     }
0923 
0924     m_unTransactionnalMessages.clear();
0925     return err;
0926 }
0927 
0928 SKGError SKGDocument::getMessages(int iIdTransaction, SKGMessageList& oMessages, bool iAll)
0929 {
0930     SKGError err;
0931     SKGTRACEINFUNCRC(10, err)
0932     oMessages = m_unTransactionnalMessages;
0933 
0934     SKGStringListList listTmp;
0935     err = executeSelectSqliteOrder(
0936               QStringLiteral("SELECT t_message, t_type FROM doctransactionmsg WHERE ") %
0937               (iAll ? "" : "t_type<>'H' AND ") %
0938               "rd_doctransaction_id=" %
0939               SKGServices::intToString(iIdTransaction) %
0940               " ORDER BY id ASC",
0941               listTmp);
0942 
0943     int nb = listTmp.count();
0944     for (int i = 1; !err && i < nb ; ++i) {
0945         QString msg = listTmp.at(i).at(0);
0946         QString type = listTmp.at(i).at(1);
0947         bool found = false;
0948         for (const auto& m : qAsConst(m_unTransactionnalMessages)) {
0949             if (m.Text == msg) {
0950                 found = true;
0951             }
0952         }
0953         if (!found) {
0954             SKGMessage m;
0955             m.Text = msg;
0956             m.Type = type == QStringLiteral("P") ? SKGDocument::Positive : type == QStringLiteral("I") ? SKGDocument::Information : type == QStringLiteral("W") ? SKGDocument::Warning : type == QStringLiteral("E") ? SKGDocument::Error : SKGDocument::Hidden;
0957             oMessages.push_back(m);
0958         }
0959     }
0960 
0961     m_unTransactionnalMessages.clear();
0962     return err;
0963 }
0964 
0965 SKGError SKGDocument::getModifications(int iIdTransaction, SKGObjectModificationList& oModifications) const
0966 {
0967     SKGError err;
0968     SKGTRACEINFUNCRC(10, err)
0969     oModifications.clear();
0970 
0971     SKGStringListList listTmp;
0972     err = executeSelectSqliteOrder(
0973               "SELECT i_object_id,t_object_table,t_action FROM doctransactionitem WHERE rd_doctransaction_id=" %
0974               SKGServices::intToString(iIdTransaction) %
0975               " ORDER BY id ASC",
0976               listTmp);
0977     int nb = listTmp.count();
0978     for (int i = 1; !err && i < nb ; ++i) {
0979         SKGObjectModification mod;
0980         mod.id = SKGServices::stringToInt(listTmp.at(i).at(0));
0981         mod.table = listTmp.at(i).at(1);
0982         QString type = listTmp.at(i).at(2);
0983         mod.type = (type == QStringLiteral("D") ? I : (type == QStringLiteral("I") ? D : U));  // Normal because in database we have to sql order to go back.
0984         mod.uuid = listTmp.at(i).at(0) % '-' % mod.table;
0985 
0986         oModifications.push_back(mod);
0987     }
0988     return err;
0989 }
0990 
0991 QStringList SKGDocument::getImpactedViews(const QString& iTable) const
0992 {
0993     SKGTRACEINFUNC(10)
0994     if (Q_UNLIKELY(d->m_ImpactedViews.isEmpty())) {
0995         // Get list of tables and views
0996         QStringList tables;
0997         SKGStringListList result;
0998         executeSelectSqliteOrder(QStringLiteral("SELECT tbl_name FROM sqlite_master WHERE tbl_name NOT LIKE '%_delete' AND type IN ('table', 'view')"), result);
0999         int nb = result.count();
1000         tables.reserve(nb);
1001         for (int i = 1; i < nb; ++i) {
1002             tables.push_back(result.at(i).at(0));
1003         }
1004 
1005         // First computation
1006         executeSelectSqliteOrder(QStringLiteral("SELECT tbl_name, sql FROM sqlite_master WHERE tbl_name NOT LIKE '%_delete' AND type='view'"), result);
1007         nb = result.count();
1008         for (int i = 1; i < nb; ++i) {
1009             const QStringList& line = result.at(i);
1010             const QString& name = line.at(0);
1011             const QString& sql = line.at(1);
1012 
1013             QStringList words = SKGServices::splitCSVLine(sql, ' ', false);
1014             words.push_back(QStringLiteral("parameters"));
1015             int nbWords = words.count();
1016             for (int j = 0; j < nbWords; ++j) {
1017                 QString word = words.at(j);
1018                 word = word.remove(',');
1019                 if (word.startsWith(QLatin1String("vm_"))) {
1020                     word.replace(0, 3, QStringLiteral("v_"));
1021                 }
1022                 if (word != name && tables.contains(word, Qt::CaseInsensitive)) {
1023                     QStringList l = d->m_ImpactedViews.value(word);
1024                     if (!l.contains(name)) {
1025                         l.push_back(name);
1026                     }
1027                     d->m_ImpactedViews[word] = l;
1028                 }
1029             }
1030         }
1031 
1032         // Now, we have some thing like this
1033         // d->m_ImpactedViews[A]={ B, C, D}
1034         // d->m_ImpactedViews[B]={ E, F}
1035         // We must build d->m_ImpactedViews[A]={ B, C, D, E, F}
1036         QStringList keys = d->m_ImpactedViews.keys();
1037         for (const auto& k : qAsConst(keys)) {
1038             QStringList l = d->m_ImpactedViews.value(k);
1039             for (int i = 0; i < l.count(); ++i) {  // Warning: the size of l will change in the loop
1040                 QString item = l.at(i);
1041                 if (d->m_ImpactedViews.contains(item)) {
1042                     // No qAsConst here, item is already const
1043                     for (const auto& name : d->m_ImpactedViews.value(item)) {
1044                         if (!l.contains(name)) {
1045                             l.push_back(name);
1046                         }
1047                     }
1048                 }
1049             }
1050             d->m_ImpactedViews[k] = l;
1051         }
1052     }
1053     return d->m_ImpactedViews.value(iTable);
1054 }
1055 
1056 SKGError SKGDocument::groupTransactions(int iFrom, int iTo)
1057 {
1058     SKGError err;
1059     SKGTRACEINFUNCRC(5, err)
1060 
1061     ++d->m_inundoRedoTransaction;  // It is a kind of undo redo
1062 
1063     // Check if a transaction is still opened
1064     err = checkExistingTransaction();
1065     IFOK(err) err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "Creation of a group of transactions is forbidden inside a transaction"));
1066     else {
1067         int iidMaster = qMax(iFrom, iTo);
1068         QString smin = SKGServices::intToString(qMin(iFrom, iTo));
1069         QString smax = SKGServices::intToString(iidMaster);
1070 
1071         // Get transaction
1072         SKGStringListList transactions;
1073         err = executeSelectSqliteOrder(
1074                   QStringLiteral("SELECT id, t_name, t_mode, i_parent FROM doctransaction WHERE id BETWEEN ") %
1075                   smin % " AND " %
1076                   smax % " ORDER BY id ASC",
1077                   transactions);
1078 
1079         // Check and get main parameter for the group
1080         int nb = transactions.count();
1081         QString transactionMode;
1082         QString communParent;
1083         QString name;
1084         for (int i = 1; !err && i < nb; ++i) {  // We forget header
1085             const QStringList& transaction = transactions.at(i);
1086             const QString& mode = transaction.at(2);
1087             if (!name.isEmpty()) {
1088                 name += ',';
1089             }
1090             name += transaction.at(1);
1091 
1092             if (!transactionMode.isEmpty() && mode != transactionMode) {
1093                 err = SKGError(ERR_INVALIDARG, QStringLiteral("Undo and Redo transactions cannot be grouped"));
1094             } else {
1095                 transactionMode = mode;
1096             }
1097 
1098             if (i == 1) {
1099                 communParent = transaction.at(3);
1100             }
1101         }
1102 
1103         // Group
1104         IFOK(err) {
1105             err = SKGDocument::beginTransaction(QStringLiteral("#INTERNAL#"));
1106             // Group items
1107             IFOKDO(err, executeSqliteOrder(
1108                        QStringLiteral("UPDATE doctransactionitem set rd_doctransaction_id=") %
1109                        smax %
1110                        " where rd_doctransaction_id BETWEEN " %
1111                        smin % " AND " % smax))
1112             IFOKDO(err, executeSqliteOrder(
1113                        QStringLiteral("UPDATE doctransaction set i_parent=") %
1114                        communParent %
1115                        ", t_name='" % SKGServices::stringToSqlString(name) %
1116                        "' where id=" % smax))
1117 
1118             IFOKDO(err, executeSqliteOrder(
1119                        QStringLiteral("DELETE FROM doctransaction WHERE id BETWEEN ") %
1120                        smin % " AND " % SKGServices::intToString(qMax(iFrom, iTo) - 1)))
1121 
1122             SKGENDTRANSACTION(this,  err)
1123         }
1124     }
1125 
1126     --d->m_inundoRedoTransaction;
1127     return err;
1128 }
1129 
1130 SKGError SKGDocument::undoRedoTransaction(UndoRedoMode iMode)
1131 {
1132     SKGError err;
1133     SKGTRACEINFUNCRC(5, err)
1134     // Check if a transaction is still opened
1135     err = checkExistingTransaction();
1136     IFOK(err) err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "Undo / Redo is forbidden inside a transaction"));
1137     else {
1138         if (iMode == SKGDocument::UNDOLASTSAVE) {
1139             // Create group
1140             SKGStringListList transactions;
1141             err = executeSelectSqliteOrder(
1142                       QStringLiteral("SELECT id, t_savestep FROM doctransaction WHERE t_mode='U' ORDER BY id DESC"),
1143                       transactions);
1144             int nb = transactions.count();
1145             int min = 0;
1146             int max = 0;
1147             for (int i = 1; !err && i < nb; ++i) {
1148                 const QStringList& transaction = transactions.at(i);
1149                 if (i == 1) {
1150                     max = SKGServices::stringToInt(transaction.at(0));
1151                 }
1152                 if (i != 1 && transaction.at(1) == QStringLiteral("Y")) {
1153                     break;
1154                 }
1155                 min = SKGServices::stringToInt(transaction.at(0));
1156             }
1157             if (min == 0) {
1158                 min = max;
1159             }
1160             if (!err && min != max && min != 0) {
1161                 err = groupTransactions(min, max);
1162             }
1163         } else {
1164             err = SKGError();  // To ignore error generated by checkExistingTransaction.
1165         }
1166 
1167         // Get ID of the transaction to undo
1168         IFOK(err) {
1169             QString name;
1170             bool saveStep = false;
1171             QDateTime date;
1172             bool refreshViews;
1173             int id = getTransactionToProcess(iMode, &name, &saveStep, &date, &refreshViews);
1174             if (id == 0) {
1175                 // No transaction found ==> generate an error
1176                 err = SKGError(ERR_INVALIDARG, QStringLiteral("No transaction found. Undo / Redo impossible."));
1177             } else {
1178                 // Undo transaction
1179                 SKGTRACEL(5) << "Undoing transaction [" << id << "]- [" << name << "]…" << SKGENDL;
1180                 SKGStringListList listSqlOrder;
1181                 err = executeSelectSqliteOrder(
1182                           "SELECT t_sqlorder FROM doctransactionitem WHERE rd_doctransaction_id=" %
1183                           SKGServices::intToString(id) %
1184                           " ORDER BY id DESC",
1185                           listSqlOrder);
1186                 IFOK(err) {
1187                     int nb = listSqlOrder.count();
1188                     err = SKGDocument::beginTransaction(name, nb + 3, date, refreshViews);
1189                     IFOK(err) {
1190                         ++d->m_inundoRedoTransaction;  // Because we will be in a undo/redo transaction
1191                         // Normal the first element is ignored because it is the header
1192                         for (int i = 1; !err && i < nb ; ++i) {
1193                             err = executeSqliteOrder(listSqlOrder.at(i).at(0));
1194 
1195                             IFOKDO(err, stepForward(i))
1196                         }
1197 
1198                         IFOK(err) {
1199                             // Set the NEW transaction in redo mode
1200                             int lastredo = getTransactionToProcess((iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE  ? SKGDocument::REDO : SKGDocument::UNDO));
1201                             int newredo = getTransactionToProcess(iMode);
1202                             IFOKDO(err, executeSqliteOrder(
1203                                        QStringLiteral("UPDATE doctransaction set t_mode=") %
1204                                        (iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE ? QStringLiteral("'R'") : QStringLiteral("'U'")) %
1205                                        ", i_parent=" %
1206                                        SKGServices::intToString(lastredo) %
1207                                        " where id=" % SKGServices::intToString(newredo)))
1208                             IFOKDO(err, stepForward(nb))
1209 
1210                             // Move messages from previous transaction to new one
1211                             IFOKDO(err, executeSqliteOrder(
1212                                        "UPDATE doctransactionmsg set rd_doctransaction_id=" %
1213                                        SKGServices::intToString(getCurrentTransaction()) %
1214                                        " where rd_doctransaction_id=" %
1215                                        SKGServices::intToString(id)))
1216                             IFOKDO(err, stepForward(nb + 1))
1217 
1218                             // delete treated transaction
1219                             IFOKDO(err, executeSqliteOrder(
1220                                        "DELETE from doctransaction where id="
1221                                        % SKGServices::intToString(id)))
1222                             IFOKDO(err, stepForward(nb + 2))
1223 
1224                             // Check that new transaction has exactly the same number of item
1225                             /* IFOK (err) {
1226                                      SKGStringListList listSqlOrder;
1227                                      err=executeSelectSqliteOrder(
1228                                                      "SELECT count(1) FROM doctransactionitem WHERE rd_doctransaction_id=" %
1229                                                      SKGServices::intToString(getCurrentTransaction()),
1230                                                      listSqlOrder);
1231                                      if (!err && SKGServices::stringToInt(listSqlOrder.at(1).at(0))!=nb-1) {
1232                                              err=SKGError(ERR_ABORT, i18nc("Error message", "Invalid number of item after undo/redo. Expected (%1) != Result (%2)",nb-1,listSqlOrder.at(1).at(0)));
1233                                      }
1234                              }*/
1235 
1236                             IFOKDO(err, stepForward(nb + 3))
1237                         }
1238 
1239                         SKGENDTRANSACTION(this,  err)
1240                         --d->m_inundoRedoTransaction;  // We left the undo / redo transaction
1241                     }
1242                 }
1243             }
1244         }
1245     }
1246 
1247     return err;
1248 }
1249 
1250 int SKGDocument::getDepthTransaction() const
1251 {
1252     return d->m_nbStepForTransaction.size();
1253 }
1254 
1255 int SKGDocument::getNbTransaction(UndoRedoMode iMode) const
1256 {
1257     SKGTRACEINFUNC(10)
1258     int output = 0;
1259     if (Q_LIKELY(getMainDatabase())) {
1260         QString sqlorder = QStringLiteral("select count(1) from doctransaction where t_mode='");
1261         sqlorder += (iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE ? QStringLiteral("U") : QStringLiteral("R"));
1262         sqlorder += '\'';
1263         QSqlQuery query = getMainDatabase()->exec(sqlorder);
1264         if (query.next()) {
1265             output = query.value(0).toInt();
1266         }
1267     }
1268     return output;
1269 }
1270 
1271 int SKGDocument::getTransactionToProcess(UndoRedoMode iMode, QString* oName, bool* oSaveStep, QDateTime* oDate, bool* oRefreshViews) const
1272 {
1273     SKGTRACEINFUNC(10)
1274     // initialisation
1275     int output = 0;
1276     if (oName != nullptr) {
1277         *oName = QLatin1String("");
1278     }
1279     if (Q_LIKELY(getMainDatabase())) {
1280         QString sqlorder = QStringLiteral("select A.id , A.t_name, A.t_savestep, A.d_date, A.t_refreshviews from doctransaction A where "
1281                                           "NOT EXISTS(select 1 from doctransaction B where B.i_parent=A.id) "
1282                                           "and A.t_mode='");
1283         sqlorder += (iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE ? QStringLiteral("U") : QStringLiteral("R"));
1284         sqlorder += '\'';
1285         QSqlQuery query = getMainDatabase()->exec(sqlorder);
1286         if (query.next()) {
1287             output = query.value(0).toInt();
1288             if (oName != nullptr) {
1289                 *oName = query.value(1).toString();
1290             }
1291             if (oSaveStep != nullptr) {
1292                 *oSaveStep = (query.value(2).toString() == QStringLiteral("Y"));
1293             }
1294             if (oDate != nullptr) {
1295                 *oDate = SKGServices::stringToTime(query.value(3).toString());
1296             }
1297             if (oRefreshViews != nullptr) {
1298                 *oRefreshViews = (query.value(4).toString() == QStringLiteral("Y"));
1299             }
1300         }
1301     }
1302     return output;
1303 }
1304 
1305 int SKGDocument::getCurrentTransaction() const
1306 {
1307     SKGTRACEINFUNC(10)
1308     return d->m_currentTransaction;
1309 }
1310 
1311 QString SKGDocument::getPassword() const
1312 {
1313     if (!d->m_password_got) {
1314         d->m_password = getParameter(QStringLiteral("SKG_PASSWORD"));
1315         d->m_password_got = true;
1316     }
1317     return d->m_password;
1318 }
1319 
1320 SKGError SKGDocument::changePassword(const QString& iNewPassword)
1321 {
1322     SKGError err;
1323     SKGTRACEINFUNCRC(10, err)
1324     IFOK(checkExistingTransaction()) err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "Change password is forbidden inside a transaction"));
1325     else {
1326         IFOKDO(err, executeSqliteOrder("PRAGMA REKEY = '" % SKGServices::stringToSqlString(iNewPassword.isEmpty() ? QStringLiteral("DEFAULTPASSWORD") : iNewPassword) % "'"))
1327         IFOKDO(err, beginTransaction(QStringLiteral("#INTERNAL#"), 0, QDateTime::currentDateTime(), false))
1328         IFOKDO(err, setParameter(QStringLiteral("SKG_PASSWORD"), iNewPassword))
1329         IFOKDO(err, setParameter(QStringLiteral("SKG_PASSWORD_LASTUPDATE"), SKGServices::dateToSqlString(QDate::currentDate())))
1330         IFOKDO(err, sendMessage(iNewPassword.isEmpty() ? i18nc("Inform the user that the password protection was removed", "The document password has been removed.") :
1331                                 i18nc("Inform the user that the password was successfully changed", "The document password has been modified."), SKGDocument::Positive))
1332         SKGENDTRANSACTION(this,  err)
1333 
1334         // Force the save
1335         IFOK(err) {
1336             d->m_lastSavedTransaction = -1;
1337 
1338             d->m_password = iNewPassword;
1339             d->m_password_got = true;
1340 
1341             // Close all thread connection
1342             auto conNameMainConnection = getMainDatabase()->connectionName();
1343             const auto conNames = QSqlDatabase::connectionNames();
1344             for (const auto& conName : conNames) {
1345                 if (conName.startsWith(conNameMainConnection % "_")) {
1346                     /* NO NEED
1347                     {
1348                         auto con = QSqlDatabase::database(conName, false);
1349                         con.close();
1350                     }*/
1351                     QSqlDatabase::removeDatabase(conName);
1352                 }
1353             }
1354         }
1355     }
1356     return err;
1357 }
1358 
1359 SKGError SKGDocument::setLanguage(const QString& iLanguage)
1360 {
1361     SKGError err;
1362     SKGTRACEINFUNCRC(5, err)
1363     QString previousLanguage = getParameter(QStringLiteral("SKG_LANGUAGE"));
1364     if (previousLanguage != iLanguage) {
1365         // Save language into the document
1366         IFOKDO(err, beginTransaction(QStringLiteral("#INTERNAL#"), 0, QDateTime::currentDateTime(), false))
1367         IFOKDO(err, setParameter(QStringLiteral("SKG_LANGUAGE"), iLanguage))
1368 
1369         // Migrate view for new language
1370         IFOKDO(err, refreshViewsIndexesAndTriggers())
1371 
1372         // close temporary transaction
1373         SKGENDTRANSACTION(this,  err)
1374     }
1375     return err;
1376 }
1377 
1378 SKGError SKGDocument::initialize()
1379 {
1380     SKGError err;
1381     SKGTRACEINFUNCRC(5, err)
1382     err = load(QLatin1String(""), QLatin1String(""));
1383     return err;
1384 }
1385 
1386 SKGError SKGDocument::recover(const QString& iName, const QString& iPassword, QString& oRecoveredFile)
1387 {
1388     SKGError err;
1389     SKGTRACEINFUNCRC(5, err)
1390     SKGTRACEL(10) << "Input parameter [name]=[" << iName << ']' << SKGENDL;
1391 
1392     QString sqliteFile = QString(iName % "_recovered.sqlite").replace(QStringLiteral(".skg_"), QStringLiteral("_"));
1393     oRecoveredFile = QString(iName % "_recovered.skg").replace(QStringLiteral(".skg_"), QStringLiteral("_"));
1394     bool mode;
1395     err = SKGServices::cryptFile(iName, sqliteFile, iPassword, false, getDocumentHeader(), mode);
1396     IFOK(err) {
1397         QFile(oRecoveredFile).remove();
1398         QString cmd = "echo .dump | sqlcipher \"" % sqliteFile % "\" | sed -e 's/ROLLBACK; -- due to errors/COMMIT;/g' | sqlcipher \"" % oRecoveredFile % '"';
1399         QProcess p;
1400         p.start(QStringLiteral("sh"), QStringList() << QStringLiteral("-c") << cmd);
1401         if (!p.waitForFinished(1000 * 60 * 2) || p.exitCode() != 0) {
1402             err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message",  "The following command line failed with code %2:\n'%1'", cmd, p.exitCode()));
1403         }
1404 
1405         // Try to load the recovered file
1406         IFOKDO(err, load(oRecoveredFile, QLatin1String("")))
1407         IFOK(err) {
1408             SKGBEGINTRANSACTION(*this, i18nc("Noun", "Recovery"), err)
1409             IFOKDO(err, refreshViewsIndexesAndTriggers(true))
1410         }
1411         IFOKDO(err, save())
1412 
1413         // Reset the current document
1414         initialize();
1415 
1416         // Clean useless file
1417         IFOK(err) {
1418             // We keep only the recovered
1419             QFile(sqliteFile).remove();
1420         } else {
1421             // We keep the sqlite file in case of
1422             QFile(oRecoveredFile).remove();
1423             err.addError(ERR_FAIL, i18nc("Error message", "Impossible to recover this file"));
1424         }
1425     }
1426 
1427     return err;
1428 }
1429 
1430 SKGError SKGDocument::load(const QString& iName, const QString& iPassword, bool iRestoreTmpFile, bool iForceReadOnly)
1431 {
1432     // Close previous document
1433     SKGError err;
1434     SKGTRACEINFUNCRC(5, err)
1435     SKGTRACEL(10) << "Input parameter [name]=[" << iName << ']' << SKGENDL;
1436     SKGTRACEL(10) << "Input parameter [iRestoreTmpFile]=[" << (iRestoreTmpFile ? "TRUE" : "FALSE") << ']' << SKGENDL;
1437     SKGTRACEL(10) << "Input parameter [iForceReadOnly]=[" << (iForceReadOnly ? "TRUE" : "FALSE") << ']' << SKGENDL;
1438 
1439     d->m_lastSavedTransaction = -1;  // To avoid double event emission
1440     d->m_modeSQLCipher = true;
1441     d->m_blockEmits = true;
1442     err = close();
1443     d->m_blockEmits = false;
1444     IFOK(err) {
1445         if (!iName.isEmpty()) {
1446             // File exist
1447             QFileInfo fi(iName);
1448             d->m_modeReadOnly = iForceReadOnly || !fi.permission(QFile::WriteUser);
1449 
1450             // Temporary file
1451             d->m_temporaryFile = SKGDocument::getTemporaryFile(iName, d->m_modeReadOnly);
1452             bool temporaryFileExisting = QFile(d->m_temporaryFile).exists();
1453             SKGTRACEL(10) << "Temporary file: [" << d->m_temporaryFile << ']' << SKGENDL;
1454             SKGTRACEL(10) << "Temporary file existing: [" << (temporaryFileExisting ? "TRUE" : "FALSE") << ']' << SKGENDL;
1455             if (!iRestoreTmpFile || !temporaryFileExisting) {
1456                 SKGTRACEL(10) << "Create the temporary file" << SKGENDL;
1457                 QFile::remove(d->m_temporaryFile);  // Must remove it to be able to copy
1458                 err = SKGServices::cryptFile(iName, d->m_temporaryFile, iPassword, false, getDocumentHeader(), d->m_modeSQLCipher);
1459             } else {
1460                 SKGTRACEL(10) << "The temporary file is existing, try a restore but we must check if the file is password protected first" << SKGENDL;
1461                 // 249955: Check if password protected vvv
1462                 // Temporary file will be loaded but first we must check if original document is password protected
1463                 QString temporaryFile2 = d->m_temporaryFile % '2';
1464                 err = SKGServices::cryptFile(iName, temporaryFile2, iPassword, false, getDocumentHeader(), d->m_modeSQLCipher);
1465 
1466                 // Try an open to check if well descrypted
1467                 IFOK(err) {
1468                     QSqlDatabase tryOpen(QSqlDatabase::addDatabase(SQLDRIVERNAME, QStringLiteral("tryOpen")));
1469                     tryOpen.setDatabaseName(temporaryFile2);
1470                     if (!tryOpen.open()) {
1471                         // Set error message
1472                         QSqlError sqlErr = tryOpen.lastError();
1473                         err = SKGError(SQLLITEERROR + sqlErr.nativeErrorCode().toInt(), sqlErr.text());
1474                     }
1475                     if (d->m_modeSQLCipher) {
1476                         IFOKDO(err, SKGServices::executeSqliteOrder(tryOpen, "PRAGMA KEY = '" % SKGServices::stringToSqlString(iPassword.isEmpty() ? QStringLiteral("DEFAULTPASSWORD") : iPassword) % "'"))
1477                         IFKO(err) {
1478                             SKGTRACEL(10) << "Wrong installation of sqlcipher (doesn't support encryption)" << SKGENDL;
1479                             err = SKGError(ERR_ENCRYPTION, i18nc("Error message", "Wrong installation"));
1480                         }
1481 
1482                         // Migrate to the last version of SQLCipher
1483                         IFOKDO(err, SKGServices::executeSqliteOrder(tryOpen, QStringLiteral("PRAGMA cipher_migrate")))
1484 
1485                         // Test the password
1486                         IFOKDO(err, SKGServices::executeSqliteOrder(tryOpen, QStringLiteral("SELECT count(*) FROM sqlite_master")))
1487                         IFKO(err) {
1488                             SKGTRACEL(10) << "Wrong password in restore mode" << SKGENDL;
1489                             err = SKGError(ERR_ENCRYPTION, i18nc("Error message", "Wrong password"));
1490                         }
1491                     }
1492                     IFOKDO(err, SKGServices::executeSqliteOrder(tryOpen, QStringLiteral("PRAGMA synchronous = OFF")))
1493                 }
1494                 QSqlDatabase::removeDatabase(QStringLiteral("tryOpen"));
1495                 QFile::remove(temporaryFile2);
1496 
1497                 // To avoid deletion of temporary file during next try
1498                 IFKO(err) d->m_temporaryFile = QLatin1String("");
1499                 // 249955: Check if password protected ^^^
1500             }
1501 
1502             // Create file database
1503             IFOK(err) {
1504                 d->m_currentDatabase = QSqlDatabase::addDatabase(SQLDRIVERNAME, d->m_databaseIdentifier);
1505                 d->m_currentDatabase.setDatabaseName(d->m_temporaryFile);
1506                 if (!d->m_currentDatabase.open()) {
1507                     // Set error message
1508                     QSqlError sqlErr = d->m_currentDatabase.lastError();
1509                     err = SKGError(SQLLITEERROR + sqlErr.nativeErrorCode().toInt(), sqlErr.text());
1510                 }
1511 
1512                 d->m_directAccessDb = true;
1513                 if (QUrl::fromUserInput(iName).isLocalFile()) {
1514                     d->m_currentFileName = iName;
1515                 }
1516             }
1517         } else {
1518             // Temporary file
1519             d->m_temporaryFile = QDir::tempPath() % "/skg_" % QUuid::createUuid().toString() % ".skg";
1520 
1521             // Create memory database
1522             d->m_currentDatabase = QSqlDatabase::addDatabase(SQLDRIVERNAME, d->m_databaseIdentifier);
1523             d->m_currentDatabase.setConnectOptions(QStringLiteral("QSQLITE_OPEN_URI"));
1524             d->m_currentDatabase.setDatabaseName(QStringLiteral("file:") + d->m_databaseIdentifier + QStringLiteral("?mode=memory&cache=shared"));
1525             if (!d->m_currentDatabase.open()) {
1526                 // Set error message
1527                 QSqlError sqlErr = d->m_currentDatabase.lastError();
1528                 err = SKGError(SQLLITEERROR + sqlErr.nativeErrorCode().toInt(), sqlErr.text());
1529             }
1530 
1531             d->m_directAccessDb = false;
1532         }
1533         if (d->m_modeSQLCipher) {
1534             // This is an encrypted data base
1535             IFOKDO(err, executeSqliteOrder("PRAGMA KEY = '" % SKGServices::stringToSqlString(iPassword.isEmpty() ? QStringLiteral("DEFAULTPASSWORD") : iPassword) % "'"))
1536             IFKO(err) {
1537                 SKGTRACEL(10) << "Wrong installation of sqlcipher (doesn't support encryption)" << SKGENDL;
1538                 err = SKGError(ERR_ENCRYPTION, i18nc("Error message", "Wrong installation"));
1539             }
1540 
1541             // Migrate to the last version of SQLCipher
1542             IFOKDO(err, executeSqliteOrder(QStringLiteral("PRAGMA cipher_migrate")))
1543 
1544             // Test the password
1545             IFOKDO(err, executeSqliteOrder(QStringLiteral("SELECT count(*) FROM sqlite_master")))
1546             IFKO(err) {
1547                 SKGTRACEL(10) << "Wrong password on temporary file" << SKGENDL;
1548                 err = SKGError(ERR_ENCRYPTION, i18nc("Error message", "Wrong password"));
1549             }
1550         }
1551 
1552         // Check if the database is correct
1553         IFOK(err) {
1554             IFOKDO(err, executeSqliteOrder(QStringLiteral("PRAGMA journal_mode=MEMORY")))
1555             IFKO(err) {
1556                 err.addError(ERR_CORRUPTION, i18nc("Error message", "Oups, this file seems to be corrupted"));
1557             }
1558         }
1559 
1560         // Optimization
1561         QStringList optimization;
1562         optimization << QStringLiteral("PRAGMA case_sensitive_like=true")
1563                      << QStringLiteral("PRAGMA journal_mode=MEMORY")
1564                      << QStringLiteral("PRAGMA temp_store=MEMORY")
1565                      // << QStringLiteral("PRAGMA locking_mode=EXCLUSIVE")
1566                      << QStringLiteral("PRAGMA synchronous = OFF")
1567                      << QStringLiteral("PRAGMA legacy_alter_table=ON")  // For migration on sqlite >=3.25 (see https://sqlite.org/lang_altertable.html)
1568                      << QStringLiteral("PRAGMA recursive_triggers=true");
1569         IFOKDO(err, executeSqliteOrders(optimization))
1570 
1571         // Add custom sqlite functions
1572         IFOKDO(err, addSqliteAddon(getMainDatabase()))
1573 
1574         if (!d->m_directAccessDb) {
1575             // Create parameter and undo redo table
1576             /**
1577             * This constant is used to initialized the data model (table creation)
1578             */
1579             QStringList InitialDataModel;
1580 
1581             // ==================================================================
1582             // Table parameters
1583             InitialDataModel << QStringLiteral("CREATE TABLE parameters "
1584                                                "(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
1585                                                "t_uuid_parent TEXT NOT NULL DEFAULT '',"
1586                                                "t_name TEXT NOT NULL,"
1587                                                "t_value TEXT NOT NULL DEFAULT '',"
1588                                                "b_blob BLOB,"
1589                                                "d_lastmodifdate DATE NOT NULL DEFAULT CURRENT_TIMESTAMP,"
1590                                                "i_tmp INTEGER NOT NULL DEFAULT 0"
1591                                                ")")
1592 
1593                              // ==================================================================
1594                              // Table node
1595                              << "CREATE TABLE node ("
1596                              "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
1597                              "t_name TEXT NOT NULL DEFAULT '' CHECK (t_name NOT LIKE '%" % OBJECTSEPARATOR % "%'),"
1598                              "t_fullname TEXT,"
1599                              "t_icon TEXT DEFAULT '',"
1600                              "f_sortorder FLOAT,"
1601                              "t_autostart VARCHAR(1) DEFAULT 'N' CHECK (t_autostart IN ('Y', 'N')),"
1602                              "t_data TEXT,"
1603                              "rd_node_id INT CONSTRAINT fk_id REFERENCES node(id) ON DELETE CASCADE)"
1604 
1605                              // ==================================================================
1606                              // Table doctransaction
1607                              << QStringLiteral("CREATE TABLE doctransaction ("
1608                                                "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
1609                                                "t_name TEXT NOT NULL,"
1610                                                "t_mode VARCHAR(1) DEFAULT 'U' CHECK (t_mode IN ('U', 'R')),"
1611                                                "d_date DATE NOT NULL,"
1612                                                "t_savestep VARCHAR(1) DEFAULT 'N' CHECK (t_savestep IN ('Y', 'N')),"
1613                                                "t_refreshviews VARCHAR(1) DEFAULT 'Y' CHECK (t_refreshviews IN ('Y', 'N')),"
1614                                                "i_parent INTEGER)")
1615 
1616                              // ==================================================================
1617                              // Table doctransactionitem
1618                              << QStringLiteral("CREATE TABLE doctransactionitem ("
1619                                                "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
1620                                                "rd_doctransaction_id INTEGER NOT NULL,"
1621                                                "i_object_id INTEGER NOT NULL,"
1622                                                "t_object_table TEXT NOT NULL,"
1623                                                "t_action VARCHAR(1) DEFAULT 'I' CHECK (t_action IN ('I', 'U', 'D')),"
1624                                                "t_sqlorder TEXT NOT NULL DEFAULT '')")
1625 
1626                              << QStringLiteral("CREATE TABLE doctransactionmsg ("
1627                                                "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
1628                                                "rd_doctransaction_id INTEGER NOT NULL,"
1629                                                "t_message TEXT NOT NULL DEFAULT '',"
1630                                                "t_type VARCHAR(1) DEFAULT 'I' CHECK (t_type IN ('P', 'I', 'W', 'E', 'H')))");  // Positive, Information, Warning, Error, Hidden
1631 
1632             IFOKDO(err, executeSqliteOrders(InitialDataModel))
1633             IFOKDO(err, SKGDocument::refreshViewsIndexesAndTriggers())
1634         }
1635     }
1636 
1637     // migrate
1638     IFOK(err) {
1639         bool mig = false;
1640         err = migrate(mig);
1641 
1642         if (!err && getParameter(QStringLiteral("SKG_DATABASE_TYPE")) != SQLDRIVERNAME && !getPassword().isEmpty()) {
1643             err = sendMessage(i18nc("Information message", "This document is protected by a password but the database is still in SQLite mode.\nDo you know that the SQLCipher mode is more secured because even the temporary file is encrypted?"), SKGDocument::Warning, QStringLiteral("skg://migrate_sqlcipher"));
1644         }
1645         if (!err && getParameter(QStringLiteral("SKG_PASSWORD_LASTUPDATE")) == QLatin1String("") && !getPassword().isEmpty()) {
1646             err = sendMessage(i18nc("Information message", "A security hole has been detected and corrected on this version of the application. We strongly encourage you to change your password."), SKGDocument::Warning, QStringLiteral("skg://file_change_password"));
1647         }
1648 
1649         // To authorize manual repair of document in case of error during migration
1650         // the error is not caught if traces are activated
1651         if (err && (SKGTraces::SKGLevelTrace != 0)) {
1652             err = sendMessage(i18nc("Popup message", "The migration failed but the document has been loaded without error because debug mode is activated"), SKGDocument::Warning);
1653         }
1654 
1655         if (!err && mig && !iName.isEmpty()) {
1656             err = sendMessage(i18nc("The document has been upgraded to the latest Skrooge version format", "The document has been migrated"), SKGDocument::Positive);
1657         }
1658     }
1659 
1660     // Optimization
1661     IFOK(err) {
1662         d->m_lastSavedTransaction = getTransactionToProcess(SKGDocument::UNDO);
1663         executeSqliteOrder(QStringLiteral("ANALYZE"));
1664     }
1665 
1666     // Creation undo/redo triggers
1667     IFOKDO(err, createUndoRedoTemporaryTriggers())
1668 
1669     IFOK(err) {
1670         QString sqliteQtVersion = getParameter(QStringLiteral("SKG_SQLITE_LAST_VERSION"));
1671         QString sqliteSystemVersion(sqlite3_libversion());
1672         QProcess sqlite3Process;
1673         QString mode;
1674         sqlite3Process.start(QStringLiteral("sqlcipher"), QStringList() << QStringLiteral("-version"));
1675         mode = QStringLiteral("SQLCipher");
1676         if (sqlite3Process.waitForFinished()) {
1677             sqliteSystemVersion = SKGServices::splitCSVLine(sqlite3Process.readAll(), ' ').value(0);
1678         }
1679         SKGTRACEL(5) << "SQLite version of Qt        :" << sqliteQtVersion << SKGENDL;
1680         SKGTRACEL(5) << "SQLite version of the system:" << sqliteSystemVersion << SKGENDL;
1681         if (!sqliteQtVersion.isEmpty() && !sqliteSystemVersion.isEmpty() &&  sqliteQtVersion != sqliteSystemVersion) {
1682             QString message = i18nc("Error message", "This application can not run correctly because the %3 version of the system (%1) is not aligned with the %4 version embedded in Qt (%2). You should rebuild Qt with the option -system-sqlite.", sqliteSystemVersion, sqliteQtVersion, mode, mode);
1683             err = sendMessage(message, Warning);
1684             SKGTRACE << "WARNING:" << message << SKGENDL;
1685         }
1686     }
1687 
1688     if (err && !iName.isEmpty()) {
1689         close();
1690     } else {
1691         // Send event
1692         d->m_uniqueIdentifier = QUuid::createUuid().toString();
1693         d->m_password = iPassword;
1694         d->m_password_got = true;
1695         Q_EMIT tableModified(QLatin1String(""), 0, false);
1696         Q_EMIT modified();
1697     }
1698 
1699     return err;
1700 }
1701 
1702 bool SKGDocument::isReadOnly() const
1703 {
1704     return d->m_modeReadOnly;
1705 }
1706 
1707 bool SKGDocument::isFileModified() const
1708 {
1709     // Get last executed transaction
1710     int last = getTransactionToProcess(SKGDocument::UNDO);
1711     //  if (nbStepForTransaction.size()) --last;
1712     return (d->m_lastSavedTransaction != last);
1713 }
1714 
1715 void SKGDocument::setFileNotModified() const
1716 {
1717     d->m_lastSavedTransaction = getTransactionToProcess(SKGDocument::UNDO);
1718 }
1719 
1720 QString SKGDocument::getCurrentFileName() const
1721 {
1722     return d->m_currentFileName;
1723 }
1724 
1725 SKGError SKGDocument::save()
1726 {
1727     SKGError err;
1728     SKGTRACEINFUNCRC(5, err)
1729     if (d->m_currentFileName.isEmpty()) {
1730         err = SKGError(ERR_INVALIDARG, i18nc("Error message: Can not save a file if it has no name yet", "Save not authorized because the file name is not yet defined"));
1731     } else {
1732         // save
1733         err = saveAs(d->m_currentFileName, true);
1734     }
1735     return err;
1736 }
1737 
1738 SKGError SKGDocument::saveAs(const QString& iName, bool iOverwrite)
1739 {
1740     SKGError err;
1741     SKGTRACEINFUNCRC(5, err)
1742     SKGTRACEL(10) << "Input parameter [name]=[" << iName << ']' << SKGENDL;
1743 
1744     bool simulateFileSystemFull = !SKGServices::getEnvVariable(QStringLiteral("SKGFILESYSTEMFULL")).isEmpty();
1745 
1746     // Check if a transaction is still opened
1747     err = checkExistingTransaction();
1748     IFOK(err) err.setReturnCode(ERR_ABORT).setMessage(i18nc("Cannot save the file while the application is still performing an SQL transaction", "Save is forbidden if a transaction is still opened"));
1749     else {
1750         err = SKGError();
1751         if (getParameter(QStringLiteral("SKG_UNDO_CLEAN_AFTER_SAVE")) == QStringLiteral("Y")) {
1752             err = executeSqliteOrder(QStringLiteral("delete from doctransaction"));
1753         }
1754 
1755         // No transaction opened ==> it is ok
1756         // We mark the last transaction as a save point
1757         IFOKDO(err, executeSqliteOrder(QStringLiteral("update doctransaction set t_savestep='Y' where id in (select A.id from doctransaction A where "
1758                                        "NOT EXISTS(select 1 from doctransaction B where B.i_parent=A.id) "
1759                                        "and A.t_mode='U')")))
1760         Q_EMIT tableModified(QStringLiteral("doctransaction"), 0, false);
1761 
1762         // Optimization
1763         IFOK(err) {
1764             err = executeSqliteOrder(QStringLiteral("VACUUM;"));
1765             IFOK(err) {
1766                 // Check if file already exist
1767                 if (!iOverwrite && QFile(iName).exists()) {
1768                     err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("There is already a file with the same name", "File '%1' already exist", iName));
1769                 } else {
1770                     // Get backup file name
1771                     bool backupFileMustBeRemoved = false;
1772                     QString backupFileName = getBackupFile(iName);
1773                     if (backupFileName.isEmpty()) {
1774                         backupFileName = iName % ".tmp";
1775                         backupFileMustBeRemoved = true;
1776                     }
1777 
1778                     // Create backup file
1779                     QFile::remove(backupFileName % '~');
1780                     QFile::rename(backupFileName, backupFileName % '~');
1781                     if (QFile(iName).exists() && (simulateFileSystemFull || !QFile(iName).copy(backupFileName))) {
1782                         this->sendMessage(i18nc("Error message: Could not create a backup file", "Creation of backup file %1 failed", backupFileName), Warning);
1783                     }
1784 
1785                     // Save database
1786                     IFOK(err) {
1787                         QFile::remove(iName % '~');
1788                         QFile::rename(iName, iName % '~');
1789 
1790                         // To be sure that db is flushed
1791                         IFOKDO(err, executeSqliteOrder(QStringLiteral("PRAGMA synchronous = FULL")))
1792                         QString pwd = getPassword();
1793 
1794                         // Copy memory to tmp db
1795                         if (!d->m_directAccessDb && !err) {
1796                             QFile::remove(d->m_temporaryFile);
1797                             auto fileDb = QSqlDatabase::addDatabase(SQLDRIVERNAME, d->m_databaseIdentifier % "_tmp");
1798                             fileDb.setDatabaseName(d->m_temporaryFile);
1799                             if (!fileDb.open()) {
1800                                 // Set error message
1801                                 QSqlError sqlErr = fileDb.lastError();
1802                                 err = SKGError(SQLLITEERROR + sqlErr.nativeErrorCode().toInt(), sqlErr.text());
1803                             } else {
1804                                 IFOKDO(err, SKGServices::executeSqliteOrder(fileDb, "PRAGMA KEY = '" % SKGServices::stringToSqlString(pwd.isEmpty() ? QStringLiteral("DEFAULTPASSWORD") : pwd) % "'"))
1805                                 addSqliteAddon(&fileDb);
1806                                 IFOKDO(err, SKGServices::copySqliteDatabase(fileDb, d->m_currentDatabase, false, pwd.isEmpty() ? QStringLiteral("DEFAULTPASSWORD") : pwd))
1807                             }
1808 
1809                             fileDb.close();
1810                             QSqlDatabase::removeDatabase(d->m_databaseIdentifier % "_tmp");
1811                         }
1812 
1813                         // To simulate a file system full
1814                         if (!err && simulateFileSystemFull) {
1815                             err = SKGError(ERR_WRITEACCESS, i18nc("Error message: writing a file failed", "Write file '%1' failed", iName));
1816                         }
1817 
1818                         // Crypt the file
1819                         if (!err) {
1820                             bool mode;
1821                             err = SKGServices::cryptFile(d->m_temporaryFile, iName, pwd, true, getDocumentHeader(), mode);
1822                         }
1823                         if (!d->m_directAccessDb && !err) {
1824                             QFile(d->m_temporaryFile).remove();
1825                         }
1826 
1827                         // For performances
1828                         IFOKDO(err, executeSqliteOrder(QStringLiteral("PRAGMA synchronous = OFF")))
1829                     }
1830 
1831                     if (backupFileMustBeRemoved) {
1832                         QFile::remove(backupFileName);
1833                     }
1834 
1835                     IFOK(err) {
1836                         // The document is not modified
1837                         QString oldtemporaryFile = d->m_temporaryFile;
1838                         d->m_currentFileName = iName;
1839                         d->m_modeReadOnly = false;
1840                         d->m_temporaryFile = getTemporaryFile(d->m_currentFileName);
1841                         if (oldtemporaryFile != d->m_temporaryFile) {
1842                             QFile(oldtemporaryFile).rename(d->m_temporaryFile);
1843                         }
1844                         d->m_lastSavedTransaction = getTransactionToProcess(SKGDocument::UNDO);
1845 
1846                         // Commit save
1847                         QFile::remove(backupFileName % '~');
1848                         QFile::remove(iName % '~');
1849                     } else {
1850                         // Rollback file
1851                         QFile::remove(backupFileName);
1852                         QFile::rename(backupFileName % '~', backupFileName);
1853 
1854                         QFile::remove(iName);
1855                         QFile::rename(iName % '~', iName);
1856                     }
1857                 }
1858             }
1859         }
1860 
1861         Q_EMIT transactionSuccessfullyEnded(0);
1862     }
1863     return err;
1864 }
1865 
1866 SKGError SKGDocument::close()
1867 {
1868     SKGTRACEINFUNC(5)
1869     if (getMainDatabase() != nullptr) {
1870         QString conNameMainConnection = getMainDatabase()->connectionName();
1871         const auto& conNames = QSqlDatabase::connectionNames();
1872         for (const auto& conName : conNames) {
1873             if (conName.startsWith(conNameMainConnection % "_")) {
1874                 /* NO NEED
1875                 {
1876                     auto con = QSqlDatabase::database(conName, false);
1877                     con.close();
1878                 }*/
1879                 QSqlDatabase::removeDatabase(conName);
1880             }
1881         }
1882         getMainDatabase()->close();
1883         d->m_currentDatabase = QSqlDatabase();  // To avoid warning on remove
1884         QSqlDatabase::removeDatabase(d->m_databaseIdentifier);
1885     }
1886 
1887     if (!d->m_temporaryFile.isEmpty()) {
1888         QFile(d->m_temporaryFile).remove();
1889         d->m_temporaryFile = QLatin1String("");
1890     }
1891 
1892     // Emit events ?
1893     bool emitEvent = (d->m_lastSavedTransaction != -1);
1894 
1895     // Init fields
1896     d->m_currentFileName = QLatin1String("");
1897     d->m_lastSavedTransaction = 0;
1898     d->m_nbStepForTransaction.clear();
1899     d->m_posStepForTransaction.clear();
1900     d->m_nameForTransaction.clear();
1901     d->m_password.clear();
1902     d->m_password_got = false;
1903 
1904     // Send event
1905     if (!d->m_blockEmits && emitEvent && qApp && !qApp->closingDown()) {
1906         Q_EMIT tableModified(QLatin1String(""), 0, false);
1907         Q_EMIT transactionSuccessfullyEnded(0);
1908         Q_EMIT modified();
1909     }
1910 
1911     return SKGError();
1912 }
1913 
1914 SKGError SKGDocument::dropViewsAndIndexes(const QStringList& iTables) const
1915 {
1916     SKGError err;
1917     // Drop all views
1918     SKGStringListList list;
1919     err = executeSelectSqliteOrder(QStringLiteral("SELECT tbl_name, name, type FROM sqlite_master WHERE type IN ('view','index')"), list);
1920     int nb = list.count();
1921     for (int i = 1; !err && i < nb; ++i) {
1922         QString name = list.at(i).at(1);
1923         QString table = SKGServices::getRealTable(list.at(i).at(0));
1924         QString type = list.at(i).at(2);
1925         if (iTables.contains(table)) {
1926             QString sql = "DROP " % type % " IF EXISTS " % name;
1927             err = this->executeSqliteOrder(sql);
1928         }
1929     }
1930     return err;
1931 }
1932 
1933 #include "skgdocument2.cpp"
1934 
1935 SKGError SKGDocument::migrate(bool& oMigrationDone)
1936 {
1937     SKGError err;
1938     SKGTRACEINFUNCRC(5, err)
1939     oMigrationDone = false;
1940 
1941     {
1942         SKGBEGINPROGRESSTRANSACTION(*this, "#INTERNAL#" % i18nc("Progression step", "Migrate document"), err, 3)
1943         if (getParameter(QStringLiteral("SKG_UNDO_MAX_DEPTH")).isEmpty()) {
1944             IFOKDO(err, setParameter(QStringLiteral("SKG_UNDO_MAX_DEPTH"), SKGServices::intToString(SKG_UNDO_MAX_DEPTH)))
1945         }
1946 
1947         if (getParameter(QStringLiteral("SKG_UNDO_CLEAN_AFTER_SAVE")).isEmpty()) {
1948             IFOKDO(err, setParameter(QStringLiteral("SKG_UNDO_CLEAN_AFTER_SAVE"), QStringLiteral("N")))
1949         }
1950 
1951         if (!err && getParameter(QStringLiteral("SKG_DATABASE_TYPE")) != (d->m_modeSQLCipher ? SQLDRIVERNAME : QStringLiteral("QSQLITE"))) {
1952             IFOKDO(err, setParameter(QStringLiteral("SKG_DATABASE_TYPE"), d->m_modeSQLCipher ? SQLDRIVERNAME : QStringLiteral("QSQLITE")))
1953         }
1954 
1955         QString version = getParameter(QStringLiteral("SKG_DB_VERSION"));
1956         QString initialversion = version;
1957         QString lastversion = QStringLiteral("1.6");
1958 
1959         if (!err && version.isEmpty()) {
1960             // First creation
1961             SKGTRACEL(10) << "Migration from 0 to " << lastversion << SKGENDL;
1962 
1963             // Set new version
1964             version = lastversion;
1965             IFOKDO(err, setParameter(QStringLiteral("SKG_DB_VERSION"), version))
1966 
1967             // Set sqlite creation version
1968             SKGStringListList listTmp;
1969             IFOKDO(err, executeSelectSqliteOrder(QStringLiteral("select sqlite_version()"), listTmp))
1970             if (!err && listTmp.count() == 2) {
1971                 err = setParameter(QStringLiteral("SKG_SQLITE_CREATION_VERSION"), listTmp.at(1).at(0));
1972             }
1973             oMigrationDone = true;
1974         }
1975 
1976         if (!err && SKGServices::stringToDouble(version) > SKGServices::stringToDouble(lastversion)) {
1977             err = SKGError(ERR_ABORT, i18nc("Error message", "Impossible to load a document generated by a more recent version"));
1978         }
1979 
1980         {
1981             // Migration steps
1982             if (!err && version == QStringLiteral("0.1")) {
1983                 // Migration from version 0.1 to 0.2
1984                 SKGTRACEL(10) << "Migration from 0.1 to 0.2" << SKGENDL;
1985 
1986                 // ==================================================================
1987                 // Table doctransactionmsg
1988                 QStringList sqlOrders;
1989                 sqlOrders << QStringLiteral("DROP TABLE IF EXISTS doctransactionmsg")
1990                           << QStringLiteral("CREATE TABLE doctransactionmsg ("
1991                                             "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
1992                                             "rd_doctransaction_id INTEGER NOT NULL,"
1993                                             "t_message TEXT NOT NULL DEFAULT '')");
1994                 err = executeSqliteOrders(sqlOrders);
1995 
1996                 // Set new version
1997                 version = QStringLiteral("0.2");
1998                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
1999                 oMigrationDone = true;
2000             }
2001             if (!err && version == QStringLiteral("0.2")) {
2002                 // Migration from version 0.2 to 0.3
2003                 SKGTRACEL(10) << "Migration from 0.2 to 0.3" << SKGENDL;
2004 
2005                 err = executeSqliteOrder(QStringLiteral("UPDATE node set f_sortorder=id"));
2006 
2007                 // Set new version
2008                 version = QStringLiteral("0.3");
2009                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2010                 oMigrationDone = true;
2011             }
2012             if (!err && version == QStringLiteral("0.3")) {
2013                 // Migration from version 0.3 to 0.4
2014                 SKGTRACEL(10) << "Migration from 0.3 to 0.4" << SKGENDL;
2015 
2016                 err = executeSqliteOrder(QStringLiteral("ALTER TABLE node ADD COLUMN t_autostart VARCHAR(1) DEFAULT 'N' CHECK (t_autostart IN ('Y', 'N'))"));
2017                 IFOKDO(err, executeSqliteOrder("UPDATE node set t_autostart='Y' where t_name='" % i18nc("Verb, automatically load when the application is started", "autostart") % '\''))
2018 
2019                 // Set new version
2020                 version = QStringLiteral("0.4");
2021                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2022                 oMigrationDone = true;
2023             }
2024             if (!err && version == QStringLiteral("0.4")) {
2025                 // Migration from version 0.4 to 0.5
2026                 SKGTRACEL(10) << "Migration from 0.4 to 0.5" << SKGENDL;
2027 
2028                 err = executeSqliteOrder(QStringLiteral("ALTER TABLE doctransactionmsg ADD COLUMN t_popup VARCHAR(1) DEFAULT 'Y' CHECK (t_popup IN ('Y', 'N'))"));
2029 
2030                 // Set new version
2031                 version = QStringLiteral("0.5");
2032                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2033                 oMigrationDone = true;
2034             }
2035             if (!err && version == QStringLiteral("0.5")) {
2036                 // Migration from version 0.5 to 0.6
2037                 SKGTRACEL(10) << "Migration from 0.5 to 0.6" << SKGENDL;
2038 
2039                 err = executeSqliteOrder(QStringLiteral("UPDATE node set t_autostart='N' where t_autostart NOT IN ('Y', 'N')"));
2040 
2041                 // Set new version
2042                 version = QStringLiteral("0.6");
2043                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2044                 oMigrationDone = true;
2045             }
2046             if (!err && version == QStringLiteral("0.6")) {
2047                 // Migration from version 0.6 to 0.7
2048                 SKGTRACEL(10) << "Migration from 0.6 to 0.7" << SKGENDL;
2049 
2050                 err = executeSqliteOrder(QStringLiteral("ALTER TABLE parameters ADD COLUMN b_blob BLOB"));
2051 
2052                 // Set new version
2053                 version = QStringLiteral("0.7");
2054                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2055                 oMigrationDone = true;
2056             }
2057             if (!err && version == QStringLiteral("0.7")) {
2058                 // Migration from version 0.7 to 0.8
2059                 SKGTRACEL(10) << "Migration from 0.7 to 0.8" << SKGENDL;
2060 
2061                 err = executeSqliteOrder(QStringLiteral("UPDATE parameters set t_name='SKG_LANGUAGE' where t_name='SKGLANGUAGE'"));
2062 
2063                 // Set new version
2064                 version = QStringLiteral("0.8");
2065                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2066                 oMigrationDone = true;
2067             }
2068             if (!err && version == QStringLiteral("0.8")) {
2069                 SKGTRACEL(10) << "Migration from 0.8 to 0.9" << SKGENDL;
2070 
2071                 QStringList sql;
2072                 sql << QStringLiteral("ALTER TABLE parameters ADD COLUMN i_tmp INTEGER NOT NULL DEFAULT 0")
2073                     << QStringLiteral("UPDATE parameters set i_tmp=0");
2074 
2075                 err = executeSqliteOrders(sql);
2076 
2077                 // Set new version
2078                 version = QStringLiteral("0.9");
2079                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2080                 oMigrationDone = true;
2081             }
2082             if (!err && version == QStringLiteral("0.9")) {
2083                 SKGTRACEL(10) << "Migration from 0.9 to 1.0" << SKGENDL;
2084 
2085                 err = SKGDocument::setParameter(QStringLiteral("SKG_UNIQUE_ID"), QLatin1String(""));
2086 
2087                 // Set new version
2088                 version = QStringLiteral("1.0");
2089                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2090                 oMigrationDone = true;
2091             }
2092             if (!err && version == QStringLiteral("1.0")) {
2093                 // Migration from version 1.0 to 1.1
2094                 SKGTRACEL(10) << "Migration from 1.0 to 1.1" << SKGENDL;
2095 
2096                 err = executeSqliteOrder(QStringLiteral("ALTER TABLE node ADD COLUMN t_icon TEXT DEFAULT ''"));
2097                 IFOK(err) {
2098                     SKGStringListList result;
2099                     err = executeSelectSqliteOrder(QStringLiteral("SELECT id,t_data from node"), result);
2100                     int nb = result.count();
2101                     for (int i = 1; !err && i < nb; ++i) {
2102                         const QStringList& line = result.at(i);
2103                         QString icon = QStringLiteral("folder-bookmark");
2104                         QStringList data = SKGServices::splitCSVLine(line.at(1));
2105                         if (data.count() > 2) {
2106                             icon = data.at(2);
2107                         }
2108                         data.removeAt(2);
2109                         err = executeSqliteOrder("UPDATE node set t_icon='" % SKGServices::stringToSqlString(icon) %
2110                                                  "', t_data='" % SKGServices::stringToSqlString(SKGServices::stringsToCsv(data)) % "' where id=" % line.at(0));
2111                     }
2112                 }
2113 
2114                 // Set new version
2115                 version = QStringLiteral("1.1");
2116                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2117                 oMigrationDone = true;
2118             }
2119             if (!err && version == QStringLiteral("1.1")) {
2120                 // Migration from version 1.1 to 1.2
2121                 SKGTRACEL(10) << "Migration from 1.1 to 1.2" << SKGENDL;
2122 
2123                 QStringList sql;
2124                 sql << QStringLiteral("ALTER TABLE doctransaction ADD COLUMN t_refreshviews VARCHAR(1) DEFAULT 'Y' CHECK (t_refreshviews IN ('Y', 'N'))")
2125                     << QStringLiteral("UPDATE doctransaction set t_refreshviews='Y'");
2126 
2127                 err = executeSqliteOrders(sql);
2128 
2129                 // Set new version
2130                 version = QStringLiteral("1.2");
2131                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2132                 oMigrationDone = true;
2133             }
2134             if (!err && version == QStringLiteral("1.2")) {
2135                 // Migration from version 1.2 to 1.3
2136                 SKGTRACEL(10) << "Migration from 1.2 to 1.3" << SKGENDL;
2137 
2138                 err = SKGDocument::refreshViewsIndexesAndTriggers();
2139 
2140                 QStringList sql;
2141                 sql << QStringLiteral("DELETE FROM node WHERE (r_node_id IS NULL OR r_node_id='') AND EXISTS (SELECT 1 FROM node n WHERE n.t_name=node.t_name AND r_node_id=0)")
2142                     << QStringLiteral("UPDATE node SET t_name=t_name");
2143                 IFOKDO(err, executeSqliteOrders(sql))
2144 
2145                 // Set new version
2146                 version = QStringLiteral("1.3");
2147                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2148                 oMigrationDone = true;
2149             }
2150             if (!err && version == QStringLiteral("1.3")) {
2151                 // Migration from version 1.3 to 1.4
2152                 SKGTRACEL(10) << "Migration from 1.3 to 1.4" << SKGENDL;
2153 
2154                 QStringList sql;
2155                 sql   << "CREATE TABLE node2 ("
2156                       "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
2157                       "t_name TEXT NOT NULL DEFAULT '' CHECK (t_name NOT LIKE '%" % OBJECTSEPARATOR % "%'),"
2158                       "t_fullname TEXT,"
2159                       "t_icon TEXT DEFAULT '',"
2160                       "f_sortorder FLOAT,"
2161                       "t_autostart VARCHAR(1) DEFAULT 'N' CHECK (t_autostart IN ('Y', 'N')),"
2162                       "t_data TEXT,"
2163                       "rd_node_id INT CONSTRAINT fk_id REFERENCES node(id) ON DELETE CASCADE)"
2164 
2165                       << QStringLiteral("INSERT INTO node2 (id, t_name, t_fullname, t_icon, f_sortorder, t_autostart, t_data, rd_node_id) "
2166                                         "SELECT id, t_name, t_fullname, t_icon, f_sortorder, t_autostart, t_data, r_node_id FROM node")
2167 
2168                       << QStringLiteral("DROP TABLE IF EXISTS node")
2169                       << QStringLiteral("ALTER TABLE node2 RENAME TO node");
2170 
2171                 err = executeSqliteOrders(sql);
2172 
2173                 // Set new version
2174                 version = QStringLiteral("1.4");
2175                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2176                 oMigrationDone = true;
2177             }
2178             if (!err && version == QStringLiteral("1.4")) {
2179                 // Migration from version 1.4 to 1.5
2180                 SKGTRACEL(10) << "Migration from 1.4 to 1.5" << SKGENDL;
2181 
2182                 err = SKGDocument::refreshViewsIndexesAndTriggers();
2183 
2184                 QStringList sql;
2185                 sql << QStringLiteral("UPDATE parameters SET t_uuid_parent='advice' WHERE t_uuid_parent='advices'");
2186                 IFOKDO(err, executeSqliteOrders(sql))
2187 
2188                 // Set new version
2189                 version = QStringLiteral("1.5");
2190                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2191                 oMigrationDone = true;
2192             }
2193             if (!err && version == QStringLiteral("1.5")) {
2194                 // Migration from version 1.5 to 1.6
2195                 SKGTRACEL(10) << "Migration from  1.5 to 1.6" << SKGENDL;
2196 
2197                 err = SKGDocument::refreshViewsIndexesAndTriggers();
2198 
2199                 QStringList sql;
2200                 sql << QStringLiteral("DROP TABLE IF EXISTS doctransactionmsg2")
2201                     << QStringLiteral("CREATE TABLE doctransactionmsg2 ("
2202                                       "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
2203                                       "rd_doctransaction_id INTEGER NOT NULL,"
2204                                       "t_message TEXT NOT NULL DEFAULT '',"
2205                                       "t_type VARCHAR(1) DEFAULT 'I' CHECK (t_type IN ('P', 'I', 'W', 'E', 'H')))")  // Positive, Information, Warning, Error, Hidden
2206                     << QStringLiteral("INSERT INTO doctransactionmsg2 (id, rd_doctransaction_id, t_message, t_type) SELECT id, rd_doctransaction_id, t_message, (CASE WHEN t_popup='Y' THEN 'I' ELSE 'H' END)  FROM doctransactionmsg")
2207 
2208                     << QStringLiteral("DROP TABLE IF EXISTS doctransactionmsg")
2209                     << QStringLiteral("ALTER TABLE doctransactionmsg2 RENAME TO doctransactionmsg");
2210                 IFOKDO(err, executeSqliteOrders(sql))
2211 
2212                 // Set new version
2213                 version = QStringLiteral("1.6");
2214                 IFOKDO(err, SKGDocument::setParameter(QStringLiteral("SKG_DB_VERSION"), version))
2215                 oMigrationDone = true;
2216             }
2217         }
2218         IFOKDO(err, stepForward(1, i18nc("Progression step", "Refresh views")))
2219 
2220         // Set sqlite last version
2221         SKGStringListList listTmp;
2222         IFOKDO(err, executeSelectSqliteOrder(QStringLiteral("select sqlite_version()"), listTmp))
2223         if (!err && listTmp.count() == 2) {
2224             err = setParameter(QStringLiteral("SKG_SQLITE_LAST_VERSION"), listTmp.at(1).at(0));
2225         }
2226 
2227         // Refresh views
2228         IFOKDO(err, refreshViewsIndexesAndTriggers())
2229         IFOKDO(err, stepForward(2, i18nc("Progression step", "Update materialized views")))
2230 
2231         // Refresh materialized views
2232         if (!err && oMigrationDone) {
2233             err = computeMaterializedViews();
2234         }
2235         IFOKDO(err, stepForward(3))
2236 
2237         IFKO(err) err.addError(ERR_FAIL, i18nc("Error message: Could not perform database migration", "Database migration from version %1 to version %2 failed", initialversion, version));
2238     }
2239 
2240     return err;
2241 }
2242 
2243 SKGError SKGDocument::createUndoRedoTemporaryTriggers() const
2244 {
2245     SKGError err;
2246     SKGTRACEINFUNCRC(10, err)
2247 
2248     // Create triggers
2249     QStringList tables;
2250     err = this->getTablesList(tables);
2251     int nbTables = tables.count();
2252     for (int i = 0; !err && i < nbTables; ++i) {
2253         // Get table name
2254         const QString& table = tables.at(i);
2255 
2256         // Do we have to treat this table
2257         if (!SKGListNotUndoable.contains("T." % table) && !table.startsWith(QLatin1String("vm_"))) {
2258             // YES
2259             // Get attributes name
2260             QStringList attributes;
2261             err = getAttributesList(table, attributes);
2262 
2263             // Build sqlorder for update and insert
2264             QString sqlorderForUpdate2;
2265             QString sqlorderForInsert1;
2266             QString sqlorderForInsert2;
2267             int nbAttributes = attributes.count();
2268             for (int j = 0; !err && j < nbAttributes; ++j) {
2269                 // Get attribute
2270                 const QString& att = attributes.at(j);
2271 
2272                 // Do we have to treat this attribute
2273                 if (!SKGListNotUndoable.contains("A." % table % '.' % att)) {
2274                     // Build for update
2275                     if (!sqlorderForUpdate2.isEmpty()) {
2276                         sqlorderForUpdate2 += ',';
2277                     }
2278                     sqlorderForUpdate2 += att % "='||quote(old." % att % ")||'";
2279 
2280                     // Build for insert part 1
2281                     if (!sqlorderForInsert1.isEmpty()) {
2282                         sqlorderForInsert1 += ',';
2283                     }
2284                     sqlorderForInsert1 += att;
2285 
2286                     // Build for insert part 2
2287                     if (!sqlorderForInsert2.isEmpty()) {
2288                         sqlorderForInsert2 += ',';
2289                     }
2290                     sqlorderForInsert2 += "'||quote(old." % att % ")||'";
2291                 }
2292             }
2293 
2294             // Create specific triggers for the current transaction
2295             QStringList sqlOrders;
2296             // DROP DELETE trigger
2297             sqlOrders << "DROP TRIGGER IF EXISTS UR_" % table % "_IN"
2298 
2299                       // Create DELETE trigger
2300                       << "CREATE TEMP TRIGGER UR_" % table % "_IN "
2301                       "AFTER  INSERT ON " % table % " BEGIN "
2302                       "INSERT INTO doctransactionitem (rd_doctransaction_id, t_sqlorder,i_object_id,t_object_table,t_action) VALUES(0,'DELETE FROM " % table %
2303                       " WHERE id='||new.id,new.id,'" % table % "','D');END"
2304 
2305                       // DROP UPDATE trigger
2306                       << "DROP TRIGGER IF EXISTS UR_" % table % "_UP"
2307 
2308                       // Create UPDATE trigger
2309                       << "CREATE TEMP TRIGGER UR_" % table % "_UP "
2310                       "AFTER UPDATE ON " % table % " BEGIN "
2311                       "INSERT INTO doctransactionitem  (rd_doctransaction_id, t_sqlorder,i_object_id,t_object_table,t_action) VALUES(0,'UPDATE " % table %
2312                       " SET " % sqlorderForUpdate2 %
2313                       " WHERE id='||new.id,new.id,'" % table % "','U');END"
2314 
2315                       // DROP INSERT trigger
2316                       << "DROP TRIGGER IF EXISTS UR_" % table % "_DE"
2317 
2318                       // Create INSERT trigger
2319                       << "CREATE TEMP TRIGGER UR_" % table % "_DE "
2320                       "AFTER DELETE ON " % table %
2321                       " BEGIN "
2322                       "INSERT INTO doctransactionitem  (rd_doctransaction_id, t_sqlorder,i_object_id,t_object_table,t_action) VALUES(0,'INSERT INTO " % table %
2323                       '(' % sqlorderForInsert1 % ") VALUES(" % sqlorderForInsert2 % ")',old.id,'" % table % "','I'); END";
2324             err = executeSqliteOrders(sqlOrders);
2325         }
2326     }
2327     return err;
2328 }
2329 
2330 QStringList SKGDocument::getParameters(const QString& iParentUUID, const QString& iWhereClause)
2331 {
2332     SKGTRACEINFUNC(10)
2333     QStringList output;
2334     QString wc = "t_uuid_parent='" % SKGServices::stringToSqlString(iParentUUID) % '\'';
2335     if (!iWhereClause.isEmpty()) {
2336         wc += " AND (" % iWhereClause % ')';
2337     }
2338     this->getDistinctValues(QStringLiteral("parameters"), QStringLiteral("t_name"), wc, output);
2339     return output;
2340 }
2341 
2342 QString SKGDocument::getParameter(const QString& iName, const QString& iParentUUID) const
2343 {
2344     SKGTRACEINFUNC(10)
2345     SKGTRACEL(10) << "Input parameter [iName]=[" << iName << ']' << SKGENDL;
2346     QString output;
2347 
2348     // Get parameter
2349     SKGObjectBase param;
2350     SKGError err = getObject(QStringLiteral("parameters"), "t_name='" % SKGServices::stringToSqlString(iName) %
2351                              "' AND t_uuid_parent='" % SKGServices::stringToSqlString(iParentUUID) % '\'', param);
2352     IFOK(err) {
2353         output = param.getAttribute(QStringLiteral("t_value"));
2354     }
2355     return output;
2356 }
2357 
2358 QVariant SKGDocument::getParameterBlob(const QString& iName, const QString& iParentUUID) const
2359 {
2360     SKGTRACEINFUNC(10)
2361     SKGTRACEL(10) << "Input parameter [iName]=[" << iName << ']' << SKGENDL;
2362 
2363     QVariant output;
2364     if (getMainDatabase() != nullptr) {
2365         QString sqlQuery = QStringLiteral("SELECT b_blob FROM parameters WHERE t_name=? AND t_uuid_parent=?");
2366         QSqlQuery query(*getMainDatabase());
2367         query.prepare(sqlQuery);
2368         query.addBindValue(iName);
2369         query.addBindValue(iParentUUID);
2370         if (Q_LIKELY(!query.exec())) {
2371             QSqlError sqlError = query.lastError();
2372             SKGTRACE << "WARNING: " << sqlQuery << SKGENDL;
2373             SKGTRACE << "         returns :" << sqlError.text() << SKGENDL;
2374         } else {
2375             if (query.next()) {
2376                 output = query.value(0);
2377             }
2378         }
2379     }
2380 
2381     return output;
2382 }
2383 
2384 SKGError SKGDocument::setParameter(const QString& iName, const QString& iValue, const QString& iFileName, const QString& iParentUUID, SKGPropertyObject* oObjectCreated) const
2385 {
2386     SKGError err;
2387     SKGTRACEINFUNCRC(10, err)
2388     SKGTRACEL(10) << "Input parameter [iName]    =[" << iName << ']' << SKGENDL;
2389     SKGTRACEL(10) << "Input parameter [iValue]   =[" << iValue << ']' << SKGENDL;
2390     SKGTRACEL(10) << "Input parameter [iFileName]=[" << iFileName << ']' << SKGENDL;
2391     QVariant blob;
2392     QString value = iValue;
2393     QFile file(iFileName);
2394     if (file.exists()) {
2395         QFileInfo fileInfo(iFileName);
2396         if (fileInfo.isDir()) {
2397             value = "file://" % iFileName;
2398         } else {
2399             // Open file
2400             if (Q_UNLIKELY(!file.open(QIODevice::ReadOnly))) {
2401                 err = SKGError(ERR_INVALIDARG, i18nc("Error message: Could not open a file", "Open file '%1' failed", iFileName));
2402             } else {
2403                 QByteArray blob_bytes = file.readAll();
2404                 if (blob_bytes.isEmpty()) {
2405                     err = SKGError(ERR_INVALIDARG, i18nc("Error message: Could not open a file", "Open file '%1' failed", iFileName));
2406                 } else {
2407                     blob = blob_bytes;
2408                     value = fileInfo.fileName();
2409                 }
2410 
2411                 // close file
2412                 file.close();
2413             }
2414         }
2415     }
2416 
2417     IFOKDO(err, setParameter(iName, value, blob, iParentUUID, oObjectCreated))
2418     return err;
2419 }
2420 
2421 SKGError SKGDocument::setParameter(const QString& iName, const QString& iValue, const QVariant& iBlob, const QString& iParentUUID, SKGPropertyObject* oObjectCreated) const
2422 {
2423     SKGError err;
2424     SKGTRACEINFUNCRC(10, err)
2425     SKGTRACEL(10) << "Input parameter [iName]    =[" << iName << ']' << SKGENDL;
2426     SKGTRACEL(10) << "Input parameter [iValue]   =[" << iValue << ']' << SKGENDL;
2427     if (getMainDatabase() == nullptr) {
2428         err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
2429     } else {
2430         SKGPropertyObject param(const_cast<SKGDocument*>(this));
2431         IFOKDO(err, param.setName(iName))
2432         IFOKDO(err, param.setValue(iValue))
2433         IFOKDO(err, param.setParentId(iParentUUID))
2434         IFOKDO(err, param.save(true, oObjectCreated != nullptr))
2435 
2436         if (!err && !iBlob.isNull()) {
2437             err = param.load();
2438             IFOK(err) {
2439                 // Set blob
2440                 QString sqlQuery = QStringLiteral("UPDATE parameters SET b_blob=? WHERE id=?");
2441                 QSqlQuery query(*getMainDatabase());
2442                 query.prepare(sqlQuery);
2443                 query.addBindValue(iBlob);
2444                 query.addBindValue(param.getID());
2445                 if (Q_LIKELY(!query.exec())) {
2446                     QSqlError sqlError = query.lastError();
2447                     QString msg = sqlQuery % ':' % sqlError.text();
2448                     err = SKGError(SQLLITEERROR + sqlError.nativeErrorCode().toInt(), msg);
2449                 }
2450             }
2451         }
2452         if (!err && oObjectCreated != nullptr) {
2453             *oObjectCreated = param;
2454         }
2455     }
2456     return err;
2457 }
2458 
2459 SKGError SKGDocument::dump(int iMode) const
2460 {
2461     SKGError err;
2462     // dump parameters
2463     SKGTRACE << "=== START DUMP ===" << SKGENDL;
2464     if ((iMode & DUMPSQLITE) != 0) {
2465         SKGTRACE << "=== DUMPSQLITE ===" << SKGENDL;
2466         err.addError(dumpSelectSqliteOrder(QStringLiteral("SELECT * FROM sqlite_master order by type")));
2467 
2468         SKGTRACE << "=== DUMPSQLITE (TEMPORARY) ===" << SKGENDL;
2469         err.addError(dumpSelectSqliteOrder(QStringLiteral("SELECT * FROM sqlite_temp_master order by type")));
2470     }
2471 
2472     if ((iMode & DUMPPARAMETERS) != 0) {
2473         SKGTRACE << "=== DUMPPARAMETERS ===" << SKGENDL;
2474         err.addError(dumpSelectSqliteOrder(QStringLiteral("SELECT * FROM parameters order by id")));
2475     }
2476 
2477     if ((iMode & DUMPNODES) != 0) {
2478         SKGTRACE << "=== DUMPNODES ===" << SKGENDL;
2479         err.addError(dumpSelectSqliteOrder(QStringLiteral("SELECT * FROM node order by id")));
2480     }
2481 
2482     if ((iMode & DUMPTRANSACTIONS) != 0) {
2483         // dump transaction
2484         SKGTRACE << "=== DUMPTRANSACTIONS ===" << SKGENDL;
2485         err.addError(dumpSelectSqliteOrder(QStringLiteral("SELECT * FROM doctransaction order by id")));
2486 
2487         // dump transaction
2488         SKGTRACE << "=== DUMPTRANSACTIONS (ITEMS) ===" << SKGENDL;
2489         err.addError(dumpSelectSqliteOrder(QStringLiteral("SELECT * FROM doctransactionitem order by rd_doctransaction_id, id")));
2490     }
2491     SKGTRACE << "=== END DUMP ===" << SKGENDL;
2492     return err;
2493 }
2494 
2495 QSqlDatabase* SKGDocument::getMainDatabase() const
2496 {
2497     if (!d->m_currentDatabase.isOpen()) {
2498         return nullptr;
2499     }
2500     return const_cast<QSqlDatabase*>(&d->m_currentDatabase);
2501 }
2502 
2503 QSqlDatabase SKGDocument::getThreadDatabase() const
2504 {
2505     if (qApp->thread() != QThread::currentThread()) {
2506         d->m_mutex.lock();
2507         QString pwd = getPassword();
2508         QString dbName = getMainDatabase()->databaseName();
2509         QString conName = getMainDatabase()->connectionName();
2510 
2511         QString id = conName % "_" % QString::number((quint64)QThread::currentThread(), 16);
2512         d->m_mutex.unlock();
2513 
2514         auto tmpDatabase = QSqlDatabase::database(id);
2515         if (!tmpDatabase.isValid()) {
2516             tmpDatabase = QSqlDatabase::addDatabase(SQLDRIVERNAME, id);
2517         }
2518         if (tmpDatabase.databaseName() != dbName) {
2519             tmpDatabase.setConnectOptions(QStringLiteral("QSQLITE_OPEN_URI"));
2520             tmpDatabase.setDatabaseName(dbName);
2521             if (tmpDatabase.open()) {
2522                 addSqliteAddon(&tmpDatabase);
2523                 if (d->m_modeSQLCipher) {
2524                     SKGServices::executeSqliteOrder(tmpDatabase, "PRAGMA KEY = '" % SKGServices::stringToSqlString(pwd.isEmpty() ? QStringLiteral("DEFAULTPASSWORD") : pwd) % "'");
2525                 }
2526             }
2527         }
2528         return tmpDatabase;
2529     }
2530     return d->m_currentDatabase;
2531 }
2532 
2533 SKGError SKGDocument::getConsolidatedView(const QString& iTable,
2534         const QString& iAsColumn,
2535         const QString& iAsRow,
2536         const QString& iAttribute,
2537         const QString& iOpAtt,
2538         const QString& iWhereClause,
2539         SKGStringListList& oTable,
2540         const QString& iMissingValue) const
2541 {
2542     SKGError err;
2543     SKGTRACEINFUNCRC(10, err)
2544     SKGTRACEL(10) << "Input parameter [iTable]=[" << iTable << ']' << SKGENDL;
2545     SKGTRACEL(10) << "Input parameter [iAsColumn]=[" << iAsColumn << ']' << SKGENDL;
2546     SKGTRACEL(10) << "Input parameter [iAsRow]=[" << iAsRow << ']' << SKGENDL;
2547     SKGTRACEL(10) << "Input parameter [iAttribute]=[" << iAttribute << ']' << SKGENDL;
2548     SKGTRACEL(10) << "Input parameter [iOpAtt]=[" << iOpAtt << ']' << SKGENDL;
2549     SKGTRACEL(10) << "Input parameter [iWhereClause]=[" << iWhereClause << ']' << SKGENDL;
2550     SKGTRACEL(10) << "Input parameter [iMissingValue]=[" << iMissingValue << ']' << SKGENDL;
2551 
2552     // Mode
2553     int mode = 0;
2554     if (!iAsColumn.isEmpty()) {
2555         mode += 1;
2556     }
2557     if (!iAsRow.isEmpty()) {
2558         mode += 2;
2559     }
2560 
2561     oTable.clear();
2562     oTable.push_back(QStringList());
2563 
2564 
2565     QStringList titles = oTable.at(0);
2566 
2567     if (mode == 3) {
2568         titles.push_back(iAsRow % '/' % iAsColumn);
2569     } else {
2570         if (mode == 1) {
2571             titles.push_back(iAsColumn);
2572 
2573             QStringList sums;
2574             sums.push_back(i18nc("Noun, the numerical sum of a list of values", "Sum"));
2575             oTable.push_back(sums);
2576         } else {
2577             if (mode == 2) {
2578                 titles.push_back(iAsRow);
2579                 titles.push_back(i18nc("Noun, the numerical sum of a list of values", "Sum"));
2580             }
2581         }
2582     }
2583     oTable.removeAt(0);
2584     oTable.insert(0, titles);
2585 
2586     // Create sqlorder
2587     QString asColumn = iAsColumn;
2588     if (asColumn.startsWith(QLatin1String("p_"))) {
2589         QString propertyName = asColumn.right(asColumn.length() - 2);
2590         asColumn = "(SELECT t_value FROM parameters WHERE t_uuid_parent=" % iTable % ".id||'-" % SKGServices::getRealTable(iTable) % "' AND t_name='" % propertyName % "')";
2591     }
2592     QString asRow = iAsRow;
2593     if (asRow.startsWith(QLatin1String("p_"))) {
2594         QString propertyName = asRow.right(asRow.length() - 2);
2595         asRow = "(SELECT t_value FROM parameters WHERE t_uuid_parent=" % iTable % ".id||'-" % SKGServices::getRealTable(iTable) % "' AND t_name='" % propertyName % "')";
2596     }
2597 
2598     QString att = asColumn;
2599     if (!att.isEmpty() && !asRow.isEmpty()) {
2600         att += ',';
2601     }
2602     att += asRow;
2603 
2604     QString sort = asRow;
2605     if (!sort.isEmpty() && !asColumn.isEmpty()) {
2606         sort += ',';
2607     }
2608     sort += asColumn;
2609 
2610     if (!att.isEmpty()) {
2611         QString sql = "SELECT " % att % ',' % iOpAtt % '(' % iAttribute % ") FROM " % iTable;
2612         if (!iWhereClause.isEmpty()) {
2613             sql += " WHERE " % iWhereClause;
2614         }
2615         if (!iOpAtt.isEmpty()) {
2616             sql += " GROUP BY " % att;
2617         }
2618         sql += " ORDER BY " % sort;
2619 
2620         QHash<QString, int> cols;
2621         QHash<QString, int> rows;
2622 
2623         SKGTRACEL(10) << "sqlorder=[" << sql << ']' << SKGENDL;
2624         SKGStringListList listTmp;
2625         err = executeSelectSqliteOrder(sql, listTmp);
2626         int nb = listTmp.count();
2627         for (int i = 1; !err && i < nb; ++i) {  // Title is ignored
2628             const QStringList& line = listTmp.at(i);
2629             int rowindex = -1;
2630             int colindex = -1;
2631             if (mode >= 2) {
2632                 const QString& rowname = line.at(mode == 3 ? 1 : 0);
2633 
2634                 if (!rows.contains(rowname)) {
2635                     QStringList r;
2636                     int nbx = oTable.at(0).count();
2637                     r.reserve(nbx);
2638                     r.push_back(rowname);
2639                     for (int j = 1; j < nbx; ++j) {
2640                         r.push_back(iMissingValue);
2641                     }
2642 
2643                     oTable.push_back(r);
2644 
2645                     rowindex = oTable.count() - 1;
2646                     rows.insert(rowname, rowindex);
2647                 } else {
2648                     rowindex = rows[rowname];
2649                 }
2650             } else {
2651                 rowindex = 1;
2652             }
2653 
2654             if (mode == 1 || mode == 3) {
2655                 const QString& colname = line.at(0);
2656 
2657                 if (!cols.contains(colname)) {
2658                     // Search better position of this column
2659                     colindex = -1;
2660                     {
2661                         QHashIterator<QString, int> cols_i(cols);
2662                         while (cols_i.hasNext()) {
2663                             cols_i.next();
2664                             if (colname > cols_i.key() && cols_i.value() > colindex) {
2665                                 colindex = cols_i.value();
2666                             }
2667                         }
2668                     }
2669                     if (colindex == -1) {
2670                         colindex = 1;
2671                     } else {
2672                         ++colindex;
2673                     }
2674 
2675                     int nbx = oTable.count();
2676                     for (int j = 0; j < nbx; ++j) {
2677                         if (j == 0) {
2678                             oTable[j].insert(colindex, colname);
2679                         } else {
2680                             oTable[j].insert(colindex, iMissingValue);
2681                         }
2682                     }
2683 
2684                     {
2685                         QHash<QString, int> tmp;
2686                         QHashIterator<QString, int> cols_i(cols);
2687                         while (cols_i.hasNext()) {
2688                             cols_i.next();
2689                             tmp.insert(cols_i.key(), cols_i.value() + (cols_i.value() >= colindex ? 1 : 0));
2690                         }
2691 
2692                         cols = tmp;
2693                     }
2694                     cols.insert(colname, colindex);
2695 
2696                 } else {
2697                     colindex = cols[colname];
2698                 }
2699             } else {
2700                 colindex = 1;
2701             }
2702 
2703             const QString& sum = line.at(mode == 3 ? 2 : 1);
2704 
2705             oTable[rowindex][colindex] = sum;
2706         }
2707 
2708         IFSKGTRACEL(10) {
2709             QStringList dump2 = SKGServices::tableToDump(oTable, SKGServices::DUMP_TEXT);
2710             int nbl = dump2.count();
2711             for (int i = 0; i < nbl; ++i) {
2712                 SKGTRACE << dump2.at(i) << SKGENDL;
2713             }
2714         }
2715 
2716         // Correction bug 205466 vvv
2717         // If some months or years are missing, we must add them.
2718         if (asColumn.startsWith(QLatin1String("d_"))) {
2719             for (int c = 1; c < oTable[0].count() - 1; ++c) {  // Dynamic size
2720                 bool forecast = false;
2721                 QString title = oTable.at(0).at(c);
2722                 if (title.isEmpty()) {
2723                     title = QStringLiteral("0000");
2724                 }
2725 
2726                 if (title.endsWith(QLatin1String("999"))) {
2727                     title = title.left(title.count() - 3);
2728                     forecast = true;
2729                 }
2730                 QString nextTitle = oTable.at(0).at(c + 1);
2731                 if (nextTitle.endsWith(QLatin1String("999"))) {
2732                     nextTitle = nextTitle.left(nextTitle.count() - 3);
2733                     forecast = true;
2734                 }
2735 
2736                 QString dateFormat = (asColumn == QStringLiteral("d_date") ? QStringLiteral("yyyy-MM-dd") :
2737                                       (asColumn == QStringLiteral("d_DATEMONTH") ? QStringLiteral("yyyy-MM") :
2738                                        (asColumn == QStringLiteral("d_DATEQUARTER") ? QStringLiteral("yyyy-QM") :
2739                                         (asColumn == QStringLiteral("d_DATESEMESTER") ? QStringLiteral("yyyy-SM") :
2740                                          (asColumn == QStringLiteral("d_DATEWEEK") ? QStringLiteral("yyyy-WM") : QStringLiteral("yyyy"))))));
2741                 QDate nextExpected = QDate::fromString(title, dateFormat);
2742                 QString nextExpectedString;
2743                 if (asColumn == QStringLiteral("d_DATEWEEK")) {
2744                     QStringList items = SKGServices::splitCSVLine(oTable.at(0).at(c), '-');
2745                     auto y = SKGServices::stringToInt(items.at(0));
2746                     QString ws = items.value(1);
2747                     ws.remove('W');
2748                     auto w = SKGServices::stringToInt(ws);
2749                     if (w == 0) {
2750                         w = 1;
2751                     }
2752 
2753                     nextExpected = QDate(y, 1, 1).addDays(7 * w);
2754                     nextExpectedString = SKGServices::dateToPeriod(nextExpected, "W");
2755                     if (nextExpectedString == title) {
2756                         nextExpected = nextExpected.addDays(7);
2757                         nextExpectedString = SKGServices::dateToPeriod(nextExpected, "W");
2758                     }
2759                 } else if (asColumn == QStringLiteral("d_DATEMONTH")) {
2760                     nextExpected = nextExpected.addMonths(1);
2761                     nextExpectedString = nextExpected.toString(dateFormat);
2762                 } else if (asColumn == QStringLiteral("d_DATEQUARTER")) {
2763                     nextExpected = nextExpected.addMonths(nextExpected.month() * 3 - nextExpected.month());  // convert quarter in month
2764                     nextExpected = nextExpected.addMonths(3);
2765                     nextExpectedString = nextExpected.toString(QStringLiteral("yyyy-Q")) % (nextExpected.month() <= 3 ? '1' : (nextExpected.month() <= 6 ? '2' : (nextExpected.month() <= 9 ? '3' : '4')));
2766                 } else if (asColumn == QStringLiteral("d_DATESEMESTER")) {
2767                     nextExpected = nextExpected.addMonths(nextExpected.month() * 6 - nextExpected.month());  // convert semester in month
2768                     nextExpected = nextExpected.addMonths(6);
2769                     nextExpectedString = nextExpected.toString(QStringLiteral("yyyy-S")) % (nextExpected.month() <= 6 ? '1' : '2');
2770                 } else if (asColumn == QStringLiteral("d_DATEYEAR")) {
2771                     nextExpected = nextExpected.addYears(1);
2772                     nextExpectedString = nextExpected.toString(dateFormat);
2773                 } else {
2774                     nextExpected = nextExpected.addDays(1);
2775                     nextExpectedString = nextExpected.toString(dateFormat);
2776                 }
2777                 if (title != QStringLiteral("0000") && nextTitle != nextExpectedString && nextTitle != title) {
2778                     int colindex = c + 1;
2779                     if (forecast) {
2780                         nextExpectedString += QStringLiteral("999");
2781                     }
2782 
2783                     int nbx = oTable.count();
2784                     oTable[0].insert(colindex, nextExpectedString);
2785                     for (int j = 1; j < nbx; ++j) {
2786                         oTable[j].insert(colindex, iMissingValue);
2787                     }
2788                 }
2789             }
2790         }
2791         // Correction bug 205466 ^^^
2792     }
2793 
2794     return err;
2795 }
2796 
2797 SKGDocument::SKGModelTemplateList SKGDocument::getDisplaySchemas(const QString& iRealTable) const
2798 {
2799     SKGDocument::SKGModelTemplateList listSchema;
2800 
2801     // Build schemas
2802     if (iRealTable == QStringLiteral("doctransaction")) {
2803         SKGModelTemplate def;
2804         def.id = QStringLiteral("default");
2805         def.name = i18nc("Noun, the default value of an item", "Default");
2806         def.icon = QStringLiteral("edit-undo");
2807         def.schema = QStringLiteral("t_name;t_value;d_lastmodifdate;t_savestep");
2808         listSchema.push_back(def);
2809 
2810         SKGModelTemplate minimum;
2811         minimum.id = QStringLiteral("minimum");
2812         minimum.name = i18nc("Noun, the minimum value of an item", "Minimum");
2813         minimum.icon = QLatin1String("");
2814         minimum.schema = QStringLiteral("t_name;t_value;d_lastmodifdate|N;t_savestep|N");
2815         listSchema.push_back(minimum);
2816     } else if (iRealTable == QStringLiteral("parameters")) {
2817         SKGModelTemplate def;
2818         def.id = QStringLiteral("default");
2819         def.name = i18nc("Noun, the default value of an item", "Default");
2820         def.icon = QStringLiteral("edit-undo");
2821         def.schema = QStringLiteral("t_name;t_value");
2822         listSchema.push_back(def);
2823     } else if (iRealTable == QStringLiteral("node")) {
2824         SKGModelTemplate def;
2825         def.id = QStringLiteral("default");
2826         def.name = i18nc("Noun, the default value of an item", "Default");
2827         def.icon = QStringLiteral("edit-undo");
2828         def.schema = QStringLiteral("t_name");
2829         listSchema.push_back(def);
2830     } else {
2831         SKGModelTemplate def;
2832         def.id = QStringLiteral("default");
2833         def.name = i18nc("Noun, the default value of an item", "Default");
2834         def.icon = QStringLiteral("edit-undo");
2835         def.schema = QLatin1String("");
2836         SKGStringListList lines;
2837         executeSelectSqliteOrder("PRAGMA table_info(" % iRealTable % ");", lines);
2838         for (const auto& line : qAsConst(lines)) {
2839             if (!def.schema.isEmpty()) {
2840                 def.schema += ';';
2841             }
2842             def.schema += line[1];
2843         }
2844         listSchema.push_back(def);
2845     }
2846 
2847     return listSchema;
2848 }
2849 
2850 QString SKGDocument::getDisplay(const QString& iString) const
2851 {
2852     QString output = iString.toLower();
2853 
2854     if (output.endsWith(QLatin1String("t_name"))) {
2855         output = i18nc("Noun, the name of an item", "Name");
2856     } else if (output.endsWith(QLatin1String("d_date"))) {
2857         output = i18nc("Noun, the date of an item", "Date");
2858     } else if (output.endsWith(QLatin1String("t_savestep"))) {
2859         output = i18nc("Verb, save a document", "Save");
2860     } else if (output.endsWith(QLatin1String("t_value"))) {
2861         output = i18nc("Noun, the value of an item", "Value");
2862     } else if (output.endsWith(QLatin1String("d_lastmodifdate"))) {
2863         output = i18nc("Noun, date of last modification", "Last modification");
2864     } else if (output.startsWith(QLatin1String("p_"))) {
2865         output = iString;
2866         output = output.right(output.count() - 2);
2867     } else if (output.contains(QStringLiteral(".p_"))) {
2868         output = iString;
2869         output = output.replace(QStringLiteral(".p_"), QStringLiteral("."));
2870     } else {
2871         output = iString;
2872     }
2873     return output;
2874 }
2875 
2876 QString SKGDocument::getIconName(const QString& iString) const
2877 {
2878     QString output = iString.toLower();
2879     if (output.startsWith(QLatin1String("p_")) || output.contains(QStringLiteral("p_"))) {
2880         return QStringLiteral("tag");
2881     }
2882     return QLatin1String("");
2883 }
2884 
2885 QIcon SKGDocument::getIcon(const QString& iString) const
2886 {
2887     return SKGServices::fromTheme(getIconName(iString));
2888 }
2889 
2890 QString SKGDocument::getRealAttribute(const QString& iString) const
2891 {
2892     if (iString == iString.toLower()) {
2893         return iString;
2894     }
2895     return QLatin1String("");
2896 }
2897 
2898 SKGServices::AttributeType SKGDocument::getAttributeType(const QString& iAttributeName) const
2899 {
2900     SKGServices::AttributeType output = SKGServices::TEXT;
2901     if (iAttributeName.startsWith(QLatin1String("d_"))) {
2902         output = SKGServices::DATE;
2903     } else if (iAttributeName.startsWith(QLatin1String("i_"))) {
2904         output = SKGServices::INTEGER;
2905     } else if (iAttributeName.startsWith(QLatin1String("rd_")) || iAttributeName.startsWith(QLatin1String("rc_")) || iAttributeName.startsWith(QLatin1String("r_")) || iAttributeName.startsWith(QLatin1String("id_"))) {
2906         output = SKGServices::LINK;
2907     } else if (iAttributeName.startsWith(QLatin1String("f_"))) {
2908         output = SKGServices::FLOAT;
2909     } else if (iAttributeName.startsWith(QLatin1String("b_"))) {
2910         output = SKGServices::BLOB;
2911     } else if (iAttributeName == QStringLiteral("id")) {
2912         output = SKGServices::ID;
2913     } else if (iAttributeName == QStringLiteral("t_savestep") || iAttributeName == QStringLiteral("t_refreshviews")) {
2914         output = SKGServices::BOOL;
2915     }
2916 
2917     return output;
2918 }
2919 
2920 SKGServices::SKGUnitInfo SKGDocument::getUnit(const QString& iPrefixInCache) const
2921 {
2922     SKGServices::SKGUnitInfo output;
2923     output.Name = getCachedValue(iPrefixInCache % "UnitCache");
2924     if (output.Name.isEmpty()) {
2925         refreshCache(QStringLiteral("unit"));
2926         output.Name = getCachedValue(iPrefixInCache % "UnitCache");
2927     }
2928     output.Symbol = getCachedValue(iPrefixInCache % "UnitSymbolCache");
2929     QString val = getCachedValue(iPrefixInCache % "UnitValueCache");
2930     if (!val.isEmpty()) {
2931         output.Value = SKGServices::stringToDouble(val);
2932     } else {
2933         output.Value = 1;
2934     }
2935     val = getCachedValue(iPrefixInCache % "UnitDecimalCache");
2936     if (!val.isEmpty()) {
2937         output.NbDecimal = SKGServices::stringToInt(val);
2938     } else {
2939         output.NbDecimal = 2;
2940     }
2941 
2942     return output;
2943 }
2944 
2945 QString SKGDocument::formatMoney(double iValue, const SKGServices::SKGUnitInfo& iUnit, bool iHtml) const
2946 {
2947     QString val = SKGServices::toCurrencyString(iValue / iUnit.Value, iUnit.Symbol, iUnit.NbDecimal);
2948     if (iHtml) {
2949         // Return value
2950         if (iValue < 0) {
2951             // Get std colors
2952             KColorScheme scheme(QPalette::Normal);
2953             return  QStringLiteral("<font color=\"") % scheme.foreground(KColorScheme::NegativeText).color().name() % "\">" % SKGServices::stringToHtml(val) % "</font>";
2954         }
2955         return  SKGServices::stringToHtml(val);
2956     }
2957     return val;
2958 }
2959 
2960 QString SKGDocument::formatPrimaryMoney(double iValue, int iForcedNbOfDigit) const
2961 {
2962     Q_UNUSED(iForcedNbOfDigit)
2963     return SKGServices::doubleToString(iValue);
2964 }
2965 
2966 QString SKGDocument::formatSecondaryMoney(double iValue, int iForcedNbOfDigit)const
2967 {
2968     Q_UNUSED(iForcedNbOfDigit)
2969     return SKGServices::doubleToString(iValue);
2970 }
2971 
2972 QString SKGDocument::formatPercentage(double iValue, bool iInvertColors) const
2973 {
2974     // Get std colors
2975     KColorScheme scheme(QPalette::Normal);
2976     QString negative = scheme.foreground(KColorScheme::NegativeText).color().name();
2977     QString positive = scheme.foreground(KColorScheme::PositiveText).color().name();
2978 
2979     // Return value
2980     QString p = SKGServices::toPercentageString(iValue);
2981     if (iValue > 0) {
2982         p = '+' % p;
2983     }
2984     if (p.count() > 20 || std::isnan(iValue) || std::isinf(iValue)) {
2985         p = QChar(8734);
2986     }
2987     return "<font color=\"" %
2988            QString((iValue < 0 && !iInvertColors) || (iValue >= 0 && iInvertColors) ? negative : positive) %
2989            "\">" % SKGServices::stringToHtml(p) %
2990            "</font>";
2991 }
2992 
2993 QString SKGDocument::getFileExtension() const
2994 {
2995     return QStringLiteral("skgc");
2996 }
2997 
2998 QString SKGDocument::getDocumentHeader() const
2999 {
3000     return QStringLiteral("SKG");
3001 }
3002 
3003 void SKGDocument::addValueInCache(const QString& iKey, const QString& iValue) const
3004 {
3005     d->m_cache[iKey] = iValue;
3006 }
3007 
3008 void SKGDocument::addSqlResultInCache(const QString& iKey, const SKGStringListList& iValue) const
3009 {
3010     d->m_mutex.lock();
3011     (*d->m_cacheSql)[iKey] = iValue;
3012     d->m_mutex.unlock();
3013 }
3014 
3015 QString SKGDocument::getCachedValue(const QString& iKey) const
3016 {
3017     return d->m_cache.value(iKey);
3018 }
3019 
3020 SKGStringListList SKGDocument::getCachedSqlResult(const QString& iKey) const
3021 {
3022     return d->m_cacheSql->value(iKey);
3023 }
3024 
3025 void SKGDocument::refreshCache(const QString& iTable) const
3026 {
3027     Q_UNUSED(iTable)
3028 }
3029 
3030 void SKGDocument::setBackupParameters(const QString& iPrefix, const QString& iSuffix) const
3031 {
3032     d->m_backupPrefix = iPrefix;
3033     d->m_backupSuffix = iSuffix;
3034 }
3035 
3036 QString SKGDocument::getCurrentTemporaryFile() const
3037 {
3038     return d->m_temporaryFile;
3039 }
3040 
3041 QString SKGDocument::getTemporaryFile(const QString& iFileName, bool iForceReadOnly)
3042 {
3043     QString output;
3044     QFileInfo fi(iFileName);
3045     QFileInfo di(fi.dir().path());
3046     if (iForceReadOnly || !QUrl::fromUserInput(iFileName).isLocalFile() || !di.permission(QFile::WriteUser)) {
3047         output = QDir::tempPath();
3048     } else {
3049         output = fi.absolutePath();
3050     }
3051     return output += "/." % fi.fileName() % ".wrk";
3052 }
3053 
3054 QString SKGDocument::getBackupFile(const QString& iFileName) const
3055 {
3056     QString output;
3057     if (!d->m_backupPrefix.isEmpty() || !d->m_backupSuffix.isEmpty()) {
3058         QFileInfo fi(iFileName);
3059         if (d->m_backupSuffix.endsWith(QLatin1String(".skg"))) {
3060             output = d->m_backupPrefix % fi.baseName() % d->m_backupSuffix;
3061         } else {
3062             output = d->m_backupPrefix % fi.fileName() % d->m_backupSuffix;
3063         }
3064         output = output.replace(QStringLiteral("<DATE>"), SKGServices::dateToSqlString(QDateTime::currentDateTime().date()));
3065         output = output.replace(QStringLiteral("<TIME>"), SKGServices::timeToString(QDateTime::currentDateTime()));
3066         if (!QFileInfo(output).isAbsolute()) {
3067             output = fi.absolutePath() % '/' % output;
3068         }
3069     }
3070 
3071     return output;
3072 }
3073 
3074 SKGError SKGDocument::getObjects(const QString& iTable, const QString& iWhereClause, SKGObjectBase::SKGListSKGObjectBase& oListObject) const
3075 {
3076     SKGError err;
3077 
3078     // Initialisation
3079     oListObject.clear();
3080 
3081     // Execute sqlorder
3082     SKGStringListList result;
3083     err = executeSelectSqliteOrder(
3084               QString("SELECT * FROM " % iTable %
3085                       (!iWhereClause.isEmpty() ? QString(" WHERE " % iWhereClause) : QString())),
3086               result);
3087 
3088     // Create output
3089     IFOK(err) {
3090         SKGStringListList::const_iterator itrow = result.constBegin();
3091         QStringList columns = *(itrow);
3092         ++itrow;
3093         for (; !err && itrow != result.constEnd(); ++itrow) {
3094             QStringList values = *(itrow);
3095             SKGObjectBase tmp(const_cast<SKGDocument*>(this), iTable);
3096             err = tmp.setAttributes(columns, values);
3097             oListObject.push_back(tmp);
3098         }
3099     }
3100     return err;
3101 }
3102 
3103 SKGError SKGDocument::existObjects(const QString& iTable, const QString& iWhereClause, bool& oExist) const
3104 {
3105     SKGError err;
3106 
3107     // Initialisation
3108     oExist = false;
3109 
3110     // Execute sqlorder
3111     SKGStringListList result;
3112     err = executeSelectSqliteOrder(
3113               "SELECT EXISTS(SELECT 1 FROM " % iTable % " WHERE " %
3114               (!iWhereClause.isEmpty() ?  iWhereClause  : QStringLiteral("1=1")) % ')',
3115               result);
3116 
3117     // Create output
3118     IFOK(err) oExist = (result.at(1).at(0) == QStringLiteral("1"));
3119     return err;
3120 }
3121 
3122 SKGError SKGDocument::getNbObjects(const QString& iTable, const QString& iWhereClause, int& oNbObjects) const
3123 {
3124     SKGError err;
3125 
3126     // Initialisation
3127     oNbObjects = 0;
3128 
3129     // Execute sqlorder
3130     SKGStringListList result;
3131     err = executeSelectSqliteOrder(
3132               QString("SELECT count(1) FROM " % iTable %
3133                       (!iWhereClause.isEmpty() ? QString(" WHERE " % iWhereClause) : QString())),
3134               result);
3135 
3136     // Create output
3137     IFOK(err) oNbObjects = SKGServices::stringToInt(result.at(1).at(0));
3138     return err;
3139 }
3140 
3141 SKGError SKGDocument::getObject(const QString& iTable, const QString& iWhereClause, SKGObjectBase& oObject) const
3142 {
3143     SKGObjectBase::SKGListSKGObjectBase temporaryResult;
3144     oObject.resetID();
3145     SKGError err = SKGDocument::getObjects(iTable, iWhereClause, temporaryResult);
3146     IFOK(err) {
3147         int size = temporaryResult.size();
3148         if (Q_UNLIKELY(size > 1)) {
3149             err = SKGError(ERR_INVALIDARG, i18nc("Error message: We expected only one object in the result, but got more", "More than one object returned in '%1' for '%2'", iTable, iWhereClause));
3150         } else {
3151             if (Q_UNLIKELY(size == 0)) {
3152                 err = SKGError(ERR_INVALIDARG, i18nc("Error message: We expected at least one object in the result, but got none", "No object returned in '%1' for '%2'", iTable, iWhereClause));
3153             } else {
3154                 oObject = *(temporaryResult.begin());
3155             }
3156         }
3157     }
3158     return err;
3159 }
3160 
3161 SKGError SKGDocument::getObject(const QString& iTable, int iId, SKGObjectBase& oObject) const
3162 {
3163     return getObject(iTable, "id=" % SKGServices::intToString(iId), oObject);
3164 }
3165 
3166 SKGError SKGDocument::getTablesList(QStringList& oResult) const
3167 {
3168     return getDistinctValues(QStringLiteral("sqlite_master"), QStringLiteral("name"),
3169                              QStringLiteral("type='table' AND name NOT LIKE 'sqlite_%'"),
3170                              oResult);
3171 }
3172 
3173 SKGError SKGDocument::getDistinctValues(const QString& iTable, const QString& iAttribute, const QString& iWhereClause, QStringList& oResult) const
3174 {
3175     SKGError err;
3176     _SKGTRACEINFUNCRC(10, err)
3177     // initialisation
3178     oResult.clear();
3179 
3180     // Search
3181     SKGStringListList temporaryResult;
3182     err = executeSelectSqliteOrder(
3183               "SELECT DISTINCT " % iAttribute %
3184               " FROM " % iTable % " WHERE (" %
3185               (!iWhereClause.isEmpty() ? iWhereClause : QStringLiteral("1=1")) %
3186               ") ORDER BY " % iAttribute
3187               // Correction bug 202167 vvv
3188               % " COLLATE NOCASE"
3189               // Correction bug 202167 ^^^
3190               , temporaryResult);
3191     IFOK(err) {
3192         SKGStringListList::const_iterator it = temporaryResult.constBegin();
3193         ++it;  // to forget column name
3194         for (; it != temporaryResult.constEnd(); ++it) {
3195             oResult.push_back(*(it->constBegin()));
3196         }
3197     }
3198 
3199     return err;
3200 }
3201 
3202 SKGError SKGDocument::getDistinctValues(const QString& iTable, const QString& iAttribute, QStringList& oResult) const
3203 {
3204     return getDistinctValues(iTable, iAttribute,
3205                              iAttribute % " IS NOT NULL AND " % iAttribute % "!=''",
3206                              oResult);
3207 }
3208 
3209 SKGError SKGDocument::executeSqliteOrder(const QString& iSqlOrder, int* iLastId) const
3210 {
3211     SKGError err;
3212     _SKGTRACEINFUNCRC(10, err)
3213     if (getMainDatabase() == nullptr) {
3214         err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
3215     } else {
3216         err = SKGServices::executeSqliteOrder(*getMainDatabase(), iSqlOrder, iLastId);
3217     }
3218     return err;
3219 }
3220 
3221 SKGError SKGDocument::executeSqliteOrder(const QString& iSqlOrder) const
3222 {
3223     return SKGDocument::executeSqliteOrder(iSqlOrder, nullptr);
3224 }
3225 
3226 SKGError SKGDocument::executeSqliteOrders(const QStringList& iSqlOrders) const
3227 {
3228     SKGError err;
3229     _SKGTRACEINFUNCRC(10, err)
3230     if (getMainDatabase() == nullptr) {
3231         err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
3232     } else {
3233         err = SKGServices::executeSqliteOrders(*getMainDatabase(), iSqlOrders);
3234     }
3235     return err;
3236 }
3237 
3238 SKGError SKGDocument::executeSqliteOrder(const QString& iSqlOrder, const QMap< QString, QVariant >& iBind, int* iLastId) const
3239 {
3240     SKGError err;
3241     _SKGTRACEINFUNCRC(10, err)
3242     if (getMainDatabase() == nullptr) {
3243         err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
3244     } else {
3245         err = SKGServices::executeSqliteOrder(*getMainDatabase(), iSqlOrder, iBind, iLastId);
3246     }
3247     return err;
3248 }
3249 
3250 SKGError SKGDocument::dumpSelectSqliteOrder(const QString& iSqlOrder, QTextStream* oStream, SKGServices::DumpMode iMode) const
3251 {
3252     SKGError err;
3253     _SKGTRACEINFUNCRC(10, err)
3254     if (getMainDatabase() == nullptr) {
3255         err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
3256     } else {
3257         err = SKGServices::dumpSelectSqliteOrder(*getMainDatabase(), iSqlOrder, oStream, iMode);
3258     }
3259     return err;
3260 }
3261 
3262 SKGError SKGDocument::dumpSelectSqliteOrder(const QString& iSqlOrder, QString& oResult, SKGServices::DumpMode iMode) const
3263 {
3264     SKGError err;
3265     _SKGTRACEINFUNCRC(10, err)
3266     if (getMainDatabase() == nullptr) {
3267         err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
3268     } else {
3269         err = SKGServices::dumpSelectSqliteOrder(*getMainDatabase(), iSqlOrder, oResult, iMode);
3270     }
3271     return err;
3272 }
3273 
3274 SKGError SKGDocument::dumpSelectSqliteOrder(const QString& iSqlOrder, QStringList& oResult, SKGServices::DumpMode iMode) const
3275 {
3276     SKGError err;
3277     _SKGTRACEINFUNCRC(10, err)
3278     if (getMainDatabase() == nullptr) {
3279         err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
3280     } else {
3281         err = SKGServices::dumpSelectSqliteOrder(*getMainDatabase(), iSqlOrder, oResult, iMode);
3282     }
3283     return err;
3284 }
3285 
3286 SKGError SKGDocument::executeSingleSelectSqliteOrder(const QString& iSqlOrder, QString& oResult, bool iUseCache) const
3287 {
3288     SKGError err;
3289     _SKGTRACEINFUNCRC(10, err)
3290     // Is the select in cache?
3291     if (iUseCache && d->m_cache.contains(iSqlOrder)) {
3292         // Yes => get the value
3293         oResult = getCachedValue(iSqlOrder);
3294         SKGTRACEL(10) << "Result retrieved from cache for:" << iSqlOrder << SKGENDL;
3295 
3296     } else {
3297         // No => Run the select
3298         oResult.clear();
3299         double elapse = SKGServices::getMicroTime();
3300         if (getMainDatabase() == nullptr) {
3301             err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
3302         } else {
3303             err = SKGServices::executeSingleSelectSqliteOrder(*getMainDatabase(), iSqlOrder, oResult);
3304         }
3305 
3306         // Is the execution time too long
3307         if (SKGServices::getMicroTime() - elapse > 50) {
3308             // Yes => put the result in cache
3309             addValueInCache(iSqlOrder, oResult);
3310         }
3311     }
3312     return err;
3313 }
3314 
3315 SKGError SKGDocument::executeSelectSqliteOrder(const QString& iSqlOrder, SKGStringListList& oResult, bool iUseCache) const
3316 {
3317     SKGError err;
3318     _SKGTRACEINFUNCRC(10, err)
3319     // Is the select in cache?
3320     if (iUseCache && d->m_cacheSql->contains(iSqlOrder)) {
3321         // Yes => get the value
3322         oResult = getCachedSqlResult(iSqlOrder);
3323         SKGTRACEL(10) << "Result retrieved from cache for:" << iSqlOrder << SKGENDL;
3324 
3325     } else {
3326         // No => Run the select
3327         oResult.clear();
3328         double elapse = SKGServices::getMicroTime();
3329         if (getMainDatabase() == nullptr) {
3330             err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
3331         } else {
3332             err = SKGServices::executeSelectSqliteOrder(*getMainDatabase(), iSqlOrder, oResult);
3333         }
3334 
3335         // Is the execution time too long
3336         if (SKGServices::getMicroTime() - elapse > 50) {
3337             // Yes => put the result in cache
3338             addSqlResultInCache(iSqlOrder, oResult);
3339         }
3340     }
3341     return err;
3342 }
3343 
3344 void SKGDocument::concurrentExecuteSelectSqliteOrder(const QString& iSqlOrder, const FuncSelect& iFunction, bool iExecuteInMainThread) const
3345 {
3346     // Is the select in cache?
3347     if (d->m_cacheSql->contains(iSqlOrder)) {
3348         // Yes => get the value and call the function
3349         iFunction(getCachedSqlResult(iSqlOrder));
3350         SKGTRACEL(10) << "Result retrieved from cache for:" << iSqlOrder << SKGENDL;
3351 
3352     } else {
3353         // No => Run the select asynchronously
3354         // Search a watcher free
3355         QFutureWatcher<SKGStringListList>* watcher = nullptr;
3356         if (iExecuteInMainThread) {
3357             for (auto w  : qAsConst(d->m_watchers)) {
3358                 if (w->isFinished()) {
3359                     watcher = w;
3360                     break;
3361                 }
3362             }
3363             if (watcher == nullptr) {
3364                 d->m_watchers.push_back(new QFutureWatcher<SKGStringListList>());
3365                 watcher = d->m_watchers.at(d->m_watchers.count() - 1);
3366             }
3367 
3368             watcher->disconnect();
3369             connect(watcher, &QFutureWatcherBase::finished, this, [ = ] {
3370                 auto w = dynamic_cast< QFutureWatcher<SKGStringListList>* >(sender());
3371                 iFunction(w->result());
3372             });
3373         }
3374 
3375         // Launch in another thread the select
3376         auto future = QtConcurrent::run([ = ] {
3377             auto tmpDatabase = this->getThreadDatabase();
3378             double elapse = SKGServices::getMicroTime();
3379             SKGStringListList listTmp;
3380             SKGServices::executeSelectSqliteOrder(tmpDatabase, iSqlOrder, listTmp);
3381 
3382             // Is the execution time too long
3383             if (SKGServices::getMicroTime() - elapse > 50)
3384             {
3385                 // Yes => put the result in cache
3386                 addSqlResultInCache(iSqlOrder, listTmp);
3387             }
3388             if (!iExecuteInMainThread)
3389             {
3390                 iFunction(listTmp);
3391             }
3392             return listTmp;
3393         });
3394         if (watcher != nullptr) {
3395             watcher->setFuture(future);
3396         }
3397     }
3398 }
3399 
3400 void SKGDocument::concurrentExistObjects(const QString& iTable, const QString& iWhereClause, const FuncExist& iFunction, bool iExecuteInMainThread)
3401 {
3402     // Execute sqlorder
3403     concurrentExecuteSelectSqliteOrder("SELECT EXISTS(SELECT 1 FROM " % iTable % " WHERE " % (!iWhereClause.isEmpty() ?  iWhereClause  : QStringLiteral("1=1")) % ')',
3404     [ = ](const SKGStringListList & iResult) {
3405         iFunction(iResult.count() > 0 && (iResult.at(1).at(0) == QStringLiteral("1")));
3406     }, iExecuteInMainThread);
3407 }
3408 
3409 SKGError SKGDocument::getAttributesDescription(const QString& iTable, SKGServices::SKGAttributesList& oResult) const
3410 {
3411     SKGError err;
3412     _SKGTRACEINFUNCRC(10, err)
3413     // initialisation
3414     oResult.clear();
3415 
3416     // Search
3417     SKGStringListList temporaryResult;
3418     err = this->executeSelectSqliteOrder("PRAGMA table_info( " % iTable % " );", temporaryResult);
3419     IFOK(err) {
3420         int nblines = temporaryResult.count();
3421         QString realTable = SKGServices::getRealTable(iTable);
3422 
3423         for (int i = 1; i < nblines; ++i) {  // the first one is ignored because it is the headers
3424             QStringList line = temporaryResult.at(i);
3425 
3426             SKGServices::SKGAttributeInfo attribute;
3427             attribute.name = line[1];
3428 
3429             QString attname = realTable % '.' % attribute.name;
3430             attribute.display = getDisplay(attname);
3431             if (attribute.display == attname) {
3432                 attribute.display = QLatin1String("");
3433             }
3434             attribute.icon = getIcon(attname);
3435             attribute.type = getAttributeType(attribute.name);
3436             attribute.notnull = (line[3] == QStringLiteral("0"));
3437             attribute.defaultvalue = line[4];
3438             oResult.push_back(attribute);
3439         }
3440     }
3441 
3442     return err;
3443 }
3444 
3445 SKGError SKGDocument::getAttributesList(const QString& iTable, QStringList& oResult) const
3446 {
3447     SKGError err;
3448     _SKGTRACEINFUNCRC(10, err)
3449     oResult.clear();
3450     SKGServices::SKGAttributesList attDesc;
3451     err = getAttributesDescription(iTable, attDesc);
3452     int nblines = attDesc.count();
3453     for (int i = 0; !err && i < nblines; ++i) {
3454         oResult.push_back(attDesc.at(i).name);
3455     }
3456     return err;
3457 }
3458 
3459 SKGReport* SKGDocument::getReport() const
3460 {
3461     return new SKGReport(const_cast<SKGDocument*>(this));
3462 }
3463 
3464 SKGError SKGDocument::copyToJson(QString& oDocument) const
3465 {
3466     SKGError err;
3467     SKGTRACEINFUNCRC(10, err)
3468     QVariantMap doc;
3469 
3470     // Copy the tables
3471     QVariantList list;
3472     QStringList listTables = getMainDatabase()->tables();
3473     int nb = listTables.count();
3474     for (int i = 0; !err && i < nb; ++i) {
3475         const QString& tableName = listTables.at(i);
3476         if (Q_UNLIKELY(!tableName.startsWith(QLatin1String("sqlite_")) && !tableName.startsWith(QLatin1String("vm_")))) {
3477             list.clear();
3478 
3479             SKGStringListList listRows;
3480             if (getMainDatabase() == nullptr) {
3481                 err = SKGError(ERR_POINTER, i18nc("Error message", "No database defined"));
3482             } else {
3483                 err = SKGServices::executeSelectSqliteOrder(*getMainDatabase(), "SELECT * FROM " % tableName, listRows);
3484             }
3485             int nbRows = listRows.count();
3486             if (Q_LIKELY(nbRows)) {
3487                 QVariantMap item;
3488                 const QStringList& titles = listRows.at(0);
3489                 for (int j = 1; !err && j < nbRows; ++j) {  // Forget title
3490                     const QStringList& values = listRows.at(j);
3491 
3492                     int nbVals = values.count();
3493                     for (int k = 0; k < nbVals; ++k) {
3494                         const QString& t = titles.at(k);
3495                         SKGServices::AttributeType type = getAttributeType(t);
3496                         if (type == SKGServices::ID || type == SKGServices::INTEGER || type == SKGServices::LINK) {
3497                             item.insert(t, SKGServices::stringToInt(values.at(k)));
3498                         } else if (type == SKGServices::FLOAT) {
3499                             item.insert(t, SKGServices::stringToDouble(values.at(k)));
3500                         } else if (type == SKGServices::BOOL) {
3501                             item.insert(t, values.at(k) == QStringLiteral("Y"));
3502                         } else {
3503                             item.insert(t, values.at(k));
3504                         }
3505                     }
3506 
3507                     list << item;
3508                 }
3509             }
3510             doc.insert(tableName, list);
3511         }
3512     }
3513 
3514     QJsonDocument serializer = QJsonDocument::fromVariant(doc);
3515     oDocument = serializer.toJson(QJsonDocument::Compact);
3516     return err;
3517 }
3518 
3519 Q_DECLARE_METATYPE(sqlite3*)  // NOLINT(readability/casting)
3520 Q_DECLARE_OPAQUE_POINTER(sqlite3*)  // NOLINT(readability/casting)