Changeset 172:ea2774e1c71c


Ignore:
Timestamp:
May 9, 2012, 2:24:28 PM (8 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Message:

Clean up of RosterItem? and RosterClientProtocol?.

RosterItem:

  • Renamed attributes jid and ask to entity and pendingOut respectively.
  • Can represent roster items to be removed or that have been removed.
  • Now has fromElement and toElement methods.

RosterRequest is a new class to represent roster request stanzas.

RosterClientProtocol:

  • Roster returned from getRoster is now indexed by JIDs (instead of the unicode representation of the JID).
  • Outgoing requests are now done using RosterRequest.
  • onRosterSet and onRosterRemove are deprecated in favor of setReceived and removeReceived, respectively. These are called with a RosterRequest to have access to addressing and roster version information.

RosterPushIgnored can be raised to return a service-unavailable stanza
error for unwanted pushes.

This also fixes a problem with checking the sender address for roster pushes.

Author: ralphm.
Fixes: #71.

Files:
4 added
2 edited

Legend:

Unmodified
Added
Removed
  • wokkel/test/test_xmppim.py

    r96 r172  
    88from twisted.internet import defer
    99from twisted.trial import unittest
     10from twisted.words.protocols.jabber import error
    1011from twisted.words.protocols.jabber.jid import JID
    1112from twisted.words.protocols.jabber.xmlstream import toResponse
     
    1415from wokkel import xmppim
    1516from wokkel.generic import ErrorStanza, parseXml
    16 from wokkel.test.helpers import XmlStreamStub
     17from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
    1718
    1819NS_XML = 'http://www.w3.org/XML/1998/namespace'
     
    450451
    451452
    452 class RosterClientProtocolTest(unittest.TestCase):
     453class RosterItemTest(unittest.TestCase):
     454    """
     455    Tests for L{xmppim.RosterItem}.
     456    """
     457
     458    def test_toElement(self):
     459        """
     460        A roster item has the correct namespace/name, lacks unset attributes.
     461        """
     462        item = xmppim.RosterItem(JID('user@example.org'))
     463        element = item.toElement()
     464        self.assertEqual('item', element.name)
     465        self.assertEqual(NS_ROSTER, element.uri)
     466        self.assertFalse(element.hasAttribute('subscription'))
     467        self.assertFalse(element.hasAttribute('ask'))
     468        self.assertEqual(u"", element.getAttribute('name', u""))
     469        self.assertFalse(element.hasAttribute('approved'))
     470        self.assertEquals(0, len(list(element.elements())))
     471
     472
     473    def test_toElementMinimal(self):
     474        """
     475        A bare roster item only has a jid attribute.
     476        """
     477        item = xmppim.RosterItem(JID('user@example.org'))
     478        element = item.toElement()
     479        self.assertEqual(u'user@example.org', element.getAttribute('jid'))
     480
     481
     482    def test_toElementSubscriptionNone(self):
     483        """
     484        A roster item with no subscription has no subscription attribute.
     485        """
     486        item = xmppim.RosterItem(JID('user@example.org'),
     487                                 subscriptionTo=False,
     488                                 subscriptionFrom=False)
     489        element = item.toElement()
     490        self.assertIdentical(None, element.getAttribute('subscription'))
     491
     492
     493    def test_toElementSubscriptionTo(self):
     494        """
     495        A roster item with subscriptionTo set has subscription 'to'.
     496        """
     497        item = xmppim.RosterItem(JID('user@example.org'),
     498                                 subscriptionTo=True,
     499                                 subscriptionFrom=False)
     500        element = item.toElement()
     501        self.assertEqual('to', element.getAttribute('subscription'))
     502
     503
     504    def test_toElementSubscriptionFrom(self):
     505        """
     506        A roster item with subscriptionFrom set has subscription 'to'.
     507        """
     508        item = xmppim.RosterItem(JID('user@example.org'),
     509                                 subscriptionTo=False,
     510                                 subscriptionFrom=True)
     511        element = item.toElement()
     512        self.assertEqual('from', element.getAttribute('subscription'))
     513
     514
     515    def test_toElementSubscriptionBoth(self):
     516        """
     517        A roster item with mutual subscription has subscription 'both'.
     518        """
     519        item = xmppim.RosterItem(JID('user@example.org'),
     520                                 subscriptionTo=True,
     521                                 subscriptionFrom=True)
     522        element = item.toElement()
     523        self.assertEqual('both', element.getAttribute('subscription'))
     524
     525
     526    def test_toElementSubscriptionRemove(self):
     527        """
     528        A roster item with remove set has subscription 'remove'.
     529        """
     530        item = xmppim.RosterItem(JID('user@example.org'))
     531        item.remove = True
     532        element = item.toElement()
     533        self.assertEqual('remove', element.getAttribute('subscription'))
     534
     535
     536    def test_toElementAsk(self):
     537        """
     538        A roster item with pendingOut set has subscription 'ask'.
     539        """
     540        item = xmppim.RosterItem(JID('user@example.org'))
     541        item.pendingOut = True
     542        element = item.toElement()
     543        self.assertEqual('subscribe', element.getAttribute('ask'))
     544
     545
     546    def test_toElementName(self):
     547        """
     548        A roster item's name is rendered to the 'name' attribute.
     549        """
     550        item = xmppim.RosterItem(JID('user@example.org'),
     551                                 name='Joe User')
     552        element = item.toElement()
     553        self.assertEqual(u'Joe User', element.getAttribute('name'))
     554
     555
     556    def test_toElementGroups(self):
     557        """
     558        A roster item's groups are rendered as 'group' child elements.
     559        """
     560        groups = set(['Friends', 'Jabber'])
     561        item = xmppim.RosterItem(JID('user@example.org'),
     562                                 groups=groups)
     563
     564        element = item.toElement()
     565        foundGroups = set()
     566        for child in element.elements():
     567            if child.uri == NS_ROSTER and child.name == 'group':
     568                foundGroups.add(unicode(child))
     569
     570        self.assertEqual(groups, foundGroups)
     571
     572
     573    def test_toElementApproved(self):
     574        """
     575        A pre-approved subscription for a roster item has an 'approved' flag.
     576        """
     577        item = xmppim.RosterItem(JID('user@example.org'))
     578        item.approved = True
     579        element = item.toElement()
     580        self.assertEqual(u'true', element.getAttribute('approved'))
     581
     582
     583    def test_fromElementMinimal(self):
     584        """
     585        A minimal roster item has a reference to the JID of the contact.
     586        """
     587
     588        xml = """
     589            <item xmlns="jabber:iq:roster"
     590                  jid="test@example.org"/>
     591        """
     592
     593        item = xmppim.RosterItem.fromElement(parseXml(xml))
     594        self.assertEqual(JID(u"test@example.org"), item.entity)
     595        self.assertEqual(u"", item.name)
     596        self.assertFalse(item.subscriptionTo)
     597        self.assertFalse(item.subscriptionFrom)
     598        self.assertFalse(item.pendingOut)
     599        self.assertFalse(item.approved)
     600        self.assertEqual(set(), item.groups)
     601
     602
     603    def test_fromElementName(self):
     604        """
     605        A roster item may have an optional name.
     606        """
     607
     608        xml = """
     609            <item xmlns="jabber:iq:roster"
     610                  jid="test@example.org"
     611                  name="Test User"/>
     612        """
     613
     614        item = xmppim.RosterItem.fromElement(parseXml(xml))
     615        self.assertEqual(u"Test User", item.name)
     616
     617
     618    def test_fromElementGroups(self):
     619        """
     620        A roster item may have one or more groups.
     621        """
     622
     623        xml = """
     624            <item xmlns="jabber:iq:roster"
     625                  jid="test@example.org">
     626              <group>Friends</group>
     627              <group>Twisted</group>
     628            </item>
     629        """
     630
     631        item = xmppim.RosterItem.fromElement(parseXml(xml))
     632        self.assertIn(u"Twisted", item.groups)
     633        self.assertIn(u"Friends", item.groups)
     634
     635
     636    def test_fromElementSubscriptionNone(self):
     637        """
     638        Subscription 'none' sets both attributes to False.
     639        """
     640
     641        xml = """
     642            <item xmlns="jabber:iq:roster"
     643                  jid="test@example.org"
     644                  subscription="none"/>
     645        """
     646
     647        item = xmppim.RosterItem.fromElement(parseXml(xml))
     648        self.assertFalse(item.remove)
     649        self.assertFalse(item.subscriptionTo)
     650        self.assertFalse(item.subscriptionFrom)
     651
     652
     653    def test_fromElementSubscriptionTo(self):
     654        """
     655        Subscription 'to' sets the corresponding attribute to True.
     656        """
     657
     658        xml = """
     659            <item xmlns="jabber:iq:roster"
     660                  jid="test@example.org"
     661                  subscription="to"/>
     662        """
     663
     664        item = xmppim.RosterItem.fromElement(parseXml(xml))
     665        self.assertFalse(item.remove)
     666        self.assertTrue(item.subscriptionTo)
     667        self.assertFalse(item.subscriptionFrom)
     668
     669
     670    def test_fromElementSubscriptionFrom(self):
     671        """
     672        Subscription 'from' sets the corresponding attribute to True.
     673        """
     674
     675        xml = """
     676            <item xmlns="jabber:iq:roster"
     677                  jid="test@example.org"
     678                  subscription="from"/>
     679        """
     680
     681        item = xmppim.RosterItem.fromElement(parseXml(xml))
     682        self.assertFalse(item.remove)
     683        self.assertFalse(item.subscriptionTo)
     684        self.assertTrue(item.subscriptionFrom)
     685
     686
     687    def test_fromElementSubscriptionBoth(self):
     688        """
     689        Subscription 'both' sets both attributes to True.
     690        """
     691
     692        xml = """
     693            <item xmlns="jabber:iq:roster"
     694                  jid="test@example.org"
     695                  subscription="both"/>
     696        """
     697
     698        item = xmppim.RosterItem.fromElement(parseXml(xml))
     699        self.assertFalse(item.remove)
     700        self.assertTrue(item.subscriptionTo)
     701        self.assertTrue(item.subscriptionFrom)
     702
     703
     704    def test_fromElementSubscriptionRemove(self):
     705        """
     706        Subscription 'remove' sets the remove attribute.
     707        """
     708
     709        xml = """
     710            <item xmlns="jabber:iq:roster"
     711                  jid="test@example.org"
     712                  subscription="remove"/>
     713        """
     714
     715        item = xmppim.RosterItem.fromElement(parseXml(xml))
     716        self.assertTrue(item.remove)
     717
     718
     719    def test_fromElementPendingOut(self):
     720        """
     721        The ask attribute, if set to 'subscription', means pending out.
     722        """
     723
     724        xml = """
     725            <item xmlns="jabber:iq:roster"
     726                  jid="test@example.org"
     727                  ask="subscribe"/>
     728        """
     729
     730        item = xmppim.RosterItem.fromElement(parseXml(xml))
     731        self.assertTrue(item.pendingOut)
     732
     733
     734    def test_fromElementApprovedTrue(self):
     735        """
     736        The approved attribute (true) signals a pre-approved subscription.
     737        """
     738
     739        xml = """
     740            <item xmlns="jabber:iq:roster"
     741                  jid="test@example.org"
     742                  approved="true"/>
     743        """
     744
     745        item = xmppim.RosterItem.fromElement(parseXml(xml))
     746        self.assertTrue(item.approved)
     747
     748
     749    def test_fromElementApproved1(self):
     750        """
     751        The approved attribute (1) signals a pre-approved subscription.
     752        """
     753
     754        xml = """
     755            <item xmlns="jabber:iq:roster"
     756                  jid="test@example.org"
     757                  approved="1"/>
     758        """
     759
     760        item = xmppim.RosterItem.fromElement(parseXml(xml))
     761        self.assertTrue(item.approved)
     762
     763
     764    def test_jidDeprecationGet(self):
     765        """
     766        Getting the jid attribute works as entity and warns deprecation.
     767        """
     768        item = xmppim.RosterItem(JID('user@example.org'))
     769        entity = self.assertWarns(DeprecationWarning,
     770                                  "wokkel.xmppim.RosterItem.jid is deprecated. "
     771                                  "Use RosterItem.entity instead.",
     772                                  xmppim.__file__,
     773                                  getattr, item, 'jid')
     774        self.assertIdentical(entity, item.entity)
     775
     776
     777    def test_jidDeprecationSet(self):
     778        """
     779        Setting the jid attribute works as entity and warns deprecation.
     780        """
     781        item = xmppim.RosterItem(JID('user@example.org'))
     782        self.assertWarns(DeprecationWarning,
     783                         "wokkel.xmppim.RosterItem.jid is deprecated. "
     784                         "Use RosterItem.entity instead.",
     785                         xmppim.__file__,
     786                         setattr, item, 'jid',
     787                         JID('other@example.org'))
     788        self.assertEqual(JID('other@example.org'), item.entity)
     789
     790
     791    def test_askDeprecationGet(self):
     792        """
     793        Getting the ask attribute works as entity and warns deprecation.
     794        """
     795        item = xmppim.RosterItem(JID('user@example.org'))
     796        item.pendingOut = True
     797        ask = self.assertWarns(DeprecationWarning,
     798                               "wokkel.xmppim.RosterItem.ask is deprecated. "
     799                               "Use RosterItem.pendingOut instead.",
     800                               xmppim.__file__,
     801                               getattr, item, 'ask')
     802        self.assertTrue(ask)
     803
     804
     805    def test_askDeprecationSet(self):
     806        """
     807        Setting the ask attribute works as entity and warns deprecation.
     808        """
     809        item = xmppim.RosterItem(JID('user@example.org'))
     810        self.assertWarns(DeprecationWarning,
     811                         "wokkel.xmppim.RosterItem.ask is deprecated. "
     812                         "Use RosterItem.pendingOut instead.",
     813                         xmppim.__file__,
     814                         setattr, item, 'ask',
     815                         True)
     816        self.assertTrue(item.pendingOut)
     817
     818
     819
     820class RosterRequestTest(unittest.TestCase):
     821    """
     822    Tests for L{xmppim.RosterRequest}.
     823    """
     824
     825    def test_fromElement(self):
     826        """
     827        A bare roster request is parsed and missing information is None.
     828        """
     829        xml = """
     830            <iq type='get' to='this@example.org/Home' from='this@example.org'>
     831              <query xmlns='jabber:iq:roster'/>
     832            </iq>
     833        """
     834
     835        request = xmppim.RosterRequest.fromElement(parseXml(xml))
     836        self.assertEqual('get', request.stanzaType)
     837        self.assertEqual(JID('this@example.org/Home'), request.recipient)
     838        self.assertEqual(JID('this@example.org'), request.sender)
     839        self.assertEqual(None, request.item)
     840
     841
     842    def test_fromElementItem(self):
     843        """
     844        If an item is present, parse it and put it in the request item.
     845        """
     846        xml = """
     847            <iq type='set' to='this@example.org/Home' from='this@example.org'>
     848              <query xmlns='jabber:iq:roster'>
     849                <item jid='user@example.org'/>
     850              </query>
     851            </iq>
     852        """
     853
     854        request = xmppim.RosterRequest.fromElement(parseXml(xml))
     855        self.assertNotIdentical(None, request.item)
     856        self.assertEqual(JID('user@example.org'), request.item.entity)
     857
     858
     859    def test_toElement(self):
     860        """
     861        A roster request has a query element in the roster namespace.
     862        """
     863        request = xmppim.RosterRequest()
     864        element = request.toElement()
     865        children = element.elements()
     866        child = children.next()
     867        self.assertEqual(NS_ROSTER, child.uri)
     868        self.assertEqual('query', child.name)
     869
     870
     871    def test_toElementItem(self):
     872        """
     873        If an item is set, it is rendered as a child of the query.
     874        """
     875        request = xmppim.RosterRequest()
     876        request.item = xmppim.RosterItem(JID('user@example.org'))
     877        element = request.toElement()
     878        children = element.query.elements()
     879        child = children.next()
     880        self.assertEqual(NS_ROSTER, child.uri)
     881        self.assertEqual('item', child.name)
     882
     883
     884
     885class FakeClient(object):
     886    """
     887    Fake client stream manager for roster tests.
     888    """
     889
     890    def __init__(self, xmlstream, jid):
     891        self.xmlstream = xmlstream
     892        self.jid = jid
     893
     894
     895    def request(self, request):
     896        element = request.toElement()
     897        self.xmlstream.send(element)
     898        return defer.Deferred()
     899
     900
     901    def addHandler(self, handler):
     902        handler.makeConnection(self.xmlstream)
     903        handler.connectionInitialized()
     904
     905
     906
     907class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
    453908    """
    454909    Tests for L{xmppim.RosterClientProtocol}.
     
    457912    def setUp(self):
    458913        self.stub = XmlStreamStub()
    459         self.protocol = xmppim.RosterClientProtocol()
    460         self.protocol.xmlstream = self.stub.xmlstream
    461         self.protocol.connectionInitialized()
     914        self.client = FakeClient(self.stub.xmlstream, JID('this@example.org'))
     915        self.service = xmppim.RosterClientProtocol()
     916        self.service.setHandlerParent(self.client)
    462917
    463918
     
    466921        Removing a roster item is setting an item with subscription C{remove}.
    467922        """
    468         d = self.protocol.removeItem(JID('test@example.org'))
     923        d = self.service.removeItem(JID('test@example.org'))
    469924
    470925        # Inspect outgoing iq request
    471926
    472927        iq = self.stub.output[-1]
    473         self.assertEquals('set', iq.getAttribute('type'))
     928        self.assertEqual('set', iq.getAttribute('type'))
    474929        self.assertNotIdentical(None, iq.query)
    475         self.assertEquals(NS_ROSTER, iq.query.uri)
     930        self.assertEqual(NS_ROSTER, iq.query.uri)
    476931
    477932        children = list(domish.generateElementsQNamed(iq.query.children,
    478933                                                      'item', NS_ROSTER))
    479         self.assertEquals(1, len(children))
     934        self.assertEqual(1, len(children))
    480935        child = children[0]
    481         self.assertEquals('test@example.org', child['jid'])
    482         self.assertEquals('remove', child['subscription'])
     936        self.assertEqual('test@example.org', child['jid'])
     937        self.assertEqual('remove', child.getAttribute('subscription'))
    483938
    484939        # Fake successful response
    485940
    486941        response = toResponse(iq, 'result')
    487         self.stub.send(response)
    488         return d
     942        d.callback(response)
     943        return d
     944
     945
     946    def test_getRoster(self):
     947        """
     948        A request for the roster is sent out and the response is parsed.
     949        """
     950        def cb(roster):
     951            self.assertIn(JID('user@example.org'), roster)
     952
     953        d = self.service.getRoster()
     954        d.addCallback(cb)
     955
     956        # Inspect outgoing iq request
     957
     958        iq = self.stub.output[-1]
     959        self.assertEqual('get', iq.getAttribute('type'))
     960        self.assertNotIdentical(None, iq.query)
     961        self.assertEqual(NS_ROSTER, iq.query.uri)
     962
     963        # Fake successful response
     964        response = toResponse(iq, 'result')
     965        query = response.addElement((NS_ROSTER, 'query'))
     966        item = query.addElement('item')
     967        item['jid'] = 'user@example.org'
     968
     969        d.callback(response)
     970        return d
     971
     972
     973    def test_onRosterSet(self):
     974        """
     975        A roster push causes onRosterSet to be called with the parsed item.
     976        """
     977        xml = """
     978          <iq type='set'>
     979            <query xmlns='jabber:iq:roster'>
     980              <item jid='user@example.org'/>
     981            </query>
     982          </iq>
     983        """
     984
     985        items = []
     986
     987        def onRosterSet(item):
     988            items.append(item)
     989
     990        def cb(result):
     991            self.assertEqual(1, len(items))
     992            self.assertEqual(JID('user@example.org'), items[0].entity)
     993
     994        self.service.onRosterSet = onRosterSet
     995
     996        d = self.assertWarns(DeprecationWarning,
     997                             "wokkel.xmppim.RosterClientProtocol.onRosterSet "
     998                             "is deprecated. "
     999                             "Use RosterClientProtocol.setReceived instead.",
     1000                             xmppim.__file__,
     1001                             self.handleRequest, xml)
     1002        d.addCallback(cb)
     1003        return d
     1004
     1005
     1006    def test_onRosterRemove(self):
     1007        """
     1008        A roster push causes onRosterSet to be called with the parsed item.
     1009        """
     1010        xml = """
     1011          <iq type='set'>
     1012            <query xmlns='jabber:iq:roster'>
     1013              <item jid='user@example.org' subscription='remove'/>
     1014            </query>
     1015          </iq>
     1016        """
     1017
     1018        entities = []
     1019
     1020        def onRosterRemove(entity):
     1021            entities.append(entity)
     1022
     1023        def cb(result):
     1024            self.assertEqual([JID('user@example.org')], entities)
     1025
     1026        self.service.onRosterRemove = onRosterRemove
     1027
     1028        d = self.assertWarns(DeprecationWarning,
     1029                             "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
     1030                             "is deprecated. "
     1031                             "Use RosterClientProtocol.removeReceived instead.",
     1032                             xmppim.__file__,
     1033                             self.handleRequest, xml)
     1034        d.addCallback(cb)
     1035        return d
     1036
     1037
     1038    def test_setReceived(self):
     1039        """
     1040        A roster set push causes setReceived.
     1041        """
     1042        xml = """
     1043          <iq type='set'>
     1044            <query xmlns='jabber:iq:roster'>
     1045              <item jid='user@example.org'/>
     1046            </query>
     1047          </iq>
     1048        """
     1049
     1050        requests = []
     1051
     1052        def setReceived(request):
     1053            requests.append(request)
     1054
     1055        def cb(result):
     1056            self.assertEqual(1, len(requests), "setReceived was not called")
     1057            self.assertEqual(JID('user@example.org'), requests[0].item.entity)
     1058
     1059        self.service.setReceived = setReceived
     1060
     1061        d = self.handleRequest(xml)
     1062        d.addCallback(cb)
     1063        return d
     1064
     1065
     1066    def test_setReceivedOtherSource(self):
     1067        """
     1068        Roster pushes can be sent from other entities, too, ignore them.
     1069        """
     1070        xml = """
     1071          <iq type='set' to='this@example.org/Home' from='other@example.org'>
     1072            <query xmlns='jabber:iq:roster'>
     1073              <item jid='user@example.org'/>
     1074            </query>
     1075          </iq>
     1076        """
     1077
     1078        def cb(result):
     1079            self.assertEquals('service-unavailable', result.condition)
     1080
     1081        d = self.handleRequest(xml)
     1082        self.assertFailure(d, error.StanzaError)
     1083        d.addCallback(cb)
     1084        return d
     1085
     1086
     1087    def test_setReceivedOtherSourceAllowed(self):
     1088        """
     1089        Roster pushes can be sent from other entities, allow them.
     1090        """
     1091        xml = """
     1092          <iq type='set' to='this@example.org/Home' from='other@example.org'>
     1093            <query xmlns='jabber:iq:roster'>
     1094              <item jid='user@example.org'/>
     1095            </query>
     1096          </iq>
     1097        """
     1098
     1099        self.service.allowAnySender = True
     1100        requests = []
     1101
     1102        def setReceived(request):
     1103            requests.append(request)
     1104
     1105        def cb(result):
     1106            self.assertEqual(1, len(requests), "setReceived was not called")
     1107
     1108        self.service.setReceived = setReceived
     1109
     1110        d = self.handleRequest(xml)
     1111        d.addCallback(cb)
     1112        return d
     1113
     1114
     1115    def test_setReceivedOtherSourceIgnored(self):
     1116        """
     1117        Roster pushes can be sent from other entities, allow them.
     1118        """
     1119        xml = """
     1120          <iq type='set' to='this@example.org/Home' from='bad@example.org'>
     1121            <query xmlns='jabber:iq:roster'>
     1122              <item jid='user@example.org'/>
     1123            </query>
     1124          </iq>
     1125        """
     1126
     1127        self.service.allowAnySender = True
     1128
     1129        def setReceived(request):
     1130            if request.sender == JID('bad@example.org'):
     1131                raise xmppim.RosterPushIgnored()
     1132
     1133        def cb(result):
     1134            self.assertEquals('service-unavailable', result.condition)
     1135
     1136        self.service.setReceived = setReceived
     1137
     1138
     1139        d = self.handleRequest(xml)
     1140        self.assertFailure(d, error.StanzaError)
     1141        d.addCallback(cb)
     1142        return d
     1143
     1144
     1145    def test_removeReceived(self):
     1146        """
     1147        A roster remove push causes removeReceived.
     1148        """
     1149        xml = """
     1150          <iq type='set'>
     1151            <query xmlns='jabber:iq:roster'>
     1152              <item jid='user@example.org' subscription='remove'/>
     1153            </query>
     1154          </iq>
     1155        """
     1156
     1157        requests = []
     1158
     1159        def removeReceived(request):
     1160            requests.append(request)
     1161
     1162        def cb(result):
     1163            self.assertEqual(1, len(requests), "removeReceived was not called")
     1164            self.assertEqual(JID('user@example.org'), requests[0].item.entity)
     1165
     1166        self.service.removeReceived = removeReceived
     1167
     1168        d = self.handleRequest(xml)
     1169        d.addCallback(cb)
     1170        return d
  • wokkel/xmppim.py

    r166 r172  
    88
    99This module provides generic implementations for the protocols defined in
    10 U{RFC 3921<http://xmpp.org/rfcs/rfc3921.html>} (XMPP IM).
    11 
    12 All of it should eventually move to Twisted.
     10U{RFC 6121<http://www.xmpp.org/rfcs/rfc6121.html>} (XMPP IM).
    1311"""
    1412
     13import warnings
     14
     15from twisted.internet import defer
     16from twisted.words.protocols.jabber import error
    1517from twisted.words.protocols.jabber.jid import JID
    1618from twisted.words.xish import domish
    1719
    18 from wokkel.compat import IQ
    19 from wokkel.generic import ErrorStanza, Stanza
     20from wokkel.generic import ErrorStanza, Stanza, Request
     21from wokkel.subprotocols import IQHandlerMixin
    2022from wokkel.subprotocols import XMPPHandler
    2123
    2224NS_XML = 'http://www.w3.org/XML/1998/namespace'
    2325NS_ROSTER = 'jabber:iq:roster'
     26
     27XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
     28
     29
    2430
    2531class Presence(domish.Element):
     
    606612    This represents one contact from an XMPP contact list known as roster.
    607613
    608     @ivar jid: The JID of the contact.
    609     @type jid: L{JID}
    610     @ivar name: The optional associated nickname for this contact.
     614    @ivar entity: The JID of the contact.
     615    @type entity: L{JID}
     616    @ivar name: The associated nickname for this contact.
    611617    @type name: C{unicode}
    612618    @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
     
    617623                            contact is subscribed to the presence information
    618624                            of the roster owner.
    619     @type subscriptionTo: C{bool}
    620     @ivar ask: Whether subscription is pending.
    621     @type ask: C{bool}
     625    @type subscriptionFrom: C{bool}
     626    @ivar pendingOut: Whether the subscription request to this contact is
     627        pending.
     628    @type pendingOut: C{bool}
    622629    @ivar groups: Set of groups this contact is categorized in. Groups are
    623630                  represented by an opaque identifier of type C{unicode}.
    624631    @type groups: C{set}
    625     """
    626 
    627     def __init__(self, jid):
    628         self.jid = jid
    629         self.name = None
    630         self.subscriptionTo = False
    631         self.subscriptionFrom = False
    632         self.ask = None
    633         self.groups = set()
    634 
    635 
    636 class RosterClientProtocol(XMPPHandler):
     632    @ivar approved: Signals pre-approved subscription.
     633    @type approved: C{bool}
     634    @ivar remove: Signals roster item removal.
     635    @type remove: C{bool}
     636    """
     637
     638    __subscriptionStates = {(False, False): None,
     639                            (True, False): 'to',
     640                            (False, True): 'from',
     641                            (True, True): 'both'}
     642
     643    def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False,
     644                       name=u'', groups=None):
     645        self.entity = entity
     646        self.subscriptionTo = subscriptionTo
     647        self.subscriptionFrom = subscriptionFrom
     648        self.name = name
     649        self.groups = groups or set()
     650
     651        self.pendingOut = False
     652        self.approved = False
     653        self.remove = False
     654
     655
     656    def __getJID(self):
     657        warnings.warn(
     658            "wokkel.xmppim.RosterItem.jid is deprecated. "
     659            "Use RosterItem.entity instead.",
     660            DeprecationWarning)
     661        return self.entity
     662
     663
     664    def __setJID(self, value):
     665        warnings.warn(
     666            "wokkel.xmppim.RosterItem.jid is deprecated. "
     667            "Use RosterItem.entity instead.",
     668            DeprecationWarning)
     669        self.entity = value
     670
     671
     672    jid = property(__getJID, __setJID, doc="""
     673            JID of the contact. Deprecated in favour of C{entity}.""")
     674
     675
     676    def __getAsk(self):
     677        warnings.warn(
     678            "wokkel.xmppim.RosterItem.ask is deprecated. "
     679            "Use RosterItem.pendingOut instead.",
     680            DeprecationWarning)
     681        return self.pendingOut
     682
     683
     684    def __setAsk(self, value):
     685        warnings.warn(
     686            "wokkel.xmppim.RosterItem.ask is deprecated. "
     687            "Use RosterItem.pendingOut instead.",
     688            DeprecationWarning)
     689        self.pendingOut = value
     690
     691
     692    ask = property(__getAsk, __setAsk, doc="""
     693            Pending out subscription. Deprecated in favour of C{pendingOut}.""")
     694
     695
     696    def toElement(self):
     697        element = domish.Element((NS_ROSTER, 'item'))
     698        element['jid'] = self.entity.full()
     699
     700        if self.remove:
     701            subscription = 'remove'
     702        else:
     703            subscription = self.__subscriptionStates[self.subscriptionTo,
     704                                                     self.subscriptionFrom]
     705
     706            if self.pendingOut:
     707                element['ask'] = u'subscribe'
     708
     709            if self.name:
     710                element['name'] = self.name
     711
     712            if self.approved:
     713                element['approved'] = u'true'
     714
     715            if self.groups:
     716                for group in self.groups:
     717                    element.addElement('group', content=group)
     718
     719        if subscription:
     720            element['subscription'] = subscription
     721
     722        return element
     723
     724
     725    @classmethod
     726    def fromElement(Class, element):
     727        entity = JID(element['jid'])
     728        item = Class(entity)
     729        subscription = element.getAttribute('subscription')
     730        if subscription == 'remove':
     731            item.remove = True
     732        else:
     733            item.name = element.getAttribute('name', u'')
     734            item.subscriptionTo = subscription in ('to', 'both')
     735            item.subscriptionFrom = subscription in ('from', 'both')
     736            item.pendingOut = element.getAttribute('ask') == 'subscribe'
     737            item.approved = element.getAttribute('approved') in ('true', '1')
     738            for subElement in domish.generateElementsQNamed(element.children,
     739                                                            'group', NS_ROSTER):
     740                item.groups.add(unicode(subElement))
     741        return item
     742
     743
     744
     745class RosterRequest(Request):
     746    """
     747    Roster request.
     748
     749    @ivar item: Roster item to be set or pushed.
     750    @type item: L{RosterItem}.
     751    """
     752    item = None
     753
     754    def parseRequest(self, element):
     755        for child in element.elements(NS_ROSTER, 'item'):
     756            self.item = RosterItem.fromElement(child)
     757            break
     758
     759
     760    def toElement(self):
     761        element = Request.toElement(self)
     762        query = element.addElement((NS_ROSTER, 'query'))
     763        if self.item:
     764            query.addChild(self.item.toElement())
     765        return element
     766
     767
     768
     769class RosterPushIgnored(Exception):
     770    """
     771    Raised when this entity doesn't want to accept/trust a roster push.
     772
     773    To avert presence leaks, a handler can raise L{RosterPushIgnored} when
     774    not accepting a roster push (directly or via Deferred). This will
     775    result in a C{'service-unavailable'} error being sent in return.
     776    """
     777
     778
     779
     780class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
    637781    """
    638782    Client side XMPP roster protocol.
    639     """
     783
     784    The roster can be retrieved using L{getRoster}. Subsequent changes to the
     785    roster will be pushed, resulting in calls to L{setReceived} or
     786    L{removeReceived}. These methods should be overridden to handle the
     787    roster pushes.
     788
     789    RFC 6121 specifically allows entities other than a user's server to
     790    hold a roster for that user. However, how a client should deal with
     791    that is currently not yet specfied.
     792
     793    By default roster pushes from other source. I.e. when C{request.sender}
     794    is set but the sender's bare JID is different from the user's bare JID.
     795    Set L{allowAnySender} to allow roster pushes from any sender. To
     796    avert presence leaks, L{RosterPushIgnored} should then be raised for
     797    pushes from untrusted senders.
     798
     799    @cvar allowAnySender: Flag to allow roster pushes from any sender.
     800        C{False} by default.
     801    @type allowAnySender: C{boolean}
     802    """
     803
     804    allowAnySender = False
     805    iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"}
     806
    640807
    641808    def connectionInitialized(self):
    642         ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
    643         self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet)
    644 
    645     def _parseRosterItem(self, element):
    646         jid = JID(element['jid'])
    647         item = RosterItem(jid)
    648         item.name = element.getAttribute('name')
    649         subscription = element.getAttribute('subscription')
    650         item.subscriptionTo = subscription in ('to', 'both')
    651         item.subscriptionFrom = subscription in ('from', 'both')
    652         item.ask = element.getAttribute('ask') == 'subscribe'
    653         for subElement in domish.generateElementsQNamed(element.children,
    654                                                         'group', NS_ROSTER):
    655             item.groups.add(unicode(subElement))
    656 
    657         return item
     809        self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
     810
    658811
    659812    def getRoster(self):
     
    669822            for element in domish.generateElementsQNamed(result.query.children,
    670823                                                         'item', NS_ROSTER):
    671                 item = self._parseRosterItem(element)
    672                 roster[item.jid.userhost()] = item
     824                item = RosterItem.fromElement(element)
     825                roster[item.entity] = item
    673826
    674827            return roster
    675828
    676         iq = IQ(self.xmlstream, 'get')
    677         iq.addElement((NS_ROSTER, 'query'))
    678         d = iq.send()
     829        request = RosterRequest(stanzaType='get')
     830        d = self.request(request)
    679831        d.addCallback(processRoster)
    680832        return d
     
    689841        @rtype: L{twisted.internet.defer.Deferred}
    690842        """
    691         iq = IQ(self.xmlstream, 'set')
    692         iq.addElement((NS_ROSTER, 'query'))
    693         item = iq.query.addElement('item')
    694         item['jid'] = entity.full()
    695         item['subscription'] = 'remove'
    696         return iq.send()
     843        request = RosterRequest(stanzaType='set')
     844        request.item = RosterItem(entity)
     845        request.item.remove = True
     846        return self.request(request)
    697847
    698848
    699849    def _onRosterSet(self, iq):
    700         if iq.handled or \
    701            iq.hasAttribute('from') and iq['from'] != self.xmlstream:
    702             return
    703 
    704         iq.handled = True
    705 
    706         itemElement = iq.query.item
    707 
    708         if unicode(itemElement['subscription']) == 'remove':
    709             self.onRosterRemove(JID(itemElement['jid']))
     850        def trapIgnored(failure):
     851            failure.trap(RosterPushIgnored)
     852            raise error.StanzaError('service-unavailable')
     853
     854        request = RosterRequest.fromElement(iq)
     855
     856        if (not self.allowAnySender and
     857                request.sender and
     858                request.sender.userhostJID() !=
     859                self.parent.jid.userhostJID()):
     860            d = defer.fail(RosterPushIgnored())
     861        elif request.item.remove:
     862            d = defer.maybeDeferred(self.removeReceived, request)
    710863        else:
    711             item = self._parseRosterItem(iq.query.item)
    712             self.onRosterSet(item)
    713 
    714     def onRosterSet(self, item):
     864            d = defer.maybeDeferred(self.setReceived, request)
     865        d.addErrback(trapIgnored)
     866        return d
     867
     868
     869    def setReceived(self, request):
    715870        """
    716871        Called when a roster push for a new or update item was received.
    717872
    718         @param item: The pushed roster item.
    719         @type item: L{RosterItem}
    720         """
    721 
    722     def onRosterRemove(self, entity):
     873        @param request: The push request.
     874        @type request: L{RosterRequest}
     875        """
     876        if hasattr(self, 'onRosterSet'):
     877            warnings.warn(
     878                "wokkel.xmppim.RosterClientProtocol.onRosterSet "
     879                "is deprecated. "
     880                "Use RosterClientProtocol.setReceived instead.",
     881                DeprecationWarning)
     882            return defer.maybeDeferred(self.onRosterSet, request.item)
     883
     884
     885    def removeReceived(self, request):
    723886        """
    724887        Called when a roster push for the removal of an item was received.
    725888
    726         @param entity: The entity for which the roster item has been removed.
    727         @type entity: L{JID}
    728         """
     889        @param request: The push request.
     890        @type request: L{RosterRequest}
     891        """
     892        if hasattr(self, 'onRosterRemove'):
     893            warnings.warn(
     894                "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
     895                "is deprecated. "
     896                "Use RosterClientProtocol.removeReceived instead.",
     897                DeprecationWarning)
     898            return defer.maybeDeferred(self.onRosterRemove,
     899                                       request.item.entity)
    729900
    730901
Note: See TracChangeset for help on using the changeset viewer.