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"