Refactor Settings DB and Handling (#1786)

* Refactor config API

* Corrections

* Test Qt 6.8

* Revert "Test Qt 6.8"

This reverts commit eceebec49e.

* Corrections 2

* Update Changelog

* Add configFilter element for getconfig call

* Do not create errors for DB updates when in read-only mode

* Have configuration migration and validation before Hyperion starts

* Correct Tests

* Corrections

* Add migration items

* Correct windows build

* Ensure that first instance as default one exists

* Remove dependency between AuthManager and SSDPHandler

* Correct typos

* Address CodeQL findings

* Replace CamkeSettings by Presets and provide debug scenarios
This commit is contained in:
LordGrey
2024-09-30 22:03:13 +02:00
committed by GitHub
parent aed4abc03b
commit ecceb4e7ae
88 changed files with 4407 additions and 2472 deletions

View File

@@ -239,18 +239,6 @@ protected:
QString saveEffect(const QJsonObject &data);
#endif
///
/// @brief Save settings object. Requires ADMIN ACCESS
/// @param data The data object
///
bool saveSettings(const QJsonObject &data);
///
/// @brief Restore settings object. Requires ADMIN ACCESS
/// @param data The data object
///
bool restoreSettings(const QJsonObject &data);
///
/// @brief Set the authorizationn state
/// @param authorized True, if authorized

View File

@@ -186,7 +186,7 @@ private:
///
void handleSourceSelectCommand(const QJsonObject &message, const JsonApiCommand& cmd);
/// Handle an incoming JSON GetConfig message and check subcommand
/// Handle an incoming JSON Config message and check subcommand
///
/// @param message the incoming message
///
@@ -204,6 +204,12 @@ private:
///
void handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd);
/// Handle an incoming JSON GetConfig message from handleConfigCommand()
///
/// @param message the incoming message
///
void handleConfigGetCommand(const QJsonObject &message, const JsonApiCommand& cmd);
/// Handle an incoming JSON RestoreConfig message from handleConfigCommand()
///
/// @param message the incoming message

View File

@@ -84,6 +84,7 @@ public:
DeleteToken,
Discover,
GetConfig,
GetConfigOld,
GetInfo,
GetPendingTokenRequests,
GetProperties,
@@ -134,6 +135,7 @@ public:
case DeleteToken: return "deleteToken";
case Discover: return "discover";
case GetConfig: return "getconfig";
case GetConfigOld: return "getconfig-old";
case GetInfo: return "getInfo";
case GetPendingTokenRequests: return "getPendingTokenRequests";
case GetProperties: return "getProperties";
@@ -274,6 +276,7 @@ public:
{ {"color", ""}, { Command::Color, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} },
{ {"componentstate", ""}, { Command::ComponentState, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} },
{ {"config", "getconfig"}, { Command::Config, SubCommand::GetConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} },
{ {"config", "getconfig-old"}, { Command::Config, SubCommand::GetConfigOld, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} },
{ {"config", "getschema"}, { Command::Config, SubCommand::GetSchema, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} },
{ {"config", "reload"}, { Command::Config, SubCommand::Reload, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} },
{ {"config", "restoreconfig"}, { Command::Config, SubCommand::RestoreConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} },

View File

@@ -31,6 +31,8 @@ public:
static QJsonObject getSystemInfo(const Hyperion* hyperion);
QJsonObject discoverSources (const QString& sourceType, const QJsonObject& params);
static QJsonObject getConfiguration(const QList<quint8>& instances = {}, const QStringList& instanceFilteredTypes = {}, const QStringList& globalFilterTypes = {} );
private:
template<typename GrabberType>

View File

