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