Ignore:
Files:
3 added
21 edited

Legend:

Unmodified
Added
Removed
  • LICENSE

    r19 r53  
    1 Copyright (c) 2003-2008 Ralph Meijer
     1Copyright (c) 2003-2009 Ralph Meijer
    22
    33Permission is hereby granted, free of charge, to any person obtaining
  • NEWS

    r31 r60  
     10.6.0 (2009-04-22)
     2==================
     3
     4Features
     5--------
     6
     7 - Server-to-server support, based on the dialback protocol (#33).
     8 - Enhancement to InternalProtocol to support multiple domains (#43).
     9 - Publish-subscribe request abstraction (#45).
     10 - Publish-subscribe abstraction to implement a node in code (#47).
     11 - Enhancement to PubSubClient to send requests from a specific JID (#46).
     12
     13Fixes
     14-----
     15
     16 - Remove type interpretation in Data Forms field parsing code (#44).
     17
     18
     190.5.0 (2009-04-07)
     20==================
     21
     22This release drops support for Twisted versions older than 8.0, including
     23Twisted 2.5 / Twisted Words 0.5.
     24 
     25Features
     26--------
     27
     28 - Support for sending and receiving Publish-Subscribe node delete
     29   notifications with redirect.
     30 - Service Discovery client support, including an overhaul of disco data
     31   classes (#28).
     32 - Initial support for building XMPP servers has been added:
     33   - XmlStreamServerFactory has been backported from Twisted Words (#29).
     34   - An XMPP router has been added (#30).
     35   - A server-side component authenticator has been added (#30).
     36   - A new server-side component service, that connects to a router within the
     37     same process, was added (#31).
     38
     39
     40Fixes
     41-----
     42
     43 - Publish-Subscribe subscriptions requests work again (#22).
     44 - Publish-Subscribe delete node requests now have the correct namespace (#27).
     45 - NodeIDs in Service Discovery requests are now returned in responses (#7).
     46 - The presence of stanzaType in toResponse is now checked correctly (#34).
     47 - Data Form fields are now rendered depending on form type (#40).
     48 - Data Form type checking issues were addressed (#41).
     49 - Some compatibility fixes for Twisted 8.0 and 8.1.
     50 - Various other fixes (#37, #42) and tracking changes to code already in
     51   Twisted.
     52
     53
    1540.4.0 (2008-08-05)
    255==================
  • README

    r31 r60  
    1 Wokkel 0.4.0
     1Wokkel 0.6.0
    22
    33What is this?
    44=============
    55
    6 Wokkel is a Python module for experimenting with future enhancements
    7 to Twisted Words. Everything is aimed to be included in the Twisted
    8 main development tree eventually.
     6Wokkel is a Python module for experimenting with future enhancements to Twisted
     7Words, that should eventually be included in the main Twisted main development
     8tree. Some of the code in Wokkel has already made that transition, but is still
     9included to be used with older Twisted releases.
    910
    10 Dependencies
     11
     12Requirements
    1113============
    1214
    13 This module depends on Twisted Words 0.5 or later.
     15 - Python 2.4 or later.
     16 - Twisted 8.0.0 or later.
    1417
    15 Copyright
     18
     19Resources
    1620=========
    1721
    18 The code in this distribution is Copyright (c) 2003-2008 Ralph Meijer, unless
     22Wokkel's home is <http://wokkel.ik.nu/>.
     23
     24Besides the general Twisted resources, help is available on the Twisted-Jabber
     25mailing list::
     26
     27  <https://mailman.ik.nu/mailman/listinfo/twisted-jabber>
     28
     29
     30Copyright and Warranty
     31======================
     32
     33The code in this distribution is Copyright (c) 2003-2009 Ralph Meijer, unless
    1934excplicitely specified otherwise.
    2035
     
    2237describes this in detail.
    2338
    24 Contact
    25 =======
    2639
    27 Questions, comments or suggestions are welcome!
     40Contributors
     41============
     42
     43 - Christopher Zorn
     44 - Jack Moffitt
     45 - Mike Malone
     46 - Pablo Martin
     47
     48
     49Author
     50======
    2851
    2952Ralph Meijer
  • setup.py

    r31 r60  
    11#!/usr/bin/env python
    22
    3 # Copyright (c) 2003-2008 Ralph Meijer
     3# Copyright (c) 2003-2009 Ralph Meijer
    44# See LICENSE for details.
    55
     
    77
    88setup(name='wokkel',
    9       version='0.4.0',
     9      version='0.6.0',
    1010      description='Twisted Jabber support library',
    1111      author='Ralph Meijer',
  • wokkel/client.py

    r14 r55  
    1212
    1313from twisted.application import service
    14 from twisted.internet import defer, protocol, reactor
     14from twisted.internet import reactor
    1515from twisted.names.srvconnect import SRVConnector
    1616from twisted.words.protocols.jabber import client, sasl, xmlstream
    1717
    18 try:
    19     from twisted.words.xish.xmlstream import XmlStreamFactoryMixin
    20 except ImportError:
    21     from wokkel.compat import XmlStreamFactoryMixin
    22 
    23 from wokkel.subprotocols import StreamManager, XMPPHandler
     18from wokkel import generic
     19from wokkel.subprotocols import StreamManager
    2420
    2521class CheckAuthInitializer(object):
     
    128124
    129125
    130 class DeferredClientFactory(XmlStreamFactoryMixin, protocol.ClientFactory):
    131     protocol = xmlstream.XmlStream
     126class DeferredClientFactory(generic.DeferredXmlStreamFactory):
    132127
    133128    def __init__(self, jid, password):
    134         self.authenticator = client.XMPPAuthenticator(jid, password)
    135         XmlStreamFactoryMixin.__init__(self, self.authenticator)
     129        authenticator = client.XMPPAuthenticator(jid, password)
     130        generic.DeferredXmlStreamFactory.__init__(self, authenticator)
    136131
    137         deferred = defer.Deferred()
    138         self.deferred = deferred
    139 
    140         self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback)
    141 
    142         class ConnectionInitializedHandler(XMPPHandler):
    143             def connectionInitialized(self):
    144                 deferred.callback(None)
    145 
    146         self.streamManager = StreamManager(self)
    147         self.addHandler(ConnectionInitializedHandler())
    148 
    149     def clientConnectionFailed(self, connector, reason):
    150         self.deferred.errback(reason)
    151132
    152133    def addHandler(self, handler):
     
    155136        """
    156137        self.streamManager.addHandler(handler)
     138
    157139
    158140    def removeHandler(self, handler):
     
    163145
    164146
     147
    165148def clientCreator(factory):
    166149    domain = factory.authenticator.jid.host
  • wokkel/compat.py

    r8 r53  
    11# -*- test-case-name: wokkel.test.test_compat -*-
    22#
    3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories.
     3# Copyright (c) 2001-2008 Twisted Matrix Laboratories.
    44# See LICENSE for details.
    55
     6from twisted.internet import protocol
     7from twisted.words.protocols.jabber import xmlstream
    68from twisted.words.xish import domish
    79
    8 def toResponse(stanza, stanzaType=None):
     10class BootstrapMixin(object):
    911    """
    10     Create a response stanza from another stanza.
     12    XmlStream factory mixin to install bootstrap event observers.
    1113
    12     This takes the addressing and id attributes from a stanza to create a (new,
    13     empty) response stanza. The addressing attributes are swapped and the id
    14     copied. Optionally, the stanza type of the response can be specified.
     14    This mixin is for factories providing
     15    L{IProtocolFactory<twisted.internet.interfaces.IProtocolFactory>} to make
     16    sure bootstrap event observers are set up on protocols, before incoming
     17    data is processed. Such protocols typically derive from
     18    L{utility.EventDispatcher}, like L{XmlStream}.
    1519
    16     @param stanza: the original stanza
    17     @type stanza: L{domish.Element}
    18     @param stanzaType: optional response stanza type
    19     @type stanzaType: C{str}
    20     @return: the response stanza.
    21     @rtype: L{domish.Element}
     20    You can set up bootstrap event observers using C{addBootstrap}. The
     21    C{event} and C{fn} parameters correspond with the C{event} and
     22    C{observerfn} arguments to L{utility.EventDispatcher.addObserver}.
     23
     24    @since: 8.2.
     25    @ivar bootstraps: The list of registered bootstrap event observers.
     26    @type bootstrap: C{list}
    2227    """
    2328
    24     toAddr = stanza.getAttribute('from')
    25     fromAddr = stanza.getAttribute('to')
    26     stanzaID = stanza.getAttribute('id')
    27 
    28     response = domish.Element((None, stanza.name))
    29     if toAddr:
    30         response['to'] = toAddr
    31     if fromAddr:
    32         response['from'] = fromAddr
    33     if stanzaID:
    34         response['id'] = stanzaID
    35     if type:
    36         response['type'] = stanzaType
    37 
    38     return response
     29    def __init__(self):
     30        self.bootstraps = []
    3931
    4032
    41 class XmlStreamFactoryMixin(object):
     33    def installBootstraps(self, dispatcher):
     34        """
     35        Install registered bootstrap observers.
     36
     37        @param dispatcher: Event dispatcher to add the observers to.
     38        @type dispatcher: L{utility.EventDispatcher}
     39        """
     40        for event, fn in self.bootstraps:
     41            dispatcher.addObserver(event, fn)
     42
     43
     44    def addBootstrap(self, event, fn):
     45        """
     46        Add a bootstrap event handler.
     47
     48        @param event: The event to register an observer for.
     49        @type event: C{str} or L{xpath.XPathQuery}
     50        @param fn: The observer callable to be registered.
     51        """
     52        self.bootstraps.append((event, fn))
     53
     54
     55    def removeBootstrap(self, event, fn):
     56        """
     57        Remove a bootstrap event handler.
     58
     59        @param event: The event the observer is registered for.
     60        @type event: C{str} or L{xpath.XPathQuery}
     61        @param fn: The registered observer callable.
     62        """
     63        self.bootstraps.remove((event, fn))
     64
     65
     66
     67class XmlStreamServerFactory(BootstrapMixin,
     68                             protocol.ServerFactory):
    4269    """
    43     XmlStream factory mixin that takes care of event handlers.
     70    Factory for Jabber XmlStream objects as a server.
    4471
    45     To make sure certain event observers are set up before incoming data is
    46     processed, you can set up bootstrap event observers using C{addBootstrap}.
    47 
    48     The C{event} and C{fn} parameters correspond with the C{event} and
    49     C{observerfn} arguments to L{utility.EventDispatcher.addObserver}.
     72    @since: 8.2.
     73    @ivar authenticatorFactory: Factory callable that takes no arguments, to
     74                                create a fresh authenticator to be associated
     75                                with the XmlStream.
    5076    """
    5177
    52     def __init__(self, *args, **kwargs):
    53         self.bootstraps = []
    54         self.args = args
    55         self.kwargs = kwargs
     78    protocol = xmlstream.XmlStream
     79
     80    def __init__(self, authenticatorFactory):
     81        BootstrapMixin.__init__(self)
     82        self.authenticatorFactory = authenticatorFactory
     83
    5684
    5785    def buildProtocol(self, addr):
     
    5987        Create an instance of XmlStream.
    6088
    61         The returned instance will have bootstrap event observers registered
    62         and will proceed to handle input on an incoming connection.
     89        A new authenticator instance will be created and passed to the new
     90        XmlStream. Registered bootstrap event observers are installed as well.
    6391        """
    64         xs = self.protocol(*self.args, **self.kwargs)
     92        authenticator = self.authenticatorFactory()
     93        xs = self.protocol(authenticator)
    6594        xs.factory = self
    66         for event, fn in self.bootstraps:
    67             xs.addObserver(event, fn)
     95        self.installBootstraps(xs)
    6896        return xs
    69 
    70     def addBootstrap(self, event, fn):
    71         """
    72         Add a bootstrap event handler.
    73         """
    74         self.bootstraps.append((event, fn))
    75 
    76     def removeBootstrap(self, event, fn):
    77         """
    78         Remove a bootstrap event handler.
    79         """
    80         self.bootstraps.remove((event, fn))
  • wokkel/component.py

    r6 r54  
    1 # Copyright (c) 2003-2007 Ralph Meijer
     1# -*- test-case-name: wokkel.test.test_component -*-
     2#
     3# Copyright (c) 2003-2008 Ralph Meijer
    24# See LICENSE for details.
    35
     
    810from twisted.application import service
    911from twisted.internet import reactor
    10 from twisted.words.protocols.jabber import component
     12from twisted.python import log
     13from twisted.words.protocols.jabber.jid import internJID as JID
     14from twisted.words.protocols.jabber import component, error, xmlstream
    1115from twisted.words.xish import domish
    1216
     17try:
     18    #from twisted.words.protocols.jabber.xmlstream import XMPPHandler
     19    from twisted.words.protocols.jabber.xmlstream import XMPPHandlerCollection
     20except ImportError:
     21    #from wokkel.subprotocols import XMPPHandler
     22    from wokkel.subprotocols import XMPPHandlerCollection
     23
     24try:
     25    from twisted.words.protocols.jabber.xmlstream import XmlStreamServerFactory
     26except ImportError:
     27    from wokkel.compat import XmlStreamServerFactory
     28
     29from wokkel.generic import XmlPipe
    1330from wokkel.subprotocols import StreamManager
     31
     32NS_COMPONENT_ACCEPT = 'jabber:component:accept'
    1433
    1534class Component(StreamManager, service.Service):
     
    5776    def _getConnection(self):
    5877        return reactor.connectTCP(self.host, self.port, self.factory)
     78
     79
     80
     81class InternalComponent(XMPPHandlerCollection, service.Service):
     82    """
     83    Component service that connects directly to a router.
     84
     85    Instead of opening a socket to connect to a router, like L{Component},
     86    components of this type connect to a router in the same process. This
     87    allows for one-process XMPP servers.
     88
     89    @ivar domains: Domains (as C{str}) this component will handle traffic for.
     90    @type domains: L{set}
     91    """
     92
     93    def __init__(self, router, domain=None):
     94        XMPPHandlerCollection.__init__(self)
     95
     96        self._router = router
     97        self.domains = set()
     98        if domain:
     99            self.domains.add(domain)
     100
     101        self.xmlstream = None
     102
     103    def startService(self):
     104        """
     105        Create a XML pipe, connect to the router and setup handlers.
     106        """
     107        service.Service.startService(self)
     108
     109        self._pipe = XmlPipe()
     110        self.xmlstream = self._pipe.source
     111
     112        for domain in self.domains:
     113            self._router.addRoute(domain, self._pipe.sink)
     114
     115        for e in self:
     116            e.makeConnection(self.xmlstream)
     117            e.connectionInitialized()
     118
     119
     120    def stopService(self):
     121        """
     122        Disconnect from the router and handlers.
     123        """
     124        service.Service.stopService(self)
     125
     126        for domain in self.domains:
     127            self._router.removeRoute(domain, self._pipe.sink)
     128
     129        self._pipe = None
     130        self.xmlstream = None
     131
     132        for e in self:
     133            e.connectionLost(None)
     134
     135
     136    def addHandler(self, handler):
     137        """
     138        Add a new handler and connect it to the stream.
     139        """
     140        XMPPHandlerCollection.addHandler(self, handler)
     141
     142        if self.xmlstream:
     143            handler.makeConnection(self.xmlstream)
     144            handler.connectionInitialized()
     145
     146
     147    def send(self, obj):
     148        """
     149        Send data to the XML stream, so it ends up at the router.
     150        """
     151        self.xmlstream.send(obj)
     152
     153
     154
     155class ListenComponentAuthenticator(xmlstream.ListenAuthenticator):
     156    """
     157    Authenticator for accepting components.
     158
     159    @ivar secret: The shared used to authorized incoming component connections.
     160    @type secret: C{str}.
     161    """
     162
     163    namespace = NS_COMPONENT_ACCEPT
     164
     165    def __init__(self, secret):
     166        self.secret = secret
     167        xmlstream.ListenAuthenticator.__init__(self)
     168
     169
     170    def associateWithStream(self, xs):
     171        """
     172        Associate the authenticator with a stream.
     173
     174        This sets the stream's version to 0.0, because the XEP-0114 component
     175        protocol was not designed for XMPP 1.0.
     176        """
     177        xs.version = (0, 0)
     178        xmlstream.ListenAuthenticator.associateWithStream(self, xs)
     179
     180
     181    def streamStarted(self, rootElement):
     182        """
     183        Called by the stream when it has started.
     184
     185        This examines the default namespace of the incoming stream and whether
     186        there is a requested hostname for the component. Then it generates a
     187        stream identifier, sends a response header and adds an observer for
     188        the first incoming element, triggering L{onElement}.
     189        """
     190
     191        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
     192
     193        # Compatibility fix for pre-8.2 implementations of ListenAuthenticator
     194        if not self.xmlstream.sid:
     195            from twisted.python import randbytes
     196            self.xmlstream.sid = randbytes.secureRandom(8).encode('hex')
     197
     198        if rootElement.defaultUri != self.namespace:
     199            exc = error.StreamError('invalid-namespace')
     200            self.xmlstream.sendStreamError(exc)
     201            return
     202
     203        # self.xmlstream.thisEntity is set to the address the component
     204        # wants to assume.
     205        if not self.xmlstream.thisEntity:
     206            exc = error.StreamError('improper-addressing')
     207            self.xmlstream.sendStreamError(exc)
     208            return
     209
     210        self.xmlstream.sendHeader()
     211        self.xmlstream.addOnetimeObserver('/*', self.onElement)
     212
     213
     214    def onElement(self, element):
     215        """
     216        Called on incoming XML Stanzas.
     217
     218        The very first element received should be a request for handshake.
     219        Otherwise, the stream is dropped with a 'not-authorized' error. If a
     220        handshake request was received, the hash is extracted and passed to
     221        L{onHandshake}.
     222        """
     223        if (element.uri, element.name) == (self.namespace, 'handshake'):
     224            self.onHandshake(unicode(element))
     225        else:
     226            exc = error.StreamError('not-authorized')
     227            self.xmlstream.sendStreamError(exc)
     228
     229
     230    def onHandshake(self, handshake):
     231        """
     232        Called upon receiving the handshake request.
     233
     234        This checks that the given hash in C{handshake} is equal to a
     235        calculated hash, responding with a handshake reply or a stream error.
     236        If the handshake was ok, the stream is authorized, and  XML Stanzas may
     237        be exchanged.
     238        """
     239        calculatedHash = xmlstream.hashPassword(self.xmlstream.sid, self.secret)
     240        if handshake != calculatedHash:
     241            exc = error.StreamError('not-authorized', text='Invalid hash')
     242            self.xmlstream.sendStreamError(exc)
     243        else:
     244            self.xmlstream.send('<handshake/>')
     245            self.xmlstream.dispatch(self.xmlstream,
     246                                    xmlstream.STREAM_AUTHD_EVENT)
     247
     248
     249
     250class Router(object):
     251    """
     252    XMPP Server's Router.
     253
     254    A router connects the different components of the XMPP service and routes
     255    messages between them based on the given routing table.
     256
     257    Connected components are trusted to have correct addressing in the
     258    stanzas they offer for routing.
     259
     260    A route destination of C{None} adds a default route. Traffic for which no
     261    specific route exists, will be routed to this default route.
     262
     263    @ivar routes: Routes based on the host part of JIDs. Maps host names to the
     264                  L{EventDispatcher<utility.EventDispatcher>}s that should
     265                  receive the traffic. A key of C{None} means the default
     266                  route.
     267    @type routes: C{dict}
     268    """
     269
     270    def __init__(self):
     271        self.routes = {}
     272
     273
     274    def addRoute(self, destination, xs):
     275        """
     276        Add a new route.
     277
     278        The passed XML Stream C{xs} will have an observer for all stanzas
     279        added to route its outgoing traffic. In turn, traffic for
     280        C{destination} will be passed to this stream.
     281
     282        @param destination: Destination of the route to be added as a host name
     283                            or C{None} for the default route.
     284        @type destination: C{str} or C{NoneType}.
     285        @param xs: XML Stream to register the route for.
     286        @type xs: L{EventDispatcher<utility.EventDispatcher>}.
     287        """
     288        self.routes[destination] = xs
     289        xs.addObserver('/*', self.route)
     290
     291
     292    def removeRoute(self, destination, xs):
     293        """
     294        Remove a route.
     295
     296        @param destination: Destination of the route that should be removed.
     297        @type destination: C{str}.
     298        @param xs: XML Stream to remove the route for.
     299        @type xs: L{EventDispatcher<utility.EventDispatcher>}.
     300        """
     301        xs.removeObserver('/*', self.route)
     302        if (xs == self.routes[destination]):
     303            del self.routes[destination]
     304
     305
     306    def route(self, stanza):
     307        """
     308        Route a stanza.
     309
     310        @param stanza: The stanza to be routed.
     311        @type stanza: L{domish.Element}.
     312        """
     313        destination = JID(stanza['to'])
     314
     315        log.msg("Routing to %s: %r" % (destination.full(), stanza.toXml()))
     316
     317        if destination.host in self.routes:
     318            self.routes[destination.host].send(stanza)
     319        else:
     320            self.routes[None].send(stanza)
     321
     322
     323
     324class XMPPComponentServerFactory(XmlStreamServerFactory):
     325    """
     326    XMPP Component Server factory.
     327
     328    This factory accepts XMPP external component connections and makes
     329    the router service route traffic for a component's bound domain
     330    to that component.
     331    """
     332
     333    logTraffic = False
     334
     335    def __init__(self, router, secret='secret'):
     336        self.router = router
     337        self.secret = secret
     338
     339        def authenticatorFactory():
     340            return ListenComponentAuthenticator(self.secret)
     341
     342        XmlStreamServerFactory.__init__(self, authenticatorFactory)
     343        self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
     344                          self.makeConnection)
     345        self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,
     346                          self.connectionInitialized)
     347
     348        self.serial = 0
     349
     350
     351    def makeConnection(self, xs):
     352        """
     353        Called when a component connection was made.
     354
     355        This enables traffic debugging on incoming streams.
     356        """
     357        xs.serial = self.serial
     358        self.serial += 1
     359
     360        def logDataIn(buf):
     361            log.msg("RECV (%d): %r" % (xs.serial, buf))
     362
     363        def logDataOut(buf):
     364            log.msg("SEND (%d): %r" % (xs.serial, buf))
     365
     366        if self.logTraffic:
     367            xs.rawDataInFn = logDataIn
     368            xs.rawDataOutFn = logDataOut
     369
     370        xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError)
     371
     372
     373    def connectionInitialized(self, xs):
     374        """
     375        Called when a component has succesfully authenticated.
     376
     377        Add the component to the routing table and establish a handler
     378        for a closed connection.
     379        """
     380        destination = xs.thisEntity.host
     381
     382        self.router.addRoute(destination, xs)
     383        xs.addObserver(xmlstream.STREAM_END_EVENT, self.connectionLost, 0,
     384                                                   destination, xs)
     385
     386
     387    def onError(self, reason):
     388        log.err(reason, "Stream Error")
     389
     390
     391    def connectionLost(self, destination, xs, reason):
     392        self.router.removeRoute(destination, xs)
  • wokkel/data_form.py

    r29 r56  
    11# -*- test-case-name: wokkel.test.test_data_form -*-
    22#
    3 # Copyright (c) 2003-2008 Ralph Meijer
     3# Copyright (c) 2003-2009 Ralph Meijer
    44# See LICENSE for details.
    55
     
    6969        Return the DOM representation of this option.
    7070
    71         @rtype L{domish.Element}.
     71        @rtype: L{domish.Element}.
    7272        """
    7373        option = domish.Element((NS_X_DATA, 'option'))
     
    220220            for value in self.values:
    221221                if self.fieldType == 'boolean':
    222                     # We send out the textual representation of boolean values
    223                     value = bool(int(value))
     222                    if isinstance(value, (str, unicode)):
     223                        checkValue = value.lower()
     224                        if not checkValue in ('0', '1', 'false', 'true'):
     225                            raise ValueError("Not a boolean")
     226                        value = checkValue in ('1', 'true')
     227                    value = bool(value)
    224228                elif self.fieldType in ('jid-single', 'jid-multi'):
    225                     value = value.full()
     229                    if not hasattr(value, 'full'):
     230                        value = JID(value)
    226231
    227232                newValues.append(value)
     
    229234            self.values = newValues
    230235
    231     def toElement(self):
     236    def toElement(self, asForm=False):
    232237        """
    233238        Return the DOM representation of this Field.
    234239
    235         @rtype L{domish.Element}.
     240        @rtype: L{domish.Element}.
    236241        """
    237242
     
    239244
    240245        field = domish.Element((NS_X_DATA, 'field'))
    241         field['type'] = self.fieldType
     246
     247        if asForm or self.fieldType != 'text-single':
     248            field['type'] = self.fieldType
    242249
    243250        if self.var is not None:
     
    247254            if self.fieldType == 'boolean':
    248255                value = unicode(value).lower()
     256            elif self.fieldType in ('jid-single', 'jid-multi'):
     257                value = value.full()
     258
    249259            field.addElement('value', content=value)
    250260
    251         if self.fieldType in ('list-single', 'list-multi'):
    252             for option in self.options:
    253                 field.addChild(option.toElement())
    254 
    255         if self.label is not None:
    256             field['label'] = self.label
    257 
    258         if self.desc is not None:
    259             field.addElement('desc', content=self.desc)
    260 
    261         if self.required:
    262             field.addElement('required')
     261        if asForm:
     262            if self.fieldType in ('list-single', 'list-multi'):
     263                for option in self.options:
     264                    field.addChild(option.toElement())
     265
     266            if self.label is not None:
     267                field['label'] = self.label
     268
     269            if self.desc is not None:
     270                field.addElement('desc', content=self.desc)
     271
     272            if self.required:
     273                field.addElement('required')
    263274
    264275        return field
     
    285296    def _parse_value(field, element):
    286297        value = unicode(element)
    287         if field.fieldType == 'boolean':
    288             value = value.lower() in ('1', 'true')
    289         elif field.fieldType in ('jid-multi', 'jid-single'):
    290             value = JID(value)
    291298        field.values.append(value)
    292299
     
    425432
    426433        for field in self.fieldList:
    427             form.addChild(field.toElement())
     434            form.addChild(field.toElement(self.formType=='form'))
    428435
    429436        return form
  • wokkel/disco.py

    r108 r128  
    11# -*- test-case-name: wokkel.test.test_disco -*-
    22#
    3 # Copyright (c) 2003-2008 Ralph Meijer
     3# Copyright (c) 2003-2009 Ralph Meijer
    44# See LICENSE for details.
    55
     
    1515from twisted.words.xish import domish
    1616
     17from wokkel import data_form
    1718from wokkel.iwokkel import IDisco
    1819from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
    1920
    20 NS = 'http://jabber.org/protocol/disco'
    21 NS_INFO = NS + '#info'
    22 NS_ITEMS = NS + '#items'
     21NS_DISCO = 'http://jabber.org/protocol/disco'
     22NS_DISCO_INFO = NS_DISCO + '#info'
     23NS_DISCO_ITEMS = NS_DISCO + '#items'
    2324
    2425IQ_GET = '/iq[@type="get"]'
    25 DISCO_INFO = IQ_GET + '/query[@xmlns="' + NS_INFO + '"]'
    26 DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + NS_ITEMS + '"]'
    27 
    28 class DiscoFeature(domish.Element):
    29     """
    30     Element representing an XMPP service discovery feature.
    31     """
    32 
    33     def __init__(self, feature):
    34         domish.Element.__init__(self, (NS_INFO, 'feature'),
    35                                 attribs={'var': feature})
    36 
    37 
    38 class DiscoIdentity(domish.Element):
    39     """
    40     Element representing an XMPP service discovery identity.
    41     """
    42 
    43     def __init__(self, category, type, name = None):
    44         domish.Element.__init__(self, (NS_INFO, 'identity'),
    45                                 attribs={'category': category,
    46                                          'type': type})
    47         if name:
    48             self['name'] = name
    49 
    50 
    51 class DiscoItem(domish.Element):
    52     """
    53     Element representing an XMPP service discovery item.
    54     """
    55 
    56     def __init__(self, jid, node='', name=None):
    57         domish.Element.__init__(self, (NS_ITEMS, 'item'),
    58                                 attribs={'jid': jid.full()})
    59         if node:
    60             self['node'] = node
    61 
    62         if name:
    63             self['name'] = name
     26DISCO_INFO = IQ_GET + '/query[@xmlns="' + NS_DISCO_INFO + '"]'
     27DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + NS_DISCO_ITEMS + '"]'
     28
     29class DiscoFeature(unicode):
     30    """
     31    XMPP service discovery feature.
     32
     33    This extends C{unicode} to convert to and from L{domish.Element}, but
     34    further behaves identically.
     35    """
     36
     37    def toElement(self):
     38        """
     39        Render to a DOM representation.
     40
     41        @rtype: L{domish.Element}.
     42        """
     43        element = domish.Element((NS_DISCO_INFO, 'feature'))
     44        element['var'] = unicode(self)
     45        return element
     46
     47
     48    @staticmethod
     49    def fromElement(element):
     50        """
     51        Parse a DOM representation into a L{DiscoFeature} instance.
     52
     53        @param element: Element that represents the disco feature.
     54        @type element: L{domish.Element}.
     55        @rtype L{DiscoFeature}.
     56        """
     57        featureURI = element.getAttribute('var', u'')
     58        feature = DiscoFeature(featureURI)
     59        return feature
     60
     61
     62
     63class DiscoIdentity(object):
     64    """
     65    XMPP service discovery identity.
     66
     67    @ivar category: The identity category.
     68    @type category: C{unicode}
     69    @ivar type: The identity type.
     70    @type type: C{unicode}
     71    @ivar name: The optional natural language name for this entity.
     72    @type name: C{unicode}
     73    """
     74
     75    def __init__(self, category, idType, name=None):
     76        self.category = category
     77        self.type = idType
     78        self.name = name
     79
     80
     81    def toElement(self):
     82        """
     83        Generate a DOM representation.
     84
     85        @rtype: L{domish.Element}.
     86        """
     87        element = domish.Element((NS_DISCO_INFO, 'identity'))
     88        if self.category:
     89            element['category'] = self.category
     90        if self.type:
     91            element['type'] = self.type
     92        if self.name:
     93            element['name'] = self.name
     94        return element
     95
     96
     97    @staticmethod
     98    def fromElement(element):
     99        """
     100        Parse a DOM representation into a L{DiscoIdentity} instance.
     101
     102        @param element: Element that represents the disco identity.
     103        @type element: L{domish.Element}.
     104        @rtype L{DiscoIdentity}.
     105        """
     106        category = element.getAttribute('category')
     107        idType = element.getAttribute('type')
     108        name = element.getAttribute('name')
     109        feature = DiscoIdentity(category, idType, name)
     110        return feature
     111
     112
     113
     114class DiscoInfo(object):
     115    """
     116    XMPP service discovery info.
     117
     118    @ivar nodeIdentifier: The optional node this info applies to.
     119    @type nodeIdentifier: C{unicode}
     120    @ivar features: Features as L{DiscoFeature}.
     121    @type features: C{set)
     122    @ivar identities: Identities as a mapping from (category, type) to name,
     123                      all C{unicode}.
     124    @type identities: C{dict}
     125    @ivar extensions: Service discovery extensions as a mapping from the
     126                      extension form's C{FORM_TYPE} (C{unicode}) to
     127                      L{data_form.Form}. Forms with no C{FORM_TYPE} field
     128                      are mapped as C{None}. Note that multiple forms
     129                      with the same C{FORM_TYPE} have the last in sequence
     130                      prevail.
     131    @type extensions: C{dict}
     132    @ivar _items: Sequence of added items.
     133    @type _items: C{list}
     134    """
     135
     136    def __init__(self):
     137        self.nodeIdentifier = ''
     138        self.features = set()
     139        self.identities = {}
     140        self.extensions = {}
     141        self._items = []
     142
     143
     144    def __iter__(self):
     145        """
     146        Iterator over sequence of items in the order added.
     147        """
     148        return iter(self._items)
     149
     150
     151    def append(self, item):
     152        """
     153        Add a piece of service discovery info.
     154
     155        @param item: A feature, identity or extension form.
     156        @type item: L{DiscoFeature}, L{DiscoIdentity} or L{data_form.Form}
     157        """
     158        self._items.append(item)
     159
     160        if isinstance(item, DiscoFeature):
     161            self.features.add(item)
     162        elif isinstance(item, DiscoIdentity):
     163            self.identities[(item.category, item.type)] = item.name
     164        elif isinstance(item, data_form.Form):
     165            self.extensions[item.formNamespace] = item
     166
     167
     168    def toElement(self):
     169        """
     170        Generate a DOM representation.
     171
     172        This takes the items added with C{append} to create a DOM
     173        representation of service discovery information.
     174
     175        @rtype: L{domish.Element}.
     176        """
     177        element = domish.Element((NS_DISCO_INFO, 'query'))
     178
     179        if self.nodeIdentifier:
     180            element['node'] = self.nodeIdentifier
     181
     182        for item in self:
     183            element.addChild(item.toElement())
     184
     185        return element
     186
     187
     188    @staticmethod
     189    def fromElement(element):
     190        """
     191        Parse a DOM representation into a L{DiscoInfo} instance.
     192
     193        @param element: Element that represents the disco info.
     194        @type element: L{domish.Element}.
     195        @rtype L{DiscoInfo}.
     196        """
     197
     198        info = DiscoInfo()
     199
     200        info.nodeIdentifier = element.getAttribute('node', '')
     201
     202        for child in element.elements():
     203            item = None
     204
     205            if (child.uri, child.name) == (NS_DISCO_INFO, 'feature'):
     206                item = DiscoFeature.fromElement(child)
     207            elif (child.uri, child.name) == (NS_DISCO_INFO, 'identity'):
     208                item = DiscoIdentity.fromElement(child)
     209            elif (child.uri, child.name) == (data_form.NS_X_DATA, 'x'):
     210                item = data_form.Form.fromElement(child)
     211
     212            if item:
     213                info.append(item)
     214
     215        return info
     216
     217
     218
     219class DiscoItem(object):
     220    """
     221    XMPP service discovery item.
     222
     223    @ivar entity: The entity holding the item.
     224    @type entity: L{jid.JID}
     225    @ivar nodeIdentifier: The optional node identifier for the item.
     226    @type nodeIdentifier: C{unicode}
     227    @ivar name: The optional natural language name for this entity.
     228    @type name: C{unicode}
     229    """
     230
     231    def __init__(self, entity, nodeIdentifier='', name=None):
     232        self.entity = entity
     233        self.nodeIdentifier = nodeIdentifier
     234        self.name = name
     235
     236
     237    def toElement(self):
     238        """
     239        Generate a DOM representation.
     240
     241        @rtype: L{domish.Element}.
     242        """
     243        element = domish.Element((NS_DISCO_ITEMS, 'item'))
     244        if self.entity:
     245            element['jid'] = self.entity.full()
     246        if self.nodeIdentifier:
     247            element['node'] = self.nodeIdentifier
     248        if self.name:
     249            element['name'] = self.name
     250        return element
     251
     252
     253    @staticmethod
     254    def fromElement(element):
     255        """
     256        Parse a DOM representation into a L{DiscoItem} instance.
     257
     258        @param element: Element that represents the disco iitem.
     259        @type element: L{domish.Element}.
     260        @rtype L{DiscoItem}.
     261        """
     262        try:
     263            entity = jid.JID(element.getAttribute('jid', ' '))
     264        except jid.InvalidFormat:
     265            entity = None
     266        nodeIdentifier = element.getAttribute('node', '')
     267        name = element.getAttribute('name')
     268        feature = DiscoItem(entity, nodeIdentifier, name)
     269        return feature
     270
     271
     272
     273class DiscoItems(object):
     274    """
     275    XMPP service discovery items.
     276
     277    @ivar nodeIdentifier: The optional node this info applies to.
     278    @type nodeIdentifier: C{unicode}
     279    @ivar _items: Sequence of added items.
     280    @type _items: C{list}
     281    """
     282
     283    def __init__(self):
     284        self.nodeIdentifier = ''
     285        self._items = []
     286
     287
     288    def __iter__(self):
     289        """
     290        Iterator over sequence of items in the order added.
     291        """
     292        return iter(self._items)
     293
     294
     295    def append(self, item):
     296        """
     297        Append item to the sequence of items.
     298
     299        @param item: Item to be added.
     300        @type item: L{DiscoItem}
     301        """
     302        self._items.append(item)
     303
     304
     305    def toElement(self):
     306        """
     307        Generate a DOM representation.
     308
     309        This takes the items added with C{append} to create a DOM
     310        representation of service discovery items.
     311
     312        @rtype: L{domish.Element}.
     313        """
     314        element = domish.Element((NS_DISCO_ITEMS, 'query'))
     315
     316        if self.nodeIdentifier:
     317            element['node'] = self.nodeIdentifier
     318
     319        for item in self:
     320            element.addChild(item.toElement())
     321
     322        return element
     323
     324
     325    @staticmethod
     326    def fromElement(element):
     327        """
     328        Parse a DOM representation into a L{DiscoItems} instance.
     329
     330        @param element: Element that represents the disco items.
     331        @type element: L{domish.Element}.
     332        @rtype L{DiscoItems}.
     333        """
     334
     335        info = DiscoItems()
     336
     337        info.nodeIdentifier = element.getAttribute('node', '')
     338
     339        for child in element.elements():
     340            if (child.uri, child.name) == (NS_DISCO_ITEMS, 'item'):
     341                item = DiscoItem.fromElement(child)
     342                info.append(item)
     343
     344        return info
     345
     346
     347
     348class _DiscoRequest(xmlstream.IQ):
     349    """
     350    Element representing an XMPP service discovery request.
     351    """
     352
     353    def __init__(self, xs, namespace, nodeIdentifier=''):
     354        """
     355        Initialize the request.
     356
     357        @param xs: XML Stream the request should go out on.
     358        @type xs: L{xmlstream.XmlStream}
     359        @param namespace: Request namespace.
     360        @type namespace: C{str}
     361        @param nodeIdentifier: Node to request info from.
     362        @type nodeIdentifier: C{unicode}
     363        """
     364        xmlstream.IQ.__init__(self, xs, "get")
     365        query = self.addElement((namespace, 'query'))
     366        if nodeIdentifier:
     367            query['node'] = nodeIdentifier
     368
     369
     370
     371class DiscoClientProtocol(XMPPHandler):
     372    """
     373    XMPP Service Discovery client protocol.
     374    """
     375
     376    def requestInfo(self, entity, nodeIdentifier=''):
     377        """
     378        Request information discovery from a node.
     379
     380        @param entity: Entity to send the request to.
     381        @type entity: L{jid.JID}
     382        @param nodeIdentifier: Optional node to request info from.
     383        @type nodeIdentifier: C{unicode}
     384        """
     385
     386        request = _DiscoRequest(self.xmlstream, NS_DISCO_INFO, nodeIdentifier)
     387
     388        d = request.send(entity.full())
     389        d.addCallback(lambda iq: DiscoInfo.fromElement(iq.query))
     390        return d
     391
     392
     393    def requestItems(self, entity, nodeIdentifier=''):
     394        """
     395        Request items discovery from a node.
     396
     397        @param entity: Entity to send the request to.
     398        @type entity: L{jid.JID}
     399        @param nodeIdentifier: Optional node to request info from.
     400        @type nodeIdentifier: C{unicode}
     401        """
     402
     403        request = _DiscoRequest(self.xmlstream, NS_DISCO_ITEMS, nodeIdentifier)
     404
     405        d = request.send(entity.full())
     406        d.addCallback(lambda iq: DiscoItems.fromElement(iq.query))
     407        return d
     408
    64409
    65410
     
    96441        self.xmlstream.addObserver(DISCO_ITEMS, self.handleRequest)
    97442
    98     def _error(self, failure):
    99         failure.trap(defer.FirstError)
    100         return failure.value.subFailure
    101443
    102444    def _onDiscoInfo(self, iq):
     445        """
     446        Called for incoming disco info requests.
     447
     448        @param iq: The request iq element.
     449        @type iq: L{Element<twisted.words.xish.domish.Element>}
     450        """
    103451        requestor = jid.internJID(iq["from"])
    104452        target = jid.internJID(iq["to"])
    105453        nodeIdentifier = iq.query.getAttribute("node", '')
    106454
    107         def toResponse(results):
    108             info = []
    109             for i in results:
    110                 info.extend(i[1])
    111 
     455        def toResponse(info):
    112456            if nodeIdentifier and not info:
    113457                raise error.StanzaError('item-not-found')
    114458            else:
    115                 response = domish.Element((NS_INFO, 'query'))
     459                response = DiscoInfo()
     460                response.nodeIdentifier = nodeIdentifier
    116461
    117462                for item in info:
    118                     response.addChild(item)
    119 
    120             return response
    121 
    122         dl = []
    123         for handler in self.parent:
    124             if IDisco.providedBy(handler):
    125                 dl.append(handler.getDiscoInfo(requestor, target,
    126                                                nodeIdentifier))
    127 
    128         d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=1)
    129         d.addCallbacks(toResponse, self._error)
     463                    response.append(item)
     464
     465            return response.toElement()
     466
     467        d = self.info(requestor, target, nodeIdentifier)
     468        d.addCallback(toResponse)
    130469        return d
    131470
     471
    132472    def _onDiscoItems(self, iq):
     473        """
     474        Called for incoming disco items requests.
     475
     476        @param iq: The request iq element.
     477        @type iq: L{Element<twisted.words.xish.domish.Element>}
     478        """
    133479        requestor = jid.internJID(iq["from"])
    134480        target = jid.internJID(iq["to"])
    135481        nodeIdentifier = iq.query.getAttribute("node", '')
    136482
    137         def toResponse(results):
    138             items = []
    139             for i in results:
    140                 items.extend(i[1])
    141 
    142             response = domish.Element((NS_ITEMS, 'query'))
     483        def toResponse(items):
     484            response = DiscoItems()
     485            response.nodeIdentifier = nodeIdentifier
    143486
    144487            for item in items:
    145                 response.addChild(item)
    146 
    147             return response
    148 
    149         dl = []
    150         for handler in self.parent:
    151             if IDisco.providedBy(handler):
    152                 dl.append(handler.getDiscoItems(requestor, target,
    153                                                 nodeIdentifier))
    154 
    155         d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=1)
    156         d.addCallbacks(toResponse, self._error)
     488                response.append(item)
     489
     490            return response.toElement()
     491
     492        d = self.items(requestor, target, nodeIdentifier)
     493        d.addCallback(toResponse)
    157494        return d
     495
     496
     497    def _gatherResults(self, deferredList):
     498        """
     499        Gather results from a list of deferreds.
     500
     501        Similar to L{defer.gatherResults}, but flattens the returned results,
     502        consumes errors after the first one and fires the errback of the
     503        returned deferred with the failure of the first deferred that fires its
     504        errback.
     505
     506        @param deferredList: List of deferreds for which the results should be
     507                             gathered.
     508        @type deferredList: C{list}
     509        @return: Deferred that fires with a list of gathered results.
     510        @rtype: L{defer.Deferred}
     511        """
     512        def cb(resultList):
     513            results = []
     514            for success, value in resultList:
     515                results.extend(value)
     516            return results
     517
     518        def eb(failure):
     519            failure.trap(defer.FirstError)
     520            return failure.value.subFailure
     521
     522        d = defer.DeferredList(deferredList, fireOnOneErrback=1,
     523                                             consumeErrors=1)
     524        d.addCallbacks(cb, eb)
     525        return d
     526
     527
     528    def info(self, requestor, target, nodeIdentifier):
     529        """
     530        Inspect all sibling protocol handlers for disco info.
     531
     532        Calls the L{getDiscoInfo<IDisco.getDiscoInfo>} method on all child
     533        handlers of the parent, that provide L{IDisco}.
     534
     535        @param requestor: The entity that sent the request.
     536        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
     537        @param target: The entity the request was sent to.
     538        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
     539        @param nodeIdentifier: The optional node being queried, or C{''}.
     540        @type nodeIdentifier: C{unicode}
     541        @return: Deferred with the gathered results from sibling handlers.
     542        @rtype: L{defer.Deferred}
     543        """
     544        dl = [handler.getDiscoInfo(requestor, target, nodeIdentifier)
     545              for handler in self.parent
     546              if IDisco.providedBy(handler)]
     547        return self._gatherResults(dl)
     548
     549
     550    def items(self, requestor, target, nodeIdentifier):
     551        """
     552        Inspect all sibling protocol handlers for disco items.
     553
     554        Calls the L{getDiscoItems<IDisco.getDiscoItems>} method on all child
     555        handlers of the parent, that provide L{IDisco}.
     556
     557        @param requestor: The entity that sent the request.
     558        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
     559        @param target: The entity the request was sent to.
     560        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
     561        @param nodeIdentifier: The optional node being queried, or C{''}.
     562        @type nodeIdentifier: C{unicode}
     563        @return: Deferred with the gathered results from sibling handlers.
     564        @rtype: L{defer.Deferred}
     565        """
     566        dl = [handler.getDiscoItems(requestor, target, nodeIdentifier)
     567              for handler in self.parent
     568              if IDisco.providedBy(handler)]
     569        return self._gatherResults(dl)
  • wokkel/generic.py

    r30 r57  
    1010from zope.interface import implements
    1111
    12 from twisted.internet import defer
    13 from twisted.words.protocols.jabber import error
     12from twisted.internet import defer, protocol
     13from twisted.words.protocols.jabber import error, jid, xmlstream
    1414from twisted.words.protocols.jabber.xmlstream import toResponse
    15 from twisted.words.xish import domish
     15from twisted.words.xish import domish, utility
     16
     17try:
     18    from twisted.words.xish.xmlstream import BootstrapMixin
     19except ImportError:
     20    from wokkel.compat import BootstrapMixin
    1621
    1722from wokkel import disco
     
    121126    def getDiscoItems(self, requestor, target, node):
    122127        return defer.succeed([])
     128
     129
     130
     131class XmlPipe(object):
     132    """
     133    XML stream pipe.
     134
     135    Connects two objects that communicate stanzas through an XML stream like
     136    interface. Each of the ends of the pipe (sink and source) can be used to
     137    send XML stanzas to the other side, or add observers to process XML stanzas
     138    that were sent from the other side.
     139
     140    XML pipes are usually used in place of regular XML streams that are
     141    transported over TCP. This is the reason for the use of the names source
     142    and sink for both ends of the pipe. The source side corresponds with the
     143    entity that initiated the TCP connection, whereas the sink corresponds with
     144    the entity that accepts that connection. In this object, though, the source
     145    and sink are treated equally.
     146
     147    Unlike Jabber
     148    L{XmlStream<twisted.words.protocols.jabber.xmlstream.XmlStream>}s, the sink
     149    and source objects are assumed to represent an eternal connected and
     150    initialized XML stream. As such, events corresponding to connection,
     151    disconnection, initialization and stream errors are not dispatched or
     152    processed.
     153
     154    @ivar source: Source XML stream.
     155    @ivar sink: Sink XML stream.
     156    """
     157
     158    def __init__(self):
     159        self.source = utility.EventDispatcher()
     160        self.sink = utility.EventDispatcher()
     161        self.source.send = lambda obj: self.sink.dispatch(obj)
     162        self.sink.send = lambda obj: self.source.dispatch(obj)
     163
     164
     165
     166class Stanza(object):
     167    """
     168    Abstract representation of a stanza.
     169
     170    @ivar sender: The sending entity.
     171    @type sender: L{jid.JID}
     172    @ivar recipient: The receiving entity.
     173    @type recipient: L{jid.JID}
     174    """
     175
     176    sender = None
     177    recipient = None
     178    stanzaType = None
     179
     180    @classmethod
     181    def fromElement(Class, element):
     182        stanza = Class()
     183        stanza.parseElement(element)
     184        return stanza
     185
     186
     187    def parseElement(self, element):
     188        self.sender = jid.internJID(element['from'])
     189        if element.hasAttribute('from'):
     190            self.sender = jid.internJID(element['from'])
     191        if element.hasAttribute('to'):
     192            self.recipient = jid.internJID(element['to'])
     193        self.stanzaType = element.getAttribute('type')
     194
     195
     196class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
     197    protocol = xmlstream.XmlStream
     198
     199    def __init__(self, authenticator):
     200        BootstrapMixin.__init__(self)
     201
     202        self.authenticator = authenticator
     203
     204        deferred = defer.Deferred()
     205        self.deferred = deferred
     206        self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.deferred.callback)
     207        self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback)
     208
     209
     210    def buildProtocol(self, addr):
     211        """
     212        Create an instance of XmlStream.
     213
     214        A new authenticator instance will be created and passed to the new
     215        XmlStream. Registered bootstrap event observers are installed as well.
     216        """
     217        xs = self.protocol(self.authenticator)
     218        xs.factory = self
     219        self.installBootstraps(xs)
     220        return xs
     221
     222
     223    def clientConnectionFailed(self, connector, reason):
     224        self.deferred.errback(reason)
  • wokkel/iwokkel.py

    r113 r128  
    1616    """
    1717
    18     manager = Attribute("""XML stream manager""")
     18    parent = Attribute("""XML stream manager for this handler""")
    1919    xmlstream = Attribute("""The managed XML stream""")
    2020
     
    2626        """
    2727
     28
    2829    def disownHandlerParent(parent):
    2930        """
     
    3233        @type parent: L{IXMPPHandlerCollection}
    3334        """
     35
    3436
    3537    def makeConnection(xs):
     
    4547        @type xs: L{XmlStream<twisted.words.protocols.jabber.XmlStream>}
    4648        """
     49
    4750
    4851    def connectionMade():
     
    5558        """
    5659
     60
    5761    def connectionInitialized():
    5862        """
     
    6468        """
    6569
     70
    6671    def connectionLost(reason):
    6772        """
     
    7378        @type reason: L{twisted.python.failure.Failure}
    7479        """
     80
    7581
    7682
     
    8793        """
    8894
     95
    8996    def addHandler(handler):
    9097        """
     
    94101        """
    95102
     103
    96104    def removeHandler(handler):
    97105        """
     
    100108        @type handler: L{IXMPPHandler}
    101109        """
     110
    102111
    103112
     
    270279        """
    271280
    272     def notifyDelete(service, nodeIdentifier, subscriptions):
     281
     282    def notifyDelete(service, nodeIdentifier, subscribers,
     283                     redirectURI=None):
    273284        """
    274285        Send out node deletion notifications.
     
    278289        @param nodeIdentifier: The identifier of the node that was deleted.
    279290        @type nodeIdentifier: C{unicode}
    280         @param subscriptions: The subscriptions for which a notification should
    281                               be sent out.
    282         @type subscriptions: C{list} of L{jid.JID}
     291        @param subscribers: The subscribers for which a notification should
     292                            be sent out.
     293        @type subscribers: C{list} of L{jid.JID}
     294        @param redirectURI: Optional XMPP URI of another node that subscribers
     295                            are redirected to.
     296        @type redirectURI: C{str}
    283297        """
    284298
     
    387401        that option in a dictionary:
    388402
    389         - C{'type'} (C{str}): The option's type (see
    390           L{Field<wokkel.data_form.Field>}'s doc string for possible values).
    391         - C{'label'} (C{unicode}): A human readable label for this option.
    392         - C{'options'} (C{dict}): Optional list of possible values for this
    393           option.
     403         - C{'type'} (C{str}): The option's type (see
     404           L{Field<wokkel.data_form.Field>}'s doc string for possible values).
     405         - C{'label'} (C{unicode}): A human readable label for this option.
     406         - C{'options'} (C{dict}): Optional list of possible values for this
     407           option.
    394408
    395409        Example::
     
    414428        """
    415429
    416     def getDefaultConfiguration(requestor, service):
     430    def getDefaultConfiguration(requestor, service, nodeType):
    417431        """
    418432        Called when a default node configuration request has been received.
     
    422436        @param service: The entity the request was addressed to.
    423437        @type service: L{jid.JID}
     438        @param nodeType: The type of node for which the configuration is
     439                         retrieved, C{'leaf'} or C{'collection'}.
     440        @type nodeType: C{str}
    424441        @return: A deferred that fires with a C{dict} representing the default
    425442                 node configuration. Keys are C{str}s that represent the
     
    511528        @type nodeIdentifier: C{unicode}
    512529        """
     530
     531
     532
     533class IPubSubResource(Interface):
     534
     535    def locateResource(request):
     536        """
     537        Locate a resource that will handle the request.
     538
     539        @param request: The publish-subscribe request.
     540        @type request: L{wokkel.pubsub.PubSubRequest}
     541        """
     542
     543
     544    def getInfo(requestor, service, nodeIdentifier):
     545        """
     546        Get node type and meta data.
     547
     548        @param requestor: The entity the request originated from.
     549        @type requestor: L{jid.JID}
     550        @param service: The publish-subscribe service entity.
     551        @type service: L{jid.JID}
     552        @param nodeIdentifier: Identifier of the node to request the info for.
     553        @type nodeIdentifier: L{unicode}
     554        @return: A deferred that fires with a dictionary. If not empty,
     555                 it must have the keys C{'type'} and C{'meta-data'} to keep
     556                 respectively the node type and a dictionary with the meta
     557                 data for that node.
     558        @rtype: L{defer.Deferred}
     559        """
     560
     561
     562    def getNodes(requestor, service, nodeIdentifier):
     563        """
     564        Get all nodes contained by this node.
     565
     566        @param requestor: The entity the request originated from.
     567        @type requestor: L{jid.JID}
     568        @param service: The publish-subscribe service entity.
     569        @type service: L{jid.JID}
     570        @param nodeIdentifier: Identifier of the node to request the childs for.
     571        @type nodeIdentifier: L{unicode}
     572        @return: A deferred that fires with a list of child node identifiers.
     573        @rtype: L{defer.Deferred}
     574        """
     575
     576
     577    def getConfigurationOptions():
     578        """
     579        Retrieve all known node configuration options.
     580
     581        The returned dictionary holds the possible node configuration options
     582        by option name. The value of each entry represents the specifics for
     583        that option in a dictionary:
     584
     585         - C{'type'} (C{str}): The option's type (see
     586           L{Field<wokkel.data_form.Field>}'s doc string for possible values).
     587         - C{'label'} (C{unicode}): A human readable label for this option.
     588         - C{'options'} (C{dict}): Optional list of possible values for this
     589           option.
     590
     591        Example::
     592
     593            {
     594            "pubsub#persist_items":
     595                {"type": "boolean",
     596                 "label": "Persist items to storage"},
     597            "pubsub#deliver_payloads":
     598                {"type": "boolean",
     599                 "label": "Deliver payloads with event notifications"},
     600            "pubsub#send_last_published_item":
     601                {"type": "list-single",
     602                 "label": "When to send the last published item",
     603                 "options": {
     604                     "never": "Never",
     605                     "on_sub": "When a new subscription is processed"}
     606                }
     607            }
     608
     609        @rtype: C{dict}.
     610        """
     611
     612
     613    def publish(request):
     614        """
     615        Called when a publish request has been received.
     616
     617        @param request: The publish-subscribe request.
     618        @type request: L{wokkel.pubsub.PubSubRequest}
     619        @return: deferred that fires on success.
     620        @rtype: L{defer.Deferred}
     621        """
     622
     623
     624    def subscribe(request):
     625        """
     626        Called when a subscribe request has been received.
     627
     628        @param request: The publish-subscribe request.
     629        @type request: L{wokkel.pubsub.PubSubRequest}
     630        @return: A deferred that fires with a
     631                 L{Subscription<wokkel.pubsub.Subscription>}.
     632        @rtype: L{defer.Deferred}
     633        """
     634
     635
     636    def unsubscribe(request):
     637        """
     638        Called when a subscribe request has been received.
     639
     640        @param request: The publish-subscribe request.
     641        @type request: L{wokkel.pubsub.PubSubRequest}
     642        @return: A deferred that fires with C{None} when unsubscription has
     643                 succeeded.
     644        @rtype: L{defer.Deferred}
     645        """
     646
     647
     648    def subscriptions(request):
     649        """
     650        Called when a subscriptions retrieval request has been received.
     651
     652        @param request: The publish-subscribe request.
     653        @type request: L{wokkel.pubsub.PubSubRequest}
     654        @return: A deferred that fires with a C{list} of subscriptions as
     655                 L{Subscription<wokkel.pubsub.Subscription>}.
     656        @rtype: L{defer.Deferred}
     657        """
     658
     659
     660    def affiliations(request):
     661        """
     662        Called when a affiliations retrieval request has been received.
     663
     664        @param request: The publish-subscribe request.
     665        @type request: L{wokkel.pubsub.PubSubRequest}
     666        @return: A deferred that fires with a C{list} of affiliations as
     667                 C{tuple}s of (node identifier as C{unicode}, affiliation state
     668                 as C{str}). The affiliation can be C{'owner'}, C{'publisher'},
     669                 or C{'outcast'}.
     670        @rtype: L{defer.Deferred}
     671        """
     672
     673
     674    def create(request):
     675        """
     676        Called when a node creation request has been received.
     677
     678        @param request: The publish-subscribe request.
     679        @type request: L{wokkel.pubsub.PubSubRequest}
     680        @return: A deferred that fires with a C{unicode} that represents
     681                 the identifier of the new node.
     682        @rtype: L{defer.Deferred}
     683        """
     684
     685
     686    def default(request):
     687        """
     688        Called when a default node configuration request has been received.
     689
     690        @param request: The publish-subscribe request.
     691        @type request: L{wokkel.pubsub.PubSubRequest}
     692        @return: A deferred that fires with a C{dict} representing the default
     693                 node configuration. Keys are C{str}s that represent the
     694                 field name. Values can be of types C{unicode}, C{int} or
     695                 C{bool}.
     696        @rtype: L{defer.Deferred}
     697        """
     698
     699
     700    def configureGet(request):
     701        """
     702        Called when a node configuration retrieval request has been received.
     703
     704        @param request: The publish-subscribe request.
     705        @type request: L{wokkel.pubsub.PubSubRequest}
     706        @return: A deferred that fires with a C{dict} representing the node
     707                 configuration. Keys are C{str}s that represent the field name.
     708                 Values can be of types C{unicode}, C{int} or C{bool}.
     709        @rtype: L{defer.Deferred}
     710        """
     711
     712
     713    def configureSet(request):
     714        """
     715        Called when a node configuration change request has been received.
     716
     717        @param request: The publish-subscribe request.
     718        @type request: L{wokkel.pubsub.PubSubRequest}
     719        @return: A deferred that fires with C{None} when the node's
     720                 configuration has been changed.
     721        @rtype: L{defer.Deferred}
     722        """
     723
     724
     725    def items(request):
     726        """
     727        Called when a items retrieval request has been received.
     728
     729        @param request: The publish-subscribe request.
     730        @type request: L{wokkel.pubsub.PubSubRequest}
     731        @return: A deferred that fires with a C{list} of L{pubsub.Item}.
     732        @rtype: L{defer.Deferred}
     733        """
     734
     735
     736    def retract(request):
     737        """
     738        Called when a item retraction request has been received.
     739
     740        @param request: The publish-subscribe request.
     741        @type request: L{wokkel.pubsub.PubSubRequest}
     742        @return: A deferred that fires with C{None} when the given items have
     743                 been retracted.
     744        @rtype: L{defer.Deferred}
     745        """
     746
     747
     748    def purge(request):
     749        """
     750        Called when a node purge request has been received.
     751
     752        @param request: The publish-subscribe request.
     753        @type request: L{wokkel.pubsub.PubSubRequest}
     754        @return: A deferred that fires with C{None} when the node has been
     755                 purged.
     756        @rtype: L{defer.Deferred}
     757        """
     758
     759
     760    def delete(request):
     761        """
     762        Called when a node deletion request has been received.
     763
     764        @param request: The publish-subscribe request.
     765        @type request: L{wokkel.pubsub.PubSubRequest}
     766        @return: A deferred that fires with C{None} when the node has been
     767                 deleted.
     768        @rtype: L{defer.Deferred}
     769        """
     770
    513771
    514772
     
    655913        """
    656914        """
    657 
  • wokkel/pubsub.py

    r30 r59  
    1414
    1515from twisted.internet import defer
     16from twisted.python import log
    1617from twisted.words.protocols.jabber import jid, error, xmlstream
    1718from twisted.words.xish import domish
    1819
    19 from wokkel import disco, data_form, shim
     20from wokkel import disco, data_form, generic, shim
    2021from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
    21 from wokkel.iwokkel import IPubSubClient, IPubSubService
     22from wokkel.iwokkel import IPubSubClient, IPubSubService, IPubSubResource
    2223
    2324# Iq get and set XPath queries
     
    3233NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config"
    3334NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data"
    34 
    35 # In publish-subscribe namespace XPath query selector.
    36 IN_NS_PUBSUB = '[@xmlns="' + NS_PUBSUB + '"]'
    37 IN_NS_PUBSUB_OWNER = '[@xmlns="' + NS_PUBSUB_OWNER + '"]'
    38 
    39 # Publish-subscribe XPath queries
    40 PUBSUB_ELEMENT = '/pubsub' + IN_NS_PUBSUB
    41 PUBSUB_OWNER_ELEMENT = '/pubsub' + IN_NS_PUBSUB_OWNER
    42 PUBSUB_GET = IQ_GET + PUBSUB_ELEMENT
    43 PUBSUB_SET = IQ_SET + PUBSUB_ELEMENT
    44 PUBSUB_OWNER_GET = IQ_GET + PUBSUB_OWNER_ELEMENT
    45 PUBSUB_OWNER_SET = IQ_SET + PUBSUB_OWNER_ELEMENT
    46 
    47 # Publish-subscribe command XPath queries
    48 PUBSUB_PUBLISH = PUBSUB_SET + '/publish' + IN_NS_PUBSUB
    49 PUBSUB_CREATE = PUBSUB_SET + '/create' + IN_NS_PUBSUB
    50 PUBSUB_SUBSCRIBE = PUBSUB_SET + '/subscribe' + IN_NS_PUBSUB
    51 PUBSUB_UNSUBSCRIBE = PUBSUB_SET + '/unsubscribe' + IN_NS_PUBSUB
    52 PUBSUB_OPTIONS_GET = PUBSUB_GET + '/options' + IN_NS_PUBSUB
    53 PUBSUB_OPTIONS_SET = PUBSUB_SET + '/options' + IN_NS_PUBSUB
    54 PUBSUB_DEFAULT = PUBSUB_OWNER_GET + '/default' + IN_NS_PUBSUB_OWNER
    55 PUBSUB_CONFIGURE_GET = PUBSUB_OWNER_GET + '/configure' + IN_NS_PUBSUB_OWNER
    56 PUBSUB_CONFIGURE_SET = PUBSUB_OWNER_SET + '/configure' + IN_NS_PUBSUB_OWNER
    57 PUBSUB_SUBSCRIPTIONS = PUBSUB_GET + '/subscriptions' + IN_NS_PUBSUB
    58 PUBSUB_AFFILIATIONS = PUBSUB_GET + '/affiliations' + IN_NS_PUBSUB
    59 PUBSUB_AFFILIATIONS_GET = PUBSUB_OWNER_GET + '/affiliations' + \
    60                           IN_NS_PUBSUB_OWNER
    61 PUBSUB_AFFILIATIONS_SET = PUBSUB_OWNER_SET + '/affiliations' + \
    62                           IN_NS_PUBSUB_OWNER
    63 PUBSUB_SUBSCRIPTIONS_GET = PUBSUB_OWNER_GET + '/subscriptions' + \
    64                           IN_NS_PUBSUB_OWNER
    65 PUBSUB_SUBSCRIPTIONS_SET = PUBSUB_OWNER_SET + '/subscriptions' + \
    66                           IN_NS_PUBSUB_OWNER
    67 PUBSUB_ITEMS = PUBSUB_GET + '/items' + IN_NS_PUBSUB
    68 PUBSUB_RETRACT = PUBSUB_SET + '/retract' + IN_NS_PUBSUB
    69 PUBSUB_PURGE = PUBSUB_OWNER_SET + '/purge' + IN_NS_PUBSUB_OWNER
    70 PUBSUB_DELETE = PUBSUB_OWNER_SET + '/delete' + IN_NS_PUBSUB_OWNER
     35NS_PUBSUB_SUBSCRIBE_OPTIONS = NS_PUBSUB + "#subscribe_options"
     36
     37# XPath to match pubsub requests
     38PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \
     39                    'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \
     40                           '@xmlns="' + NS_PUBSUB_OWNER + '"]'
    7141
    7242class SubscriptionPending(Exception):
     
    9969
    10070
    101 class BadRequest(PubSubError):
     71class BadRequest(error.StanzaError):
    10272    """
    10373    Bad request stanza error.
    10474    """
    10575    def __init__(self, pubsubCondition=None, text=None):
    106         PubSubError.__init__(self, 'bad-request', pubsubCondition, text)
     76        if pubsubCondition:
     77            appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition))
     78        else:
     79            appCondition = None
     80        error.StanzaError.__init__(self, 'bad-request',
     81                                         text=text,
     82                                         appCondition=appCondition)
    10783
    10884
     
    11086class Unsupported(PubSubError):
    11187    def __init__(self, feature, text=None):
     88        self.feature = feature
    11289        PubSubError.__init__(self, 'feature-not-implemented',
    11390                                   'unsupported',
     
    11592                                   text)
    11693
     94    def __str__(self):
     95        message = PubSubError.__str__(self)
     96        message += ', feature %r' % self.feature
     97        return message
    11798
    11899
     
    168149
    169150
    170 class _PubSubRequest(xmlstream.IQ):
    171     """
    172     Publish subscribe request.
    173 
    174     @ivar verb: Request verb
    175     @type verb: C{str}
    176     @ivar namespace: Request namespace.
    177     @type namespace: C{str}
    178     @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
    179     @type method: C{str}
    180     @ivar command: Command element of the request. This is the direct child of
    181                    the C{pubsub} element in the C{namespace} with the name
    182                    C{verb}.
    183     """
    184 
    185     def __init__(self, xs, verb, namespace=NS_PUBSUB, method='set'):
    186         xmlstream.IQ.__init__(self, xs, method)
    187         self.addElement((namespace, 'pubsub'))
    188 
    189         self.command = self.pubsub.addElement(verb)
    190 
    191 
    192     def send(self, to):
    193         """
    194         Send out request.
    195 
    196         Extends L{xmlstream.IQ.send} by requiring the C{to} parameter to be
    197         a L{JID} instance.
    198 
    199         @param to: Entity to send the request to.
    200         @type to: L{JID}
    201         """
    202         destination = to.full()
    203         return xmlstream.IQ.send(self, destination)
     151class PubSubRequest(generic.Stanza):
     152    """
     153    A publish-subscribe request.
     154
     155    The set of instance variables used depends on the type of request. If
     156    a variable is not applicable or not passed in the request, its value is
     157    C{None}.
     158
     159    @ivar verb: The type of publish-subscribe request. See L{_requestVerbMap}.
     160    @type verb: C{str}.
     161
     162    @ivar affiliations: Affiliations to be modified.
     163    @type affiliations: C{set}
     164    @ivar items: The items to be published, as L{domish.Element}s.
     165    @type items: C{list}
     166    @ivar itemIdentifiers: Identifiers of the items to be retrieved or
     167                           retracted.
     168    @type itemIdentifiers: C{set}
     169    @ivar maxItems: Maximum number of items to retrieve.
     170    @type maxItems: C{int}.
     171    @ivar nodeIdentifier: Identifier of the node the request is about.
     172    @type nodeIdentifier: C{unicode}
     173    @ivar nodeType: The type of node that should be created, or for which the
     174                    configuration is retrieved. C{'leaf'} or C{'collection'}.
     175    @type nodeType: C{str}
     176    @ivar options: Configurations options for nodes, subscriptions and publish
     177                   requests.
     178    @type options: L{data_form.Form}
     179    @ivar subscriber: The subscribing entity.
     180    @type subscriber: L{JID}
     181    @ivar subscriptionIdentifier: Identifier for a specific subscription.
     182    @type subscriptionIdentifier: C{unicode}
     183    @ivar subscriptions: Subscriptions to be modified, as a set of
     184                         L{Subscription}.
     185    @type subscriptions: C{set}
     186    """
     187
     188    verb = None
     189
     190    affiliations = None
     191    items = None
     192    itemIdentifiers = None
     193    maxItems = None
     194    nodeIdentifier = None
     195    nodeType = None
     196    options = None
     197    subscriber = None
     198    subscriptionIdentifier = None
     199    subscriptions = None
     200
     201    # Map request iq type and subelement name to request verb
     202    _requestVerbMap = {
     203        ('set', NS_PUBSUB, 'publish'): 'publish',
     204        ('set', NS_PUBSUB, 'subscribe'): 'subscribe',
     205        ('set', NS_PUBSUB, 'unsubscribe'): 'unsubscribe',
     206        ('get', NS_PUBSUB, 'options'): 'optionsGet',
     207        ('set', NS_PUBSUB, 'options'): 'optionsSet',
     208        ('get', NS_PUBSUB, 'subscriptions'): 'subscriptions',
     209        ('get', NS_PUBSUB, 'affiliations'): 'affiliations',
     210        ('set', NS_PUBSUB, 'create'): 'create',
     211        ('get', NS_PUBSUB_OWNER, 'default'): 'default',
     212        ('get', NS_PUBSUB_OWNER, 'configure'): 'configureGet',
     213        ('set', NS_PUBSUB_OWNER, 'configure'): 'configureSet',
     214        ('get', NS_PUBSUB, 'items'): 'items',
     215        ('set', NS_PUBSUB, 'retract'): 'retract',
     216        ('set', NS_PUBSUB_OWNER, 'purge'): 'purge',
     217        ('set', NS_PUBSUB_OWNER, 'delete'): 'delete',
     218        ('get', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsGet',
     219        ('set', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsSet',
     220        ('get', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsGet',
     221        ('set', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsSet',
     222    }
     223
     224    # Map request verb to request iq type and subelement name
     225    _verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems()))
     226
     227    # Map request verb to parameter handler names
     228    _parameters = {
     229        'publish': ['node', 'items'],
     230        'subscribe': ['nodeOrEmpty', 'jid'],
     231        'unsubscribe': ['nodeOrEmpty', 'jid'],
     232        'optionsGet': ['nodeOrEmpty', 'jid'],
     233        'optionsSet': ['nodeOrEmpty', 'jid', 'options'],
     234        'subscriptions': [],
     235        'affiliations': [],
     236        'create': ['nodeOrNone'],
     237        'default': ['default'],
     238        'configureGet': ['nodeOrEmpty'],
     239        'configureSet': ['nodeOrEmpty', 'configure'],
     240        'items': ['node', 'maxItems', 'itemIdentifiers'],
     241        'retract': ['node', 'itemIdentifiers'],
     242        'purge': ['node'],
     243        'delete': ['node'],
     244        'affiliationsGet': ['nodeOrEmpty'],
     245        'affiliationsSet': [],
     246        'subscriptionsGet': ['nodeOrEmpty'],
     247        'subscriptionsSet': [],
     248    }
     249
     250    def __init__(self, verb=None):
     251        self.verb = verb
     252
     253
     254    @staticmethod
     255    def _findForm(element, formNamespace):
     256        """
     257        Find a Data Form.
     258
     259        Look for an element that represents a Data Form with the specified
     260        form namespace as a child element of the given element.
     261        """
     262        if not element:
     263            return None
     264
     265        form = None
     266        for child in element.elements():
     267            try:
     268                form = data_form.Form.fromElement(child)
     269            except data_form.Error:
     270                continue
     271
     272            if form.formNamespace != NS_PUBSUB_NODE_CONFIG:
     273                continue
     274
     275        return form
     276
     277
     278    def _parse_node(self, verbElement):
     279        """
     280        Parse the required node identifier out of the verbElement.
     281        """
     282        try:
     283            self.nodeIdentifier = verbElement["node"]
     284        except KeyError:
     285            raise BadRequest('nodeid-required')
     286
     287
     288    def _render_node(self, verbElement):
     289        """
     290        Render the required node identifier on the verbElement.
     291        """
     292        if not self.nodeIdentifier:
     293            raise Exception("Node identifier is required")
     294
     295        verbElement['node'] = self.nodeIdentifier
     296
     297
     298    def _parse_nodeOrEmpty(self, verbElement):
     299        """
     300        Parse the node identifier out of the verbElement. May be empty.
     301        """
     302        self.nodeIdentifier = verbElement.getAttribute("node", '')
     303
     304
     305    def _render_nodeOrEmpty(self, verbElement):
     306        """
     307        Render the node identifier on the verbElement. May be empty.
     308        """
     309        if self.nodeIdentifier:
     310            verbElement['node'] = self.nodeIdentifier
     311
     312
     313    def _parse_nodeOrNone(self, verbElement):
     314        """
     315        Parse the optional node identifier out of the verbElement.
     316        """
     317        self.nodeIdentifier = verbElement.getAttribute("node")
     318
     319
     320    def _render_nodeOrNone(self, verbElement):
     321        """
     322        Render the optional node identifier on the verbElement.
     323        """
     324        if self.nodeIdentifier:
     325            verbElement['node'] = self.nodeIdentifier
     326
     327
     328    def _parse_items(self, verbElement):
     329        """
     330        Parse items out of the verbElement for publish requests.
     331        """
     332        self.items = []
     333        for element in verbElement.elements():
     334            if element.uri == NS_PUBSUB and element.name == 'item':
     335                self.items.append(element)
     336
     337
     338    def _render_items(self, verbElement):
     339        """
     340        Render items into the verbElement for publish requests.
     341        """
     342        if self.items:
     343            for item in self.items:
     344                verbElement.addChild(item)
     345
     346
     347    def _parse_jid(self, verbElement):
     348        """
     349        Parse subscriber out of the verbElement for un-/subscribe requests.
     350        """
     351        try:
     352            self.subscriber = jid.internJID(verbElement["jid"])
     353        except KeyError:
     354            raise BadRequest('jid-required')
     355
     356
     357    def _render_jid(self, verbElement):
     358        """
     359        Render subscriber into the verbElement for un-/subscribe requests.
     360        """
     361        verbElement['jid'] = self.subscriber.full()
     362
     363
     364    def _parse_default(self, verbElement):
     365        """
     366        Parse node type out of a request for the default node configuration.
     367        """
     368        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
     369        if form and form.formType == 'submit':
     370            values = form.getValues()
     371            self.nodeType = values.get('pubsub#node_type', 'leaf')
     372        else:
     373            self.nodeType = 'leaf'
     374
     375
     376    def _parse_configure(self, verbElement):
     377        """
     378        Parse options out of a request for setting the node configuration.
     379        """
     380        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
     381        if form:
     382            if form.formType == 'submit':
     383                self.options = form.getValues()
     384            elif form.formType == 'cancel':
     385                self.options = {}
     386            else:
     387                raise BadRequest(text="Unexpected form type %r" % form.formType)
     388        else:
     389            raise BadRequest(text="Missing configuration form")
     390
     391
     392
     393    def _parse_itemIdentifiers(self, verbElement):
     394        """
     395        Parse item identifiers out of items and retract requests.
     396        """
     397        self.itemIdentifiers = []
     398        for element in verbElement.elements():
     399            if element.uri == NS_PUBSUB and element.name == 'item':
     400                try:
     401                    self.itemIdentifiers.append(element["id"])
     402                except KeyError:
     403                    raise BadRequest()
     404
     405
     406    def _render_itemIdentifiers(self, verbElement):
     407        """
     408        Render item identifiers into items and retract requests.
     409        """
     410        if self.itemIdentifiers:
     411            for itemIdentifier in self.itemIdentifiers:
     412                item = verbElement.addElement('item')
     413                item['id'] = itemIdentifier
     414
     415
     416    def _parse_maxItems(self, verbElement):
     417        """
     418        Parse maximum items out of an items request.
     419        """
     420        value = verbElement.getAttribute('max_items')
     421
     422        if value:
     423            try:
     424                self.maxItems = int(value)
     425            except ValueError:
     426                raise BadRequest(text="Field max_items requires a positive " +
     427                                      "integer value")
     428
     429
     430    def _render_maxItems(self, verbElement):
     431        """
     432        Parse maximum items into an items request.
     433        """
     434        if self.maxItems:
     435            verbElement['max_items'] = unicode(self.maxItems)
     436
     437
     438    def _parse_options(self, verbElement):
     439        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS)
     440        if form:
     441            if form.formType == 'submit':
     442                self.options = form.getValues()
     443            elif form.formType == 'cancel':
     444                self.options = {}
     445            else:
     446                raise BadRequest(text="Unexpected form type %r" % form.formType)
     447        else:
     448            raise BadRequest(text="Missing options form")
     449
     450    def parseElement(self, element):
     451        """
     452        Parse the publish-subscribe verb and parameters out of a request.
     453        """
     454        generic.Stanza.parseElement(self, element)
     455
     456        for child in element.pubsub.elements():
     457            key = (self.stanzaType, child.uri, child.name)
     458            try:
     459                verb = self._requestVerbMap[key]
     460            except KeyError:
     461                continue
     462            else:
     463                self.verb = verb
     464                break
     465
     466        if not self.verb:
     467            raise NotImplementedError()
     468
     469        for parameter in self._parameters[verb]:
     470            getattr(self, '_parse_%s' % parameter)(child)
     471
     472
     473    def send(self, xs):
     474        """
     475        Send this request to its recipient.
     476
     477        This renders all of the relevant parameters for this specific
     478        requests into an L{xmlstream.IQ}, and invoke its C{send} method.
     479        This returns a deferred that fires upon reception of a response. See
     480        L{xmlstream.IQ} for details.
     481
     482        @param xs: The XML stream to send the request on.
     483        @type xs: L{xmlstream.XmlStream}
     484        @rtype: L{defer.Deferred}.
     485        """
     486
     487        try:
     488            (self.stanzaType,
     489             childURI,
     490             childName) = self._verbRequestMap[self.verb]
     491        except KeyError:
     492            raise NotImplementedError()
     493
     494        iq = xmlstream.IQ(xs, self.stanzaType)
     495        iq.addElement((childURI, 'pubsub'))
     496        verbElement = iq.pubsub.addElement(childName)
     497
     498        if self.sender:
     499            iq['from'] = self.sender.full()
     500        if self.recipient:
     501            iq['to'] = self.recipient.full()
     502
     503        for parameter in self._parameters[self.verb]:
     504            getattr(self, '_render_%s' % parameter)(verbElement)
     505
     506        return iq.send()
    204507
    205508
     
    246549    """
    247550
     551    redirectURI = None
     552
    248553
    249554
     
    303608        nodeIdentifier = action["node"]
    304609        event = DeleteEvent(sender, recipient, nodeIdentifier, headers)
     610        if action.redirect:
     611            event.redirectURI = action.redirect.getAttribute('uri')
    305612        self.deleteReceived(event)
    306613
     
    324631
    325632
    326     def createNode(self, service, nodeIdentifier=None):
     633    def createNode(self, service, nodeIdentifier=None, sender=None):
    327634        """
    328635        Create a publish subscribe node.
     
    333640        @type nodeIdentifier: C{unicode}
    334641        """
    335 
    336 
    337         request = _PubSubRequest(self.xmlstream, 'create')
    338         if nodeIdentifier:
    339             request.command['node'] = nodeIdentifier
     642        request = PubSubRequest('create')
     643        request.recipient = service
     644        request.nodeIdentifier = nodeIdentifier
     645        request.sender = sender
    340646
    341647        def cb(iq):
     
    347653            return new_node
    348654
    349         return request.send(service).addCallback(cb)
    350 
    351 
    352     def deleteNode(self, service, nodeIdentifier):
     655        d = request.send(self.xmlstream)
     656        d.addCallback(cb)
     657        return d
     658
     659
     660    def deleteNode(self, service, nodeIdentifier, sender=None):
    353661        """
    354662        Delete a publish subscribe node.
     
    359667        @type nodeIdentifier: C{unicode}
    360668        """
    361         request = _PubSubRequest(self.xmlstream, 'delete')
    362         request.command['node'] = nodeIdentifier
    363         return request.send(service)
    364 
    365 
    366     def subscribe(self, service, nodeIdentifier, subscriber):
     669        request = PubSubRequest('delete')
     670        request.recipient = service
     671        request.nodeIdentifier = nodeIdentifier
     672        request.sender = sender
     673        return request.send(self.xmlstream)
     674
     675
     676    def subscribe(self, service, nodeIdentifier, subscriber, sender=None):
    367677        """
    368678        Subscribe to a publish subscribe node.
     
    376686        @type subscriber: L{JID}
    377687        """
    378         request = _PubSubRequest(self.xmlstream, 'subscribe')
    379         if nodeIdentifier:
    380             request.command['node'] = nodeIdentifier
    381         request.command['jid'] = subscriber.full()
     688        request = PubSubRequest('subscribe')
     689        request.recipient = service
     690        request.nodeIdentifier = nodeIdentifier
     691        request.subscriber = subscriber
     692        request.sender = sender
    382693
    383694        def cb(iq):
     
    394705                return None
    395706
    396         return request.send(service).addCallback(cb)
    397 
    398 
    399     def unsubscribe(self, service, nodeIdentifier, subscriber):
     707        d = request.send(self.xmlstream)
     708        d.addCallback(cb)
     709        return d
     710
     711
     712    def unsubscribe(self, service, nodeIdentifier, subscriber, sender=None):
    400713        """
    401714        Unsubscribe from a publish subscribe node.
     
    408721        @type subscriber: L{JID}
    409722        """
    410         request = _PubSubRequest(self.xmlstream, 'unsubscribe')
    411         if nodeIdentifier:
    412             request.command['node'] = nodeIdentifier
    413         request.command['jid'] = subscriber.full()
    414         return request.send(service)
    415 
    416 
    417     def publish(self, service, nodeIdentifier, items=None):
     723        request = PubSubRequest('unsubscribe')
     724        request.recipient = service
     725        request.nodeIdentifier = nodeIdentifier
     726        request.subscriber = subscriber
     727        request.sender = sender
     728        return request.send(self.xmlstream)
     729
     730
     731    def publish(self, service, nodeIdentifier, items=None, sender=None):
    418732        """
    419733        Publish to a publish subscribe node.
     
    426740        @type items: C{list}
    427741        """
    428         request = _PubSubRequest(self.xmlstream, 'publish')
    429         request.command['node'] = nodeIdentifier
    430         if items:
    431             for item in items:
    432                 request.command.addChild(item)
    433 
    434         return request.send(service)
    435 
    436 
    437     def items(self, service, nodeIdentifier, maxItems=None):
     742        request = PubSubRequest('publish')
     743        request.recipient = service
     744        request.nodeIdentifier = nodeIdentifier
     745        request.items = items
     746        request.sender = sender
     747        return request.send(self.xmlstream)
     748
     749
     750    def items(self, service, nodeIdentifier, maxItems=None, sender=None):
    438751        """
    439752        Retrieve previously published items from a publish subscribe node.
     
    446759        @type maxItems: C{int}
    447760        """
    448         request = _PubSubRequest(self.xmlstream, 'items', method='get')
    449         if nodeIdentifier:
    450             request.command['node'] = nodeIdentifier
     761        request = PubSubRequest('items')
     762        request.recipient = service
     763        request.nodeIdentifier = nodeIdentifier
    451764        if maxItems:
    452             request.command["max_items"] = str(int(maxItems))
     765            request.maxItems = str(int(maxItems))
     766        request.sender = sender
    453767
    454768        def cb(iq):
     
    459773            return items
    460774
    461         return request.send(service).addCallback(cb)
     775        d = request.send(self.xmlstream)
     776        d.addCallback(cb)
     777        return d
    462778
    463779
     
    494810
    495811    iqHandlers = {
    496             PUBSUB_PUBLISH: '_onPublish',
    497             PUBSUB_CREATE: '_onCreate',
    498             PUBSUB_SUBSCRIBE: '_onSubscribe',
    499             PUBSUB_OPTIONS_GET: '_onOptionsGet',
    500             PUBSUB_OPTIONS_SET: '_onOptionsSet',
    501             PUBSUB_AFFILIATIONS: '_onAffiliations',
    502             PUBSUB_ITEMS: '_onItems',
    503             PUBSUB_RETRACT: '_onRetract',
    504             PUBSUB_SUBSCRIPTIONS: '_onSubscriptions',
    505             PUBSUB_UNSUBSCRIBE: '_onUnsubscribe',
    506 
    507             PUBSUB_AFFILIATIONS_GET: '_onAffiliationsGet',
    508             PUBSUB_AFFILIATIONS_SET: '_onAffiliationsSet',
    509             PUBSUB_CONFIGURE_GET: '_onConfigureGet',
    510             PUBSUB_CONFIGURE_SET: '_onConfigureSet',
    511             PUBSUB_DEFAULT: '_onDefault',
    512             PUBSUB_PURGE: '_onPurge',
    513             PUBSUB_DELETE: '_onDelete',
    514             PUBSUB_SUBSCRIPTIONS_GET: '_onSubscriptionsGet',
    515             PUBSUB_SUBSCRIPTIONS_SET: '_onSubscriptionsSet',
    516 
     812            '/*': '_onPubSubRequest',
    517813            }
    518814
    519 
    520     def __init__(self):
     815    _legacyHandlers = {
     816        'publish': ('publish', ['sender', 'recipient',
     817                                'nodeIdentifier', 'items']),
     818        'subscribe': ('subscribe', ['sender', 'recipient',
     819                                    'nodeIdentifier', 'subscriber']),
     820        'unsubscribe': ('unsubscribe', ['sender', 'recipient',
     821                                        'nodeIdentifier', 'subscriber']),
     822        'subscriptions': ('subscriptions', ['sender', 'recipient']),
     823        'affiliations': ('affiliations', ['sender', 'recipient']),
     824        'create': ('create', ['sender', 'recipient', 'nodeIdentifier']),
     825        'getConfigurationOptions': ('getConfigurationOptions', []),
     826        'default': ('getDefaultConfiguration',
     827                    ['sender', 'recipient', 'nodeType']),
     828        'configureGet': ('getConfiguration', ['sender', 'recipient',
     829                                              'nodeIdentifier']),
     830        'configureSet': ('setConfiguration', ['sender', 'recipient',
     831                                              'nodeIdentifier', 'options']),
     832        'items': ('items', ['sender', 'recipient', 'nodeIdentifier',
     833                            'maxItems', 'itemIdentifiers']),
     834        'retract': ('retract', ['sender', 'recipient', 'nodeIdentifier',
     835                                'itemIdentifiers']),
     836        'purge': ('purge', ['sender', 'recipient', 'nodeIdentifier']),
     837        'delete': ('delete', ['sender', 'recipient', 'nodeIdentifier']),
     838    }
     839
     840    hideNodes = False
     841
     842    def __init__(self, resource=None):
     843        self.resource = resource
    521844        self.discoIdentity = {'category': 'pubsub',
    522845                              'type': 'generic',
     
    527850
    528851    def connectionMade(self):
    529         self.xmlstream.addObserver(PUBSUB_GET, self.handleRequest)
    530         self.xmlstream.addObserver(PUBSUB_SET, self.handleRequest)
    531         self.xmlstream.addObserver(PUBSUB_OWNER_GET, self.handleRequest)
    532         self.xmlstream.addObserver(PUBSUB_OWNER_SET, self.handleRequest)
     852        self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest)
    533853
    534854
    535855    def getDiscoInfo(self, requestor, target, nodeIdentifier):
    536         info = []
    537 
    538         if not nodeIdentifier:
    539             info.append(disco.DiscoIdentity(**self.discoIdentity))
    540 
    541             info.append(disco.DiscoFeature(disco.NS_ITEMS))
    542             info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature))
    543                          for feature in self.pubSubFeatures])
    544 
    545         def toInfo(nodeInfo):
     856        def toInfo(nodeInfo, info):
    546857            if not nodeInfo:
    547                 return
     858                return info
    548859
    549860            (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data']
     
    563874                    form.addField(data_form.Field.fromDict(metaDatum))
    564875
    565                 info.append(form.toElement())
    566 
    567         d = self.getNodeInfo(requestor, target, nodeIdentifier or '')
    568         d.addCallback(toInfo)
    569         d.addBoth(lambda result: info)
     876                info.append(form)
     877
     878            return info
     879
     880        info = []
     881
     882        request = PubSubRequest('discoInfo')
     883
     884        if self.resource is not None:
     885            resource = self.resource.locateResource(request)
     886            identity = resource.discoIdentity
     887            features = resource.features
     888            getInfo = resource.getInfo
     889        else:
     890            category, idType, name = self.discoIdentity
     891            identity = disco.DiscoIdentity(category, idType, name)
     892            features = self.pubSubFeatures
     893            getInfo = self.getNodeInfo
     894
     895        if not nodeIdentifier:
     896            info.append(identity)
     897            info.append(disco.DiscoFeature(disco.NS_DISCO_ITEMS))
     898            info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature))
     899                         for feature in features])
     900
     901        d = getInfo(requestor, target, nodeIdentifier or '')
     902        d.addCallback(toInfo, info)
     903        d.addErrback(log.err)
    570904        return d
    571905
    572906
    573907    def getDiscoItems(self, requestor, target, nodeIdentifier):
    574         if nodeIdentifier or self.hideNodes:
    575             return defer.succeed([])
    576 
    577         d = self.getNodes(requestor, target)
     908        if self.hideNodes:
     909            d = defer.succeed([])
     910        elif self.resource is not None:
     911            request = PubSubRequest('discoInfo')
     912            resource = self.resource.locateResource(request)
     913            d = resource.getNodes(requestor, target, nodeIdentifier)
     914        elif nodeIdentifier:
     915            d = self.getNodes(requestor, target)
     916        else:
     917            d = defer.succeed([])
     918           
     919
     920
    578921        d.addCallback(lambda nodes: [disco.DiscoItem(target, node)
    579922                                     for node in nodes])
     
    581924
    582925
    583     def _findForm(self, element, formNamespace):
    584         if not element:
    585             return None
    586 
    587         form = None
    588         for child in element.elements():
     926    def _onPubSubRequest(self, iq):
     927        request = PubSubRequest.fromElement(iq)
     928
     929        if self.resource is not None:
     930            resource = self.resource.locateResource(request)
     931        else:
     932            resource = self
     933
     934        # Preprocess the request, knowing the handling resource
     935        try:
     936            preProcessor = getattr(self, '_preProcess_%s' % request.verb)
     937        except AttributeError:
     938            pass
     939        else:
     940            request = preProcessor(resource, request)
     941            if request is None:
     942                return defer.succeed(None)
     943
     944        # Process the request itself,
     945        if resource is not self:
    589946            try:
    590                 form = data_form.Form.fromElement(child)
    591             except data_form.Error:
    592                 continue
    593 
    594             if form.formNamespace != NS_PUBSUB_NODE_CONFIG:
    595                 continue
    596 
    597         return form
    598 
    599 
    600     def _getParameter_node(self, commandElement):
     947                handler = getattr(resource, request.verb)
     948            except AttributeError:
     949                # fix lookup feature
     950                text = "Request verb: %s" % request.verb
     951                return defer.fail(Unsupported('', text))
     952
     953            d = handler(request)
     954        else:
     955            handlerName, argNames = self._legacyHandlers[request.verb]
     956            handler = getattr(self, handlerName)
     957            args = [getattr(request, arg) for arg in argNames]
     958            d = handler(*args)
     959
     960        # If needed, translate the result into a response
    601961        try:
    602             return commandElement["node"]
    603         except KeyError:
    604             raise BadRequest('nodeid-required')
    605 
    606 
    607     def _getParameter_nodeOrEmpty(self, commandElement):
    608         return commandElement.getAttribute("node", '')
    609 
    610 
    611     def _getParameter_jid(self, commandElement):
    612         try:
    613             return jid.internJID(commandElement["jid"])
    614         except KeyError:
    615             raise BadRequest('jid-required')
    616 
    617 
    618     def _getParameter_max_items(self, commandElement):
    619         value = commandElement.getAttribute('max_items')
    620 
    621         if value:
    622             try:
    623                 return int(value)
    624             except ValueError:
    625                 raise BadRequest(text="Field max_items requires a positive " +
    626                                       "integer value")
     962            cb = getattr(self, '_toResponse_%s' % request.verb)
     963        except AttributeError:
     964            pass
     965        else:
     966            d.addCallback(cb, resource, request)
     967
     968        return d
     969
     970
     971    def _toResponse_subscribe(self, result, resource, request):
     972        response = domish.Element((NS_PUBSUB, "pubsub"))
     973        subscription = response.addElement("subscription")
     974        if result.nodeIdentifier:
     975            subscription["node"] = result.nodeIdentifier
     976        subscription["jid"] = result.subscriber.full()
     977        subscription["subscription"] = result.state
     978        return response
     979
     980
     981    def _toResponse_subscriptions(self, result, resource, request):
     982        response = domish.Element((NS_PUBSUB, 'pubsub'))
     983        subscriptions = response.addElement('subscriptions')
     984        for subscription in result:
     985            item = subscriptions.addElement('subscription')
     986            item['node'] = subscription.nodeIdentifier
     987            item['jid'] = subscription.subscriber.full()
     988            item['subscription'] = subscription.state
     989        return response
     990
     991
     992    def _toResponse_affiliations(self, result, resource, request):
     993        response = domish.Element((NS_PUBSUB, 'pubsub'))
     994        affiliations = response.addElement('affiliations')
     995
     996        for nodeIdentifier, affiliation in result:
     997            item = affiliations.addElement('affiliation')
     998            item['node'] = nodeIdentifier
     999            item['affiliation'] = affiliation
     1000
     1001        return response
     1002
     1003
     1004    def _toResponse_create(self, result, resource, request):
     1005        if not request.nodeIdentifier or request.nodeIdentifier != result:
     1006            response = domish.Element((NS_PUBSUB, 'pubsub'))
     1007            create = response.addElement('create')
     1008            create['node'] = result
     1009            return response
    6271010        else:
    6281011            return None
    629 
    630 
    631     def _getParameters(self, iq, *names):
    632         requestor = jid.internJID(iq["from"]).userhostJID()
    633         service = jid.internJID(iq["to"])
    634 
    635         params = [requestor, service]
    636 
    637         if names:
    638             command = names[0]
    639             commandElement = getattr(iq.pubsub, command)
    640             if not commandElement:
    641                 raise Exception("Could not find command element %r" % command)
    642 
    643         for name in names[1:]:
    644             try:
    645                 getter = getattr(self, '_getParameter_' + name)
    646             except KeyError:
    647                 raise Exception("No parameter getter for this name")
    648 
    649             params.append(getter(commandElement))
    650 
    651         return params
    652 
    653 
    654     def _onPublish(self, iq):
    655         requestor, service, nodeIdentifier = self._getParameters(
    656                 iq, 'publish', 'node')
    657 
    658         items = []
    659         for element in iq.pubsub.publish.elements():
    660             if element.uri == NS_PUBSUB and element.name == 'item':
    661                 items.append(element)
    662 
    663         return self.publish(requestor, service, nodeIdentifier, items)
    664 
    665 
    666     def _onSubscribe(self, iq):
    667         requestor, service, nodeIdentifier, subscriber = self._getParameters(
    668                 iq, 'subscribe', 'nodeOrEmpty', 'jid')
    669 
    670         def toResponse(result):
    671             response = domish.Element((NS_PUBSUB, "pubsub"))
    672             subscription = response.addElement("subscription")
    673             if result.nodeIdentifier:
    674                 subscription["node"] = result.nodeIdentifier
    675             subscription["jid"] = result.subscriber.full()
    676             subscription["subscription"] = result.state
    677             return response
    678 
    679         d = self.subscribe(requestor, service, nodeIdentifier, subscriber)
    680         d.addCallback(toResponse)
    681         return d
    682 
    683 
    684     def _onUnsubscribe(self, iq):
    685         requestor, service, nodeIdentifier, subscriber = self._getParameters(
    686                 iq, 'unsubscribe', 'nodeOrEmpty', 'jid')
    687 
    688         return self.unsubscribe(requestor, service, nodeIdentifier, subscriber)
    689 
    690 
    691     def _onOptionsGet(self, iq):
    692         raise Unsupported('subscription-options')
    693 
    694 
    695     def _onOptionsSet(self, iq):
    696         raise Unsupported('subscription-options')
    697 
    698 
    699     def _onSubscriptions(self, iq):
    700         requestor, service = self._getParameters(iq)
    701 
    702         def toResponse(result):
    703             response = domish.Element((NS_PUBSUB, 'pubsub'))
    704             subscriptions = response.addElement('subscriptions')
    705             for node, subscriber, state in result:
    706                 item = subscriptions.addElement('subscription')
    707                 item['node'] = node
    708                 item['jid'] = subscriber.full()
    709                 item['subscription'] = state
    710             return response
    711 
    712         d = self.subscriptions(requestor, service)
    713         d.addCallback(toResponse)
    714         return d
    715 
    716 
    717     def _onAffiliations(self, iq):
    718         requestor, service = self._getParameters(iq)
    719 
    720         def toResponse(result):
    721             response = domish.Element((NS_PUBSUB, 'pubsub'))
    722             affiliations = response.addElement('affiliations')
    723 
    724             for nodeIdentifier, affiliation in result:
    725                 item = affiliations.addElement('affiliation')
    726                 item['node'] = nodeIdentifier
    727                 item['affiliation'] = affiliation
    728 
    729             return response
    730 
    731         d = self.affiliations(requestor, service)
    732         d.addCallback(toResponse)
    733         return d
    734 
    735 
    736     def _onCreate(self, iq):
    737         requestor, service = self._getParameters(iq)
    738         nodeIdentifier = iq.pubsub.create.getAttribute("node")
    739 
    740         def toResponse(result):
    741             if not nodeIdentifier or nodeIdentifier != result:
    742                 response = domish.Element((NS_PUBSUB, 'pubsub'))
    743                 create = response.addElement('create')
    744                 create['node'] = result
    745                 return response
    746             else:
    747                 return None
    748 
    749         d = self.create(requestor, service, nodeIdentifier)
    750         d.addCallback(toResponse)
    751         return d
    7521012
    7531013
     
    7671027        return fields
    7681028
    769     def _formFromConfiguration(self, values):
    770         options = self.getConfigurationOptions()
     1029
     1030    def _formFromConfiguration(self, resource, values):
     1031        options = resource.getConfigurationOptions()
    7711032        fields = self._makeFields(options, values)
    7721033        form = data_form.Form(formType="form",
     
    7761037        return form
    7771038
    778     def _checkConfiguration(self, values):
    779         options = self.getConfigurationOptions()
     1039
     1040    def _checkConfiguration(self, resource, values):
     1041        options = resource.getConfigurationOptions()
    7801042        processedValues = {}
    7811043
     
    8011063
    8021064
    803     def _onDefault(self, iq):
    804         requestor, service = self._getParameters(iq)
    805 
    806         def toResponse(options):
    807             response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
    808             default = response.addElement("default")
    809             default.addChild(self._formFromConfiguration(options).toElement())
    810             return response
    811 
    812         form = self._findForm(iq.pubsub.config, NS_PUBSUB_NODE_CONFIG)
    813         values = form and form.formType == 'result' and form.getValues() or {}
    814         nodeType = values.get('pubsub#node_type', 'leaf')
    815 
    816         if nodeType not in ('leaf', 'collections'):
    817             return defer.fail(error.StanzaError('not-acceptable'))
    818 
    819         d = self.getDefaultConfiguration(requestor, service, nodeType)
    820         d.addCallback(toResponse)
    821         return d
    822 
    823 
    824     def _onConfigureGet(self, iq):
    825         requestor, service, nodeIdentifier = self._getParameters(
    826                 iq, 'configure', 'nodeOrEmpty')
    827 
    828         def toResponse(options):
    829             response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
    830             configure = response.addElement("configure")
    831             configure.addChild(self._formFromConfiguration(options).toElement())
    832 
    833             if nodeIdentifier:
    834                 configure["node"] = nodeIdentifier
    835 
    836             return response
    837 
    838         d = self.getConfiguration(requestor, service, nodeIdentifier)
    839         d.addCallback(toResponse)
    840         return d
    841 
    842 
    843     def _onConfigureSet(self, iq):
    844         requestor, service, nodeIdentifier = self._getParameters(
    845                 iq, 'configure', 'nodeOrEmpty')
    846 
    847         # Search configuration form with correct FORM_TYPE and process it
    848 
    849         form = self._findForm(iq.pubsub.configure, NS_PUBSUB_NODE_CONFIG)
    850 
    851         if form:
    852             if form.formType == 'submit':
    853                 options = self._checkConfiguration(form.getValues())
    854 
    855                 return self.setConfiguration(requestor, service,
    856                                              nodeIdentifier, options)
    857             elif form.formType == 'cancel':
    858                 return None
    859 
    860         raise BadRequest()
    861 
    862 
    863     def _onItems(self, iq):
    864         requestor, service, nodeIdentifier, maxItems = self._getParameters(
    865                 iq, 'items', 'nodeOrEmpty', 'max_items')
    866 
    867         itemIdentifiers = []
    868         for child in iq.pubsub.items.elements():
    869             if child.name == 'item' and child.uri == NS_PUBSUB:
    870                 try:
    871                     itemIdentifiers.append(child["id"])
    872                 except KeyError:
    873                     raise BadRequest()
    874 
    875         def toResponse(result):
    876             response = domish.Element((NS_PUBSUB, 'pubsub'))
    877             items = response.addElement('items')
    878             if nodeIdentifier:
    879                 items["node"] = nodeIdentifier
    880 
    881             for item in result:
    882                 items.addChild(item)
    883 
    884             return response
    885 
    886         d = self.items(requestor, service, nodeIdentifier, maxItems,
    887                        itemIdentifiers)
    888         d.addCallback(toResponse)
    889         return d
    890 
    891 
    892     def _onRetract(self, iq):
    893         requestor, service, nodeIdentifier = self._getParameters(
    894                 iq, 'retract', 'node')
    895 
    896         itemIdentifiers = []
    897         for child in iq.pubsub.retract.elements():
    898             if child.uri == NS_PUBSUB and child.name == 'item':
    899                 try:
    900                     itemIdentifiers.append(child["id"])
    901                 except KeyError:
    902                     raise BadRequest()
    903 
    904         return self.retract(requestor, service, nodeIdentifier,
    905                             itemIdentifiers)
    906 
    907 
    908     def _onPurge(self, iq):
    909         requestor, service, nodeIdentifier = self._getParameters(
    910                 iq, 'purge', 'node')
    911         return self.purge(requestor, service, nodeIdentifier)
    912 
    913 
    914     def _onDelete(self, iq):
    915         requestor, service, nodeIdentifier = self._getParameters(
    916                 iq, 'delete', 'node')
    917         return self.delete(requestor, service, nodeIdentifier)
    918 
    919 
    920     def _onAffiliationsGet(self, iq):
    921         raise Unsupported('modify-affiliations')
    922 
    923 
    924     def _onAffiliationsSet(self, iq):
    925         raise Unsupported('modify-affiliations')
    926 
    927 
    928     def _onSubscriptionsGet(self, iq):
    929         raise Unsupported('manage-subscriptions')
    930 
    931 
    932     def _onSubscriptionsSet(self, iq):
    933         raise Unsupported('manage-subscriptions')
    934 
    935     # public methods
     1065    def _preProcess_default(self, resource, request):
     1066        if request.nodeType not in ('leaf', 'collection'):
     1067            raise error.StanzaError('not-acceptable')
     1068        else:
     1069            return request
     1070
     1071
     1072    def _toResponse_default(self, options, resource, request):
     1073        response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
     1074        default = response.addElement("default")
     1075        form = self._formFromConfiguration(resource, options)
     1076        default.addChild(form.toElement())
     1077        return response
     1078
     1079
     1080    def _toResponse_configureGet(self, options, resource, request):
     1081        response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
     1082        configure = response.addElement("configure")
     1083        form = self._formFromConfiguration(resource, options)
     1084        configure.addChild(form.toElement())
     1085
     1086        if request.nodeIdentifier:
     1087            configure["node"] = request.nodeIdentifier
     1088
     1089        return response
     1090
     1091
     1092    def _preProcess_configureSet(self, resource, request):
     1093        if request.options:
     1094            request.options = self._checkConfiguration(resource,
     1095                                                       request.options)
     1096            return request
     1097        else:
     1098            return None
     1099
     1100
     1101    def _toResponse_items(self, result, resource, request):
     1102        response = domish.Element((NS_PUBSUB, 'pubsub'))
     1103        items = response.addElement('items')
     1104        items["node"] = request.nodeIdentifier
     1105
     1106        for item in result:
     1107            items.addChild(item)
     1108
     1109        return response
     1110
    9361111
    9371112    def _createNotification(self, eventType, service, nodeIdentifier,
     
    9561131
    9571132        return message
     1133
     1134    # public methods
    9581135
    9591136    def notifyPublish(self, service, nodeIdentifier, notifications):
     
    9661143
    9671144
    968     def notifyDelete(self, service, nodeIdentifier, subscriptions):
    969         for subscription in subscriptions:
     1145    def notifyDelete(self, service, nodeIdentifier, subscribers,
     1146                           redirectURI=None):
     1147        for subscriber in subscribers:
    9701148            message = self._createNotification('delete', service,
    9711149                                               nodeIdentifier,
    972                                                subscription.subscriber)
     1150                                               subscriber)
     1151            if redirectURI:
     1152                redirect = message.event.delete.addElement('redirect')
     1153                redirect['uri'] = redirectURI
    9731154            self.send(message)
    9741155
     
    10101191
    10111192
    1012     def getDefaultConfiguration(self, requestor, service):
     1193    def getDefaultConfiguration(self, requestor, service, nodeType):
    10131194        raise Unsupported('retrieve-default')
    10141195
     
    10371218    def delete(self, requestor, service, nodeIdentifier):
    10381219        raise Unsupported('delete-nodes')
     1220
     1221
     1222
     1223class PubSubResource(object):
     1224
     1225    implements(IPubSubResource)
     1226
     1227    features = []
     1228    discoIdentity = disco.DiscoIdentity('pubsub',
     1229                                        'service',
     1230                                        'Publish-Subscribe Service')
     1231
     1232
     1233    def locateResource(self, request):
     1234        return self
     1235
     1236
     1237    def getInfo(self, requestor, service, nodeIdentifier):
     1238        return defer.succeed(None)
     1239
     1240
     1241    def getNodes(self, requestor, service, nodeIdentifier):
     1242        return defer.succeed([])
     1243
     1244
     1245    def getConfigurationOptions(self):
     1246        return {}
     1247
     1248
     1249    def publish(self, request):
     1250        return defer.fail(Unsupported('publish'))
     1251
     1252
     1253    def subscribe(self, request):
     1254        return defer.fail(Unsupported('subscribe'))
     1255
     1256
     1257    def unsubscribe(self, request):
     1258        return defer.fail(Unsupported('subscribe'))
     1259
     1260
     1261    def subscriptions(self, request):
     1262        return defer.fail(Unsupported('retrieve-subscriptions'))
     1263
     1264
     1265    def affiliations(self, request):
     1266        return defer.fail(Unsupported('retrieve-affiliations'))
     1267
     1268
     1269    def create(self, request):
     1270        return defer.fail(Unsupported('create-nodes'))
     1271
     1272
     1273    def default(self, request):
     1274        return defer.fail(Unsupported('retrieve-default'))
     1275
     1276
     1277    def configureGet(self, request):
     1278        return defer.fail(Unsupported('config-node'))
     1279
     1280
     1281    def configureSet(self, request):
     1282        return defer.fail(Unsupported('config-node'))
     1283
     1284
     1285    def items(self, request):
     1286        return defer.fail(Unsupported('retrieve-items'))
     1287
     1288
     1289    def retract(self, request):
     1290        return defer.fail(Unsupported('retract-items'))
     1291
     1292
     1293    def purge(self, request):
     1294        return defer.fail(Unsupported('purge-nodes'))
     1295
     1296
     1297    def delete(self, request):
     1298        return defer.fail(Unsupported('delete-nodes'))
     1299
     1300
     1301    def affiliationsGet(self, request):
     1302        return defer.fail(Unsupported('modify-affiliations'))
     1303
     1304
     1305    def affiliationsSet(self, request):
     1306        return defer.fail(Unsupported('modify-affiliations'))
     1307
     1308
     1309    def subscriptionsGet(self, request):
     1310        return defer.fail(Unsupported('manage-subscriptions'))
     1311
     1312
     1313    def subscriptionsSet(self, request):
     1314        return defer.fail(Unsupported('manage-subscriptions'))
  • wokkel/subprotocols.py

    r22 r53  
    11# -*- test-case-name: wokkel.test.test_subprotocols -*-
    22#
    3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories.
     3# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
    44# See LICENSE for details.
    55
     
    1313from twisted.python import log
    1414from twisted.words.protocols.jabber import error, xmlstream
     15from twisted.words.protocols.jabber.xmlstream import toResponse
    1516from twisted.words.xish import xpath
    1617from twisted.words.xish.domish import IElement
    1718
    18 try:
    19     from twisted.words.protocols.jabber.xmlstream import toResponse
    20 except ImportError:
    21     from wokkel.compat import toResponse
    22 
    2319from wokkel.iwokkel import IXMPPHandler, IXMPPHandlerCollection
    2420
    2521class XMPPHandler(object):
     22    """
     23    XMPP protocol handler.
     24
     25    Classes derived from this class implement (part of) one or more XMPP
     26    extension protocols, and are referred to as a subprotocol implementation.
     27    """
     28
    2629    implements(IXMPPHandler)
     30
     31    def __init__(self):
     32        self.parent = None
     33        self.xmlstream = None
     34
    2735
    2836    def setHandlerParent(self, parent):
     
    3038        self.parent.addHandler(self)
    3139
     40
    3241    def disownHandlerParent(self, parent):
    3342        self.parent.removeHandler(self)
    3443        self.parent = None
    3544
     45
    3646    def makeConnection(self, xs):
    3747        self.xmlstream = xs
    3848        self.connectionMade()
    3949
     50
    4051    def connectionMade(self):
    41         pass
     52        """
     53        Called after a connection has been established.
     54
     55        Can be overridden to perform work before stream initialization.
     56        """
     57
    4258
    4359    def connectionInitialized(self):
    44         pass
     60        """
     61        The XML stream has been initialized.
     62
     63        Can be overridden to perform work after stream initialization, e.g. to
     64        set up observers and start exchanging XML stanzas.
     65        """
     66
    4567
    4668    def connectionLost(self, reason):
     69        """
     70        The XML stream has been closed.
     71
     72        This method can be extended to inspect the C{reason} argument and
     73        act on it.
     74        """
    4775        self.xmlstream = None
     76
    4877
    4978    def send(self, obj):
     
    6594
    6695
     96
    6797class XMPPHandlerCollection(object):
    6898    """
     
    72102    L{XMPPHandler} itself, so this is not recursive.
    73103
    74     @ivar xmlstream: Currently managed XML stream.
    75     @type xmlstream: L{XmlStream}
    76104    @ivar handlers: List of protocol handlers.
    77105    @type handlers: L{list} of objects providing
     
    83111    def __init__(self):
    84112        self.handlers = []
    85         self.xmlstream = None
    86         self._initialized = False
     113
    87114
    88115    def __iter__(self):
     
    92119        return iter(self.handlers)
    93120
     121
    94122    def addHandler(self, handler):
    95123        """
     
    97125
    98126        Protocol handlers are expected to provide L{IXMPPHandler}.
    99 
    100         When an XML stream has already been established, the handler's
    101         C{connectionInitialized} will be called to get it up to speed.
    102         """
    103 
     127        """
    104128        self.handlers.append(handler)
    105129
    106         # get protocol handler up to speed when a connection has already
    107         # been established
    108         if self.xmlstream and self._initialized:
    109             handler.makeConnection(self.xmlstream)
    110             handler.connectionInitialized()
    111130
    112131    def removeHandler(self, handler):
     
    114133        Remove protocol handler.
    115134        """
    116 
    117135        self.handlers.remove(handler)
     136
     137
    118138
    119139class StreamManager(XMPPHandlerCollection):
     
    126146    using L{addHandler}.
    127147
     148    @ivar xmlstream: currently managed XML stream
     149    @type xmlstream: L{XmlStream}
    128150    @ivar logTraffic: if true, log all traffic.
    129151    @type logTraffic: L{bool}
     152    @ivar _initialized: Whether the stream represented by L{xmlstream} has
     153                        been initialized. This is used when caching outgoing
     154                        stanzas.
     155    @type _initialized: C{bool}
    130156    @ivar _packetQueue: internal buffer of unsent data. See L{send} for details.
    131157    @type _packetQueue: L{list}
     
    135161
    136162    def __init__(self, factory):
    137         self.handlers = []
     163        XMPPHandlerCollection.__init__(self)
    138164        self.xmlstream = None
    139165        self._packetQueue = []
     
    147173        self.factory = factory
    148174
     175
     176    def addHandler(self, handler):
     177        """
     178        Add protocol handler.
     179
     180        When an XML stream has already been established, the handler's
     181        C{connectionInitialized} will be called to get it up to speed.
     182        """
     183        XMPPHandlerCollection.addHandler(self, handler)
     184
     185        # get protocol handler up to speed when a connection has already
     186        # been established
     187        if self.xmlstream and self._initialized:
     188            handler.makeConnection(self.xmlstream)
     189            handler.connectionInitialized()
     190
     191
    149192    def _connected(self, xs):
     193        """
     194        Called when the transport connection has been established.
     195
     196        Here we optionally set up traffic logging (depending on L{logTraffic})
     197        and call each handler's C{makeConnection} method with the L{XmlStream}
     198        instance.
     199        """
    150200        def logDataIn(buf):
    151201            log.msg("RECV: %r" % buf)
     
    163213            e.makeConnection(xs)
    164214
     215
    165216    def _authd(self, xs):
     217        """
     218        Called when the stream has been initialized.
     219
     220        Send out cached stanzas and call each handler's
     221        C{connectionInitialized} method.
     222        """
    166223        # Flush all pending packets
    167224        for p in self._packetQueue:
     
    175232            e.connectionInitialized()
    176233
     234
    177235    def initializationFailed(self, reason):
    178236        """
     
    188246        """
    189247
     248
    190249    def _disconnected(self, _):
     250        """
     251        Called when the stream has been closed.
     252
     253        From this point on, the manager doesn't interact with the
     254        L{XmlStream} anymore and notifies each handler that the connection
     255        was lost by calling its C{connectionLost} method.
     256        """
    191257        self.xmlstream = None
    192258        self._initialized = False
     
    195261        # the IService interface
    196262        for e in self:
    197             e.xmlstream = None
    198263            e.connectionLost(None)
     264
    199265
    200266    def send(self, obj):
     
    208274                    L{xmlstream.XmlStream.send} for details.
    209275        """
    210 
    211276        if self._initialized:
    212277            self.xmlstream.send(obj)
    213278        else:
    214279            self._packetQueue.append(obj)
     280
    215281
    216282
  • wokkel/test/helpers.py

    r10 r46  
    66"""
    77
     8from twisted.internet import defer
     9from twisted.words.xish import xpath
    810from twisted.words.xish.utility import EventDispatcher
     11
     12from wokkel.generic import parseXml
    913
    1014class XmlStreamStub(object):
     
    5559        """
    5660        self.xmlstream.dispatch(obj)
     61
     62
     63class TestableRequestHandlerMixin(object):
     64    """
     65    Mixin for testing XMPPHandlers that process iq requests.
     66
     67    Handlers that use L{wokkel.subprotocols.IQHandlerMixin} define a
     68    C{iqHandlers} attribute that lists the handlers to be called for iq
     69    requests. This mixin provides L{handleRequest} to mimic the handler
     70    processing for easier testing.
     71    """
     72
     73    def handleRequest(self, xml):
     74        """
     75        Find a handler and call it directly.
     76
     77        @param xml: XML stanza that may yield a handler being called.
     78        @type xml: C{str}.
     79        @return: Deferred that fires with the result of a handler for this
     80                 stanza. If no handler was found, the deferred has its errback
     81                 called with a C{NotImplementedError} exception.
     82        """
     83        handler = None
     84        iq = parseXml(xml)
     85        for queryString, method in self.service.iqHandlers.iteritems():
     86            if xpath.internQuery(queryString).matches(iq):
     87                handler = getattr(self.service, method)
     88
     89        if handler:
     90            d = defer.maybeDeferred(handler, iq)
     91        else:
     92            d = defer.fail(NotImplementedError())
     93
     94        return d
  • wokkel/test/test_client.py

    r8 r42  
    66"""
    77
     8from twisted.internet import defer
    89from twisted.trial import unittest
     10from twisted.words.protocols.jabber import xmlstream
     11from twisted.words.protocols.jabber.client import XMPPAuthenticator
    912from twisted.words.protocols.jabber.jid import JID
    1013from twisted.words.protocols.jabber.xmlstream import STREAM_AUTHD_EVENT
    1114from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT
    1215
    13 from wokkel.client import DeferredClientFactory
     16from wokkel import client
     17from wokkel.test.test_compat import BootstrapMixinTest
    1418
    15 class DeferredClientFactoryTest(unittest.TestCase):
     19class DeferredClientFactoryTest(BootstrapMixinTest):
     20    """
     21    Tests for L{client.DeferredClientFactory}.
     22    """
     23
     24    def setUp(self):
     25        self.factory = client.DeferredClientFactory(JID('user@example.org'),
     26                                                    'secret')
     27
     28
     29    def test_buildProtocol(self):
     30        """
     31        The authenticator factory should be passed to its protocol and it
     32        should instantiate the authenticator and save it.
     33        L{xmlstream.XmlStream}s do that, but we also want to ensure it really
     34        is one.
     35        """
     36        xs = self.factory.buildProtocol(None)
     37        self.assertIdentical(self.factory, xs.factory)
     38        self.assertIsInstance(xs, xmlstream.XmlStream)
     39        self.assertIsInstance(xs.authenticator, XMPPAuthenticator)
     40
    1641
    1742    def test_deferredOnInitialized(self):
     
    2045        """
    2146
    22         f = DeferredClientFactory(JID('user@example.org'), 'secret')
    23         xmlstream = f.buildProtocol(None)
    24         xmlstream.dispatch(xmlstream, STREAM_AUTHD_EVENT)
    25         return f.deferred
     47        xs = self.factory.buildProtocol(None)
     48        xs.dispatch(xs, STREAM_AUTHD_EVENT)
     49        return self.factory.deferred
     50
    2651
    2752    def test_deferredOnNotInitialized(self):
     
    3055        """
    3156
    32         f = DeferredClientFactory(JID('user@example.org'), 'secret')
    33         xmlstream = f.buildProtocol(None)
    34 
    3557        class TestException(Exception):
    3658            pass
    3759
    38         xmlstream.dispatch(TestException(), INIT_FAILED_EVENT)
    39         self.assertFailure(f.deferred, TestException)
    40         return f.deferred
     60        xs = self.factory.buildProtocol(None)
     61        xs.dispatch(TestException(), INIT_FAILED_EVENT)
     62        self.assertFailure(self.factory.deferred, TestException)
     63        return self.factory.deferred
     64
    4165
    4266    def test_deferredOnConnectionFailure(self):
     
    4569        """
    4670
    47         f = DeferredClientFactory(JID('user@example.org'), 'secret')
    48         xmlstream = f.buildProtocol(None)
    49 
    5071        class TestException(Exception):
    5172            pass
    5273
    53         f.clientConnectionFailed(self, TestException())
    54         self.assertFailure(f.deferred, TestException)
    55         return f.deferred
     74        xs = self.factory.buildProtocol(None)
     75        self.factory.clientConnectionFailed(self, TestException())
     76        self.assertFailure(self.factory.deferred, TestException)
     77        return self.factory.deferred
     78
     79
     80
     81class ClientCreatorTest(unittest.TestCase):
     82    """
     83    Tests for L{client.clientCreator}.
     84    """
     85
     86    def test_call(self):
     87        """
     88        The factory is passed to an SRVConnector and a connection initiated.
     89        """
     90
     91        d1 = defer.Deferred()
     92        factory = client.DeferredClientFactory(JID('user@example.org'),
     93                                               'secret')
     94
     95        def cb(connector):
     96            self.assertEqual('xmpp-client', connector.service)
     97            self.assertEqual('example.org', connector.domain)
     98            self.assertEqual(factory, connector.factory)
     99
     100        def connect(connector):
     101            d1.callback(connector)
     102
     103        d1.addCallback(cb)
     104        self.patch(client.SRVConnector, 'connect', connect)
     105
     106        d2 = client.clientCreator(factory)
     107        self.assertEqual(factory.deferred, d2)
     108
     109        return d1
  • wokkel/test/test_compat.py

    r8 r53  
    1 # Copyright (c) 2001-2007 Twisted Matrix Laboratories.
    2 # Copyright (c) 2008 Ralph Meijer
     1# Copyright (c) 2001-2008 Twisted Matrix Laboratories.
     2# Copyright (c) 2008-2009 Ralph Meijer
    33# See LICENSE for details.
    44
     
    77"""
    88
     9from zope.interface.verify import verifyObject
    910from twisted.internet import defer, protocol
     11from twisted.internet.interfaces import IProtocolFactory
    1012from twisted.trial import unittest
    1113from twisted.words.xish import domish, utility
    12 from wokkel.compat import toResponse, XmlStreamFactoryMixin
     14from twisted.words.protocols.jabber import xmlstream
     15from wokkel.compat import BootstrapMixin, XmlStreamServerFactory
    1316
    1417class DummyProtocol(protocol.Protocol, utility.EventDispatcher):
     
    1619    I am a protocol with an event dispatcher without further processing.
    1720
    18     This protocol is only used for testing XmlStreamFactoryMixin to make
     21    This protocol is only used for testing BootstrapMixin to make
    1922    sure the bootstrap observers are added to the protocol instance.
    2023    """
     
    2831
    2932
    30 class XmlStreamFactoryMixinTest(unittest.TestCase):
    3133
    32     def test_buildProtocol(self):
     34class BootstrapMixinTest(unittest.TestCase):
     35    """
     36    Tests for L{BootstrapMixin}.
     37
     38    @ivar factory: Instance of the factory or mixin under test.
     39    """
     40
     41    def setUp(self):
     42        self.factory = BootstrapMixin()
     43
     44
     45    def test_installBootstraps(self):
    3346        """
    34         Test building of protocol.
     47        Dispatching an event should fire registered bootstrap observers.
     48        """
     49        called = []
    3550
    36         Arguments passed to Factory should be passed to protocol on
    37         instantiation. Bootstrap observers should be setup.
    38         """
    39         d = defer.Deferred()
     51        def cb(data):
     52            called.append(data)
    4053
    41         f = XmlStreamFactoryMixin(None, test=None)
    42         f.protocol = DummyProtocol
    43         f.addBootstrap('//event/myevent', d.callback)
    44         xs = f.buildProtocol(None)
     54        dispatcher = DummyProtocol()
     55        self.factory.addBootstrap('//event/myevent', cb)
     56        self.factory.installBootstraps(dispatcher)
    4557
    46         self.assertEquals(f, xs.factory)
    47         self.assertEquals((None,), xs.args)
    48         self.assertEquals({'test': None}, xs.kwargs)
    49         xs.dispatch(None, '//event/myevent')
    50         return d
     58        dispatcher.dispatch(None, '//event/myevent')
     59        self.assertEquals(1, len(called))
     60
    5161
    5262    def test_addAndRemoveBootstrap(self):
     
    5464        Test addition and removal of a bootstrap event handler.
    5565        """
    56         def cb(self):
    57             pass
    5866
    59         f = XmlStreamFactoryMixin(None, test=None)
     67        called = []
    6068
    61         f.addBootstrap('//event/myevent', cb)
    62         self.assertIn(('//event/myevent', cb), f.bootstraps)
     69        def cb(data):
     70            called.append(data)
    6371
    64         f.removeBootstrap('//event/myevent', cb)
    65         self.assertNotIn(('//event/myevent', cb), f.bootstraps)
     72        self.factory.addBootstrap('//event/myevent', cb)
     73        self.factory.removeBootstrap('//event/myevent', cb)
    6674
    67 class ToResponseTest(unittest.TestCase):
     75        dispatcher = DummyProtocol()
     76        self.factory.installBootstraps(dispatcher)
    6877
    69     def test_toResponse(self):
     78        dispatcher.dispatch(None, '//event/myevent')
     79        self.assertFalse(called)
     80
     81
     82
     83class XmlStreamServerFactoryTest(BootstrapMixinTest):
     84    """
     85    Tests for L{XmlStreamServerFactory}.
     86    """
     87
     88    def setUp(self):
    7089        """
    71         Test that a response stanza is generated with addressing swapped.
     90        Set up a server factory with a authenticator factory function.
    7291        """
    73         stanza = domish.Element(('jabber:client', 'iq'))
    74         stanza['type'] = 'get'
    75         stanza['to'] = 'user1@example.com'
    76         stanza['from'] = 'user2@example.com/resource'
    77         stanza['id'] = 'stanza1'
    78         response = toResponse(stanza, 'result')
    79         self.assertNotIdentical(stanza, response)
    80         self.assertEqual(response['from'], 'user1@example.com')
    81         self.assertEqual(response['to'], 'user2@example.com/resource')
    82         self.assertEqual(response['type'], 'result')
    83         self.assertEqual(response['id'], 'stanza1')
     92        class TestAuthenticator(object):
     93            def __init__(self):
     94                self.xmlstreams = []
    8495
    85     def test_toResponseNoFrom(self):
     96            def associateWithStream(self, xs):
     97                self.xmlstreams.append(xs)
     98
     99        def authenticatorFactory():
     100            return TestAuthenticator()
     101
     102        self.factory = XmlStreamServerFactory(authenticatorFactory)
     103
     104
     105    def test_interface(self):
    86106        """
    87         Test that a response is generated from a stanza without a from address.
     107        L{XmlStreamServerFactory} is a L{Factory}.
    88108        """
    89         stanza = domish.Element(('jabber:client', 'iq'))
    90         stanza['type'] = 'get'
    91         stanza['to'] = 'user1@example.com'
    92         response = toResponse(stanza)
    93         self.assertEqual(response['from'], 'user1@example.com')
    94         self.failIf(response.hasAttribute('to'))
     109        verifyObject(IProtocolFactory, self.factory)
    95110
    96     def test_toResponseNoTo(self):
     111
     112    def test_buildProtocolAuthenticatorInstantiation(self):
    97113        """
    98         Test that a response is generated from a stanza without a to address.
     114        The authenticator factory should be used to instantiate the
     115        authenticator and pass it to the protocol.
     116
     117        The default protocol, L{XmlStream} stores the authenticator it is
     118        passed, and calls its C{associateWithStream} method. so we use that to
     119        check whether our authenticator factory is used and the protocol
     120        instance gets an authenticator.
    99121        """
    100         stanza = domish.Element(('jabber:client', 'iq'))
    101         stanza['type'] = 'get'
    102         stanza['from'] = 'user2@example.com/resource'
    103         response = toResponse(stanza)
    104         self.failIf(response.hasAttribute('from'))
    105         self.assertEqual(response['to'], 'user2@example.com/resource')
     122        xs = self.factory.buildProtocol(None)
     123        self.assertEquals([xs], xs.authenticator.xmlstreams)
    106124
    107     def test_toResponseNoAddressing(self):
     125
     126    def test_buildProtocolXmlStream(self):
    108127        """
    109         Test that a response is generated from a stanza without any addressing.
     128        The protocol factory creates Jabber XML Stream protocols by default.
    110129        """
    111         stanza = domish.Element(('jabber:client', 'message'))
    112         stanza['type'] = 'chat'
    113         response = toResponse(stanza)
    114         self.failIf(response.hasAttribute('to'))
    115         self.failIf(response.hasAttribute('from'))
     130        xs = self.factory.buildProtocol(None)
     131        self.assertIsInstance(xs, xmlstream.XmlStream)
    116132
    117     def test_noID(self):
     133
     134    def test_buildProtocolTwice(self):
    118135        """
    119         Test that a proper response is generated without id attribute.
     136        Subsequent calls to buildProtocol should result in different instances
     137        of the protocol, as well as their authenticators.
    120138        """
    121         stanza = domish.Element(('jabber:client', 'message'))
    122         response = toResponse(stanza)
    123         self.failIf(response.hasAttribute('id'))
     139        xs1 = self.factory.buildProtocol(None)
     140        xs2 = self.factory.buildProtocol(None)
     141        self.assertNotIdentical(xs1, xs2)
     142        self.assertNotIdentical(xs1.authenticator, xs2.authenticator)
     143
     144
     145    def test_buildProtocolInstallsBootstraps(self):
     146        """
     147        The protocol factory installs bootstrap event handlers on the protocol.
     148        """
     149        called = []
     150
     151        def cb(data):
     152            called.append(data)
     153
     154        self.factory.addBootstrap('//event/myevent', cb)
     155
     156        xs = self.factory.buildProtocol(None)
     157        xs.dispatch(None, '//event/myevent')
     158
     159        self.assertEquals(1, len(called))
     160
     161
     162    def test_buildProtocolStoresFactory(self):
     163        """
     164        The protocol factory is saved in the protocol.
     165        """
     166        xs = self.factory.buildProtocol(None)
     167        self.assertIdentical(self.factory, xs.factory)
  • wokkel/test/test_data_form.py

    r29 r56  
    1 # Copyright (c) 2003-2008 Ralph Meijer
     1# Copyright (c) 2003-2009 Ralph Meijer
    22# See LICENSE for details.
    33
     
    88from twisted.trial import unittest
    99from twisted.words.xish import domish
    10 
    11 from wokkel.data_form import Field, Form, Option, FieldNameRequiredError
     10from twisted.words.protocols.jabber import jid
     11
     12from wokkel import data_form
    1213
    1314NS_X_DATA = 'jabber:x:data'
    1415
    15 
    16 
    1716class OptionTest(unittest.TestCase):
    1817    """
    19     Tests for L{Option}.
     18    Tests for L{data_form.Option}.
    2019    """
    2120
    2221    def test_toElement(self):
    23         option = Option('value', 'label')
     22        option = data_form.Option('value', 'label')
    2423        element = option.toElement()
    2524        self.assertEquals('option', element.name)
     
    3433class FieldTest(unittest.TestCase):
    3534    """
    36     Tests for L{Field}.
     35    Tests for L{data_form.Field}.
    3736    """
    3837
    3938    def test_basic(self):
    40         field = Field(var='test')
     39        """
     40        Test basic field initialization.
     41        """
     42        field = data_form.Field(var='test')
    4143        self.assertEqual('text-single', field.fieldType)
    4244        self.assertEqual('test', field.var)
    4345
     46
     47    def test_toElement(self):
     48        """
     49        Test rendering to a DOM.
     50        """
     51        field = data_form.Field(var='test')
    4452        element = field.toElement()
    4553
     
    4755        self.assertEquals('field', element.name)
    4856        self.assertEquals(NS_X_DATA, element.uri)
    49         self.assertEquals('text-single', element['type'])
     57        self.assertEquals('text-single',
     58                          element.getAttribute('type', 'text-single'))
    5059        self.assertEquals('test', element['var'])
    5160        self.assertEquals([], element.children)
    5261
    5362
    54     def test_noFieldName(self):
    55         field = Field()
    56         self.assertRaises(FieldNameRequiredError, field.toElement)
     63    def test_toElementTypeNotListSingle(self):
     64        """
     65        Always render the field type, if different from list-single.
     66        """
     67        field = data_form.Field('hidden', var='test')
     68        element = field.toElement()
     69
     70        self.assertEquals('hidden', element.getAttribute('type'))
     71
     72
     73    def test_toElementAsForm(self):
     74        """
     75        Always render the field type, if asForm is True.
     76        """
     77        field = data_form.Field(var='test')
     78        element = field.toElement(True)
     79
     80        self.assertEquals('text-single', element.getAttribute('type'))
     81
     82
     83    def test_toElementOptions(self):
     84        """
     85        Test rendering to a DOM with options.
     86        """
     87        field = data_form.Field('list-single', var='test')
     88        field.options = [data_form.Option(u'option1'),
     89                         data_form.Option(u'option2')]
     90        element = field.toElement(True)
     91
     92        self.assertEqual(2, len(element.children))
     93
     94
     95    def test_toElementLabel(self):
     96        """
     97        Test rendering to a DOM with a label.
     98        """
     99        field = data_form.Field(var='test', label=u'my label')
     100        element = field.toElement(True)
     101
     102        self.assertEqual(u'my label', element.getAttribute('label'))
     103
     104
     105    def test_toElementDescription(self):
     106        """
     107        Test rendering to a DOM with options.
     108        """
     109        field = data_form.Field(var='test', desc=u'My desc')
     110        element = field.toElement(True)
     111
     112        self.assertEqual(1, len(element.children))
     113        child = element.children[0]
     114        self.assertEqual('desc', child.name)
     115        self.assertEqual(NS_X_DATA, child.uri)
     116        self.assertEqual(u'My desc', unicode(child))
     117
     118
     119    def test_toElementRequired(self):
     120        """
     121        Test rendering to a DOM with options.
     122        """
     123        field = data_form.Field(var='test', required=True)
     124        element = field.toElement(True)
     125
     126        self.assertEqual(1, len(element.children))
     127        child = element.children[0]
     128        self.assertEqual('required', child.name)
     129        self.assertEqual(NS_X_DATA, child.uri)
     130
     131
     132    def test_toElementJID(self):
     133        field = data_form.Field(fieldType='jid-single', var='test',
     134                                value=jid.JID(u'test@example.org'))
     135        element = field.toElement()
     136        self.assertEqual(u'test@example.org', unicode(element.value))
     137
     138
     139    def test_typeCheckNoFieldName(self):
     140        """
     141        A field not of type fixed must have a var.
     142        """
     143        field = data_form.Field(fieldType='list-single')
     144        self.assertRaises(data_form.FieldNameRequiredError, field.typeCheck)
     145
     146
     147    def test_typeCheckTooManyValues(self):
     148        """
     149        Expect an exception if too many values are set, depending on type.
     150        """
     151        field = data_form.Field(fieldType='list-single', var='test',
     152                                values=[u'value1', u'value2'])
     153        self.assertRaises(data_form.TooManyValuesError, field.typeCheck)
     154
     155
     156    def test_typeCheckBooleanFalse(self):
     157        """
     158        Test possible False values for a boolean field.
     159        """
     160        field = data_form.Field(fieldType='boolean', var='test')
     161
     162        for value in (False, 0, '0', 'false', 'False', []):
     163            field.value = value
     164            field.typeCheck()
     165            self.assertIsInstance(field.value, bool)
     166            self.assertFalse(field.value)
     167
     168
     169    def test_typeCheckBooleanTrue(self):
     170        """
     171        Test possible True values for a boolean field.
     172        """
     173        field = data_form.Field(fieldType='boolean', var='test')
     174
     175        for value in (True, 1, '1', 'true', 'True', ['test']):
     176            field.value = value
     177            field.typeCheck()
     178            self.assertIsInstance(field.value, bool)
     179            self.assertTrue(field.value)
     180
     181
     182    def test_typeCheckBooleanBad(self):
     183        """
     184        A bad value for a boolean field should raise a ValueError
     185        """
     186        field = data_form.Field(fieldType='boolean', var='test')
     187        field.value = 'test'
     188        self.assertRaises(ValueError, field.typeCheck)
     189
     190
     191    def test_typeCheckJID(self):
     192        """
     193        The value of jid field should be a JID or coercable to one.
     194        """
     195        field = data_form.Field(fieldType='jid-single', var='test',
     196                                value=jid.JID('test@example.org'))
     197        field.typeCheck()
     198
     199
     200    def test_typeCheckJIDString(self):
     201        """
     202        The string value of jid field should be coercable into a JID.
     203        """
     204        field = data_form.Field(fieldType='jid-single', var='test',
     205                                value='test@example.org')
     206        field.typeCheck()
     207        self.assertEquals(jid.JID(u'test@example.org'), field.value)
     208
     209
     210    def test_typeCheckJIDBad(self):
     211        """
     212        An invalid JID string should raise an exception.
     213        """
     214        field = data_form.Field(fieldType='jid-single', var='test',
     215                                value='test@@example.org')
     216        self.assertRaises(jid.InvalidFormat, field.typeCheck)
    57217
    58218
     
    60220        element = domish.Element((NS_X_DATA, 'field'))
    61221        element['type'] = 'fixed'
    62         field = Field.fromElement(element)
     222        field = data_form.Field.fromElement(element)
    63223        self.assertEquals('fixed', field.fieldType)
    64224
     
    66226    def test_fromElementNoType(self):
    67227        element = domish.Element((NS_X_DATA, 'field'))
    68         field = Field.fromElement(element)
     228        field = data_form.Field.fromElement(element)
    69229        self.assertEquals(None, field.fieldType)
    70230
    71231
    72     def test_fromElementValue(self):
    73         element = domish.Element((NS_X_DATA, 'field'))
    74         element.addElement("value", content="text")
    75         field = Field.fromElement(element)
     232    def test_fromElementValueTextSingle(self):
     233        """
     234        Parsed text-single field values should be of type C{unicode}.
     235        """
     236        element = domish.Element((NS_X_DATA, 'field'))
     237        element['type'] = 'text-single'
     238        element.addElement('value', content=u'text')
     239        field = data_form.Field.fromElement(element)
    76240        self.assertEquals('text', field.value)
    77241
    78242
     243    def test_fromElementValueJID(self):
     244        """
     245        Parsed jid-single field values should be of type C{unicode}.
     246        """
     247        element = domish.Element((NS_X_DATA, 'field'))
     248        element['type'] = 'jid-single'
     249        element.addElement('value', content=u'user@example.org')
     250        field = data_form.Field.fromElement(element)
     251        self.assertEquals(u'user@example.org', field.value)
     252
     253    def test_fromElementValueJIDMalformed(self):
     254        """
     255        Parsed jid-single field values should be of type C{unicode}.
     256
     257        No validation should be done at this point, so invalid JIDs should
     258        also be passed as-is.
     259        """
     260        element = domish.Element((NS_X_DATA, 'field'))
     261        element['type'] = 'jid-single'
     262        element.addElement('value', content=u'@@')
     263        field = data_form.Field.fromElement(element)
     264        self.assertEquals(u'@@', field.value)
     265
     266
     267    def test_fromElementValueBoolean(self):
     268        """
     269        Parsed boolean field values should be of type C{unicode}.
     270        """
     271        element = domish.Element((NS_X_DATA, 'field'))
     272        element['type'] = 'boolean'
     273        element.addElement('value', content=u'false')
     274        field = data_form.Field.fromElement(element)
     275        self.assertEquals(u'false', field.value)
     276
     277
    79278
    80279class FormTest(unittest.TestCase):
    81280    """
    82     Tests for L{Form}.
     281    Tests for L{data_form.Form}.
    83282    """
    84283
     
    88287        """
    89288
    90         form = Form('result')
     289        form = data_form.Form('result')
    91290        self.assertEqual('result', form.formType)
    92291
     
    95294        The toElement method returns a form's DOM representation.
    96295        """
    97         form = Form('result')
     296        form = data_form.Form('result')
    98297        element = form.toElement()
    99298
     
    107306    def test_fromElement(self):
    108307        """
    109         The fromElement static method creates a L{Form} from a L{DOM.
     308        C{fromElement} creates a L{data_form.Form} from a DOM representation.
    110309        """
    111310        element = domish.Element((NS_X_DATA, 'x'))
    112311        element['type'] = 'result'
    113         form = Form.fromElement(element)
     312        form = data_form.Form.fromElement(element)
    114313
    115314        self.assertEquals('result', form.formType)
     
    124323        """
    125324        element = domish.Element((NS_X_DATA, 'form'))
    126         self.assertRaises(Exception, Form.fromElement, element)
     325        self.assertRaises(Exception, data_form.Form.fromElement, element)
    127326
    128327
     
    132331        """
    133332        element = domish.Element(('myns', 'x'))
    134         self.assertRaises(Exception, Form.fromElement, element)
     333        self.assertRaises(Exception, data_form.Form.fromElement, element)
    135334
    136335
     
    138337        element = domish.Element((NS_X_DATA, 'x'))
    139338        element.addElement('title', content='My title')
    140         form = Form.fromElement(element)
     339        form = data_form.Form.fromElement(element)
    141340
    142341        self.assertEquals('My title', form.title)
     
    146345        element = domish.Element((NS_X_DATA, 'x'))
    147346        element.addElement('instructions', content='instruction')
    148         form = Form.fromElement(element)
     347        form = data_form.Form.fromElement(element)
    149348
    150349        self.assertEquals(['instruction'], form.instructions)
     
    154353        element.addElement('instructions', content='instruction 1')
    155354        element.addElement('instructions', content='instruction 2')
    156         form = Form.fromElement(element)
     355        form = data_form.Form.fromElement(element)
    157356
    158357        self.assertEquals(['instruction 1', 'instruction 2'], form.instructions)
     
    162361        element = domish.Element((NS_X_DATA, 'x'))
    163362        element.addElement('field')
    164         form = Form.fromElement(element)
     363        form = data_form.Form.fromElement(element)
    165364
    166365        self.assertEquals(1, len(form.fieldList))
     
    172371        element.addElement('field')['var'] = 'field1'
    173372        element.addElement('field')['var'] = 'field2'
    174         form = Form.fromElement(element)
     373        form = data_form.Form.fromElement(element)
    175374
    176375        self.assertEquals(2, len(form.fieldList))
  • wokkel/test/test_disco.py

    r31 r53  
    1 # Copyright (c) 2003-2008 Ralph Meijer
     1# Copyright (c) 2003-2009 Ralph Meijer
    22# See LICENSE for details.
    33
     
    66"""
    77
     8from zope.interface import implements
     9
    810from twisted.internet import defer
    911from twisted.trial import unittest
    10 from twisted.words.xish.xmlstream import XmlStreamFactory
    11 from zope.interface import implements
    12 
    13 from wokkel.subprotocols import XMPPHandler, StreamManager
    14 
    15 from wokkel import disco
     12from twisted.words.protocols.jabber.jid import JID
     13from twisted.words.protocols.jabber.xmlstream import toResponse
     14from twisted.words.xish import domish
     15
     16from wokkel import data_form, disco
     17from wokkel.generic import parseXml
     18from wokkel.subprotocols import XMPPHandler
     19from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
    1620
    1721NS_DISCO_INFO = 'http://jabber.org/protocol/disco#info'
    1822NS_DISCO_ITEMS = 'http://jabber.org/protocol/disco#items'
    1923
    20 class DiscoResponder(XMPPHandler):
    21     implements(disco.IDisco)
    22 
    23     def getDiscoInfo(self, requestor, target, nodeIdentifier):
    24         if not nodeIdentifier:
     24class DiscoFeatureTest(unittest.TestCase):
     25    """
     26    Tests for L{disco.DiscoFeature}.
     27    """
     28
     29    def test_init(self):
     30        """
     31        Test initialization with a with feature namespace URI.
     32        """
     33        feature = disco.DiscoFeature(u'testns')
     34        self.assertEqual(u'testns', feature)
     35
     36
     37    def test_toElement(self):
     38        """
     39        Test proper rendering to a DOM representation.
     40
     41        The returned element should be properly named and have a C{var}
     42        attribute that holds the feature namespace URI.
     43        """
     44        feature = disco.DiscoFeature(u'testns')
     45        element = feature.toElement()
     46        self.assertEqual(NS_DISCO_INFO, element.uri)
     47        self.assertEqual(u'feature', element.name)
     48        self.assertTrue(element.hasAttribute(u'var'))
     49        self.assertEqual(u'testns', element[u'var'])
     50
     51
     52    def test_fromElement(self):
     53        """
     54        Test creating L{disco.DiscoFeature} from L{domish.Element}.
     55        """
     56        element = domish.Element((NS_DISCO_INFO, u'feature'))
     57        element['var'] = u'testns'
     58        feature = disco.DiscoFeature.fromElement(element)
     59        self.assertEqual(u'testns', feature)
     60
     61
     62
     63class DiscoIdentityTest(unittest.TestCase):
     64    """
     65    Tests for L{disco.DiscoIdentity}.
     66    """
     67
     68    def test_init(self):
     69        """
     70        Test initialization with a category, type and name.
     71        """
     72        identity = disco.DiscoIdentity(u'conference', u'text', u'The chatroom')
     73        self.assertEqual(u'conference', identity.category)
     74        self.assertEqual(u'text', identity.type)
     75        self.assertEqual(u'The chatroom', identity.name)
     76
     77
     78    def test_toElement(self):
     79        """
     80        Test proper rendering to a DOM representation.
     81
     82        The returned element should be properly named and have C{conference},
     83        C{type}, and C{name} attributes.
     84        """
     85        identity = disco.DiscoIdentity(u'conference', u'text', u'The chatroom')
     86        element = identity.toElement()
     87        self.assertEqual(NS_DISCO_INFO, element.uri)
     88        self.assertEqual(u'identity', element.name)
     89        self.assertEqual(u'conference', element.getAttribute(u'category'))
     90        self.assertEqual(u'text', element.getAttribute(u'type'))
     91        self.assertEqual(u'The chatroom', element.getAttribute(u'name'))
     92
     93
     94    def test_toElementWithoutName(self):
     95        """
     96        Test proper rendering to a DOM representation without a name.
     97
     98        The returned element should be properly named and have C{conference},
     99        C{type} attributes, no C{name} attribute.
     100        """
     101        identity = disco.DiscoIdentity(u'conference', u'text')
     102        element = identity.toElement()
     103        self.assertEqual(NS_DISCO_INFO, element.uri)
     104        self.assertEqual(u'identity', element.name)
     105        self.assertEqual(u'conference', element.getAttribute(u'category'))
     106        self.assertEqual(u'text', element.getAttribute(u'type'))
     107        self.assertFalse(element.hasAttribute(u'name'))
     108
     109
     110    def test_fromElement(self):
     111        """
     112        Test creating L{disco.DiscoIdentity} from L{domish.Element}.
     113        """
     114        element = domish.Element((NS_DISCO_INFO, u'identity'))
     115        element['category'] = u'conference'
     116        element['type'] = u'text'
     117        element['name'] = u'The chatroom'
     118        identity = disco.DiscoIdentity.fromElement(element)
     119        self.assertEqual(u'conference', identity.category)
     120        self.assertEqual(u'text', identity.type)
     121        self.assertEqual(u'The chatroom', identity.name)
     122
     123
     124    def test_fromElementWithoutName(self):
     125        """
     126        Test creating L{disco.DiscoIdentity} from L{domish.Element}, no name.
     127        """
     128        element = domish.Element((NS_DISCO_INFO, u'identity'))
     129        element['category'] = u'conference'
     130        element['type'] = u'text'
     131        identity = disco.DiscoIdentity.fromElement(element)
     132        self.assertEqual(u'conference', identity.category)
     133        self.assertEqual(u'text', identity.type)
     134        self.assertEqual(None, identity.name)
     135
     136
     137
     138class DiscoInfoTest(unittest.TestCase):
     139    """
     140    Tests for L{disco.DiscoInfo}.
     141    """
     142
     143    def test_toElement(self):
     144        """
     145        Test C{toElement} creates a correctly namespaced element, no node.
     146        """
     147        info = disco.DiscoInfo()
     148        element = info.toElement()
     149
     150        self.assertEqual(NS_DISCO_INFO, element.uri)
     151        self.assertEqual(u'query', element.name)
     152        self.assertFalse(element.hasAttribute(u'node'))
     153
     154
     155    def test_toElementNode(self):
     156        """
     157        Test C{toElement} with a node.
     158        """
     159        info = disco.DiscoInfo()
     160        info.nodeIdentifier = u'test'
     161        element = info.toElement()
     162
     163        self.assertEqual(u'test', element.getAttribute(u'node'))
     164
     165
     166    def test_toElementChildren(self):
     167        """
     168        Test C{toElement} creates a DOM with proper childs.
     169        """
     170        info = disco.DiscoInfo()
     171        info.append(disco.DiscoFeature(u'jabber:iq:register'))
     172        info.append(disco.DiscoIdentity(u'conference', u'text'))
     173        info.append(data_form.Form(u'result'))
     174        element = info.toElement()
     175
     176        featureElements = domish.generateElementsQNamed(element.children,
     177                                                        u'feature',
     178                                                        NS_DISCO_INFO)
     179        self.assertEqual(1, len(list(featureElements)))
     180
     181        identityElements = domish.generateElementsQNamed(element.children,
     182                                                         u'identity',
     183                                                         NS_DISCO_INFO)
     184        self.assertEqual(1, len(list(identityElements)))
     185
     186        extensionElements = domish.generateElementsQNamed(element.children,
     187                                                         u'x',
     188                                                         data_form.NS_X_DATA)
     189        self.assertEqual(1, len(list(extensionElements)))
     190
     191
     192    def test_fromElement(self):
     193        """
     194        Test properties when creating L{disco.DiscoInfo} from L{domish.Element}.
     195        """
     196        xml = """<query xmlns='http://jabber.org/protocol/disco#info'>
     197                   <identity category='conference'
     198                             type='text'
     199                             name='A Dark Cave'/>
     200                   <feature var='http://jabber.org/protocol/muc'/>
     201                   <feature var='jabber:iq:register'/>
     202                   <x xmlns='jabber:x:data' type='result'>
     203                     <field var='FORM_TYPE' type='hidden'>
     204                       <value>http://jabber.org/protocol/muc#roominfo</value>
     205                     </field>
     206                   </x>
     207                 </query>"""
     208
     209        element = parseXml(xml)
     210        info = disco.DiscoInfo.fromElement(element)
     211
     212        self.assertIn(u'http://jabber.org/protocol/muc', info.features)
     213        self.assertIn(u'jabber:iq:register', info.features)
     214
     215        self.assertIn((u'conference', u'text'), info.identities)
     216        self.assertEqual(u'A Dark Cave',
     217                          info.identities[(u'conference', u'text')])
     218
     219        self.assertIn(u'http://jabber.org/protocol/muc#roominfo',
     220                      info.extensions)
     221
     222
     223    def test_fromElementItems(self):
     224        """
     225        Test items when creating L{disco.DiscoInfo} from L{domish.Element}.
     226        """
     227        xml = """<query xmlns='http://jabber.org/protocol/disco#info'>
     228                   <identity category='conference'
     229                             type='text'
     230                             name='A Dark Cave'/>
     231                   <feature var='http://jabber.org/protocol/muc'/>
     232                   <feature var='jabber:iq:register'/>
     233                   <x xmlns='jabber:x:data' type='result'>
     234                     <field var='FORM_TYPE' type='hidden'>
     235                       <value>http://jabber.org/protocol/muc#roominfo</value>
     236                     </field>
     237                   </x>
     238                 </query>"""
     239
     240        element = parseXml(xml)
     241        info = disco.DiscoInfo.fromElement(element)
     242
     243        info = list(info)
     244        self.assertEqual(4, len(info))
     245
     246        identity = info[0]
     247        self.assertEqual(u'conference', identity.category)
     248
     249        self.assertEqual(u'http://jabber.org/protocol/muc', info[1])
     250        self.assertEqual(u'jabber:iq:register', info[2])
     251
     252        extension = info[3]
     253        self.assertEqual(u'http://jabber.org/protocol/muc#roominfo',
     254                         extension.formNamespace)
     255
     256
     257    def test_fromElementNoNode(self):
     258        """
     259        Test creating L{disco.DiscoInfo} from L{domish.Element}, no node.
     260        """
     261        xml = """<query xmlns='http://jabber.org/protocol/disco#info'/>"""
     262
     263        element = parseXml(xml)
     264        info = disco.DiscoInfo.fromElement(element)
     265
     266        self.assertEqual(u'', info.nodeIdentifier)
     267
     268
     269    def test_fromElementNode(self):
     270        """
     271        Test creating L{disco.DiscoInfo} from L{domish.Element}, with node.
     272        """
     273        xml = """<query xmlns='http://jabber.org/protocol/disco#info'
     274                        node='test'>
     275                 </query>"""
     276
     277        element = parseXml(xml)
     278        info = disco.DiscoInfo.fromElement(element)
     279
     280        self.assertEqual(u'test', info.nodeIdentifier)
     281
     282
     283
     284class DiscoItemTest(unittest.TestCase):
     285    """
     286    Tests for L{disco.DiscoItem}.
     287    """
     288
     289    def test_init(self):
     290        """
     291        Test initialization with a category, type and name.
     292        """
     293        item = disco.DiscoItem(JID(u'example.org'), u'test', u'The node')
     294        self.assertEqual(JID(u'example.org'), item.entity)
     295        self.assertEqual(u'test', item.nodeIdentifier)
     296        self.assertEqual(u'The node', item.name)
     297
     298
     299    def test_toElement(self):
     300        """
     301        Test proper rendering to a DOM representation.
     302
     303        The returned element should be properly named and have C{jid}, C{node},
     304        and C{name} attributes.
     305        """
     306        item = disco.DiscoItem(JID(u'example.org'), u'test', u'The node')
     307        element = item.toElement()
     308        self.assertEqual(NS_DISCO_ITEMS, element.uri)
     309        self.assertEqual(u'item', element.name)
     310        self.assertEqual(u'example.org', element.getAttribute(u'jid'))
     311        self.assertEqual(u'test', element.getAttribute(u'node'))
     312        self.assertEqual(u'The node', element.getAttribute(u'name'))
     313
     314
     315    def test_toElementWithoutName(self):
     316        """
     317        Test proper rendering to a DOM representation without a name.
     318
     319        The returned element should be properly named and have C{jid}, C{node}
     320        attributes, no C{name} attribute.
     321        """
     322        item = disco.DiscoItem(JID(u'example.org'), u'test')
     323        element = item.toElement()
     324        self.assertEqual(NS_DISCO_ITEMS, element.uri)
     325        self.assertEqual(u'item', element.name)
     326        self.assertEqual(u'example.org', element.getAttribute(u'jid'))
     327        self.assertEqual(u'test', element.getAttribute(u'node'))
     328        self.assertFalse(element.hasAttribute(u'name'))
     329
     330
     331    def test_fromElement(self):
     332        """
     333        Test creating L{disco.DiscoItem} from L{domish.Element}.
     334        """
     335        element = domish.Element((NS_DISCO_ITEMS, u'item'))
     336        element[u'jid'] = u'example.org'
     337        element[u'node'] = u'test'
     338        element[u'name'] = u'The node'
     339        item = disco.DiscoItem.fromElement(element)
     340        self.assertEqual(JID(u'example.org'), item.entity)
     341        self.assertEqual(u'test', item.nodeIdentifier)
     342        self.assertEqual(u'The node', item.name)
     343
     344    def test_fromElementNoNode(self):
     345        """
     346        Test creating L{disco.DiscoItem} from L{domish.Element}, no node.
     347        """
     348        element = domish.Element((NS_DISCO_ITEMS, u'item'))
     349        element[u'jid'] = u'example.org'
     350        element[u'name'] = u'The node'
     351        item = disco.DiscoItem.fromElement(element)
     352        self.assertEqual(JID(u'example.org'), item.entity)
     353        self.assertEqual(u'', item.nodeIdentifier)
     354        self.assertEqual(u'The node', item.name)
     355
     356
     357    def test_fromElementNoName(self):
     358        """
     359        Test creating L{disco.DiscoItem} from L{domish.Element}, no name.
     360        """
     361        element = domish.Element((NS_DISCO_ITEMS, u'item'))
     362        element[u'jid'] = u'example.org'
     363        element[u'node'] = u'test'
     364        item = disco.DiscoItem.fromElement(element)
     365        self.assertEqual(JID(u'example.org'), item.entity)
     366        self.assertEqual(u'test', item.nodeIdentifier)
     367        self.assertEqual(None, item.name)
     368
     369    def test_fromElementBadJID(self):
     370        """
     371        Test creating L{disco.DiscoItem} from L{domish.Element}, bad JID.
     372        """
     373        element = domish.Element((NS_DISCO_ITEMS, u'item'))
     374        element[u'jid'] = u'ex@@@ample.org'
     375        item = disco.DiscoItem.fromElement(element)
     376        self.assertIdentical(None, item.entity)
     377
     378
     379
     380class DiscoItemsTest(unittest.TestCase):
     381    """
     382    Tests for L{disco.DiscoItems}.
     383    """
     384
     385    def test_toElement(self):
     386        """
     387        Test C{toElement} creates a correctly namespaced element, no node.
     388        """
     389        items = disco.DiscoItems()
     390        element = items.toElement()
     391
     392        self.assertEqual(NS_DISCO_ITEMS, element.uri)
     393        self.assertEqual(u'query', element.name)
     394        self.assertFalse(element.hasAttribute(u'node'))
     395
     396
     397    def test_toElementNode(self):
     398        """
     399        Test C{toElement} with a node.
     400        """
     401        items = disco.DiscoItems()
     402        items.nodeIdentifier = u'test'
     403        element = items.toElement()
     404
     405        self.assertEqual(u'test', element.getAttribute(u'node'))
     406
     407
     408    def test_toElementChildren(self):
     409        """
     410        Test C{toElement} creates a DOM with proper childs.
     411        """
     412        items = disco.DiscoItems()
     413        items.append(disco.DiscoItem(JID(u'example.org'), u'test', u'A node'))
     414        element = items.toElement()
     415
     416        itemElements = domish.generateElementsQNamed(element.children,
     417                                                     u'item',
     418                                                     NS_DISCO_ITEMS)
     419        self.assertEqual(1, len(list(itemElements)))
     420
     421
     422    def test_fromElement(self):
     423        """
     424        Test creating L{disco.DiscoItems} from L{domish.Element}.
     425        """
     426        xml = """<query xmlns='http://jabber.org/protocol/disco#items'>
     427                   <item jid='example.org' node='test' name='A node'/>
     428                 </query>"""
     429
     430        element = parseXml(xml)
     431        items = disco.DiscoItems.fromElement(element)
     432
     433        items = list(items)
     434        self.assertEqual(1, len(items))
     435        item = items[0]
     436
     437        self.assertEqual(JID(u'example.org'), item.entity)
     438        self.assertEqual(u'test', item.nodeIdentifier)
     439        self.assertEqual(u'A node', item.name)
     440
     441
     442    def test_fromElementNoNode(self):
     443        """
     444        Test creating L{disco.DiscoItems} from L{domish.Element}, no node.
     445        """
     446        xml = """<query xmlns='http://jabber.org/protocol/disco#items'/>"""
     447
     448        element = parseXml(xml)
     449        items = disco.DiscoItems.fromElement(element)
     450
     451        self.assertEqual(u'', items.nodeIdentifier)
     452
     453
     454    def test_fromElementNode(self):
     455        """
     456        Test creating L{disco.DiscoItems} from L{domish.Element}, with node.
     457        """
     458        xml = """<query xmlns='http://jabber.org/protocol/disco#items'
     459                        node='test'>
     460                 </query>"""
     461
     462        element = parseXml(xml)
     463        items = disco.DiscoItems.fromElement(element)
     464
     465        self.assertEqual(u'test', items.nodeIdentifier)
     466
     467
     468
     469class DiscoClientProtocolTest(unittest.TestCase):
     470    """
     471    Tests for L{disco.DiscoClientProtocol}.
     472    """
     473
     474    def setUp(self):
     475        """
     476        Set up stub and protocol for testing.
     477        """
     478        self.stub = XmlStreamStub()
     479        self.protocol = disco.DiscoClientProtocol()
     480        self.protocol.xmlstream = self.stub.xmlstream
     481        self.protocol.connectionInitialized()
     482
     483
     484    def test_requestItems(self):
     485        """
     486        Test request sent out by C{requestItems} and parsing of response.
     487        """
     488        def cb(items):
     489            items = list(items)
     490            self.assertEqual(2, len(items))
     491            self.assertEqual(JID(u'test.example.org'), items[0].entity)
     492
     493        d = self.protocol.requestItems(JID(u'example.org'),u"foo")
     494        d.addCallback(cb)
     495
     496        iq = self.stub.output[-1]
     497        self.assertEqual(u'example.org', iq.getAttribute(u'to'))
     498        self.assertEqual(u'get', iq.getAttribute(u'type'))
     499        self.assertEqual(u'foo', iq.query.getAttribute(u'node'))
     500        self.assertEqual(NS_DISCO_ITEMS, iq.query.uri)
     501
     502        response = toResponse(iq, u'result')
     503        query = response.addElement((NS_DISCO_ITEMS, u'query'))
     504
     505        element = query.addElement(u'item')
     506        element[u'jid'] = u'test.example.org'
     507        element[u'node'] = u'music'
     508        element[u'name'] = u'Music from the time of Shakespeare'
     509
     510        element = query.addElement(u'item')
     511        element[u'jid'] = u"test2.example.org"
     512
     513        self.stub.send(response)
     514        return d
     515
     516
     517    def test_requestInfo(self):
     518        """
     519        Test request sent out by C{requestInfo} and parsing of response.
     520        """
     521        def cb(info):
     522            self.assertIn((u'conference', u'text'), info.identities)
     523            self.assertIn(u'http://jabber.org/protocol/disco#info',
     524                          info.features)
     525            self.assertIn(u'http://jabber.org/protocol/muc',
     526                          info.features)
     527
     528        d = self.protocol.requestInfo(JID(u'example.org'),'foo')
     529        d.addCallback(cb)
     530
     531        iq = self.stub.output[-1]
     532        self.assertEqual(u'example.org', iq.getAttribute(u'to'))
     533        self.assertEqual(u'get', iq.getAttribute(u'type'))
     534        self.assertEqual(u'foo', iq.query.getAttribute(u'node'))
     535        self.assertEqual(NS_DISCO_INFO, iq.query.uri)
     536
     537        response = toResponse(iq, u'result')
     538        query = response.addElement((NS_DISCO_INFO, u'query'))
     539
     540        element = query.addElement(u"identity")
     541        element[u'category'] = u'conference' # required
     542        element[u'type'] = u'text' # required
     543        element[u"name"] = u'Romeo and Juliet, Act II, Scene II' # optional
     544
     545        element = query.addElement("feature")
     546        element[u'var'] = u'http://jabber.org/protocol/disco#info' # required
     547
     548        element = query.addElement(u"feature")
     549        element[u'var'] = u'http://jabber.org/protocol/muc'
     550
     551        self.stub.send(response)
     552        return d
     553
     554
     555
     556class DiscoHandlerTest(unittest.TestCase, TestableRequestHandlerMixin):
     557    """
     558    Tests for L{disco.DiscoHandler}.
     559    """
     560
     561    def setUp(self):
     562        self.service = disco.DiscoHandler()
     563
     564
     565    def test_onDiscoInfo(self):
     566        """
     567        C{onDiscoInfo} should process an info request and return a response.
     568
     569        The request should be parsed, C{info} called with the extracted
     570        parameters, and then the result should be formatted into a proper
     571        response element.
     572        """
     573        xml = """<iq from='test@example.com' to='example.com'
     574                     type='get'>
     575                   <query xmlns='%s'/>
     576                 </iq>""" % NS_DISCO_INFO
     577
     578        def cb(element):
     579            self.assertEqual('query', element.name)
     580            self.assertEqual(NS_DISCO_INFO, element.uri)
     581            self.assertEqual(NS_DISCO_INFO, element.identity.uri)
     582            self.assertEqual('dummy', element.identity['category'])
     583            self.assertEqual('generic', element.identity['type'])
     584            self.assertEqual('Generic Dummy Entity', element.identity['name'])
     585            self.assertEqual(NS_DISCO_INFO, element.feature.uri)
     586            self.assertEqual('jabber:iq:version', element.feature['var'])
     587
     588        def info(requestor, target, nodeIdentifier):
     589            self.assertEqual(JID('test@example.com'), requestor)
     590            self.assertEqual(JID('example.com'), target)
     591            self.assertEqual('', nodeIdentifier)
     592
    25593            return defer.succeed([
    26594                disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'),
    27595                disco.DiscoFeature('jabber:iq:version')
    28596            ])
    29         else:
    30             return defer.succeed([])
    31 
    32 class DiscoHandlerTest(unittest.TestCase):
    33     def test_DiscoInfo(self):
    34         factory = XmlStreamFactory()
    35         sm = StreamManager(factory)
    36         disco.DiscoHandler().setHandlerParent(sm)
    37         DiscoResponder().setHandlerParent(sm)
    38         xs = factory.buildProtocol(None)
    39         output = []
    40         xs.send = output.append
    41         xs.connectionMade()
    42         xs.dispatch(xs, "//event/stream/authd")
    43         xs.dataReceived("<stream>")
    44         xs.dataReceived("""<iq from='test@example.com' to='example.com'
    45                                type='get'>
    46                              <query xmlns='%s'/>
    47                            </iq>""" % NS_DISCO_INFO)
    48         reply = output[0]
    49         self.assertEqual(NS_DISCO_INFO, reply.query.uri)
    50         self.assertEqual(NS_DISCO_INFO, reply.query.identity.uri)
    51         self.assertEqual('dummy', reply.query.identity['category'])
    52         self.assertEqual('generic', reply.query.identity['type'])
    53         self.assertEqual('Generic Dummy Entity', reply.query.identity['name'])
    54         self.assertEqual(NS_DISCO_INFO, reply.query.feature.uri)
    55         self.assertEqual('jabber:iq:version', reply.query.feature['var'])
    56 
     597
     598        self.service.info = info
     599        d = self.handleRequest(xml)
     600        d.addCallback(cb)
     601        return d
     602
     603
     604    def test_onDiscoInfoWithNode(self):
     605        """
     606        An info request for a node should return it in the response.
     607        """
     608        xml = """<iq from='test@example.com' to='example.com'
     609                     type='get'>
     610                   <query xmlns='%s' node='test'/>
     611                 </iq>""" % NS_DISCO_INFO
     612
     613        def cb(element):
     614            self.assertTrue(element.hasAttribute('node'))
     615            self.assertEqual('test', element['node'])
     616
     617        def info(requestor, target, nodeIdentifier):
     618            self.assertEqual('test', nodeIdentifier)
     619
     620            return defer.succeed([
     621                disco.DiscoFeature('jabber:iq:version')
     622            ])
     623
     624        self.service.info = info
     625        d = self.handleRequest(xml)
     626        d.addCallback(cb)
     627        return d
     628
     629
     630    def test_onDiscoItems(self):
     631        """
     632        C{onDiscoItems} should process an items request and return a response.
     633
     634        The request should be parsed, C{items} called with the extracted
     635        parameters, and then the result should be formatted into a proper
     636        response element.
     637        """
     638        xml = """<iq from='test@example.com' to='example.com'
     639                     type='get'>
     640                   <query xmlns='%s'/>
     641                 </iq>""" % NS_DISCO_ITEMS
     642
     643        def cb(element):
     644            self.assertEqual('query', element.name)
     645            self.assertEqual(NS_DISCO_ITEMS, element.uri)
     646            self.assertEqual(NS_DISCO_ITEMS, element.item.uri)
     647            self.assertEqual('example.com', element.item['jid'])
     648            self.assertEqual('test', element.item['node'])
     649            self.assertEqual('Test node', element.item['name'])
     650
     651        def items(requestor, target, nodeIdentifier):
     652            self.assertEqual(JID('test@example.com'), requestor)
     653            self.assertEqual(JID('example.com'), target)
     654            self.assertEqual('', nodeIdentifier)
     655
     656            return defer.succeed([
     657                disco.DiscoItem(JID('example.com'), 'test', 'Test node'),
     658            ])
     659
     660        self.service.items = items
     661        d = self.handleRequest(xml)
     662        d.addCallback(cb)
     663        return d
     664
     665
     666    def test_onDiscoItemsWithNode(self):
     667        """
     668        An items request for a node should return it in the response.
     669        """
     670        xml = """<iq from='test@example.com' to='example.com'
     671                     type='get'>
     672                   <query xmlns='%s' node='test'/>
     673                 </iq>""" % NS_DISCO_ITEMS
     674
     675        def cb(element):
     676            self.assertTrue(element.hasAttribute('node'))
     677            self.assertEqual('test', element['node'])
     678
     679        def items(requestor, target, nodeIdentifier):
     680            self.assertEqual('test', nodeIdentifier)
     681
     682            return defer.succeed([
     683                disco.DiscoFeature('jabber:iq:version')
     684            ])
     685
     686        self.service.items = items
     687        d = self.handleRequest(xml)
     688        d.addCallback(cb)
     689        return d
     690
     691
     692    def test_info(self):
     693        """
     694        C{info} should gather disco info from sibling handlers.
     695        """
     696        discoItems = [disco.DiscoIdentity('dummy', 'generic',
     697                                          'Generic Dummy Entity'),
     698                      disco.DiscoFeature('jabber:iq:version')
     699        ]
     700
     701        class DiscoResponder(XMPPHandler):
     702            implements(disco.IDisco)
     703
     704            def getDiscoInfo(self, requestor, target, nodeIdentifier):
     705                if not nodeIdentifier:
     706                    return defer.succeed(discoItems)
     707                else:
     708                    return defer.succeed([])
     709
     710        def cb(result):
     711            self.assertEquals(discoItems, result)
     712
     713        self.service.parent = [self.service, DiscoResponder()]
     714        d = self.service.info(JID('test@example.com'), JID('example.com'), '')
     715        d.addCallback(cb)
     716        return d
     717
     718
     719    def test_items(self):
     720        """
     721        C{info} should gather disco items from sibling handlers.
     722        """
     723        discoItems = [disco.DiscoItem(JID('example.com'), 'test', 'Test node')]
     724
     725        class DiscoResponder(XMPPHandler):
     726            implements(disco.IDisco)
     727
     728            def getDiscoItems(self, requestor, target, nodeIdentifier):
     729                if not nodeIdentifier:
     730                    return defer.succeed(discoItems)
     731                else:
     732                    return defer.succeed([])
     733
     734        def cb(result):
     735            self.assertEquals(discoItems, result)
     736
     737        self.service.parent = [self.service, DiscoResponder()]
     738        d = self.service.items(JID('test@example.com'), JID('example.com'), '')
     739        d.addCallback(cb)
     740        return d
  • wokkel/test/test_generic.py

    r20 r35  
    4949        self.assertEquals(1, len(elements))
    5050        self.assertEquals('0.1.0', unicode(elements[0]))
     51
     52
     53
     54class XmlPipeTest(unittest.TestCase):
     55    """
     56    Tests for L{wokkel.generic.XmlPipe}.
     57    """
     58
     59    def setUp(self):
     60        self.pipe = generic.XmlPipe()
     61
     62
     63    def test_sendFromSource(self):
     64        """
     65        Send an element from the source and observe it from the sink.
     66        """
     67        def cb(obj):
     68            called.append(obj)
     69
     70        called = []
     71        self.pipe.sink.addObserver('/test[@xmlns="testns"]', cb)
     72        element = domish.Element(('testns', 'test'))
     73        self.pipe.source.send(element)
     74        self.assertEquals([element], called)
     75
     76
     77    def test_sendFromSink(self):
     78        """
     79        Send an element from the sink and observe it from the source.
     80        """
     81        def cb(obj):
     82            called.append(obj)
     83
     84        called = []
     85        self.pipe.source.addObserver('/test[@xmlns="testns"]', cb)
     86        element = domish.Element(('testns', 'test'))
     87        self.pipe.sink.send(element)
     88        self.assertEquals([element], called)
  • wokkel/test/test_pubsub.py

    r30 r59  
    1 # Copyright (c) 2003-2008 Ralph Meijer
     1# Copyright (c) 2003-2009 Ralph Meijer
    22# See LICENSE for details.
    33
     
    1010from twisted.trial import unittest
    1111from twisted.internet import defer
    12 from twisted.words.xish import domish, xpath
     12from twisted.words.xish import domish
    1313from twisted.words.protocols.jabber import error
    1414from twisted.words.protocols.jabber.jid import JID
    15 
    16 from wokkel import data_form, iwokkel, pubsub, shim
     15from twisted.words.protocols.jabber.xmlstream import toResponse
     16
     17from wokkel import data_form, disco, iwokkel, pubsub, shim
    1718from wokkel.generic import parseXml
    18 from wokkel.test.helpers import XmlStreamStub
    19 
    20 try:
    21     from twisted.words.protocols.jabber.xmlstream import toResponse
    22 except ImportError:
    23     from wokkel.compat import toResponse
     19from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
    2420
    2521NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
     
    2824NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event'
    2925NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner'
     26NS_PUBSUB_META_DATA = 'http://jabber.org/protocol/pubsub#meta-data'
    3027
    3128def calledAsync(fn):
     
    116113
    117114
    118     def test_event_delete(self):
     115    def test_eventDelete(self):
    119116        """
    120117        Test receiving a delete event resulting in a call to deleteReceived.
     
    124121        message['to'] = 'user@example.org/home'
    125122        event = message.addElement((NS_PUBSUB_EVENT, 'event'))
    126         items = event.addElement('delete')
    127         items['node'] = 'test'
     123        delete = event.addElement('delete')
     124        delete['node'] = 'test'
    128125
    129126        def deleteReceived(event):
     
    131128            self.assertEquals(JID('pubsub.example.org'), event.sender)
    132129            self.assertEquals('test', event.nodeIdentifier)
     130
     131        d, self.protocol.deleteReceived = calledAsync(deleteReceived)
     132        self.stub.send(message)
     133        return d
     134
     135
     136    def test_eventDeleteRedirect(self):
     137        """
     138        Test receiving a delete event with a redirect URI.
     139        """
     140        message = domish.Element((None, 'message'))
     141        message['from'] = 'pubsub.example.org'
     142        message['to'] = 'user@example.org/home'
     143        event = message.addElement((NS_PUBSUB_EVENT, 'event'))
     144        delete = event.addElement('delete')
     145        delete['node'] = 'test'
     146        uri = 'xmpp:pubsub.example.org?;node=test2'
     147        delete.addElement('redirect')['uri'] = uri
     148
     149        def deleteReceived(event):
     150            self.assertEquals(JID('user@example.org/home'), event.recipient)
     151            self.assertEquals(JID('pubsub.example.org'), event.sender)
     152            self.assertEquals('test', event.nodeIdentifier)
     153            self.assertEquals(uri, event.redirectURI)
    133154
    134155        d, self.protocol.deleteReceived = calledAsync(deleteReceived)
     
    235256
    236257
     258    def test_createNodeWithSender(self):
     259        """
     260        Test sending create request from a specific JID.
     261        """
     262
     263        d = self.protocol.createNode(JID('pubsub.example.org'), 'test',
     264                                     sender=JID('user@example.org'))
     265
     266        iq = self.stub.output[-1]
     267        self.assertEquals('user@example.org', iq['from'])
     268
     269        response = toResponse(iq, 'result')
     270        self.stub.send(response)
     271        return d
     272
     273
    237274    def test_deleteNode(self):
    238275        """
     
    246283        self.assertEquals('set', iq.getAttribute('type'))
    247284        self.assertEquals('pubsub', iq.pubsub.name)
    248         self.assertEquals(NS_PUBSUB, iq.pubsub.uri)
     285        self.assertEquals(NS_PUBSUB_OWNER, iq.pubsub.uri)
    249286        children = list(domish.generateElementsQNamed(iq.pubsub.children,
    250                                                       'delete', NS_PUBSUB))
     287                                                      'delete', NS_PUBSUB_OWNER))
    251288        self.assertEquals(1, len(children))
    252289        child = children[0]
    253290        self.assertEquals('test', child['node'])
     291
     292        response = toResponse(iq, 'result')
     293        self.stub.send(response)
     294        return d
     295
     296
     297    def test_deleteNodeWithSender(self):
     298        """
     299        Test sending delete request.
     300        """
     301
     302        d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test',
     303                                     sender=JID('user@example.org'))
     304
     305        iq = self.stub.output[-1]
     306        self.assertEquals('user@example.org', iq['from'])
    254307
    255308        response = toResponse(iq, 'result')
     
    309362
    310363
     364    def test_publishWithSender(self):
     365        """
     366        Test sending publish request from a specific JID.
     367        """
     368
     369        item = pubsub.Item()
     370        d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item],
     371                                  JID('user@example.org'))
     372
     373        iq = self.stub.output[-1]
     374        self.assertEquals('user@example.org', iq['from'])
     375
     376        response = toResponse(iq, 'result')
     377        self.stub.send(response)
     378        return d
     379
     380
    311381    def test_subscribe(self):
    312382        """
     
    378448
    379449
     450    def test_subscribeWithSender(self):
     451        """
     452        Test sending subscription request from a specific JID.
     453        """
     454        d = self.protocol.subscribe(JID('pubsub.example.org'), 'test',
     455                                      JID('user@example.org'),
     456                                      sender=JID('user@example.org'))
     457
     458        iq = self.stub.output[-1]
     459        self.assertEquals('user@example.org', iq['from'])
     460
     461        response = toResponse(iq, 'result')
     462        pubsub = response.addElement((NS_PUBSUB, 'pubsub'))
     463        subscription = pubsub.addElement('subscription')
     464        subscription['node'] = 'test'
     465        subscription['jid'] = 'user@example.org'
     466        subscription['subscription'] = 'subscribed'
     467        self.stub.send(response)
     468        return d
     469
     470
    380471    def test_unsubscribe(self):
    381472        """
     
    401492
    402493
     494    def test_unsubscribeWithSender(self):
     495        """
     496        Test sending unsubscription request from a specific JID.
     497        """
     498        d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test',
     499                                      JID('user@example.org'),
     500                                      sender=JID('user@example.org'))
     501
     502        iq = self.stub.output[-1]
     503        self.assertEquals('user@example.org', iq['from'])
     504        self.stub.send(toResponse(iq, 'result'))
     505        return d
     506
     507
    403508    def test_items(self):
    404509        """
     
    467572
    468573
    469 
    470 class PubSubServiceTest(unittest.TestCase):
    471     """
    472     Tests for L{pubsub.PubSubService}.
    473     """
    474 
    475     def setUp(self):
    476         self.service = pubsub.PubSubService()
    477 
    478     def handleRequest(self, xml):
    479         """
    480         Find a handler and call it directly
    481         """
    482         handler = None
    483         iq = parseXml(xml)
    484         for queryString, method in self.service.iqHandlers.iteritems():
    485             if xpath.internQuery(queryString).matches(iq):
    486                 handler = getattr(self.service, method)
    487 
    488         if handler:
    489             d = defer.maybeDeferred(handler, iq)
    490         else:
    491             d = defer.fail(NotImplementedError())
    492 
    493         return d
    494 
    495 
    496     def test_interface(self):
    497         """
    498         Do instances of L{pubsub.PubSubService} provide L{iwokkel.IPubSubService}?
    499         """
    500         verify.verifyObject(iwokkel.IPubSubService, self.service)
    501 
    502 
    503     def test_onPublishNoNode(self):
    504         """
    505         The root node is always a collection, publishing is a bad request.
     574    def test_itemsWithSender(self):
     575        """
     576        Test sending items request from a specific JID.
     577        """
     578
     579        d = self.protocol.items(JID('pubsub.example.org'), 'test',
     580                               sender=JID('user@example.org'))
     581
     582        iq = self.stub.output[-1]
     583        self.assertEquals('user@example.org', iq['from'])
     584
     585        response = toResponse(iq, 'result')
     586        items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items')
     587        items['node'] = 'test'
     588
     589        self.stub.send(response)
     590        return d
     591
     592
     593
     594class PubSubRequestTest(unittest.TestCase):
     595
     596    def test_fromElementPublish(self):
     597        """
     598        Test parsing a publish request.
     599        """
     600
     601        xml = """
     602        <iq type='set' to='pubsub.example.org'
     603                       from='user@example.org'>
     604          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     605            <publish node='test'/>
     606          </pubsub>
     607        </iq>
     608        """
     609
     610        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     611        self.assertEqual('publish', request.verb)
     612        self.assertEqual(JID('user@example.org'), request.sender)
     613        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     614        self.assertEqual('test', request.nodeIdentifier)
     615        self.assertEqual([], request.items)
     616
     617
     618    def test_fromElementPublishItems(self):
     619        """
     620        Test parsing a publish request with items.
     621        """
     622
     623        xml = """
     624        <iq type='set' to='pubsub.example.org'
     625                       from='user@example.org'>
     626          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     627            <publish node='test'>
     628              <item id="item1"/>
     629              <item id="item2"/>
     630            </publish>
     631          </pubsub>
     632        </iq>
     633        """
     634
     635        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     636        self.assertEqual(2, len(request.items))
     637        self.assertEqual(u'item1', request.items[0]["id"])
     638        self.assertEqual(u'item2', request.items[1]["id"])
     639
     640
     641    def test_fromElementPublishNoNode(self):
     642        """
     643        A publish request to the root node should raise an exception.
    506644        """
    507645        xml = """
     
    514652        """
    515653
    516         def cb(result):
    517             self.assertEquals('bad-request', result.condition)
    518 
    519         d = self.handleRequest(xml)
    520         self.assertFailure(d, error.StanzaError)
    521         d.addCallback(cb)
    522         return d
    523 
    524 
    525     def test_onPublish(self):
    526         """
    527         A publish request should result in L{PubSubService.publish} being
    528         called.
    529         """
    530 
    531         xml = """
    532         <iq type='set' to='pubsub.example.org'
    533                        from='user@example.org'>
    534           <pubsub xmlns='http://jabber.org/protocol/pubsub'>
    535             <publish node='test'/>
    536           </pubsub>
    537         </iq>
    538         """
    539 
    540         def publish(requestor, service, nodeIdentifier, items):
    541             self.assertEqual(JID('user@example.org'), requestor)
    542             self.assertEqual(JID('pubsub.example.org'), service)
    543             self.assertEqual('test', nodeIdentifier)
    544             self.assertEqual([], items)
    545             return defer.succeed(None)
    546 
    547         self.service.publish = publish
    548         return self.handleRequest(xml)
    549 
    550 
    551     def test_onOptionsGet(self):
    552         """
    553         Subscription options are not supported.
     654        err = self.assertRaises(error.StanzaError,
     655                                pubsub.PubSubRequest.fromElement,
     656                                parseXml(xml))
     657        self.assertEqual('bad-request', err.condition)
     658        self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
     659        self.assertEqual('nodeid-required', err.appCondition.name)
     660
     661
     662    def test_fromElementSubscribe(self):
     663        """
     664        Test parsing a subscription request.
     665        """
     666
     667        xml = """
     668        <iq type='set' to='pubsub.example.org'
     669                       from='user@example.org'>
     670          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     671            <subscribe node='test' jid='user@example.org/Home'/>
     672          </pubsub>
     673        </iq>
     674        """
     675
     676        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     677        self.assertEqual('subscribe', request.verb)
     678        self.assertEqual(JID('user@example.org'), request.sender)
     679        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     680        self.assertEqual('test', request.nodeIdentifier)
     681        self.assertEqual(JID('user@example.org/Home'), request.subscriber)
     682
     683
     684    def test_fromElementSubscribeEmptyNode(self):
     685        """
     686        Test parsing a subscription request to the root node.
     687        """
     688
     689        xml = """
     690        <iq type='set' to='pubsub.example.org'
     691                       from='user@example.org'>
     692          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     693            <subscribe jid='user@example.org/Home'/>
     694          </pubsub>
     695        </iq>
     696        """
     697
     698        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     699        self.assertEqual('', request.nodeIdentifier)
     700
     701
     702    def test_fromElementSubscribeNoJID(self):
     703        """
     704        Subscribe requests without a JID should raise a bad-request exception.
     705        """
     706        xml = """
     707        <iq type='set' to='pubsub.example.org'
     708                       from='user@example.org'>
     709          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     710            <subscribe node='test'/>
     711          </pubsub>
     712        </iq>
     713        """
     714        err = self.assertRaises(error.StanzaError,
     715                                pubsub.PubSubRequest.fromElement,
     716                                parseXml(xml))
     717        self.assertEqual('bad-request', err.condition)
     718        self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
     719        self.assertEqual('jid-required', err.appCondition.name)
     720
     721    def test_fromElementUnsubscribe(self):
     722        """
     723        Test parsing an unsubscription request.
     724        """
     725
     726        xml = """
     727        <iq type='set' to='pubsub.example.org'
     728                       from='user@example.org'>
     729          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     730            <unsubscribe node='test' jid='user@example.org/Home'/>
     731          </pubsub>
     732        </iq>
     733        """
     734
     735        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     736        self.assertEqual('unsubscribe', request.verb)
     737        self.assertEqual(JID('user@example.org'), request.sender)
     738        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     739        self.assertEqual('test', request.nodeIdentifier)
     740        self.assertEqual(JID('user@example.org/Home'), request.subscriber)
     741
     742
     743    def test_fromElementUnsubscribeNoJID(self):
     744        """
     745        Unsubscribe requests without a JID should raise a bad-request exception.
     746        """
     747        xml = """
     748        <iq type='set' to='pubsub.example.org'
     749                       from='user@example.org'>
     750          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     751            <unsubscribe node='test'/>
     752          </pubsub>
     753        </iq>
     754        """
     755        err = self.assertRaises(error.StanzaError,
     756                                pubsub.PubSubRequest.fromElement,
     757                                parseXml(xml))
     758        self.assertEqual('bad-request', err.condition)
     759        self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
     760        self.assertEqual('jid-required', err.appCondition.name)
     761
     762
     763    def test_fromElementOptionsGet(self):
     764        """
     765        Test parsing a request for getting subscription options.
    554766        """
    555767
     
    558770                       from='user@example.org'>
    559771          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
    560             <options/>
    561           </pubsub>
    562         </iq>
    563         """
    564 
    565         def cb(result):
    566             self.assertEquals('feature-not-implemented', result.condition)
    567             self.assertEquals('unsupported', result.appCondition.name)
    568             self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
    569 
    570         d = self.handleRequest(xml)
    571         self.assertFailure(d, error.StanzaError)
    572         d.addCallback(cb)
    573         return d
    574 
    575 
    576     def test_onDefault(self):
    577         """
    578         A default request should result in
    579         L{PubSubService.getDefaultConfiguration} being called.
     772            <options node='test' jid='user@example.org/Home'/>
     773          </pubsub>
     774        </iq>
     775        """
     776
     777        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     778        self.assertEqual('optionsGet', request.verb)
     779
     780
     781    def test_fromElementOptionsSet(self):
     782        """
     783        Test parsing a request for setting subscription options.
     784        """
     785
     786        xml = """
     787        <iq type='set' to='pubsub.example.org'
     788                       from='user@example.org'>
     789          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     790            <options node='test' jid='user@example.org/Home'>
     791              <x xmlns='jabber:x:data' type='submit'>
     792                <field var='FORM_TYPE' type='hidden'>
     793                  <value>http://jabber.org/protocol/pubsub#subscribe_options</value>
     794                </field>
     795                <field var='pubsub#deliver'><value>1</value></field>
     796              </x>
     797            </options>
     798          </pubsub>
     799        </iq>
     800        """
     801
     802        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     803        self.assertEqual('optionsSet', request.verb)
     804        self.assertEqual(JID('user@example.org'), request.sender)
     805        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     806        self.assertEqual('test', request.nodeIdentifier)
     807        self.assertEqual(JID('user@example.org/Home'), request.subscriber)
     808        self.assertEqual({'pubsub#deliver': '1'}, request.options)
     809
     810
     811    def test_fromElementOptionsSetCancel(self):
     812        """
     813        Test parsing a request for cancelling setting subscription options.
     814        """
     815
     816        xml = """
     817        <iq type='set' to='pubsub.example.org'
     818                       from='user@example.org'>
     819          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     820            <options node='test' jid='user@example.org/Home'>
     821              <x xmlns='jabber:x:data' type='cancel'/>
     822            </options>
     823          </pubsub>
     824        </iq>
     825        """
     826
     827        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     828        self.assertEqual({}, request.options)
     829
     830
     831    def test_fromElementOptionsSetBadFormType(self):
     832        """
     833        On a options set request unknown fields should be ignored.
     834        """
     835
     836        xml = """
     837        <iq type='set' to='pubsub.example.org'
     838                       from='user@example.org'>
     839          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     840            <options node='test' jid='user@example.org/Home'>
     841              <x xmlns='jabber:x:data' type='result'>
     842                <field var='FORM_TYPE' type='hidden'>
     843                  <value>http://jabber.org/protocol/pubsub#node_config</value>
     844                </field>
     845                <field var='pubsub#deliver'><value>1</value></field>
     846              </x>
     847            </options>
     848          </pubsub>
     849        </iq>
     850        """
     851
     852        err = self.assertRaises(error.StanzaError,
     853                                pubsub.PubSubRequest.fromElement,
     854                                parseXml(xml))
     855        self.assertEqual('bad-request', err.condition)
     856        self.assertEqual(None, err.appCondition)
     857
     858
     859    def test_fromElementOptionsSetNoForm(self):
     860        """
     861        On a options set request a form is required.
     862        """
     863
     864        xml = """
     865        <iq type='set' to='pubsub.example.org'
     866                       from='user@example.org'>
     867          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     868            <options node='test' jid='user@example.org/Home'/>
     869          </pubsub>
     870        </iq>
     871        """
     872        err = self.assertRaises(error.StanzaError,
     873                                pubsub.PubSubRequest.fromElement,
     874                                parseXml(xml))
     875        self.assertEqual('bad-request', err.condition)
     876        self.assertEqual(None, err.appCondition)
     877
     878
     879    def test_fromElementSubscriptions(self):
     880        """
     881        Test parsing a request for all subscriptions.
     882        """
     883
     884        xml = """
     885        <iq type='get' to='pubsub.example.org'
     886                       from='user@example.org'>
     887          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     888            <subscriptions/>
     889          </pubsub>
     890        </iq>
     891        """
     892
     893        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     894        self.assertEqual('subscriptions', request.verb)
     895        self.assertEqual(JID('user@example.org'), request.sender)
     896        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     897
     898
     899    def test_fromElementAffiliations(self):
     900        """
     901        Test parsing a request for all affiliations.
     902        """
     903
     904        xml = """
     905        <iq type='get' to='pubsub.example.org'
     906                       from='user@example.org'>
     907          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     908            <affiliations/>
     909          </pubsub>
     910        </iq>
     911        """
     912
     913        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     914        self.assertEqual('affiliations', request.verb)
     915        self.assertEqual(JID('user@example.org'), request.sender)
     916        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     917
     918
     919    def test_fromElementCreate(self):
     920        """
     921        Test parsing a request to create a node.
     922        """
     923
     924        xml = """
     925        <iq type='set' to='pubsub.example.org'
     926                       from='user@example.org'>
     927          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     928            <create node='mynode'/>
     929          </pubsub>
     930        </iq>
     931        """
     932
     933        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     934        self.assertEqual('create', request.verb)
     935        self.assertEqual(JID('user@example.org'), request.sender)
     936        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     937        self.assertEqual('mynode', request.nodeIdentifier)
     938
     939
     940    def test_fromElementCreateInstant(self):
     941        """
     942        Test parsing a request to create an instant node.
     943        """
     944
     945        xml = """
     946        <iq type='set' to='pubsub.example.org'
     947                       from='user@example.org'>
     948          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     949            <create/>
     950          </pubsub>
     951        </iq>
     952        """
     953
     954        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     955        self.assertIdentical(None, request.nodeIdentifier)
     956
     957
     958    def test_fromElementDefault(self):
     959        """
     960        Test parsing a request for the default node configuration.
    580961        """
    581962
     
    589970        """
    590971
    591         def getConfigurationOptions():
    592             return {
    593                 "pubsub#persist_items":
    594                     {"type": "boolean",
    595                      "label": "Persist items to storage"},
    596                 "pubsub#deliver_payloads":
    597                     {"type": "boolean",
    598                      "label": "Deliver payloads with event notifications"}
    599                 }
    600 
    601         def getDefaultConfiguration(requestor, service, nodeType):
    602             self.assertEqual(JID('user@example.org'), requestor)
    603             self.assertEqual(JID('pubsub.example.org'), service)
    604             self.assertEqual('leaf', nodeType)
    605             return defer.succeed({})
    606 
    607         def cb(element):
    608             self.assertEqual('pubsub', element.name)
    609             self.assertEqual(NS_PUBSUB_OWNER, element.uri)
    610             self.assertEqual(NS_PUBSUB_OWNER, element.default.uri)
    611             form = data_form.Form.fromElement(element.default.x)
    612             self.assertEqual(NS_PUBSUB_CONFIG, form.formNamespace)
    613 
    614         self.service.getConfigurationOptions = getConfigurationOptions
    615         self.service.getDefaultConfiguration = getDefaultConfiguration
    616         d = self.handleRequest(xml)
    617         d.addCallback(cb)
    618         return d
    619 
    620 
    621     def test_onConfigureGet(self):
    622         """
    623         On a node configuration get request L{PubSubService.getConfiguration}
    624         is called and results in a data form with the configuration.
     972        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     973        self.assertEqual('default', request.verb)
     974        self.assertEqual(JID('user@example.org'), request.sender)
     975        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     976        self.assertEqual('leaf', request.nodeType)
     977
     978
     979    def test_fromElementDefaultCollection(self):
     980        """
     981        Parsing a request for the default configuration extracts the node type.
     982        """
     983
     984        xml = """
     985        <iq type='get' to='pubsub.example.org'
     986                       from='user@example.org'>
     987          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     988            <default>
     989              <x xmlns='jabber:x:data' type='submit'>
     990                <field var='FORM_TYPE' type='hidden'>
     991                  <value>http://jabber.org/protocol/pubsub#node_config</value>
     992                </field>
     993                <field var='pubsub#node_type'>
     994                  <value>collection</value>
     995                </field>
     996              </x>
     997            </default>
     998
     999          </pubsub>
     1000        </iq>
     1001        """
     1002
     1003        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     1004        self.assertEqual('collection', request.nodeType)
     1005
     1006
     1007    def test_fromElementConfigureGet(self):
     1008        """
     1009        Test parsing a node configuration get request.
    6251010        """
    6261011
     
    6341019        """
    6351020
    636         def getConfigurationOptions():
    637             return {
    638                 "pubsub#persist_items":
    639                     {"type": "boolean",
    640                      "label": "Persist items to storage"},
    641                 "pubsub#deliver_payloads":
    642                     {"type": "boolean",
    643                      "label": "Deliver payloads with event notifications"}
    644                 }
    645 
    646         def getConfiguration(requestor, service, nodeIdentifier):
    647             self.assertEqual(JID('user@example.org'), requestor)
    648             self.assertEqual(JID('pubsub.example.org'), service)
    649             self.assertEqual('test', nodeIdentifier)
    650 
    651             return defer.succeed({'pubsub#deliver_payloads': '0',
    652                                   'pubsub#persist_items': '1'})
    653 
    654         def cb(element):
    655             self.assertEqual('pubsub', element.name)
    656             self.assertEqual(NS_PUBSUB_OWNER, element.uri)
    657             self.assertEqual(NS_PUBSUB_OWNER, element.configure.uri)
    658             form = data_form.Form.fromElement(element.configure.x)
    659             self.assertEqual(NS_PUBSUB_CONFIG, form.formNamespace)
    660             fields = form.fields
    661 
    662             self.assertIn('pubsub#deliver_payloads', fields)
    663             field = fields['pubsub#deliver_payloads']
    664             self.assertEqual('boolean', field.fieldType)
    665             self.assertEqual(False, field.value)
    666 
    667             self.assertIn('pubsub#persist_items', fields)
    668             field = fields['pubsub#persist_items']
    669             self.assertEqual('boolean', field.fieldType)
    670             self.assertEqual(True, field.value)
    671 
    672         self.service.getConfigurationOptions = getConfigurationOptions
    673         self.service.getConfiguration = getConfiguration
    674         d = self.handleRequest(xml)
    675         d.addCallback(cb)
    676         return d
    677 
    678 
    679     def test_onConfigureSet(self):
    680         """
    681         On a node configuration set request the Data Form is parsed and
    682         L{PubSubService.setConfiguration} is called with the passed options.
     1021        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     1022        self.assertEqual('configureGet', request.verb)
     1023        self.assertEqual(JID('user@example.org'), request.sender)
     1024        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     1025        self.assertEqual('test', request.nodeIdentifier)
     1026
     1027
     1028    def test_fromElementConfigureSet(self):
     1029        """
     1030        On a node configuration set request the Data Form is parsed.
    6831031        """
    6841032
     
    7001048        """
    7011049
     1050        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     1051        self.assertEqual('configureSet', request.verb)
     1052        self.assertEqual(JID('user@example.org'), request.sender)
     1053        self.assertEqual(JID('pubsub.example.org'), request.recipient)
     1054        self.assertEqual('test', request.nodeIdentifier)
     1055        self.assertEqual({'pubsub#deliver_payloads': '0',
     1056                          'pubsub#persist_items': '1'}, request.options)
     1057
     1058
     1059    def test_fromElementConfigureSetCancel(self):
     1060        """
     1061        The node configuration is cancelled, so no options.
     1062        """
     1063
     1064        xml = """
     1065        <iq type='set' to='pubsub.example.org'
     1066                       from='user@example.org'>
     1067          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     1068            <configure node='test'>
     1069              <x xmlns='jabber:x:data' type='cancel'/>
     1070            </configure>
     1071          </pubsub>
     1072        </iq>
     1073        """
     1074
     1075        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     1076        self.assertEqual({}, request.options)
     1077
     1078
     1079    def test_fromElementConfigureSetBadFormType(self):
     1080        """
     1081