@@ -1,12 +1,7 @@
#pragma once
#ifndef AUTHSTABLE_H
#define AUTHSTABLE_H
// hyperion
#include <db/DBManager.h>
#include <QCryptographicHash>
// qt
#include <QDateTime>
#include <QUuid>
namespace hyperion {
const char DEFAULT_USER[] = "Hyperion";
@@ -21,69 +16,30 @@ class AuthTable : public DBManager
public:
/// construct wrapper with auth table
AuthTable(const QString& rootPath = "", QObject* parent = nullptr, bool readonlyMode = false)
: DBManager(parent)
{
setReadonlyMode(readonlyMode);
if(!rootPath.isEmpty()){
// Init Hyperion database usage
setRootPath(rootPath);
setDatabaseName("hyperion");
}
// init Auth table
setTable("auth");
// create table columns
createTable(QStringList()<<"user TEXT"<<"password BLOB"<<"token BLOB"<<"salt BLOB"<<"comment TEXT"<<"id TEXT"<<"created_at TEXT"<<"last_use TEXT");
};
explicit AuthTable(QObject* parent = nullptr);
///
/// @brief Create a user record, if called on a existing user the auth is recreated
/// @param[in] user The username
/// @param[in] pw The password
/// @param[in] password The password
/// @return true on success else false
///
inline bool createUser(const QString& user, const QString& pw)
{
// new salt
QByteArray salt = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex();
QVariantMap map;
map["user"] = user;
map["salt"] = salt;
map["password"] = hashPasswordWithSalt(pw,salt);
map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
VectorPair cond;
cond.append(CPair("user",user));
return createRecord(cond, map);
}
bool createUser(const QString& user, const QString& password);
///
/// @brief Test if user record exists
/// @param[in] user The user id
/// @return true on success else false
///
inline bool userExist(const QString& user)
{
VectorPair cond;
cond.append(CPair("user",user));
return recordExists(cond);
}
bool userExist(const QString& user);
///
/// @brief Test if a user is authorized for access with given pw.
/// @param user The user name
/// @param pw The password
/// @return True on success else false
/// @param user The user name
/// @param password The password
/// @return True on success else false
///
inline bool isUserAuthorized(const QString& user, const QString& pw)
{
if(userExist(user) && (calcPasswordHashOfUser(user, pw) == getPasswordHashOfUser(user)))
{
updateUserUsed(user);
return true;
}
return false;
}
bool isUserAuthorized(const QString& user, const QString& password);
///
/// @brief Test if a user token is authorized for access.
@@ -91,197 +47,92 @@ public:
/// @param token The token
/// @return True on success else false
///
inline bool isUserTokenAuthorized(const QString& usr, const QString& token)
{
if(getUserToken(usr) == token.toUtf8())
{
updateUserUsed(usr);
return true;
}
return false;
}
bool isUserTokenAuthorized(const QString& usr, const QString& token);
///
/// @brief Update token of a user. It's an alternate login path which is replaced on startup. This token is NOT hashed(!)
/// @param user The user name
/// @return True on success else false
///
inline bool setUserToken(const QString& user)
{
QVariantMap map;
map["token"] = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex();
VectorPair cond;
cond.append(CPair("user", user));
return updateRecord(cond, map);
}
bool setUserToken(const QString& user);
///
/// @brief Get token of a user. This token is NOT hashed(!)
/// @param user The user name
/// @return The token
///
inline const QByteArray getUserToken(const QString& user)
{
QVariantMap results;
VectorPair cond;
cond.append(CPair("user", user));
getRecord(cond, results, QStringList()<<"token");
return results["token"].toByteArray();
}
const QByteArray getUserToken(const QString& user);
///
/// @brief update password of given user. The user should be tested (isUserAuthorized) to verify this change
/// @param user The user name
/// @param newPw The new password to set
/// @return True on success else false
/// @param user The user name
/// @param newassword The new password to set
/// @return True on success else false
///
inline bool updateUserPassword(const QString& user, const QString& newPw)
{
QVariantMap map;
map["password"] = calcPasswordHashOfUser(user, newPw);
VectorPair cond;
cond.append(CPair("user", user));
return updateRecord(cond, map);
}
bool updateUserPassword(const QString& user, const QString& newPassword);
///
/// @brief Reset password of Hyperion user !DANGER! Used in Hyperion main.cpp
/// @return True on success else false
///
inline bool resetHyperionUser()
{
QVariantMap map;
map["password"] = calcPasswordHashOfUser(hyperion::DEFAULT_USER, hyperion::DEFAULT_PASSWORD);
VectorPair cond;
cond.append(CPair("user", hyperion::DEFAULT_USER));
return updateRecord(cond, map);
}
bool resetHyperionUser();
///
/// @brief Update 'last_use' column entry for the corresponding user
/// @param[in] user The user to search for
///
inline void updateUserUsed(const QString& user)
{
QVariantMap map;
map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
VectorPair cond;
cond.append(CPair("user", user));
updateRecord(cond, map);
}
void updateUserUsed(const QString& user);
///
/// @brief Test if token record exists, updates last_use on success
/// @param[in] token The token id
/// @return true on success else false
///
inline bool tokenExist(const QString& token)
{
QVariantMap map;
map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
VectorPair cond;
cond.append(CPair("token", hashToken(token)));
if(recordExists(cond))
{
// update it
createRecord(cond,map);
return true;
}
return false;
}
bool tokenExist(const QString& token);
///
/// @brief Create a new token record with comment
/// @param[in] token The token id as plaintext
/// @param[in] comment The comment for the token (eg a human readable identifier)
/// @param[in] id The id for the token
/// @param[in] identifier The identifier for the token
/// @return true on success else false
///
inline bool createToken(const QString& token, const QString& comment, const QString& id)
{
QVariantMap map;
map["comment"] = comment;
map["id"] = idExist(id) ? QUuid::createUuid().toString().remove("{").remove("}").left(5) : id;
map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
VectorPair cond;
cond.append(CPair("token", hashToken(token)));
return createRecord(cond, map);
}
bool createToken(const QString& token, const QString& comment, const QString& identifier);
///
/// @brief Delete token record by id
/// @param[in] id The token id
/// @brief Delete token record by identifier
/// @param[in] identifier The token identifier
/// @return true on success else false
///
inline bool deleteToken(const QString& id)
{
VectorPair cond;
cond.append(CPair("id", id));
return deleteRecord(cond);
}
bool deleteToken(const QString& identifier);
///
/// @brief Rename token record by id
/// @param[in] id The token id
/// @brief Rename token record by identifier
/// @param[in] identifier The token identifier
/// @param[in] comment The new comment
/// @return true on success else false
///
inline bool renameToken(const QString &id, const QString &comment)
{
QVariantMap map;
map["comment"] = comment;
VectorPair cond;
cond.append(CPair("id", id));
return updateRecord(cond, map);
}
bool renameToken(const QString &identifier, const QString &comment);
///
/// @brief Get all 'comment', 'last_use' and 'id' column entries
/// @return A vector of all lists
///
inline const QVector<QVariantMap> getTokenList()
{
QVector<QVariantMap> results;
getRecords(results, QStringList() << "comment" << "id" << "last_use");
return results;
}
const QVector<QVariantMap> getTokenList();
///
/// @brief Test if id exists
/// @param[in] id The id
/// @brief Test if identifier exists
/// @param[in] identifier The identifier
/// @return true on success else false
///
inline bool idExist(const QString& id)
{
VectorPair cond;
cond.append(CPair("id", id));
return recordExists(cond);
}
bool identifierExist(const QString& identifier);
///
/// @brief Get the passwort hash of a user from db
/// @param user The user name
/// @return password as hash
///
inline const QByteArray getPasswordHashOfUser(const QString& user)
{
QVariantMap results;
VectorPair cond;
cond.append(CPair("user", user));
getRecord(cond, results, QStringList()<<"password");
return results["password"].toByteArray();
}
const QByteArray getPasswordHashOfUser(const QString& user);
///
/// @brief Calc the password hash of a user based on user name and password
@@ -289,36 +140,22 @@ public:
/// @param pw The password
/// @return The calced password hash
///
inline const QByteArray calcPasswordHashOfUser(const QString& user, const QString& pw)
{
// get salt
QVariantMap results;
VectorPair cond;
cond.append(CPair("user", user));
getRecord(cond, results, QStringList()<<"salt");
// calc
return hashPasswordWithSalt(pw,results["salt"].toByteArray());
}
const QByteArray calcPasswordHashOfUser(const QString& user, const QString& password);
///
/// @brief Create a password hash of plaintex password + salt
/// @param pw The plaintext password
/// @param password The plaintext password
/// @param salt The salt
/// @return The password hash with salt
///
inline const QByteArray hashPasswordWithSalt(const QString& pw, const QByteArray& salt)
{
return QCryptographicHash::hash(pw.toUtf8().append(salt), QCryptographicHash::Sha512).toHex();
}
const QByteArray hashPasswordWithSalt(const QString& password, const QByteArray& salt);
///
/// @brief Create a token hash
/// @param token The plaintext token
/// @return The token hash
///
inline const QByteArray hashToken(const QString& token)
{
return QCryptographicHash::hash(token.toUtf8(), QCryptographicHash::Sha512).toHex();
}
const QByteArray hashToken(const QString& token);
};
#endif // AUTHSTABLE_H

