# encoding: UTF-8 module Vines class Stanza include Nokogiri::XML attr_reader :stream EMPTY = ''.freeze FROM, MESSAGE, TO = %w[from message to].map {|s| s.freeze } ROUTABLE_STANZAS = %w[message iq presence].freeze @@types = {} def self.register(xpath, ns={}) @@types[[xpath, ns]] = self end def self.from_node(node, stream) # optimize common case return Message.new(node, stream) if node.name == MESSAGE found = @@types.select {|pair, v| node.xpath(*pair).any? } .sort {|a, b| b[0][0].length - a[0][0].length }.first found ? found[1].new(node, stream) : nil end def initialize(node, stream) @node, @stream = node, stream end # Send the stanza to all recipients, stamping it with from and # to addresses first. def broadcast(recipients) @node[FROM] = stream.user.jid.to_s recipients.each do |recipient| @node[TO] = recipient.user.jid.to_s recipient.write(@node) end end # Returns true if this stanza should be processed locally. Returns false # if it's destined for a remote domain or external component. def local? return true unless ROUTABLE_STANZAS.include?(@node.name) to = JID.new(@node[TO]) to.empty? || local_jid?(to) end def local_jid?(*jids) stream.config.local_jid?(*jids) end # Return true if this stanza is addressed to a pubsub subdomain hosted # at this server. This helps differentiate between IQ stanzas addressed # to the server and stanzas addressed to pubsub domains, both of which must # be handled locally and not routed. def to_pubsub_domain? stream.config.pubsub?(validate_to) end def route stream.router.route(@node) end def router stream.router end def storage(domain=stream.domain) stream.storage(domain) end def process raise 'subclass must implement' end # Broadcast unavailable presence from the user's available resources to the # recipient's available resources. Route the stanza to a remote server if # the recipient isn't hosted locally. def send_unavailable(from, to) available = router.available_resources(from, to) stanzas = available.map {|stream| unavailable(stream.user.jid) } broadcast_to_available_resources(stanzas, to) end # Return an unavailable presence stanza addressed from the given JID. def unavailable(from) doc = Document.new doc.create_element('presence', 'from' => from.to_s, 'id' => Kit.uuid, 'type' => 'unavailable') end # Return nil if this stanza has no 'to' attribute. Return a Vines::JID # if it contains a valid 'to' attribute. Raise a JidMalformed error if # the JID is invalid. def validate_to validate_address(TO) end # Return nil if this stanza has no 'from' attribute. Return a Vines::JID # if it contains a valid 'from' attribute. Raise a JidMalformed error if # the JID is invalid. def validate_from validate_address(FROM) end def method_missing(method, *args, &block) @node.send(method, *args, &block) end private # Send the stanzas to the destination JID, routing to a s2s stream # if the address is remote. This method properly stamps the to address # on each stanza before it's sent. The caller must set the from address. def broadcast_to_available_resources(stanzas, to) return if send_to_remote(stanzas, to) send_to_recipients(stanzas, stream.available_resources(to)) end # Send the stanzas to the destination JID, routing to a s2s stream # if the address is remote. This method properly stamps the to address # on each stanza before it's sent. The caller must set the from address. def broadcast_to_interested_resources(stanzas, to) return if send_to_remote(stanzas, to) send_to_recipients(stanzas, stream.interested_resources(to)) end # Route the stanzas to a remote server, stamping a bare JID as the # to address. Bare JIDs are required for presence subscription stanzas # sent to the remote contact's server. Return true if the stanzas were # routed, false if they must be delivered locally. def send_to_remote(stanzas, to) return false if local_jid?(to) to = JID.new(to) stanzas.each do |el| el[TO] = to.bare.to_s router.route(el) end true end # Send the stanzas to the local recipient streams, stamping a full JID as # the to address. It's important to use full JIDs, even when sending to # local clients, because the stanzas may be routed to other cluster nodes # for delivery. We need the receiving cluster node to send the stanza just # to this full JID, not to lookup all JIDs for this user. def send_to_recipients(stanzas, recipients) recipients.each do |recipient| stanzas.each do |el| el[TO] = recipient.user.jid.to_s recipient.write(el) end end end # Return true if the to and from JIDs are allowed to communicate with one # another based on the cross_domain_messages setting in conf/config.rb. If # a domain's users are isolated to sending messages only within their own # domain, pubsub stanzas must not be processed from remote JIDs. def allowed? stream.config.allowed?(validate_to || stream.domain, stream.user.jid) end def validate_address(attr) jid = (self[attr] || EMPTY) return if jid.empty? JID.new(jid) rescue raise StanzaErrors::JidMalformed.new(self, 'modify') end end end