1 /*
2  * Copyright (C) 2004-2008 Jive Software. All rights reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.jivesoftware.openfire.roster;
18 
19 import org.dom4j.io.SAXReader;
20 import org.jivesoftware.openfire.SharedGroupException;
21 import org.jivesoftware.openfire.group.Group;
22 import org.jivesoftware.openfire.group.GroupManager;
23 import org.jivesoftware.openfire.group.GroupNotFoundException;
24 import org.jivesoftware.openfire.user.UserNameManager;
25 import org.jivesoftware.openfire.user.UserNotFoundException;
26 import org.jivesoftware.util.cache.CacheSizes;
27 import org.jivesoftware.util.cache.Cacheable;
28 import org.jivesoftware.util.cache.CannotCalculateSizeException;
29 import org.jivesoftware.util.cache.ExternalizableUtil;
30 import org.xmpp.packet.JID;
31 import org.xmpp.packet.Presence;
32 
33 import javax.xml.bind.Element;
34 import java.io.*;
35 import java.util.*;
36 
37 /**
38  * <p>Represents a single roster item for a User's Roster.</p>
39  * <p>The server doesn't need to know anything about roster groups so they are
40  * not stored with easy retrieval or manipulation in mind. The important data
41  * elements of a roster item (beyond the jid adddress of the roster entry) includes:</p>
42  * <ul>
43  * <li>nick   - A nickname for the user when used in this roster</li>
44  * <li>sub    - A subscription type: to, from, none, both</li>
45  * <li>ask    - An optional subscription ask status: subscribe, unsubscribe</li>
46  * <li>groups - A list of groups to organize roster entries under (e.g. friends, co-workers, etc)</li>
47  * </ul>
48  *
49  * @author Gaston Dombiak
50  */
51 public class RosterItem implements Cacheable, Externalizable {
52 
53     public enum SubType {
54 
55         /**
56          * Indicates the roster item should be removed.
57          */
58         REMOVE(-1),
59         /**
60          * No subscription is established.
61          */
62         NONE(0),
63         /**
64          * The roster owner has a subscription to the roster item's presence.
65          */
66         TO(1),
67         /**
68          * The roster item has a subscription to the roster owner's presence.
69          */
70         FROM(2),
71         /**
72          * The roster item and owner have a mutual subscription.
73          */
74         BOTH(3);
75 
76         private final int value;
77 
SubType(int value)78         SubType(int value) {
79             this.value = value;
80         }
81 
getValue()82         public int getValue() {
83             return value;
84         }
85 
getName()86         public String getName() {
87             return name().toLowerCase();
88         }
89 
getTypeFromInt(int value)90         public static SubType getTypeFromInt(int value) {
91             for (SubType subType : values()) {
92                 if (subType.value == value) {
93                     return subType;
94                 }
95             }
96             return null;
97         }
98     }
99 
100     public enum AskType {
101 
102         /**
103          * The roster item has no pending subscription requests.
104          */
105         NONE(-1),
106         /**
107          * The roster item has been asked for permission to subscribe to their presence
108          * but no response has been received.
109          */
110         SUBSCRIBE(0),
111         /**
112          * The roster owner has asked to the roster item to unsubscribe from it's
113          * presence but has not received confirmation.
114          */
115         UNSUBSCRIBE(1);
116 
117         private final int value;
118 
AskType(int value)119         AskType(int value) {
120             this.value = value;
121 
122         }
123 
getValue()124         public int getValue() {
125             return value;
126         }
127 
getTypeFromInt(int value)128         public static AskType getTypeFromInt(int value) {
129             for (AskType askType : values()) {
130                 if (askType.value == value) {
131                     return askType;
132                 }
133             }
134             return null;
135         }
136     }
137 
138     public enum RecvType {
139 
140         /**
141          * There are no subscriptions that have been received but not presented to the user.
142          */
143         NONE(-1),
144         /**
145          * The server has received a subscribe request, but has not forwarded it to the user.
146          */
147         SUBSCRIBE(1),
148         /**
149          * The server has received an unsubscribe request, but has not forwarded it to the user.
150          */
151         UNSUBSCRIBE(2);
152 
153         private final int value;
154 
RecvType(int value)155         RecvType(int value) {
156             this.value = value;
157         }
158 
getValue()159         public int getValue() {
160             return value;
161         }
162 
getTypeFromInt(int value)163         public static RecvType getTypeFromInt(int value) {
164             for (RecvType recvType : values()) {
165                 if (recvType.value == value) {
166                     return recvType;
167                 }
168             }
169             return null;
170         }
171     }
172 
173     /**
174      * <p>Indicates the roster item should be removed.</p>
175      */
176     public static final SubType SUB_REMOVE = SubType.REMOVE;
177     /**
178      * <p>No subscription is established.</p>
179      */
180     public static final SubType SUB_NONE = SubType.NONE;
181     /**
182      * <p>The roster owner has a subscription to the roster item's presence.</p>
183      */
184     public static final SubType SUB_TO = SubType.TO;
185     /**
186      * <p>The roster item has a subscription to the roster owner's presence.</p>
187      */
188     public static final SubType SUB_FROM = SubType.FROM;
189     /**
190      * <p>The roster item and owner have a mutual subscription.</p>
191      */
192     public static final SubType SUB_BOTH = SubType.BOTH;
193 
194     /**
195      * <p>The roster item has no pending subscription requests.</p>
196      */
197     public static final AskType ASK_NONE = AskType.NONE;
198     /**
199      * <p>The roster item has been asked for permission to subscribe to their presence
200      * but no response has been received.</p>
201      */
202     public static final AskType ASK_SUBSCRIBE = AskType.SUBSCRIBE;
203     /**
204      * <p>The roster owner has asked to the roster item to unsubscribe from it's
205      * presence but has not received confirmation.</p>
206      */
207     public static final AskType ASK_UNSUBSCRIBE = AskType.UNSUBSCRIBE;
208 
209     /**
210      * <p>There are no subscriptions that have been received but not presented to the user.</p>
211      */
212     public static final RecvType RECV_NONE = RecvType.NONE;
213     /**
214      * <p>The server has received a subscribe request, but has not forwarded it to the user.</p>
215      */
216     public static final RecvType RECV_SUBSCRIBE = RecvType.SUBSCRIBE;
217     /**
218      * <p>The server has received an unsubscribe request, but has not forwarded it to the user.</p>
219      */
220     public static final RecvType RECV_UNSUBSCRIBE = RecvType.UNSUBSCRIBE;
221 
222     protected RecvType recvStatus;
223     protected JID jid;
224     protected String nickname;
225     protected List<String> groups;
226     protected Set<String> sharedGroups = new HashSet<>();
227     protected Set<String> invisibleSharedGroups = new HashSet<>();
228     protected SubType subStatus;
229     protected AskType askStatus;
230     // Presence for type='subscribe'.
231     protected Presence subscribeStanza;
232     /**
233      * Holds the ID that uniquely identifies the roster in the backend store. A value of
234      * zero means that the roster item is not persistent.
235      */
236     private long rosterID;
237 
238     /**
239      * Constructor added for Externalizable. Do not use this constructor.
240      */
RosterItem()241     public RosterItem() {
242     }
243 
RosterItem(long id, JID jid, SubType subStatus, AskType askStatus, RecvType recvStatus, String nickname, List<String> groups)244     public RosterItem(long id,
245                                 JID jid,
246                                 SubType subStatus,
247                                 AskType askStatus,
248                                 RecvType recvStatus,
249                                 String nickname,
250                                 List<String> groups) {
251         this(jid, subStatus, askStatus, recvStatus, nickname, groups);
252         this.rosterID = id;
253     }
254 
RosterItem(JID jid, SubType subStatus, AskType askStatus, RecvType recvStatus, String nickname, List<String> groups)255     public RosterItem(JID jid,
256                            SubType subStatus,
257                            AskType askStatus,
258                            RecvType recvStatus,
259                            String nickname,
260                            List<String> groups) {
261         this.jid = jid;
262         this.subStatus = subStatus;
263         this.askStatus = askStatus;
264         this.recvStatus = recvStatus;
265         this.nickname = nickname;
266         this.groups = new LinkedList<>();
267         if (groups != null) {
268             for (String group : groups) {
269                 this.groups.add(group);
270             }
271         }
272     }
273 
274     /**
275      * Create a roster item from the data in another one.
276      *
277      * @param item Item that contains the info of the roster item.
278      */
RosterItem(org.xmpp.packet.Roster.Item item)279     public RosterItem(org.xmpp.packet.Roster.Item item) {
280         this(item.getJID(),
281                 getSubType(item),
282                 getAskStatus(item),
283                 RosterItem.RECV_NONE,
284                 item.getName(),
285                 new LinkedList<>(item.getGroups()));
286     }
287 
getAskStatus(org.xmpp.packet.Roster.Item item)288     public static RosterItem.AskType getAskStatus(org.xmpp.packet.Roster.Item item) {
289         if (item.getAsk() == org.xmpp.packet.Roster.Ask.subscribe) {
290             return RosterItem.ASK_SUBSCRIBE;
291         }
292         else if (item.getAsk() == org.xmpp.packet.Roster.Ask.unsubscribe) {
293             return RosterItem.ASK_UNSUBSCRIBE;
294         }
295         else {
296             return RosterItem.ASK_NONE;
297         }
298     }
299 
getSubType(org.xmpp.packet.Roster.Item item)300     public static RosterItem.SubType getSubType(org.xmpp.packet.Roster.Item item) {
301         if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.to) {
302             return RosterItem.SUB_TO;
303         }
304         else if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.from) {
305             return RosterItem.SUB_FROM;
306         }
307         else if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.both) {
308             return RosterItem.SUB_BOTH;
309         }
310         else if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.remove) {
311             return RosterItem.SUB_REMOVE;
312         }
313         else {
314             return RosterItem.SUB_NONE;
315         }
316     }
317 
318     /**
319      * <p>Obtain the current subscription status of the item.</p>
320      *
321      * @return The subscription status of the item
322      */
getSubStatus()323     public SubType getSubStatus() {
324         return subStatus;
325     }
326 
327     /**
328      * <p>Set the current subscription status of the item.</p>
329      *
330      * @param subStatus The subscription status of the item
331      */
setSubStatus(SubType subStatus)332     public void setSubStatus(SubType subStatus) {
333         // Optimization: Load user only if we need to set the nickname of the roster item
334         if ("".equals(nickname) && (subStatus == SUB_BOTH || subStatus == SUB_TO)) {
335             try {
336                 nickname = UserNameManager.getUserName(jid);
337             }
338             catch (UserNotFoundException e) {
339                 // Do nothing
340             }
341         }
342         this.subStatus = subStatus;
343     }
344 
345     /**
346      * <p>Obtain the current ask status of the item.</p>
347      *
348      * @return The ask status of the item
349      */
getAskStatus()350     public AskType getAskStatus() {
351         if (isShared()) {
352             // Redefine the ask status since the item belongs to a shared group
353             return ASK_NONE;
354         }
355         else {
356             return askStatus;
357         }
358     }
359 
360     /**
361      * <p>Set the current ask status of the item.</p>
362      *
363      * @param askStatus The ask status of the item
364      */
setAskStatus(AskType askStatus)365     public void setAskStatus(AskType askStatus) {
366         this.askStatus = askStatus;
367     }
368 
369     /**
370      * <p>Obtain the current recv status of the item.</p>
371      *
372      * @return The recv status of the item
373      */
getRecvStatus()374     public RecvType getRecvStatus() {
375         return recvStatus;
376     }
377 
378     /**
379      * <p>Set the current recv status of the item.</p>
380      *
381      * @param recvStatus The recv status of the item
382      */
setRecvStatus(RecvType recvStatus)383     public void setRecvStatus(RecvType recvStatus) {
384         this.recvStatus = recvStatus;
385     }
386 
387     /**
388      * <p>Obtain the address of the item.</p>
389      *
390      * @return The address of the item
391      */
getJid()392     public JID getJid() {
393         return jid;
394     }
395 
396     /**
397      * <p>Obtain the current nickname for the item.</p>
398      *
399      * @return The subscription status of the item
400      */
getNickname()401     public String getNickname() {
402         return nickname;
403     }
404 
405     /**
406      * <p>Set the current nickname for the item.</p>
407      *
408      * @param nickname The subscription status of the item
409      */
setNickname(String nickname)410     public void setNickname(String nickname) {
411         this.nickname = nickname;
412     }
413 
414     /**
415      * Returns the groups for the item. Shared groups won't be included in the answer.
416      *
417      * @return The groups for the item.
418      */
getGroups()419     public List<String> getGroups() {
420         return groups;
421     }
422 
423     /**
424      * Set the current groups for the item.
425      *
426      * @param groups The new lists of groups the item belongs to.
427      * @throws org.jivesoftware.openfire.SharedGroupException if trying to remove shared group.
428      */
setGroups(List<String> groups)429     public void setGroups(List<String> groups) throws SharedGroupException {
430         if (groups == null) {
431             this.groups = new LinkedList<>();
432         }
433         else {
434             // Raise an error if the user is trying to remove the item from a shared group
435             for (Group group: getSharedGroups()) {
436                 // Get the display name of the group
437                 String groupName = group.getProperties().get("sharedRoster.displayName");
438                 // Check if the group has been removed from the new groups list
439                 if (!groups.contains(groupName)) {
440                     throw new SharedGroupException("Cannot remove item from shared group");
441                 }
442             }
443 
444             // Remove shared groups from the param
445             for (Iterator<String> it=groups.iterator(); it.hasNext();) {
446                 String groupName = it.next();
447                 try {
448                     Group group = GroupManager.getInstance().getGroup(groupName);
449                     if (RosterManager.isSharedGroup(group)) {
450                         it.remove();
451                     }
452                 } catch (GroupNotFoundException e) {
453                     // Check now if there is a group whose display name matches the requested group
454                     Collection<Group> groupsWithProp = GroupManager
455                             .getInstance()
456                             .search("sharedRoster.displayName", groupName);
457                     Iterator<Group> itr = groupsWithProp.iterator();
458                     while(itr.hasNext()) {
459                         Group group = itr.next();
460                         if (RosterManager.isSharedGroup(group)) {
461                             it.remove();
462                         }
463                     }
464                 }
465             }
466             this.groups = groups;
467         }
468     }
469 
470     /**
471      * Returns the shared groups for the item.
472      *
473      * @return The shared groups this item belongs to.
474      */
getSharedGroups()475     public Collection<Group> getSharedGroups() {
476         Collection<Group> groups = new ArrayList<>(sharedGroups.size());
477         for (String groupName : sharedGroups) {
478             try {
479                 groups.add(GroupManager.getInstance().getGroup(groupName));
480             }
481             catch (GroupNotFoundException e) {
482                 // Do nothing
483             }
484         }
485         return groups;
486     }
487 
488     /**
489      * Returns the invisible shared groups for the item. These groups are for internal use
490      * and help track the reason why a roster item has a presence subscription of type FROM
491      * when using shared groups.
492      *
493      * @return The shared groups this item belongs to.
494      */
getInvisibleSharedGroups()495     public Collection<Group> getInvisibleSharedGroups() {
496         Collection<Group> groups = new ArrayList<>(invisibleSharedGroups.size());
497         for (String groupName : invisibleSharedGroups) {
498             try {
499                 groups.add(GroupManager.getInstance().getGroup(groupName));
500             }
501             catch (GroupNotFoundException e) {
502                 // Do nothing
503             }
504         }
505         return groups;
506     }
507 
getInvisibleSharedGroupsNames()508     Set<String> getInvisibleSharedGroupsNames() {
509         return invisibleSharedGroups;
510     }
511 
setInvisibleSharedGroupsNames(Set<String> groupsNames)512     void setInvisibleSharedGroupsNames(Set<String> groupsNames) {
513         invisibleSharedGroups = groupsNames;
514     }
515 
516     /**
517      * Adds a new group to the shared groups list.
518      *
519      * @param sharedGroup The shared group to add to the list of shared groups.
520      */
addSharedGroup(Group sharedGroup)521     public void addSharedGroup(Group sharedGroup) {
522         sharedGroups.add(sharedGroup.getName());
523         invisibleSharedGroups.remove(sharedGroup.getName());
524     }
525 
526     /**
527      * Adds a new group to the list shared groups that won't be sent to the user. These groups
528      * are for internal use and help track the reason why a roster item has a presence
529      * subscription of type FROM when using shared groups.
530      *
531      * @param sharedGroup The shared group to add to the list of shared groups.
532      */
addInvisibleSharedGroup(Group sharedGroup)533     public void addInvisibleSharedGroup(Group sharedGroup) {
534         invisibleSharedGroups.add(sharedGroup.getName());
535     }
536 
537     /**
538      * Removes a group from the shared groups list.
539      *
540      * @param sharedGroup The shared group to remove from the list of shared groups.
541      */
removeSharedGroup(Group sharedGroup)542     public void removeSharedGroup(Group sharedGroup) {
543         sharedGroups.remove(sharedGroup.getName());
544         invisibleSharedGroups.remove(sharedGroup.getName());
545     }
546 
547     /**
548      * Returns true if this item belongs to a shared group. Return true even if the item belongs
549      * to a personal group and a shared group.
550      *
551      * @return true if this item belongs to a shared group.
552      */
isShared()553     public boolean isShared() {
554         return !sharedGroups.isEmpty() || !invisibleSharedGroups.isEmpty();
555     }
556 
557     /**
558      * Returns true if this item belongs ONLY to shared groups. This means that the the item is
559      * considered to be "only shared" if it doesn't belong to a personal group but only to shared
560      * groups.
561      *
562      * @return true if this item belongs ONLY to shared groups.
563      */
isOnlyShared()564     public boolean isOnlyShared() {
565         return isShared() && groups.isEmpty();
566     }
567 
568     /**
569      * Returns the roster ID associated with this particular roster item. A value of zero
570      * means that the roster item is not being persisted in the backend store.<p>
571      *
572      * Databases can use the roster ID as the key in locating roster items.
573      *
574      * @return The roster ID
575      */
getID()576     public long getID() {
577         return rosterID;
578     }
579 
580     /**
581      * Sets the roster ID associated with this particular roster item. A value of zero
582      * means that the roster item is not being persisted in the backend store.<p>
583      *
584      * Databases can use the roster ID as the key in locating roster items.
585      *
586      * @param rosterID The roster ID.
587      */
setID(long rosterID)588     public void setID(long rosterID) {
589         this.rosterID = rosterID;
590     }
591 
592     /**
593      * <p>Update the cached item as a copy of the given item.</p>
594      * <p>A convenience for getting the item and setting each attribute.</p>
595      *
596      * @param item The item who's settings will be copied into the cached copy
597      * @throws org.jivesoftware.openfire.SharedGroupException if trying to remove shared group.
598      */
setAsCopyOf(org.xmpp.packet.Roster.Item item)599     public void setAsCopyOf(org.xmpp.packet.Roster.Item item) throws SharedGroupException {
600         setGroups(new LinkedList<>(item.getGroups()));
601         setNickname(item.getName());
602     }
603 
604     /*
605      * (non-Javadoc)
606      *
607      * @see org.jivesoftware.util.cache.Cacheable#getCachedSize()
608      */
609     @Override
getCachedSize()610     public int getCachedSize() throws CannotCalculateSizeException {
611         int size = jid.toBareJID().length();
612         size += CacheSizes.sizeOfString(nickname);
613         size += CacheSizes.sizeOfCollection(groups);
614         size += CacheSizes.sizeOfCollection(invisibleSharedGroups);
615         size += CacheSizes.sizeOfCollection(sharedGroups);
616         size += CacheSizes.sizeOfInt(); // subStatus
617         size += CacheSizes.sizeOfInt(); // askStatus
618         size += CacheSizes.sizeOfInt(); // recvStatus
619         size += CacheSizes.sizeOfLong(); // id
620         if (subscribeStanza != null) {
621             size += CacheSizes.sizeOfString(subscribeStanza.toXML());
622         }
623         return size;
624     }
625 
626     @Override
writeExternal(ObjectOutput out)627     public void writeExternal(ObjectOutput out) throws IOException {
628         ExternalizableUtil.getInstance().writeSerializable(out, jid);
629         ExternalizableUtil.getInstance().writeBoolean(out, nickname != null);
630         if (nickname != null) {
631             ExternalizableUtil.getInstance().writeSafeUTF(out, nickname);
632         }
633         ExternalizableUtil.getInstance().writeStrings(out, groups);
634         ExternalizableUtil.getInstance().writeStrings(out, sharedGroups);
635         ExternalizableUtil.getInstance().writeStrings(out, invisibleSharedGroups);
636         ExternalizableUtil.getInstance().writeInt(out, recvStatus.getValue());
637         ExternalizableUtil.getInstance().writeInt(out, subStatus.getValue());
638         ExternalizableUtil.getInstance().writeInt(out, askStatus.getValue());
639         ExternalizableUtil.getInstance().writeLong(out, rosterID);
640         ExternalizableUtil.getInstance().writeBoolean(out, subscribeStanza != null);
641         if (subscribeStanza != null) {
642             ExternalizableUtil.getInstance().writeXML(out, subscribeStanza.getElement());
643         }
644     }
645 
646     @Override
readExternal(ObjectInput in)647     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
648         jid = (JID) ExternalizableUtil.getInstance().readSerializable(in);
649         if (ExternalizableUtil.getInstance().readBoolean(in)) {
650             nickname = ExternalizableUtil.getInstance().readSafeUTF(in);
651         }
652         this.groups = new LinkedList<>();
653         ExternalizableUtil.getInstance().readStrings(in, groups);
654         ExternalizableUtil.getInstance().readStrings(in, sharedGroups);
655         ExternalizableUtil.getInstance().readStrings(in, invisibleSharedGroups);
656         recvStatus = RecvType.getTypeFromInt(ExternalizableUtil.getInstance().readInt(in));
657         subStatus = SubType.getTypeFromInt(ExternalizableUtil.getInstance().readInt(in));
658         askStatus = AskType.getTypeFromInt(ExternalizableUtil.getInstance().readInt(in));
659         rosterID = ExternalizableUtil.getInstance().readLong(in);
660         if (ExternalizableUtil.getInstance().readBoolean(in)) {
661             subscribeStanza = new Presence(ExternalizableUtil.getInstance().readXML(in));
662         }
663     }
664 
getSubscribeStanza()665     public Presence getSubscribeStanza() throws IllegalStateException {
666         if (recvStatus != RecvType.SUBSCRIBE) {
667             throw new IllegalStateException("Wrong receive state");
668         }
669         if (subscribeStanza == null) {
670             Presence presence = new Presence();
671             presence.setFrom(jid);
672             presence.setType(Presence.Type.subscribe);
673             return presence;
674         } else {
675             return subscribeStanza;
676         }
677     }
678 
getStoredSubscribeStanza()679     public Presence getStoredSubscribeStanza() {
680         return subscribeStanza;
681     }
682 
setStoredSubscribeStanza(Presence subscribeStanza)683     public void setStoredSubscribeStanza(Presence subscribeStanza) {
684         this.subscribeStanza = subscribeStanza;
685     }
686 }
687