View File

@@ -0,0 +1,42 @@
#ifndef DBCONFGMANAGER_H
#define DBConfigManager_H
#include <db/DBManager.h>
#include "db/SettingsTable.h"
#include "db/InstanceTable.h"
class DBConfigManager : public DBManager
{
public:
DBConfigManager(QObject* parent = nullptr);
QPair<bool, QStringList> importJson(const QString& configFile);
bool exportJson(const QString& path = "") const;
QJsonObject getConfiguration(const QList<quint8>& instances = {}, const QStringList& instanceFilteredTypes = {}, const QStringList& globalFilterTypes = {} ) const;
QPair<bool, QStringList> validateConfiguration();
QPair<bool, QStringList> validateConfiguration(QJsonObject& config, bool doCorrections = false);
QPair<bool, QStringList> addMissingDefaults();
QPair<bool, QStringList> updateConfiguration();
QPair<bool, QStringList> updateConfiguration(QJsonObject& config, bool doCorrections = false);
QPair<bool, QStringList> migrateConfiguration();
private:
// Function to import global settings from the configuration
bool importGlobalSettings(const QJsonObject& config, QStringList& errorList);
// Function to import all instances from the configuration
bool importInstances(const QJsonObject& config, QStringList& errorList);
// Function to import a single instance
bool importInstance(InstanceTable& instanceTable, const QJsonObject& instanceConfig, quint8 instanceIdx, QStringList& errorList);
// Function to import settings for a specific instance
bool importInstanceSettings(SettingsTable& settingsTable, const QJsonObject& instanceSettings, QStringList& errorList);
};
#endif // DBCONFGMANAGER_H

View File

@@ -1,10 +1,17 @@
#pragma once
#ifdef Unsorted
#undef Unsorted
#endif
#include <utils/Logger.h>
#include <QMap>
#include <QVariant>
#include <QPair>
#include <QVector>
#include <QFileInfo>
#include <QDir>
#include <QThreadStorage>
class QSqlDatabase;
class QSqlQuery;
@@ -26,13 +33,22 @@ class DBManager : public QObject
Q_OBJECT
public:
DBManager(QObject* parent = nullptr);
~DBManager() override;
explicit DBManager(QObject* parent = nullptr);
static void initializeDatabase(const QDir& dataDirectory, bool isReadOnly);
static QDir getDataDirectory() { return _dataDirectory;}
static QDir getDirectory() { return _databaseDirectory;}
static QFileInfo getFileInfo() { return _databaseFile;}
static bool isReadOnly() { return _isReadOnly; }
///
/// @brief Sets the database in read-only mode.
/// Updates will not written to the tables
/// @param[in] readOnly True read-only, false - read/write
///
static void setReadonly(bool isReadOnly) { _isReadOnly = isReadOnly; }
/// set root path
void setRootPath(const QString& rootPath);
/// define the database to work with
void setDatabaseName(const QString& dbn) { _dbn = dbn; };
/// set a table to work with
void setTable(const QString& table);
@@ -61,6 +77,8 @@ public:
///
bool recordExists(const VectorPair& conditions) const;
bool recordsNotExisting(const QVariantList& testValues,const QString& column, QStringList& nonExistingRecs, const QString& condition ) const;
///
/// @brief Create a new record in table when the conditions find no existing entry. Add additional key:value pairs in columns
/// DO NOT repeat column keys between 'conditions' and 'columns' as they will be merged on creation
@@ -98,6 +116,18 @@ public:
///
bool getRecords(QVector<QVariantMap>& results, const QStringList& tColumns = QStringList(), const QStringList& tOrder = QStringList()) const;
///
/// @brief Get data of multiple records, you need to specify the columns. This search is without conditions. Good to grab all data from db
/// @param[in] conditions condition to search for (WHERE)
/// @param[out] results results of query
/// @param[in] tColumns target columns to search in (optional) if not provided returns all columns
/// @param[in] tOrder target order columns with order by ASC/DESC (optional)
/// @return True on success else false
///
bool getRecords(const VectorPair& conditions, QVector<QVariantMap>& results, const QStringList& tColumns = {}, const QStringList& tOrder = {}) const;
bool getRecords(const QString& condition, const QVariantList& bindValues, QVector<QVariantMap>& results, const QStringList& tColumns = {}, const QStringList& tOrder = {}) const;
///
/// @brief Delete a record determined by conditions
/// @param[in] conditions conditions of the row to delete it (WHERE)
@@ -119,23 +149,36 @@ public:
///
bool deleteTable(const QString& table) const;
///
/// @brief Sets a table in read-only mode.
/// Updates will not written to the table
/// @param[in] readOnly True read-only, false - read/write
///
void setReadonlyMode(bool readOnly) { _readonlyMode = readOnly; };
bool executeQuery(QSqlQuery& query) const;
bool startTransaction(QSqlDatabase& idb) const;
bool startTransaction(QSqlDatabase& idb, QStringList& errorList);
bool commiTransaction(QSqlDatabase& idb) const;
bool commiTransaction(QSqlDatabase& idb, QStringList& errorList);
bool rollbackTransaction(QSqlDatabase& idb) const;
bool rollbackTransaction(QSqlDatabase& idb, QStringList& errorList);
// Utility function to log errors and append to error list
void logErrorAndAppend(const QString& errorText, QStringList& errorList);
protected:
Logger* _log;
private:
static QDir _dataDirectory;
static QDir _databaseDirectory;
static QFileInfo _databaseFile;
static QThreadStorage<QSqlDatabase> _databasePool;
static bool _isReadOnly;
Logger* _log;
/// databse connection & file name, defaults to hyperion
QString _dbn = "hyperion";
/// table in database
QString _table;
/// databse connection & file name, defaults to hyperion
QString _dbn = "hyperion";
bool _readonlyMode;
/// table in database
QString _table;
/// addBindValue to query given by QVariantList
void doAddBindValue(QSqlQuery& query, const QVariantList& variants) const;
/// addBindValues to query given by QVariantList
void addBindValues(QSqlQuery& query, const QVariantList& variants) const;
QString constructExecutedQuery(const QSqlQuery& query) const;
};

