diff options
Diffstat (limited to 'SQLiteStudio3/coreSQLiteStudio/services')
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<int></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 |
