pgLab/pglab/ConnectionListModel.cpp

734 lines
20 KiB
C++

#include "ConnectionListModel.h"
#include "ScopeGuard.h"
#include "util.h"
#include <botan/cryptobox.h>
#include <QDir>
#include <QSettings>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QStandardPaths>
namespace {
const char * const q_create_table_conngroup =
R"__(
CREATE TABLE IF NOT EXISTS conngroup (
conngroup_id INTEGER PRIMARY KEY,
gname TEXT NOT NULL UNIQUE
);)__";
const char * const q_create_table_connection =
R"__(
CREATE TABLE IF NOT EXISTS connection (
uuid TEXT PRIMARY KEY,
cname TEXT NOT NULL,
conngroup_id INTEGER NOT NULL,
host TEXT NOT NULL,
hostaddr TEXT NOT NULL,
port INTEGER NOT NULL,
user TEXT NOT NULL,
dbname TEXT NOT NULL,
sslmode INTEGER NOT NULL,
sslcert TEXT NOT NULL,
sslkey TEXT NOT NULL,
sslrootcert TEXT NOT NULL,
sslcrl TEXT NOT NULL,
passwordstate INTEGER NOT NULL
);)__";
const char * const q_insert_or_replace_into_connection =
R"__(INSERT OR REPLACE INTO connection
VALUES (:uuid, :name, :conngroup_id, :host, :hostaddr, :port, :user, :dbname,
:sslmode, :sslcert, :sslkey, :sslrootcert, :sslcrl, :passwordstate);
)__" ;
std::tuple<bool, QSqlError> InitConnectionTables(QSqlDatabase &db)
{
QSqlQuery q_create_table(db);
q_create_table.prepare(q_create_table_conngroup);
if (!q_create_table.exec()) {
auto err = q_create_table.lastError();
return { false, err };
}
q_create_table.prepare(q_create_table_connection);
if (!q_create_table.exec()) {
auto err = q_create_table.lastError();
return { false, err };
}
return {true, {}};
}
std::optional<QSqlError> SaveConnectionConfig(QSqlDatabase &db, const ConnectionConfig &cc, int conngroup_id)
{
QSqlQuery q(db);
q.prepare(q_insert_or_replace_into_connection);
q.bindValue(":uuid", cc.uuid().toString());
q.bindValue(":name", stdStrToQ(cc.name()));
q.bindValue(":conngroup_id", conngroup_id);
q.bindValue(":host", stdStrToQ(cc.host()));
q.bindValue(":hostaddr", stdStrToQ(cc.hostAddr()));
q.bindValue(":port", cc.port());
q.bindValue(":user", stdStrToQ(cc.user()));
q.bindValue(":dbname", stdStrToQ(cc.dbname()));
q.bindValue(":sslmode", static_cast<int>(cc.sslMode()));
q.bindValue(":sslcert", stdStrToQ(cc.sslCert()));
q.bindValue(":sslkey", stdStrToQ(cc.sslKey()));
q.bindValue(":sslrootcert", stdStrToQ(cc.sslRootCert()));
q.bindValue(":sslcrl", stdStrToQ(cc.sslCrl()));
q.bindValue(":passwordstate", static_cast<int>(cc.passwordState()));
if (!q.exec()) {
return q.lastError();
}
return {};
}
/** Saves a connection configuration.
Before calling this you may want to call beginGroup.
*/
// void SaveConnectionConfig(QSettings &settings, const ConnectionConfig &cc)
// {
// settings.setValue("name", stdStrToQ(cc.name()));
// settings.setValue("host", stdStrToQ(cc.host()));
// settings.setValue("hostaddr", stdStrToQ(cc.hostAddr()));
// settings.setValue("port", cc.port());
// settings.setValue("user", stdStrToQ(cc.user()));
// //settings.setValue("password", stdStrToQ(cc.password()));
// settings.setValue("dbname", stdStrToQ(cc.dbname()));
// settings.setValue("sslmode", static_cast<int>(cc.sslMode()));
// settings.setValue("sslcert", stdStrToQ(cc.sslCert()));
// settings.setValue("sslkey", stdStrToQ(cc.sslKey()));
// settings.setValue("sslrootcert", stdStrToQ(cc.sslRootCert()));
// settings.setValue("sslcrl", stdStrToQ(cc.sslCrl()));
// settings.setValue("passwordState", static_cast<int>(cc.passwordState()));
// }
// template <typename S, typename T>
// bool in_range(T value)
// {
// return value >= std::numeric_limits<S>::min() && value <= std::numeric_limits<S>::max();
// }
// void LoadConnectionConfig(QSettings &settings, ConnectionConfig &cc)
// {
// cc.setName(qvarToStdStr(settings.value("name")));
// cc.setHost(qvarToStdStr(settings.value("host")));
// cc.setHostAddr(qvarToStdStr(settings.value("hostaddr")));
// int p = settings.value("port", 5432).toInt();
// if (!in_range<uint16_t>(p)) {
// p = 0; // let the user re-enter a valid value
// }
// cc.setPort(static_cast<uint16_t>(p));
// cc.setUser(qvarToStdStr(settings.value("user")));
// //cc.setPassword(qvarToStdStr(settings.value("password")));
// cc.setDbname(qvarToStdStr(settings.value("dbname")));
// cc.setSslMode(static_cast<SslMode>(settings.value("sslmode").toInt()));
// cc.setSslCert(qvarToStdStr(settings.value("sslcert")));
// cc.setSslKey(qvarToStdStr(settings.value("sslkey")));
// cc.setSslRootCert(qvarToStdStr(settings.value("sslrootcert")));
// cc.setSslCrl(qvarToStdStr(settings.value("sslcrl")));
// PasswordState pwstate;
// QVariant v = settings.value("passwordState");
// if (v.isNull()) pwstate = PasswordState::NotStored;
// else pwstate = static_cast<PasswordState>(v.toInt());
// cc.setPasswordState(pwstate);
// }
} // end of unnamed namespace
#if false
ConnectionListModel::ConnectionListModel(QObject *parent)
: QAbstractListModel(parent)
{
}
ConnectionListModel::~ConnectionListModel() = default;
int ConnectionListModel::rowCount(const QModelIndex &parent) const
{
int result = 0;
if (parent == QModelIndex()) {
result = m_connections.size();
}
return result;
}
int ConnectionListModel::columnCount(const QModelIndex &/*parent*/) const
{
return ColCount;
}
QVariant ConnectionListModel::data(const QModelIndex &index, int role) const
{
QVariant result;
if (role == Qt::DisplayRole || role == Qt::EditRole) {
int row = index.row();
int col = index.column();
const ConnectionConfig& cfg = m_connections.at(row);
switch (col) {
case Description:
result = makeLongDescription(cfg);
break;
case Name:
result = stdStrToQ(cfg.name());
break;
case Host:
result = stdStrToQ(cfg.host());
break;
case Port:
result = cfg.port();
break;
case User:
result = stdStrToQ(cfg.user());
break;
case Password:
result = stdStrToQ(cfg.password());
break;
case DbName:
result = stdStrToQ(cfg.dbname());
break;
}
}
return result;
}
bool ConnectionListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
bool result = false;
if (role == Qt::EditRole) {
int row = index.row();
int col = index.column();
// auto& elem = m_connections.at(row);
// elem.m_dirty = true;
// ConnectionConfig& cfg = elem.m_config;
ConnectionConfig& cfg = m_connections[row];
if (col > 0) {
result = true;
}
switch (col) {
case Description:
break;
case Name:
cfg.setName( qStrToStd(value.toString()) );
break;
case Host:
cfg.setHost( qStrToStd(value.toString()) );
break;
case Port:
cfg.setPort( value.toInt() );
break;
case User:
cfg.setUser( qStrToStd(value.toString()) );
break;
case Password:
cfg.setPassword( qStrToStd(value.toString()) );
break;
case DbName:
cfg.setDbname( qStrToStd(value.toString()) );
break;
}
}
if (result) {
emit dataChanged(index, index);
}
return result;
}
Qt::ItemFlags ConnectionListModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags result;
int row = index.row();
if (row >= 0 && row < m_connections.size()) {
result = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
if (index.column() != Description)
result |= Qt::ItemIsEditable;
}
return result;
}
QString ConnectionListModel::makeLongDescription(const ConnectionConfig &cfg)
{
std::string result(cfg.name());
result += " (";
result += cfg.user();
result += "@";
result += cfg.host();
result += ":";
result += std::to_string(cfg.port());
result += "/";
result += cfg.dbname();
result += ")";
return stdStrToQ(result);
}
bool ConnectionListModel::removeRows(int row, int count, const QModelIndex &parent)
{
bool result = false;
if (row >= 0 && row < m_connections.size()) {
beginRemoveRows(parent, row, row + count -1);
SCOPE_EXIT { endRemoveRows(); };
QString file_name = iniFileName();
QSettings settings(file_name, QSettings::IniFormat);
for (int idx = 0; idx < count; ++idx) {
auto&& cc = m_connections[idx+row];
settings.remove(cc.uuid().toString());
}
settings.sync();
m_connections.remove(row, count);
result = true;
}
return result;
}
//void ConnectionListModel::newItem()
//{
//// int i = m_connections->createNew();
//// auto idx = createIndex(i, 0);
//// emit dataChanged(idx, idx);
//}
Expected<ConnectionConfig> ConnectionListModel::get(int row)
{
if (row < m_connections.size()) {
return m_connections.at(row);
}
return Expected<ConnectionConfig>::fromException(std::out_of_range("Invalid row"));
}
void ConnectionListModel::load()
{
QString file_name = iniFileName();
QSettings settings(file_name, QSettings::IniFormat);
auto groups = settings.childGroups();
for (auto&& grp : groups) {
if (grp == "c_IniGroupSecurity") {
// Read security settings
} else {
QUuid uuid(grp);
if ( ! uuid.isNull() ) {
settings.beginGroup(grp);
SCOPE_EXIT { settings.endGroup(); };
ConnectionConfig cc;
cc.setUuid(uuid);
LoadConnectionConfig(settings, cc);
m_connections.push_back(cc);
}
}
}
}
void ConnectionListModel::save()
{
QString file_name = iniFileName();
QSettings settings(file_name, QSettings::IniFormat);
for (auto& e : m_connections) {
settings.beginGroup(e.uuid().toString());
SCOPE_EXIT { settings.endGroup(); };
SaveConnectionConfig(settings, e);
e.clean();
}
settings.sync();
}
void ConnectionListModel::save(int index)
{
auto& e = m_connections[index];
if (e.dirty()) {
QString file_name = iniFileName();
QSettings settings(file_name, QSettings::IniFormat);
settings.beginGroup(e.uuid().toString());
SaveConnectionConfig(settings, e);
e.clean();
settings.sync();
}
}
void ConnectionListModel::save(const ConnectionConfig &cc)
{
auto find_res = std::find(m_connections.begin(), m_connections.end(), cc.uuid());
int i;
if (find_res == m_connections.end()) {
m_connections.push_back(cc);
i = m_connections.size() - 1;
}
else {
*find_res = cc;
i = find_res - m_connections.begin();
}
emit dataChanged(createIndex(i, 0), createIndex(i, ColCount-1));
save(i);
}
QString ConnectionListModel::iniFileName()
{
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
path += "/connections.ini";
return path;
}
#endif
ConnectionTreeModel::ConnectionTreeModel(QObject *parent, QSqlDatabase &db)
: QAbstractItemModel(parent)
, m_db(db)
{
}
void ConnectionTreeModel::load()
{
InitConnectionTables(m_db);
QSqlQuery q(m_db);
q.prepare("SELECT conngroup_id, gname FROM conngroup;");
if (!q.exec()) {
// auto err = q_create_table.lastError();
// return { false, err };
throw std::runtime_error("Loading groups failed");
}
while (q.next()) {
int id = q.value(0).toInt();
QString name = q.value(1).toString();
auto g = std::make_shared<ConnectionGroup>();
g->conngroup_id = id;
g->name = name;
m_groups.push_back(g);
}
q.prepare("SELECT uuid, cname, conngroup_id, host, hostaddr, port, "
" user, dbname, sslmode, sslcert, sslkey, sslrootcert, sslcrl, "
" passwordstate "
"FROM connection ORDER BY conngroup_id, cname;");
if (!q.exec()) {
// auto err = q_create_table.lastError();
// return { false, err };
throw std::runtime_error("Loading groups failed");
}
while (q.next()) {
auto cc = std::make_shared<ConnectionConfig>();
cc->setUuid(q.value(0).toUuid());
cc->setName(qvarToStdStr(q.value(1)));
cc->setHost(qvarToStdStr(q.value(3)));
cc->setHostAddr(qvarToStdStr(q.value(4)));
cc->setPort(static_cast<uint16_t>(q.value(5).toInt()));
cc->setUser(qvarToStdStr(q.value(6)));
cc->setDbname(qvarToStdStr(q.value(7)));
cc->setSslMode(static_cast<SslMode>(q.value(8).toInt()));
cc->setSslCert(qvarToStdStr(q.value(9)));
cc->setSslKey(qvarToStdStr(q.value(10)));
cc->setSslRootCert(qvarToStdStr(q.value(11)));
cc->setSslCrl(qvarToStdStr(q.value(12)));
cc->setPasswordState(static_cast<PasswordState>(q.value(13).toInt()));
int group_id = q.value(2).toInt();
auto find_res = std::find_if(m_groups.begin(), m_groups.end(),
[group_id] (auto item) { return item->conngroup_id == group_id; });
if (find_res != m_groups.end()) {
(*find_res)->add(cc);
}
else {
throw std::runtime_error("conngroup missing");
}
}
// auto g1 = std::make_shared<ConnectionGroup>();
// g1->name = "Testing";
// for (int i = 1; i < 3; ++i) {
// auto cc = std::make_shared<ConnectionConfig>();
// cc->setUuid(QUuid::createUuid());
// cc->setName("testconn " + std::to_string(i));
// g1->add(cc);
// }
// auto g2 = std::make_shared<ConnectionGroup>();
// g2->name = "Production";
// for (int i = 1; i < 4; ++i) {
// auto cc = std::make_shared<ConnectionConfig>();
// cc->setUuid(QUuid::createUuid());
// cc->setName("prodconn " + std::to_string(i));
// g2->add(cc);
// }
// m_groups = { g1, g2 };
}
QVariant ConnectionTreeModel::data(const QModelIndex &index, int role) const
{
// Code below assumes two level tree groups/connections
// it will fail for nested groups
QVariant v;
auto privdata = static_cast<ConnectionNode*>(index.internalPointer());
if (auto group = dynamic_cast<ConnectionGroup*>(privdata); group != nullptr) {
// This is a group
if (role == Qt::DisplayRole) {
if (index.column() == Name) {
v = group->name;
}
}
}
else if (auto conn = dynamic_cast<ConnectionConfig*>(privdata); conn != nullptr) {
// This is a connection
if (role == Qt::DisplayRole) {
switch (index.column()) {
case Name: v = stdStrToQ(conn->name()); break;
case Host: v = stdStrToQ(conn->host()); break;
case Port: v = conn->port(); break;
case User: v = stdStrToQ(conn->user()); break;
case DbName: v= stdStrToQ(conn->dbname()); break;
}
}
}
return v;
}
QVariant ConnectionTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
{
QVariant v;
if (orientation == Qt::Horizontal) {
if (role == Qt::DisplayRole) {
switch (section) {
case Name: v = tr("Name"); break;
case Host: v = tr("Host"); break;
case Port: v = tr("Port"); break;
case User: v = tr("User"); break;
case DbName: v= tr("Database"); break;
}
}
}
return v;
}
QModelIndex ConnectionTreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return {};
const ConnectionNode *node = nullptr;
if (parent.isValid()) {
auto privdata = static_cast<ConnectionNode*>(parent.internalPointer());
if (auto group = dynamic_cast<ConnectionGroup*>(privdata); group != nullptr) {
node = group->connections().at(row).get();
}
else {
throw std::logic_error("Should never ask for a child index of a connectionconfig");
}
}
else {
node = m_groups[row].get();
}
return createIndex(row, column, const_cast<ConnectionNode*>(node));
// void *p = nullptr;
// if (parent.isValid()) {
// auto cg = static_cast<ConnectionGroup *>(parent.internalPointer());
// auto cc = &cg->connections().at(row);
// p = const_cast<ConnectionConfig*>(cc);
// }
// else {
// p = const_cast<ConnectionGroup*>(&m_groups.at(row));
// }
// return createIndex(row, column, p);
}
QModelIndex ConnectionTreeModel::parent(const QModelIndex &index) const
{
if (!index.isValid())
return {};
auto privdata = static_cast<ConnectionNode*>(index.internalPointer());
if (auto group = dynamic_cast<ConnectionGroup*>(privdata); group != nullptr) {
return {};
}
else if (auto config = dynamic_cast<ConnectionConfig*>(privdata); config != nullptr) {
auto p = config->parent();
auto find_res = std::find_if(m_groups.begin(), m_groups.end(), [p] (auto item) -> bool { return *p == *item; });
if (find_res != m_groups.end()) {
return createIndex(find_res - m_groups.begin(), 0,
const_cast<ConnectionGroup*>(config->parent()));
}
}
throw std::logic_error("Should never get here");
}
int ConnectionTreeModel::rowCount(const QModelIndex &parent) const
{
int result = 0;
if (parent.isValid()) {
// if (parent.column() <= 0) {
// result = m_groups[parent.row()].connections().size();
// }
// else {
// result = 1;
// }
auto privdata = static_cast<ConnectionNode*>(parent.internalPointer());
if (auto group = dynamic_cast<ConnectionGroup*>(privdata); group != nullptr) {
result = group->connections().size();
}
else if (auto config = dynamic_cast<ConnectionConfig*>(privdata); config != nullptr) {
result = 0;
}
}
else {
result = m_groups.size();
}
return result;
}
int ConnectionTreeModel::columnCount(const QModelIndex &) const
{
return ColCount;
}
bool ConnectionTreeModel::removeRows(int row, int count, const QModelIndex &parent)
{
if (parent.isValid() && count == 1) {
// should be a group
auto grp = m_groups[parent.row()];
for (int i = 0; i < count; ++i) {
QUuid uuid = grp->connections().at(row + i)->uuid();
QSqlQuery q(m_db);
q.prepare(
"DELETE FROM connection "
" WHERE uuid=:uuid");
q.bindValue(":uuid", uuid);
if (!q.exec()) {
auto err = q.lastError();
throw std::runtime_error("QqlError");
}
}
beginRemoveRows(parent, row, row + count - 1);
SCOPE_EXIT { endRemoveRows(); };
grp->erase(row, count);
}
}
void ConnectionTreeModel::save(const QString &group_name, const ConnectionConfig &cc)
{
auto [grp_idx, conn_idx] = findConfig(cc.uuid());
if (grp_idx >= 0) {
auto grp = m_groups[grp_idx];
if (grp->name == group_name) {
// update config
grp->update(conn_idx, cc);
// send change event
auto node = grp->connections().at(conn_idx);
dataChanged(
createIndex(conn_idx, 0, node.get()),
createIndex(conn_idx, ColCount-1, node.get()));
saveToDb(*node);
return;
}
else {
auto parent = createIndex(grp_idx, 0, grp.get());
beginRemoveRows(parent, conn_idx, conn_idx);
SCOPE_EXIT { endRemoveRows(); };
grp->erase(conn_idx);
}
}
// Here we can assume we have to find the new group or create a new group
// because if the connection was in the right group the function has already returned.
// We assume the model is in sync with the DB as the DB should not be shared!
int new_grp_idx = findGroup(group_name);
if (new_grp_idx < 0) {
// Group not found we are g
auto add_grp_res = addGroup(group_name);
if (std::holds_alternative<int>(add_grp_res)) {
new_grp_idx = std::get<int>(add_grp_res);
}
else {
throw std::runtime_error("SqlError1");
}
}
auto new_grp = m_groups[new_grp_idx];
auto parent = createIndex(new_grp_idx, 0, new_grp.get());
auto idx = new_grp->connections().size();
beginInsertRows(parent, idx, idx);
SCOPE_EXIT { endInsertRows(); };
auto node = std::make_shared<ConnectionConfig>(cc);
int node_idx = new_grp->add(node);
// dataChanged(
// createIndex(node_idx, 0, node.get()),
// createIndex(node_idx, ColCount-1, node.get()));
auto save_res = saveToDb(*node);
if (save_res) {
throw std::runtime_error("SqlError2");
}
}
void ConnectionTreeModel::save(const ConnectionConfig &cc)
{
saveToDb(cc);
}
std::tuple<int, int> ConnectionTreeModel::findConfig(const QUuid uuid) const
{
int group_idx = -1, connection_idx = -1;
for (int grp_idx = 0; grp_idx < m_groups.size(); ++grp_idx) {
auto && grp = m_groups[grp_idx];
auto && conns = grp->connections();
auto find_res = std::find_if(conns.begin(), conns.end(),
[&uuid] (auto item) -> bool { return item->uuid() == uuid; });
if (find_res != conns.end()) {
group_idx = grp_idx;
connection_idx = find_res - conns.begin();
break;
}
}
return { group_idx, connection_idx };
}
int ConnectionTreeModel::findGroup(QString name) const
{
for (int idx = 0; idx < m_groups.size(); ++idx) {
if (m_groups[idx]->name == name) return idx;
}
return -1;
}
std::variant<int, QSqlError> ConnectionTreeModel::addGroup(QString group_name)
{
QSqlQuery q(m_db);
q.prepare("INSERT INTO conngroup (gname) VALUES (:name)");
q.bindValue(":name", group_name);
if (!q.exec()) {
auto err = q.lastError();
return { err };
}
return q.lastInsertId().toInt();
}
std::optional<QSqlError> ConnectionTreeModel::saveToDb(const ConnectionConfig &cc)
{
return SaveConnectionConfig(m_db, cc, cc.parent()->conngroup_id);
}