View File

@@ -0,0 +1,33 @@
#ifndef DBMIGRATIONMANAGER_H
#define DBMIGRATIONMANAGER_H
#include <db/DBManager.h>
#include <utils/version.hpp>
#include <QObject>
class DBMigrationManager : public DBManager
{
Q_OBJECT
public:
explicit DBMigrationManager(QObject *parent = nullptr);
bool isMigrationRequired();
bool migrateSettings(QJsonObject& config);
private:
bool upgradeGlobalSettings(const semver::version& currentVersion, QJsonObject& config);
bool upgradeGlobalSettings_alpha_9(semver::version& currentVersion, QJsonObject& config);
bool upgradeGlobalSettings_2_0_12(semver::version& currentVersion, QJsonObject& config);
bool upgradeGlobalSettings_2_0_16(semver::version& currentVersion, QJsonObject& config);
bool upgradeGlobalSettings_2_1_0(semver::version& currentVersion, QJsonObject& config);
bool upgradeInstanceSettings(const semver::version& currentVersion, quint8 instance, QJsonObject& config);
bool upgradeInstanceSettings_alpha_9(semver::version& currentVersion, quint8 instance, QJsonObject& config);
bool upgradeInstanceSettings_2_0_12(semver::version& currentVersion, quint8 instance, QJsonObject& config);
bool upgradeInstanceSettings_2_0_13(semver::version& currentVersion, quint8 instance, QJsonObject& config);
bool upgradeInstanceSettings_2_0_16(semver::version& currentVersion, quint8 instance, QJsonObject& config);
};
#endif // DBMIGRATIONMANAGER_H

View File

@@ -1,11 +1,7 @@
#pragma once
#ifndef INSTANCETABLE_H
#define INSTANCETABLE_H
// db
#include <db/DBManager.h>
#include <db/SettingsTable.h>
// qt
#include <QDateTime>
///
/// @brief Hyperion instance manager specific database interface. prepares also the Hyperion database for all follow up usage (Init QtSqlConnection) along with db name
@@ -14,22 +10,7 @@ class InstanceTable : public DBManager
{
public:
InstanceTable(const QString& rootPath, QObject* parent = nullptr, bool readonlyMode = false)
: DBManager(parent)
{
setReadonlyMode(readonlyMode);
// Init Hyperion database usage
setRootPath(rootPath);
setDatabaseName("hyperion");
// Init instance table
setTable("instances");
createTable(QStringList()<<"instance INTEGER"<<"friendly_name TEXT"<<"enabled INTEGER DEFAULT 0"<<"last_use TEXT");
// start/create the first Hyperion instance index 0
createInstance();
};
explicit InstanceTable(QObject* parent = nullptr);
///
/// @brief Create a new Hyperion instance entry, the name needs to be unique
@@ -37,53 +18,19 @@ public:
/// @param[out] inst The id that has been assigned
/// @return True on success else false
///
inline bool createInstance(const QString& name, quint8& inst)
{
VectorPair fcond;
fcond.append(CPair("friendly_name",name));
bool createInstance(const QString& name, quint8& inst);
// check duplicate
if(!recordExists(fcond))
{
inst = 0;
VectorPair cond;
cond.append(CPair("instance",inst));
// increment to next avail index
while(recordExists(cond))
{
inst++;
cond.removeFirst();
cond.append(CPair("instance",inst));
}
// create
QVariantMap data;
data["friendly_name"] = name;
data["instance"] = inst;
VectorPair lcond;
return createRecord(lcond, data);
}
return false;
}
///
/// @brief Create first Hyperion instance entry, if index 0 is not found.
///
void createDefaultInstance();
///
/// @brief Delete a Hyperion instance
/// @param inst The id that has been assigned
/// @return True on success else false
///
inline bool deleteInstance(quint8 inst)
{
VectorPair cond;
cond.append(CPair("instance",inst));
if(deleteRecord(cond))
{
// delete settings entries
SettingsTable settingsTable(inst);
settingsTable.deleteInstance();
return true;
}
return false;
}
bool deleteInstance(quint8 inst);
///
/// @brief Assign a new name for the given instance
@@ -91,141 +38,59 @@ public:
/// @param name The new name of the instance
/// @return True on success else false (instance not found)
///
inline bool saveName(quint8 inst, const QString& name)
{
VectorPair fcond;
fcond.append(CPair("friendly_name",name));
// check duplicate
if(!recordExists(fcond))
{
if(instanceExist(inst))
{
VectorPair cond;
cond.append(CPair("instance",inst));
QVariantMap data;
data["friendly_name"] = name;
return updateRecord(cond, data);
}
}
return false;
}
bool saveName(quint8 inst, const QString& name);
///
/// @brief Get all instances with all columns
/// @param justEnabled return just enabled instances if true
/// @param onlyEnabled return only enabled instances if true
/// @return The found instances
///
inline QVector<QVariantMap> getAllInstances(bool justEnabled = false)
{
QVector<QVariantMap> results;
getRecords(results, QStringList(), QStringList() << "instance ASC");
if(justEnabled)
{
for (auto it = results.begin(); it != results.end();)
{
if( ! (*it)["enabled"].toBool())
{
it = results.erase(it);
continue;
}
++it;
}
}
return results;
}
QVector<QVariantMap> getAllInstances(bool onlyEnabled = false);
///
/// @brief Get all instance IDs
/// @param onlyEnabled return only enabled instance IDs if true
/// @return The found instances
///
QList<quint8> getAllInstanceIDs (bool onlyEnabled = false);
///
/// @brief Test if instance record exists
/// @param[in] user The user id
/// @return true on success else false
///
inline bool instanceExist(quint8 inst)
{
VectorPair cond;
cond.append(CPair("instance",inst));
return recordExists(cond);
}
bool instanceExist(quint8 inst);
///
/// @brief Get instance name by instance index
/// @param index The index to search for
/// @return The name of this index, may return NOT FOUND if not found
///
inline const QString getNamebyIndex(quint8 index)
{
QVariantMap results;
VectorPair cond;
cond.append(CPair("instance", index));
getRecord(cond, results, QStringList("friendly_name"));
QString name = results["friendly_name"].toString();
return name.isEmpty() ? "NOT FOUND" : name;
}
QString getNamebyIndex(quint8 index);
///
/// @brief Update 'last_use' timestamp
/// @param inst The instance to update
/// @return True on success else false
///
inline void setLastUse(quint8 inst)
{
VectorPair cond;
cond.append(CPair("instance", inst));
QVariantMap map;
map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
updateRecord(cond, map);
}
bool setLastUse(quint8 inst);
///
/// @brief Update 'enabled' column by instance index
/// @param inst The instance to update
/// @param newState True when enabled else false
/// @return True on success else false
///
inline void setEnable(quint8 inst, bool newState)
{
VectorPair cond;
cond.append(CPair("instance", inst));
QVariantMap map;
map["enabled"] = newState;
updateRecord(cond, map);
}
bool setEnable(quint8 inst, bool newState);
///
/// @brief Get state of 'enabled' column by instance index
/// @param inst The instance to get
/// @return True when enabled else false
///
inline bool isEnabled(quint8 inst)
{
VectorPair cond;
cond.append(CPair("instance", inst));
QVariantMap results;
getRecord(cond, results);
return results["enabled"].toBool();
}
bool isEnabled(quint8 inst);
private:
///
/// @brief Create first Hyperion instance entry, if index 0 is not found.
///
inline void createInstance()
{
if(instanceExist(0))
setEnable(0, true);
else
{
QVariantMap data;
data["friendly_name"] = "First LED Hardware instance";
VectorPair cond;
cond.append(CPair("instance", 0));
if(createRecord(cond, data))
setEnable(0, true);
else
throw std::runtime_error("Failed to create Hyperion root instance in db! This should never be the case...");
}
}
};
#endif // INSTANCETABLE_H

