summaryrefslogtreecommitdiffstats
path: root/SQLiteStudio3/coreSQLiteStudio/services
diff options
context:
space:
mode:
Diffstat (limited to 'SQLiteStudio3/coreSQLiteStudio/services')
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/bugreporter.cpp202
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/bugreporter.h62
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/codeformatter.cpp91
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/codeformatter.h30
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/collationmanager.h41
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/config.cpp9
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/config.h178
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/dbmanager.cpp17
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/dbmanager.h288
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/exportmanager.cpp283
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/exportmanager.h234
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/extralicensemanager.cpp28
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/extralicensemanager.h21
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/functionmanager.cpp39
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/functionmanager.h74
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.cpp125
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.h36
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.cpp770
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.h127
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.cpp524
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.h183
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.cpp694
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.h96
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.cpp820
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.h321
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/importmanager.cpp104
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/importmanager.h85
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/notifymanager.cpp85
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/notifymanager.h58
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/pluginmanager.h528
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/populatemanager.cpp93
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/populatemanager.h48
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp1058
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/updatemanager.h137
34 files changed, 7489 insertions, 0 deletions
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/bugreporter.cpp b/SQLiteStudio3/coreSQLiteStudio/services/bugreporter.cpp
new file mode 100644
index 0000000..54b0905
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/bugreporter.cpp
@@ -0,0 +1,202 @@
+#include "bugreporter.h"
+#include "services/config.h"
+#include "services/notifymanager.h"
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QUrlQuery>
+
+BugReporter::BugReporter(QObject *parent) :
+ QObject(parent)
+{
+ networkManager = new QNetworkAccessManager(this);
+ connect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(finished(QNetworkReply*)));
+}
+
+QUrl BugReporter::getReporterEmailHelpUrl() const
+{
+ return QUrl(QString::fromLatin1(reporterEmailHelpUrl));
+}
+
+QUrl BugReporter::getReporterUserAndPasswordHelpUrl() const
+{
+ return QUrl(QString::fromLatin1(reporterUserPassHelpUrl));
+}
+
+void BugReporter::validateBugReportCredentials(const QString& login, const QString& password)
+{
+ if (credentialsValidationInProgress)
+ {
+ credentialsValidationInProgress->abort();
+ credentialsValidationInProgress->deleteLater();
+ }
+
+ QUrlQuery query;
+ query.addQueryItem("validateUser", login);
+ query.addQueryItem("password", password);
+
+ QUrl url = QUrl(QString::fromLatin1(bugReportServiceUrl) + "?" + query.query(QUrl::FullyEncoded));
+ QNetworkRequest request(url);
+ credentialsValidationInProgress = networkManager->get(request);
+ replyToHandler[credentialsValidationInProgress] = [this](bool success, const QString& data)
+ {
+ if (success && data.trimmed() != "OK")
+ {
+ success = false;
+ emit credentialsValidationResult(success, tr("Invalid login or password"));
+ }
+ else
+ {
+ emit credentialsValidationResult(success, success ? QString() : data);
+ }
+ };
+}
+
+void BugReporter::abortCredentialsValidation()
+{
+ if (credentialsValidationInProgress)
+ {
+ credentialsValidationInProgress->abort();
+ credentialsValidationInProgress->deleteLater();
+ credentialsValidationInProgress = nullptr;
+ }
+}
+
+void BugReporter::useBugReportCredentials(const QString& login, const QString& password)
+{
+ CFG_CORE.Internal.BugReportUser.set(login);
+ CFG_CORE.Internal.BugReportPassword.set(password);
+}
+
+void BugReporter::clearBugReportCredentials()
+{
+ CFG_CORE.Internal.BugReportUser.set(QString());
+ CFG_CORE.Internal.BugReportPassword.set(QString());
+}
+
+void BugReporter::reportBug(const QString& title, const QString& details, const QString& version, const QString& os, const QString& plugins, BugReporter::ResponseHandler responseHandler, const QString& urlSuffix)
+{
+ static_qstring(contentsTpl, "%1\n\n<b>Plugins loaded:</b>\n%2\n\n<b>Version:</b>\n%3\n\n<b>Operating System:</b>\n%4");
+ QString contents = contentsTpl.arg(escapeParam(details), plugins, version, os);
+
+ QUrlQuery query;
+ query.addQueryItem("brief", escapeParam(title));
+ query.addQueryItem("contents", contents);
+ query.addQueryItem("os", os);
+ query.addQueryItem("version", version);
+ query.addQueryItem("featureRequest", "0");
+
+ QUrl url = QUrl(QString::fromLatin1(bugReportServiceUrl) + "?" + escapeUrl(query.query(QUrl::FullyEncoded) + urlSuffix));
+ QNetworkRequest request(url);
+ QNetworkReply* reply = networkManager->get(request);
+ if (responseHandler)
+ replyToHandler[reply] = responseHandler;
+
+ replyToTypeAndTitle[reply] = QPair<bool,QString>(false, title);
+}
+
+void BugReporter::requestFeature(const QString& title, const QString& details, BugReporter::ResponseHandler responseHandler, const QString& urlSuffix)
+{
+ QUrlQuery query;
+ query.addQueryItem("brief", escapeParam(title));
+ query.addQueryItem("contents", escapeParam(details));
+ query.addQueryItem("featureRequest", "1");
+
+ QUrl url = QUrl(QString::fromLatin1(bugReportServiceUrl) + "?" + escapeUrl(query.query(QUrl::FullyEncoded) + urlSuffix));
+ QNetworkRequest request(url);
+ QNetworkReply* reply = networkManager->get(request);
+ if (responseHandler)
+ replyToHandler[reply] = responseHandler;
+
+ replyToTypeAndTitle[reply] = QPair<bool,QString>(true, title);
+}
+
+QString BugReporter::escapeParam(const QString &input)
+{
+ return input.toHtmlEscaped();
+}
+
+QString BugReporter::escapeUrl(const QString &input)
+{
+ // For some reason the ";" character is not encodedy by QUrlQuery when using FullEncoded. Pity. We have to do it manually.
+ QString copy = input;
+ return copy.replace(";", "%3B");
+}
+
+void BugReporter::finished(QNetworkReply* reply)
+{
+ if (reply == credentialsValidationInProgress)
+ credentialsValidationInProgress = nullptr;
+
+ if (!replyToHandler.contains(reply))
+ {
+ reply->deleteLater();
+ return;
+ }
+
+ bool success = (reply->error() == QNetworkReply::NoError);
+ QString data;
+ if (success)
+ data = QString::fromLatin1(reply->readAll());
+ else
+ data = reply->errorString();
+
+ replyToHandler[reply](success, data);
+ replyToHandler.remove(reply);
+
+ if (replyToTypeAndTitle.contains(reply))
+ {
+ if (success)
+ CFG->addReportHistory(replyToTypeAndTitle[reply].first, replyToTypeAndTitle[reply].second, data);
+
+ replyToTypeAndTitle.remove(reply);
+ }
+
+ reply->deleteLater();
+}
+
+void BugReporter::reportBug(const QString& email, const QString& title, const QString& details, const QString& version, const QString& os, const QString& plugins,
+ ResponseHandler responseHandler)
+{
+ QUrlQuery query;
+ query.addQueryItem("byEmail", email);
+ QString urlSuffix = "&" + query.query(QUrl::FullyEncoded);
+
+ reportBug(title, details, version, os, plugins, responseHandler, urlSuffix);
+}
+
+void BugReporter::reportBug(const QString& title, const QString& details, const QString& version, const QString& os,
+ const QString& plugins, ResponseHandler responseHandler)
+{
+ QString user = CFG_CORE.Internal.BugReportUser.get();
+ QString pass = CFG_CORE.Internal.BugReportPassword.get();
+
+ QUrlQuery query;
+ query.addQueryItem("byUser", user);
+ query.addQueryItem("password", pass);
+ QString urlSuffix = "&" + query.query(QUrl::FullyEncoded);
+
+ reportBug(title, details, version, os, plugins, responseHandler, urlSuffix);
+}
+
+void BugReporter::requestFeature(const QString& email, const QString& title, const QString& details, ResponseHandler responseHandler)
+{
+ QUrlQuery query;
+ query.addQueryItem("byEmail", email);
+ QString urlSuffix = "&" + query.query(QUrl::FullyEncoded);
+
+ requestFeature(title, details, responseHandler, urlSuffix);
+}
+
+void BugReporter::requestFeature(const QString& title, const QString& details, ResponseHandler responseHandler)
+{
+ QString user = CFG_CORE.Internal.BugReportUser.get();
+ QString pass = CFG_CORE.Internal.BugReportPassword.get();
+
+ QUrlQuery query;
+ query.addQueryItem("byUser", user);
+ query.addQueryItem("password", pass);
+ QString urlSuffix = "&" + query.query(QUrl::FullyEncoded);
+
+ requestFeature(title, details, responseHandler, urlSuffix);
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/bugreporter.h b/SQLiteStudio3/coreSQLiteStudio/services/bugreporter.h
new file mode 100644
index 0000000..3e8eb8d
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/bugreporter.h
@@ -0,0 +1,62 @@
+#ifndef BUGREPORTER_H
+#define BUGREPORTER_H
+
+#include "common/global.h"
+#include "sqlitestudio.h"
+#include <QObject>
+#include <QHash>
+
+class QNetworkAccessManager;
+class QNetworkReply;
+
+class API_EXPORT BugReporter : public QObject
+{
+ Q_OBJECT
+
+ public:
+ typedef std::function<void(bool success, const QString& data)> ResponseHandler;
+
+ explicit BugReporter(QObject *parent = 0);
+
+ QUrl getReporterEmailHelpUrl() const;
+ QUrl getReporterUserAndPasswordHelpUrl() const;
+ void validateBugReportCredentials(const QString& login, const QString& password);
+ void abortCredentialsValidation();
+ void useBugReportCredentials(const QString& login, const QString& password);
+ void clearBugReportCredentials();
+
+ private:
+ void reportBug(const QString& title, const QString& details, const QString& version, const QString& os, const QString& plugins,
+ ResponseHandler responseHandler, const QString& urlSuffix);
+ void requestFeature(const QString& title, const QString& details, ResponseHandler responseHandler, const QString& urlSuffix);
+
+ static QString escapeParam(const QString& input);
+ static QString escapeUrl(const QString& input);
+
+ QNetworkAccessManager* networkManager = nullptr;
+ QHash<QNetworkReply*,ResponseHandler> replyToHandler;
+ QHash<QNetworkReply*,QPair<bool,QString>> replyToTypeAndTitle;
+ QNetworkReply* credentialsValidationInProgress = nullptr;
+
+ static_char* bugReportServiceUrl = "http://sqlitestudio.pl/report_bug3.rvt";
+ static_char* reporterEmailHelpUrl = "http://wiki.sqlitestudio.pl/index.php/User_Manual#Reporter_email_address";
+ static_char* reporterUserPassHelpUrl = "http://wiki.sqlitestudio.pl/index.php/User_Manual#Reporter_user_and_password";
+
+ signals:
+ void credentialsValidationResult(bool success, const QString& errorMessage);
+
+ private slots:
+ void finished(QNetworkReply* reply);
+
+ public slots:
+ void reportBug(const QString& email, const QString& title, const QString& details, const QString& version, const QString& os, const QString& plugins,
+ ResponseHandler responseHandler = nullptr);
+ void reportBug(const QString& title, const QString& details, const QString& version, const QString& os, const QString& plugins,
+ ResponseHandler responseHandler = nullptr);
+ void requestFeature(const QString& email, const QString& title, const QString& details, ResponseHandler responseHandler = nullptr);
+ void requestFeature(const QString& title, const QString& details, ResponseHandler responseHandler = nullptr);
+};
+
+#define BUGS SQLITESTUDIO->getBugReporter()
+
+#endif // BUGREPORTER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/codeformatter.cpp b/SQLiteStudio3/coreSQLiteStudio/services/codeformatter.cpp
new file mode 100644
index 0000000..e02508f
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/codeformatter.cpp
@@ -0,0 +1,91 @@
+#include "codeformatter.h"
+#include "parser/parser.h"
+#include "plugins/codeformatterplugin.h"
+#include "services/pluginmanager.h"
+#include <QDebug>
+
+void CodeFormatter::setFormatter(const QString& lang, CodeFormatterPlugin *formatterPlugin)
+{
+ currentFormatter[lang] = formatterPlugin;
+}
+
+CodeFormatterPlugin* CodeFormatter::getFormatter(const QString& lang)
+{
+ if (hasFormatter(lang))
+ return currentFormatter[lang];
+
+ return nullptr;
+}
+
+bool CodeFormatter::hasFormatter(const QString& lang)
+{
+ return currentFormatter.contains(lang);
+}
+
+void CodeFormatter::fullUpdate()
+{
+ availableFormatters.clear();
+ QList<CodeFormatterPlugin*> formatterPlugins = PLUGINS->getLoadedPlugins<CodeFormatterPlugin>();
+ for (CodeFormatterPlugin* plugin : formatterPlugins)
+ availableFormatters[plugin->getLanguage()][plugin->getName()] = plugin;
+
+ updateCurrent();
+}
+
+void CodeFormatter::updateCurrent()
+{
+ if (modifyingConfig)
+ return;
+
+ modifyingConfig = true;
+
+ bool modified = false;
+ currentFormatter.clear();
+ QHash<QString,QVariant> config = CFG_CORE.General.ActiveCodeFormatter.get();
+ QString name;
+ QStringList names = availableFormatters.keys();
+ qSort(names);
+ for (const QString& lang : names)
+ {
+ name = config[lang].toString();
+ if (config.contains(lang) && availableFormatters[lang].contains(name))
+ {
+ currentFormatter[lang] = availableFormatters[lang][name];
+ }
+ else
+ {
+ currentFormatter[lang] = availableFormatters[lang].begin().value();
+ config[lang] = currentFormatter[lang]->getName();
+ modified = true;
+ }
+ }
+
+ if (modified)
+ CFG_CORE.General.ActiveCodeFormatter.set(config);
+
+ modifyingConfig = false;
+}
+
+void CodeFormatter::storeCurrentSettings()
+{
+ QHash<QString,QVariant> config = CFG_CORE.General.ActiveCodeFormatter.get();
+ QHashIterator<QString,CodeFormatterPlugin*> it(currentFormatter);
+ while (it.hasNext())
+ {
+ it.next();
+ config[it.key()] = it.value()->getName();
+ }
+
+ CFG_CORE.General.ActiveCodeFormatter.set(config);
+}
+
+QString CodeFormatter::format(const QString& lang, const QString& code, Db* contextDb)
+{
+ if (!hasFormatter(lang))
+ {
+ qWarning() << "No formatter plugin defined for CodeFormatter for language:" << lang;
+ return code;
+ }
+
+ return currentFormatter[lang]->format(code, contextDb);
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/codeformatter.h b/SQLiteStudio3/coreSQLiteStudio/services/codeformatter.h
new file mode 100644
index 0000000..015bbfe
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/codeformatter.h
@@ -0,0 +1,30 @@
+#ifndef CODEFORMATTER_H
+#define CODEFORMATTER_H
+
+#include "coreSQLiteStudio_global.h"
+#include "sqlitestudio.h"
+
+class CodeFormatterPlugin;
+class Db;
+
+class API_EXPORT CodeFormatter
+{
+ public:
+ QString format(const QString& lang, const QString& code, Db* contextDb);
+
+ void setFormatter(const QString& lang, CodeFormatterPlugin* formatterPlugin);
+ CodeFormatterPlugin* getFormatter(const QString& lang);
+ bool hasFormatter(const QString& lang);
+ void fullUpdate();
+ void updateCurrent();
+ void storeCurrentSettings();
+
+ private:
+ QHash<QString,QHash<QString,CodeFormatterPlugin*>> availableFormatters;
+ QHash<QString,CodeFormatterPlugin*> currentFormatter;
+ bool modifyingConfig = false;
+};
+
+#define FORMATTER SQLITESTUDIO->getCodeFormatter()
+
+#endif // CODEFORMATTER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/collationmanager.h b/SQLiteStudio3/coreSQLiteStudio/services/collationmanager.h
new file mode 100644
index 0000000..5a05b0f
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/collationmanager.h
@@ -0,0 +1,41 @@
+#ifndef COLLATIONMANAGER_H
+#define COLLATIONMANAGER_H
+
+#include "coreSQLiteStudio_global.h"
+#include "common/global.h"
+#include <QList>
+#include <QSharedPointer>
+#include <QObject>
+#include <QStringList>
+
+class Db;
+
+class API_EXPORT CollationManager : public QObject
+{
+ Q_OBJECT
+
+ public:
+ struct API_EXPORT Collation
+ {
+ QString name;
+ QString lang;
+ QString code;
+ QStringList databases;
+ bool allDatabases = true;
+ };
+
+ typedef QSharedPointer<Collation> CollationPtr;
+
+ virtual void setCollations(const QList<CollationPtr>& newCollations) = 0;
+ virtual QList<CollationPtr> getAllCollations() const = 0;
+ virtual QList<CollationPtr> getCollationsForDatabase(const QString& dbName) const = 0;
+ virtual int evaluate(const QString& name, const QString& value1, const QString& value2) = 0;
+ virtual int evaluateDefault(const QString& value1, const QString& value2) = 0;
+
+ signals:
+ void collationListChanged();
+};
+
+#define COLLATIONS SQLITESTUDIO->getCollationManager()
+
+#endif // COLLATIONMANAGER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/config.cpp b/SQLiteStudio3/coreSQLiteStudio/services/config.cpp
new file mode 100644
index 0000000..1fef317
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/config.cpp
@@ -0,0 +1,9 @@
+#include "services/config.h"
+
+CFG_DEFINE(Core)
+
+static const QString DB_FILE_NAME = QStringLiteral("settings3");
+
+Config::~Config()
+{
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/config.h b/SQLiteStudio3/coreSQLiteStudio/services/config.h
new file mode 100644
index 0000000..5a3f594
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/config.h
@@ -0,0 +1,178 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#include "coreSQLiteStudio_global.h"
+#include "config_builder.h"
+#include "services/functionmanager.h"
+#include "collationmanager.h"
+#include "sqlitestudio.h"
+#include "common/utils.h"
+#include <QObject>
+#include <QVariant>
+#include <QHash>
+#include <QStringList>
+#include <QSharedPointer>
+#include <QDateTime>
+
+const int SQLITESTUDIO_CONFIG_VERSION = 1;
+
+CFG_CATEGORIES(Core,
+ CFG_CATEGORY(General,
+ CFG_ENTRY(int, SqlHistorySize, 10000)
+ CFG_ENTRY(int, DdlHistorySize, 1000)
+ CFG_ENTRY(QString, LoadedPlugins, "")
+ CFG_ENTRY(QVariantHash, ActiveCodeFormatter, QVariantHash())
+ CFG_ENTRY(bool, CheckUpdatesOnStartup, true)
+ )
+ CFG_CATEGORY(Console,
+ CFG_ENTRY(int, HistorySize, 100)
+ )
+ CFG_CATEGORY(Internal,
+ CFG_ENTRY(QVariantList, Functions, QVariantList())
+ CFG_ENTRY(QVariantList, Collations, QVariantList())
+ CFG_ENTRY(QString, BugReportUser, QString())
+ CFG_ENTRY(QString, BugReportPassword, QString())
+ CFG_ENTRY(QString, BugReportRecentTitle, QString())
+ CFG_ENTRY(QString, BugReportRecentContents, QString())
+ CFG_ENTRY(bool, BugReportRecentError, false)
+ )
+)
+
+#define CFG_CORE CFG_INSTANCE(Core)
+
+class QAbstractItemModel;
+class DdlHistoryModel;
+
+class API_EXPORT Config : public QObject
+{
+ Q_OBJECT
+
+ public:
+ virtual ~Config();
+
+ struct CfgDb
+ {
+ QString name;
+ QString path;
+ QHash<QString,QVariant> options;
+ };
+
+ typedef QSharedPointer<CfgDb> CfgDbPtr;
+
+ struct DbGroup;
+ typedef QSharedPointer<DbGroup> DbGroupPtr;
+
+ struct DbGroup
+ {
+ qint64 id;
+ QString referencedDbName;
+ QString name;
+ QList<DbGroupPtr> childs;
+ int order;
+ bool open = false;
+ };
+
+ struct SqlHistoryEntry
+ {
+ QString query;
+ QString dbName;
+ int rowsAffected;
+ int unixtime;
+ };
+
+ typedef QSharedPointer<SqlHistoryEntry> SqlHistoryEntryPtr;
+
+ struct DdlHistoryEntry
+ {
+ QString dbName;
+ QString dbFile;
+ QDateTime timestamp;
+ QString queries;
+ };
+
+ typedef QSharedPointer<DdlHistoryEntry> DdlHistoryEntryPtr;
+
+ struct ReportHistoryEntry
+ {
+ int id = 0;
+ bool isFeatureRequest = false;
+ int timestamp = 0;
+ QString title;
+ QString url;
+ };
+
+ typedef QSharedPointer<ReportHistoryEntry> ReportHistoryEntryPtr;
+
+ virtual void init() = 0;
+ virtual void cleanUp() = 0;
+ virtual const QString& getConfigDir() const = 0;
+ virtual QString getConfigFilePath() const = 0;
+
+ virtual void beginMassSave() = 0;
+ virtual void commitMassSave() = 0;
+ virtual void rollbackMassSave() = 0;
+ virtual bool isMassSaving() const = 0;
+ virtual void set(const QString& group, const QString& key, const QVariant& value) = 0;
+ virtual QVariant get(const QString& group, const QString& key) = 0;
+ virtual QHash<QString,QVariant> getAll() = 0;
+
+ virtual bool addDb(const QString& name, const QString& path, const QHash<QString, QVariant> &options) = 0;
+ virtual bool updateDb(const QString& name, const QString &newName, const QString& path, const QHash<QString, QVariant> &options) = 0;
+ virtual bool removeDb(const QString& name) = 0;
+ virtual bool isDbInConfig(const QString& name) = 0;
+ virtual QString getLastErrorString() const = 0;
+
+ /**
+ * @brief Provides list of all registered databases.
+ * @return List of database entries.
+ *
+ * Registered databases are those that user added to the application. They are not necessary valid or supported.
+ * They can be inexisting or unsupported, but they are kept in registry in case user fixes file path,
+ * or loads plugin to support it.
+ */
+ virtual QList<CfgDbPtr> dbList() = 0;
+ virtual CfgDbPtr getDb(const QString& dbName) = 0;
+
+ virtual void storeGroups(const QList<DbGroupPtr>& groups) = 0;
+ virtual QList<DbGroupPtr> getGroups() = 0;
+ virtual DbGroupPtr getDbGroup(const QString& dbName) = 0;
+
+ virtual qint64 addSqlHistory(const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) = 0;
+ virtual void updateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) = 0;
+ virtual void clearSqlHistory() = 0;
+ virtual QAbstractItemModel* getSqlHistoryModel() = 0;
+
+ virtual void addCliHistory(const QString& text) = 0;
+ virtual void applyCliHistoryLimit() = 0;
+ virtual void clearCliHistory() = 0;
+ virtual QStringList getCliHistory() const = 0;
+
+ virtual void addDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile) = 0;
+ virtual QList<DdlHistoryEntryPtr> getDdlHistoryFor(const QString& dbName, const QString& dbFile, const QDate& date) = 0;
+ virtual DdlHistoryModel* getDdlHistoryModel() = 0;
+ virtual void clearDdlHistory() = 0;
+
+ virtual void addReportHistory(bool isFeatureRequest, const QString& title, const QString& url) = 0;
+ virtual QList<ReportHistoryEntryPtr> getReportHistory() = 0;
+ virtual void deleteReport(int id) = 0;
+ virtual void clearReportHistory() = 0;
+
+ virtual void begin() = 0;
+ virtual void commit() = 0;
+ virtual void rollback() = 0;
+
+ signals:
+ void massSaveBegins();
+ void massSaveCommited();
+ void sqlHistoryRefreshNeeded();
+ void ddlHistoryRefreshNeeded();
+ void reportsHistoryRefreshNeeded();
+
+ public slots:
+ virtual void refreshSqlHistory() = 0;
+ virtual void refreshDdlHistory() = 0;
+};
+
+#define CFG SQLITESTUDIO->getConfig()
+
+#endif // CONFIG_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/dbmanager.cpp b/SQLiteStudio3/coreSQLiteStudio/services/dbmanager.cpp
new file mode 100644
index 0000000..91aff79
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/dbmanager.cpp
@@ -0,0 +1,17 @@
+#include "dbmanager.h"
+#include <QFileInfo>
+
+DbManager::DbManager(QObject *parent) :
+ QObject(parent)
+{
+}
+
+DbManager::~DbManager()
+{
+}
+
+QString DbManager::generateDbName(const QString &filePath)
+{
+ QFileInfo fi(filePath);
+ return fi.completeBaseName();
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/dbmanager.h b/SQLiteStudio3/coreSQLiteStudio/services/dbmanager.h
new file mode 100644
index 0000000..5a56151
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/dbmanager.h
@@ -0,0 +1,288 @@
+#ifndef DBMANAGER_H
+#define DBMANAGER_H
+
+#include "db/db.h"
+#include "coreSQLiteStudio_global.h"
+#include "common/global.h"
+#include "sqlitestudio.h"
+#include <QObject>
+#include <QList>
+#include <QHash>
+
+/** @file */
+
+class DbPlugin;
+class Config;
+class Plugin;
+class PluginType;
+
+/**
+ * @brief Database registry manager.
+ *
+ * Manages list of databases in SQLiteStudio core.
+ *
+ * It's a singleton asseccible with DBLIST macro.
+ */
+class API_EXPORT DbManager : public QObject
+{
+ Q_OBJECT
+
+ public:
+ /**
+ * @brief Creates database manager.
+ * @param parent Parent object passed to QObject constructor.
+ */
+ explicit DbManager(QObject *parent = 0);
+
+ /**
+ * @brief Default destructor.
+ */
+ ~DbManager();
+
+ /**
+ * @brief Adds database to the manager.
+ * @param name Symbolic name of the database, as it will be presented in the application.
+ * @param path Path to the database file.
+ * @param options Key-value custom options for database, that can be used in the DbPlugin implementation, like connection password, etc.
+ * @param permanent If true, then the database will be remembered in configuration, otherwise it will be disappear after application restart.
+ * @return true if the database has been successfully added, or false otherwise.
+ *
+ * The method can return false if given database file exists, but is not supported SQLite version (including invalid files,
+ * that are not SQLite database). It basicly returns false if DbPlugin#getInstance() returned null for given database parameters.
+ */
+ virtual bool addDb(const QString &name, const QString &path, const QHash<QString, QVariant> &options, bool permanent = true) = 0;
+
+ /**
+ * @overload
+ */
+ virtual bool addDb(const QString &name, const QString &path, bool permanent = true) = 0;
+
+ /**
+ * @brief Adds database as temporary, with generated name.
+ * @param path Path to database.
+ * @param options Key-value custom options for database.
+ * @return Added database name, if the database has been successfully added, or null string otherwise.
+ *
+ * This method is used for example when database was passed as argument to application command line arguments.
+ */
+ virtual QString quickAddDb(const QString &path, const QHash<QString, QVariant> &options) = 0;
+
+ /**
+ * @brief Updates registered database with new data.
+ * @param db Registered database.
+ * @param name New symbolic name for the database.
+ * @param path New database file path.
+ * @param options New database options. See addDb() for details.
+ * @param permanent True to make the database stored in configuration, false to make it disappear after application restart.
+ * @return true if the database was successfully updated, or false otherwise.
+ */
+ virtual bool updateDb(Db* db, const QString &name, const QString &path, const QHash<QString, QVariant> &options, bool permanent) = 0;
+
+ /**
+ * @brief Removes database from application.
+ * @param name Symbolic name of the database.
+ * @param cs Should the name be compare with case sensitivity?
+ */
+ virtual void removeDbByName(const QString& name, Qt::CaseSensitivity cs = Qt::CaseSensitive) = 0;
+
+ /**
+ * @brief Removes database from application.
+ * @param path Database file path as it was passed to addDb() or updateDb().
+ */
+ virtual void removeDbByPath(const QString& path) = 0;
+
+ /**
+ * @brief Removes database from application.
+ * @param db Database to be removed.
+ */
+ virtual void removeDb(Db* db) = 0;
+
+ /**
+ * @brief Gives list of databases registered in the application.
+ * @return List of databases, no matter if database is open or not.
+ *
+ * The results list includes invalid databases (not supported by driver plugin, or with no read access, etc).
+ */
+ virtual QList<Db*> getDbList() = 0;
+
+ /**
+ * @brief Gives list of valid databases.
+ * @return List of open databases.
+ */
+ virtual QList<Db*> getValidDbList() = 0;
+
+ /**
+ * @brief Gives list of currently open databases.
+ * @return List of open databases.
+ */
+ virtual QList<Db*> getConnectedDbList() = 0;
+
+ /**
+ * @brief Gives list of database names.
+ * @return List of database names that are registered in the application.
+ */
+ virtual QStringList getDbNames() = 0;
+
+ /**
+ * @brief Gives database object by its name.
+ * @param name Symbolic name of the database.
+ * @param cs Should the \p name be compared with case sensitivity?
+ * @return Database object, or null pointer if the database could not be found.
+ *
+ * This method is fast, as it uses hash table lookup.
+ */
+ virtual Db* getByName(const QString& name, Qt::CaseSensitivity cs = Qt::CaseSensitive) = 0;
+
+ /**
+ * @brief Gives database object by its file path.
+ * @param path Database file path as it was passed to addDb() or updateDb().
+ * @return Database matched by file path, or null if no database was found.
+ *
+ * This method is fast, as it uses hash table lookup.
+ */
+ virtual Db* getByPath(const QString& path) = 0;
+
+ /**
+ * @brief Creates in-memory SQLite3 database.
+ * @return Created database.
+ *
+ * Created database can be used for any purpose. Note that DbManager doesn't own created
+ * database and it's up to the caller to delete the database when it's no longer needed.
+ */
+ virtual Db* createInMemDb() = 0;
+
+ /**
+ * @brief Tells if given database is temporary.
+ * @param db Database to check.
+ * @return true if database is temporary, or false if it's stored in the configuration.
+ *
+ * Temporary databases are databases that are not stored in configuration and will not be restored
+ * upon next SQLiteStudio start. This can be decided by user on UI when he edits database registration info
+ * (there is a checkbox for that).
+ */
+ virtual bool isTemporary(Db* db) = 0;
+
+ /**
+ * @brief Generates database name.
+ * @param filePath Database file path.
+ * @return A name, using database file name as a hint for a name.
+ *
+ * This method doesn't care about uniqueness of the name. It just gets the file name from provided path
+ * and uses it as a name.
+ */
+ static QString generateDbName(const QString& filePath);
+
+ public slots:
+ /**
+ * @brief Rescans configuration for new database entries.
+ *
+ * Looks into the configuration for new databases. If there are any, adds them to list of managed databases.
+ */
+ virtual void scanForNewDatabasesInConfig() = 0;
+
+ /**
+ * @brief Sends signal to all interested entities, that databases are loaded.
+ *
+ * This is called by the managing entity (the SQLiteStudio instance) to let all know,
+ * that all db-related plugins and configuration related to databases are now loaded
+ * and list of databases in the manager is complete.
+ */
+ virtual void notifyDatabasesAreLoaded() = 0;
+
+ signals:
+ /**
+ * @brief Application just connected to the database.
+ * @param db Database object that the connection was made to.
+ *
+ * Emitted just after application has connected to the database.
+ */
+ void dbConnected(Db* db);
+
+ /**
+ * @brief Application just disconnected from the database.
+ * @param db Database object that the connection was closed with.
+ */
+ void dbDisconnected(Db* db);
+
+ /**
+ * @brief The database is about to be disconnected and user can still deny it.
+ * @param db Database to be closed.
+ * @param deny If set to true, then disconnecting will be aborted.
+ */
+ void dbAboutToBeDisconnected(Db* db, bool& deny);
+
+ /**
+ * @brief A database has been added to the application.
+ * @param db Database added.
+ * Emitted from addDb() methods in case of success.
+ */
+ void dbAdded(Db* db);
+
+ /**
+ * @brief A database has been removed from the application.
+ * @param db Database object that was removed. The object still exists, but will be removed soon after this signal is handled.
+ *
+ * Emitted from removeDb(). As the argument is a smart pointer, the object will be deleted after last reference to the pointer
+ * is deleted, which is very likely that the pointer instance in this signal is the last one.
+ */
+ void dbRemoved(Db* db);
+
+ /**
+ * @brief A database registration data has been updated.
+ * @param oldName The name of the database before the update - in case the name was updated.
+ * @param db Database object that was updated.
+ *
+ * Emitted from updateDb() after successful update.
+ *
+ * The name of the database is a key for tables related to the databases, so if it changed, we dbUpdated() provides
+ * the original name before update, so any tables can be updated basing on the old name.
+ */
+ void dbUpdated(const QString& oldName, Db* db);
+
+ /**
+ * @brief Loaded plugin to support the database.
+ * @param db Database object handled by the plugin.
+ *
+ * Emitted after a plugin was loaded and it turned out to handle the database that was already registered in the application,
+ * but wasn't managed by database manager, because no handler plugin was loaded earlier.
+ *
+ * Also emitted when database details were edited and saved, which fixes database configuration (for example path).
+ */
+ void dbLoaded(Db* db);
+
+ /**
+ * @brief Plugin supporting the database is about to be unloaded.
+ * @param db Database object to be removed from the manager.
+ * @param plugin Plugin that handles the database.
+ *
+ * Emitted when PluginManager is about to unload the plugin which is handling the database.
+ * All classes using this database object should stop using it immediately, or the application may crash.
+ *
+ * The plugin itself should not use this signal. Instead it should implement Plugin::deinit() method
+ * to perform deinitialization before unloading. The Plugin::deinit() method is called before this signal is emitted.
+ */
+ void dbAboutToBeUnloaded(Db* db, DbPlugin* plugin);
+
+ /**
+ * @brief Plugins supporting the database was just unloaded.
+ * @param db The new database object (InvalidDb) that replaced the previous one.
+ *
+ * This is emitted after the plugin for the database was unloaded. The \p db object is now a different object.
+ * It is of InvalidDb class and it represents a database in an invalid state. It still has name, path and connection options,
+ * but no operation can be performed on the database.
+ */
+ void dbUnloaded(Db* db);
+
+ /**
+ * @brief Emited when the initial database list has been loaded.
+ */
+ void dbListLoaded();
+};
+
+/**
+ * @brief Database manager.
+ * Provides direct access to the database manager.
+ */
+#define DBLIST SQLITESTUDIO->getDbManager()
+
+#endif // DBMANAGER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/exportmanager.cpp b/SQLiteStudio3/coreSQLiteStudio/services/exportmanager.cpp
new file mode 100644
index 0000000..6f916bc
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/exportmanager.cpp
@@ -0,0 +1,283 @@
+#include "exportmanager.h"
+#include "services/pluginmanager.h"
+#include "plugins/exportplugin.h"
+#include "services/notifymanager.h"
+#include "db/queryexecutor.h"
+#include "exportworker.h"
+#include <QThreadPool>
+#include <QTextCodec>
+#include <QBuffer>
+#include <QDebug>
+#include <QDir>
+#include <QFile>
+
+ExportManager::ExportManager(QObject *parent) :
+ PluginServiceBase(parent)
+{
+}
+
+ExportManager::~ExportManager()
+{
+ safe_delete(config);
+}
+
+QStringList ExportManager::getAvailableFormats(ExportMode exportMode) const
+{
+ QStringList formats;
+ for (ExportPlugin* plugin : PLUGINS->getLoadedPlugins<ExportPlugin>())
+ {
+ if (exportMode == UNDEFINED || plugin->getSupportedModes().testFlag(exportMode))
+ formats << plugin->getFormatName();
+ }
+
+ return formats;
+}
+
+void ExportManager::configure(const QString& format, const StandardExportConfig& config)
+{
+ configure(format, new StandardExportConfig(config));
+}
+
+void ExportManager::configure(const QString& format, StandardExportConfig* config)
+{
+ if (exportInProgress)
+ {
+ qWarning() << "Tried to configure export while another export is in progress.";
+ return;
+ }
+
+ plugin = getPluginForFormat(format);
+ if (!plugin)
+ {
+ invalidFormat(format);
+ return;
+ }
+
+ safe_delete(this->config);
+ this->config = config;
+}
+
+bool ExportManager::isExportInProgress() const
+{
+ return exportInProgress;
+}
+
+void ExportManager::exportQueryResults(Db* db, const QString& query)
+{
+ if (!checkInitialConditions())
+ return;
+
+ if (!plugin->getSupportedModes().testFlag(QUERY_RESULTS))
+ {
+ notifyError(tr("Export plugin %1 doesn't support exporing query results.").arg(plugin->getFormatName()));
+ emit exportFailed();
+ emit exportFinished();
+ return;
+ }
+
+ exportInProgress = true;
+ mode = QUERY_RESULTS;
+
+ ExportWorker* worker = prepareExport();
+ if (!worker)
+ return;
+
+ worker->prepareExportQueryResults(db, query);
+ QThreadPool::globalInstance()->start(worker);
+}
+
+void ExportManager::exportTable(Db* db, const QString& database, const QString& table)
+{
+ static const QString sql = QStringLiteral("SELECT * FROM %1");
+
+ if (!checkInitialConditions())
+ return;
+
+ if (!plugin->getSupportedModes().testFlag(TABLE))
+ {
+ notifyError(tr("Export plugin %1 doesn't support exporing tables.").arg(plugin->getFormatName()));
+ emit exportFailed();
+ emit exportFinished();
+ return;
+ }
+
+ exportInProgress = true;
+ mode = TABLE;
+
+ ExportWorker* worker = prepareExport();
+ if (!worker)
+ return;
+
+ worker->prepareExportTable(db, database, table);
+ QThreadPool::globalInstance()->start(worker);
+}
+
+void ExportManager::exportDatabase(Db* db, const QStringList& objectListToExport)
+{
+ if (!checkInitialConditions())
+ return;
+
+ if (!plugin->getSupportedModes().testFlag(DATABASE))
+ {
+ notifyError(tr("Export plugin %1 doesn't support exporing databases.").arg(plugin->getFormatName()));
+ emit exportFailed();
+ emit exportFinished();
+ return;
+ }
+
+ exportInProgress = true;
+ mode = DATABASE;
+
+ ExportWorker* worker = prepareExport();
+ if (!worker)
+ return;
+
+ worker->prepareExportDatabase(db, objectListToExport);
+ QThreadPool::globalInstance()->start(worker);
+}
+
+void ExportManager::interrupt()
+{
+ emit orderWorkerToInterrupt();
+}
+
+ExportPlugin* ExportManager::getPluginForFormat(const QString& formatName) const
+{
+ for (ExportPlugin* plugin : PLUGINS->getLoadedPlugins<ExportPlugin>())
+ if (plugin->getFormatName() == formatName)
+ return plugin;
+
+ return nullptr;
+}
+
+void ExportManager::invalidFormat(const QString& format)
+{
+ notifyError(tr("Export format '%1' is not supported. Supported formats are: %2.").arg(format).arg(getAvailableFormats().join(", ")));
+}
+
+bool ExportManager::checkInitialConditions()
+{
+ if (exportInProgress)
+ {
+ qWarning() << "Tried to call export while another export is in progress.";
+ emit exportFailed();
+ emit exportFinished();
+ return false;
+ }
+
+ if (!plugin)
+ {
+ qWarning() << "Tried to call export while no export plugin was configured.";
+ emit exportFailed();
+ emit exportFinished();
+ return false;
+ }
+
+ return true;
+}
+
+ExportWorker* ExportManager::prepareExport()
+{
+ bool usesOutput = plugin->getSupportedModes().testFlag(FILE) || plugin->getSupportedModes().testFlag(CLIPBOARD);
+ QIODevice* output = nullptr;
+ if (usesOutput)
+ {
+ output = getOutputStream();
+ if (!output)
+ {
+ emit exportFailed();
+ emit exportFinished();
+ exportInProgress = false;
+ return nullptr;
+ }
+ }
+
+ ExportWorker* worker = new ExportWorker(plugin, config, output);
+ connect(worker, SIGNAL(finished(bool,QIODevice*)), this, SLOT(finalizeExport(bool,QIODevice*)));
+ connect(this, SIGNAL(orderWorkerToInterrupt()), worker, SLOT(interrupt()));
+ return worker;
+}
+
+void ExportManager::handleClipboardExport()
+{
+ if (plugin->getMimeType().isNull())
+ {
+ QString str = codecForName(config->codec)->toUnicode(bufferForClipboard->buffer());
+ emit storeInClipboard(str);
+ }
+ else
+ emit storeInClipboard(bufferForClipboard->buffer(), plugin->getMimeType());
+}
+
+void ExportManager::finalizeExport(bool result, QIODevice* output)
+{
+ if (result)
+ {
+ if (config->intoClipboard)
+ {
+ notifyInfo(tr("Export to the clipboard was successful."));
+ handleClipboardExport();
+ }
+ else if (!config->outputFileName.isEmpty())
+ notifyInfo(tr("Export to the file '%1' was successful.").arg(config->outputFileName));
+ else
+ notifyInfo(tr("Export to was successful.").arg(config->outputFileName));
+
+ emit exportSuccessful();
+ }
+ else
+ {
+ emit exportFailed();
+ }
+ emit exportFinished();
+
+ if (output)
+ {
+ output->close();
+ delete output;
+ }
+
+ bufferForClipboard = nullptr;
+ exportInProgress = false;
+}
+
+QIODevice* ExportManager::getOutputStream()
+{
+ QFile::OpenMode openMode;
+ if (config->intoClipboard)
+ {
+ openMode = QIODevice::WriteOnly;
+ if (!plugin->isBinaryData())
+ openMode |= QIODevice::Text;
+
+ bufferForClipboard = new QBuffer();
+ bufferForClipboard->open(openMode);
+ return bufferForClipboard;
+ }
+ else if (!config->outputFileName.trimmed().isEmpty())
+ {
+ openMode = QIODevice::WriteOnly|QIODevice::Truncate;
+ if (!plugin->isBinaryData())
+ openMode |= QIODevice::Text;
+
+ QFile* file = new QFile(config->outputFileName);
+ if (!file->open(openMode))
+ {
+ notifyError(tr("Could not export to file %1. File cannot be open for writting.").arg(config->outputFileName));
+ delete file;
+ return nullptr;
+ }
+ return file;
+ }
+ else
+ {
+ qCritical() << "ExportManager::getOutputStream(): neither clipboard or output file was specified";
+ }
+
+ return nullptr;
+}
+
+bool ExportManager::isAnyPluginAvailable()
+{
+ return !PLUGINS->getLoadedPlugins<ExportPlugin>().isEmpty();
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/exportmanager.h b/SQLiteStudio3/coreSQLiteStudio/services/exportmanager.h
new file mode 100644
index 0000000..64d1136
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/exportmanager.h
@@ -0,0 +1,234 @@
+#ifndef EXPORTMANAGER_H
+#define EXPORTMANAGER_H
+
+#include "coreSQLiteStudio_global.h"
+#include "db/sqlquery.h"
+#include "db/db.h"
+#include "pluginservicebase.h"
+#include "sqlitestudio.h"
+#include <QObject>
+
+class ExportPlugin;
+class QueryExecutor;
+class ExportWorker;
+class QBuffer;
+class CfgEntry;
+
+/**
+ * @brief Provides database exporting capabilities.
+ *
+ * ExportManager is not thread-safe. Use it from single thread.
+ */
+class API_EXPORT ExportManager : public PluginServiceBase
+{
+ Q_OBJECT
+ public:
+ enum ExportMode
+ {
+ UNDEFINED = 0x00,
+ CLIPBOARD = 0x01,
+ DATABASE = 0x02,
+ TABLE = 0x04,
+ QUERY_RESULTS = 0x08,
+ FILE = 0x10
+ };
+
+ Q_DECLARE_FLAGS(ExportModes, ExportMode)
+
+ /**
+ * @brief Flags for requesting additional information for exporting by plugins.
+ *
+ * Each plugin implementation might ask ExportWorker to provide additional information for exporting.
+ * Such information is usually expensive operation (an additional database query to execute), therefore
+ * they are not enabled by default for all plugins. Each plugin has to ask for them individually
+ * by returning this enum values from ExportPlugin::getProviderFlags().
+ *
+ * For each enum value returned from the ExportPlugin::getProviderFlags(), a single QHash entry will be prepared
+ * and that hash will be then passed to one of ExportPlugin::beforeExportQueryResults(), ExportPlugin::exportTable(),
+ * or ExportPlugin::exportVirtualTable(). If no flags were returned from ExportPlugin::getProviderFlags(), then
+ * empty hash will be passed to those methods.
+ *
+ * Each entry in the QHash has a key equal to one of values from this enum. Values from the hash are of QVariant type
+ * and therefore they need to be casted (by QVariant means) into desired type. For each enum value its description
+ * will tell you what actually is stored in the QVariant, so you can extract the information.
+ */
+ enum ExportProviderFlag
+ {
+ NONE = 0x00, /**< This is a default. Nothing will be stored in the hash. */
+ DATA_LENGTHS = 0x01, /**<
+ * Will provide maximum number of characters or bytes (depending on column type)
+ * for each exported table or qurey result column. It will be a <tt>QList&lt;int&gt;</tt>.
+ */
+ ROW_COUNT = 0x02 /**<
+ * Will provide total number of rows that will be exported for the table or query results.
+ * It will be an integer value.
+ */
+ };
+
+ Q_DECLARE_FLAGS(ExportProviderFlags, ExportProviderFlag)
+
+ struct ExportObject
+ {
+ enum Type
+ {
+ TABLE,
+ INDEX,
+ TRIGGER,
+ VIEW
+ };
+
+ Type type;
+ QString database; // TODO fill when dbnames are fully supported
+ QString name;
+ QString ddl;
+ SqlQueryPtr data;
+ QHash<ExportManager::ExportProviderFlag,QVariant> providerData;
+ };
+
+ typedef QSharedPointer<ExportObject> ExportObjectPtr;
+
+ /**
+ * @brief Standard configuration for all exporting processes.
+ *
+ * Object of this type is passed to all exporting processes.
+ * It is configured with standard UI config for export.
+ */
+ struct StandardExportConfig
+ {
+ /**
+ * @brief Text encoding.
+ *
+ * Always one of QTextCodec::availableCodecs().
+ */
+ QString codec;
+
+ /**
+ * @brief Name of the file that the export being done to.
+ *
+ * This is provided just for information to the export process,
+ * but the plugin should use data stream provided to each called export method,
+ * instead of opening the file from this name.
+ *
+ * It will be null string if exporting is not performed into a file, but somewhere else
+ * (for example into a clipboard).
+ */
+ QString outputFileName;
+
+ /**
+ * @brief Indicates exporting to clipboard.
+ *
+ * This is just for an information, like outputFileName. Exporting methods will
+ * already have stream prepared for exporting to clipboard.
+ *
+ * Default is false.
+ */
+ bool intoClipboard = false;
+
+ /**
+ * @brief When exporting table or database, this indicates if table data should also be exported.
+ *
+ * Default is true.
+ */
+ bool exportData = true;
+
+ /**
+ * @brief When exporting only a table, this indicates if indexes related to that table should also be exported.
+ *
+ * Default is true.
+ */
+ bool exportTableIndexes = true;
+
+ /**
+ * @brief When exporting only a table, this indicates if triggers related to that table should also be exported.
+ *
+ * Default is true.
+ */
+ bool exportTableTriggers = true;
+ };
+
+ /**
+ * @brief Standard export configuration options to be displayed on UI.
+ *
+ * Each of enum represents single property of StandardExportConfig which will be
+ * available on UI to configure.
+ */
+ enum StandardConfigFlag
+ {
+ CODEC = 0x01, /**< Text encoding (see StandardExportConfig::codec). */
+ };
+
+ Q_DECLARE_FLAGS(StandardConfigFlags, StandardConfigFlag)
+
+
+ explicit ExportManager(QObject *parent = 0);
+ ~ExportManager();
+
+ QStringList getAvailableFormats(ExportMode exportMode = UNDEFINED) const;
+ ExportPlugin* getPluginForFormat(const QString& formatName) const;
+
+ /**
+ * @brief Configures export service for export.
+ * @param format Format to be used in upcoming export.
+ * @param config Standard configuration options to be used in upcoming export.
+ *
+ * ExportManager takes ownership of the config object.
+ *
+ * Call this method just befor any of export*() methods is called to prepare ExportManager for upcoming export process.
+ * Otherwise the export process will use settings from last configure() call.
+ *
+ * If any export is already in progress, this method reports error in logs and does nothing.
+ * If plugin for specified format cannot be found, then this method reports warning in logs and does nothing.
+ */
+ void configure(const QString& format, StandardExportConfig* config);
+
+ /**
+ * @brief Configures export service for export.
+ * @param format Format to be used in upcoming export.
+ * @param config Standard configuration options to be used in upcoming export.
+ *
+ * Same as method above, except it makes its own copy of the config object.
+ */
+ void configure(const QString& format, const StandardExportConfig& config);
+ bool isExportInProgress() const;
+ void exportQueryResults(Db* db, const QString& query);
+ void exportTable(Db* db, const QString& database, const QString& table);
+ void exportDatabase(Db* db, const QStringList& objectListToExport);
+
+ static bool isAnyPluginAvailable();
+
+ private:
+ void invalidFormat(const QString& format);
+ bool checkInitialConditions();
+ QIODevice* getOutputStream();
+ ExportWorker* prepareExport();
+ void handleClipboardExport();
+
+ bool exportInProgress = false;
+ ExportMode mode;
+ StandardExportConfig* config = nullptr;
+ QString format;
+ ExportPlugin* plugin = nullptr;
+ QBuffer* bufferForClipboard = nullptr;
+
+ public slots:
+ void interrupt();
+
+ private slots:
+ void finalizeExport(bool result, QIODevice* output);
+
+ signals:
+ void exportFinished();
+ void exportSuccessful();
+ void exportFailed();
+ void storeInClipboard(const QString& str);
+ void storeInClipboard(const QByteArray& bytes, const QString& mimeType);
+ void orderWorkerToInterrupt();
+};
+
+#define EXPORT_MANAGER SQLITESTUDIO->getExportManager()
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(ExportManager::StandardConfigFlags)
+Q_DECLARE_OPERATORS_FOR_FLAGS(ExportManager::ExportModes)
+Q_DECLARE_OPERATORS_FOR_FLAGS(ExportManager::ExportProviderFlags)
+
+#endif // EXPORTMANAGER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/extralicensemanager.cpp b/SQLiteStudio3/coreSQLiteStudio/services/extralicensemanager.cpp
new file mode 100644
index 0000000..23fb513
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/extralicensemanager.cpp
@@ -0,0 +1,28 @@
+#include "extralicensemanager.h"
+
+ExtraLicenseManager::ExtraLicenseManager()
+{
+}
+
+bool ExtraLicenseManager::addLicense(const QString& title, const QString& filePath)
+{
+ if (licenses.contains(title))
+ return false;
+
+ licenses[title] = filePath;
+ return true;
+}
+
+bool ExtraLicenseManager::removeLicense(const QString& title)
+{
+ if (!licenses.contains(title))
+ return false;
+
+ licenses.remove(title);
+ return true;
+}
+
+const QHash<QString, QString>&ExtraLicenseManager::getLicenses() const
+{
+ return licenses;
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/extralicensemanager.h b/SQLiteStudio3/coreSQLiteStudio/services/extralicensemanager.h
new file mode 100644
index 0000000..fcf1203
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/extralicensemanager.h
@@ -0,0 +1,21 @@
+#ifndef EXTRALICENSEMANAGER_H
+#define EXTRALICENSEMANAGER_H
+
+#include "coreSQLiteStudio_global.h"
+#include <QString>
+#include <QHash>
+
+class API_EXPORT ExtraLicenseManager
+{
+ public:
+ ExtraLicenseManager();
+
+ bool addLicense(const QString& title, const QString& filePath);
+ bool removeLicense(const QString& title);
+ const QHash<QString,QString>& getLicenses() const;
+
+ private:
+ QHash<QString,QString> licenses;
+};
+
+#endif // EXTRALISENCEMANAGER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/functionmanager.cpp b/SQLiteStudio3/coreSQLiteStudio/services/functionmanager.cpp
new file mode 100644
index 0000000..10db318
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/functionmanager.cpp
@@ -0,0 +1,39 @@
+#include "services/functionmanager.h"
+
+FunctionManager::FunctionBase::FunctionBase()
+{
+}
+
+FunctionManager::FunctionBase::~FunctionBase()
+{
+}
+
+QString FunctionManager::FunctionBase::toString() const
+{
+ static const QString format = "%1(%2)";
+ QString args = undefinedArgs ? "..." : arguments.join(", ");
+ return format.arg(name).arg(args);
+}
+
+QString FunctionManager::FunctionBase::typeString(Type type)
+{
+ switch (type)
+ {
+ case ScriptFunction::SCALAR:
+ return "SCALAR";
+ case ScriptFunction::AGGREGATE:
+ return "AGGREGATE";
+ }
+ return QString::null;
+}
+
+FunctionManager::ScriptFunction::Type FunctionManager::FunctionBase::typeString(const QString& type)
+{
+ if (type == "SCALAR")
+ return ScriptFunction::SCALAR;
+
+ if (type == "AGGREGATE")
+ return ScriptFunction::AGGREGATE;
+
+ return ScriptFunction::SCALAR;
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/functionmanager.h b/SQLiteStudio3/coreSQLiteStudio/services/functionmanager.h
new file mode 100644
index 0000000..b848c93
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/functionmanager.h
@@ -0,0 +1,74 @@
+#ifndef FUNCTIONMANAGER_H
+#define FUNCTIONMANAGER_H
+
+#include "coreSQLiteStudio_global.h"
+#include "common/global.h"
+#include <QList>
+#include <QSharedPointer>
+#include <QObject>
+#include <QStringList>
+
+class Db;
+
+class API_EXPORT FunctionManager : public QObject
+{
+ Q_OBJECT
+
+ public:
+ struct API_EXPORT FunctionBase
+ {
+ enum Type
+ {
+ SCALAR = 0,
+ AGGREGATE = 1
+ };
+
+ FunctionBase();
+ virtual ~FunctionBase();
+
+ virtual QString toString() const;
+
+ static QString typeString(Type type);
+ static Type typeString(const QString& type);
+
+ QString name;
+ QStringList arguments;
+ Type type = SCALAR;
+ bool undefinedArgs = true;
+ };
+
+ struct API_EXPORT ScriptFunction : public FunctionBase
+ {
+ QString lang;
+ QString code;
+ QString initCode;
+ QString finalCode;
+ QStringList databases;
+ bool allDatabases = true;
+ };
+
+ struct API_EXPORT NativeFunction : public FunctionBase
+ {
+ typedef std::function<QVariant(const QList<QVariant>& args, Db* db, bool& ok)> ImplementationFunction;
+
+ ImplementationFunction functionPtr;
+ };
+
+ virtual void setScriptFunctions(const QList<ScriptFunction*>& newFunctions) = 0;
+ virtual QList<ScriptFunction*> getAllScriptFunctions() const = 0;
+ virtual QList<ScriptFunction*> getScriptFunctionsForDatabase(const QString& dbName) const = 0;
+ virtual QList<NativeFunction*> getAllNativeFunctions() const = 0;
+
+ virtual QVariant evaluateScalar(const QString& name, int argCount, const QList<QVariant>& args, Db* db, bool& ok) = 0;
+ virtual void evaluateAggregateInitial(const QString& name, int argCount, Db* db, QHash<QString, QVariant>& aggregateStorage) = 0;
+ virtual void evaluateAggregateStep(const QString& name, int argCount, const QList<QVariant>& args, Db* db,
+ QHash<QString, QVariant>& aggregateStorage) = 0;
+ virtual QVariant evaluateAggregateFinal(const QString& name, int argCount, Db* db, bool& ok, QHash<QString, QVariant>& aggregateStorage) = 0;
+
+ signals:
+ void functionListChanged();
+};
+
+#define FUNCTIONS SQLITESTUDIO->getFunctionManager()
+
+#endif // FUNCTIONMANAGER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.cpp
new file mode 100644
index 0000000..5876021
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.cpp
@@ -0,0 +1,125 @@
+#include "collationmanagerimpl.h"
+#include "services/pluginmanager.h"
+#include "plugins/scriptingplugin.h"
+#include "services/notifymanager.h"
+#include "services/dbmanager.h"
+#include "common/utils.h"
+#include <QDebug>
+
+CollationManagerImpl::CollationManagerImpl()
+{
+ init();
+}
+
+void CollationManagerImpl::setCollations(const QList<CollationManager::CollationPtr>& newCollations)
+{
+ collations = newCollations;
+ refreshCollationsByKey();
+ storeInConfig();
+ emit collationListChanged();
+}
+
+QList<CollationManager::CollationPtr> CollationManagerImpl::getAllCollations() const
+{
+ return collations;
+}
+
+QList<CollationManager::CollationPtr> CollationManagerImpl::getCollationsForDatabase(const QString& dbName) const
+{
+ QList<CollationPtr> results;
+ foreach (const CollationPtr& coll, collations)
+ {
+ if (coll->allDatabases || coll->databases.contains(dbName, Qt::CaseInsensitive))
+ results << coll;
+ }
+ return results;
+}
+
+int CollationManagerImpl::evaluate(const QString& name, const QString& value1, const QString& value2)
+{
+ if (!collationsByKey.contains(name))
+ {
+ qWarning() << "Could not find requested collation" << name << ", so using default collation.";
+ return evaluateDefault(value1, value2);
+ }
+
+ ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(collationsByKey[name]->lang);
+ if (!plugin)
+ {
+ qWarning() << "Plugin for collation" << name << ", not loaded, so using default collation.";
+ return evaluateDefault(value1, value2);
+ }
+
+ QString err;
+ QVariant result = plugin->evaluate(collationsByKey[name]->code, {value1, value2}, &err);
+
+ if (!err.isNull())
+ {
+ qWarning() << "Error while evaluating collation:" << err;
+ return evaluateDefault(value1, value2);
+ }
+
+ bool ok;
+ int intResult = result.toInt(&ok);
+ if (!ok)
+ {
+ qWarning() << "Not integer result from collation:" << result.toString();
+ return evaluateDefault(value1, value2);
+ }
+
+ return intResult;
+}
+
+int CollationManagerImpl::evaluateDefault(const QString& value1, const QString& value2)
+{
+ return value1.compare(value2, Qt::CaseInsensitive);
+}
+
+void CollationManagerImpl::init()
+{
+ loadFromConfig();
+ refreshCollationsByKey();
+}
+
+void CollationManagerImpl::storeInConfig()
+{
+ QVariantList list;
+ QHash<QString,QVariant> collHash;
+ for (CollationPtr coll : collations)
+ {
+ collHash["name"] = coll->name;
+ collHash["lang"] = coll->lang;
+ collHash["code"] = coll->code;
+ collHash["allDatabases"] = coll->allDatabases;
+ collHash["databases"] =common(DBLIST->getDbNames(), coll->databases);
+ list << collHash;
+ }
+ CFG_CORE.Internal.Collations.set(list);
+}
+
+void CollationManagerImpl::loadFromConfig()
+{
+ collations.clear();
+
+ QVariantList list = CFG_CORE.Internal.Collations.get();
+ QHash<QString,QVariant> collHash;
+ CollationPtr coll;
+ for (const QVariant& var : list)
+ {
+ collHash = var.toHash();
+ coll = CollationPtr::create();
+ coll->name = collHash["name"].toString();
+ coll->lang = collHash["lang"].toString();
+ coll->code = collHash["code"].toString();
+ coll->databases = collHash["databases"].toStringList();
+ coll->allDatabases = collHash["allDatabases"].toBool();
+ collations << coll;
+ }
+}
+
+void CollationManagerImpl::refreshCollationsByKey()
+{
+ collationsByKey.clear();
+ foreach (CollationPtr collation, collations)
+ collationsByKey[collation->name] = collation;
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.h
new file mode 100644
index 0000000..f5a5937
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.h
@@ -0,0 +1,36 @@
+#ifndef COLLATIONMANAGERIMPL_H
+#define COLLATIONMANAGERIMPL_H
+
+#include "services/collationmanager.h"
+
+class ScriptingPlugin;
+class Plugin;
+class PluginType;
+
+class API_EXPORT CollationManagerImpl : public CollationManager
+{
+ public:
+ CollationManagerImpl();
+
+ void setCollations(const QList<CollationPtr>& newCollations);
+ QList<CollationPtr> getAllCollations() const;
+ QList<CollationPtr> getCollationsForDatabase(const QString& dbName) const;
+ int evaluate(const QString& name, const QString& value1, const QString& value2);
+ int evaluateDefault(const QString& value1, const QString& value2);
+
+ private:
+ void init();
+ void storeInConfig();
+ void loadFromConfig();
+ void refreshCollationsByKey();
+
+ QList<CollationPtr> collations;
+ QHash<QString,CollationPtr> collationsByKey;
+ QHash<QString,ScriptingPlugin*> scriptingPlugins;
+
+ private slots:
+ void pluginLoaded(Plugin* plugin, PluginType* type);
+ void pluginUnloaded(Plugin* plugin, PluginType* type);
+};
+
+#endif // COLLATIONMANAGERIMPL_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.cpp
new file mode 100644
index 0000000..e210f01
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.cpp
@@ -0,0 +1,770 @@
+#include "configimpl.h"
+#include "sqlhistorymodel.h"
+#include "ddlhistorymodel.h"
+#include "services/notifymanager.h"
+#include "sqlitestudio.h"
+#include "db/dbsqlite3.h"
+#include <QtGlobal>
+#include <QDebug>
+#include <QList>
+#include <QDir>
+#include <QFileInfo>
+#include <QDataStream>
+#include <QRegExp>
+#include <QDateTime>
+#include <QSysInfo>
+#include <QtConcurrent/QtConcurrentRun>
+
+static_qstring(DB_FILE_NAME, "settings3");
+qint64 ConfigImpl::sqlHistoryId = -1;
+
+ConfigImpl::~ConfigImpl()
+{
+ cleanUp();
+}
+
+void ConfigImpl::init()
+{
+ initDbFile();
+ initTables();
+
+ connect(this, SIGNAL(sqlHistoryRefreshNeeded()), this, SLOT(refreshSqlHistory()));
+ connect(this, SIGNAL(ddlHistoryRefreshNeeded()), this, SLOT(refreshDdlHistory()));
+}
+
+void ConfigImpl::cleanUp()
+{
+ if (db->isOpen())
+ db->close();
+
+ safe_delete(db);
+}
+
+const QString &ConfigImpl::getConfigDir() const
+{
+ return configDir;
+}
+
+QString ConfigImpl::getConfigFilePath() const
+{
+ if (!db)
+ return QString();
+
+ return db->getPath();
+}
+
+void ConfigImpl::beginMassSave()
+{
+ if (isMassSaving())
+ return;
+
+ emit massSaveBegins();
+ db->exec("BEGIN;");
+ massSaving = true;
+}
+
+void ConfigImpl::commitMassSave()
+{
+ if (!isMassSaving())
+ return;
+
+ db->exec("COMMIT;");
+ emit massSaveCommited();
+ massSaving = false;
+}
+
+void ConfigImpl::rollbackMassSave()
+{
+ if (!isMassSaving())
+ return;
+
+ db->exec("ROLLBACK;");
+ massSaving = false;
+}
+
+bool ConfigImpl::isMassSaving() const
+{
+ return massSaving;
+}
+
+void ConfigImpl::set(const QString &group, const QString &key, const QVariant &value)
+{
+ QByteArray bytes;
+ QDataStream stream(&bytes, QIODevice::WriteOnly);
+ stream << value;
+
+ db->exec("INSERT OR REPLACE INTO settings VALUES (?, ?, ?)", {group, key, bytes});
+}
+
+QVariant ConfigImpl::get(const QString &group, const QString &key)
+{
+ SqlQueryPtr results = db->exec("SELECT value FROM settings WHERE [group] = ? AND [key] = ?", {group, key});
+ return deserializeValue(results->getSingleCell());
+}
+
+QHash<QString,QVariant> ConfigImpl::getAll()
+{
+ SqlQueryPtr results = db->exec("SELECT [group], [key], value FROM settings");
+
+ QHash<QString,QVariant> cfg;
+ QString key;
+ SqlResultsRowPtr row;
+ while (results->hasNext())
+ {
+ row = results->next();
+ key = row->value("group").toString() + "." + row->value("key").toString();
+ cfg[key] = deserializeValue(row->value("value"));
+ }
+ return cfg;
+}
+
+bool ConfigImpl::storeErrorAndReturn(SqlQueryPtr results)
+{
+ if (results->isError())
+ {
+ lastQueryError = results->getErrorText();
+ return true;
+ }
+ else
+ return false;
+}
+
+void ConfigImpl::printErrorIfSet(SqlQueryPtr results)
+{
+ if (results && results->isError())
+ {
+ qCritical() << "Config error while executing query:" << results->getErrorText();
+ storeErrorAndReturn(results);
+ }
+}
+
+bool ConfigImpl::addDb(const QString& name, const QString& path, const QHash<QString,QVariant>& options)
+{
+ QByteArray optBytes = hashToBytes(options);
+ SqlQueryPtr results = db->exec("INSERT INTO dblist VALUES (?, ?, ?)", {name, path, optBytes});
+ return !storeErrorAndReturn(results);
+}
+
+bool ConfigImpl::updateDb(const QString &name, const QString &newName, const QString &path, const QHash<QString,QVariant> &options)
+{
+ QByteArray optBytes = hashToBytes(options);
+ SqlQueryPtr results = db->exec("UPDATE dblist SET name = ?, path = ?, options = ? WHERE name = ?",
+ {newName, path, optBytes, name});
+
+ return (!storeErrorAndReturn(results) && results->rowsAffected() > 0);
+}
+
+bool ConfigImpl::removeDb(const QString &name)
+{
+ SqlQueryPtr results = db->exec("DELETE FROM dblist WHERE name = ?", {name});
+ return (!storeErrorAndReturn(results) && results->rowsAffected() > 0);
+}
+
+bool ConfigImpl::isDbInConfig(const QString &name)
+{
+ SqlQueryPtr results = db->exec("SELECT * FROM dblist WHERE name = ?", {name});
+ return (!storeErrorAndReturn(results) && results->hasNext());
+}
+
+QString ConfigImpl::getLastErrorString() const
+{
+ QString msg = db->getErrorText();
+ if (msg.trimmed().isEmpty())
+ return lastQueryError;
+
+ return msg;
+}
+
+QList<ConfigImpl::CfgDbPtr> ConfigImpl::dbList()
+{
+ QList<CfgDbPtr> entries;
+ SqlQueryPtr results = db->exec("SELECT name, path, options FROM dblist");
+ CfgDbPtr cfgDb;
+ SqlResultsRowPtr row;
+ while (results->hasNext())
+ {
+ row = results->next();
+ cfgDb = CfgDbPtr::create();
+ cfgDb->name = row->value("name").toString();
+ cfgDb->path = row->value("path").toString();
+ cfgDb->options = deserializeValue(row->value("options")).toHash();
+ entries += cfgDb;
+ }
+
+ return entries;
+}
+
+ConfigImpl::CfgDbPtr ConfigImpl::getDb(const QString& dbName)
+{
+ SqlQueryPtr results = db->exec("SELECT path, options FROM dblist WHERE name = ?", {dbName});
+
+ if (!results->hasNext())
+ return CfgDbPtr();
+
+ SqlResultsRowPtr row = results->next();
+
+ CfgDbPtr cfgDb = CfgDbPtr::create();
+ cfgDb->name = dbName;
+ cfgDb->path = row->value("path").toString();
+ cfgDb->options = deserializeValue(row->value("options")).toHash();
+ return cfgDb;
+}
+
+void ConfigImpl::storeGroups(const QList<DbGroupPtr>& groups)
+{
+ db->begin();
+ db->exec("DELETE FROM groups");
+
+ foreach (const DbGroupPtr& group, groups)
+ storeGroup(group);
+
+ db->commit();
+}
+
+void ConfigImpl::storeGroup(const ConfigImpl::DbGroupPtr &group, qint64 parentId)
+{
+ QVariant parent = QVariant(QVariant::LongLong);
+ if (parentId > -1)
+ parent = parentId;
+
+ SqlQueryPtr results = db->exec("INSERT INTO groups (name, [order], parent, open, dbname) VALUES (?, ?, ?, ?, ?)",
+ {group->name, group->order, parent, group->open, group->referencedDbName});
+
+ qint64 newParentId = results->getRegularInsertRowId();
+ foreach (const DbGroupPtr& childGroup, group->childs)
+ storeGroup(childGroup, newParentId);
+}
+
+QList<ConfigImpl::DbGroupPtr> ConfigImpl::getGroups()
+{
+ DbGroupPtr topGroup = DbGroupPtr::create();
+ topGroup->id = -1;
+ readGroupRecursively(topGroup);
+ return topGroup->childs;
+}
+
+ConfigImpl::DbGroupPtr ConfigImpl::getDbGroup(const QString& dbName)
+{
+ SqlQueryPtr results = db->exec("SELECT id, name, [order], open, dbname FROM groups WHERE dbname = ? LIMIT 1", {dbName});
+
+ DbGroupPtr group = DbGroupPtr::create();
+ group->referencedDbName = dbName;
+
+ if (!results->hasNext())
+ return group;
+
+ SqlResultsRowPtr row = results->next();
+ group->id = row->value("id").toULongLong();
+ group->name = row->value("name").toString();
+ group->order = row->value("order").toInt();
+ group->open = row->value("open").toBool();
+ return group;
+}
+
+qint64 ConfigImpl::addSqlHistory(const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected)
+{
+ if (sqlHistoryId < 0)
+ {
+ SqlQueryPtr results = db->exec("SELECT max(id) FROM sqleditor_history");
+ if (results->isError())
+ {
+ qCritical() << "Cannot add SQL history, because cannot determinate sqleditor_history max(id):" << results->getErrorText();
+ return -1;
+ }
+
+ if (results->hasNext())
+ sqlHistoryId = results->getSingleCell().toLongLong() + 1;
+ else
+ sqlHistoryId = 0;
+ }
+
+ QtConcurrent::run(this, &ConfigImpl::asyncAddSqlHistory, sqlHistoryId, sql, dbName, timeSpentMillis, rowsAffected);
+ sqlHistoryId++;
+ return sqlHistoryId;
+}
+
+void ConfigImpl::updateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected)
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncUpdateSqlHistory, id, sql, dbName, timeSpentMillis, rowsAffected);
+}
+
+void ConfigImpl::clearSqlHistory()
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncClearSqlHistory);
+}
+
+QAbstractItemModel* ConfigImpl::getSqlHistoryModel()
+{
+ if (!sqlHistoryModel)
+ sqlHistoryModel = new SqlHistoryModel(db, this);
+
+ return sqlHistoryModel;
+}
+
+void ConfigImpl::addCliHistory(const QString& text)
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncAddCliHistory, text);
+}
+
+void ConfigImpl::applyCliHistoryLimit()
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncApplyCliHistoryLimit);
+}
+
+void ConfigImpl::clearCliHistory()
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncClearCliHistory);
+}
+
+QStringList ConfigImpl::getCliHistory() const
+{
+ static_qstring(selectQuery, "SELECT text FROM cli_history ORDER BY id");
+
+ SqlQueryPtr results = db->exec(selectQuery);
+ if (results->isError())
+ qWarning() << "Error while getting CLI history:" << db->getErrorText();
+
+ return results->columnAsList<QString>("text");
+}
+
+void ConfigImpl::addDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile)
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncAddDdlHistory, queries, dbName, dbFile);
+}
+
+QList<ConfigImpl::DdlHistoryEntryPtr> ConfigImpl::getDdlHistoryFor(const QString& dbName, const QString& dbFile, const QDate& date)
+{
+ static_qstring(sql,
+ "SELECT timestamp,"
+ " queries"
+ " FROM ddl_history"
+ " WHERE dbname = ?"
+ " AND file = ?"
+ " AND date(timestamp, 'unixepoch') = ?");
+
+ SqlQueryPtr results = db->exec(sql, {dbName, dbFile, date.toString("yyyy-MM-dd")});
+
+ QList<DdlHistoryEntryPtr> entries;
+ DdlHistoryEntryPtr entry;
+ SqlResultsRowPtr row;
+ while (results->hasNext())
+ {
+ row = results->next();
+ entry = DdlHistoryEntryPtr::create();
+ entry->dbName = dbName;
+ entry->dbFile = dbFile;
+ entry->timestamp = QDateTime::fromTime_t(row->value("timestamp").toUInt());
+ entry->queries = row->value("queries").toString();
+ entries << entry;
+ }
+ return entries;
+}
+
+DdlHistoryModel* ConfigImpl::getDdlHistoryModel()
+{
+ if (!ddlHistoryModel)
+ ddlHistoryModel = new DdlHistoryModel(db, this);
+
+ return ddlHistoryModel;
+}
+
+void ConfigImpl::clearDdlHistory()
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncClearDdlHistory);
+}
+
+void ConfigImpl::addReportHistory(bool isFeatureRequest, const QString& title, const QString& url)
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncAddReportHistory, isFeatureRequest, title, url);
+}
+
+QList<Config::ReportHistoryEntryPtr> ConfigImpl::getReportHistory()
+{
+ static_qstring(sql, "SELECT id, timestamp, title, url, feature_request FROM reports_history");
+
+ SqlQueryPtr results = db->exec(sql);
+
+ QList<ReportHistoryEntryPtr> entries;
+ SqlResultsRowPtr row;
+ ReportHistoryEntryPtr entry;
+ while (results->hasNext())
+ {
+ row = results->next();
+ entry = ReportHistoryEntryPtr::create();
+ entry->id = row->value("id").toInt();
+ entry->timestamp = row->value("timestamp").toInt();
+ entry->title = row->value("title").toString();
+ entry->url = row->value("url").toString();
+ entry->isFeatureRequest = row->value("feature_request").toBool();
+ entries << entry;
+ }
+ return entries;
+}
+
+void ConfigImpl::deleteReport(int id)
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncDeleteReport, id);
+}
+
+void ConfigImpl::clearReportHistory()
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncClearReportHistory);
+}
+
+void ConfigImpl::readGroupRecursively(ConfigImpl::DbGroupPtr group)
+{
+ SqlQueryPtr results;
+ if (group->id < 0)
+ results = db->exec("SELECT id, name, [order], open, dbname FROM groups WHERE parent IS NULL ORDER BY [order]");
+ else
+ results = db->exec("SELECT id, name, [order], open, dbname FROM groups WHERE parent = ? ORDER BY [order]", {group->id});
+
+ DbGroupPtr childGroup;
+ SqlResultsRowPtr row;
+ while (results->hasNext())
+ {
+ row = results->next();
+ childGroup = DbGroupPtr::create();
+ childGroup->id = row->value("id").toULongLong();
+ childGroup->name = row->value("name").toString();
+ childGroup->order = row->value("order").toInt();
+ childGroup->open = row->value("open").toBool();
+ childGroup->referencedDbName = row->value("dbname").toString();
+ group->childs += childGroup;
+ }
+
+ for (int i = 0; i < group->childs.size(); i++)
+ readGroupRecursively(group->childs[i]);
+}
+
+void ConfigImpl::begin()
+{
+ db->begin();
+}
+
+void ConfigImpl::commit()
+{
+ db->commit();
+}
+
+void ConfigImpl::rollback()
+{
+ db->rollback();
+}
+
+QString ConfigImpl::getConfigPath()
+{
+#ifdef Q_OS_WIN
+ if (QSysInfo::windowsVersion() & QSysInfo::WV_NT_based)
+ return SQLITESTUDIO->getEnv("APPDATA")+"/sqlitestudio";
+ else
+ return SQLITESTUDIO->getEnv("HOME")+"/sqlitestudio";
+#else
+ return SQLITESTUDIO->getEnv("HOME")+"/.config/sqlitestudio";
+#endif
+}
+
+QString ConfigImpl::getPortableConfigPath()
+{
+ QFileInfo file;
+ QDir dir("./sqlitestudio-cfg");
+
+ file = QFileInfo(dir.absolutePath());
+ if (!file.exists())
+ return dir.absolutePath();
+
+ if (!file.isDir() || !file.isReadable() || !file.isWritable())
+ return QString::null;
+
+ foreach (file, dir.entryInfoList())
+ {
+ if (!file.isReadable() || !file.isWritable())
+ return QString::null;
+ }
+
+ return dir.absolutePath();
+}
+
+void ConfigImpl::initTables()
+{
+ SqlQueryPtr results = db->exec("SELECT lower(name) AS name FROM sqlite_master WHERE type = 'table'");
+ QList<QString> tables = results->columnAsList<QString>(0);
+
+ if (!tables.contains("version"))
+ {
+ QString table;
+ foreach (table, tables)
+ db->exec("DROP TABLE "+table);
+
+ tables.clear();
+ db->exec("CREATE TABLE version (version NUMERIC)");
+ db->exec("INSERT INTO version VALUES ("+QString::number(SQLITESTUDIO_CONFIG_VERSION)+")");
+ }
+
+ if (!tables.contains("settings"))
+ db->exec("CREATE TABLE settings ([group] TEXT, [key] TEXT, value, PRIMARY KEY([group], [key]))");
+
+ if (!tables.contains("sqleditor_history"))
+ db->exec("CREATE TABLE sqleditor_history (id INTEGER PRIMARY KEY, dbname TEXT, date INTEGER, time_spent INTEGER, rows INTEGER, sql TEXT)");
+
+ if (!tables.contains("dblist"))
+ db->exec("CREATE TABLE dblist (name TEXT PRIMARY KEY, path TEXT UNIQUE, options TEXT)");
+
+ if (!tables.contains("groups"))
+ db->exec("CREATE TABLE groups (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, parent INTEGER REFERENCES groups(id), "
+ "[order] INTEGER, open INTEGER DEFAULT 0, dbname TEXT UNIQUE REFERENCES dblist(name) ON UPDATE CASCADE ON DELETE CASCADE, "
+ "UNIQUE(name, parent))");
+
+ if (!tables.contains("ddl_history"))
+ db->exec("CREATE TABLE ddl_history (id INTEGER PRIMARY KEY AUTOINCREMENT, dbname TEXT, file TEXT, timestamp INTEGER, "
+ "queries TEXT)");
+
+ if (!tables.contains("cli_history"))
+ db->exec("CREATE TABLE cli_history (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT)");
+
+ if (!tables.contains("reports_history"))
+ db->exec("CREATE TABLE reports_history (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, feature_request BOOLEAN, title TEXT, url TEXT)");
+}
+
+void ConfigImpl::initDbFile()
+{
+ // Determinate global config location and portable one
+ QString globalPath = getConfigPath();
+ QString portablePath = getPortableConfigPath();
+
+ QStringList paths;
+ if (!globalPath.isNull() && !portablePath.isNull())
+ {
+ if (QFileInfo(portablePath).exists())
+ {
+ paths << portablePath+"/"+DB_FILE_NAME;
+ paths << globalPath+"/"+DB_FILE_NAME;
+ }
+ else
+ {
+ paths << globalPath+"/"+DB_FILE_NAME;
+ paths << portablePath+"/"+DB_FILE_NAME;
+ }
+ }
+ else if (!globalPath.isNull())
+ {
+ paths << globalPath+"/"+DB_FILE_NAME;
+ }
+ else if (!portablePath.isNull())
+ {
+ paths << portablePath+"/"+DB_FILE_NAME;
+ }
+
+ // Create global config directory if not existing
+ QDir dir;
+ if (!globalPath.isNull())
+ {
+ dir = QDir(globalPath);
+ if (!dir.exists())
+ QDir::root().mkpath(globalPath);
+ }
+
+ // A fallback to in-memory db
+ paths << ":memory:";
+
+ // Go through all candidates and pick one
+ QString path;
+ foreach (path, paths)
+ {
+ dir = QDir(path);
+ if (path != ":memory:")
+ dir.cdUp();
+
+ if (tryInitDbFile(path))
+ {
+ configDir = dir.absolutePath();
+ break;
+ }
+ }
+
+ // We ended up with in-memory one? That's not good.
+ if (configDir == ":memory:")
+ {
+ paths.removeLast();
+ notifyError(QObject::tr("Could not initialize configuration file. Any configuration changes and queries history will be lost after application restart."
+ " Tried to initialize the file at following localizations: %1.").arg(paths.join(", ")));
+ }
+
+ qDebug() << "Using configuration directory:" << configDir;
+ db->exec("PRAGMA foreign_keys = 1;");
+}
+
+bool ConfigImpl::tryInitDbFile(const QString &dbPath)
+{
+ db = new DbSqlite3("SQLiteStudio settings", dbPath, {{DB_PURE_INIT, true}});
+ if (!db->open())
+ {
+ safe_delete(db);
+ return false;
+ }
+
+ SqlQueryPtr results = db->exec("SELECT * FROM sqlite_master");
+ if (results->isError())
+ {
+ safe_delete(db);
+ return false;
+ }
+
+ return true;
+}
+
+QVariant ConfigImpl::deserializeValue(const QVariant &value)
+{
+ if (!value.isValid())
+ return QVariant();
+
+ QByteArray bytes = value.toByteArray();
+ if (bytes.isNull())
+ return QVariant();
+
+ QVariant deserializedValue;
+ QDataStream stream(bytes);
+ stream >> deserializedValue;
+ return deserializedValue;
+}
+
+void ConfigImpl::asyncAddSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected)
+{
+ db->begin();
+ SqlQueryPtr results = db->exec("INSERT INTO sqleditor_history (id, dbname, date, time_spent, rows, sql) VALUES (?, ?, ?, ?, ?, ?)",
+ {id, dbName, (QDateTime::currentMSecsSinceEpoch() / 1000), timeSpentMillis, rowsAffected, sql});
+
+ if (results->isError())
+ {
+ qDebug() << "Error adding SQL history:" << results->getErrorText();
+ db->rollback();
+ return;
+ }
+
+ int maxHistorySize = CFG_CORE.General.SqlHistorySize.get();
+
+ results = db->exec("SELECT count(*) FROM sqleditor_history");
+ if (results->hasNext() && results->getSingleCell().toInt() > maxHistorySize)
+ {
+ results = db->exec(QString("SELECT id FROM sqleditor_history ORDER BY id DESC LIMIT 1 OFFSET %1").arg(maxHistorySize));
+ if (results->hasNext())
+ {
+ int id = results->getSingleCell().toInt();
+ if (id > 0) // it will be 0 on fail conversion, but we won't delete id <= 0 ever.
+ db->exec("DELETE FROM sqleditor_history WHERE id <= ?", {id});
+ }
+ }
+ db->commit();
+
+ emit sqlHistoryRefreshNeeded();
+}
+
+void ConfigImpl::asyncUpdateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected)
+{
+ db->exec("UPDATE sqleditor_history SET dbname = ?, time_spent = ?, rows = ?, sql = ? WHERE id = ?",
+ {dbName, timeSpentMillis, rowsAffected, sql, id});
+
+ emit sqlHistoryRefreshNeeded();
+}
+
+void ConfigImpl::asyncClearSqlHistory()
+{
+ db->exec("DELETE FROM sqleditor_history");
+ emit sqlHistoryRefreshNeeded();
+}
+
+void ConfigImpl::asyncAddCliHistory(const QString& text)
+{
+ static_qstring(insertQuery, "INSERT INTO cli_history (text) VALUES (?)");
+
+ SqlQueryPtr results = db->exec(insertQuery, {text});
+ if (results->isError())
+ qWarning() << "Error while adding CLI history:" << results->getErrorText();
+
+ applyCliHistoryLimit();
+}
+
+void ConfigImpl::asyncApplyCliHistoryLimit()
+{
+ static_qstring(limitQuery, "DELETE FROM cli_history WHERE id >= (SELECT id FROM cli_history ORDER BY id LIMIT 1 OFFSET %1)");
+
+ SqlQueryPtr results = db->exec(limitQuery.arg(CFG_CORE.Console.HistorySize.get()));
+ if (results->isError())
+ qWarning() << "Error while limiting CLI history:" << db->getErrorText();
+}
+
+void ConfigImpl::asyncClearCliHistory()
+{
+ static_qstring(clearQuery, "DELETE FROM cli_history");
+
+ SqlQueryPtr results = db->exec(clearQuery);
+ if (results->isError())
+ qWarning() << "Error while clearing CLI history:" << db->getErrorText();
+}
+
+void ConfigImpl::asyncAddDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile)
+{
+ static_qstring(insert, "INSERT INTO ddl_history (dbname, file, timestamp, queries) VALUES (?, ?, ?, ?)");
+ static_qstring(countSql, "SELECT count(*) FROM ddl_history");
+ static_qstring(idSql, "SELECT id FROM ddl_history ORDER BY id DESC LIMIT 1 OFFSET %1");
+ static_qstring(deleteSql, "DELETE FROM ddl_history WHERE id <= ?");
+
+ db->begin();
+ db->exec(insert, {dbName, dbFile, QDateTime::currentDateTime().toTime_t(), queries});
+
+ int maxHistorySize = CFG_CORE.General.DdlHistorySize.get();
+
+ SqlQueryPtr results = db->exec(countSql);
+ if (results->hasNext() && results->getSingleCell().toInt() > maxHistorySize)
+ {
+ results = db->exec(QString(idSql).arg(maxHistorySize), Db::Flag::NO_LOCK);
+ if (results->hasNext())
+ {
+ int id = results->getSingleCell().toInt();
+ if (id > 0) // it will be 0 on fail conversion, but we won't delete id <= 0 ever.
+ db->exec(deleteSql, {id});
+ }
+ }
+ db->commit();
+
+ emit ddlHistoryRefreshNeeded();
+}
+
+void ConfigImpl::asyncClearDdlHistory()
+{
+ db->exec("DELETE FROM ddl_history");
+ emit ddlHistoryRefreshNeeded();
+}
+
+void ConfigImpl::asyncAddReportHistory(bool isFeatureRequest, const QString& title, const QString& url)
+{
+ static_qstring(sql, "INSERT INTO reports_history (feature_request, timestamp, title, url) VALUES (?, ?, ?, ?)");
+ db->exec(sql, {(isFeatureRequest ? 1 : 0), QDateTime::currentDateTime().toTime_t(), title, url});
+ emit reportsHistoryRefreshNeeded();
+}
+
+void ConfigImpl::asyncDeleteReport(int id)
+{
+ static_qstring(sql, "DELETE FROM reports_history WHERE id = ?");
+ db->exec(sql, {id});
+ emit reportsHistoryRefreshNeeded();
+}
+
+void ConfigImpl::asyncClearReportHistory()
+{
+ static_qstring(sql, "DELETE FROM reports_history");
+ db->exec(sql);
+ emit reportsHistoryRefreshNeeded();
+}
+
+void ConfigImpl::refreshSqlHistory()
+{
+ if (sqlHistoryModel)
+ sqlHistoryModel->refresh();
+}
+
+void ConfigImpl::refreshDdlHistory()
+{
+ if (ddlHistoryModel)
+ ddlHistoryModel->refresh();
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.h
new file mode 100644
index 0000000..ec32e8d
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.h
@@ -0,0 +1,127 @@
+#ifndef CONFIGIMPL_H
+#define CONFIGIMPL_H
+
+#include "coreSQLiteStudio_global.h"
+#include "services/config.h"
+#include "db/sqlquery.h"
+
+class AsyncConfigHandler;
+class SqlHistoryModel;
+
+class API_EXPORT ConfigImpl : public Config
+{
+ Q_OBJECT
+
+ friend class AsyncConfigHandler;
+
+ public:
+ virtual ~ConfigImpl();
+
+ void init();
+ void cleanUp();
+ const QString& getConfigDir() const;
+ QString getConfigFilePath() const;
+
+ void beginMassSave();
+ void commitMassSave();
+ void rollbackMassSave();
+ bool isMassSaving() const;
+ void set(const QString& group, const QString& key, const QVariant& value);
+ QVariant get(const QString& group, const QString& key);
+ QHash<QString,QVariant> getAll();
+
+ bool addDb(const QString& name, const QString& path, const QHash<QString, QVariant> &options);
+ bool updateDb(const QString& name, const QString &newName, const QString& path, const QHash<QString, QVariant> &options);
+ bool removeDb(const QString& name);
+ bool isDbInConfig(const QString& name);
+ QString getLastErrorString() const;
+
+ /**
+ * @brief Provides list of all registered databases.
+ * @return List of database entries.
+ *
+ * Registered databases are those that user added to the application. They are not necessary valid or supported.
+ * They can be inexisting or unsupported, but they are kept in registry in case user fixes file path,
+ * or loads plugin to support it.
+ */
+ QList<CfgDbPtr> dbList();
+ CfgDbPtr getDb(const QString& dbName);
+
+ void storeGroups(const QList<DbGroupPtr>& groups);
+ QList<DbGroupPtr> getGroups();
+ DbGroupPtr getDbGroup(const QString& dbName);
+
+ qint64 addSqlHistory(const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected);
+ void updateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected);
+ void clearSqlHistory();
+ QAbstractItemModel* getSqlHistoryModel();
+
+ void addCliHistory(const QString& text);
+ void applyCliHistoryLimit();
+ void clearCliHistory();
+ QStringList getCliHistory() const;
+
+ void addDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile);
+ QList<DdlHistoryEntryPtr> getDdlHistoryFor(const QString& dbName, const QString& dbFile, const QDate& date);
+ DdlHistoryModel* getDdlHistoryModel();
+ void clearDdlHistory();
+
+ void addReportHistory(bool isFeatureRequest, const QString& title, const QString& url);
+ QList<ReportHistoryEntryPtr> getReportHistory();
+ void deleteReport(int id);
+ void clearReportHistory();
+
+ void begin();
+ void commit();
+ void rollback();
+
+ private:
+ /**
+ * @brief Stores error from query in class member.
+ * @param query Query to get error from.
+ * @return true if the query had any error set, or false if not.
+ *
+ * If the error was defined in the query, its message is stored in lastQueryError.
+ */
+ bool storeErrorAndReturn(SqlQueryPtr results);
+ void printErrorIfSet(SqlQueryPtr results);
+ void storeGroup(const DbGroupPtr& group, qint64 parentId = -1);
+ void readGroupRecursively(DbGroupPtr group);
+ QString getConfigPath();
+ QString getPortableConfigPath();
+ void initTables();
+ void initDbFile();
+ bool tryInitDbFile(const QString& dbPath);
+ QVariant deserializeValue(const QVariant& value);
+
+ void asyncAddSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected);
+ void asyncUpdateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected);
+ void asyncClearSqlHistory();
+
+ void asyncAddCliHistory(const QString& text);
+ void asyncApplyCliHistoryLimit();
+ void asyncClearCliHistory();
+
+ void asyncAddDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile);
+ void asyncClearDdlHistory();
+
+ void asyncAddReportHistory(bool isFeatureRequest, const QString& title, const QString& url);
+ void asyncDeleteReport(int id);
+ void asyncClearReportHistory();
+
+ static Config* instance;
+ static qint64 sqlHistoryId;
+
+ Db* db = nullptr;
+ QString configDir;
+ QString lastQueryError;
+ bool massSaving = false;
+ SqlHistoryModel* sqlHistoryModel = nullptr;
+ DdlHistoryModel* ddlHistoryModel = nullptr;
+
+ public slots:
+ void refreshDdlHistory();
+ void refreshSqlHistory();
+};
+
+#endif // CONFIGIMPL_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.cpp
new file mode 100644
index 0000000..43fc953
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.cpp
@@ -0,0 +1,524 @@
+#include "dbmanagerimpl.h"
+#include "db/db.h"
+#include "services/config.h"
+#include "plugins//dbplugin.h"
+#include "services/pluginmanager.h"
+#include "services/notifymanager.h"
+#include "common/utils.h"
+#include <QCoreApplication>
+#include <QFileInfo>
+#include <QHash>
+#include <QHashIterator>
+#include <QPluginLoader>
+#include <QDebug>
+#include <QUrl>
+#include <db/invaliddb.h>
+
+DbManagerImpl::DbManagerImpl(QObject *parent) :
+ DbManager(parent)
+{
+ init();
+}
+
+DbManagerImpl::~DbManagerImpl()
+{
+ foreach (Db* db, dbList)
+ {
+ disconnect(db, SIGNAL(disconnected()), this, SLOT(dbDisconnectedSlot()));
+ disconnect(db, SIGNAL(aboutToDisconnect(bool&)), this, SLOT(dbAboutToDisconnect(bool&)));
+ if (db->isOpen())
+ db->close();
+
+ delete db;
+ }
+ dbList.clear();
+ nameToDb.clear();
+ pathToDb.clear();
+}
+
+bool DbManagerImpl::addDb(const QString &name, const QString &path, bool permanent)
+{
+ return addDb(name, path, QHash<QString,QVariant>(), permanent);
+}
+
+bool DbManagerImpl::addDb(const QString &name, const QString &path, const QHash<QString,QVariant>& options, bool permanent)
+{
+ if (getByName(name))
+ {
+ qWarning() << "Tried to add database with name that was already on the list:" << name;
+ return false; // db with this name exists
+ }
+
+ QString errorMessage;
+ Db* db = createDb(name, path, options, &errorMessage);
+ if (!db)
+ {
+ notifyError(tr("Could not add database %1: %2").arg(path).arg(errorMessage));
+ return false;
+ }
+
+ listLock.lockForWrite();
+ addDbInternal(db, permanent);
+ listLock.unlock();
+
+ emit dbAdded(db);
+
+ return true;
+}
+
+bool DbManagerImpl::updateDb(Db* db, const QString &name, const QString &path, const QHash<QString, QVariant> &options, bool permanent)
+{
+ if (db->isOpen())
+ {
+ if (!db->close())
+ return false;
+ }
+
+ listLock.lockForWrite();
+ nameToDb.remove(db->getName(), Qt::CaseInsensitive);
+ pathToDb.remove(db->getPath());
+
+ bool pathDifferent = db->getPath() != path;
+
+ QString oldName = db->getName();
+ db->setName(name);
+ db->setPath(path);
+ db->setConnectionOptions(options);
+
+ bool result = false;
+ if (permanent)
+ {
+ if (CFG->isDbInConfig(oldName))
+ result = CFG->updateDb(oldName, name, path, options);
+ else
+ result = CFG->addDb(name, path, options);
+ }
+ else if (CFG->isDbInConfig(name)) // switched "permanent" off?
+ result = CFG->removeDb(name);
+
+ InvalidDb* invalidDb = dynamic_cast<InvalidDb*>(db);
+ Db* reloadedDb = db;
+ if (pathDifferent && invalidDb)
+ reloadedDb = tryToLoadDb(invalidDb);
+
+ if (reloadedDb) // reloading was not necessary (was not invalid) or it was successful
+ db = reloadedDb;
+
+ nameToDb[name] = db;
+ pathToDb[path] = db;
+
+ listLock.unlock();
+
+ if (result && reloadedDb)
+ emit dbUpdated(oldName, db);
+ else if (reloadedDb) // database reloaded correctly, but update failed
+ notifyError(tr("Database %1 could not be updated, because of an error: %2").arg(oldName).arg(CFG->getLastErrorString()));
+
+ return result;
+}
+
+void DbManagerImpl::removeDbByName(const QString &name, Qt::CaseSensitivity cs)
+{
+ listLock.lockForRead();
+ bool contains = nameToDb.contains(name, cs);
+ listLock.unlock();
+
+ if (!contains)
+ return;
+
+ listLock.lockForWrite();
+ Db* db = nameToDb[name];
+ removeDbInternal(db);
+ listLock.unlock();
+
+ emit dbRemoved(db);
+
+ delete db;
+}
+
+void DbManagerImpl::removeDbByPath(const QString &path)
+{
+ listLock.lockForRead();
+ bool contains = pathToDb.contains(path);
+ listLock.unlock();
+ if (!contains)
+ return;
+
+ listLock.lockForWrite();
+ Db* db = pathToDb[path];
+ removeDbInternal(db);
+ listLock.unlock();
+
+ emit dbRemoved(db);
+
+ delete db;
+}
+
+void DbManagerImpl::removeDb(Db* db)
+{
+ db->close();
+
+ listLock.lockForWrite();
+ removeDbInternal(db);
+ listLock.unlock();
+
+ emit dbRemoved(db);
+ delete db;
+}
+
+void DbManagerImpl::removeDbInternal(Db* db, bool alsoFromConfig)
+{
+ QString name = db->getName();
+ if (alsoFromConfig)
+ CFG->removeDb(name);
+
+ nameToDb.remove(name);
+ pathToDb.remove(db->getPath());
+ dbList.removeOne(db);
+ disconnect(db, SIGNAL(connected()), this, SLOT(dbConnectedSlot()));
+ disconnect(db, SIGNAL(disconnected()), this, SLOT(dbDisconnectedSlot()));
+ disconnect(db, SIGNAL(aboutToDisconnect(bool&)), this, SLOT(dbAboutToDisconnect(bool&)));
+}
+
+QList<Db*> DbManagerImpl::getDbList()
+{
+ listLock.lockForRead();
+ QList<Db*> list = dbList;
+ listLock.unlock();
+ return list;
+}
+
+QList<Db*> DbManagerImpl::getValidDbList()
+{
+ QList<Db*> list = getDbList();
+ QMutableListIterator<Db*> it(list);
+ while (it.hasNext())
+ {
+ it.next();
+ if (!it.value()->isValid())
+ it.remove();
+ }
+
+ return list;
+}
+
+QList<Db*> DbManagerImpl::getConnectedDbList()
+{
+ QList<Db*> list = getDbList();
+ QMutableListIterator<Db*> it(list);
+ while (it.hasNext())
+ {
+ it.next();
+ if (!it.value()->isOpen())
+ it.remove();
+ }
+
+ return list;
+}
+
+QStringList DbManagerImpl::getDbNames()
+{
+ QReadLocker lock(&listLock);
+ return nameToDb.keys();
+}
+
+Db* DbManagerImpl::getByName(const QString &name, Qt::CaseSensitivity cs)
+{
+ QReadLocker lock(&listLock);
+ return nameToDb.value(name, cs);
+}
+
+Db* DbManagerImpl::getByPath(const QString &path)
+{
+ return pathToDb.value(path);
+}
+
+Db* DbManagerImpl::createInMemDb()
+{
+ if (!inMemDbCreatorPlugin)
+ return nullptr;
+
+ return inMemDbCreatorPlugin->getInstance("", ":memory:", {});
+}
+
+bool DbManagerImpl::isTemporary(Db* db)
+{
+ return CFG->getDb(db->getName()).isNull();
+}
+
+QString DbManagerImpl::quickAddDb(const QString& path, const QHash<QString, QVariant>& options)
+{
+ QString newName = DbManager::generateDbName(path);
+ newName = generateUniqueName(newName, DBLIST->getDbNames());
+ if (!DBLIST->addDb(newName, path, options, false))
+ return QString::null;
+
+ return newName;
+}
+
+void DbManagerImpl::setInMemDbCreatorPlugin(DbPlugin* plugin)
+{
+ inMemDbCreatorPlugin = plugin;
+}
+
+void DbManagerImpl::init()
+{
+ Q_ASSERT(PLUGINS);
+
+ loadInitialDbList();
+
+ connect(PLUGINS, SIGNAL(aboutToUnload(Plugin*,PluginType*)), this, SLOT(aboutToUnload(Plugin*,PluginType*)));
+ connect(PLUGINS, SIGNAL(loaded(Plugin*,PluginType*)), this, SLOT(loaded(Plugin*,PluginType*)));
+}
+
+void DbManagerImpl::loadInitialDbList()
+{
+ QUrl url;
+ InvalidDb* db = nullptr;
+ foreach (const Config::CfgDbPtr& cfgDb, CFG->dbList())
+ {
+ db = new InvalidDb(cfgDb->name, cfgDb->path, cfgDb->options);
+
+ url = QUrl::fromUserInput(cfgDb->path);
+ if (url.isLocalFile() && !QFile::exists(cfgDb->path))
+ db->setError(tr("Database file doesn't exist."));
+ else
+ db->setError(tr("No supporting plugin loaded."));
+
+ addDbInternal(db, false);
+ }
+}
+
+void DbManagerImpl::notifyDatabasesAreLoaded()
+{
+ // Any databases were already loaded by loaded() slot, which is called when DbPlugin was loaded.
+ emit dbListLoaded();
+}
+
+void DbManagerImpl::scanForNewDatabasesInConfig()
+{
+ QList<Config::CfgDbPtr> cfgDbList = CFG->dbList();
+
+ QUrl url;
+ InvalidDb* db = nullptr;
+ for (const Config::CfgDbPtr& cfgDb : cfgDbList)
+ {
+ if (getByName(cfgDb->name) || getByPath(cfgDb->path))
+ continue;
+
+ db = new InvalidDb(cfgDb->name, cfgDb->path, cfgDb->options);
+
+ url = QUrl::fromUserInput(cfgDb->path);
+ if (url.isLocalFile() && !QFile::exists(cfgDb->path))
+ db->setError(tr("Database file doesn't exist."));
+ else
+ db->setError(tr("No supporting plugin loaded."));
+
+ addDbInternal(db);
+ tryToLoadDb(db);
+ }
+}
+
+void DbManagerImpl::addDbInternal(Db* db, bool alsoToConfig)
+{
+ if (alsoToConfig)
+ CFG->addDb(db->getName(), db->getPath(), db->getConnectionOptions());
+
+ dbList << db;
+ nameToDb[db->getName()] = db;
+ pathToDb[db->getPath()] = db;
+ connect(db, SIGNAL(connected()), this, SLOT(dbConnectedSlot()));
+ connect(db, SIGNAL(disconnected()), this, SLOT(dbDisconnectedSlot()));
+ connect(db, SIGNAL(aboutToDisconnect(bool&)), this, SLOT(dbAboutToDisconnect(bool&)));
+}
+
+QList<Db*> DbManagerImpl::getInvalidDatabases() const
+{
+ return filter<Db*>(dbList, [](Db* db) -> bool
+ {
+ return !db->isValid();
+ });
+}
+
+Db* DbManagerImpl::tryToLoadDb(InvalidDb* invalidDb)
+{
+ QUrl url = QUrl::fromUserInput(invalidDb->getPath());
+ if (url.isLocalFile() && !QFile::exists(invalidDb->getPath()))
+ return nullptr;
+
+ Db* db = createDb(invalidDb->getName(), invalidDb->getPath(), invalidDb->getConnectionOptions());
+ if (!db)
+ return nullptr;
+
+ removeDbInternal(invalidDb, false);
+ delete invalidDb;
+
+ addDbInternal(db, false);
+
+ if (CFG->getDbGroup(db->getName())->open)
+ db->open();
+
+ emit dbLoaded(db);
+ return db;
+}
+
+Db* DbManagerImpl::createDb(const QString &name, const QString &path, const QHash<QString,QVariant> &options, QString* errorMessages)
+{
+ QList<DbPlugin*> dbPlugins = PLUGINS->getLoadedPlugins<DbPlugin>();
+ DbPlugin* dbPlugin = nullptr;
+ Db* db = nullptr;
+ QStringList messages;
+ QString message;
+ foreach (dbPlugin, dbPlugins)
+ {
+ if (options.contains("plugin") && options["plugin"] != dbPlugin->getName())
+ continue;
+
+ db = dbPlugin->getInstance(name, path, options, &message);
+ if (!db)
+ {
+ messages << message;
+ continue;
+ }
+
+ if (!db->initAfterCreated())
+ {
+ messages << tr("Database could not be initialized.");
+ continue;
+ }
+
+ return db;
+ }
+
+ if (errorMessages)
+ {
+ if (messages.size() == 0)
+ messages << tr("No suitable database driver plugin found.");
+
+ *errorMessages = messages.join("; ");
+ }
+
+ return nullptr;
+}
+
+
+void DbManagerImpl::dbConnectedSlot()
+{
+ QObject* sdr = sender();
+ Db* db = dynamic_cast<Db*>(sdr);
+ if (!db)
+ {
+ qWarning() << "Received connected() signal but could not cast it to Db!";
+ return;
+ }
+ emit dbConnected(db);
+}
+
+void DbManagerImpl::dbDisconnectedSlot()
+{
+ QObject* sdr = sender();
+ Db* db = dynamic_cast<Db*>(sdr);
+ if (!db)
+ {
+ qWarning() << "Received disconnected() signal but could not cast it to Db!";
+ return;
+ }
+ emit dbDisconnected(db);
+}
+
+void DbManagerImpl::dbAboutToDisconnect(bool& deny)
+{
+ QObject* sdr = sender();
+ Db* db = dynamic_cast<Db*>(sdr);
+ if (!db)
+ {
+ qWarning() << "Received dbAboutToDisconnect() signal but could not cast it to Db!";
+ return;
+ }
+ emit dbAboutToBeDisconnected(db, deny);
+}
+
+void DbManagerImpl::aboutToUnload(Plugin* plugin, PluginType* type)
+{
+ if (!type->isForPluginType<DbPlugin>())
+ return;
+
+ InvalidDb* invalidDb = nullptr;
+ DbPlugin* dbPlugin = dynamic_cast<DbPlugin*>(plugin);
+ QList<Db*> toRemove;
+ for (Db* db : dbList)
+ {
+ if (!dbPlugin->checkIfDbServedByPlugin(db))
+ continue;
+
+ toRemove << db;
+ }
+
+ for (Db* db : toRemove)
+ {
+ emit dbAboutToBeUnloaded(db, dbPlugin);
+
+ if (db->isOpen())
+ db->close();
+
+ removeDbInternal(db, false);
+
+ invalidDb = new InvalidDb(db->getName(), db->getPath(), db->getConnectionOptions());
+ invalidDb->setError(tr("No supporting plugin loaded."));
+ addDbInternal(invalidDb, false);
+
+ delete db;
+
+ emit dbUnloaded(invalidDb);
+ }
+}
+
+void DbManagerImpl::loaded(Plugin* plugin, PluginType* type)
+{
+ if (!type->isForPluginType<DbPlugin>())
+ return;
+
+ DbPlugin* dbPlugin = dynamic_cast<DbPlugin*>(plugin);
+ Db* db = nullptr;
+
+ QUrl url;
+ for (Db* invalidDb : getInvalidDatabases())
+ {
+ if (invalidDb->getConnectionOptions().contains(DB_PLUGIN) && invalidDb->getConnectionOptions()[DB_PLUGIN].toString() != dbPlugin->getName())
+ continue;
+
+ url = QUrl::fromUserInput(invalidDb->getPath());
+ if (url.isLocalFile() && !QFile::exists(invalidDb->getPath()))
+ continue;
+
+ db = createDb(invalidDb->getName(), invalidDb->getPath(), invalidDb->getConnectionOptions());
+ if (!db)
+ continue; // For this db driver was not loaded yet.
+
+ if (!dbPlugin->checkIfDbServedByPlugin(db))
+ {
+ qDebug() << "Managed to load database" << db->getPath() << " (" << db->getName() << ")"
+ << "but it doesn't use DbPlugin that was just loaded, so it will not be loaded to the db manager";
+
+ delete db;
+ continue;
+ }
+
+ removeDbInternal(invalidDb, false);
+ delete invalidDb;
+
+ addDbInternal(db, false);
+
+ if (!db->getConnectionOptions().contains(DB_PLUGIN))
+ {
+ db->getConnectionOptions()[DB_PLUGIN] = dbPlugin->getName();
+ if (!CFG->updateDb(db->getName(), db->getName(), db->getPath(), db->getConnectionOptions()))
+ qWarning() << "Could not store handling plugin in options for database" << db->getName();
+ }
+
+ if (CFG->getDbGroup(db->getName())->open)
+ db->open();
+
+ emit dbLoaded(db);
+ }
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.h
new file mode 100644
index 0000000..8e28080
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.h
@@ -0,0 +1,183 @@
+#ifndef DBMANAGERIMPL_H
+#define DBMANAGERIMPL_H
+
+#include "db/db.h"
+#include "coreSQLiteStudio_global.h"
+#include "common/strhash.h"
+#include "common/global.h"
+#include "services/dbmanager.h"
+#include <QObject>
+#include <QList>
+#include <QHash>
+#include <QReadWriteLock>
+#include <QSharedPointer>
+
+class InvalidDb;
+
+class API_EXPORT DbManagerImpl : public DbManager
+{
+ Q_OBJECT
+
+ public:
+ /**
+ * @brief Creates database manager.
+ * @param parent Parent object passed to QObject constructor.
+ */
+ explicit DbManagerImpl(QObject *parent = 0);
+
+ /**
+ * @brief Default destructor.
+ */
+ ~DbManagerImpl();
+
+ bool addDb(const QString &name, const QString &path, const QHash<QString, QVariant> &options, bool permanent = true);
+ bool addDb(const QString &name, const QString &path, bool permanent = true);
+ bool updateDb(Db* db, const QString &name, const QString &path, const QHash<QString, QVariant> &options, bool permanent);
+ void removeDbByName(const QString& name, Qt::CaseSensitivity cs = Qt::CaseSensitive);
+ void removeDbByPath(const QString& path);
+ void removeDb(Db* db);
+ QList<Db*> getDbList();
+ QList<Db*> getValidDbList();
+ QList<Db*> getConnectedDbList();
+ QStringList getDbNames();
+ Db* getByName(const QString& name, Qt::CaseSensitivity cs = Qt::CaseSensitive);
+ Db* getByPath(const QString& path);
+ Db* createInMemDb();
+ bool isTemporary(Db* db);
+ QString quickAddDb(const QString &path, const QHash<QString, QVariant> &options);
+
+ /**
+ * @brief Defines database plugin used for creating in-memory databases.
+ * @param plugin Plugin to use.
+ */
+ void setInMemDbCreatorPlugin(DbPlugin* plugin);
+
+ private:
+ /**
+ * @brief Internal manager initialization.
+ *
+ * Called from any constructor.
+ */
+ void init();
+
+ /**
+ * @brief Loads initial list of databases.
+ *
+ * Loaded databases are initially the invalid databases.
+ * They are turned into valid databases once their plugins are loaded.
+ */
+ void loadInitialDbList();
+
+ /**
+ * @brief Removes database from application.
+ * @param db Database to be removed.
+ * @param alsoFromConfig If true, database will also be removed from configuration file, otherwise it's just from the manager.
+ *
+ * This method is internally called by public methods, as they all do pretty much the same thing,
+ * except they accept different input parameter. Then this method does the actual job.
+ */
+ void removeDbInternal(Db* db, bool alsoFromConfig = true);
+
+ /**
+ * @brief Adds database to the application.
+ * @param db Database to be added.
+ * @param alsoToConfig If true, the database will also be added to configuration file, otherwise it will be onle to the manager.
+ *
+ * When addDb() is called, it calls DbPlugin#getInstance() and if it returns object, then this method
+ * is called to register the database object in dbList variable.
+ */
+ void addDbInternal(Db* db, bool alsoToConfig = true);
+
+ /**
+ * @brief Filters invalid databases from all managed databases.
+ * @return Only invalid databases from this manager.
+ */
+ QList<Db*> getInvalidDatabases() const;
+
+ Db* tryToLoadDb(InvalidDb* invalidDb);
+
+ /**
+ * @brief Creates database object.
+ * @param name Symbolic name of the database.
+ * @param path Database file path.
+ * @param options Database options, such as password, etc.
+ * @param errorMessages If not null, then the error messages from DbPlugins are stored in that string (in case this method returns null).
+ * @return Database object, or null pointer.
+ *
+ * This method is used internally by addDb() methods. It goes through all DbPlugin instances
+ * and checks if any of them supports given file path and options and returns a database object.
+ * First plugin that provides database object is accepted and its result is returned from the method.
+ */
+ static Db* createDb(const QString &name, const QString &path, const QHash<QString, QVariant> &options, QString* errorMessages = nullptr);
+
+ /**
+ * @brief Registered databases list. Both permanent and transient databases.
+ */
+ QList<Db*> dbList;
+
+ /**
+ * @brief Database ame to database instance mapping, with keys being case insensitive.
+ */
+ StrHash<Db*> nameToDb;
+
+ /**
+ * @brief Mapping from file path to the database.
+ *
+ * Mapping from database file path (as passed to addDb() or updateDb()) to the actual database object.
+ */
+ QHash<QString,Db*> pathToDb;
+
+ /**
+ * @brief Lock for dbList.
+ * Lock for dbList, so the list can be accessed from multiple threads.
+ */
+ QReadWriteLock listLock;
+
+ /**
+ * @brief Database plugin used to create in-memory databases.
+ */
+ DbPlugin* inMemDbCreatorPlugin = nullptr;
+
+ private slots:
+ /**
+ * @brief Slot called when connected to db.
+ *
+ * The slot is connected to the database object, therefore the database object has to be extracted from signal sender
+ * and converted to database type, then passed to the dbConnected(Db* db) signal.
+ */
+ void dbConnectedSlot();
+ /**
+ * @brief Slot called when connected to db.
+ *
+ * The slot is connected to the database object, therefore the database object has to be extracted from signal sender
+ * and converted to database type, then passed to the dbConnected(Db* db) signal.
+ */
+ void dbDisconnectedSlot();
+
+ /**
+ * @brief Passes Db::aboutToDisconnect() signal to dbAboutToBeDisconnected() signal.
+ */
+ void dbAboutToDisconnect(bool& deny);
+
+ /**
+ * @brief Removes databases handled by the plugin from the list.
+ * @param plugin DbPlugin (any other will be ignored).
+ * @param type DbPlugin type.
+ * It removes all databases handled by the plugin being unloaded from the list of managed databases.
+ */
+ void aboutToUnload(Plugin* plugin, PluginType* type);
+
+ /**
+ * @brief Adds all configured databases handled by the plugin to managed list.
+ * @param plugin DbPlugin (any other will be ignored).
+ * @param type DbPlugin type.
+ * Checks configuration for any databases managed by the plugin and if there is any, it's loaded into the managed list.
+ */
+ void loaded(Plugin* plugin, PluginType* type);
+
+ public slots:
+ void notifyDatabasesAreLoaded();
+ void scanForNewDatabasesInConfig();
+};
+
+#endif // DBMANAGERIMPL_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.cpp
new file mode 100644
index 0000000..c94e4c2
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.cpp
@@ -0,0 +1,694 @@
+#include "functionmanagerimpl.h"
+#include "services/config.h"
+#include "services/pluginmanager.h"
+#include "services/notifymanager.h"
+#include "plugins/scriptingplugin.h"
+#include "common/unused.h"
+#include "common/utils.h"
+#include "common/utils_sql.h"
+#include "services/dbmanager.h"
+#include "db/queryexecutor.h"
+#include "db/sqlquery.h"
+#include <QVariantList>
+#include <QHash>
+#include <QDebug>
+#include <QRegularExpression>
+#include <QFile>
+#include <QUrl>
+
+FunctionManagerImpl::FunctionManagerImpl()
+{
+ init();
+}
+
+void FunctionManagerImpl::setScriptFunctions(const QList<ScriptFunction*>& newFunctions)
+{
+ clearFunctions();
+ functions = newFunctions;
+ refreshFunctionsByKey();
+ storeInConfig();
+ emit functionListChanged();
+}
+
+QList<FunctionManager::ScriptFunction*> FunctionManagerImpl::getAllScriptFunctions() const
+{
+ return functions;
+}
+
+QList<FunctionManager::ScriptFunction*> FunctionManagerImpl::getScriptFunctionsForDatabase(const QString& dbName) const
+{
+ QList<ScriptFunction*> results;
+ foreach (ScriptFunction* func, functions)
+ {
+ if (func->allDatabases || func->databases.contains(dbName, Qt::CaseInsensitive))
+ results << func;
+ }
+ return results;
+}
+
+QVariant FunctionManagerImpl::evaluateScalar(const QString& name, int argCount, const QList<QVariant>& args, Db* db, bool& ok)
+{
+ Key key;
+ key.name = name;
+ key.argCount = argCount;
+ key.type = ScriptFunction::SCALAR;
+ if (functionsByKey.contains(key))
+ {
+ ScriptFunction* function = functionsByKey[key];
+ return evaluateScriptScalar(function, name, argCount, args, db, ok);
+ }
+ else if (nativeFunctionsByKey.contains(key))
+ {
+ NativeFunction* function = nativeFunctionsByKey[key];
+ return evaluateNativeScalar(function, args, db, ok);
+ }
+
+ ok = false;
+ return cannotFindFunctionError(name, argCount);
+}
+
+void FunctionManagerImpl::evaluateAggregateInitial(const QString& name, int argCount, Db* db, QHash<QString,QVariant>& aggregateStorage)
+{
+ Key key;
+ key.name = name;
+ key.argCount = argCount;
+ key.type = ScriptFunction::AGGREGATE;
+ if (functionsByKey.contains(key))
+ {
+ ScriptFunction* function = functionsByKey[key];
+ evaluateScriptAggregateInitial(function, db, aggregateStorage);
+ }
+}
+
+void FunctionManagerImpl::evaluateAggregateStep(const QString& name, int argCount, const QList<QVariant>& args, Db* db, QHash<QString,QVariant>& aggregateStorage)
+{
+ Key key;
+ key.name = name;
+ key.argCount = argCount;
+ key.type = ScriptFunction::AGGREGATE;
+ if (functionsByKey.contains(key))
+ {
+ ScriptFunction* function = functionsByKey[key];
+ evaluateScriptAggregateStep(function, args, db, aggregateStorage);
+ }
+}
+
+QVariant FunctionManagerImpl::evaluateAggregateFinal(const QString& name, int argCount, Db* db, bool& ok, QHash<QString,QVariant>& aggregateStorage)
+{
+ Key key;
+ key.name = name;
+ key.argCount = argCount;
+ key.type = ScriptFunction::AGGREGATE;
+ if (functionsByKey.contains(key))
+ {
+ ScriptFunction* function = functionsByKey[key];
+ return evaluateScriptAggregateFinal(function, name, argCount, db, ok, aggregateStorage);
+ }
+
+ ok = false;
+ return cannotFindFunctionError(name, argCount);
+}
+
+QVariant FunctionManagerImpl::evaluateScriptScalar(ScriptFunction* func, const QString& name, int argCount, const QList<QVariant>& args, Db* db, bool& ok)
+{
+ ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang);
+ if (!plugin)
+ {
+ ok = false;
+ return langUnsupportedError(name, argCount, func->lang);
+ }
+ DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast<DbAwareScriptingPlugin*>(plugin);
+
+ QString error;
+ QVariant result;
+
+ if (dbAwarePlugin)
+ result = dbAwarePlugin->evaluate(func->code, args, db, false, &error);
+ else
+ result = plugin->evaluate(func->code, args, &error);
+
+ if (!error.isEmpty())
+ {
+ ok = false;
+ return error;
+ }
+ return result;
+}
+
+void FunctionManagerImpl::evaluateScriptAggregateInitial(ScriptFunction* func, Db* db, QHash<QString, QVariant>& aggregateStorage)
+{
+ ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang);
+ if (!plugin)
+ return;
+
+ DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast<DbAwareScriptingPlugin*>(plugin);
+
+ ScriptingPlugin::Context* ctx = plugin->createContext();
+ aggregateStorage["context"] = QVariant::fromValue(ctx);
+
+ if (dbAwarePlugin)
+ dbAwarePlugin->evaluate(ctx, func->code, {}, db, false);
+ else
+ plugin->evaluate(ctx, func->code, {});
+
+ if (plugin->hasError(ctx))
+ {
+ aggregateStorage["error"] = true;
+ aggregateStorage["errorMessage"] = plugin->getErrorMessage(ctx);
+ }
+}
+
+void FunctionManagerImpl::evaluateScriptAggregateStep(ScriptFunction* func, const QList<QVariant>& args, Db* db, QHash<QString, QVariant>& aggregateStorage)
+{
+ ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang);
+ if (!plugin)
+ return;
+
+ if (aggregateStorage.contains("error"))
+ return;
+
+ DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast<DbAwareScriptingPlugin*>(plugin);
+
+ ScriptingPlugin::Context* ctx = aggregateStorage["context"].value<ScriptingPlugin::Context*>();
+ if (dbAwarePlugin)
+ dbAwarePlugin->evaluate(ctx, func->code, args, db, false);
+ else
+ plugin->evaluate(ctx, func->code, args);
+
+ if (plugin->hasError(ctx))
+ {
+ aggregateStorage["error"] = true;
+ aggregateStorage["errorMessage"] = plugin->getErrorMessage(ctx);
+ }
+}
+
+QVariant FunctionManagerImpl::evaluateScriptAggregateFinal(ScriptFunction* func, const QString& name, int argCount, Db* db, bool& ok, QHash<QString, QVariant>& aggregateStorage)
+{
+ ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang);
+ if (!plugin)
+ {
+ ok = false;
+ return langUnsupportedError(name, argCount, func->lang);
+ }
+
+ ScriptingPlugin::Context* ctx = aggregateStorage["context"].value<ScriptingPlugin::Context*>();
+ if (aggregateStorage.contains("error"))
+ {
+ ok = false;
+ plugin->releaseContext(ctx);
+ return aggregateStorage["errorMessage"];
+ }
+
+ DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast<DbAwareScriptingPlugin*>(plugin);
+
+ QVariant result;
+ if (dbAwarePlugin)
+ result = dbAwarePlugin->evaluate(ctx, func->code, {}, db, false);
+ else
+ result = plugin->evaluate(ctx, func->code, {});
+
+ if (plugin->hasError(ctx))
+ {
+ ok = false;
+ QString msg = plugin->getErrorMessage(ctx);
+ plugin->releaseContext(ctx);
+ return msg;
+ }
+
+ plugin->releaseContext(ctx);
+ return result;
+}
+
+QList<FunctionManager::NativeFunction*> FunctionManagerImpl::getAllNativeFunctions() const
+{
+ return nativeFunctions;
+}
+
+QVariant FunctionManagerImpl::evaluateNativeScalar(NativeFunction* func, const QList<QVariant>& args, Db* db, bool& ok)
+{
+ if (!func->undefinedArgs && args.size() != func->arguments.size())
+ {
+ ok = false;
+ return tr("Invalid number of arguments to function '%1'. Expected %2, but got %3.").arg(func->name, QString::number(func->arguments.size()),
+ QString::number(args.size()));
+ }
+
+ return func->functionPtr(args, db, ok);
+}
+
+void FunctionManagerImpl::init()
+{
+ loadFromConfig();
+ initNativeFunctions();
+ refreshFunctionsByKey();
+}
+
+void FunctionManagerImpl::initNativeFunctions()
+{
+ registerNativeFunction("regexp", {"pattern", "arg"}, FunctionManagerImpl::nativeRegExp);
+ registerNativeFunction("sqlfile", {"file"}, FunctionManagerImpl::nativeSqlFile);
+ registerNativeFunction("readfile", {"file"}, FunctionManagerImpl::nativeReadFile);
+ registerNativeFunction("writefile", {"file", "data"}, FunctionManagerImpl::nativeWriteFile);
+ registerNativeFunction("langs", {}, FunctionManagerImpl::nativeLangs);
+ registerNativeFunction("script", {"language", "code"}, FunctionManagerImpl::nativeScript);
+ registerNativeFunction("html_escape", {"string"}, FunctionManagerImpl::nativeHtmlEscape);
+ registerNativeFunction("url_encode", {"string"}, FunctionManagerImpl::nativeUrlEncode);
+ registerNativeFunction("url_decode", {"string"}, FunctionManagerImpl::nativeUrlDecode);
+ registerNativeFunction("base64_encode", {"data"}, FunctionManagerImpl::nativeBase64Encode);
+ registerNativeFunction("base64_decode", {"data"}, FunctionManagerImpl::nativeBase64Decode);
+ registerNativeFunction("md4_bin", {"data"}, FunctionManagerImpl::nativeMd4);
+ registerNativeFunction("md4", {"data"}, FunctionManagerImpl::nativeMd4Hex);
+ registerNativeFunction("md5_bin", {"data"}, FunctionManagerImpl::nativeMd5);
+ registerNativeFunction("md5", {"data"}, FunctionManagerImpl::nativeMd5Hex);
+ registerNativeFunction("sha1", {"data"}, FunctionManagerImpl::nativeSha1);
+ registerNativeFunction("sha224", {"data"}, FunctionManagerImpl::nativeSha224);
+ registerNativeFunction("sha256", {"data"}, FunctionManagerImpl::nativeSha256);
+ registerNativeFunction("sha384", {"data"}, FunctionManagerImpl::nativeSha384);
+ registerNativeFunction("sha512", {"data"}, FunctionManagerImpl::nativeSha512);
+ registerNativeFunction("sha3_224", {"data"}, FunctionManagerImpl::nativeSha3_224);
+ registerNativeFunction("sha3_256", {"data"}, FunctionManagerImpl::nativeSha3_256);
+ registerNativeFunction("sha3_384", {"data"}, FunctionManagerImpl::nativeSha3_384);
+ registerNativeFunction("sha3_512", {"data"}, FunctionManagerImpl::nativeSha3_512);
+}
+
+void FunctionManagerImpl::refreshFunctionsByKey()
+{
+ functionsByKey.clear();
+ foreach (ScriptFunction* func, functions)
+ functionsByKey[Key(func)] = func;
+
+ foreach (NativeFunction* func, nativeFunctions)
+ nativeFunctionsByKey[Key(func)] = func;
+}
+
+void FunctionManagerImpl::storeInConfig()
+{
+ QVariantList list;
+ QHash<QString,QVariant> fnHash;
+ foreach (ScriptFunction* func, functions)
+ {
+ fnHash["name"] = func->name;
+ fnHash["lang"] = func->lang;
+ fnHash["code"] = func->code;
+ fnHash["initCode"] = func->initCode;
+ fnHash["finalCode"] = func->finalCode;
+ fnHash["databases"] = common(DBLIST->getDbNames(), func->databases);
+ fnHash["arguments"] = func->arguments;
+ fnHash["type"] = static_cast<int>(func->type);
+ fnHash["undefinedArgs"] = func->undefinedArgs;
+ fnHash["allDatabases"] = func->allDatabases;
+ list << fnHash;
+ }
+ CFG_CORE.Internal.Functions.set(list);
+}
+
+void FunctionManagerImpl::loadFromConfig()
+{
+ clearFunctions();
+
+ QVariantList list = CFG_CORE.Internal.Functions.get();
+ QHash<QString,QVariant> fnHash;
+ ScriptFunction* func = nullptr;
+ for (const QVariant& var : list)
+ {
+ fnHash = var.toHash();
+ func = new ScriptFunction();
+ func->name = fnHash["name"].toString();
+ func->lang = fnHash["lang"].toString();
+ func->code = fnHash["code"].toString();
+ func->initCode = fnHash["initCode"].toString();
+ func->finalCode = fnHash["finalCode"].toString();
+ func->databases = fnHash["databases"].toStringList();
+ func->arguments = fnHash["arguments"].toStringList();
+ func->type = static_cast<ScriptFunction::Type>(fnHash["type"].toInt());
+ func->undefinedArgs = fnHash["undefinedArgs"].toBool();
+ func->allDatabases = fnHash["allDatabases"].toBool();
+ functions << func;
+ }
+}
+
+void FunctionManagerImpl::clearFunctions()
+{
+ for (ScriptFunction* fn : functions)
+ delete fn;
+
+ functions.clear();
+}
+
+QString FunctionManagerImpl::cannotFindFunctionError(const QString& name, int argCount)
+{
+ QStringList argMarkers = getArgMarkers(argCount);
+ return tr("No such function registered in SQLiteStudio: %1(%2)").arg(name).arg(argMarkers.join(","));
+}
+
+QString FunctionManagerImpl::langUnsupportedError(const QString& name, int argCount, const QString& lang)
+{
+ QStringList argMarkers = getArgMarkers(argCount);
+ return tr("Function %1(%2) was registered with language %3, but the plugin supporting that language is not currently loaded.")
+ .arg(name).arg(argMarkers.join(",")).arg(lang);
+}
+
+QVariant FunctionManagerImpl::nativeRegExp(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 2)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ QRegularExpression re(args[0].toString());
+ if (!re.isValid())
+ {
+ ok = false;
+ return tr("Invalid regular expression pattern: %1").arg(args[0].toString());
+ }
+
+ QRegularExpressionMatch match = re.match(args[1].toString());
+ return match.hasMatch();
+}
+
+QVariant FunctionManagerImpl::nativeSqlFile(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ if (args.size() != 1)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ QFile file(args[0].toString());
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ ok = false;
+ return tr("Could not open file %1 for reading: %2").arg(args[0].toString(), file.errorString());
+ }
+
+ QTextStream stream(&file);
+ QString sql = stream.readAll();
+ file.close();
+
+ QueryExecutor executor(db);
+ executor.setAsyncMode(false);
+ executor.exec(sql);
+ SqlQueryPtr results = executor.getResults();
+ if (results->isError())
+ {
+ ok = false;
+ return results->getErrorText();
+ }
+ return results->getSingleCell();
+}
+
+QVariant FunctionManagerImpl::nativeReadFile(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 1)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ QFile file(args[0].toString());
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ ok = false;
+ return tr("Could not open file %1 for reading: %2").arg(args[0].toString(), file.errorString());
+ }
+
+ QByteArray data = file.readAll();
+ file.close();
+ return data;
+}
+
+QVariant FunctionManagerImpl::nativeWriteFile(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 2)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ QFile file(args[0].toString());
+ if (!file.open(QIODevice::WriteOnly|QIODevice::Truncate))
+ {
+ ok = false;
+ return tr("Could not open file %1 for writting: %2").arg(args[0].toString(), file.errorString());
+ }
+
+ QByteArray data;
+ switch (args[1].type())
+ {
+ case QVariant::String:
+ data = args[1].toString().toLocal8Bit();
+ break;
+ default:
+ data = args[1].toByteArray();
+ break;
+ }
+
+ int res = file.write(data);
+ file.close();
+
+ if (res < 0)
+ {
+ ok = false;
+ return tr("Error while writting to file %1: %2").arg(args[0].toString(), file.errorString());
+ }
+
+ return res;
+}
+
+QVariant FunctionManagerImpl::nativeScript(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ if (args.size() != 2)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(args[0].toString());
+ if (!plugin)
+ {
+ ok = false;
+ return tr("Unsupported scripting language: %1").arg(args[0].toString());
+ }
+ DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast<DbAwareScriptingPlugin*>(plugin);
+
+ QString error;
+ QVariant result;
+
+ if (dbAwarePlugin)
+ result = dbAwarePlugin->evaluate(args[1].toString(), QList<QVariant>(), db, false, &error);
+ else
+ result = plugin->evaluate(args[1].toString(), QList<QVariant>(), &error);
+
+ if (!error.isEmpty())
+ {
+ ok = false;
+ return error;
+ }
+ return result;
+}
+
+QVariant FunctionManagerImpl::nativeLangs(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 0)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ QStringList names;
+ for (ScriptingPlugin* plugin : PLUGINS->getLoadedPlugins<ScriptingPlugin>())
+ names << plugin->getLanguage();
+
+ return names.join(", ");
+}
+
+QVariant FunctionManagerImpl::nativeHtmlEscape(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 1)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ return args[0].toString().toHtmlEscaped();
+}
+
+QVariant FunctionManagerImpl::nativeUrlEncode(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 1)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ return QUrl::toPercentEncoding(args[0].toString());
+}
+
+QVariant FunctionManagerImpl::nativeUrlDecode(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 1)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ return QUrl::fromPercentEncoding(args[0].toString().toLocal8Bit());
+}
+
+QVariant FunctionManagerImpl::nativeBase64Encode(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 1)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ return args[0].toByteArray().toBase64();
+}
+
+QVariant FunctionManagerImpl::nativeBase64Decode(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 1)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ return QByteArray::fromBase64(args[0].toByteArray());
+}
+
+QVariant FunctionManagerImpl::nativeCryptographicFunction(const QList<QVariant>& args, Db* db, bool& ok, QCryptographicHash::Algorithm algo)
+{
+ UNUSED(db);
+
+ if (args.size() != 1)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ return QCryptographicHash::hash(args[0].toByteArray(), algo);
+}
+
+QVariant FunctionManagerImpl::nativeMd4(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md4);
+}
+
+QVariant FunctionManagerImpl::nativeMd4Hex(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md4).toByteArray().toHex();
+}
+
+QVariant FunctionManagerImpl::nativeMd5(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md5);
+}
+
+QVariant FunctionManagerImpl::nativeMd5Hex(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md5).toByteArray().toHex();
+}
+
+QVariant FunctionManagerImpl::nativeSha1(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha1);
+}
+
+QVariant FunctionManagerImpl::nativeSha224(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha224);
+}
+
+QVariant FunctionManagerImpl::nativeSha256(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha256);
+}
+
+QVariant FunctionManagerImpl::nativeSha384(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha384);
+}
+
+QVariant FunctionManagerImpl::nativeSha512(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha512);
+}
+
+QVariant FunctionManagerImpl::nativeSha3_224(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_224);
+}
+
+QVariant FunctionManagerImpl::nativeSha3_256(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_256);
+}
+
+QVariant FunctionManagerImpl::nativeSha3_384(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_384);
+}
+
+QVariant FunctionManagerImpl::nativeSha3_512(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_512);
+}
+
+QStringList FunctionManagerImpl::getArgMarkers(int argCount)
+{
+ QStringList argMarkers;
+ for (int i = 0; i < argCount; i++)
+ argMarkers << "?";
+
+ return argMarkers;
+}
+
+void FunctionManagerImpl::registerNativeFunction(const QString& name, const QStringList& args, FunctionManager::NativeFunction::ImplementationFunction funcPtr)
+{
+ NativeFunction* nf = new NativeFunction();
+ nf->name = name;
+ nf->arguments = args;
+ nf->type = FunctionBase::SCALAR;
+ nf->undefinedArgs = false;
+ nf->functionPtr = funcPtr;
+ nativeFunctions << nf;
+}
+
+int qHash(const FunctionManagerImpl::Key& key)
+{
+ return qHash(key.name) ^ key.argCount ^ static_cast<int>(key.type);
+}
+
+bool operator==(const FunctionManagerImpl::Key& key1, const FunctionManagerImpl::Key& key2)
+{
+ return key1.name == key2.name && key1.type == key2.type && key1.argCount == key2.argCount;
+}
+
+FunctionManagerImpl::Key::Key()
+{
+}
+
+FunctionManagerImpl::Key::Key(FunctionBase* function) :
+ name(function->name), argCount(function->undefinedArgs ? -1 : function->arguments.size()), type(function->type)
+{
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.h
new file mode 100644
index 0000000..d8734e6
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.h
@@ -0,0 +1,96 @@
+#ifndef FUNCTIONMANAGERIMPL_H
+#define FUNCTIONMANAGERIMPL_H
+
+#include "services/functionmanager.h"
+#include <QCryptographicHash>
+
+class SqlFunctionPlugin;
+class Plugin;
+class PluginType;
+
+class API_EXPORT FunctionManagerImpl : public FunctionManager
+{
+ Q_OBJECT
+
+ public:
+ FunctionManagerImpl();
+
+ void setScriptFunctions(const QList<ScriptFunction*>& newFunctions);
+ QList<ScriptFunction*> getAllScriptFunctions() const;
+ QList<ScriptFunction*> getScriptFunctionsForDatabase(const QString& dbName) const;
+ QList<NativeFunction*> getAllNativeFunctions() const;
+ QVariant evaluateScalar(const QString& name, int argCount, const QList<QVariant>& args, Db* db, bool& ok);
+ void evaluateAggregateInitial(const QString& name, int argCount, Db* db, QHash<QString, QVariant>& aggregateStorage);
+ void evaluateAggregateStep(const QString& name, int argCount, const QList<QVariant>& args, Db* db, QHash<QString, QVariant>& aggregateStorage);
+ QVariant evaluateAggregateFinal(const QString& name, int argCount, Db* db, bool& ok, QHash<QString, QVariant>& aggregateStorage);
+ QVariant evaluateScriptScalar(ScriptFunction* func, const QString& name, int argCount, const QList<QVariant>& args, Db* db, bool& ok);
+ void evaluateScriptAggregateInitial(ScriptFunction* func, Db* db,
+ QHash<QString, QVariant>& aggregateStorage);
+ void evaluateScriptAggregateStep(ScriptFunction* func, const QList<QVariant>& args, Db* db,
+ QHash<QString, QVariant>& aggregateStorage);
+ QVariant evaluateScriptAggregateFinal(ScriptFunction* func, const QString& name, int argCount, Db* db, bool& ok,
+ QHash<QString, QVariant>& aggregateStorage);
+ QVariant evaluateNativeScalar(NativeFunction* func, const QList<QVariant>& args, Db* db, bool& ok);
+
+ private:
+ struct Key
+ {
+ Key();
+ Key(FunctionBase* function);
+
+ QString name;
+ int argCount;
+ FunctionBase::Type type;
+ };
+
+ friend int qHash(const FunctionManagerImpl::Key& key);
+ friend bool operator==(const FunctionManagerImpl::Key& key1, const FunctionManagerImpl::Key& key2);
+
+ void init();
+ void initNativeFunctions();
+ void refreshFunctionsByKey();
+ void refreshNativeFunctionsByKey();
+ void storeInConfig();
+ void loadFromConfig();
+ void clearFunctions();
+ QString cannotFindFunctionError(const QString& name, int argCount);
+ QString langUnsupportedError(const QString& name, int argCount, const QString& lang);
+ void registerNativeFunction(const QString& name, const QStringList& args, NativeFunction::ImplementationFunction funcPtr);
+
+ static QStringList getArgMarkers(int argCount);
+ static QVariant nativeRegExp(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSqlFile(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeReadFile(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeWriteFile(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeScript(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeLangs(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeHtmlEscape(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeUrlEncode(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeUrlDecode(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeBase64Encode(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeBase64Decode(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeCryptographicFunction(const QList<QVariant>& args, Db* db, bool& ok, QCryptographicHash::Algorithm algo);
+ static QVariant nativeMd4(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeMd4Hex(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeMd5(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeMd5Hex(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha1(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha224(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha256(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha384(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha512(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha3_224(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha3_256(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha3_384(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha3_512(const QList<QVariant>& args, Db* db, bool& ok);
+
+ QList<ScriptFunction*> functions;
+ QHash<Key,ScriptFunction*> functionsByKey;
+ QList<NativeFunction*> nativeFunctions;
+ QHash<Key,NativeFunction*> nativeFunctionsByKey;
+};
+
+int qHash(const FunctionManagerImpl::Key& key);
+bool operator==(const FunctionManagerImpl::Key& key1, const FunctionManagerImpl::Key& key2);
+
+#endif // FUNCTIONMANAGERIMPL_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.cpp
new file mode 100644
index 0000000..5d7a517
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.cpp
@@ -0,0 +1,820 @@
+#include "pluginmanagerimpl.h"
+#include "plugins/scriptingplugin.h"
+#include "plugins/genericplugin.h"
+#include "services/notifymanager.h"
+#include "common/unused.h"
+#include <QCoreApplication>
+#include <QDir>
+#include <QDebug>
+#include <QJsonArray>
+#include <QJsonValue>
+
+PluginManagerImpl::PluginManagerImpl()
+{
+}
+
+PluginManagerImpl::~PluginManagerImpl()
+{
+}
+
+void PluginManagerImpl::init()
+{
+ pluginDirs += qApp->applicationDirPath() + "/plugins";
+ pluginDirs += QDir(CFG->getConfigDir()).absoluteFilePath("plugins");
+
+ QString envDirs = SQLITESTUDIO->getEnv("SQLITESTUDIO_PLUGINS");
+ if (!envDirs.isNull())
+ pluginDirs += envDirs.split(PATH_LIST_SEPARATOR);
+
+#ifdef PLUGINS_DIR
+ pluginDirs += STRINGIFY(PLUGINS_DIR);
+#endif
+
+#ifdef SYS_PLUGINS_DIR
+ pluginDirs += STRINGIFY(SYS_PLUGINS_DIR);
+#endif
+
+#ifdef Q_OS_MACX
+ pluginDirs += QCoreApplication::applicationDirPath()+"/../PlugIns";
+#endif
+
+ scanPlugins();
+ loadPlugins();
+}
+
+void PluginManagerImpl::deinit()
+{
+ emit aboutToQuit();
+
+ // Plugin containers and their plugins
+ foreach (PluginContainer* container, pluginContainer.values())
+ {
+ if (container->builtIn)
+ {
+ container->plugin->deinit();
+ delete container->plugin;
+ }
+ else
+ unload(container->name);
+ }
+
+ foreach (PluginContainer* container, pluginContainer.values())
+ delete container;
+
+ pluginContainer.clear();
+
+ // Types
+ foreach (PluginType* type, registeredPluginTypes)
+ delete type;
+
+ registeredPluginTypes.clear();
+ pluginCategories.clear();
+}
+
+QList<PluginType*> PluginManagerImpl::getPluginTypes() const
+{
+ return registeredPluginTypes;
+}
+
+QStringList PluginManagerImpl::getPluginDirs() const
+{
+ return pluginDirs;
+}
+
+QString PluginManagerImpl::getFilePath(Plugin* plugin) const
+{
+ if (!pluginContainer.contains(plugin->getName()))
+ return QString::null;
+
+ return pluginContainer[plugin->getName()]->filePath;
+}
+
+bool PluginManagerImpl::loadBuiltInPlugin(Plugin* plugin)
+{
+ bool res = initPlugin(plugin);
+ res &= plugin->init();
+ return res;
+}
+
+PluginType* PluginManagerImpl::getPluginType(Plugin* plugin) const
+{
+ if (!pluginContainer.contains(plugin->getName()))
+ return nullptr;
+
+ return pluginContainer[plugin->getName()]->type;
+}
+
+void PluginManagerImpl::scanPlugins()
+{
+ QStringList nameFilters;
+ nameFilters << "*.so" << "*.dll" << "*.dylib";
+
+ QPluginLoader* loader = nullptr;
+ foreach (QString pluginDirPath, pluginDirs)
+ {
+ QDir pluginDir(pluginDirPath);
+ foreach (QString fileName, pluginDir.entryList(nameFilters, QDir::Files))
+ {
+ fileName = pluginDir.absoluteFilePath(fileName);
+ loader = new QPluginLoader(fileName);
+ loader->setLoadHints(QLibrary::ExportExternalSymbolsHint|QLibrary::ResolveAllSymbolsHint);
+
+ if (!initPlugin(loader, fileName))
+ {
+ qDebug() << "File" << fileName << "was loaded as plugin, but SQLiteStudio couldn't initialize plugin.";
+ delete loader;
+ }
+ }
+ }
+
+ QStringList names;
+ for (PluginContainer* container : pluginContainer.values())
+ {
+ if (!container->builtIn)
+ names << container->name;
+ }
+
+ qDebug() << "Following plugins found:" << names;
+}
+
+void PluginManagerImpl::loadPlugins()
+{
+ QStringList alreadyAttempted;
+ for (const QString& pluginName : pluginContainer.keys())
+ {
+ if (shouldAutoLoad(pluginName))
+ load(pluginName, alreadyAttempted);
+ }
+
+ pluginsAreInitiallyLoaded = true;
+ emit pluginsInitiallyLoaded();
+}
+
+bool PluginManagerImpl::initPlugin(QPluginLoader* loader, const QString& fileName)
+{
+ QJsonObject pluginMetaData = loader->metaData();
+ QString pluginTypeName = pluginMetaData.value("MetaData").toObject().value("type").toString();
+ PluginType* pluginType = nullptr;
+ foreach (PluginType* type, registeredPluginTypes)
+ {
+ if (type->getName() == pluginTypeName)
+ {
+ pluginType = type;
+ break;
+ }
+ }
+
+ if (!pluginType)
+ {
+ qWarning() << "Could not load plugin" + fileName + "because its type was not recognized:" << pluginTypeName;
+ return false;
+ }
+
+ QString pluginName = pluginMetaData.value("className").toString();
+ QJsonObject metaObject = pluginMetaData.value("MetaData").toObject();
+
+ if (!checkPluginRequirements(pluginName, metaObject))
+ return false;
+
+ PluginContainer* container = new PluginContainer;
+ container->type = pluginType;
+ container->filePath = fileName;
+ container->loaded = false;
+ container->loader = loader;
+ pluginCategories[pluginType] << container;
+ pluginContainer[pluginName] = container;
+
+ if (!readDependencies(pluginName, container, metaObject.value("dependencies")))
+ return false;
+
+ if (!readConflicts(pluginName, container, metaObject.value("conflicts")))
+ return false;
+
+ if (!readMetaData(container))
+ {
+ delete container;
+ return false;
+ }
+
+ return true;
+}
+
+bool PluginManagerImpl::checkPluginRequirements(const QString& pluginName, const QJsonObject& metaObject)
+{
+ if (metaObject.value("gui").toBool(false) && !SQLITESTUDIO->isGuiAvailable())
+ {
+ qDebug() << "Plugin" << pluginName << "skipped, because it requires GUI and this is not GUI client running.";
+ return false;
+ }
+
+ int minVer = metaObject.value("minQtVersion").toInt(0);
+ if (QT_VERSION_CHECK(minVer / 10000, minVer / 100 % 100, minVer % 10000) > QT_VERSION)
+ {
+ qDebug() << "Plugin" << pluginName << "skipped, because it requires at least Qt version" << toPrintableVersion(minVer) << ", but got" << QT_VERSION_STR;
+ return false;
+ }
+
+ int maxVer = metaObject.value("maxQtVersion").toInt(999999);
+ if (QT_VERSION_CHECK(maxVer / 10000, maxVer / 100 % 100, maxVer % 10000) < QT_VERSION)
+ {
+ qDebug() << "Plugin" << pluginName << "skipped, because it requires at most Qt version" << toPrintableVersion(maxVer) << ", but got" << QT_VERSION_STR;
+ return false;
+ }
+
+ minVer = metaObject.value("minAppVersion").toInt(0);
+ if (SQLITESTUDIO->getVersion() < minVer)
+ {
+ qDebug() << "Plugin" << pluginName << "skipped, because it requires at least SQLiteStudio version" << toPrintableVersion(minVer) << ", but got"
+ << SQLITESTUDIO->getVersionString();
+ return false;
+ }
+
+ maxVer = metaObject.value("maxAppVersion").toInt(999999);
+ if (SQLITESTUDIO->getVersion() > maxVer)
+ {
+ qDebug() << "Plugin" << pluginName << "skipped, because it requires at most SQLiteStudio version" << toPrintableVersion(maxVer) << ", but got"
+ << SQLITESTUDIO->getVersionString();
+ return false;
+ }
+
+ return true;
+}
+
+bool PluginManagerImpl::readDependencies(const QString& pluginName, PluginManagerImpl::PluginContainer* container, const QJsonValue& depsValue)
+{
+ if (depsValue.isUndefined())
+ return true;
+
+ QJsonArray depsArray;
+ if (depsValue.type() == QJsonValue::Array)
+ depsArray = depsValue.toArray();
+ else
+ depsArray.append(depsValue);
+
+ PluginDependency dep;
+ QJsonObject depObject;
+ for (const QJsonValue& value : depsArray)
+ {
+ if (value.type() == QJsonValue::Object)
+ {
+ depObject = value.toObject();
+ if (!depObject.contains("name"))
+ {
+ qWarning() << "Invalid dependency entry in plugin" << pluginName << " - doesn't contain 'name' of the dependency.";
+ return false;
+ }
+
+ dep.name = depObject.value("name").toString();
+ dep.minVersion = depObject.value("minVersion").toInt(0);
+ dep.maxVersion = depObject.value("maxVersion").toInt(0);
+ }
+ else
+ {
+ dep.maxVersion = 0;
+ dep.minVersion = 0;
+ dep.name = value.toString();
+ }
+ container->dependencies << dep;
+ }
+ return true;
+}
+
+bool PluginManagerImpl::readConflicts(const QString& pluginName, PluginManagerImpl::PluginContainer* container, const QJsonValue& confValue)
+{
+ UNUSED(pluginName);
+
+ if (confValue.isUndefined())
+ return true;
+
+ QJsonArray confArray;
+ if (confValue.type() == QJsonValue::Array)
+ confArray = confValue.toArray();
+ else
+ confArray.append(confValue);
+
+ for (const QJsonValue& value : confArray)
+ container->conflicts << value.toString();
+
+ return true;
+}
+
+bool PluginManagerImpl::initPlugin(Plugin* plugin)
+{
+ QString pluginName = plugin->getName();
+ PluginType* pluginType = nullptr;
+ foreach (PluginType* type, registeredPluginTypes)
+ {
+ if (type->test(plugin))
+ {
+ pluginType = type;
+ break;
+ }
+ }
+
+ if (!pluginType)
+ {
+ qWarning() << "Could not load built-in plugin" + pluginName + "because its type was not recognized.";
+ return false;
+ }
+
+ PluginContainer* container = new PluginContainer;
+ container->type = pluginType;
+ container->loaded = true;
+ container->builtIn = true;
+ container->plugin = plugin;
+ pluginCategories[pluginType] << container;
+ pluginContainer[pluginName] = container;
+ if (!readMetaData(container))
+ {
+ delete container;
+ return false;
+ }
+
+ pluginLoaded(container);
+ return true;
+}
+
+bool PluginManagerImpl::shouldAutoLoad(const QString& pluginName)
+{
+ QStringList loadedPlugins = CFG_CORE.General.LoadedPlugins.get().split(",", QString::SkipEmptyParts);
+ QStringList pair;
+ foreach (const QString& loadedPlugin, loadedPlugins)
+ {
+ pair = loadedPlugin.split("=");
+ if (pair.size() != 2)
+ {
+ qWarning() << "Invalid entry in config General.LoadedPlugins:" << loadedPlugin;
+ continue;
+ }
+
+ if (pair[0] == pluginName)
+ return (bool)pair[1].toInt();
+ }
+
+ return true;
+}
+
+QStringList PluginManagerImpl::getAllPluginNames(PluginType* type) const
+{
+ QStringList names;
+ if (!pluginCategories.contains(type))
+ return names;
+
+ foreach (PluginContainer* container, pluginCategories[type])
+ names << container->name;
+
+ return names;
+}
+
+QStringList PluginManagerImpl::getAllPluginNames() const
+{
+ return pluginContainer.keys();
+}
+
+PluginType* PluginManagerImpl::getPluginType(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ return nullptr;
+
+ return pluginContainer[pluginName]->type;
+}
+
+QString PluginManagerImpl::getAuthor(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ return QString::null;
+
+ return pluginContainer[pluginName]->author;
+}
+
+QString PluginManagerImpl::getTitle(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ return QString::null;
+
+ return pluginContainer[pluginName]->title;
+}
+
+QString PluginManagerImpl::getPrintableVersion(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ return QString::null;
+
+ return pluginContainer[pluginName]->printableVersion;
+}
+
+int PluginManagerImpl::getVersion(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ return 0;
+
+ return pluginContainer[pluginName]->version;
+}
+
+QString PluginManagerImpl::getDescription(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ return QString::null;
+
+ return pluginContainer[pluginName]->description;
+}
+
+void PluginManagerImpl::unload(Plugin* plugin)
+{
+ if (!plugin)
+ return;
+
+ unload(plugin->getName());
+}
+
+void PluginManagerImpl::unload(const QString& pluginName)
+{
+ if (!pluginContainer.contains(pluginName))
+ {
+ qWarning() << "No such plugin in containers:" << pluginName << "while trying to unload plugin.";
+ return;
+ }
+
+ // Checking preconditions
+ PluginContainer* container = pluginContainer[pluginName];
+ if (container->builtIn)
+ return;
+
+ if (!container->loaded)
+ return;
+
+ // Unloading depdendent plugins
+ for (PluginContainer* otherContainer : pluginContainer.values())
+ {
+ if (otherContainer == container)
+ continue;
+
+ for (const PluginDependency& dep : otherContainer->dependencies)
+ {
+ if (dep.name == pluginName)
+ {
+ unload(otherContainer->name);
+ break;
+ }
+ }
+ }
+
+ // Removing from fast-lookup collections
+ removePluginFromCollections(container->plugin);
+
+ // Deinitializing and unloading plugin
+ emit aboutToUnload(container->plugin, container->type);
+ container->plugin->deinit();
+
+ QPluginLoader* loader = container->loader;
+ if (!loader->isLoaded())
+ {
+ qWarning() << "QPluginLoader says the plugin is not loaded. Weird.";
+ emit unloaded(container->name, container->type);
+ return;
+ }
+
+ loader->unload();
+
+ container->plugin = nullptr;
+ container->loaded = false;
+
+ emit unloaded(container->name, container->type);
+
+ qDebug() << pluginName << "unloaded:" << container->filePath;
+}
+
+bool PluginManagerImpl::load(const QString& pluginName)
+{
+ QStringList alreadyAttempted;
+ bool res = load(pluginName, alreadyAttempted);
+ if (!res)
+ emit failedToLoad(pluginName);
+
+ return res;
+}
+
+bool PluginManagerImpl::load(const QString& pluginName, QStringList& alreadyAttempted, int minVersion, int maxVersion)
+{
+ if (alreadyAttempted.contains(pluginName))
+ return false;
+
+ // Checking initial conditions
+ if (!pluginContainer.contains(pluginName))
+ {
+ qWarning() << "No such plugin in containers:" << pluginName << "while trying to load plugin.";
+ alreadyAttempted.append(pluginName);
+ return false;
+ }
+
+ PluginContainer* container = pluginContainer[pluginName];
+
+ if (minVersion > 0 && container->version < minVersion)
+ {
+ qWarning() << "Requested plugin" << pluginName << "in version at least" << minVersion << "but have:" << container->version;
+ return false;
+ }
+
+ if (maxVersion > 0 && container->version > maxVersion)
+ {
+ qWarning() << "Requested plugin" << pluginName << "in version at most" << maxVersion << "but have:" << container->version;
+ return false;
+ }
+
+ if (container->builtIn)
+ return true;
+
+ QPluginLoader* loader = container->loader;
+ if (loader->isLoaded())
+ return true;
+
+ // Checking for conflicting plugins
+ for (PluginContainer* otherContainer : pluginContainer.values())
+ {
+ if (!otherContainer->loaded || otherContainer->name == pluginName)
+ continue;
+
+ if (container->conflicts.contains(otherContainer->name) || otherContainer->conflicts.contains(pluginName))
+ {
+ notifyWarn(tr("Cannot load plugin %1, because it's in conflict with plugin %2.").arg(pluginName, otherContainer->name));
+ alreadyAttempted.append(pluginName);
+ return false;
+ }
+ }
+
+ // Loading depended plugins
+ for (const PluginDependency& dep : container->dependencies)
+ {
+ if (!load(dep.name, alreadyAttempted, dep.minVersion, dep.maxVersion))
+ {
+ notifyWarn(tr("Cannot load plugin %1, because its dependency was not loaded: %2.").arg(pluginName, dep.name));
+ alreadyAttempted.append(pluginName);
+ return false;
+ }
+ }
+
+ // Loading pluginName
+ if (!loader->load())
+ {
+ notifyWarn(tr("Cannot load plugin %1. Error details: %2").arg(pluginName, loader->errorString()));
+ alreadyAttempted.append(pluginName);
+ return false;
+ }
+
+ // Initializing loaded plugin
+ Plugin* plugin = dynamic_cast<Plugin*>(container->loader->instance());
+ GenericPlugin* genericPlugin = dynamic_cast<GenericPlugin*>(plugin);
+ if (genericPlugin)
+ {
+ genericPlugin->loadMetaData(container->loader->metaData());
+ }
+
+ if (!plugin->init())
+ {
+ loader->unload();
+ notifyWarn(tr("Cannot load plugin %1 (error while initializing plugin).").arg(pluginName));
+ alreadyAttempted.append(pluginName);
+ return false;
+ }
+
+ pluginLoaded(container);
+
+ return true;
+}
+
+void PluginManagerImpl::pluginLoaded(PluginManagerImpl::PluginContainer* container)
+{
+ if (!container->builtIn)
+ {
+ container->plugin = dynamic_cast<Plugin*>(container->loader->instance());
+ container->loaded = true;
+ }
+ addPluginToCollections(container->plugin);
+
+ emit loaded(container->plugin, container->type);
+ if (!container->builtIn)
+ qDebug() << container->name << "loaded:" << container->filePath;
+}
+
+void PluginManagerImpl::addPluginToCollections(Plugin* plugin)
+{
+ ScriptingPlugin* scriptingPlugin = dynamic_cast<ScriptingPlugin*>(plugin);
+ if (scriptingPlugin)
+ scriptingPlugins[scriptingPlugin->getLanguage()] = scriptingPlugin;
+}
+
+void PluginManagerImpl::removePluginFromCollections(Plugin* plugin)
+{
+ ScriptingPlugin* scriptingPlugin = dynamic_cast<ScriptingPlugin*>(plugin);
+ if (scriptingPlugin && scriptingPlugins.contains(scriptingPlugin->getLanguage()))
+ scriptingPlugins.remove(plugin->getName());
+}
+
+bool PluginManagerImpl::readMetaData(PluginManagerImpl::PluginContainer* container)
+{
+ if (container->loader)
+ {
+ QHash<QString, QVariant> metaData = readMetaData(container->loader->metaData());
+ container->name = metaData["name"].toString();
+ container->version = metaData["version"].toInt();
+ container->printableVersion = toPrintableVersion(metaData["version"].toInt());
+ container->author = metaData["author"].toString();
+ container->description = metaData["description"].toString();
+ container->title = metaData["title"].toString();
+ }
+ else if (container->plugin)
+ {
+ container->name = container->plugin->getName();
+ container->version = container->plugin->getVersion();
+ container->printableVersion = container->plugin->getPrintableVersion();
+ container->author = container->plugin->getAuthor();
+ container->description = container->plugin->getDescription();
+ container->title = container->plugin->getTitle();
+ }
+ else
+ {
+ qCritical() << "Could not read metadata for some plugin. It has no loader or plugin object defined.";
+ return false;
+ }
+ return true;
+}
+
+bool PluginManagerImpl::isLoaded(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ {
+ qWarning() << "No such plugin in containers:" << pluginName << "while trying to get plugin 'loaded' status.";
+ return false;
+ }
+
+ return pluginContainer[pluginName]->loaded;
+}
+
+bool PluginManagerImpl::isBuiltIn(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ {
+ qWarning() << "No such plugin in containers:" << pluginName << "while trying to get plugin 'builtIn' status.";
+ return false;
+ }
+
+ return pluginContainer[pluginName]->builtIn;
+}
+
+Plugin* PluginManagerImpl::getLoadedPlugin(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ return nullptr;
+
+ if (!pluginContainer[pluginName]->loaded)
+ return nullptr;
+
+ return pluginContainer[pluginName]->plugin;
+}
+
+QList<Plugin*> PluginManagerImpl::getLoadedPlugins(PluginType* type) const
+{
+ QList<Plugin*> list;
+ if (!pluginCategories.contains(type))
+ return list;
+
+ foreach (PluginContainer* container, pluginCategories[type])
+ {
+ if (container->loaded)
+ list << container->plugin;
+ }
+
+ return list;
+}
+
+ScriptingPlugin* PluginManagerImpl::getScriptingPlugin(const QString& languageName) const
+{
+ if (scriptingPlugins.contains(languageName))
+ return scriptingPlugins[languageName];
+
+ return nullptr;
+}
+
+QHash<QString, QVariant> PluginManagerImpl::readMetaData(const QJsonObject& metaData)
+{
+ QHash<QString, QVariant> results;
+ results["name"] = metaData.value("className").toString();
+
+ QJsonObject root = metaData.value("MetaData").toObject();
+ results["type"] = root.value("type").toString();
+ results["title"] = root.value("title").toString();
+ results["description"] = root.value("description").toString();
+ results["author"] = root.value("author").toString();
+ results["version"] = root.value("version").toInt();
+ results["ui"] = root.value("ui").toString();
+ return results;
+}
+
+QString PluginManagerImpl::toPrintableVersion(int version) const
+{
+ static const QString versionStr = QStringLiteral("%1.%2.%3");
+ return versionStr.arg(version / 10000)
+ .arg(version / 100 % 100)
+ .arg(version % 100);
+}
+
+QStringList PluginManagerImpl::getDependencies(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ return QStringList();
+
+ static const QString verTpl = QStringLiteral(" (%1)");
+ QString minVerTpl = tr("min: %1", "plugin dependency version");
+ QString maxVerTpl = tr("max: %1", "plugin dependency version");
+ QStringList outputList;
+ QString depStr;
+ QStringList depVerList;
+ for (const PluginDependency& dep : pluginContainer[pluginName]->dependencies)
+ {
+ depStr = dep.name;
+ if (dep.minVersion > 0 || dep.maxVersion > 0)
+ {
+ depVerList.clear();
+ if (dep.minVersion > 0)
+ depVerList << minVerTpl.arg(toPrintableVersion(dep.minVersion));
+
+ if (dep.maxVersion > 0)
+ depVerList << minVerTpl.arg(toPrintableVersion(dep.maxVersion));
+
+ depStr += verTpl.arg(depVerList.join(", "));
+ }
+ outputList << depStr;
+ }
+
+ return outputList;
+}
+
+QStringList PluginManagerImpl::getConflicts(const QString& pluginName) const
+{
+ if (!pluginContainer.contains(pluginName))
+ return QStringList();
+
+ return pluginContainer[pluginName]->conflicts;
+}
+
+bool PluginManagerImpl::arePluginsInitiallyLoaded() const
+{
+ return pluginsAreInitiallyLoaded;
+}
+
+QList<Plugin*> PluginManagerImpl::getLoadedPlugins() const
+{
+ QList<Plugin*> plugins;
+ foreach (PluginContainer* container, pluginContainer.values())
+ {
+ if (container->loaded)
+ plugins << container->plugin;
+ }
+ return plugins;
+}
+
+QStringList PluginManagerImpl::getLoadedPluginNames() const
+{
+ QStringList names;
+ foreach (PluginContainer* container, pluginContainer.values())
+ {
+ if (container->loaded)
+ names << container->name;
+ }
+ return names;
+}
+
+QList<PluginManager::PluginDetails> PluginManagerImpl::getAllPluginDetails() const
+{
+ QList<PluginManager::PluginDetails> results;
+ PluginManager::PluginDetails details;
+ foreach (PluginContainer* container, pluginContainer.values())
+ {
+ details.name = container->name;
+ details.title = container->title;
+ details.description = container->description;
+ details.builtIn = container->builtIn;
+ details.version = container->version;
+ details.filePath = container->filePath;
+ details.versionString = formatVersion(container->version);
+ results << details;
+ }
+ return results;
+}
+
+QList<PluginManager::PluginDetails> PluginManagerImpl::getLoadedPluginDetails() const
+{
+ QList<PluginManager::PluginDetails> results = getAllPluginDetails();
+ QMutableListIterator<PluginManager::PluginDetails> it(results);
+ while (it.hasNext())
+ {
+ if (!isLoaded(it.next().name))
+ it.remove();
+ }
+ return results;
+}
+
+void PluginManagerImpl::registerPluginType(PluginType* type)
+{
+ registeredPluginTypes << type;
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.h
new file mode 100644
index 0000000..6968cab
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.h
@@ -0,0 +1,321 @@
+#ifndef PLUGINMANAGERIMPL_H
+#define PLUGINMANAGERIMPL_H
+
+#include "services/pluginmanager.h"
+#include <QPluginLoader>
+#include <QHash>
+
+class API_EXPORT PluginManagerImpl : public PluginManager
+{
+ Q_OBJECT
+
+ public:
+ /**
+ * @brief Creates plugin manager.
+ */
+ PluginManagerImpl();
+
+ /**
+ * @brief Deletes plugin manager.
+ */
+ ~PluginManagerImpl();
+
+ void init();
+ void deinit();
+ QList<PluginType*> getPluginTypes() const;
+ QStringList getPluginDirs() const;
+ QString getFilePath(Plugin* plugin) const;
+ bool loadBuiltInPlugin(Plugin* plugin);
+ bool load(const QString& pluginName);
+ void unload(const QString& pluginName);
+ void unload(Plugin* plugin);
+ bool isLoaded(const QString& pluginName) const;
+ bool isBuiltIn(const QString& pluginName) const;
+ Plugin* getLoadedPlugin(const QString& pluginName) const;
+ QStringList getAllPluginNames(PluginType* type) const;
+ QStringList getAllPluginNames() const;
+ PluginType* getPluginType(const QString& pluginName) const;
+ QString getAuthor(const QString& pluginName) const;
+ QString getTitle(const QString& pluginName) const;
+ QString getPrintableVersion(const QString& pluginName) const;
+ int getVersion(const QString& pluginName) const;
+ QString getDescription(const QString& pluginName) const;
+ PluginType* getPluginType(Plugin* plugin) const;
+ QList<Plugin*> getLoadedPlugins(PluginType* type) const;
+ ScriptingPlugin* getScriptingPlugin(const QString& languageName) const;
+ QHash<QString,QVariant> readMetaData(const QJsonObject& metaData);
+ QString toPrintableVersion(int version) const;
+ QStringList getDependencies(const QString& pluginName) const;
+ QStringList getConflicts(const QString& pluginName) const;
+ bool arePluginsInitiallyLoaded() const;
+ QList<Plugin*> getLoadedPlugins() const;
+ QStringList getLoadedPluginNames() const;
+ QList<PluginDetails> getAllPluginDetails() const;
+ QList<PluginDetails> getLoadedPluginDetails() const;
+
+ protected:
+ void registerPluginType(PluginType* type);
+
+ private:
+ struct PluginDependency
+ {
+ QString name;
+ int minVersion = 0;
+ int maxVersion = 0;
+ };
+
+ /**
+ * @brief Container for plugin related data.
+ *
+ * The container is used to represent plugin available to the application,
+ * no matter if it's loaded or not. It keeps all plugin related data,
+ * so it's available even the plugin is not loaded.
+ */
+ struct PluginContainer
+ {
+ /**
+ * @brief Name of the plugin.
+ */
+ QString name;
+
+ /**
+ * @brief Title of the plugin, used on UI.
+ */
+ QString title;
+
+ /**
+ * @brief Plugin's detailed description.
+ */
+ QString description;
+
+ /**
+ * @brief Plugin's author.
+ */
+ QString author;
+
+ /**
+ * @brief Numeric verion of the plugin.
+ */
+ int version;
+
+ /**
+ * @brief Human-readable version.
+ */
+ QString printableVersion;
+
+ /**
+ * @brief Type of the plugin.
+ */
+ PluginType* type = nullptr;
+
+ /**
+ * @brief Full path to the plugin's file.
+ */
+ QString filePath;
+
+ /**
+ * @brief Plugin's loaded state flag.
+ */
+ bool loaded;
+
+ /**
+ * @brief Qt's plugin framework loaded for this plugin.
+ */
+ QPluginLoader* loader = nullptr;
+
+ /**
+ * @brief Plugin object.
+ *
+ * It's null when plugin is not loaded.
+ */
+ Plugin* plugin = nullptr;
+
+ /**
+ * @brief Flag indicating that the plugin is built in.
+ *
+ * Plugins built-in are classes implementing plugin's interface,
+ * but they are compiled and statically linked to the main application binary.
+ * They cannot be loaded or unloaded - they are loaded by default.
+ */
+ bool builtIn = false;
+
+ /**
+ * @brief Names of plugnis that this plugin depends on.
+ */
+ QList<PluginDependency> dependencies;
+
+ /**
+ * @brief Names of plugins that this plugin conflicts with.
+ */
+ QStringList conflicts;
+ };
+
+ /**
+ * @brief List of plugins, both loaded and unloaded.
+ */
+ typedef QList<PluginContainer*> PluginContainerList;
+
+ /**
+ * @brief Scans plugin directories to find out available plugins.
+ *
+ * It looks in the following locations:
+ * <ul>
+ * <li> application_directory/plugins/
+ * <li> application_config_directory/plugins/
+ * <li> directory pointed by the SQLITESTUDIO_PLUGINS environment variable
+ * <li> directory compiled in as PLUGINS_DIR parameter of the compilation
+ * </ul>
+ *
+ * The application_directory is a directory where the application executable is.
+ * The application_config_directory can be different, see ConfigImpl::initDbFile() for details.
+ * The SQLITESTUDIO_PLUGINS variable can contain several paths, separated by : (for Unix/Mac) or ; (for Windows).
+ */
+ void scanPlugins();
+
+ /**
+ * @brief Loads plugins defined in configuration.
+ *
+ * It loads all plugins that are available to the application
+ * and are not marked to not load in the configuration.
+ *
+ * In other words, every plugin will load by default, unless it was
+ * explicitly unloaded previously and that was saved in the configuration
+ * (when application was closing).
+ */
+ void loadPlugins();
+
+ /**
+ * @brief Loads given plugin.
+ * @param pluginName Name of the plugin to load.
+ * @param alreadyAttempted List of plugin names that were already attempted to be loaded.
+ * @param minVersion Minimum required version of the plugin to load.
+ * @param maxVersion Maximum required version of the plugin to load.
+ * @return true on success, false on failure.
+ *
+ * This is pretty much what the public load() method does, except this one tracks what plugins were already
+ * attempted to be loaded (and failed), so it doesn't warn twice about the same plugin if it failed
+ * to load while it was a dependency for some other plugins.
+ *
+ * It also allows to define minimum and maximum plugin version, so if SQLiteStudio has the plugin available,
+ * but the version is out of required range, it will also fail to load.
+ */
+ bool load(const QString& pluginName, QStringList& alreadyAttempted, int minVersion = 0, int maxVersion = 0);
+
+ /**
+ * @brief Executes standard routines after plugin was loaded.
+ * @param container Container for the loaded plugin.
+ *
+ * It fills all members of the plugin container and emits loaded() signal.
+ */
+ void pluginLoaded(PluginContainer* container);
+
+ /**
+ * @brief Stores some specific plugin types in internal collections for faster access.
+ * @param plugin Plugin that was just loaded.
+ *
+ * This is called after we are sure we have a Plugin instance.
+ *
+ * The method stores certain plugin types in internal collections, so they can be accessed
+ * faster, instead of calling getLoadedPlugin<T>(), which is not as fast.
+ *
+ * The internal collections are used for plugins that are likely to be accessed frequently,
+ * like ScriptingPlugin.
+ */
+ void addPluginToCollections(Plugin* plugin);
+
+ /**
+ * @brief Removes plugin from internal collections.
+ * @param plugin Plugin that is about to be unloaded.
+ *
+ * This is the reverse operation to what addPluginToCollections(Plugin*) does.
+ */
+ void removePluginFromCollections(Plugin* plugin);
+
+ /**
+ * @brief Reads title, description, author, etc. from the plugin.
+ * @param plugin Plugin to read data from.
+ * @param container Container to put the data to.
+ * @return true on success, false on problems (with details in logs)
+ *
+ * It does the reading by calling all related methods from Plugin interface,
+ * then stores those information in given \p container.
+ *
+ * The built-in plugins define those methods using their class metadata.
+ *
+ * External plugins provide this information in their file metadata
+ * and this method uses QPluginLoader to read this metadata.
+ */
+ bool readMetaData(PluginContainer* container);
+
+ /**
+ * @brief Creates plugin container and initializes it.
+ * @param loader Qt's plugin framework loader used to load this plugin.
+ * For built-in plugins (statically linked) this must be null.
+ * @param fileName Plugin's file path. For built-in plugins it's ignored.
+ * @param plugin Plugin object from loaded plugin.
+ * @return true if the initialization succeeded, or false otherwise.
+ *
+ * It assigns plugin type to the plugin, creates plugin container and fills
+ * all necessary data for the plugin. If the plugin was configured to not load,
+ * then this method unloads the file, before plugin was initialized (with Plugin::init()).
+ *
+ * All plugins are loaded at the start, but before they are fully initialized
+ * and enabled, they are simply queried for metadata, then either unloaded
+ * (when configured to not load at startup), or the initialization proceeds.
+ */
+ bool initPlugin(QPluginLoader* loader, const QString& fileName);
+
+ bool checkPluginRequirements(const QString& pluginName, const QJsonObject& metaObject);
+ bool readDependencies(const QString& pluginName, PluginContainer* container, const QJsonValue& depsValue);
+ bool readConflicts(const QString& pluginName, PluginContainer* container, const QJsonValue& confValue);
+
+ /**
+ * @brief Creates plugin container and initializes it.
+ * @param plugin Built-in plugin object.
+ * @return true if the initialization succeeded, or false otherwise.
+ *
+ * This is pretty much the same as the other initPlugin() method, but this one is for built-in plugins.
+ */
+ bool initPlugin(Plugin* plugin);
+
+ /**
+ * @brief Tests if given plugin is configured to be loaded at startup.
+ * @param plugin Tested plugin object.
+ * @return true if plugin should be loaded at startup, or false otherwise.
+ *
+ * This method checks General.LoadedPlugins configuration entry to see if plugin
+ * was explicitly disabled for loading at startup.
+ */
+ bool shouldAutoLoad(const QString& pluginName);
+
+ /**
+ * @brief List of plugin directories (not necessarily absolute paths).
+ */
+ QStringList pluginDirs;
+
+ /**
+ * @brief List of registered plugin types.
+ */
+ QList<PluginType*> registeredPluginTypes;
+
+ /**
+ * @brief Table with plugin types as keys and list of plugins assigned for each type.
+ */
+ QHash<PluginType*,PluginContainerList> pluginCategories;
+
+ /**
+ * @brief Table with plugin names and containers assigned for each plugin.
+ */
+ QHash<QString,PluginContainer*> pluginContainer;
+
+ /**
+ * @brief Internal list of scripting plugins, updated on load/unload of plugins.
+ *
+ * Keys are scripting language name. It's a separate table to optimize querying scripting plugins.
+ */
+ QHash<QString,ScriptingPlugin*> scriptingPlugins;
+
+ bool pluginsAreInitiallyLoaded = false;
+};
+
+#endif // PLUGINMANAGERIMPL_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/importmanager.cpp b/SQLiteStudio3/coreSQLiteStudio/services/importmanager.cpp
new file mode 100644
index 0000000..53803e5
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/importmanager.cpp
@@ -0,0 +1,104 @@
+#include "importmanager.h"
+#include "services/pluginmanager.h"
+#include "services/notifymanager.h"
+#include "plugins/importplugin.h"
+#include "importworker.h"
+#include "db/db.h"
+#include "common/unused.h"
+#include <QThreadPool>
+#include <QDebug>
+
+ImportManager::ImportManager()
+{
+}
+
+QStringList ImportManager::getImportDataSourceTypes() const
+{
+ QStringList types;
+ for (ImportPlugin* plugin : PLUGINS->getLoadedPlugins<ImportPlugin>())
+ types << plugin->getDataSourceTypeName();
+
+ return types;
+}
+
+ImportPlugin* ImportManager::getPluginForDataSourceType(const QString& dataSourceType) const
+{
+ for (ImportPlugin* plugin : PLUGINS->getLoadedPlugins<ImportPlugin>())
+ {
+ if (plugin->getDataSourceTypeName() == dataSourceType)
+ return plugin;
+ }
+
+ return nullptr;
+}
+
+void ImportManager::configure(const QString& dataSourceType, const ImportManager::StandardImportConfig& config)
+{
+ plugin = getPluginForDataSourceType(dataSourceType);
+ importConfig = config;
+}
+
+void ImportManager::importToTable(Db* db, const QString& table)
+{
+ this->db = db;
+ this->table = table;
+
+ if (importInProgress)
+ {
+ emit importFailed();
+ qCritical() << "Tried to import while other import was in progress.";
+ return;
+ }
+
+ if (!db->isOpen())
+ {
+ emit importFailed();
+ qCritical() << "Tried to import into closed database.";
+ return;
+ }
+
+ if (!plugin)
+ {
+ emit importFailed();
+ qCritical() << "Tried to import, while ImportPlugin was null.";
+ return;
+ }
+
+ importInProgress = true;
+
+ ImportWorker* worker = new ImportWorker(plugin, &importConfig, db, table);
+ connect(worker, SIGNAL(finished(bool)), this, SLOT(finalizeImport(bool)));
+ connect(worker, SIGNAL(createdTable(Db*,QString)), this, SLOT(handleTableCreated(Db*,QString)));
+ connect(this, SIGNAL(orderWorkerToInterrupt()), worker, SLOT(interrupt()));
+
+ QThreadPool::globalInstance()->start(worker);
+}
+
+void ImportManager::interrupt()
+{
+ emit orderWorkerToInterrupt();
+}
+
+bool ImportManager::isAnyPluginAvailable()
+{
+ return PLUGINS->getLoadedPlugins<ImportPlugin>().size() > 0;
+}
+
+void ImportManager::finalizeImport(bool result)
+{
+ importInProgress = false;
+ emit importFinished();
+ if (result)
+ {
+ notifyInfo(tr("Imported data to the table '%1' successfully.").arg(table));
+ emit importSuccessful();
+ }
+ else
+ emit importFailed();
+}
+
+void ImportManager::handleTableCreated(Db* db, const QString& table)
+{
+ UNUSED(table);
+ emit schemaModified(db);
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/importmanager.h b/SQLiteStudio3/coreSQLiteStudio/services/importmanager.h
new file mode 100644
index 0000000..6f13826
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/importmanager.h
@@ -0,0 +1,85 @@
+#ifndef IMPORTMANAGER_H
+#define IMPORTMANAGER_H
+
+#include "pluginservicebase.h"
+#include "coreSQLiteStudio_global.h"
+#include <QFlags>
+#include <QStringList>
+
+class ImportPlugin;
+class Db;
+class CfgEntry;
+
+class API_EXPORT ImportManager : public PluginServiceBase
+{
+ Q_OBJECT
+
+ public:
+ struct StandardImportConfig
+ {
+ /**
+ * @brief Text encoding.
+ *
+ * Always one of QTextCodec::availableCodecs().
+ * Codec is important for text-based data. For binary data it should irrelevant to the import plugin.
+ */
+ QString codec;
+
+ /**
+ * @brief Name of the file that the import is being done from.
+ *
+ * This is provided just for information to the import process,
+ * but the plugin should use data stream provided to each called import method,
+ * instead of opening the file from this name.
+ *
+ * It will be null string if importing is not performed from a file, but from somewhere else
+ * (for example from a clipboard).
+ */
+ QString inputFileName;
+ };
+
+ enum StandardConfigFlag
+ {
+ CODEC = 0x01, /**< Text encoding (see StandardImportConfig::codec). */
+ FILE_NAME = 0x02, /**< Input file (see StandardImportConfig::inputFileName). */
+ };
+
+ Q_DECLARE_FLAGS(StandardConfigFlags, StandardConfigFlag)
+
+ ImportManager();
+
+ QStringList getImportDataSourceTypes() const;
+ ImportPlugin* getPluginForDataSourceType(const QString& dataSourceType) const;
+
+ void configure(const QString& dataSourceType, const StandardImportConfig& config);
+ void importToTable(Db* db, const QString& table);
+
+ static bool isAnyPluginAvailable();
+
+ private:
+ StandardImportConfig importConfig;
+ ImportPlugin* plugin = nullptr;
+ bool importInProgress = false;
+ Db* db = nullptr;
+ QString table;
+
+ public slots:
+ void interrupt();
+
+ private slots:
+ void finalizeImport(bool result);
+ void handleTableCreated(Db* db, const QString& table);
+
+ signals:
+ void importFinished();
+ void importSuccessful();
+ void importFailed();
+ void orderWorkerToInterrupt();
+ void schemaModified(Db* db);
+};
+
+#define IMPORT_MANAGER SQLITESTUDIO->getImportManager()
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(ImportManager::StandardConfigFlags)
+
+#endif // IMPORTMANAGER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/notifymanager.cpp b/SQLiteStudio3/coreSQLiteStudio/services/notifymanager.cpp
new file mode 100644
index 0000000..0980399
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/notifymanager.cpp
@@ -0,0 +1,85 @@
+#include "services/notifymanager.h"
+
+DEFINE_SINGLETON(NotifyManager)
+
+NotifyManager::NotifyManager(QObject *parent) :
+ QObject(parent)
+{
+}
+
+void NotifyManager::error(const QString &msg)
+{
+ addToRecentList(recentErrors, msg);
+ emit notifyError(msg);
+}
+
+void NotifyManager::warn(const QString &msg)
+{
+ addToRecentList(recentWarnings, msg);
+ emit notifyWarning(msg);
+}
+
+void NotifyManager::info(const QString &msg)
+{
+ addToRecentList(recentInfos, msg);
+ emit notifyInfo(msg);
+}
+
+void NotifyManager::modified(Db* db, const QString& database, const QString& object)
+{
+ emit objectModified(db, database, object);
+}
+
+void NotifyManager::deleted(Db* db, const QString& database, const QString& object)
+{
+ emit objectDeleted(db, database, object);
+}
+
+void NotifyManager::createded(Db* db, const QString& database, const QString& object)
+{
+ emit objectCreated(db, database, object);
+}
+
+void NotifyManager::renamed(Db* db, const QString& database, const QString& oldObject, const QString& newObject)
+{
+ emit objectRenamed(db, database, oldObject, newObject);
+}
+
+void NotifyManager::addToRecentList(QStringList& list, const QString &message)
+{
+ list << message;
+ if (list.size() <= maxRecentMessages)
+ return;
+
+ list = list.mid(list.length() - maxRecentMessages);
+}
+
+QList<QString> NotifyManager::getRecentInfos() const
+{
+ return recentInfos;
+}
+
+QList<QString> NotifyManager::getRecentWarnings() const
+{
+ return recentWarnings;
+}
+
+QList<QString> NotifyManager::getRecentErrors() const
+{
+ return recentErrors;
+}
+
+void notifyError(const QString &msg)
+{
+ NotifyManager::getInstance()->error(msg);
+}
+
+void notifyWarn(const QString &msg)
+{
+ NotifyManager::getInstance()->warn(msg);
+}
+
+void notifyInfo(const QString &msg)
+{
+ NotifyManager::getInstance()->info(msg);
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/notifymanager.h b/SQLiteStudio3/coreSQLiteStudio/services/notifymanager.h
new file mode 100644
index 0000000..5bb4571
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/notifymanager.h
@@ -0,0 +1,58 @@
+#ifndef NOTIFYMANAGER_H
+#define NOTIFYMANAGER_H
+
+#include "db/db.h"
+#include "common/global.h"
+#include <QStringList>
+#include <QObject>
+
+class API_EXPORT NotifyManager : public QObject
+{
+ Q_OBJECT
+
+ DECLARE_SINGLETON(NotifyManager)
+
+ public:
+ explicit NotifyManager(QObject *parent = 0);
+
+ QList<QString> getRecentErrors() const;
+ QList<QString> getRecentWarnings() const;
+ QList<QString> getRecentInfos() const;
+
+ signals:
+ void notifyError(const QString& msg);
+ void notifyWarning(const QString& msg);
+ void notifyInfo(const QString& msg);
+
+ void objectModified(Db* db, const QString& database, const QString& object);
+ void objectDeleted(Db* db, const QString& database, const QString& object);
+ void objectCreated(Db* db, const QString& database, const QString& object);
+ void objectRenamed(Db* db, const QString& database, const QString& oldObject, const QString& newObject);
+
+ public slots:
+ void error(const QString& msg);
+ void warn(const QString& msg);
+ void info(const QString& msg);
+
+ void modified(Db* db, const QString& database, const QString& object);
+ void deleted(Db* db, const QString& database, const QString& object);
+ void createded(Db* db, const QString& database, const QString& object);
+ void renamed(Db* db, const QString& database, const QString& oldObject, const QString& newObject);
+
+ private:
+ void addToRecentList(QStringList& list, const QString& message);
+
+ static const constexpr int maxRecentMessages = 10;
+
+ QStringList recentErrors;
+ QStringList recentWarnings;
+ QStringList recentInfos;
+};
+
+#define NOTIFY_MANAGER NotifyManager::getInstance()
+
+void API_EXPORT notifyError(const QString& msg);
+void API_EXPORT notifyWarn(const QString& msg);
+void API_EXPORT notifyInfo(const QString& msg);
+
+#endif // NOTIFYMANAGER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/pluginmanager.h b/SQLiteStudio3/coreSQLiteStudio/services/pluginmanager.h
new file mode 100644
index 0000000..4f822bc
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/pluginmanager.h
@@ -0,0 +1,528 @@
+#ifndef PLUGINMANAGER_H
+#define PLUGINMANAGER_H
+
+#include "coreSQLiteStudio_global.h"
+#include "plugins/plugin.h"
+#include "plugins/plugintype.h"
+#include "common/global.h"
+#include "sqlitestudio.h"
+#include <QStringList>
+
+class Plugin;
+class ScriptingPlugin;
+
+/** @file */
+
+/**
+ * @brief The plugin manager.
+ *
+ * It's a singleton accessible with PLUGINS macro.
+ *
+ * It provides methods to load, unload and query plugins. It stores loaded
+ * plugins in configuration on application close and loads that plugins during next startup.
+ * If there's a plugin which was not defined if it was loaded or not - it is loaded by default.
+ *
+ * Description of Plugin interface contains list of directories scanned for plugins.
+ *
+ * There's a macro for global access to the PluginManager - ::PLUGINS. It actually calls
+ * SQLiteStudio::getInstance() and from there it calls SQLiteStudio::getPluginManager().
+ *
+ * Plugins in PluginManager are organized by types. The manager has a list of types and for each type
+ * there's a list of plugins of that type. Plugin types are represented by PluginType class.
+ *
+ * @section querying_plugins Querying available and loaded plugins
+ *
+ * To query all plugins available to the application (including those not loaded) use getAllPluginNames().
+ *
+ * To query if certain plugin is loaded use isLoaded().
+ *
+ * To query all plugins loaded to the application use getLoadedPlugins(). It requires either PluginType,
+ * or plugin interface class (for template method version) to determinate what group of plugins you're
+ * interested in. To return all plugins (no matter what type), use template method version with Plugin
+ * as an interface type for parameter. An example of getting all SQL formatter plugins:
+ * @code
+ * QList<SqlFormatterPlugin*> formatterPlugins = PLUGINS->getLoadedPlugins<SqlFormatterPlugin>();
+ * @endcode
+ *
+ * To get list of plugin types use getPluginTypes().
+ *
+ * To get PluginType for given plugin interface use getPluginType<PluginInterfaceClass>().
+ *
+ * These are just the most important methods to query plugins. See full list of methods for more.
+ *
+ * @section load_unload Loading and unloading plugins
+ *
+ * To load plugin use load().
+ *
+ * To unload plugin use unload().
+ *
+ * Apart from that, all plugins are loaded initially (unless they were unloaded last time during
+ * application close).
+ *
+ * @section plugin_types Specialized plugin types
+ *
+ * Each plugin must implement Plugin interface, but it also can implement other interfaces,
+ * which makes them suitable for fulfilling certain functionalities. For example all plugins
+ * implementing SqlFormatterPlugin will automatically be available to SqlFormatter object,
+ * because PluginManager knows which plugins implement SqlFormatterPlugin and can provide full
+ * list of those plugins to SqlFormatter. This is done by call to registerPluginType().
+ *
+ * The registerPluginType() registers new type of plugins that will be recognizable by PluginManager.
+ * Once the new interface is registered with this method, all plugins will be tested against
+ * implementation for that type and those which implement the interface will be stored
+ * in the proper collection assigned for that plugin type.
+ *
+ * This way PluginManager can provide list of all plugins implementing given interface
+ * with getLoadedPlugins().
+ *
+ * All registered plugin types can be queries by getPluginTypes() method.
+ */
+class API_EXPORT PluginManager : public QObject
+{
+ Q_OBJECT
+
+ public:
+ struct PluginDetails
+ {
+ QString name;
+ QString title;
+ QString description;
+ bool builtIn = false;
+ int version = 0;
+ QString versionString;
+ QString filePath;
+ };
+
+ /**
+ * @brief Loads all plugins.
+ *
+ * Scans all plugin directories and tries to load all plugins found there. For list of directories
+ * see description of Plugin class.
+ */
+ virtual void init() = 0;
+
+ /**
+ * @brief Unloads all loaded plugins.
+ *
+ * Also deregisters all plugin types.
+ */
+ virtual void deinit() = 0;
+
+ /**
+ * @brief Provides list of registered plugin types.
+ * @return List of registered plugin types.
+ */
+ virtual QList<PluginType*> getPluginTypes() const = 0;
+
+ /**
+ * @brief Provides list of plugin directories.
+ * @return List of directory paths (not necessarily absolute paths).
+ */
+ virtual QStringList getPluginDirs() const = 0;
+
+ /**
+ * @brief Provides absolute path to the plugin's file.
+ * @param plugin Loaded plugin.
+ * @return Absolute path to the plugin file.
+ */
+ virtual QString getFilePath(Plugin* plugin) const = 0;
+
+ /**
+ * @brief Loads instance of built-in plugin into the manager.
+ * @param plugin Plugin instance.
+ * @return true on success or false on failure (plugin's type could not be matched to registered plugin types).
+ *
+ * Built-in plugins are classes that implement plugin interface, but they are not in separate library.
+ * Instead they are classes compiled and linked to the main application. Such classes should be instantiated
+ * and passed to this method, so the PluginManager can treat it as any other plugin.
+ *
+ * @note Built-in plugins cannot be loaded or unloaded, so calls to load() or unload() will make no effect.
+ */
+ virtual bool loadBuiltInPlugin(Plugin* plugin) = 0;
+
+ /**
+ * @brief Loads the plugin.
+ * @param pluginName Name of the plugin to load.
+ * @return true on success, or false on failure.
+ *
+ * When loading a plugin, PluginManager loads the plugin file and resolves all its symbols inside.
+ * If that failed, file gets unloaded and the method returns false.
+ *
+ * Qt plugins framework will require that the loaded plugin will provide exactly one Plugin interface
+ * implementation. Otherwise file will be unloaded and this method will return false.
+ *
+ * Then the Plugin::init() method is called. It it returns false, then plugin is unloaded
+ * and this method returns false.
+ *
+ * Then meta information is read from the plugin (title, version, author, etc) - see Plugin for details.
+ *
+ * Then loaded plugin passes several tests against all registered plugin types. If it implements
+ * any type, it's added to the plugin list of that type.
+ *
+ * Then the loaded() signal is emitted. Finally, the true value is returned.
+ */
+ virtual bool load(const QString& pluginName) = 0;
+
+ /**
+ * @brief Unloads plugin.
+ * @param pluginName Plugin name to be unloaded.
+ *
+ * If the plugin is not loaded, this method does nothing.
+ * First the aboutToUnload() signal is emitted. Then Plugin::deinit() is called.
+ * Then the plugin library is unloaded (which causes Qt's plugins framework to delete the object
+ * implementing Plugin interface before the actual unloading).
+ *
+ * Finally, the unloaded() signal is emitted.
+ */
+ virtual void unload(const QString& pluginName) = 0;
+
+ /**
+ * @brief Unloads plugin.
+ * @param plugin Loaded plugin to be unloaded.
+ * @overload
+ */
+ virtual void unload(Plugin* plugin) = 0;
+
+ /**
+ * @brief Tests if given plugin is loaded.
+ * @param pluginName Name of the plugin to test.
+ * @return true if the plugin is loaded, or false otherwise.
+ */
+ virtual bool isLoaded(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Tests whether given plugin is one of built-in plugins.
+ * @param pluginName Name of the plugin to test.
+ * @return true if the plugin is the built-in one, or false otherwise.
+ *
+ * @see loadBuiltInPlugin()
+ */
+ virtual bool isBuiltIn(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Finds loaded plugin by name.
+ * @param pluginName Plugin name to look for.
+ * @return Loaded plugin object, or null of the plugin is not loaded.
+ */
+ virtual Plugin* getLoadedPlugin(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Provides list of plugin names of given type.
+ * @param type Type of plugins to get names for.
+ * @return List of names.
+ *
+ * It returns names for all plugins available for the application,
+ * no matter they're currently loaded or not.
+ */
+ virtual QStringList getAllPluginNames(PluginType* type) const = 0;
+
+ virtual QList<PluginDetails> getAllPluginDetails() const = 0;
+ virtual QList<PluginDetails> getLoadedPluginDetails() const = 0;
+
+ /**
+ * @brief Provides list of all plugin names.
+ * @return All available plugin names, no matter if loaded or not.
+ */
+ virtual QStringList getAllPluginNames() const = 0;
+
+ /**
+ * @brief Finds plugin's type.
+ * @param pluginName Plugin name (can be unloaded plugin).
+ * @return Type of the plugin, or null if plugin was not found by the name.
+ */
+ virtual PluginType* getPluginType(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Provides plugin's author.
+ * @param pluginName Name of the plugin (can be unloaded plugin).
+ * @return Author string defined in the plugin.
+ */
+ virtual QString getAuthor(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Provides plugin's title.
+ * @param pluginName Name of the plugin (can be unloaded plugin).
+ * @return Title string defined in the plugin.
+ */
+ virtual QString getTitle(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Provides human-readable version of the plugin.
+ * @param pluginName Name of the plugin (can be unloaded plugin).
+ * @return Version string defined in the plugin.
+ */
+ virtual QString getPrintableVersion(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Provides numeric version of the plugin.
+ * @param pluginName Name of the plugin (can be unloaded plugin).
+ * @return Numeric version defined in the plugin.
+ */
+ virtual int getVersion(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Provides detailed description about the plugin.
+ * @param pluginName Name of the plugin (can be unloaded plugin).
+ * @return Description defined in the plugin.
+ */
+ virtual QString getDescription(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Tells plugin's type.
+ * @param plugin Loaded plugin.
+ * @return Type of the plugin.
+ */
+ virtual PluginType* getPluginType(Plugin* plugin) const = 0;
+
+ /**
+ * @brief Provides list of plugins for given plugin type.
+ * @param type Type of plugins.
+ * @return List of plugins for given type.
+ *
+ * This version of the method takes plugin type object as an discriminator.
+ * This way you can iterate through all types (using getPluginTypes())
+ * and then for each type get list of plugins for that type, using this method.
+ */
+ virtual QList<Plugin*> getLoadedPlugins(PluginType* type) const = 0;
+
+ /**
+ * @brief Provides list of all loaded plugins.
+ * @return List of plugins.
+ */
+ virtual QList<Plugin*> getLoadedPlugins() const = 0;
+
+ /**
+ * @brief Provides names of all loaded plugins.
+ * @return List of plugin names.
+ */
+ virtual QStringList getLoadedPluginNames() const = 0;
+
+ /**
+ * @brief Provides scripting plugin for given scripting language if available.
+ * @param languageName Scripting language name to get plugin for.
+ * @return Plugin object or null if proper plugin was not found.
+ *
+ * Calling this function is similar in results to call to getLoadedPlugins<ScriptingPlugin>()
+ * and then extracting a single plugin with desired scripting language support, except
+ * calling this function is much faster. PluginManager keeps scripting language plugins
+ * internally in hash table with language names as keys, so getting scripting plugin
+ * for desired language is way faster when using this method.
+ */
+ virtual ScriptingPlugin* getScriptingPlugin(const QString& languageName) const = 0;
+
+ /**
+ * @brief Loads metadata from given Json object.
+ * @param The metadata from json file.
+ * @return Metadata with keys: type, name, title, description, version, author, ui (optional).
+ */
+ virtual QHash<QString,QVariant> readMetaData(const QJsonObject& metaData) = 0;
+
+ /**
+ * @brief Converts integer version to string version.
+ * @param version Integer version in XXYYZZ standard (see Plugin::getVersion() for details).
+ * @return Printable version string.
+ */
+ virtual QString toPrintableVersion(int version) const = 0;
+
+ /**
+ * @brief Provides list of plugin names that the queried plugin depends on.
+ * @param pluginName Queried plugin name.
+ * @return List of plugin names, usually an empty list.
+ *
+ * This is the list that is declared in plugins metadata under the "dependencies" key.
+ * The plugin can be loaded only if all its dependencies were successfully loaded.
+ */
+ virtual QStringList getDependencies(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Provides list of plugin names that are declared to be in conflict with queries plugin.
+ * @param pluginName Queried plugin name,
+ * @return List of plugin names, usually an empty list.
+ *
+ * If a plugin declares other plugin (by name) to be its conflict (a "conflicts" key in plugin's metadata),
+ * then those 2 plugins cannot be loaded at the same time. SQLiteStudio will always refuse to load
+ * the other one, if the first one is already loaded - and vice versa.
+ *
+ * Declaring conflicts for a plugin can be useful for example if somebody wants to proivde an alternative
+ * implementation of SQLite2 database plugin, etc. In that case SQLiteStudio won't get confused in
+ * deciding which plugin to use for supporting such databases.
+ */
+ virtual QStringList getConflicts(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Tells if plugins were already loaded on startup, or is this yet to happen.
+ * @return true if plugins were loaded, false if they are going to be loaded.
+ */
+ virtual bool arePluginsInitiallyLoaded() const = 0;
+
+ /**
+ * @brief registerPluginType Registers plugin type for loading and managing.
+ * @tparam T Interface class (as defined by Qt plugins standard)
+ * @param form Optional name of form object.
+ * @param title Optional title for configuration dialog.
+ * The form object name is different if you register new type by general type plugin.
+ * Built-in types are defined as the name of page from ConfigDialog.
+ * Types registered from plugins should use top widget name defined in the ui file.
+ * The title parameter is required if the configuration form was defined outside (in plugin).
+ * Title will be used for configuration dialog to display plugin type category (on the left of the dialog).
+ */
+ template <class T>
+ void registerPluginType(const QString& title, const QString& form = QString())
+ {
+ registerPluginType(new DefinedPluginType<T>(title, form));
+ }
+
+ /**
+ * @brief Gets plugin type for given plugin interface.
+ * @tparam T Interface class of the plugin.
+ * @return Type of the plugin for given interface if registered, or null otherwise.
+ */
+ template <class T>
+ PluginType* getPluginType() const
+ {
+ foreach (PluginType* type, getPluginTypes())
+ {
+ if (!dynamic_cast<DefinedPluginType<T>*>(type))
+ continue;
+
+ return type;
+ }
+ return nullptr;
+ }
+
+ /**
+ * @brief Provide list of plugins of given type.
+ * @tparam T Interface class of plugins, that we want to get.
+ *
+ * This method version gets plugin interface type as template parameter,
+ * so it returns list of loaded plugins that are already casted to requested
+ * interface type.
+ */
+ template <class T>
+ QList<T*> getLoadedPlugins() const
+ {
+ QList<T*> typedPlugins;
+ PluginType* type = getPluginType<T>();
+ if (!type)
+ return typedPlugins;
+
+ foreach (Plugin* plugin, getLoadedPlugins(type))
+ typedPlugins << dynamic_cast<T*>(plugin);
+
+ return typedPlugins;
+ }
+
+ /**
+ * @brief Provide list of plugin names of given type.
+ * @tparam T Interface class of plugins, that we want to get names for.
+ *
+ * This method version gets plugin interface type as template parameter,
+ * so it returns list of names of loaded plugins.
+ */
+ template <class T>
+ QStringList getLoadedPluginNames() const
+ {
+ QStringList names;
+ PluginType* type = getPluginType<T>();
+ if (!type)
+ return names;
+
+ foreach (Plugin* plugin, getLoadedPlugins(type))
+ names << plugin->getName();
+
+ return names;
+ }
+
+ protected:
+ /**
+ * @brief Adds given type to registered plugins list.
+ * @param type Type instance.
+ *
+ * This is a helper method for registerPluginType<T>() template function.
+ * The implementation should register given plugin type, that is - add it to a list of registered types.
+ */
+ virtual void registerPluginType(PluginType* type) = 0;
+
+ signals:
+ /**
+ * @brief Emitted just before plugin is unloaded.
+ * @param plugin Plugin object to be unloaded.
+ * @param type Type of the plugin.
+ *
+ * It's emitted just before call to Plugin::deinit(), destroying plugin object
+ * and unloading the plugin file.
+ *
+ * Any code using certain plugin should listen for this signal and stop using
+ * the plugin immediately when received this signal. Otherwise application may crash.
+ */
+ void aboutToUnload(Plugin* plugin, PluginType* type);
+
+ /**
+ * @brief Emitted just after plugin was loaded.
+ * @param plugin Plugin object from loaded plugin.
+ * @param type Plugin type.
+ *
+ * It's emitted after plugin was loaded and successfully initialized (which includes
+ * successful Plugin::init() call).
+ */
+ void loaded(Plugin* plugin, PluginType* type);
+
+ /**
+ * @brief Emitted after plugin was unloaded.
+ * @param pluginName Name of the plugin that was unloaded.
+ * @param type Type of the plugin.
+ *
+ * Emitted after plugin was deinitialized and unloaded. At this stage a plugin object
+ * is no longer available, only it's name and other metadata (like description, version, etc).
+ */
+ void unloaded(const QString& pluginName, PluginType* type);
+
+ /**
+ * @brief Emitted after initial plugin set was loaded.
+ *
+ * The initial load is performed at application startup. Any code that relies on
+ * some plugins being loaded (like for example code that loads list of databases relies on
+ * database support plugins) should listen to this signal.
+ */
+ void pluginsInitiallyLoaded();
+
+ /**
+ * @brief Emitted when the plugin manager is deinitializing and will unload all plugins in a moment.
+ *
+ * It's emitted when user closes application, so the plugin manager deinitializes and unloads all plugins.
+ * This signal is emitted just before plugins get unloaded.
+ * If some signal handler is not interested in mass plugin unloading, then it can handle this signal
+ * and disconnect from unloaded() signal.
+ */
+ void aboutToQuit();
+
+ /**
+ * @brief Emitted when plugin load was requested, but it failed.
+ * @param pluginName Name of the plugin that failed to load.
+ *
+ * It's used for example by ConfigDialog to uncheck plugin that was requested to load (checked) and it failed.
+ */
+ void failedToLoad(const QString& pluginName);
+};
+
+/**
+ * @def PLUGINS
+ * @brief PluginsManager instance access macro.
+ *
+ * Since SQLiteStudio creates only one instance of PluginsManager,
+ * there is a standard method for accessing it, using code:
+ * @code
+ * QList<PluginType*> types = SQLiteStudio::getInstance()->getPluginManager()->getPluginTypes();
+ * @endcode
+ * or there's a slightly simpler way:
+ * @code
+ * QList<PluginType*> types = SQLITESTUDIO->getPluginManager()->getPluginTypes();
+ * @endcode
+ * or there is a very simplified method, using this macro:
+ * @code
+ * QList<PluginType*> types = PLUGINS->getPluginTypes();
+ * @endcode
+ */
+#define PLUGINS SQLITESTUDIO->getPluginManager()
+
+#endif // PLUGINMANAGER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/populatemanager.cpp b/SQLiteStudio3/coreSQLiteStudio/services/populatemanager.cpp
new file mode 100644
index 0000000..237667d
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/populatemanager.cpp
@@ -0,0 +1,93 @@
+#include "populatemanager.h"
+#include "services/pluginmanager.h"
+#include "plugins/populateplugin.h"
+#include "services/notifymanager.h"
+#include "populateworker.h"
+#include "plugins/populatesequence.h"
+#include "plugins/populaterandom.h"
+#include "plugins/populaterandomtext.h"
+#include "plugins/populateconstant.h"
+#include "plugins/populatedictionary.h"
+#include "plugins/populatescript.h"
+#include <QDebug>
+#include <QThreadPool>
+
+PopulateManager::PopulateManager(QObject *parent) :
+ PluginServiceBase(parent)
+{
+ PLUGINS->loadBuiltInPlugin(new PopulateSequence());
+ PLUGINS->loadBuiltInPlugin(new PopulateRandom());
+ PLUGINS->loadBuiltInPlugin(new PopulateRandomText());
+ PLUGINS->loadBuiltInPlugin(new PopulateConstant());
+ PLUGINS->loadBuiltInPlugin(new PopulateDictionary());
+ PLUGINS->loadBuiltInPlugin(new PopulateScript());
+}
+
+void PopulateManager::populate(Db* db, const QString& table, const QHash<QString, PopulateEngine*>& engines, qint64 rows)
+{
+ if (workInProgress)
+ {
+ error();
+ qCritical() << "Tried to call second populating process at the same time.";
+ return;
+ }
+
+ if (!db->isOpen())
+ {
+ error();
+ qCritical() << "Tried to populate table in closed database.";
+ return;
+ }
+
+ workInProgress = true;
+
+ columns.clear();
+ engineList.clear();
+ for (const QString& column : engines.keys())
+ {
+ columns << column;
+ engineList << engines[column];
+ }
+
+
+ this->db = db;
+ this->table = table;
+
+ PopulateWorker* worker = new PopulateWorker(db, table, columns, engineList, rows);
+ connect(worker, SIGNAL(finished(bool)), this, SLOT(finalizePopulating(bool)));
+ connect(this, SIGNAL(orderWorkerToInterrupt()), worker, SLOT(interrupt()));
+
+ QThreadPool::globalInstance()->start(worker);
+
+}
+
+void PopulateManager::error()
+{
+ emit populatingFinished();
+ emit populatingFailed();
+}
+
+void PopulateManager::deleteEngines(const QList<PopulateEngine*>& engines)
+{
+ for (PopulateEngine* engine : engines)
+ delete engine;
+}
+
+void PopulateManager::interrupt()
+{
+ emit orderWorkerToInterrupt();
+}
+
+void PopulateManager::finalizePopulating(bool result)
+{
+ workInProgress = false;
+
+ emit populatingFinished();
+ if (result)
+ {
+ notifyInfo(tr("Table '%1' populated successfully.").arg(table));
+ emit populatingSuccessful();
+ }
+ else
+ emit populatingFailed();
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/populatemanager.h b/SQLiteStudio3/coreSQLiteStudio/services/populatemanager.h
new file mode 100644
index 0000000..05b1f82
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/populatemanager.h
@@ -0,0 +1,48 @@
+#ifndef POPULATEMANAGER_H
+#define POPULATEMANAGER_H
+
+#include "pluginservicebase.h"
+#include "sqlitestudio.h"
+#include <QObject>
+#include <QHash>
+#include <QStringList>
+
+class PopulatePlugin;
+class PopulateEngine;
+class Db;
+
+class API_EXPORT PopulateManager : public PluginServiceBase
+{
+ Q_OBJECT
+
+ public:
+ explicit PopulateManager(QObject *parent = 0);
+
+ void populate(Db* db, const QString& table, const QHash<QString, PopulateEngine*>& engines, qint64 rows);
+
+ private:
+ void error();
+ void deleteEngines(const QList<PopulateEngine*>& engines);
+
+ bool workInProgress = false;
+ Db* db = nullptr;
+ QString table;
+ QStringList columns;
+ QList<PopulateEngine*> engineList;
+
+ public slots:
+ void interrupt();
+
+ private slots:
+ void finalizePopulating(bool result);
+
+ signals:
+ void populatingFinished();
+ void populatingSuccessful();
+ void populatingFailed();
+ void orderWorkerToInterrupt();
+};
+
+#define POPULATE_MANAGER SQLITESTUDIO->getPopulateManager()
+
+#endif // POPULATEMANAGER_H
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp b/SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp
new file mode 100644
index 0000000..dae8238
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp
@@ -0,0 +1,1058 @@
+#include "updatemanager.h"
+#include "services/pluginmanager.h"
+#include "services/notifymanager.h"
+#include "common/unused.h"
+#include <QTemporaryDir>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QUrl>
+#include <QUrlQuery>
+#include <QDebug>
+#include <QCoreApplication>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QProcess>
+#include <QThread>
+#include <QtConcurrent/QtConcurrent>
+
+#ifdef Q_OS_WIN32
+#include "JlCompress.h"
+#include <windows.h>
+#include <shellapi.h>
+#endif
+
+// Note on creating update packages:
+// Packages for Linux and MacOSX should be an archive of _contents_ of SQLiteStudio directory,
+// while for Windows it should be an archive of SQLiteStudio directory itself.
+
+QString UpdateManager::staticErrorMessage;
+UpdateManager::RetryFunction UpdateManager::retryFunction = nullptr;
+
+UpdateManager::UpdateManager(QObject *parent) :
+ QObject(parent)
+{
+ networkManager = new QNetworkAccessManager(this);
+ connect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(finished(QNetworkReply*)));
+ connect(this, SIGNAL(updatingError(QString)), NOTIFY_MANAGER, SLOT(error(QString)));
+}
+
+UpdateManager::~UpdateManager()
+{
+ cleanup();
+}
+
+void UpdateManager::checkForUpdates()
+{
+ getUpdatesMetadata(updatesCheckReply);
+}
+
+void UpdateManager::update()
+{
+ if (updatesGetUrlsReply || updatesInProgress)
+ return;
+
+ getUpdatesMetadata(updatesGetUrlsReply);
+}
+
+QString UpdateManager::getPlatformForUpdate() const
+{
+#if defined(Q_OS_LINUX)
+ if (QSysInfo::WordSize == 64)
+ return "linux64";
+ else
+ return "linux32";
+#elif defined(Q_OS_WIN)
+ return "win32";
+#elif defined(Q_OS_OSX)
+ return "macosx";
+#else
+ return QString();
+#endif
+}
+
+QString UpdateManager::getCurrentVersions() const
+{
+ QJsonArray versionsArray;
+
+ QJsonObject arrayEntry;
+ arrayEntry["component"] = "SQLiteStudio";
+ arrayEntry["version"] = SQLITESTUDIO->getVersionString();
+ versionsArray.append(arrayEntry);
+
+ for (const PluginManager::PluginDetails& details : PLUGINS->getAllPluginDetails())
+ {
+ if (details.builtIn)
+ continue;
+
+ arrayEntry["component"] = details.name;
+ arrayEntry["version"] = details.versionString;
+ versionsArray.append(arrayEntry);
+ }
+
+ QJsonObject topObj;
+ topObj["versions"] = versionsArray;
+
+ QJsonDocument doc(topObj);
+ return QString::fromLatin1(doc.toJson(QJsonDocument::Compact));
+}
+
+bool UpdateManager::isPlatformEligibleForUpdate() const
+{
+ return !getPlatformForUpdate().isNull() && getDistributionType() != DistributionType::OS_MANAGED;
+}
+
+#if defined(Q_OS_WIN32)
+bool UpdateManager::executePreFinalStepWin(const QString &tempDir, const QString &backupDir, const QString &appDir, bool reqAdmin)
+{
+ bool res;
+ if (reqAdmin)
+ res = executeFinalStepAsRootWin(tempDir, backupDir, appDir);
+ else
+ res = executeFinalStep(tempDir, backupDir, appDir);
+
+ if (res)
+ {
+ QFileInfo path(qApp->applicationFilePath());
+ QProcess::startDetached(appDir + "/" + path.fileName(), {WIN_POST_FINAL_UPDATE_OPTION_NAME, tempDir});
+ }
+ return res;
+}
+#endif
+
+void UpdateManager::handleAvailableUpdatesReply(QNetworkReply* reply)
+{
+ if (reply->error() != QNetworkReply::NoError)
+ {
+ updatingFailed(tr("An error occurred while checking for updates: %1.").arg(reply->errorString()));
+ reply->deleteLater();
+ return;
+ }
+
+ QJsonParseError err;
+ QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ QJsonDocument doc = QJsonDocument::fromJson(data, &err);
+ if (err.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Invalid response from update service:" << err.errorString() << "\n" << "The data was:" << QString::fromLatin1(data);
+ notifyWarn(tr("Could not check available updates, because server responded with invalid message format. It is safe to ignore this warning."));
+ return;
+ }
+
+ QList<UpdateEntry> updates = readMetadata(doc);
+ if (updates.size() > 0)
+ emit updatesAvailable(updates);
+ else
+ emit noUpdatesAvailable();
+}
+
+void UpdateManager::getUpdatesMetadata(QNetworkReply*& replyStoragePointer)
+{
+#ifndef NO_AUTO_UPDATES
+ if (!isPlatformEligibleForUpdate() || replyStoragePointer)
+ return;
+
+ QUrlQuery query;
+ query.addQueryItem("platform", getPlatformForUpdate());
+ query.addQueryItem("data", getCurrentVersions());
+
+ QUrl url(QString::fromLatin1(updateServiceUrl) + "?" + query.query(QUrl::FullyEncoded));
+ QNetworkRequest request(url);
+ replyStoragePointer = networkManager->get(request);
+#endif
+}
+
+void UpdateManager::handleUpdatesMetadata(QNetworkReply* reply)
+{
+ if (reply->error() != QNetworkReply::NoError)
+ {
+ updatingFailed(tr("An error occurred while reading updates metadata: %1.").arg(reply->errorString()));
+ reply->deleteLater();
+ return;
+ }
+
+ QJsonParseError err;
+ QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ QJsonDocument doc = QJsonDocument::fromJson(data, &err);
+ if (err.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Invalid response from update service for getting metadata:" << err.errorString() << "\n" << "The data was:" << QString::fromLatin1(data);
+ notifyWarn(tr("Could not download updates, because server responded with invalid message format. "
+ "You can try again later or download and install updates manually. See <a href=\"%1\">User Manual</a> for details.").arg(manualUpdatesHelpUrl));
+ return;
+ }
+
+ tempDir = new QTemporaryDir();
+ if (!tempDir->isValid()) {
+ notifyWarn(tr("Could not create temporary directory for downloading the update. Updating aborted."));
+ return;
+ }
+
+ updatesInProgress = true;
+ updatesToDownload = readMetadata(doc);
+ totalDownloadsCount = updatesToDownload.size();
+ totalPercent = 0;
+
+ if (totalDownloadsCount == 0)
+ {
+ updatingFailed(tr("There was no updates to download. Updating aborted."));
+ return;
+ }
+
+ downloadUpdates();
+}
+
+QList<UpdateManager::UpdateEntry> UpdateManager::readMetadata(const QJsonDocument& doc)
+{
+ QList<UpdateEntry> updates;
+ UpdateEntry entry;
+ QJsonObject obj = doc.object();
+ QJsonArray versionsArray = obj["newVersions"].toArray();
+ QJsonObject entryObj;
+ for (const QJsonValue& value : versionsArray)
+ {
+ entryObj = value.toObject();
+ entry.compontent = entryObj["component"].toString();
+ entry.version = entryObj["version"].toString();
+ entry.url = entryObj["url"].toString();
+ updates << entry;
+ }
+
+ return updates;
+}
+
+void UpdateManager::downloadUpdates()
+{
+ if (updatesToDownload.size() == 0)
+ {
+ QtConcurrent::run(this, &UpdateManager::installUpdates);
+ return;
+ }
+
+ UpdateEntry entry = updatesToDownload.takeFirst();
+ currentJobTitle = tr("Downloading: %1").arg(entry.compontent);
+ emit updatingProgress(currentJobTitle, 0, totalPercent);
+
+ QStringList parts = entry.url.split("/");
+ if (parts.size() < 1)
+ {
+ updatingFailed(tr("Could not determinate file name from update URL: %1. Updating aborted.").arg(entry.url));
+ return;
+ }
+
+ QString path = tempDir->path() + QLatin1Char('/') + parts.last();
+ currentDownloadFile = new QFile(path);
+ if (!currentDownloadFile->open(QIODevice::WriteOnly))
+ {
+ updatingFailed(tr("Failed to open file '%1' for writting: %2. Updating aborted.").arg(path, currentDownloadFile->errorString()));
+ return;
+ }
+
+ updatesToInstall[entry.compontent] = path;
+
+ QNetworkRequest request(QUrl(entry.url));
+ updatesGetReply = networkManager->get(request);
+ connect(updatesGetReply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(downloadProgress(qint64,qint64)));
+ connect(updatesGetReply, SIGNAL(readyRead()), this, SLOT(readDownload()));
+}
+
+void UpdateManager::updatingFailed(const QString& errMsg)
+{
+ cleanup();
+ updatesInProgress = false;
+ emit updatingError(errMsg);
+}
+
+void UpdateManager::installUpdates()
+{
+ currentJobTitle = tr("Installing updates.");
+ totalPercent = (totalDownloadsCount - updatesToDownload.size()) * 100 / (totalDownloadsCount + 1);
+ emit updatingProgress(currentJobTitle, 0, totalPercent);
+
+ requireAdmin = doRequireAdminPrivileges();
+
+ QTemporaryDir installTempDir;
+ QString appDirName = QDir(getAppDirPath()).dirName();
+ QString targetDir = installTempDir.path() + QLatin1Char('/') + appDirName;
+ if (!copyRecursively(getAppDirPath(), targetDir))
+ {
+ updatingFailed(tr("Could not copy current application directory into %1 directory.").arg(installTempDir.path()));
+ return;
+ }
+ emit updatingProgress(currentJobTitle, 40, totalPercent);
+
+ int i = 0;
+ int updatesCnt = updatesToInstall.size();
+ for (const QString& component : updatesToInstall.keys())
+ {
+ if (!installComponent(component, targetDir))
+ {
+ cleanup();
+ updatesInProgress = false;
+ return;
+ }
+ i++;
+ emit updatingProgress(currentJobTitle, (30 + (50 / updatesCnt * i)), totalPercent);
+ }
+
+ if (!executeFinalStep(targetDir))
+ {
+ cleanup();
+ updatesInProgress = false;
+ return;
+ }
+
+ currentJobTitle = QString();
+ totalPercent = 100;
+ emit updatingProgress(currentJobTitle, 100, totalPercent);
+ cleanup();
+ updatesInProgress = false;
+#ifdef Q_OS_WIN32
+ installTempDir.setAutoRemove(false);
+#endif
+
+ SQLITESTUDIO->setImmediateQuit(true);
+ qApp->exit(0);
+}
+
+bool UpdateManager::executeFinalStep(const QString& tempDir, const QString& backupDir, const QString& appDir)
+{
+ bool isWin = false;
+#ifdef Q_OS_WIN32
+ isWin = true;
+
+ // Windows needs to wait for previus process to exit
+ QThread::sleep(3);
+
+ QDir dir(backupDir);
+ QString dirName = dir.dirName();
+ dir.cdUp();
+ if (!dir.mkdir(dirName))
+ {
+ staticUpdatingFailed(tr("Could not create directory %1.").arg(backupDir));
+ return false;
+ }
+#endif
+ while (!moveDir(appDir, backupDir, isWin))
+ {
+ if (!retryFunction)
+ {
+ staticUpdatingFailed(tr("Could not rename directory %1 to %2.\nDetails: %3").arg(appDir, backupDir, staticErrorMessage));
+ return false;
+ }
+
+ if (!retryFunction(tr("Cannot not rename directory %1 to %2.\nDetails: %3").arg(appDir, backupDir, staticErrorMessage)))
+ return false;
+ }
+
+ if (!moveDir(tempDir, appDir, isWin))
+ {
+ if (!moveDir(backupDir, appDir, isWin))
+ {
+ staticUpdatingFailed(tr("Could not move directory %1 to %2 and also failed to restore original directory, "
+ "so the original SQLiteStudio directory is now located at: %3").arg(tempDir, appDir, backupDir));
+ }
+ else
+ {
+ staticUpdatingFailed(tr("Could not rename directory %1 to %2. Rolled back to the original SQLiteStudio version.").arg(tempDir, appDir));
+ }
+ deleteDir(backupDir);
+ return false;
+ }
+
+ deleteDir(backupDir);
+ return true;
+}
+
+bool UpdateManager::handleUpdateOptions(const QStringList& argList, int& returnCode)
+{
+ if (argList.size() == 5 && argList[1] == UPDATE_OPTION_NAME)
+ {
+ bool result = UpdateManager::executeFinalStep(argList[2], argList[3], argList[4]);
+ if (result)
+ returnCode = 0;
+ else
+ returnCode = 1;
+
+ return true;
+ }
+
+#ifdef Q_OS_WIN32
+ if (argList.size() == 6 && argList[1] == WIN_PRE_FINAL_UPDATE_OPTION_NAME)
+ {
+ bool result = UpdateManager::executePreFinalStepWin(argList[2], argList[3], argList[4], (bool)argList[5].toInt());
+ if (result)
+ returnCode = 0;
+ else
+ returnCode = -1;
+
+ return true;
+ }
+
+ if (argList.size() == 3 && argList[1] == WIN_POST_FINAL_UPDATE_OPTION_NAME)
+ {
+ QThread::sleep(1); // to make sure that the previous process has quit
+ returnCode = 0;
+ UpdateManager::executePostFinalStepWin(argList[2]);
+ return true;
+ }
+#endif
+
+ return false;
+}
+
+QString UpdateManager::getStaticErrorMessage()
+{
+ return staticErrorMessage;
+}
+
+bool UpdateManager::executeFinalStep(const QString& tempDir)
+{
+ QString appDir = getAppDirPath();
+
+ // Find inexisting dir name next to app dir
+ QDir backupDir(getBackupDir(appDir));
+
+#if defined(Q_OS_WIN32)
+ return runAnotherInstanceForUpdate(tempDir, backupDir.absolutePath(), qApp->applicationDirPath(), requireAdmin);
+#else
+ bool res;
+ if (requireAdmin)
+ res = executeFinalStepAsRoot(tempDir, backupDir.absolutePath(), appDir);
+ else
+ res = executeFinalStep(tempDir, backupDir.absolutePath(), appDir);
+
+ if (res)
+ QProcess::startDetached(qApp->applicationFilePath(), QStringList());
+
+ return res;
+#endif
+}
+
+bool UpdateManager::installComponent(const QString& component, const QString& tempDir)
+{
+ if (!unpackToDir(updatesToInstall[component], tempDir))
+ {
+ updatingFailed(tr("Could not unpack component %1 into %2 directory.").arg(component, tempDir));
+ return false;
+ }
+
+ // In future here we might also delete/change some files, according to some update script.
+ return true;
+}
+
+void UpdateManager::cleanup()
+{
+ safe_delete(currentDownloadFile);
+ safe_delete(tempDir);
+ updatesToDownload.clear();
+ updatesToInstall.clear();
+ requireAdmin = false;
+}
+
+bool UpdateManager::waitForProcess(QProcess& proc)
+{
+ if (!proc.waitForFinished(-1))
+ {
+ qDebug() << "Update QProcess timed out.";
+ return false;
+ }
+
+ if (proc.exitStatus() == QProcess::CrashExit)
+ {
+ qDebug() << "Update QProcess finished by crashing.";
+ return false;
+ }
+
+ if (proc.exitCode() != 0)
+ {
+ qDebug() << "Update QProcess finished with code:" << proc.exitCode();
+ return false;
+ }
+
+ return true;
+}
+
+QString UpdateManager::readError(QProcess& proc, bool reverseOrder)
+{
+ QString err = QString::fromLocal8Bit(reverseOrder ? proc.readAllStandardOutput() : proc.readAllStandardError());
+ if (err.isEmpty())
+ err = QString::fromLocal8Bit(reverseOrder ? proc.readAllStandardError() : proc.readAllStandardOutput());
+
+ QString errStr = proc.errorString();
+ if (!errStr.isEmpty())
+ err += "\n" + errStr;
+
+ return err;
+}
+
+void UpdateManager::staticUpdatingFailed(const QString& errMsg)
+{
+#if defined(Q_OS_WIN32)
+ staticErrorMessage = errMsg;
+#else
+ UPDATES->handleStaticFail(errMsg);
+#endif
+ qCritical() << errMsg;
+}
+
+bool UpdateManager::executeFinalStepAsRoot(const QString& tempDir, const QString& backupDir, const QString& appDir)
+{
+#if defined(Q_OS_LINUX)
+ return executeFinalStepAsRootLinux(tempDir, backupDir, appDir);
+#elif defined(Q_OS_WIN32)
+ return executeFinalStepAsRootWin(tempDir, backupDir, appDir);
+#elif defined(Q_OS_MACX)
+ return executeFinalStepAsRootMac(tempDir, backupDir, appDir);
+#else
+ qCritical() << "Unknown update platform in UpdateManager::executeFinalStepAsRoot() for package" << packagePath;
+ return false;
+#endif
+}
+
+#if defined(Q_OS_LINUX)
+bool UpdateManager::executeFinalStepAsRootLinux(const QString& tempDir, const QString& backupDir, const QString& appDir)
+{
+ QStringList args = {qApp->applicationFilePath(), UPDATE_OPTION_NAME, tempDir, backupDir, appDir};
+
+ QProcess proc;
+ LinuxPermElevator elevator = findPermElevatorForLinux();
+ switch (elevator)
+ {
+ case LinuxPermElevator::KDESU:
+ proc.setProgram("kdesu");
+ args.prepend("-t");
+ proc.setArguments(args);
+ break;
+ case LinuxPermElevator::GKSU:
+ proc.setProgram("gksu"); // TODO test gksu updates
+ proc.setArguments(args);
+ break;
+ case LinuxPermElevator::PKEXEC:
+ {
+ // We call CLI for doing final step, because pkexec runs cmd completly in root env, so there's no X server.
+ args[0] += "cli";
+
+ QStringList newArgs;
+ for (const QString& arg : args)
+ newArgs << wrapCmdLineArgument(arg);
+
+ QString cmd = "cd " + wrapCmdLineArgument(qApp->applicationDirPath()) +"; " + newArgs.join(" ");
+
+ proc.setProgram("pkexec");
+ proc.setArguments({"sh", "-c", cmd});
+ }
+ break;
+ case LinuxPermElevator::NONE:
+ updatingFailed(tr("Could not find permissions elevator application to run update as a root. Looked for: %1").arg("kdesu, gksu, pkexec"));
+ return false;
+ }
+
+ proc.start();
+ if (!waitForProcess(proc))
+ {
+ updatingFailed(tr("Could not execute final updating steps as root: %1").arg(readError(proc, (elevator == LinuxPermElevator::KDESU))));
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+#ifdef Q_OS_MACX
+bool UpdateManager::executeFinalStepAsRootMac(const QString& tempDir, const QString& backupDir, const QString& appDir)
+{
+ // Prepare script for updater
+ // osascript -e "do shell script \"stufftorunasroot\" with administrator privileges"
+ QStringList args = {wrapCmdLineArgument(qApp->applicationFilePath() + "cli"),
+ UPDATE_OPTION_NAME,
+ wrapCmdLineArgument(tempDir),
+ wrapCmdLineArgument(backupDir),
+ wrapCmdLineArgument(appDir)};
+ QProcess proc;
+
+ QString innerCmd = wrapCmdLineArgument(args.join(" "));
+
+ static_qstring(scriptTpl, "do shell script %1 with administrator privileges");
+ QString scriptCmd = scriptTpl.arg(innerCmd);
+
+ // Prepare updater temporary directory
+ QTemporaryDir updaterDir;
+ if (!updaterDir.isValid())
+ {
+ updatingFailed(tr("Could not execute final updating steps as admin: %1").arg(tr("Cannot create temporary directory for updater.")));
+ return false;
+ }
+
+ // Create updater script
+ QString scriptPath = updaterDir.path() + "/UpdateSQLiteStudio.scpt";
+ QFile updaterScript(scriptPath);
+ if (!updaterScript.open(QIODevice::WriteOnly))
+ {
+ updatingFailed(tr("Could not execute final updating steps as admin: %1").arg(tr("Cannot create updater script file.")));
+ return false;
+ }
+ updaterScript.write(scriptCmd.toLocal8Bit());
+ updaterScript.close();
+
+ // Compile script to updater application
+ QString updaterApp = updaterDir.path() + "/UpdateSQLiteStudio.app";
+ proc.setProgram("osacompile");
+ proc.setArguments({"-o", updaterApp, scriptPath});
+ proc.start();
+ if (!waitForProcess(proc))
+ {
+ updatingFailed(tr("Could not execute final updating steps as admin: %1").arg(readError(proc)));
+ return false;
+ }
+
+ // Execute updater
+ proc.setProgram(updaterApp + "/Contents/MacOS/applet");
+ proc.setArguments({});
+ proc.start();
+ if (!waitForProcess(proc))
+ {
+ updatingFailed(tr("Could not execute final updating steps as admin: %1").arg(readError(proc)));
+ return false;
+ }
+
+ // Validating update
+ // The updater script will not return error if the user canceled the password prompt.
+ // We need to check if the update was actually made and return true only then.
+ if (QDir(tempDir).exists())
+ {
+ // Temp dir still exists, so it was not moved by root process
+ updatingFailed(tr("Updating canceled."));
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+#ifdef Q_OS_WIN32
+bool UpdateManager::executeFinalStepAsRootWin(const QString& tempDir, const QString& backupDir, const QString& appDir)
+{
+ QString updateBin = qApp->applicationDirPath() + "/" + WIN_UPDATER_BINARY;
+
+ QString installFilePath = tempDir + "/" + WIN_INSTALL_FILE;
+ QFile installFile(installFilePath);
+ installFile.open(QIODevice::WriteOnly);
+ QString nl("\n");
+ installFile.write(UPDATE_OPTION_NAME);
+ installFile.write(nl.toLocal8Bit());
+ installFile.write(backupDir.toLocal8Bit());
+ installFile.write(nl.toLocal8Bit());
+ installFile.write(appDir.toLocal8Bit());
+ installFile.write(nl.toLocal8Bit());
+ installFile.close();
+
+ int res = (int)::ShellExecuteA(0, "runas", updateBin.toUtf8().constData(), 0, 0, SW_SHOWNORMAL);
+ if (res < 32)
+ {
+ staticUpdatingFailed(tr("Could not execute final updating steps as administrator."));
+ return false;
+ }
+
+ // Since I suck as a developer and I cannot implement a simple synchronous app call under Windows
+ // (QProcess does it somehow, but I'm too lazy to look it up and probably the solution wouldn't be compatible
+ // with our "privileges elevation" trick above... so after all I think we're stuck with this solution for now),
+ // I do the workaround here, which makes this process wait for the other process to create the "done"
+ // file when it's done, so this process knows when the other has ended. This way we can proceed with this
+ // process and we will delete some directories later on, which were required by that other process.
+ if (!waitForFileToDisappear(installFilePath, 10))
+ {
+ staticUpdatingFailed(tr("Could not execute final updating steps as administrator. Updater startup timed out."));
+ return false;
+ }
+
+ if (!waitForFileToAppear(appDir + QLatin1Char('/') + WIN_UPDATE_DONE_FILE, 30))
+ {
+ staticUpdatingFailed(tr("Could not execute final updating steps as administrator. Updater operation timed out."));
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+#if defined(Q_OS_WIN32)
+bool UpdateManager::executePostFinalStepWin(const QString &tempDir)
+{
+ QString doneFile = qApp->applicationDirPath() + QLatin1Char('/') + WIN_UPDATE_DONE_FILE;
+ QFile::remove(doneFile);
+
+ QDir dir(tempDir);
+ dir.cdUp();
+ if (!deleteDir(dir.absolutePath()))
+ staticUpdatingFailed(tr("Could not clean up temporary directory %1. You can delete it manually at any time.").arg(dir.absolutePath()));
+
+ QProcess::startDetached(qApp->applicationFilePath(), QStringList());
+ return true;
+}
+
+bool UpdateManager::waitForFileToDisappear(const QString &filePath, int seconds)
+{
+ QFile file(filePath);
+ while (file.exists() && seconds > 0)
+ {
+ QThread::sleep(1);
+ seconds--;
+ }
+
+ return !file.exists();
+}
+
+bool UpdateManager::waitForFileToAppear(const QString &filePath, int seconds)
+{
+ QFile file(filePath);
+ while (!file.exists() && seconds > 0)
+ {
+ QThread::sleep(1);
+ seconds--;
+ }
+
+ return file.exists();
+}
+
+bool UpdateManager::runAnotherInstanceForUpdate(const QString &tempDir, const QString &backupDir, const QString &appDir, bool reqAdmin)
+{
+ bool res = QProcess::startDetached(tempDir + "/SQLiteStudio.exe", {WIN_PRE_FINAL_UPDATE_OPTION_NAME, tempDir, backupDir, appDir,
+ QString::number((int)reqAdmin)});
+ if (!res)
+ {
+ updatingFailed(tr("Could not run new version for continuing update."));
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+UpdateManager::LinuxPermElevator UpdateManager::findPermElevatorForLinux()
+{
+#if defined(Q_OS_LINUX)
+ QProcess proc;
+ proc.setProgram("which");
+
+ if (!SQLITESTUDIO->getEnv("DISPLAY").isEmpty())
+ {
+ proc.setArguments({"kdesu"});
+ proc.start();
+ if (waitForProcess(proc))
+ return LinuxPermElevator::KDESU;
+
+ proc.setArguments({"gksu"});
+ proc.start();
+ if (waitForProcess(proc))
+ return LinuxPermElevator::GKSU;
+ }
+
+ proc.setArguments({"pkexec"});
+ proc.start();
+ if (waitForProcess(proc))
+ return LinuxPermElevator::PKEXEC;
+#endif
+
+ return LinuxPermElevator::NONE;
+}
+
+QString UpdateManager::wrapCmdLineArgument(const QString& arg)
+{
+ return "\"" + escapeCmdLineArgument(arg) + "\"";
+}
+
+QString UpdateManager::escapeCmdLineArgument(const QString& arg)
+{
+ if (!arg.contains("\\") && !arg.contains("\""))
+ return arg;
+
+ QString str = arg;
+ return str.replace("\\", "\\\\").replace("\"", "\\\"");
+}
+
+QString UpdateManager::getBackupDir(const QString &appDir)
+{
+ static_qstring(bakDirTpl, "%1.old%2");
+ QDir backupDir(bakDirTpl.arg(appDir, ""));
+ int cnt = 1;
+ while (backupDir.exists())
+ backupDir = QDir(bakDirTpl.arg(appDir, QString::number(cnt)));
+
+ return backupDir.absolutePath();
+}
+
+bool UpdateManager::unpackToDir(const QString& packagePath, const QString& outputDir)
+{
+#if defined(Q_OS_LINUX)
+ return unpackToDirLinux(packagePath, outputDir);
+#elif defined(Q_OS_WIN32)
+ return unpackToDirWin(packagePath, outputDir);
+#elif defined(Q_OS_MACX)
+ return unpackToDirMac(packagePath, outputDir);
+#else
+ qCritical() << "Unknown update platform in UpdateManager::unpackToDir() for package" << packagePath;
+ return false;
+#endif
+}
+
+#if defined(Q_OS_LINUX)
+bool UpdateManager::unpackToDirLinux(const QString &packagePath, const QString &outputDir)
+{
+ QProcess proc;
+ proc.setWorkingDirectory(outputDir);
+ proc.setStandardOutputFile(QProcess::nullDevice());
+ proc.setStandardErrorFile(QProcess::nullDevice());
+
+ if (!packagePath.endsWith("tar.gz"))
+ {
+ updatingFailed(tr("Package not in tar.gz format, cannot install: %1").arg(packagePath));
+ return false;
+ }
+
+ proc.start("mv", {packagePath, outputDir});
+ if (!waitForProcess(proc))
+ {
+ updatingFailed(tr("Package %1 cannot be installed, because cannot move it to directory: %2").arg(packagePath, outputDir));
+ return false;
+ }
+
+ QString fileName = packagePath.split("/").last();
+ QString newPath = outputDir + "/" + fileName;
+ proc.start("tar", {"-xzf", newPath});
+ if (!waitForProcess(proc))
+ {
+ updatingFailed(tr("Package %1 cannot be installed, because cannot unpack it: %2").arg(packagePath, readError(proc)));
+ return false;
+ }
+
+ QProcess::execute("rm", {"-f", newPath});
+ return true;
+}
+#endif
+
+#if defined(Q_OS_MACX)
+bool UpdateManager::unpackToDirMac(const QString &packagePath, const QString &outputDir)
+{
+ QProcess proc;
+ proc.setWorkingDirectory(outputDir);
+ proc.setStandardOutputFile(QProcess::nullDevice());
+ proc.setStandardErrorFile(QProcess::nullDevice());
+
+ if (!packagePath.endsWith("zip"))
+ {
+ updatingFailed(tr("Package not in zip format, cannot install: %1").arg(packagePath));
+ return false;
+ }
+
+ proc.start("unzip", {"-o", "-d", outputDir, packagePath});
+ if (!waitForProcess(proc))
+ {
+ updatingFailed(tr("Package %1 cannot be installed, because cannot unzip it to directory %2: %3")
+ .arg(packagePath, outputDir, readError(proc)));
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+#if defined(Q_OS_WIN32)
+bool UpdateManager::unpackToDirWin(const QString& packagePath, const QString& outputDir)
+{
+ if (JlCompress::extractDir(packagePath, outputDir + "/..").isEmpty())
+ {
+ updatingFailed(tr("Package %1 cannot be installed, because cannot unzip it to directory: %2").arg(packagePath, outputDir));
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+void UpdateManager::handleStaticFail(const QString& errMsg)
+{
+ emit updatingFailed(errMsg);
+}
+
+QString UpdateManager::getAppDirPath() const
+{
+ static QString appDir;
+ if (appDir.isNull())
+ {
+ appDir = qApp->applicationDirPath();
+#ifdef Q_OS_MACX
+ QDir tmpAppDir(appDir);
+ tmpAppDir.cdUp();
+ tmpAppDir.cdUp();
+ appDir = tmpAppDir.absolutePath();
+#endif
+ }
+ return appDir;
+}
+
+bool UpdateManager::moveDir(const QString& src, const QString& dst, bool contentsOnly)
+{
+ // If we're doing a rename in the very same parent directory then we don't want
+ // the 'move between partitions' to be involved, cause any failure to rename
+ // is due to permissions or file lock.
+ QFileInfo srcFi(src);
+ QFileInfo dstFi(dst);
+ bool sameParentDir = (srcFi.dir() == dstFi.dir());
+
+ QDir dir;
+ if (contentsOnly)
+ {
+ QString localSrc;
+ QString localDst;
+ QDir srcDir(src);
+ for (const QFileInfo& entry : srcDir.entryInfoList(QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot|QDir::Hidden|QDir::System))
+ {
+ localSrc = entry.absoluteFilePath();
+ localDst = dst + "/" + entry.fileName();
+ if (!dir.rename(localSrc, localDst) && (sameParentDir || !renameBetweenPartitions(localSrc, localDst)))
+ {
+ staticUpdatingFailed(tr("Could not rename directory %1 to %2.").arg(localSrc, localDst));
+ return false;
+ }
+ }
+ }
+ else
+ {
+ if (!dir.rename(src, dst) && (sameParentDir || !renameBetweenPartitions(src, dst)))
+ {
+ staticUpdatingFailed(tr("Could not rename directory %1 to %2.").arg(src, dst));
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool UpdateManager::deleteDir(const QString& path)
+{
+ QDir dir(path);
+ if (!dir.removeRecursively())
+ {
+ staticUpdatingFailed(tr("Could not delete directory %1.").arg(path));
+ return false;
+ }
+
+ return true;
+}
+
+bool UpdateManager::execCmd(const QString& cmd, const QStringList& args, QString* errorMsg)
+{
+ QProcess proc;
+ proc.start(cmd, args);
+ QString cmdString = QString("%1 \"%2\"").arg(cmd, args.join("\\\" \\\""));
+
+ if (!waitForProcess(proc))
+ {
+ if (errorMsg)
+ *errorMsg = tr("Error executing update command: %1\nError message: %2").arg(cmdString).arg(readError(proc));
+
+ return false;
+ }
+
+ return true;
+}
+
+void UpdateManager::setRetryFunction(const RetryFunction &value)
+{
+ retryFunction = value;
+}
+
+bool UpdateManager::doRequireAdminPrivileges()
+{
+ QString appDirPath = getAppDirPath();
+ QDir appDir(appDirPath);
+ bool isWritable = isWritableRecursively(appDir.absolutePath());
+
+ appDir.cdUp();
+ QFileInfo fi(appDir.absolutePath());
+ isWritable &= fi.isWritable();
+
+ if (isWritable)
+ {
+ QDir backupDir(getBackupDir(appDirPath));
+ QString backupDirName = backupDir.dirName();
+ backupDir.cdUp();
+ if (backupDir.mkdir(backupDirName))
+ backupDir.rmdir(backupDirName);
+ else
+ isWritable = false;
+ }
+
+ return !isWritable;
+}
+
+void UpdateManager::finished(QNetworkReply* reply)
+{
+ if (reply == updatesCheckReply)
+ {
+ updatesCheckReply = nullptr;
+ handleAvailableUpdatesReply(reply);
+ return;
+ }
+
+ if (reply == updatesGetUrlsReply)
+ {
+ updatesGetUrlsReply = nullptr;
+ handleUpdatesMetadata(reply);
+ return;
+ }
+
+ if (reply == updatesGetReply)
+ {
+ handleDownloadReply(reply);
+ if (reply == updatesGetReply) // if no new download is requested
+ updatesGetReply = nullptr;
+
+ return;
+ }
+}
+
+void UpdateManager::handleDownloadReply(QNetworkReply* reply)
+{
+ if (reply->error() != QNetworkReply::NoError)
+ {
+ updatingFailed(tr("An error occurred while downloading updates: %1. Updating aborted.").arg(reply->errorString()));
+ reply->deleteLater();
+ return;
+ }
+
+ totalPercent = (totalDownloadsCount - updatesToDownload.size()) * 100 / (totalDownloadsCount + 1);
+
+ readDownload();
+ currentDownloadFile->close();
+
+ safe_delete(currentDownloadFile);
+
+ reply->deleteLater();
+ downloadUpdates();
+}
+
+void UpdateManager::downloadProgress(qint64 bytesReceived, qint64 totalBytes)
+{
+ int perc;
+ if (totalBytes < 0)
+ perc = -1;
+ else if (totalBytes == 0)
+ perc = 100;
+ else
+ perc = bytesReceived * 100 / totalBytes;
+
+ emit updatingProgress(currentJobTitle, perc, totalPercent);
+}
+
+void UpdateManager::readDownload()
+{
+ currentDownloadFile->write(updatesGetReply->readAll());
+}
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/updatemanager.h b/SQLiteStudio3/coreSQLiteStudio/services/updatemanager.h
new file mode 100644
index 0000000..b8e6006
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/services/updatemanager.h
@@ -0,0 +1,137 @@
+#ifndef UPDATEMANAGER_H
+#define UPDATEMANAGER_H
+
+#include "common/global.h"
+#include "sqlitestudio.h"
+#include <QObject>
+#include <functional>
+#include <QProcess>
+
+class QNetworkAccessManager;
+class QNetworkReply;
+class QTemporaryDir;
+class QFile;
+
+class API_EXPORT UpdateManager : public QObject
+{
+ Q_OBJECT
+ public:
+ typedef std::function<bool(const QString& msg)> RetryFunction;
+
+ struct UpdateEntry
+ {
+ QString compontent;
+ QString version;
+ QString url;
+ };
+
+ explicit UpdateManager(QObject *parent = 0);
+ ~UpdateManager();
+
+ void checkForUpdates();
+ void update();
+ bool isPlatformEligibleForUpdate() const;
+ static bool executeFinalStep(const QString& tempDir, const QString& backupDir, const QString& appDir);
+ static bool handleUpdateOptions(const QStringList& argList, int& returnCode);
+ static QString getStaticErrorMessage();
+
+ static void setRetryFunction(const RetryFunction &value);
+
+ static_char* UPDATE_OPTION_NAME = "--update-final-step";
+ static_char* WIN_INSTALL_FILE = "install.dat";
+ static_char* WIN_UPDATE_DONE_FILE = "UpdateFinished.lck";
+
+ private:
+ enum class LinuxPermElevator
+ {
+ KDESU,
+ GKSU,
+ PKEXEC,
+ NONE
+ };
+
+ QString getPlatformForUpdate() const;
+ QString getCurrentVersions() const;
+ void handleAvailableUpdatesReply(QNetworkReply* reply);
+ void handleDownloadReply(QNetworkReply* reply);
+ void getUpdatesMetadata(QNetworkReply*& replyStoragePointer);
+ void handleUpdatesMetadata(QNetworkReply* reply);
+ QList<UpdateEntry> readMetadata(const QJsonDocument& doc);
+ void downloadUpdates();
+ void updatingFailed(const QString& errMsg);
+ void installUpdates();
+ bool installComponent(const QString& component, const QString& tempDir);
+ bool executeFinalStep(const QString& tempDir);
+ bool executeFinalStepAsRoot(const QString& tempDir, const QString& backupDir, const QString& appDir);
+#if defined(Q_OS_LINUX)
+ bool executeFinalStepAsRootLinux(const QString& tempDir, const QString& backupDir, const QString& appDir);
+ bool unpackToDirLinux(const QString& packagePath, const QString& outputDir);
+#elif defined(Q_OS_MACX)
+ bool unpackToDirMac(const QString& packagePath, const QString& outputDir);
+ bool executeFinalStepAsRootMac(const QString& tempDir, const QString& backupDir, const QString& appDir);
+#elif defined(Q_OS_WIN32)
+ bool runAnotherInstanceForUpdate(const QString& tempDir, const QString& backupDir, const QString& appDir, bool reqAdmin);
+ bool unpackToDirWin(const QString& packagePath, const QString& outputDir);
+#endif
+ bool doRequireAdminPrivileges();
+ bool unpackToDir(const QString& packagePath, const QString& outputDir);
+ void handleStaticFail(const QString& errMsg);
+ QString getAppDirPath() const;
+ void cleanup();
+
+ static bool moveDir(const QString& src, const QString& dst, bool contentsOnly = false);
+ static bool deleteDir(const QString& path);
+ static bool execCmd(const QString& cmd, const QStringList& args, QString* errorMsg = nullptr);
+ static bool waitForProcess(QProcess& proc);
+ static QString readError(QProcess& proc, bool reverseOrder = false);
+ static void staticUpdatingFailed(const QString& errMsg);
+ static LinuxPermElevator findPermElevatorForLinux();
+ static QString wrapCmdLineArgument(const QString& arg);
+ static QString escapeCmdLineArgument(const QString& arg);
+ static QString getBackupDir(const QString& appDir);
+#if defined(Q_OS_WIN32)
+ static bool executePreFinalStepWin(const QString& tempDir, const QString& backupDir, const QString& appDir, bool reqAdmin);
+ static bool executeFinalStepAsRootWin(const QString& tempDir, const QString& backupDir, const QString& appDir);
+ static bool executePostFinalStepWin(const QString& tempDir);
+ static bool waitForFileToDisappear(const QString& filePath, int seconds);
+ static bool waitForFileToAppear(const QString& filePath, int seconds);
+#endif
+
+ QNetworkAccessManager* networkManager = nullptr;
+ QNetworkReply* updatesCheckReply = nullptr;
+ QNetworkReply* updatesGetUrlsReply = nullptr;
+ QNetworkReply* updatesGetReply = nullptr;
+ bool updatesInProgress = false;
+ QList<UpdateEntry> updatesToDownload;
+ QHash<QString,QString> updatesToInstall;
+ QTemporaryDir* tempDir = nullptr;
+ QFile* currentDownloadFile = nullptr;
+ int totalPercent = 0;
+ int totalDownloadsCount = 0;
+ QString currentJobTitle;
+ bool requireAdmin = false;
+ static RetryFunction retryFunction;
+
+ static QString staticErrorMessage;
+ static_char* WIN_PRE_FINAL_UPDATE_OPTION_NAME = "--update-pre-final-step";
+ static_char* WIN_POST_FINAL_UPDATE_OPTION_NAME = "--update-post-final-step";
+ static_char* WIN_UPDATER_BINARY = "UpdateSQLiteStudio.exe";
+ static_char* updateServiceUrl = "http://sqlitestudio.pl/updates3.rvt";
+ static_char* manualUpdatesHelpUrl = "http://wiki.sqlitestudio.pl/index.php/User_Manual#Manual";
+
+ private slots:
+ void finished(QNetworkReply* reply);
+ void downloadProgress(qint64 bytesReceived, qint64 totalBytes);
+ void readDownload();
+
+ signals:
+ void updatesAvailable(const QList<UpdateManager::UpdateEntry>& updates);
+ void noUpdatesAvailable();
+ void updatingProgress(const QString& jobTitle, int jobPercent, int totalPercent);
+ void updatingFinished();
+ void updatingError(const QString& errorMessage);
+};
+
+#define UPDATES SQLITESTUDIO->getUpdateManager()
+
+#endif // UPDATEMANAGER_H