1 /*
2 * Copyright (C) 2008-2021 The QXmpp developers
3 *
4 * Authors:
5 * Manjeet Dahiya
6 * Jeremy Lainé
7 *
8 * Source:
9 * https://github.com/qxmpp-project/qxmpp
10 *
11 * This file is a part of QXmpp library.
12 *
13 * This library is free software; you can redistribute it and/or
14 * modify it under the terms of the GNU Lesser General Public
15 * License as published by the Free Software Foundation; either
16 * version 2.1 of the License, or (at your option) any later version.
17 *
18 * This library is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 * Lesser General Public License for more details.
22 *
23 */
24
25 #include "QXmppRosterManager.h"
26
27 #include "QXmppClient.h"
28 #include "QXmppPresence.h"
29 #include "QXmppRosterIq.h"
30 #include "QXmppUtils.h"
31
32 #include <QDomElement>
33
34 class QXmppRosterManagerPrivate
35 {
36 public:
37 QXmppRosterManagerPrivate();
38
39 void clear();
40
41 // map of bareJid and its rosterEntry
42 QMap<QString, QXmppRosterIq::Item> entries;
43
44 // map of resources of the jid and map of resources and presences
45 QMap<QString, QMap<QString, QXmppPresence>> presences;
46
47 // flag to store that the roster has been populated
48 bool isRosterReceived;
49
50 // id of the initial roster request
51 QString rosterReqId;
52 };
53
QXmppRosterManagerPrivate()54 QXmppRosterManagerPrivate::QXmppRosterManagerPrivate()
55 : isRosterReceived(false)
56 {
57 }
58
clear()59 void QXmppRosterManagerPrivate::clear()
60 {
61 entries.clear();
62 presences.clear();
63 rosterReqId.clear();
64 isRosterReceived = false;
65 }
66
67 ///
68 /// Constructs a roster manager.
69 ///
QXmppRosterManager(QXmppClient * client)70 QXmppRosterManager::QXmppRosterManager(QXmppClient *client)
71 : d(new QXmppRosterManagerPrivate())
72 {
73 connect(client, &QXmppClient::connected,
74 this, &QXmppRosterManager::_q_connected);
75
76 connect(client, &QXmppClient::disconnected,
77 this, &QXmppRosterManager::_q_disconnected);
78
79 connect(client, &QXmppClient::presenceReceived,
80 this, &QXmppRosterManager::_q_presenceReceived);
81 }
82
~QXmppRosterManager()83 QXmppRosterManager::~QXmppRosterManager()
84 {
85 delete d;
86 }
87
88 ///
89 /// Accepts an existing subscription request or pre-approves future subscription
90 /// requests.
91 ///
92 /// You can call this method in reply to the subscriptionRequest() signal or to
93 /// create a pre-approved subscription.
94 ///
95 /// \note Pre-approving subscription requests is only allowed, if the server
96 /// supports RFC6121 and advertises the 'urn:xmpp:features:pre-approval' stream
97 /// feature.
98 ///
99 /// \sa QXmppStreamFeatures::preApprovedSubscriptionsSupported()
100 ///
acceptSubscription(const QString & bareJid,const QString & reason)101 bool QXmppRosterManager::acceptSubscription(const QString &bareJid, const QString &reason)
102 {
103 QXmppPresence presence;
104 presence.setTo(bareJid);
105 presence.setType(QXmppPresence::Subscribed);
106 presence.setStatusText(reason);
107 return client()->sendPacket(presence);
108 }
109
110 ///
111 /// Upon XMPP connection, request the roster.
112 ///
_q_connected()113 void QXmppRosterManager::_q_connected()
114 {
115 // clear cache if stream has not been resumed
116 if (client()->streamManagementState() != QXmppClient::ResumedStream) {
117 d->clear();
118 }
119
120 if (!d->isRosterReceived) {
121 QXmppRosterIq roster;
122 roster.setType(QXmppIq::Get);
123 roster.setFrom(client()->configuration().jid());
124
125 // TODO: Request MIX annotations only when the server supports MIX-PAM.
126 roster.setMixAnnotate(true);
127
128 d->rosterReqId = roster.id();
129 if (client()->isAuthenticated())
130 client()->sendPacket(roster);
131 }
132 }
133
_q_disconnected()134 void QXmppRosterManager::_q_disconnected()
135 {
136 // clear cache if stream cannot be resumed
137 if (client()->streamManagementState() == QXmppClient::NoStreamManagement) {
138 d->clear();
139 }
140 }
141
142 /// \cond
handleStanza(const QDomElement & element)143 bool QXmppRosterManager::handleStanza(const QDomElement &element)
144 {
145 if (element.tagName() != "iq" || !QXmppRosterIq::isRosterIq(element))
146 return false;
147
148 // Security check: only server should send this iq
149 // from() should be either empty or bareJid of the user
150 const auto fromJid = element.attribute("from");
151 if (!fromJid.isEmpty() && QXmppUtils::jidToBareJid(fromJid) != client()->configuration().jidBare())
152 return false;
153
154 QXmppRosterIq rosterIq;
155 rosterIq.parse(element);
156
157 bool isInitial = (d->rosterReqId == rosterIq.id());
158 if (isInitial)
159 d->rosterReqId.clear();
160
161 switch (rosterIq.type()) {
162 case QXmppIq::Set: {
163 // send result iq
164 QXmppIq returnIq(QXmppIq::Result);
165 returnIq.setId(rosterIq.id());
166 client()->sendPacket(returnIq);
167
168 // store updated entries and notify changes
169 const auto items = rosterIq.items();
170 for (const auto &item : items) {
171 const QString bareJid = item.bareJid();
172 if (item.subscriptionType() == QXmppRosterIq::Item::Remove) {
173 if (d->entries.remove(bareJid)) {
174 // notify the user that the item was removed
175 emit itemRemoved(bareJid);
176 }
177 } else {
178 const bool added = !d->entries.contains(bareJid);
179 d->entries.insert(bareJid, item);
180 if (added) {
181 // notify the user that the item was added
182 emit itemAdded(bareJid);
183 } else {
184 // notify the user that the item changed
185 emit itemChanged(bareJid);
186 }
187 }
188 }
189 } break;
190 case QXmppIq::Result: {
191 const auto items = rosterIq.items();
192 for (const auto &item : items) {
193 const auto bareJid = item.bareJid();
194 d->entries.insert(bareJid, item);
195 }
196 if (isInitial) {
197 d->isRosterReceived = true;
198 emit rosterReceived();
199 }
200 break;
201 }
202 default:
203 break;
204 }
205
206 return true;
207 }
208 /// \endcond
209
_q_presenceReceived(const QXmppPresence & presence)210 void QXmppRosterManager::_q_presenceReceived(const QXmppPresence &presence)
211 {
212 const auto jid = presence.from();
213 const auto bareJid = QXmppUtils::jidToBareJid(jid);
214 const auto resource = QXmppUtils::jidToResource(jid);
215
216 if (bareJid.isEmpty())
217 return;
218
219 switch (presence.type()) {
220 case QXmppPresence::Available:
221 d->presences[bareJid][resource] = presence;
222 emit presenceChanged(bareJid, resource);
223 break;
224 case QXmppPresence::Unavailable:
225 d->presences[bareJid].remove(resource);
226 emit presenceChanged(bareJid, resource);
227 break;
228 case QXmppPresence::Subscribe:
229 if (client()->configuration().autoAcceptSubscriptions()) {
230 // accept subscription request
231 acceptSubscription(bareJid);
232
233 // ask for reciprocal subscription
234 subscribe(bareJid);
235 } else {
236 emit subscriptionReceived(bareJid);
237 }
238 break;
239 default:
240 break;
241 }
242 }
243
244 ///
245 /// Refuses a subscription request.
246 ///
247 /// You can call this method in reply to the subscriptionRequest() signal.
248 ///
refuseSubscription(const QString & bareJid,const QString & reason)249 bool QXmppRosterManager::refuseSubscription(const QString &bareJid, const QString &reason)
250 {
251 QXmppPresence presence;
252 presence.setTo(bareJid);
253 presence.setType(QXmppPresence::Unsubscribed);
254 presence.setStatusText(reason);
255 return client()->sendPacket(presence);
256 }
257
258 ///
259 /// Adds a new item to the roster without sending any subscription requests.
260 ///
261 /// As a result, the server will initiate a roster push, causing the
262 /// itemAdded() or itemChanged() signal to be emitted.
263 ///
264 /// \param bareJid
265 /// \param name Optional name for the item.
266 /// \param groups Optional groups for the item.
267 ///
addItem(const QString & bareJid,const QString & name,const QSet<QString> & groups)268 bool QXmppRosterManager::addItem(const QString &bareJid, const QString &name, const QSet<QString> &groups)
269 {
270 QXmppRosterIq::Item item;
271 item.setBareJid(bareJid);
272 item.setName(name);
273 item.setGroups(groups);
274 item.setSubscriptionType(QXmppRosterIq::Item::NotSet);
275
276 QXmppRosterIq iq;
277 iq.setType(QXmppIq::Set);
278 iq.addItem(item);
279 return client()->sendPacket(iq);
280 }
281
282 ///
283 /// Removes a roster item and cancels subscriptions to and from the contact.
284 ///
285 /// As a result, the server will initiate a roster push, causing the
286 /// itemRemoved() signal to be emitted.
287 ///
288 /// \param bareJid
289 ///
removeItem(const QString & bareJid)290 bool QXmppRosterManager::removeItem(const QString &bareJid)
291 {
292 QXmppRosterIq::Item item;
293 item.setBareJid(bareJid);
294 item.setSubscriptionType(QXmppRosterIq::Item::Remove);
295
296 QXmppRosterIq iq;
297 iq.setType(QXmppIq::Set);
298 iq.addItem(item);
299 return client()->sendPacket(iq);
300 }
301
302 ///
303 /// Renames a roster item.
304 ///
305 /// As a result, the server will initiate a roster push, causing the
306 /// itemChanged() signal to be emitted.
307 ///
308 /// \param bareJid
309 /// \param name
310 ///
renameItem(const QString & bareJid,const QString & name)311 bool QXmppRosterManager::renameItem(const QString &bareJid, const QString &name)
312 {
313 if (!d->entries.contains(bareJid))
314 return false;
315
316 auto item = d->entries.value(bareJid);
317 item.setName(name);
318
319 // If there is a pending subscription, do not include the corresponding attribute in the stanza.
320 if (!item.subscriptionStatus().isEmpty())
321 item.setSubscriptionStatus({});
322
323 QXmppRosterIq iq;
324 iq.setType(QXmppIq::Set);
325 iq.addItem(item);
326 return client()->sendPacket(iq);
327 }
328
329 ///
330 /// Requests a subscription to the given contact.
331 ///
332 /// As a result, the server will initiate a roster push, causing the
333 /// itemAdded() or itemChanged() signal to be emitted.
334 ///
subscribe(const QString & bareJid,const QString & reason)335 bool QXmppRosterManager::subscribe(const QString &bareJid, const QString &reason)
336 {
337 QXmppPresence packet;
338 packet.setTo(QXmppUtils::jidToBareJid(bareJid));
339 packet.setType(QXmppPresence::Subscribe);
340 packet.setStatusText(reason);
341 return client()->sendPacket(packet);
342 }
343
344 ///
345 /// Removes a subscription to the given contact.
346 ///
347 /// As a result, the server will initiate a roster push, causing the
348 /// itemChanged() signal to be emitted.
349 ///
unsubscribe(const QString & bareJid,const QString & reason)350 bool QXmppRosterManager::unsubscribe(const QString &bareJid, const QString &reason)
351 {
352 QXmppPresence packet;
353 packet.setTo(QXmppUtils::jidToBareJid(bareJid));
354 packet.setType(QXmppPresence::Unsubscribe);
355 packet.setStatusText(reason);
356 return client()->sendPacket(packet);
357 }
358
359 ///
360 /// Function to get all the bareJids present in the roster.
361 ///
362 /// \return QStringList list of all the bareJids
363 ///
getRosterBareJids() const364 QStringList QXmppRosterManager::getRosterBareJids() const
365 {
366 return d->entries.keys();
367 }
368
369 ///
370 /// Returns the roster entry of the given bareJid. If the bareJid is not in the
371 /// database and empty QXmppRosterIq::Item will be returned.
372 ///
373 /// \param bareJid as a QString
374 ///
getRosterEntry(const QString & bareJid) const375 QXmppRosterIq::Item QXmppRosterManager::getRosterEntry(
376 const QString &bareJid) const
377 {
378 // will return blank entry if bareJid doesn't exist
379 if (d->entries.contains(bareJid))
380 return d->entries.value(bareJid);
381 return {};
382 }
383
384 ///
385 /// Get all the associated resources with the given bareJid.
386 ///
387 /// \param bareJid as a QString
388 /// \return list of associated resources as a QStringList
389 ///
getResources(const QString & bareJid) const390 QStringList QXmppRosterManager::getResources(const QString &bareJid) const
391 {
392 if (d->presences.contains(bareJid))
393 return d->presences[bareJid].keys();
394 return {};
395 }
396
397 ///
398 /// Get all the presences of all the resources of the given bareJid. A bareJid
399 /// can have multiple resources and each resource will have a presence
400 /// associated with it.
401 ///
402 /// \param bareJid as a QString
403 /// \return Map of resource and its respective presence QMap<QString, QXmppPresence>
404 ///
getAllPresencesForBareJid(const QString & bareJid) const405 QMap<QString, QXmppPresence> QXmppRosterManager::getAllPresencesForBareJid(
406 const QString &bareJid) const
407 {
408 if (d->presences.contains(bareJid))
409 return d->presences.value(bareJid);
410 return {};
411 }
412
413 ///
414 /// Get the presence of the given resource of the given bareJid.
415 ///
416 /// \param bareJid as a QString
417 /// \param resource as a QString
418 /// \return QXmppPresence
419 ///
getPresence(const QString & bareJid,const QString & resource) const420 QXmppPresence QXmppRosterManager::getPresence(const QString &bareJid,
421 const QString &resource) const
422 {
423 if (d->presences.contains(bareJid) && d->presences[bareJid].contains(resource)) {
424 return d->presences[bareJid][resource];
425 }
426
427 QXmppPresence presence;
428 presence.setType(QXmppPresence::Unavailable);
429 return presence;
430 }
431
432 ///
433 /// Function to check whether the roster has been received or not.
434 ///
435 /// On disconnecting this is reset to false if no stream management is used by
436 /// the client and so the stream cannot be resumed later.
437 ///
438 /// \return true if roster received else false
439 ///
isRosterReceived() const440 bool QXmppRosterManager::isRosterReceived() const
441 {
442 return d->isRosterReceived;
443 }
444