View File

@@ -1,14 +1,9 @@
#pragma once
#ifndef METATABLE_H
#define METATABLE_H
// hyperion
#include <db/DBManager.h>
// qt
#include <QDateTime>
#include <QUuid>
#include <QNetworkInterface>
#include <QCryptographicHash>
///
/// @brief meta table specific database interface
///
@@ -17,47 +12,13 @@ class MetaTable : public DBManager
public:
/// construct wrapper with plugins table and columns
MetaTable(QObject* parent = nullptr, bool readonlyMode = false)
: DBManager(parent)
{
setReadonlyMode(readonlyMode);
setTable("meta");
createTable(QStringList()<<"uuid TEXT"<<"created_at TEXT");
};
explicit MetaTable(QObject* parent = nullptr);
///
/// @brief Get the uuid, if the uuid is not set it will be created
/// @return The uuid
///
inline QString getUUID() const
{
QVector<QVariantMap> results;
getRecords(results, QStringList() << "uuid");
for(const auto & entry : results)
{
if(!entry["uuid"].toString().isEmpty())
return entry["uuid"].toString();
}
// create new uuidv5 based on net adapter MAC, save to db and return
QString hash;
foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces())
{
if (!(interface.flags() & QNetworkInterface::IsLoopBack))
{
hash = QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit(),QCryptographicHash::Sha1).toHex();
break;
}
}
const QString newUuid = QUuid::createUuidV5(QUuid(), hash).toString().mid(1, 36);
VectorPair cond;
cond.append(CPair("uuid",newUuid));
QVariantMap map;
map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
createRecord(cond, map);
return newUuid;
}
QString getUUID() const;
};
#endif // METATABLE_H

View File

