File indexing completed on 2024-04-21 15:02:48

0001 /*
0002     SPDX-FileCopyrightText: 2013 David Edmundson <davidedmundson@kde.org>
0003     SPDX-FileCopyrightText: 2013 Martin Klapetek <mklapetek@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-or-later
0006 */
0007 
0008 #include "personmanager_p.h"
0009 
0010 #include "kpeople_debug.h"
0011 
0012 #include <QDir>
0013 #include <QSqlQuery>
0014 #include <QStandardPaths>
0015 #include <QVariant>
0016 
0017 #ifdef QT_DBUS_LIB
0018 #include <QDBusConnection>
0019 #include <QDBusMessage>
0020 #endif
0021 
0022 class Transaction
0023 {
0024 public:
0025     Transaction(const QSqlDatabase &db);
0026     void cancel();
0027     ~Transaction();
0028     Transaction(const Transaction &) = delete;
0029     Transaction &operator=(const Transaction &) = delete;
0030 
0031 private:
0032     QSqlDatabase m_db;
0033     bool m_cancelled = false;
0034 };
0035 
0036 Transaction::Transaction(const QSqlDatabase &db)
0037     : m_db(db)
0038 {
0039     m_db.transaction();
0040 }
0041 
0042 void Transaction::cancel()
0043 {
0044     m_db.rollback();
0045     m_cancelled = true;
0046 }
0047 
0048 Transaction::~Transaction()
0049 {
0050     if (!m_cancelled) {
0051         m_db.commit();
0052     }
0053 }
0054 
0055 PersonManager::PersonManager(const QString &databasePath, QObject *parent)
0056     : QObject(parent)
0057     , m_db(QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("kpeoplePersonsManager")))
0058 {
0059     m_db.setDatabaseName(databasePath);
0060     if (!m_db.open()) {
0061         qCWarning(KPEOPLE_LOG) << "Couldn't open the database at" << databasePath;
0062     }
0063     m_db.exec(QStringLiteral("CREATE TABLE IF NOT EXISTS persons (contactID VARCHAR UNIQUE NOT NULL, personID INT NOT NULL)"));
0064     m_db.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS contactIdIndex ON persons (contactId)"));
0065     m_db.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS personIdIndex ON persons (personId)"));
0066 
0067 #ifdef QT_DBUS_LIB
0068     QDBusConnection::sessionBus().connect(QString(),
0069                                           QStringLiteral("/KPeople"),
0070                                           QStringLiteral("org.kde.KPeople"),
0071                                           QStringLiteral("ContactAddedToPerson"),
0072                                           this,
0073                                           SIGNAL(contactAddedToPerson(QString, QString)));
0074     QDBusConnection::sessionBus().connect(QString(),
0075                                           QStringLiteral("/KPeople"),
0076                                           QStringLiteral("org.kde.KPeople"),
0077                                           QStringLiteral("ContactRemovedFromPerson"),
0078                                           this,
0079                                           SIGNAL(contactRemovedFromPerson(QString)));
0080 #endif
0081 }
0082 
0083 PersonManager::~PersonManager()
0084 {
0085 }
0086 
0087 QMultiHash<QString, QString> PersonManager::allPersons() const
0088 {
0089     QMultiHash<QString /*PersonID*/, QString /*ContactID*/> contactMapping;
0090 
0091     QSqlQuery query = m_db.exec(QStringLiteral("SELECT personID, contactID FROM persons"));
0092     while (query.next()) {
0093         const QString personUri = QLatin1String("kpeople://") + query.value(0).toString(); // we store as ints internally, convert it to a string here
0094         const QString contactID = query.value(1).toString();
0095         contactMapping.insert(personUri, contactID);
0096     }
0097     return contactMapping;
0098 }
0099 
0100 QStringList PersonManager::contactsForPersonUri(const QString &personUri) const
0101 {
0102     if (!personUri.startsWith(QLatin1String("kpeople://"))) {
0103         return QStringList();
0104     }
0105 
0106     QStringList contactUris;
0107     // TODO port to the proper qsql method for args
0108     QSqlQuery query(m_db);
0109     query.prepare(QStringLiteral("SELECT contactID FROM persons WHERE personId = ?"));
0110     query.bindValue(0, personUri.mid(strlen("kpeople://")));
0111     query.exec();
0112 
0113     while (query.next()) {
0114         contactUris << query.value(0).toString();
0115     }
0116     return contactUris;
0117 }
0118 
0119 QString PersonManager::personUriForContact(const QString &contactUri) const
0120 {
0121     QSqlQuery query(m_db);
0122     query.prepare(QStringLiteral("SELECT personId FROM persons WHERE contactId = ?"));
0123     query.bindValue(0, contactUri);
0124     query.exec();
0125     if (query.next()) {
0126         return QLatin1String("kpeople://") + query.value(0).toString();
0127     }
0128     return QString();
0129 }
0130 
0131 QString PersonManager::mergeContacts(const QStringList &ids)
0132 {
0133     // no merging if we have only 0 || 1 ids
0134     if (ids.size() < 2) {
0135         return QString();
0136     }
0137 
0138     QStringList metacontacts;
0139     QStringList contacts;
0140 
0141     bool rc = true;
0142 
0143 #ifdef QT_DBUS_LIB
0144     QList<QDBusMessage> pendingMessages;
0145 #endif
0146 
0147     // separate the passed ids to metacontacts and simple contacts
0148     for (const QString &id : ids) {
0149         if (id.startsWith(QLatin1String("kpeople://"))) {
0150             metacontacts << id;
0151         } else {
0152             contacts << id;
0153         }
0154     }
0155 
0156     // create new personUriString
0157     //   - if we're merging two simple contacts, create completely new id
0158     //   - if we're merging an existing metacontact, take the first id and use it
0159     QString personUriString;
0160     if (metacontacts.isEmpty()) {
0161         // query for the highest existing ID in the database and +1 it
0162         int personUri = 0;
0163         QSqlQuery query = m_db.exec(QStringLiteral("SELECT MAX(personID) FROM persons"));
0164         if (query.next()) {
0165             personUri = query.value(0).toInt();
0166             personUri++;
0167         }
0168 
0169         personUriString = QLatin1String("kpeople://") + QString::number(personUri);
0170     } else {
0171         personUriString = metacontacts.first();
0172     }
0173 
0174     // start a db transaction (ends automatically on destruction)
0175     Transaction t(m_db);
0176 
0177     // processed passed metacontacts
0178     if (metacontacts.count() > 1) {
0179         // collect all the contacts from other persons
0180         QStringList personContacts;
0181         for (const QString &id : std::as_const(metacontacts)) {
0182             if (id != personUriString) {
0183                 personContacts << contactsForPersonUri(id);
0184             }
0185         }
0186 
0187         // iterate over all of the contacts and change their personID to the new personUriString
0188         for (const QString &id : std::as_const(personContacts)) {
0189             QSqlQuery updateQuery(m_db);
0190             updateQuery.prepare(QStringLiteral("UPDATE persons SET personID = ? WHERE contactID = ?"));
0191             updateQuery.bindValue(0, personUriString.mid(strlen("kpeople://")));
0192             updateQuery.bindValue(1, id);
0193             if (!updateQuery.exec()) {
0194                 rc = false;
0195             }
0196 
0197 #ifdef QT_DBUS_LIB
0198             QDBusMessage message =
0199                 QDBusMessage::createSignal(QStringLiteral("/KPeople"), QStringLiteral("org.kde.KPeople"), QStringLiteral("ContactRemovedFromPerson"));
0200 
0201             message.setArguments(QVariantList() << id);
0202             pendingMessages << message;
0203 
0204             message = QDBusMessage::createSignal(QStringLiteral("/KPeople"), QStringLiteral("org.kde.KPeople"), QStringLiteral("ContactAddedToPerson"));
0205 
0206             message.setArguments(QVariantList() << id << personUriString);
0207             pendingMessages << message;
0208 #endif
0209         }
0210     }
0211 
0212     // process passed contacts
0213     if (!contacts.isEmpty()) {
0214         for (const QString &id : std::as_const(contacts)) {
0215             QSqlQuery insertQuery(m_db);
0216             insertQuery.prepare(QStringLiteral("INSERT INTO persons VALUES (?, ?)"));
0217             insertQuery.bindValue(0, id);
0218             insertQuery.bindValue(1, personUriString.mid(strlen("kpeople://"))); // strip kpeople://
0219             if (!insertQuery.exec()) {
0220                 rc = false;
0221             }
0222 
0223 #ifdef QT_DBUS_LIB
0224             // FUTURE OPTIMIZATION - this would be best as one signal, but arguments become complex
0225             QDBusMessage message =
0226                 QDBusMessage::createSignal(QStringLiteral("/KPeople"), QStringLiteral("org.kde.KPeople"), QStringLiteral("ContactAddedToPerson"));
0227 
0228             message.setArguments(QVariantList() << id << personUriString);
0229             pendingMessages << message;
0230 #endif
0231         }
0232     }
0233 
0234     // if success send all messages to other clients
0235     // otherwise roll back our database changes and return an empty string
0236     if (rc) {
0237 #ifdef QT_DBUS_LIB
0238         for (const QDBusMessage &message : std::as_const(pendingMessages)) {
0239             QDBusConnection::sessionBus().send(message);
0240         }
0241 #endif
0242     } else {
0243         t.cancel();
0244         personUriString.clear();
0245     }
0246 
0247     return personUriString;
0248 }
0249 
0250 bool PersonManager::unmergeContact(const QString &id)
0251 {
0252     // remove rows from DB
0253     if (id.startsWith(QLatin1String("kpeople://"))) {
0254         QSqlQuery query(m_db);
0255 
0256         const QStringList contactUris = contactsForPersonUri(id);
0257         query.prepare(QStringLiteral("DELETE FROM persons WHERE personId = ?"));
0258         query.bindValue(0, id.mid(strlen("kpeople://")));
0259         query.exec();
0260 
0261 #ifdef QT_DBUS_LIB
0262         for (const QString &contactUri : contactUris) {
0263             // FUTURE OPTIMIZATION - this would be best as one signal, but arguments become complex
0264             QDBusMessage message =
0265                 QDBusMessage::createSignal(QStringLiteral("/KPeople"), QStringLiteral("org.kde.KPeople"), QStringLiteral("ContactRemovedFromPerson"));
0266 
0267             message.setArguments(QVariantList() << contactUri);
0268             QDBusConnection::sessionBus().send(message);
0269         }
0270 #endif
0271     } else {
0272         QSqlQuery query(m_db);
0273         query.prepare(QStringLiteral("DELETE FROM persons WHERE contactId = ?"));
0274         query.bindValue(0, id);
0275         query.exec();
0276         // emit signal(dbus)
0277         Q_EMIT contactRemovedFromPerson(id);
0278     }
0279 
0280     // TODO return if removing rows worked
0281     return true;
0282 }
0283 
0284 PersonManager *PersonManager::instance(const QString &databasePath)
0285 {
0286     static PersonManager *s_instance = nullptr;
0287     if (!s_instance) {
0288         QString path = databasePath;
0289         if (path.isEmpty()) {
0290             path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kpeople/");
0291 
0292             QDir().mkpath(path);
0293             path += QLatin1String("persondb");
0294         }
0295         s_instance = new PersonManager(path);
0296     }
0297     return s_instance;
0298 }
0299 
0300 #include "moc_personmanager_p.cpp"