From d232fcc41523a0e9c18b340583e03354620b7af5 Mon Sep 17 00:00:00 2001 From: Taras Melnychuk Date: Wed, 14 Mar 2012 19:03:07 +0200 Subject: [PATCH 1/3] added login as email support --- jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js | 15 ++++-- jarn/xmpp/core/browser/pubsub.py | 25 ++++++--- jarn/xmpp/core/browser/userinfo.py | 7 ++- jarn/xmpp/core/configure.zcml | 1 + jarn/xmpp/core/interfaces.py | 14 +++++ jarn/xmpp/core/settings.py | 2 + jarn/xmpp/core/subscribers/user_management.py | 2 +- jarn/xmpp/core/tests/test_node_escaping.py | 36 +++++++++++++ jarn/xmpp/core/utils/configure.zcml | 8 +++ jarn/xmpp/core/utils/node.py | 51 +++++++++++++++++++ 10 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 jarn/xmpp/core/tests/test_node_escaping.py create mode 100644 jarn/xmpp/core/utils/configure.zcml create mode 100644 jarn/xmpp/core/utils/node.py diff --git a/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js b/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js index d159006..2eba526 100644 --- a/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js +++ b/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js @@ -82,6 +82,11 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google }; })(); + escapeSelector = function(selector) { + return selector.replace(/\\/g, "\\\\") + .replace(/[@#;&,.+*~':"!^$[\]()=>|\/]/g, "\\$&"); + }; + jarnxmpp.UI = { _: null, @@ -186,7 +191,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google $(document).bind('jarnxmpp.presence', function (event, jid, status, presence) { var user_id = Strophe.getNodeFromJid(jid), barejid = Strophe.getBareJidFromJid(jid), - existing_user_element = $('#online-users-' + user_id), + existing_user_element = $('#online-users-' + escapeSelector(Strophe.unescapeNode(user_id))), online_count; if (barejid === Strophe.getBareJidFromJid(jarnxmpp.connection.jid)) { return; @@ -240,7 +245,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google $(document).bind('jarnxmpp.message', function (event) { var user_id = Strophe.getNodeFromJid(event.from), $text_p = $('

').html(event.body), - $form = $('#online-users li#online-users-' + user_id + ' .replyForm').clone(), + $form = $('#online-users li#online-users-' + escapeSelector(Strophe.unescapeNode(user_id)) + ' .replyForm').clone(), $reply_p = $('

').append($form), text = $('

').append($text_p).append($reply_p).remove().html(); $('input[type="submit"]', $form).attr('value', 'Reply'); @@ -351,7 +356,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google // Follow/unfollow user. $('a.followingStatus').live('click', function (e) { var $following_link = $(this), - node_id = $following_link.attr('data-user'), + node_id = Strophe.escapeNode($following_link.attr('data-user')), fullname = $following_link.attr('data-fullname'); jarnxmpp.PubSub.getSubscriptions(function (following) { if (following.indexOf(node_id) > -1) { @@ -588,7 +593,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google .attr('value', node))); jarnxmpp.Presence.getUserInfo(node, function (info) { if (info) { - $('input[value=' + node + ']', $sl).after(info.fullname); + $('input[value=' + escapeSelector(node) + ']').after(info.fullname); } else { $('input[value=' + node + ']', $sl).parent().remove(); } @@ -606,7 +611,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google $('#follow-selected').attr('checked', 'checked'); } $.each(subscribed_nodes, function (idx, node) { - $('input[value=' + node + ']', $sl) + $('input[value=' + escapeSelector(node) + ']', $sl) .attr('checked', 'checked') .parent().addClass('subscribed'); }); diff --git a/jarn/xmpp/core/browser/pubsub.py b/jarn/xmpp/core/browser/pubsub.py index 0141de9..13fbd14 100644 --- a/jarn/xmpp/core/browser/pubsub.py +++ b/jarn/xmpp/core/browser/pubsub.py @@ -8,6 +8,7 @@ from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from jarn.xmpp.core.interfaces import IPubSubStorage +from jarn.xmpp.core.interfaces import INodeEscaper class PubSubItem(BrowserView): @@ -19,8 +20,10 @@ def __init__(self, context, request): self.mt = getToolByName(self.context, 'portal_membership') self.host = urlparse(getToolByName(self.context, 'portal_url')()).netloc self.storage = getUtility(IPubSubStorage) + self.escaper = getUtility(INodeEscaper) def fullname(self, author): + author = self.escaper.unescape(author) member = self.mt.getMemberById(author) return member.getProperty('fullname', None) @@ -48,6 +51,8 @@ def __call__(self, item=None, isLeaf=False): 'longitude': self.request.get('geolocation[longitude]')} if self.request.get('isLeaf') == 'false': isLeaf = False + if item['author']: + item['author'] = self.escaper.unescape(item['author']) self.item = item self.isLeaf = isLeaf @@ -80,13 +85,18 @@ class PubSubFeedMixIn(object): def __init__(self, context): self.storage = getUtility(IPubSubStorage) - if self.node in self.storage.leaf_nodes: + self.escaper = getUtility(INodeEscaper) + self.escape = self.escaper.escape + self.unescape = self.escaper.unescape + + if self.escape(self.node) in self.storage.leaf_nodes: self.nodeType = 'leaf' else: self.nodeType = 'collection' self.mt = getToolByName(self.context, 'portal_membership') def fullname(self, author): + author = self.unescape(author) member = self.mt.getMemberById(author) if member: return member.getProperty('fullname', None) @@ -102,22 +112,25 @@ def postNode(self): if self.mt.isAnonymousUser(): return if self.node is None: - return self.mt.getAuthenticatedMember().id + return self.escape(self.mt.getAuthenticatedMember().id) user_id = self.mt.getAuthenticatedMember().id if self.nodeType == 'leaf': - if user_id in self.storage.publishers[self.node]: + if self.escape(user_id) in \ + self.storage.publishers[self.escape(self.node)]: return self.node else: publisher_nodes = [node - for node in self.storage.collections[self.node] - if user_id in self.storage.publishers[node]] + for node in self.storage.collections[self.escape(self.node)] + if user_id in self.storage.publishers[node]] if len(publisher_nodes) == 1: return publisher_nodes[0] def items(self, node=None, start=0, count=20): if node is None: node = self.node - return self.storage.itemsFromNodes([node], start=start, count=count) + return self.storage.itemsFromNodes([self.escape(node)], + start=start, + count=count) class PubSubFeed(BrowserView, PubSubFeedMixIn): diff --git a/jarn/xmpp/core/browser/userinfo.py b/jarn/xmpp/core/browser/userinfo.py index 4635daf..f8f4d16 100644 --- a/jarn/xmpp/core/browser/userinfo.py +++ b/jarn/xmpp/core/browser/userinfo.py @@ -1,15 +1,20 @@ import json +from zope.component import getUtility + from twisted.words.protocols.jabber.jid import JID from AccessControl import Unauthorized from Products.Five.browser import BrowserView from Products.CMFCore.utils import getToolByName +from jarn.xmpp.core.interfaces import INodeEscaper + class XMPPUserInfo(BrowserView): def __call__(self, user_id): + user_id = getUtility(INodeEscaper).unescape(user_id) pm = getToolByName(self.context, 'portal_membership') if pm.isAnonymousUser(): raise Unauthorized @@ -31,7 +36,7 @@ class XMPPUserDetails(BrowserView): def __init__(self, context, request): super(BrowserView, self).__init__(context, request) self.jid = request.get('jid') - self.user_id = JID(self.jid).user + self.user_id = getUtility(INodeEscaper).unescape(JID(self.jid).user) self.bare_jid = JID(self.jid).userhost() self.pm = getToolByName(context, 'portal_membership') info = self.pm.getMemberInfo(self.user_id) diff --git a/jarn/xmpp/core/configure.zcml b/jarn/xmpp/core/configure.zcml index b254eef..431fc2a 100644 --- a/jarn/xmpp/core/configure.zcml +++ b/jarn/xmpp/core/configure.zcml @@ -14,6 +14,7 @@ + diff --git a/jarn/xmpp/core/interfaces.py b/jarn/xmpp/core/interfaces.py index 2ed3d25..f66de41 100644 --- a/jarn/xmpp/core/interfaces.py +++ b/jarn/xmpp/core/interfaces.py @@ -55,3 +55,17 @@ def __init__(self, obj): class IXMPPLoaderVM(IViewletManager): """Viewlet manager for the loader viewlet. """ + +class INodeEscaper(Interface): + """ Utility that provides basic escape mechanism for node (XEP-0106).""" + + def escape(self, node): + """Replaces all disallowed characters according to the algorithm + described in XEP-0106. + """ + + def unescape(self, node): + """Replaces all disallowed characters that were escaped + with unescaped ones. + """ + diff --git a/jarn/xmpp/core/settings.py b/jarn/xmpp/core/settings.py index 142a1cf..055f70f 100644 --- a/jarn/xmpp/core/settings.py +++ b/jarn/xmpp/core/settings.py @@ -7,6 +7,7 @@ from jarn.xmpp.core.interfaces import IXMPPPasswordStorage from jarn.xmpp.core.interfaces import IXMPPUsers +from jarn.xmpp.core.interfaces import INodeEscaper logger = logging.getLogger('jarn.xmpp.core') @@ -18,6 +19,7 @@ class XMPPUsers(object): def getUserJID(self, user_id): registry = getUtility(IRegistry) xmpp_domain = registry['jarn.xmpp.xmppDomain'] + user_id = getUtility(INodeEscaper).escape(user_id) return JID("%s@%s" % (user_id, xmpp_domain)) def getUserPassword(self, user_id): diff --git a/jarn/xmpp/core/subscribers/user_management.py b/jarn/xmpp/core/subscribers/user_management.py index 7d9fc21..dee538b 100644 --- a/jarn/xmpp/core/subscribers/user_management.py +++ b/jarn/xmpp/core/subscribers/user_management.py @@ -37,7 +37,7 @@ def onUserCreation(event): principal_pass = pass_storage.set(principal_id) storage.leaf_nodes.append(principal_id) - storage.node_items[principal_id] = [] + storage.node_items[principal_jid.user] = [] storage.collections['people'].append(principal_id) storage.publishers[principal_id] = [principal_id] diff --git a/jarn/xmpp/core/tests/test_node_escaping.py b/jarn/xmpp/core/tests/test_node_escaping.py new file mode 100644 index 0000000..bec512d --- /dev/null +++ b/jarn/xmpp/core/tests/test_node_escaping.py @@ -0,0 +1,36 @@ +import unittest +from jarn.xmpp.core.utils.node import NodeEscaper + +class NodeEscaperTests(unittest.TestCase): + + jids = [('space cadet@example.com', 'space\\20cadet@example.com'), + ('call me "ishmael"@example.com', + 'call\\20me\\20\\22ishmael\\22@example.com'), + ('at&t guy@example.com', 'at\\26t\\20guy@example.com'), + ('d\'artagnan@example.com', 'd\\27artagnan@example.com'), + ('/.fanboy@example.com', '\\2f.fanboy@example.com'), + ('::foo::@example.com', '\\3a\\3afoo\\3a\\3a@example.com'), + ('@example.com', '\\3cfoo\\3e@example.com'), + ('user@host@example.com', 'user\\40host@example.com'), + ('c:\\net@example.com', 'c\\3a\\net@example.com'), + ('c:\\net@example.com', 'c\\3a\\net@example.com'), + ('c:\\cool stuff@example.com', 'c\\3a\\cool\\20stuff@example.com'), + ('c:\\5commas@example.com', 'c\\3a\\5c5commas@example.com'), + ('example@example.com', 'example@example.com')] + + def setUp(self): + self.escaper = NodeEscaper() + + def test_jid_escaping(self): + for jid in self.jids: + node, host = tuple(jid[0].rsplit('@', 1)) + self.assertEqual('%s@%s' % (self.escaper.escape(node), host), jid[1]) + + def test_jid_unescaping(self): + for jid in self.jids: + node, host = tuple(jid[1].rsplit('@', 1)) + self.assertEqual('%s@%s' % (self.escaper.unescape(node), host), jid[0]) + + +def test_suite(): + return unittest.defaultTestLoader.loadTestsFromName(__name__) diff --git a/jarn/xmpp/core/utils/configure.zcml b/jarn/xmpp/core/utils/configure.zcml new file mode 100644 index 0000000..61b5df6 --- /dev/null +++ b/jarn/xmpp/core/utils/configure.zcml @@ -0,0 +1,8 @@ + + + + + diff --git a/jarn/xmpp/core/utils/node.py b/jarn/xmpp/core/utils/node.py new file mode 100644 index 0000000..9b9ee8d --- /dev/null +++ b/jarn/xmpp/core/utils/node.py @@ -0,0 +1,51 @@ +from zope.interface import implements + +from jarn.xmpp.core.interfaces import INodeEscaper + + +class NodeEscaper(object): + """Implements basic XEP106 escape mechanism.""" + + implements(INodeEscaper) + + XEP0106_mapping = [(' ','20'), + ('"','22'), + ('&','26'), + ('\'','27'), + ('/','2f'), + (':','3a'), + ('<','3c'), + ('>','3e'), + ('@','40')] + + + def escape(self, node): + """Replaces all characters disallowed by the Nodeprep profile of + stringprep using escape mapping. + """ + if not node: + return + + node = node.replace('\\5c', '\\5c5c') + + for char, repl in self.XEP0106_mapping: + node = node.replace('\\%s' % repl, '\\5c%s' % repl) + + for char, repl in self.XEP0106_mapping: + node = node.replace(char, '\\%s' % repl) + + return node + + def unescape(self, node): + """Replaces all disallowed characters that were escaped + with unescaped ones. + """ + + if not node: + return + + for char, repl in self.XEP0106_mapping: + node = node.replace('\\%s' % repl, char) + + return node.replace('\\5c', '\\') + From 82f3c11382c8102b9feb1203439d87feffc7280b Mon Sep 17 00:00:00 2001 From: Taras Melnychuk Date: Wed, 14 Mar 2012 22:37:29 +0200 Subject: [PATCH 2/3] fixed presense viewlet list --- jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js b/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js index 2eba526..0ff90fa 100644 --- a/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js +++ b/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js @@ -191,7 +191,8 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google $(document).bind('jarnxmpp.presence', function (event, jid, status, presence) { var user_id = Strophe.getNodeFromJid(jid), barejid = Strophe.getBareJidFromJid(jid), - existing_user_element = $('#online-users-' + escapeSelector(Strophe.unescapeNode(user_id))), + user_selector = escapeSelector(Strophe.unescapeNode(user_id)), + existing_user_element = $('#online-users-' + user_selector), online_count; if (barejid === Strophe.getBareJidFromJid(jarnxmpp.connection.jid)) { return; @@ -203,7 +204,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google existing_user_element.attr('class', status); } else { $.get(portal_url + '/xmpp-userDetails?jid=' + barejid, function (user_details) { - if ($('#online-users-' + user_id).length > 0) { + if ($('#online-users-' + user_selector).length > 0) { return; } user_details = $(user_details); From 4f558575e8824339a8ca6970112ef75c4f86ae58 Mon Sep 17 00:00:00 2001 From: Taras Melnychuk Date: Wed, 14 Mar 2012 23:32:51 +0200 Subject: [PATCH 3/3] fixed leaf_nodes, fixed commenting on thread with following turn on --- jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js | 2 +- jarn/xmpp/core/subscribers/user_management.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js b/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js index 0ff90fa..67befe8 100644 --- a/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js +++ b/jarn/xmpp/core/browser/js/jarnxmpp.core.ui.js @@ -288,7 +288,7 @@ $msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google return; } $('#site-stream-link').addClass('newStreamMessage'); - $('.pubsubNode[data-node*=' + event.node + '], .pubsubNode[data-node=people]').each(function (idx, node) { + $('.pubsubNode[data-node*=' + escapeSelector(event.node) + '], .pubsubNode[data-node=people]').each(function (idx, node) { var $li, $node = $(node), isLeaf = $node.attr('data-leaf') === 'True'; diff --git a/jarn/xmpp/core/subscribers/user_management.py b/jarn/xmpp/core/subscribers/user_management.py index dee538b..a770e20 100644 --- a/jarn/xmpp/core/subscribers/user_management.py +++ b/jarn/xmpp/core/subscribers/user_management.py @@ -36,10 +36,10 @@ def onUserCreation(event): pass_storage = getUtility(IXMPPPasswordStorage) principal_pass = pass_storage.set(principal_id) - storage.leaf_nodes.append(principal_id) + storage.leaf_nodes.append(principal_jid.user) storage.node_items[principal_jid.user] = [] storage.collections['people'].append(principal_id) - storage.publishers[principal_id] = [principal_id] + storage.publishers[principal_jid.user] = [principal_jid.user] d = setupPrincipal(client, principal_jid, principal_pass, members_jids) return d