@@ -1,12 +1,20 @@
#pragma once
#ifndef SETTINGSTABLE_H
#define SETTINGSTABLE_H
#include <limits>
#ifdef WIN32
#undef max
#endif
// hyperion
#include <db/DBManager.h>
#include <utils/version.hpp>
// qt
#include <QDateTime>
#include <QJsonDocument>
const int GLOABL_INSTANCE_ID = std::numeric_limits<quint8>::max();;
const char DEFAULT_CONFIG_VERSION[] = "2.0.0-alpha.8";
///
/// @brief settings table db interface
///
@@ -15,14 +23,7 @@ class SettingsTable : public DBManager
public:
/// construct wrapper with settings table
SettingsTable(quint8 instance, QObject* parent = nullptr)
: DBManager(parent)
, _hyperion_inst(instance)
{
setTable("settings");
// create table columns
createTable(QStringList()<<"type TEXT"<<"config TEXT"<<"hyperion_inst INTEGER"<<"updated_at TEXT");
};
SettingsTable(quint8 instance = GLOABL_INSTANCE_ID, QObject* parent = nullptr);
///
/// @brief Create or update a settings record
@@ -30,19 +31,7 @@ public:
/// @param[in] config The configuration data
/// @return true on success else false
///
inline bool createSettingsRecord(const QString& type, const QString& config) const
{
QVariantMap map;
map["config"] = config;
map["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
VectorPair cond;
cond.append(CPair("type",type));
// when a setting is not global we are searching also for the instance
if(!isSettingGlobal(type))
cond.append(CPair("AND hyperion_inst",_hyperion_inst));
return createRecord(cond, map);
}
bool createSettingsRecord(const QString& type, const QString& config) const;
///
/// @brief Test if record exist, type can be global setting or local (instance)
@@ -50,76 +39,64 @@ public:
/// @param[in] hyperion_inst The instance of hyperion assigned (might be empty)
/// @return true on success else false
///
inline bool recordExist(const QString& type) const
{
VectorPair cond;
cond.append(CPair("type",type));
// when a setting is not global we are searching also for the instance
if(!isSettingGlobal(type))
cond.append(CPair("AND hyperion_inst",_hyperion_inst));
return recordExists(cond);
}
bool recordExist(const QString& type) const;
///
/// @brief Get 'config' column of settings entry as QJsonDocument
/// @param[in] type The settings type
/// @return The QJsonDocument
///
inline QJsonDocument getSettingsRecord(const QString& type) const
{
QVariantMap results;
VectorPair cond;
cond.append(CPair("type",type));
// when a setting is not global we are searching also for the instance
if(!isSettingGlobal(type))
cond.append(CPair("AND hyperion_inst",_hyperion_inst));
getRecord(cond, results, QStringList("config"));
return QJsonDocument::fromJson(results["config"].toByteArray());
}
QJsonDocument getSettingsRecord(const QString& type) const;
///
/// @brief Get 'config' column of settings entry as QString
/// @param[in] type The settings type
/// @return The QString
///
inline QString getSettingsRecordString(const QString& type) const
{
QVariantMap results;
VectorPair cond;
cond.append(CPair("type",type));
// when a setting is not global we are searching also for the instance
if(!isSettingGlobal(type))
cond.append(CPair("AND hyperion_inst",_hyperion_inst));
getRecord(cond, results, QStringList("config"));
return results["config"].toString();
}
QString getSettingsRecordString(const QString& type) const;
QJsonObject getSettings(const QStringList& filteredTypes = {} ) const;
QJsonObject getSettings(const QVariant& instance, const QStringList& filteredTypes = {} ) const;
QStringList nonExtingTypes() const;
QPair<bool, QStringList> addMissingDefaults();
///
/// @brief Delete all settings entries associated with this instance, called from InstanceTable of HyperionIManager
///
inline void deleteInstance() const
{
VectorPair cond;
cond.append(CPair("hyperion_inst",_hyperion_inst));
deleteRecord(cond);
}
void deleteInstance() const;
inline bool isSettingGlobal(const QString& type) const
{
// list of global settings
QStringList list;
// server port services
list << "jsonServer" << "protoServer" << "flatbufServer" << "forwarder" << "webConfig" << "network"
// capture
<< "framegrabber" << "grabberV4L2" << "grabberAudio"
//Events
<< "osEvents" << "cecEvents" << "schedEvents"
// other
<< "logger" << "general";
const QVector<QString>& getGlobalSettingTypes() const;
bool isGlobalSettingType(const QString& type) const;
return list.contains(type);
}
const QVector<QString>& getInstanceSettingTypes() const;
bool isInstanceSettingType(const QString& type) const;
const QJsonObject& getDefaultSettings() const;
semver::version getConfigVersion();
QString getConfigVersionString();
bool resolveConfigVersion();
bool resolveConfigVersion(QJsonObject generalConfig);
private:
const quint8 _hyperion_inst;
QString fixVersion(const QString& version);
QVector<QString> initializeGlobalSettingTypes() const;
static QVector<QString> globalSettingTypes;
static bool areGlobalSettingTypesInitialised;
QVector<QString> initializeInstanceSettingTypes() const;
static QVector<QString> instanceSettingTypes;
static bool areInstanceSettingTypesInitialised;
QJsonObject initializeDefaultSettings() const;
static QJsonObject defaultSettings;
static bool areDefaultSettingsInitialised;
const quint8 _instance;
semver::version _configVersion;
};
#endif // SETTINGSTABLE_H

View File

@@ -23,7 +23,7 @@ class AuthManager : public QObject
private:
friend class HyperionDaemon;
/// constructor is private, can be called from HyperionDaemon
AuthManager(QObject *parent = nullptr, bool readonlyMode = false);
AuthManager(QObject *parent = nullptr);
public:
struct AuthDefinition

View File

@@ -108,8 +108,6 @@ public:
///
QString getActiveDeviceType() const;
bool getReadOnlyMode() const {return _readOnlyMode; }
public slots:
///
@@ -335,18 +333,9 @@ public slots:
///
/// @brief Save a complete json config
/// @param config The entire config object
/// @param correct If true will correct json against schema before save
/// @return True on success else false
///
bool saveSettings(const QJsonObject& config, bool correct = false);
///
/// @brief Restore a complete json config
/// @param config The entire config object
/// @param correct If true will correct json against schema before save
/// @return True on success else false
///
bool restoreSettings(const QJsonObject& config, bool correct = false);
QPair<bool, QStringList> saveSettings(const QJsonObject& config);
/// ############
/// COMPONENTREGISTER
@@ -552,7 +541,7 @@ private:
/// @brief Constructs the Hyperion instance, just accessible for HyperionIManager
/// @param instance The instance index
///
Hyperion(quint8 instance, bool readonlyMode = false);
Hyperion(quint8 instance);
/// instance index
const quint8 _instIndex;
@@ -615,6 +604,4 @@ private:
/// Boblight instance
BoblightServer* _boblightServer;
#endif
bool _readOnlyMode;
};

View File

@@ -59,12 +59,18 @@ public slots:
///
QVector<QVariantMap> getInstanceData() const;
QString getInstanceName(quint8 inst = 0);
///
/// @brief Get all instance indicies of running instances
///
QList<quint8> getRunningInstanceIdx() const;
///
/// @brief Get all instance indicies configured
///
QList<quint8> getInstanceIds() const;
///
/// @brief Start a Hyperion instance
/// @param instance Instance index
@@ -115,8 +121,6 @@ public slots:
///
bool saveName(quint8 inst, const QString& name);
QString getRootPath() const { return _rootPath; }
signals:
///
/// @brief Emits whenever the state of a instance changes according to enum instanceState
@@ -195,9 +199,8 @@ private:
friend class HyperionDaemon;
///
/// @brief Construct the Manager
/// @param The root path of all userdata
///
HyperionIManager(const QString& rootPath, QObject* parent = nullptr, bool readonlyMode = false);
HyperionIManager(QObject* parent = nullptr);
///
/// @brief Start all instances that are marked as enabled in db. Non blocking
@@ -218,12 +221,9 @@ private:
private:
Logger* _log;
InstanceTable* _instanceTable;
const QString _rootPath;
QMap<quint8, Hyperion*> _runningInstances;
QList<quint8> _startQueue;
bool _readonlyMode;
/// All pending requests
QMap<quint8, PendingRequests> _pendingRequests;
};

View File

@@ -3,14 +3,11 @@
#include <utils/Logger.h>
#include <utils/settings.h>
#include <utils/version.hpp>
using namespace semver;
#include <db/SettingsTable.h>
// qt includes
#include <QJsonObject>
const int GLOABL_INSTANCE_ID = 255;
class Hyperion;
class SettingsTable;
@@ -26,23 +23,21 @@ public:
/// @params instance Instance index of HyperionInstanceManager
/// @params parent The parent hyperion instance
///
SettingsManager(quint8 instance, QObject* parent = nullptr, bool readonlyMode = false);
SettingsManager(quint8 instance = GLOABL_INSTANCE_ID, QObject* parent = nullptr);
///
/// @brief Save a complete json configuration
/// @brief Save a complete JSON configuration
/// @param config The entire config object
/// @param correct If true will correct json against schema before save
/// @return True on success else false
/// @return True on success else false, plus validation errors
///
bool saveSettings(QJsonObject config, bool correct = false);
QPair<bool, QStringList> saveSettings(const QJsonObject& config);
///
/// @brief Restore a complete json configuration
/// @brief Correct a complete JSON configuration
/// @param config The entire config object
/// @param correct If true will correct json against schema before save
/// @return True on success else false
/// @return True on success else false, plus correction details
///
bool restoreSettings(QJsonObject config, bool correct = false);
QPair<bool, QStringList> correctSettings(QJsonObject& config);
///
/// @brief get a single setting json from configuration
@@ -52,10 +47,18 @@ public:
QJsonDocument getSetting(settings::type type) const;
///
/// @brief get the full settings object of this instance (with global settings)
/// @brief get a single setting json from configuration
/// @param type The type as string
/// @return The requested json data as QJsonDocument
///
QJsonDocument getSetting(const QString& type) const;
///
/// @brief get the selected settings objects of this instance (including global settings)
/// @return The requested json
///
QJsonObject getSettings() const;
QJsonObject getSettings(const QStringList& filteredTypes = {}) const;
QJsonObject getSettings(const QVariant& instance, const QStringList& filteredTypes = {} ) const;
signals:
///
@@ -71,31 +74,19 @@ private:
/// @param config The configuration object
/// @return True when a migration has been triggered
///
bool handleConfigUpgrade(QJsonObject& config);
bool upgradeConfig(QJsonObject& config);
bool resolveConfigVersion(QJsonObject& config);
/// Logger instance
Logger* _log;
/// Hyperion instance
Hyperion* _hyperion;
/// Instance number
quint8 _instance;
/// instance of database table interface
SettingsTable* _sTable;
/// the schema
static QJsonObject schemaJson;
/// the current configuration of this instance
QJsonObject _qconfig;
semver::version _configVersion;
semver::version _previousVersion;
bool _readonlyMode;
};

View File

@@ -3,13 +3,14 @@
#include <utils/FileUtils.h>
#include <QJsonObject>
#include <QJsonDocument>
#include <QPair>
#include <QStringList>
#include <utils/Logger.h>
namespace JsonUtils {
///
/// @brief read a json file and get the parsed result on success
/// @brief read a JSON file and get the parsed result on success
/// @param[in] path The file path to read
/// @param[out] obj Returns the parsed QJsonObject
/// @param[in] log The logger of the caller to print errors
@@ -17,6 +18,7 @@ namespace JsonUtils {
/// @return true on success else false
///
QPair<bool, QStringList> readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError=false);
QPair<bool, QStringList> readFile(const QString& path, QJsonValue& obj, Logger* log, bool ignError=false);
///
/// @brief read a schema file and resolve $refs
@@ -28,18 +30,19 @@ namespace JsonUtils {
bool readSchema(const QString& path, QJsonObject& obj, Logger* log);
///
/// @brief parse a json QString and get a QJsonObject. Overloaded funtion
/// @param[in] path The file path/name just used for log messages
/// @brief parse a JSON QString and get a QJsonObject. Overloaded funtion
/// @param[in] path The file path/name context used for log messages
/// @param[in] data Data to parse
/// @param[out] obj Retuns the parsed QJsonObject
/// @param[in] log The logger of the caller to print errors
/// @return true on success else false
///
QPair<bool, QStringList> parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log);
QPair<bool, QStringList> parse(const QString& path, const QString& data, QJsonValue& value, Logger* log);
///
/// @brief parse a json QString and get a QJsonArray. Overloaded function
/// @param[in] path The file path/name just used for log messages
/// @brief parse a JSON QString and get a QJsonArray. Overloaded function
/// @param[in] path The file path/name context used for log messages
/// @param[in] data Data to parse
/// @param[out] arr Retuns the parsed QJsonArray
/// @param[in] log The logger of the caller to print errors
@@ -48,8 +51,8 @@ namespace JsonUtils {
QPair<bool, QStringList> parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log);
///
/// @brief parse a json QString and get a QJsonDocument
/// @param[in] path The file path/name just used for log messages
/// @brief parse a JSON QString and get a QJsonDocument
/// @param[in] path The file path/name context used for log messages
/// @param[in] data Data to parse
/// @param[out] doc Retuns the parsed QJsonDocument
/// @param[in] log The logger of the caller to print errors
@@ -58,29 +61,39 @@ namespace JsonUtils {
QPair<bool, QStringList> parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log);
///
/// @brief Validate json data against a schema
/// @param[in] file The path/name of json file just used for log messages
/// @param[in] json The json data
/// @brief Validate JSON data against a schema
/// @param[in] file The path/name of JSON file context used for log messages
/// @param[in] json The JSON data
/// @param[in] schemaP The schema path
/// @param[in] log The logger of the caller to print errors
/// @return true on success else false
/// @return true on success else false, plus validation errors
///
QPair<bool, QStringList> validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log);
QPair<bool, QStringList> validate(const QString& file, const QJsonValue& json, const QString& schemaPath, Logger* log);
///
/// @brief Validate json data against a schema
/// @param[in] file The path/name of json file just used for log messages
/// @param[in] json The json data
/// @brief Validate JSON data against a schema
/// @param[in] file The path/name of JSON file context used for log messages
/// @param[in] json The JSON data
/// @param[in] schema The schema object
/// @param[in] log The logger of the caller to print errors
/// @return true on success else false
/// @return true on success else false, plus validation errors
///
QPair<bool, QStringList> validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log);
QPair<bool, QStringList> validate(const QString& file, const QJsonValue& json, const QJsonObject& schema, Logger* log);
///
/// @brief Write json data to file
/// @brief Validate JSON data against a schema
/// @param[in] file The path/name of JSON file context used for log messages
/// @param[in/out] json The JSON data
/// @param[in] schema The schema object
/// @param[in] log The logger of the caller to print errors
/// @return true on success else false, plus correction messages
///
QPair<bool, QStringList> correct(const QString& file, QJsonValue& json, const QJsonObject& schema, Logger* log);
///
/// @brief Write JSON data to file
/// @param[in] filenameThe file path to write
/// @param[in] json The json data to write
/// @param[in] json The JSON data to write
/// @param[in] log The logger of the caller to print errors
/// @return true on success else false
///
@@ -94,4 +107,15 @@ namespace JsonUtils {
/// @return true on success else false
///
bool resolveRefs(const QJsonObject& schema, QJsonObject& obj, Logger* log);
///
/// @brief Function to convert QJsonValue to QString using QJsonDocument
///
QString jsonValueToQString(const QJsonValue &value, QJsonDocument::JsonFormat format = QJsonDocument::Compact);
///
/// @brief Function to merge two QJsonObjects
///
QJsonObject mergeJsonObjects(const QJsonObject &obj1, const QJsonObject &obj2, bool overrideObj1 = false);
}

View File

@@ -44,7 +44,7 @@ public:
/// @return The first boolean is true when the arguments is valid according to the schema. The second is true when the schema contains no errors
/// @return TODO: Check the Schema in SetSchema() function and remove the QPair result
///
QPair<bool, bool> validate(const QJsonObject& value, bool ignoreRequired = false);
QPair<bool, bool> validate(const QJsonValue& value, bool ignoreRequired = false);
///
/// @brief Auto correct a JSON structure
@@ -52,7 +52,7 @@ public:
/// @param ignoreRequired Ignore the "required" keyword in hyperion schema. Default is false
/// @return The corrected JSON structure
///
QJsonObject getAutoCorrectedConfig(const QJsonObject& value, bool ignoreRequired = false);
QJsonValue getAutoCorrectedConfig(const QJsonValue& value, bool ignoreRequired = false);
///
/// @return A list of error messages
@@ -207,7 +207,7 @@ private:
/// Auto correction variable
QString _correct;
/// The auto corrected json-configuration
QJsonObject _autoCorrected;
QJsonValue _autoCorrected;
/// The current location into a json-configuration structure being checked
QStringList _currentPath;
/// The result messages collected during the schema verification

View File

@@ -11,7 +11,7 @@ class QJsonUtils
{
public:
static void modify(QJsonObject& value, QStringList path, const QJsonValue& newValue = QJsonValue::Null, QString propertyName = "")
static void modify(QJsonValue& value, QStringList path, const QJsonValue& newValue = QJsonValue::Null, QString propertyName = "")
{
QJsonObject result;
@@ -27,7 +27,7 @@ public:
*it = current.mid(1, current.size()-1);
}
if (!value.isEmpty())
if (! (value.toObject().isEmpty() && value.toArray().isEmpty()) )
modifyValue(value, result, path, newValue, propertyName);
else if (newValue != QJsonValue::Null && !propertyName.isEmpty())
result[propertyName] = newValue;

View File

@@ -385,7 +385,7 @@ namespace semver {
return -1;
}
version& operator= (version& rgt)
version& operator= (const version& rgt)
{
if ((*this) != rgt)
{
@@ -404,17 +404,17 @@ namespace semver {
return *this;
}
friend bool operator== (version &lft, version &rgt)
friend bool operator== (const version &lft, const version &rgt)
{
return !(lft != rgt);
}
friend bool operator!= (version &lft, version &rgt)
friend bool operator!= (const version &lft, const version &rgt)
{
return (lft > rgt) || (lft < rgt);
}
friend bool operator> (version &lft, version &rgt)
friend bool operator> (const version &lft, const version &rgt)
{
// Major
if (lft.getMajor() < 0 && rgt.getMajor() >= 0)
@@ -522,17 +522,17 @@ namespace semver {
return false;
}
friend bool operator>= (version &lft, version &rgt)
friend bool operator>= (const version &lft, const version &rgt)
{
return (lft > rgt) || (lft == rgt);
}
friend bool operator< (version &lft, version &rgt)
friend bool operator< (const version &lft, const version &rgt)
{
return (rgt > lft);
}
friend bool operator<= (version &lft, version &rgt)
friend bool operator<= (const version &lft, const version &rgt)
{
return (lft < rgt) || (lft == rgt);
}