├── .gitignore
├── CHANGELOG.txt
├── LICENSE.txt
├── Makefile
├── README.md
├── contrib
└── discojs
│ ├── README.txt
│ ├── css
│ └── disco.css
│ ├── index.html
│ ├── punjab.tac
│ └── scripts
│ ├── basic.js
│ └── disco.js
├── examples
├── attach
│ ├── README
│ ├── __init__.py
│ ├── attacher
│ │ ├── __init__.py
│ │ └── views.py
│ ├── boshclient.py
│ ├── manage.py
│ ├── settings.py
│ ├── templates
│ │ └── attacher
│ │ │ └── index.html
│ └── urls.py
├── basic.html
├── basic.js
├── crossdomain.html
├── crossdomain.js
├── crossdomain.xml
├── echobot.html
├── echobot.js
├── prebind.html
└── prebind.js
├── package.json
├── plugins
└── strophe.flxhr.js
├── release_checklist.txt
├── src
├── base64.js
├── core.js
├── md5.js
└── sha1.js
└── tests
├── muc.html
├── muc.js
├── pubsub.html
├── pubsub.js
├── strophe.html
├── testrunner.js
├── tests.js
└── testsuite.css
/.gitignore:
--------------------------------------------------------------------------------
1 | version.txt
2 | strophe.js
3 | strophe.min.js
4 | ndproj
5 | doc
6 | .taperc
7 | plugins/*.min.js
8 | *.tar.gz
9 | *.zip
10 | *~
--------------------------------------------------------------------------------
/CHANGELOG.txt:
--------------------------------------------------------------------------------
1 | # Strophe.js Change Log
2 |
3 | ## Version 1.0.2 - 2011-06-19
4 |
5 | * Fix security bug where DIGEST-MD5 client nonce was not properly
6 | randomized.
7 | * Fix double escaping in copyElement.
8 | * Fix IE errors related to importNode.
9 | * Add ability to pass text into Builder.c().
10 | * Improve performance by skipping debugging callbacks when not
11 | overridden.
12 | * Wrap handler runs in try/catch so they don't affect or remove later
13 | handlers.
14 | * Add ' and " to escaped characters and other escaping fixes.
15 | * Fix _throttledRequestHandler to use proper window size.
16 | * Fix timed handler management.
17 | * Fix flXHR plugin to better deal with errors.
18 | * Fix bind() to be ECMAScript 5 compatible.
19 | * Use bosh.metajack.im in examples so they work out of the box.
20 | * Add simple XHR tests.
21 | * Update basic example to HTML5.
22 | * Move community plugins to their own repository.
23 | * Fix bug causing infinite retries.
24 | * Fix 5xx error handling.
25 | * Store stream:features for later use by plugins.
26 | * Fix to prevent passing stanzas during disconnect.
27 | * Fix handling of disconnect responses.
28 | * Fix getBareJidFromJid to return null on error.
29 | * Fix equality testing in matchers so that string literals and string
30 | objects both match.
31 | * Fix bare matching on missing from attributes.
32 | * Remove use of reserved word self.
33 | * Fix various documentation errors.
34 |
35 | ## Version 1.0.1 - 2010-01-27
36 |
37 | * Fix handling of window, hold, and wait attributes. Bug #75.
38 |
39 | ## Version 1.0 - 2010-01-01
40 |
41 | * First release.
42 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2006-2009 Collecta, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL=/bin/bash
2 |
3 | SRC_DIR = src
4 | DOC_DIR = doc
5 | PLUGIN_DIR = plugins
6 | NDPROJ_DIR = ndproj
7 |
8 | BASE_FILES = $(SRC_DIR)/base64.js \
9 | $(SRC_DIR)/sha1.js \
10 | $(SRC_DIR)/md5.js \
11 | $(SRC_DIR)/core.js
12 |
13 | STROPHE = strophe.js
14 | STROPHE_MIN = strophe.min.js
15 |
16 | PLUGIN_FILES = $(shell ls $(PLUGIN_DIR)/strophe.*.js | grep -v min)
17 | PLUGIN_FILES_MIN = $(PLUGIN_FILES:.js=.min.js)
18 |
19 | DIST_FILES = LICENSE.txt README.txt contrib examples plugins tests doc \
20 | $(STROPHE) $(STROPHE_MIN)
21 |
22 | VERSION = $(shell if [ -f version.txt ]; then cat version.txt; else VERSION=`git rev-list HEAD -n1`; echo $${VERSION:0:7}; fi)
23 |
24 | all: normal min
25 |
26 | normal: $(STROPHE)
27 |
28 | min: $(STROPHE_MIN) $(PLUGIN_FILES_MIN)
29 |
30 | $(STROPHE): $(BASE_FILES)
31 | @@echo "Building" $(STROPHE) "..."
32 | @@cat $(BASE_FILES) | sed -e 's/@VERSION@/$(VERSION)/' > $(STROPHE)
33 | @@echo $(STROPHE) "built."
34 | @@echo
35 |
36 | $(STROPHE_MIN): $(STROPHE)
37 | @@echo "Building" $(STROPHE_MIN) "..."
38 | ifdef YUI_COMPRESSOR
39 | @@java -jar $(YUI_COMPRESSOR) --type js --nomunge \
40 | $(STROPHE) > $(STROPHE_MIN)
41 | @@echo $(STROPHE_MIN) "built."
42 | else
43 | @@echo $(STROPHE_MIN) "not built."
44 | @@echo " YUI Compressor required to build minified version."
45 | @@echo " Please set YUI_COMPRESSOR to the path to the jar file."
46 | endif
47 | @@echo
48 |
49 | %.min.js: %.js
50 | @@echo "Building" $@ "..."
51 | ifdef YUI_COMPRESSOR
52 | @@java -jar $(YUI_COMPRESSOR) --type js --nomunge \
53 | $< > $@
54 | @@echo $@ "built."
55 | else
56 | @@echo $@ "not built."
57 | @@echo " YUI Compressor required to build minified version."
58 | @@echo " Please set YUI_COMPRESSOR to the path to the jar file."
59 | endif
60 | @@echo
61 |
62 | doc:
63 | @@echo "Building Strophe documentation..."
64 | @@if [ ! -d $(NDPROJ_DIR) ]; then mkdir $(NDPROJ_DIR); fi
65 | @@if [ ! -d $(DOC_DIR) ]; then mkdir $(DOC_DIR); fi
66 | @@NaturalDocs -q -i $(SRC_DIR) -i $(PLUGINS_DIR) -o html $(DOC_DIR) -p $(NDPROJ_DIR)
67 | @@echo "Documentation built."
68 | @@echo
69 |
70 | release: normal min doc
71 | @@echo "Creating release packages..."
72 | @@mkdir strophejs-$(VERSION)
73 | @@cp -R $(DIST_FILES) strophejs-$(VERSION)
74 | @@tar czf strophejs-$(VERSION).tar.gz strophejs-$(VERSION)
75 | @@zip -qr strophejs-$(VERSION).zip strophejs-$(VERSION)
76 | @@rm -rf strophejs-$(VERSION)
77 | @@echo "Release created."
78 | @@echo
79 |
80 | clean:
81 | @@echo "Cleaning" $(STROPHE) "..."
82 | @@rm -f $(STROPHE)
83 | @@echo $(STROPHE) "cleaned."
84 | @@echo "Cleaning" $(STROPHE_MIN) "..."
85 | @@rm -f $(STROPHE_MIN)
86 | @@echo $(STROPHE_MIN) "cleaned."
87 | @@echo "Cleaning minified plugins..."
88 | @@rm -f $(PLUGIN_FILES_MIN)
89 | @@echo "Minified plugins cleaned."
90 | @@echo "Cleaning documentation..."
91 | @@rm -rf $(NDPROJ_DIR) $(DOC_DIR)
92 | @@echo "Documentation cleaned."
93 | @@echo
94 |
95 | .PHONY: all normal min doc release clean
96 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Repository Moved
2 |
3 | This repository has moved to https://github.com/strophe/strophejs
4 |
--------------------------------------------------------------------------------
/contrib/discojs/README.txt:
--------------------------------------------------------------------------------
1 | Disco Dancing with XMPP
2 |
3 | There are many things one can do via XMPP. The list is
4 | endlist. But one thing that some forget about is discovering
5 | services a XMPP entity or server provides. In most cases a human or
6 | user does not care about this information and should not care. But
7 | you may have a website or web application that needs this
8 | information in order to decide what options to show to your
9 | users. You can do this very easily with JQuery, Strophe, and
10 | Punjab.
11 |
12 | We start with Punjab or a BOSH connection manager. This is
13 | needed so we can connect to a XMPP server. First, lets download
14 | punjab.
15 |
16 | svn co https://code.stanziq.com/svn/punjab/trunk punjab
17 |
18 | After we have punjab go into the directory and install punjab.
19 |
20 | cd punjab
21 | python setup.py install
22 |
23 | Then create a .tac file to configure Punjab.
24 |
25 | See punjab.tac
26 |
27 | Next, we will need Strophe. Lets download thelatest version from
28 | svn too.
29 |
30 | cd /directory/where/you/configured/punjab/html
31 |
32 | svn co https://code.stanziq.com/svn/strophe/trunk/strophejs
33 |
34 | In your html directory you will then begin to create your disco browser.
35 |
36 | Version 1 we take the basic example and modify it to do disco.
37 |
38 | Version 2 we add anonymous login
39 |
40 | Version 3 we make it pretty
41 |
42 | Version 4 we add handlers for different services
43 |
--------------------------------------------------------------------------------
/contrib/discojs/css/disco.css:
--------------------------------------------------------------------------------
1 | #login .leftinput
2 | {
3 | margin-bottom: .5em;
4 | }
5 |
6 | #login input
7 | {
8 | margin-bottom: 10px;
9 | }
10 |
11 | #log {
12 | width: 100%;
13 | height: 200px;
14 | overflow: auto;
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/contrib/discojs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | XMPP Disco Dancing
5 |
8 |
11 |
14 |
17 |
20 |
23 |
26 |
27 |
28 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/contrib/discojs/punjab.tac:
--------------------------------------------------------------------------------
1 | # punjab tac file
2 | from twisted.web import server, resource, static
3 | from twisted.application import service, internet
4 |
5 | from punjab.httpb import Httpb, HttpbService
6 |
7 | root = static.File("./") # This needs to be the directory
8 | # where you will have your html
9 | # and javascript.
10 |
11 | b = resource.IResource(HttpbService(1)) # turn on debug with 1
12 | root.putChild('bosh', b)
13 |
14 |
15 | site = server.Site(root)
16 |
17 | application = service.Application("punjab")
18 | internet.TCPServer(5280, site).setServiceParent(application)
19 |
--------------------------------------------------------------------------------
/contrib/discojs/scripts/basic.js:
--------------------------------------------------------------------------------
1 | var BOSH_SERVICE = 'http://localhost:5280/bosh';
2 |
3 | var connection = null;
4 | var browser = null;
5 | var show_log = true;
6 |
7 | function log(msg)
8 | {
9 | $('#log').append('').append(document.createTextNode(msg));
10 | }
11 |
12 |
13 | function rawInput(data)
14 | {
15 | log('RECV: ' + data);
16 | }
17 |
18 | function rawOutput(data)
19 | {
20 | log('SENT: ' + data);
21 | }
22 |
23 | function onConnect(status)
24 | {
25 | if (status == Strophe.Status.CONNECTING) {
26 | log('Strophe is connecting.');
27 |
28 | } else if (status == Strophe.Status.CONNFAIL) {
29 | log('Strophe failed to connect.');
30 | showConnect();
31 | } else if (status == Strophe.Status.DISCONNECTING) {
32 | log('Strophe is disconnecting.');
33 | } else if (status == Strophe.Status.DISCONNECTED) {
34 | log('Strophe is disconnected.');
35 | showConnect();
36 |
37 | } else if (status == Strophe.Status.CONNECTED) {
38 | log('Strophe is connected.');
39 | // Start up disco browser
40 | browser.showBrowser();
41 | }
42 | }
43 |
44 | function showConnect()
45 | {
46 | var jid = $('#jid');
47 | var pass = $('#pass');
48 | var button = $('#connect').get(0);
49 |
50 | browser.closeBrowser();
51 |
52 | $('label').show();
53 | jid.show();
54 | pass.show();
55 | $('#anon').show();
56 | button.value = 'connect';
57 | return false;
58 | }
59 |
60 | function showDisconnect()
61 | {
62 | var jid = $('#jid');
63 | var pass = $('#pass');
64 | var button = $('#connect').get(0);
65 |
66 | button.value = 'disconnect';
67 | pass.hide();
68 | jid.hide();
69 | $('label').hide();
70 | $('#anon').hide();
71 | return false;
72 | }
73 |
74 | $(document).ready(function () {
75 | connection = new Strophe.Connection(BOSH_SERVICE);
76 | connection.rawInput = rawInput;
77 | connection.rawOutput = rawOutput;
78 |
79 | browser = new Disco();
80 |
81 | $("#log_container").bind('click', function () {
82 | $("#log").toggle();
83 | }
84 | );
85 |
86 | $('#cred').bind('submit', function () {
87 | var button = $('#connect').get(0);
88 | var jid = $('#jid');
89 | var pass = $('#pass');
90 |
91 | if (button.value == 'connect') {
92 | showDisconnect();
93 | connection.connect(jid.get(0).value,
94 | pass.get(0).value,
95 | onConnect);
96 | } else {
97 | connection.disconnect();
98 | showConnect();
99 | }
100 | return false;
101 | });
102 | });
--------------------------------------------------------------------------------
/contrib/discojs/scripts/disco.js:
--------------------------------------------------------------------------------
1 |
2 | var NS_DISCO_INFO = 'http://jabber.org/protocol/disco#info';
3 | var NS_DISCO_ITEM = 'http://jabber.org/protocol/disco#items';
4 |
5 |
6 | // Disco stuff
7 | Disco = function () {
8 | // Class that does nothing
9 | };
10 |
11 | Disco.prototype = {
12 | showBrowser: function() {
13 | // Browser Display
14 | var disco = $('#disco');
15 | var jid = $('#jid');
16 | var server = connection.jid.split('@')[1];
17 |
18 | // display input box
19 | disco.append("");
20 |
21 | // add handler for search form
22 | $("#browse").bind('submit', function () {
23 | this.startBrowse($("#server").get(0).value);
24 | return false;
25 | });
26 |
27 | this.startBrowse(server);
28 | },
29 |
30 | closeBrowser: function() {
31 | var disco = $('#disco');
32 |
33 | disco.empty();
34 | },
35 |
36 | startBrowse: function(server) {
37 | // build iq request
38 | var id = 'startBrowse';
39 |
40 | var discoiq = $iq({'from':connection.jid+"/"+connection.resource,
41 | 'to':server,
42 | 'id':id,
43 | 'type':'get'}
44 | )
45 | .c('query', {'xmlns': NS_DISCO_INFO});
46 |
47 | connection.addHandler(this._cbBrowse, null, 'iq', 'result', id);
48 | connection.send(discoiq.tree());
49 |
50 | },
51 |
52 | _cbBrowse: function(e) {
53 | var elem = $(e); // make this Element a JQuery Element
54 | alert(e);
55 |
56 | return false; // return false to remove the handler
57 | },
58 |
59 | };
60 |
61 |
--------------------------------------------------------------------------------
/examples/attach/README:
--------------------------------------------------------------------------------
1 | This is an example of Strophe attaching to a pre-existing BOSH session
2 | that is created externally. This example requires a bit more than
3 | HTML and JavaScript. Specifically it contains a very simple Web
4 | application written in Django which creates a BOSH session before
5 | rendering the page.
6 |
7 | Requirements:
8 |
9 | * Django 1.0 (http://www.djangoproject.com)
10 | * Twisted 8.1.x (http://twistedmatrix.com)
11 | * Punjab 0.3 (http://code.stanziq.com/punjab)
12 |
13 | Note that Twisted and Punjab are only used for small functions related
14 | to JID and BOSH parsing.
15 |
16 | How It Works:
17 |
18 | The Django app contains one view which is tied to the root URL. This
19 | view uses the BOSHClient class to start a BOSH session using the
20 | settings from settings.py.
21 |
22 | Once the connection is established, Django passes the JID, SID, and
23 | RID for the BOSH session into the template engine and renders the
24 | page.
25 |
26 | The template assigns the JID, SID, and RID to global vars like so:
27 |
28 | var BOSH_JID = {{ jid }};
29 | var BOSH_SID = {{ sid }};
30 | var BOSH_RID = {{ rid }};
31 |
32 | The connection is attached to Strophe by calling
33 | Strophe.Connection.attach() with this data and a connection callback
34 | handler.
35 |
36 | To show that the session is attached and works, a disco info ping is
37 | done to jabber.org.
38 |
--------------------------------------------------------------------------------
/examples/attach/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metajack/strophejs/85d35a62b6bf83067116af2f5841ac56fed0d28d/examples/attach/__init__.py
--------------------------------------------------------------------------------
/examples/attach/attacher/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metajack/strophejs/85d35a62b6bf83067116af2f5841ac56fed0d28d/examples/attach/attacher/__init__.py
--------------------------------------------------------------------------------
/examples/attach/attacher/views.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponse
2 | from django.template import Context, loader
3 |
4 | from attach.settings import BOSH_SERVICE, JABBERID, PASSWORD
5 | from attach.boshclient import BOSHClient
6 |
7 | def index(request):
8 | bc = BOSHClient(JABBERID, PASSWORD, BOSH_SERVICE)
9 | bc.startSessionAndAuth()
10 |
11 | t = loader.get_template("attacher/index.html")
12 | c = Context({
13 | 'jid': bc.jabberid.full(),
14 | 'sid': bc.sid,
15 | 'rid': bc.rid,
16 | })
17 |
18 | return HttpResponse(t.render(c))
19 |
--------------------------------------------------------------------------------
/examples/attach/boshclient.py:
--------------------------------------------------------------------------------
1 | import sys, os
2 | import httplib, urllib
3 | import random, binascii
4 | from urlparse import urlparse
5 |
6 | from punjab.httpb import HttpbParse
7 |
8 | from twisted.words.xish import domish
9 | from twisted.words.protocols.jabber import jid
10 |
11 | TLS_XMLNS = 'urn:ietf:params:xml:ns:xmpp-tls'
12 | SASL_XMLNS = 'urn:ietf:params:xml:ns:xmpp-sasl'
13 | BIND_XMLNS = 'urn:ietf:params:xml:ns:xmpp-bind'
14 | SESSION_XMLNS = 'urn:ietf:params:xml:ns:xmpp-session'
15 |
16 |
17 | class BOSHClient:
18 | def __init__(self, jabberid, password, bosh_service):
19 | self.rid = random.randint(0, 10000000)
20 | self.jabberid = jid.internJID(jabberid)
21 | self.password = password
22 |
23 | self.authid = None
24 | self.sid = None
25 | self.logged_in = False
26 | self.headers = {"Content-type": "text/xml",
27 | "Accept": "text/xml"}
28 |
29 | self.bosh_service = urlparse(bosh_service)
30 |
31 | def buildBody(self, child=None):
32 | """Build a BOSH body.
33 | """
34 |
35 | body = domish.Element(("http://jabber.org/protocol/httpbind", "body"))
36 | body['content'] = 'text/xml; charset=utf-8'
37 | self.rid = self.rid + 1
38 | body['rid'] = str(self.rid)
39 | body['sid'] = str(self.sid)
40 | body['xml:lang'] = 'en'
41 |
42 | if child is not None:
43 | body.addChild(child)
44 |
45 | return body
46 |
47 | def sendBody(self, body):
48 | """Send the body.
49 | """
50 |
51 | parser = HttpbParse(True)
52 |
53 | # start new session
54 | conn = httplib.HTTPConnection(self.bosh_service.netloc)
55 | conn.request("POST", self.bosh_service.path,
56 | body.toXml(), self.headers)
57 |
58 | response = conn.getresponse()
59 | data = ''
60 | if response.status == 200:
61 | data = response.read()
62 | conn.close()
63 |
64 | return parser.parse(data)
65 |
66 | def startSessionAndAuth(self, hold='1', wait='70'):
67 | # Create a session
68 | # create body
69 | body = domish.Element(("http://jabber.org/protocol/httpbind", "body"))
70 |
71 | body['content'] = 'text/xml; charset=utf-8'
72 | body['hold'] = hold
73 | body['rid'] = str(self.rid)
74 | body['to'] = self.jabberid.host
75 | body['wait'] = wait
76 | body['window'] = '5'
77 | body['xml:lang'] = 'en'
78 |
79 |
80 | retb, elems = self.sendBody(body)
81 | if type(retb) != str and retb.hasAttribute('authid') and \
82 | retb.hasAttribute('sid'):
83 | self.authid = retb['authid']
84 | self.sid = retb['sid']
85 |
86 | # go ahead and auth
87 | auth = domish.Element((SASL_XMLNS, 'auth'))
88 | auth['mechanism'] = 'PLAIN'
89 |
90 | # TODO: add authzid
91 | if auth['mechanism'] == 'PLAIN':
92 | auth_str = ""
93 | auth_str += "\000"
94 | auth_str += self.jabberid.user.encode('utf-8')
95 | auth_str += "\000"
96 | try:
97 | auth_str += self.password.encode('utf-8').strip()
98 | except UnicodeDecodeError:
99 | auth_str += self.password.decode('latin1') \
100 | .encode('utf-8').strip()
101 |
102 | auth.addContent(binascii.b2a_base64(auth_str))
103 |
104 | retb, elems = self.sendBody(self.buildBody(auth))
105 | if len(elems) == 0:
106 | # poll for data
107 | retb, elems = self.sendBody(self.buildBody())
108 |
109 | if len(elems) > 0:
110 | if elems[0].name == 'success':
111 | retb, elems = self.sendBody(self.buildBody())
112 |
113 | if elems[0].firstChildElement().name == 'bind':
114 | iq = domish.Element(('jabber:client', 'iq'))
115 | iq['type'] = 'set'
116 | iq.addUniqueId()
117 | iq.addElement('bind')
118 | iq.bind['xmlns'] = BIND_XMLNS
119 | if self.jabberid.resource:
120 | iq.bind.addElement('resource')
121 | iq.bind.resource.addContent(
122 | self.jabberid.resource)
123 |
124 | retb, elems = self.sendBody(self.buildBody(iq))
125 | if type(retb) != str and retb.name == 'body':
126 | # send session
127 | iq = domish.Element(('jabber:client', 'iq'))
128 | iq['type'] = 'set'
129 | iq.addUniqueId()
130 | iq.addElement('session')
131 | iq.session['xmlns'] = SESSION_XMLNS
132 |
133 | retb, elems = self.sendBody(self.buildBody(iq))
134 |
135 | # did not bind, TODO - add a retry?
136 | if type(retb) != str and retb.name == 'body':
137 | self.logged_in = True
138 | # bump up the rid, punjab already
139 | # received self.rid
140 | self.rid += 1
141 |
142 |
143 | if __name__ == '__main__':
144 | USERNAME = sys.argv[1]
145 | PASSWORD = sys.argv[2]
146 | SERVICE = sys.argv[3]
147 |
148 | c = BOSHClient(USERNAME, PASSWORD, SERVICE)
149 | c.startSessionAndAuth()
150 |
151 | print c.logged_in
152 |
153 |
--------------------------------------------------------------------------------
/examples/attach/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from django.core.management import execute_manager
3 | try:
4 | import settings # Assumed to be in the same directory.
5 | except ImportError:
6 | import sys
7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
8 | sys.exit(1)
9 |
10 | if __name__ == "__main__":
11 | execute_manager(settings)
12 |
--------------------------------------------------------------------------------
/examples/attach/settings.py:
--------------------------------------------------------------------------------
1 | # Django settings for attach project.
2 |
3 | DEBUG = True
4 | TEMPLATE_DEBUG = DEBUG
5 |
6 | ADMINS = (
7 | ('Some Body', 'romeo@example.com'),
8 | )
9 |
10 | MANAGERS = ADMINS
11 |
12 | DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
13 | DATABASE_NAME = '/path/to/attach.db' # Or path to database file if using sqlite3.
14 | DATABASE_USER = '' # Not used with sqlite3.
15 | DATABASE_PASSWORD = '' # Not used with sqlite3.
16 | DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
17 | DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
18 |
19 | # Local time zone for this installation. Choices can be found here:
20 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
21 | # although not all choices may be available on all operating systems.
22 | # If running in a Windows environment this must be set to the same as your
23 | # system time zone.
24 | TIME_ZONE = 'America/Denver'
25 |
26 | # Language code for this installation. All choices can be found here:
27 | # http://www.i18nguy.com/unicode/language-identifiers.html
28 | LANGUAGE_CODE = 'en-us'
29 |
30 | SITE_ID = 1
31 |
32 | # If you set this to False, Django will make some optimizations so as not
33 | # to load the internationalization machinery.
34 | USE_I18N = True
35 |
36 | # Absolute path to the directory that holds media.
37 | # Example: "/home/media/media.lawrence.com/"
38 | MEDIA_ROOT = ''
39 |
40 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a
41 | # trailing slash if there is a path component (optional in other cases).
42 | # Examples: "http://media.lawrence.com", "http://example.com/media/"
43 | MEDIA_URL = ''
44 |
45 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
46 | # trailing slash.
47 | # Examples: "http://foo.com/media/", "/media/".
48 | ADMIN_MEDIA_PREFIX = '/media/'
49 |
50 | # Make this unique, and don't share it with anybody.
51 | SECRET_KEY = 'asdf'
52 |
53 | # List of callables that know how to import templates from various sources.
54 | TEMPLATE_LOADERS = (
55 | 'django.template.loaders.filesystem.load_template_source',
56 | 'django.template.loaders.app_directories.load_template_source',
57 | # 'django.template.loaders.eggs.load_template_source',
58 | )
59 |
60 | MIDDLEWARE_CLASSES = (
61 | 'django.middleware.common.CommonMiddleware',
62 | 'django.contrib.sessions.middleware.SessionMiddleware',
63 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
64 | )
65 |
66 | ROOT_URLCONF = 'attach.urls'
67 |
68 | TEMPLATE_DIRS = (
69 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
70 | # Always use forward slashes, even on Windows.
71 | # Don't forget to use absolute paths, not relative paths.
72 | '/path/to/attach/templates',
73 | )
74 |
75 | INSTALLED_APPS = (
76 | 'django.contrib.auth',
77 | 'django.contrib.contenttypes',
78 | 'django.contrib.sessions',
79 | 'django.contrib.sites',
80 | 'attach.attacher',
81 | )
82 |
83 | BOSH_SERVICE = 'http://example.com/xmpp-httpbind'
84 | JABBERID = 'romeo@example.com/bosh'
85 | PASSWORD = 'juliet.is.hawt'
86 |
--------------------------------------------------------------------------------
/examples/attach/templates/attacher/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Strophe Attach Example
5 |
8 |
11 |
14 |
17 |
20 |
79 |
80 |
81 | Strophe Attach Example
82 | This example shows how to attach to an existing BOSH session with
83 | Strophe.
84 | Log
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/examples/attach/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 |
3 | # Uncomment the next two lines to enable the admin:
4 | # from django.contrib import admin
5 | # admin.autodiscover()
6 |
7 | urlpatterns = patterns('',
8 | # Example:
9 | # (r'^attach/', include('attach.foo.urls')),
10 |
11 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
12 | # to INSTALLED_APPS to enable admin documentation:
13 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
14 |
15 | # Uncomment the next line to enable the admin:
16 | # (r'^admin/(.*)', admin.site.root),
17 |
18 | (r'^$', 'attach.attacher.views.index'),
19 | )
20 |
--------------------------------------------------------------------------------
/examples/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Strophe.js Basic Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/basic.js:
--------------------------------------------------------------------------------
1 | var BOSH_SERVICE = 'http://bosh.metajack.im:5280/xmpp-httpbind'
2 | var connection = null;
3 |
4 | function log(msg)
5 | {
6 | $('#log').append('').append(document.createTextNode(msg));
7 | }
8 |
9 | function rawInput(data)
10 | {
11 | log('RECV: ' + data);
12 | }
13 |
14 | function rawOutput(data)
15 | {
16 | log('SENT: ' + data);
17 | }
18 |
19 | function onConnect(status)
20 | {
21 | if (status == Strophe.Status.CONNECTING) {
22 | log('Strophe is connecting.');
23 | } else if (status == Strophe.Status.CONNFAIL) {
24 | log('Strophe failed to connect.');
25 | $('#connect').get(0).value = 'connect';
26 | } else if (status == Strophe.Status.DISCONNECTING) {
27 | log('Strophe is disconnecting.');
28 | } else if (status == Strophe.Status.DISCONNECTED) {
29 | log('Strophe is disconnected.');
30 | $('#connect').get(0).value = 'connect';
31 | } else if (status == Strophe.Status.CONNECTED) {
32 | log('Strophe is connected.');
33 | connection.disconnect();
34 | }
35 | }
36 |
37 | $(document).ready(function () {
38 | connection = new Strophe.Connection(BOSH_SERVICE);
39 | connection.rawInput = rawInput;
40 | connection.rawOutput = rawOutput;
41 |
42 | $('#connect').bind('click', function () {
43 | var button = $('#connect').get(0);
44 | if (button.value == 'connect') {
45 | button.value = 'disconnect';
46 |
47 | connection.connect($('#jid').get(0).value,
48 | $('#pass').get(0).value,
49 | onConnect);
50 | } else {
51 | button.value = 'connect';
52 | connection.disconnect();
53 | }
54 | });
55 | });
--------------------------------------------------------------------------------
/examples/crossdomain.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Strophe.js Basic Cross-Domain Example
5 |
6 |
8 |
10 |
11 |
13 |
15 |
16 |
18 |
19 |
20 |
21 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/examples/crossdomain.js:
--------------------------------------------------------------------------------
1 | // The BOSH_SERVICE here doesn't need to be on the same domain/port, but
2 | // it must have a /crossdomain.xml policy file that allows access from
3 | // wherever crossdomain.html lives.
4 | //
5 | // Most BOSH connection managers can serve static html files, so you should
6 | // be able to configure them to serve a /crossdomain.xml file to allow
7 | // access.
8 | var BOSH_SERVICE = 'http://bosh.metajack.im:5280/xmpp-httpbind'
9 | var connection = null;
10 |
11 | function log(msg)
12 | {
13 | $('#log').append('').append(document.createTextNode(msg));
14 | }
15 |
16 | function rawInput(data)
17 | {
18 | log('RECV: ' + data);
19 | }
20 |
21 | function rawOutput(data)
22 | {
23 | log('SENT: ' + data);
24 | }
25 |
26 | function onConnect(status)
27 | {
28 | if (status == Strophe.Status.CONNECTING) {
29 | log('Strophe is connecting.');
30 | } else if (status == Strophe.Status.CONNFAIL) {
31 | log('Strophe failed to connect.');
32 | $('#connect').get(0).value = 'connect';
33 | } else if (status == Strophe.Status.DISCONNECTING) {
34 | log('Strophe is disconnecting.');
35 | } else if (status == Strophe.Status.DISCONNECTED) {
36 | log('Strophe is disconnected.');
37 | $('#connect').get(0).value = 'connect';
38 | } else if (status == Strophe.Status.CONNECTED) {
39 | log('Strophe is connected.');
40 | connection.disconnect();
41 | }
42 | }
43 |
44 | $(document).ready(function () {
45 | connection = new Strophe.Connection(BOSH_SERVICE);
46 | connection.rawInput = rawInput;
47 | connection.rawOutput = rawOutput;
48 |
49 | $('#connect').bind('click', function () {
50 | var button = $('#connect').get(0);
51 | if (button.value == 'connect') {
52 | button.value = 'disconnect';
53 |
54 | connection.connect($('#jid').get(0).value,
55 | $('#pass').get(0).value,
56 | onConnect);
57 | } else {
58 | button.value = 'connect';
59 | connection.disconnect();
60 | }
61 | });
62 | });
--------------------------------------------------------------------------------
/examples/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/echobot.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Strophe.js Echobot Example
5 |
7 |
9 |
11 |
12 |
13 |
14 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/examples/echobot.js:
--------------------------------------------------------------------------------
1 | var BOSH_SERVICE = '/xmpp-httpbind';
2 | var connection = null;
3 |
4 | function log(msg)
5 | {
6 | $('#log').append('').append(document.createTextNode(msg));
7 | }
8 |
9 | function onConnect(status)
10 | {
11 | if (status == Strophe.Status.CONNECTING) {
12 | log('Strophe is connecting.');
13 | } else if (status == Strophe.Status.CONNFAIL) {
14 | log('Strophe failed to connect.');
15 | $('#connect').get(0).value = 'connect';
16 | } else if (status == Strophe.Status.DISCONNECTING) {
17 | log('Strophe is disconnecting.');
18 | } else if (status == Strophe.Status.DISCONNECTED) {
19 | log('Strophe is disconnected.');
20 | $('#connect').get(0).value = 'connect';
21 | } else if (status == Strophe.Status.CONNECTED) {
22 | log('Strophe is connected.');
23 | log('ECHOBOT: Send a message to ' + connection.jid +
24 | ' to talk to me.');
25 |
26 | connection.addHandler(onMessage, null, 'message', null, null, null);
27 | connection.send($pres().tree());
28 | }
29 | }
30 |
31 | function onMessage(msg) {
32 | var to = msg.getAttribute('to');
33 | var from = msg.getAttribute('from');
34 | var type = msg.getAttribute('type');
35 | var elems = msg.getElementsByTagName('body');
36 |
37 | if (type == "chat" && elems.length > 0) {
38 | var body = elems[0];
39 |
40 | log('ECHOBOT: I got a message from ' + from + ': ' +
41 | Strophe.getText(body));
42 |
43 | var reply = $msg({to: from, from: to, type: 'chat'})
44 | .cnode(Strophe.copyElement(body));
45 | connection.send(reply.tree());
46 |
47 | log('ECHOBOT: I sent ' + from + ': ' + Strophe.getText(body));
48 | }
49 |
50 | // we must return true to keep the handler alive.
51 | // returning false would remove it after it finishes.
52 | return true;
53 | }
54 |
55 | $(document).ready(function () {
56 | connection = new Strophe.Connection(BOSH_SERVICE);
57 |
58 | // Uncomment the following lines to spy on the wire traffic.
59 | //connection.rawInput = function (data) { log('RECV: ' + data); };
60 | //connection.rawOutput = function (data) { log('SEND: ' + data); };
61 |
62 | // Uncomment the following line to see all the debug output.
63 | //Strophe.log = function (level, msg) { log('LOG: ' + msg); };
64 |
65 |
66 | $('#connect').bind('click', function () {
67 | var button = $('#connect').get(0);
68 | if (button.value == 'connect') {
69 | button.value = 'disconnect';
70 |
71 | connection.connect($('#jid').get(0).value,
72 | $('#pass').get(0).value,
73 | onConnect);
74 | } else {
75 | button.value = 'connect';
76 | connection.disconnect();
77 | }
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/examples/prebind.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
18 | Strophe.js Pre-Bind Example
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/examples/prebind.js:
--------------------------------------------------------------------------------
1 | // http-pre-bind example
2 | // This example works with mod_http_pre_bind found here:
3 | // http://github.com/thepug/Mod-Http-Pre-Bind
4 | //
5 | // It expects both /xmpp-httpbind to be proxied and /http-pre-bind
6 | //
7 | // If you want to test this out without setting it up, you can use Collecta's
8 | // at http://www.collecta.com/xmpp-httpbind and
9 | // http://www.collecta.com/http-pre-bind
10 | // Use a JID of 'guest.collecta.com' to test.
11 |
12 | var BOSH_SERVICE = '/xmpp-httpbind';
13 | var PREBIND_SERVICE = '/http-pre-bind';
14 | var connection = null;
15 |
16 | function log(msg)
17 | {
18 | $('#log').append('').append(document.createTextNode(msg));
19 | }
20 |
21 | function rawInput(data)
22 | {
23 | log('RECV: ' + data);
24 | }
25 |
26 | function rawOutput(data)
27 | {
28 | log('SENT: ' + data);
29 | }
30 |
31 | function onConnect(status)
32 | {
33 | if (status === Strophe.Status.CONNECTING) {
34 | log('Strophe is connecting.');
35 | } else if (status === Strophe.Status.CONNFAIL) {
36 | log('Strophe failed to connect.');
37 | $('#connect').get(0).value = 'connect';
38 | } else if (status === Strophe.Status.DISCONNECTING) {
39 | log('Strophe is disconnecting.');
40 | } else if (status === Strophe.Status.DISCONNECTED) {
41 | log('Strophe is disconnected.');
42 | $('#connect').get(0).value = 'connect';
43 | } else if (status === Strophe.Status.CONNECTED) {
44 | log('Strophe is connected.');
45 | connection.disconnect();
46 | } else if (status === Strophe.Status.ATTACHED) {
47 | log('Strophe is attached.');
48 | connection.disconnect();
49 | }
50 | }
51 |
52 | function normal_connect() {
53 | log('Prebind failed. Connecting normally...');
54 |
55 | connection = new Strophe.Connection(BOSH_SERVICE);
56 | connection.rawInput = rawInput;
57 | connection.rawOutput = rawOutput;
58 |
59 | connection.connect($('#jid').val(), $('#pass').val(), onConnect);
60 | }
61 |
62 | function attach(data) {
63 | log('Prebind succeeded. Attaching...');
64 |
65 | connection = new Strophe.Connection(BOSH_SERVICE);
66 | connection.rawInput = rawInput;
67 | connection.rawOutput = rawOutput;
68 |
69 | var $body = $(data.documentElement);
70 | connection.attach($body.find('jid').text(),
71 | $body.attr('sid'),
72 | parseInt($body.attr('rid'), 10) + 1,
73 | onConnect);
74 | }
75 |
76 | $(document).ready(function () {
77 | $('#connect').bind('click', function () {
78 | var button = $('#connect').get(0);
79 | if (button.value == 'connect') {
80 | button.value = 'disconnect';
81 |
82 | // attempt prebind
83 | $.ajax({
84 | type: 'POST',
85 | url: PREBIND_SERVICE,
86 | contentType: 'text/xml',
87 | processData: false,
88 | data: $build('body', {
89 | to: Strophe.getDomainFromJid($('#jid').val()),
90 | rid: '' + Math.floor(Math.random() * 4294967295),
91 | wait: '60',
92 | hold: '1'}).toString(),
93 | dataType: 'xml',
94 | error: normal_connect,
95 | success: attach});
96 | } else {
97 | button.value = 'connect';
98 | if (connection) {
99 | connection.disconnect();
100 | }
101 | }
102 | });
103 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "Strophe.js",
3 | "description" : "Strophe.js is an XMPP library for JavaScript",
4 | "version" : "1.0.2",
5 | "homepage" : "http://strophe.im/strophejs",
6 | "repository" : { "type": "git", "url": "git://github.com/metajack/strophejs.git" },
7 | "keywords" : ["xmpp", "message", "browser"],
8 | "author" : "",
9 | "contributors" : [],
10 | "licenses" : ["MIT"],
11 | "dependencies" : [],
12 | "main" : "./strophe.js",
13 | "scripts" : { "install": "make", "update": "make" },
14 | "directories" : { "lib": "./src", "test": "./tests" },
15 | "engines" : { "browser": "*" }
16 | }
17 |
--------------------------------------------------------------------------------
/plugins/strophe.flxhr.js:
--------------------------------------------------------------------------------
1 | /* flXHR plugin
2 | **
3 | ** This plugin implements cross-domain XmlHttpRequests via an invisible
4 | ** Flash plugin.
5 | **
6 | ** In order for this to work, the BOSH service *must* serve a
7 | ** crossdomain.xml file that allows the client access.
8 | **
9 | ** flXHR.js should be loaded before this plugin.
10 | */
11 |
12 | Strophe.addConnectionPlugin('flxhr', {
13 | init: function (conn) {
14 | // replace Strophe.Request._newXHR with new flXHR version
15 | // if flXHR is detected
16 | if (flensed && flensed.flXHR) {
17 | Strophe.Request.prototype._newXHR = function () {
18 | var xhr = new flensed.flXHR({
19 | autoUpdatePlayer: true,
20 | instancePooling: true,
21 | noCacheHeader: false,
22 | onerror: function () {
23 | conn._changeConnectStatus(Strophe.Status.CONNFAIL,
24 | "flXHR connection error");
25 | conn._onDisconnectTimeout();
26 | }});
27 | xhr.onreadystatechange = this.func.bind(null, this);
28 |
29 | return xhr;
30 | };
31 | } else {
32 | Strophe.error("flXHR plugin loaded, but flXHR not found." +
33 | " Falling back to native XHR implementation.");
34 | }
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/release_checklist.txt:
--------------------------------------------------------------------------------
1 | # -*- mode: org -*-
2 |
3 | * Release Checklist
4 | *** Make sure all tests pass
5 | *** Decide on version number
6 | *** Update CHANGELOG.txt
7 | *** Tag code with version (git tag -s release-VERSION head)
8 | *** Push repo and tags (git push && git push --tags)
9 | *** Create version.txt with version
10 | *** Run make release
11 | *** Upload zip file and tarball to GitHub
12 | *** Add documentation to strophe.im repo
13 | *** Update website
14 | *** Tell the world
15 |
--------------------------------------------------------------------------------
/src/base64.js:
--------------------------------------------------------------------------------
1 | // This code was written by Tyler Akins and has been placed in the
2 | // public domain. It would be nice if you left this header intact.
3 | // Base64 code from Tyler Akins -- http://rumkin.com
4 |
5 | var Base64 = (function () {
6 | var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
7 |
8 | var obj = {
9 | /**
10 | * Encodes a string in base64
11 | * @param {String} input The string to encode in base64.
12 | */
13 | encode: function (input) {
14 | var output = "";
15 | var chr1, chr2, chr3;
16 | var enc1, enc2, enc3, enc4;
17 | var i = 0;
18 |
19 | do {
20 | chr1 = input.charCodeAt(i++);
21 | chr2 = input.charCodeAt(i++);
22 | chr3 = input.charCodeAt(i++);
23 |
24 | enc1 = chr1 >> 2;
25 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
26 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
27 | enc4 = chr3 & 63;
28 |
29 | if (isNaN(chr2)) {
30 | enc3 = enc4 = 64;
31 | } else if (isNaN(chr3)) {
32 | enc4 = 64;
33 | }
34 |
35 | output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
36 | keyStr.charAt(enc3) + keyStr.charAt(enc4);
37 | } while (i < input.length);
38 |
39 | return output;
40 | },
41 |
42 | /**
43 | * Decodes a base64 string.
44 | * @param {String} input The string to decode.
45 | */
46 | decode: function (input) {
47 | var output = "";
48 | var chr1, chr2, chr3;
49 | var enc1, enc2, enc3, enc4;
50 | var i = 0;
51 |
52 | // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
53 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
54 |
55 | do {
56 | enc1 = keyStr.indexOf(input.charAt(i++));
57 | enc2 = keyStr.indexOf(input.charAt(i++));
58 | enc3 = keyStr.indexOf(input.charAt(i++));
59 | enc4 = keyStr.indexOf(input.charAt(i++));
60 |
61 | chr1 = (enc1 << 2) | (enc2 >> 4);
62 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
63 | chr3 = ((enc3 & 3) << 6) | enc4;
64 |
65 | output = output + String.fromCharCode(chr1);
66 |
67 | if (enc3 != 64) {
68 | output = output + String.fromCharCode(chr2);
69 | }
70 | if (enc4 != 64) {
71 | output = output + String.fromCharCode(chr3);
72 | }
73 | } while (i < input.length);
74 |
75 | return output;
76 | }
77 | };
78 |
79 | return obj;
80 | })();
81 |
--------------------------------------------------------------------------------
/src/md5.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
3 | * Digest Algorithm, as defined in RFC 1321.
4 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
6 | * Distributed under the BSD License
7 | * See http://pajhome.org.uk/crypt/md5 for more info.
8 | */
9 |
10 | var MD5 = (function () {
11 | /*
12 | * Configurable variables. You may need to tweak these to be compatible with
13 | * the server-side, but the defaults work in most cases.
14 | */
15 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
16 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
17 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
18 |
19 | /*
20 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally
21 | * to work around bugs in some JS interpreters.
22 | */
23 | var safe_add = function (x, y) {
24 | var lsw = (x & 0xFFFF) + (y & 0xFFFF);
25 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
26 | return (msw << 16) | (lsw & 0xFFFF);
27 | };
28 |
29 | /*
30 | * Bitwise rotate a 32-bit number to the left.
31 | */
32 | var bit_rol = function (num, cnt) {
33 | return (num << cnt) | (num >>> (32 - cnt));
34 | };
35 |
36 | /*
37 | * Convert a string to an array of little-endian words
38 | * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
39 | */
40 | var str2binl = function (str) {
41 | var bin = [];
42 | var mask = (1 << chrsz) - 1;
43 | for(var i = 0; i < str.length * chrsz; i += chrsz)
44 | {
45 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
46 | }
47 | return bin;
48 | };
49 |
50 | /*
51 | * Convert an array of little-endian words to a string
52 | */
53 | var binl2str = function (bin) {
54 | var str = "";
55 | var mask = (1 << chrsz) - 1;
56 | for(var i = 0; i < bin.length * 32; i += chrsz)
57 | {
58 | str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
59 | }
60 | return str;
61 | };
62 |
63 | /*
64 | * Convert an array of little-endian words to a hex string.
65 | */
66 | var binl2hex = function (binarray) {
67 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
68 | var str = "";
69 | for(var i = 0; i < binarray.length * 4; i++)
70 | {
71 | str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
72 | hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
73 | }
74 | return str;
75 | };
76 |
77 | /*
78 | * Convert an array of little-endian words to a base-64 string
79 | */
80 | var binl2b64 = function (binarray) {
81 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
82 | var str = "";
83 | var triplet, j;
84 | for(var i = 0; i < binarray.length * 4; i += 3)
85 | {
86 | triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) |
87 | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) |
88 | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
89 | for(j = 0; j < 4; j++)
90 | {
91 | if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
92 | else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
93 | }
94 | }
95 | return str;
96 | };
97 |
98 | /*
99 | * These functions implement the four basic operations the algorithm uses.
100 | */
101 | var md5_cmn = function (q, a, b, x, s, t) {
102 | return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b);
103 | };
104 |
105 | var md5_ff = function (a, b, c, d, x, s, t) {
106 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
107 | };
108 |
109 | var md5_gg = function (a, b, c, d, x, s, t) {
110 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
111 | };
112 |
113 | var md5_hh = function (a, b, c, d, x, s, t) {
114 | return md5_cmn(b ^ c ^ d, a, b, x, s, t);
115 | };
116 |
117 | var md5_ii = function (a, b, c, d, x, s, t) {
118 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
119 | };
120 |
121 | /*
122 | * Calculate the MD5 of an array of little-endian words, and a bit length
123 | */
124 | var core_md5 = function (x, len) {
125 | /* append padding */
126 | x[len >> 5] |= 0x80 << ((len) % 32);
127 | x[(((len + 64) >>> 9) << 4) + 14] = len;
128 |
129 | var a = 1732584193;
130 | var b = -271733879;
131 | var c = -1732584194;
132 | var d = 271733878;
133 |
134 | var olda, oldb, oldc, oldd;
135 | for (var i = 0; i < x.length; i += 16)
136 | {
137 | olda = a;
138 | oldb = b;
139 | oldc = c;
140 | oldd = d;
141 |
142 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
143 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
144 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
145 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
146 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
147 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
148 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
149 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
150 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
151 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
152 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
153 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
154 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
155 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
156 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
157 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
158 |
159 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
160 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
161 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
162 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
163 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
164 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
165 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
166 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
167 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
168 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
169 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
170 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
171 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
172 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
173 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
174 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
175 |
176 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
177 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
178 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
179 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
180 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
181 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
182 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
183 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
184 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
185 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
186 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
187 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
188 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
189 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
190 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
191 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
192 |
193 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
194 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
195 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
196 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
197 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
198 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
199 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
200 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
201 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
202 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
203 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
204 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
205 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
206 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
207 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
208 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
209 |
210 | a = safe_add(a, olda);
211 | b = safe_add(b, oldb);
212 | c = safe_add(c, oldc);
213 | d = safe_add(d, oldd);
214 | }
215 | return [a, b, c, d];
216 | };
217 |
218 |
219 | /*
220 | * Calculate the HMAC-MD5, of a key and some data
221 | */
222 | var core_hmac_md5 = function (key, data) {
223 | var bkey = str2binl(key);
224 | if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); }
225 |
226 | var ipad = new Array(16), opad = new Array(16);
227 | for(var i = 0; i < 16; i++)
228 | {
229 | ipad[i] = bkey[i] ^ 0x36363636;
230 | opad[i] = bkey[i] ^ 0x5C5C5C5C;
231 | }
232 |
233 | var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
234 | return core_md5(opad.concat(hash), 512 + 128);
235 | };
236 |
237 | var obj = {
238 | /*
239 | * These are the functions you'll usually want to call.
240 | * They take string arguments and return either hex or base-64 encoded
241 | * strings.
242 | */
243 | hexdigest: function (s) {
244 | return binl2hex(core_md5(str2binl(s), s.length * chrsz));
245 | },
246 |
247 | b64digest: function (s) {
248 | return binl2b64(core_md5(str2binl(s), s.length * chrsz));
249 | },
250 |
251 | hash: function (s) {
252 | return binl2str(core_md5(str2binl(s), s.length * chrsz));
253 | },
254 |
255 | hmac_hexdigest: function (key, data) {
256 | return binl2hex(core_hmac_md5(key, data));
257 | },
258 |
259 | hmac_b64digest: function (key, data) {
260 | return binl2b64(core_hmac_md5(key, data));
261 | },
262 |
263 | hmac_hash: function (key, data) {
264 | return binl2str(core_hmac_md5(key, data));
265 | },
266 |
267 | /*
268 | * Perform a simple self-test to see if the VM is working
269 | */
270 | test: function () {
271 | return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72";
272 | }
273 | };
274 |
275 | return obj;
276 | })();
277 |
--------------------------------------------------------------------------------
/src/sha1.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
3 | * in FIPS PUB 180-1
4 | * Version 2.1a Copyright Paul Johnston 2000 - 2002.
5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
6 | * Distributed under the BSD License
7 | * See http://pajhome.org.uk/crypt/md5 for details.
8 | */
9 |
10 | /*
11 | * Configurable variables. You may need to tweak these to be compatible with
12 | * the server-side, but the defaults work in most cases.
13 | */
14 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
15 | var b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance */
16 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
17 |
18 | /*
19 | * These are the functions you'll usually want to call
20 | * They take string arguments and return either hex or base-64 encoded strings
21 | */
22 | function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
23 | function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
24 | function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
25 | function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
26 | function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
27 | function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}
28 |
29 | /*
30 | * Perform a simple self-test to see if the VM is working
31 | */
32 | function sha1_vm_test()
33 | {
34 | return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
35 | }
36 |
37 | /*
38 | * Calculate the SHA-1 of an array of big-endian words, and a bit length
39 | */
40 | function core_sha1(x, len)
41 | {
42 | /* append padding */
43 | x[len >> 5] |= 0x80 << (24 - len % 32);
44 | x[((len + 64 >> 9) << 4) + 15] = len;
45 |
46 | var w = new Array(80);
47 | var a = 1732584193;
48 | var b = -271733879;
49 | var c = -1732584194;
50 | var d = 271733878;
51 | var e = -1009589776;
52 |
53 | var i, j, t, olda, oldb, oldc, oldd, olde;
54 | for (i = 0; i < x.length; i += 16)
55 | {
56 | olda = a;
57 | oldb = b;
58 | oldc = c;
59 | oldd = d;
60 | olde = e;
61 |
62 | for (j = 0; j < 80; j++)
63 | {
64 | if (j < 16) { w[j] = x[i + j]; }
65 | else { w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); }
66 | t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
67 | safe_add(safe_add(e, w[j]), sha1_kt(j)));
68 | e = d;
69 | d = c;
70 | c = rol(b, 30);
71 | b = a;
72 | a = t;
73 | }
74 |
75 | a = safe_add(a, olda);
76 | b = safe_add(b, oldb);
77 | c = safe_add(c, oldc);
78 | d = safe_add(d, oldd);
79 | e = safe_add(e, olde);
80 | }
81 | return [a, b, c, d, e];
82 | }
83 |
84 | /*
85 | * Perform the appropriate triplet combination function for the current
86 | * iteration
87 | */
88 | function sha1_ft(t, b, c, d)
89 | {
90 | if (t < 20) { return (b & c) | ((~b) & d); }
91 | if (t < 40) { return b ^ c ^ d; }
92 | if (t < 60) { return (b & c) | (b & d) | (c & d); }
93 | return b ^ c ^ d;
94 | }
95 |
96 | /*
97 | * Determine the appropriate additive constant for the current iteration
98 | */
99 | function sha1_kt(t)
100 | {
101 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 :
102 | (t < 60) ? -1894007588 : -899497514;
103 | }
104 |
105 | /*
106 | * Calculate the HMAC-SHA1 of a key and some data
107 | */
108 | function core_hmac_sha1(key, data)
109 | {
110 | var bkey = str2binb(key);
111 | if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * chrsz); }
112 |
113 | var ipad = new Array(16), opad = new Array(16);
114 | for (var i = 0; i < 16; i++)
115 | {
116 | ipad[i] = bkey[i] ^ 0x36363636;
117 | opad[i] = bkey[i] ^ 0x5C5C5C5C;
118 | }
119 |
120 | var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
121 | return core_sha1(opad.concat(hash), 512 + 160);
122 | }
123 |
124 | /*
125 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally
126 | * to work around bugs in some JS interpreters.
127 | */
128 | function safe_add(x, y)
129 | {
130 | var lsw = (x & 0xFFFF) + (y & 0xFFFF);
131 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
132 | return (msw << 16) | (lsw & 0xFFFF);
133 | }
134 |
135 | /*
136 | * Bitwise rotate a 32-bit number to the left.
137 | */
138 | function rol(num, cnt)
139 | {
140 | return (num << cnt) | (num >>> (32 - cnt));
141 | }
142 |
143 | /*
144 | * Convert an 8-bit or 16-bit string to an array of big-endian words
145 | * In 8-bit function, characters >255 have their hi-byte silently ignored.
146 | */
147 | function str2binb(str)
148 | {
149 | var bin = [];
150 | var mask = (1 << chrsz) - 1;
151 | for (var i = 0; i < str.length * chrsz; i += chrsz)
152 | {
153 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
154 | }
155 | return bin;
156 | }
157 |
158 | /*
159 | * Convert an array of big-endian words to a string
160 | */
161 | function binb2str(bin)
162 | {
163 | var str = "";
164 | var mask = (1 << chrsz) - 1;
165 | for (var i = 0; i < bin.length * 32; i += chrsz)
166 | {
167 | str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
168 | }
169 | return str;
170 | }
171 |
172 | /*
173 | * Convert an array of big-endian words to a hex string.
174 | */
175 | function binb2hex(binarray)
176 | {
177 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
178 | var str = "";
179 | for (var i = 0; i < binarray.length * 4; i++)
180 | {
181 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
182 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
183 | }
184 | return str;
185 | }
186 |
187 | /*
188 | * Convert an array of big-endian words to a base-64 string
189 | */
190 | function binb2b64(binarray)
191 | {
192 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
193 | var str = "";
194 | var triplet, j;
195 | for (var i = 0; i < binarray.length * 4; i += 3)
196 | {
197 | triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) |
198 | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) |
199 | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
200 | for (j = 0; j < 4; j++)
201 | {
202 | if (i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
203 | else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
204 | }
205 | }
206 | return str;
207 | }
208 |
--------------------------------------------------------------------------------
/tests/muc.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 | QUnit tests for Strophe
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/tests/muc.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | Strophe.Test = {
4 | BOSH_URL: "/xmpp-httpbind",
5 | XMPP_DOMAIN: 'speeqe.com',
6 | room_name: 'speeqers@chat.speeqe.com',
7 | connection: null, //connection object created in run function
8 |
9 | run: function() {
10 | $(document).ready(function(){
11 | //Connect strophe, uses localhost to test
12 | Strophe.Test.connection =
13 | new Strophe.Connection(Strophe.Test.BOSH_URL);
14 |
15 | //connect anonymously to run most tests
16 | Strophe.Test.connection.connect(Strophe.Test.XMPP_DOMAIN,
17 | null,
18 | Strophe.Test.connectCallback);
19 |
20 | //set up the test client UI
21 | $("#disconnect").click(function() {
22 | Strophe.Test.connection.disconnect();
23 | });
24 | $("#run_tests").click(function() {
25 | test("Anonymous connection test.", function() {
26 | if(Strophe.Test.connection.connected)
27 | {
28 | ok( true, "all good");
29 | }
30 | else
31 | {
32 | ok( false, "not connected anonymously");
33 | }
34 | });
35 | test("join a room test",function() {
36 | Strophe.Test.connection.muc.join(Strophe.Test.room_name,
37 | "testnick",
38 | function(msg) {
39 | $('#muc_item').append($(msg).text());
40 | },
41 | function(pres) {
42 | $('#muc_item').append($(pres).text());
43 | });
44 | ok(true,
45 | "joined " + Strophe.Test.room_name);
46 |
47 | });
48 | test("send a message", function() {
49 | Strophe.Test.connection.muc.message(Strophe.Test.room_name,
50 | "testnick",
51 | "test message");
52 | });
53 | test("configure room", function() {
54 | Strophe.Test
55 | .connection.muc.configure(Strophe.Test.room_name);
56 | Strophe.Test
57 | .connection.muc.cancelConfigure(Strophe.Test.room_name);
58 | });
59 | test("leave a room test", function() {
60 | var iqid = Strophe.Test
61 | .connection.muc.leave(Strophe.Test.room_name,
62 | "testnick",
63 | function() {
64 | $('#muc_item').append("left room "+
65 | Strophe.Test.room_name);
66 | });
67 | if(iqid)
68 | ok(true,
69 | "left room");
70 | });
71 | });
72 | });
73 | },
74 |
75 | connectCallback: function(status,cond) {
76 | var error_message = null;
77 | if(status == Strophe.Status.CONNECTED)
78 | {
79 | $('#run_tests').show();
80 | $('#disconnect').show();
81 | var bare_jid =
82 | Strophe.getBareJidFromJid(Strophe.Test.connection.jid)
83 | .split("@")[0];
84 | }
85 | else if (status == Strophe.Status.DISCONNECTED || status == Strophe.Status.DICONNECTING)
86 | {
87 | $('#run_tests').hide();
88 | $('#disconnect').hide();
89 | }
90 | else if ((status == 0) || (status == Strophe.Status.CONNFAIL))
91 | {
92 | error_message = "Failed to connect to xmpp server.";
93 | }
94 | else if (status == Strophe.Status.AUTHFAIL)
95 | {
96 | error_message = "Failed to authenticate to xmpp server.";
97 | }
98 | if(error_message)
99 | {
100 | $('muc_item').text(error_message);
101 |
102 | }
103 | }
104 | };
105 |
--------------------------------------------------------------------------------
/tests/pubsub.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 | QUnit tests for Strophe
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/tests/pubsub.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | Strophe.Test = {
4 | BOSH_URL: "/xmpp-httpbind",
5 | XMPP_DOMAIN: 'localhost',
6 | PUBSUB_COMPONENT: "pubsub.localhost",
7 | _node_name: "", //node name created in connectCallback function
8 | connection: null, //connection object created in run function
9 |
10 | run: function() {
11 | $(document).ready(function(){
12 | //Connect strophe, uses localhost to test
13 | Strophe.Test.connection = new Strophe.Connection(Strophe.Test.BOSH_URL);
14 |
15 | //connect anonymously to run most tests
16 | Strophe.Test.connection.connect(Strophe.Test.XMPP_DOMAIN,
17 | null,
18 | Strophe.Test.connectCallback);
19 |
20 | //set up the test client UI
21 | $("#disconnect").click(function() {
22 | Strophe.Test.connection.disconnect();
23 | });
24 | $("#run_tests").click(function() {
25 | test("Anonymous connection test.", function() {
26 | if(Strophe.Test.connection.connected)
27 | {
28 | ok( true, "all good");
29 | }
30 | else
31 | {
32 | ok( false, "not connected anonymously");
33 | }
34 | });
35 |
36 | test("Create default node test.",function(){
37 | var iqid = Strophe.Test.connection.pubsub
38 | .createNode(Strophe.Test.connection.jid,
39 | Strophe.Test.PUBSUB_COMPONENT,
40 | Strophe.Test._node_name, {},
41 | function(stanza) {
42 | test("handled create node.",
43 | function() {
44 | var error = $(stanza).find("error");
45 |
46 | if(error.length == 0)
47 | {
48 | ok(true, "returned");
49 | }
50 | else
51 | {
52 | ok(false,"error creating node.");
53 | }
54 | });
55 | });
56 | ok(true,"sent create request. "+ iqid);
57 | });
58 |
59 | test("subscribe to a node",function() {
60 | var iqid = Strophe.Test.connection.pubsub
61 | .subscribe(Strophe.Test.connection.jid,
62 | Strophe.Test.PUBSUB_COMPONENT,
63 | Strophe.Test._node_name,
64 | [],
65 | function(stanza) {
66 | test("items received", function() {
67 | console.log(stanza);
68 | if($(stanza).length > 0)
69 | {
70 | ok(true,"item received.");
71 | }
72 | else
73 | {
74 | ok(false,"no items.");
75 | }
76 | });
77 | },
78 | function(stanza) {
79 | var error = $(stanza).find("error");
80 |
81 | test("handled subscribe",
82 | function() {
83 | if(error.length == 0)
84 | {
85 | ok(true,"subscribed");
86 | }
87 | else
88 | {
89 | console.log(error.get(0));
90 | ok(false,
91 | "not subscribed");
92 | }
93 |
94 | });
95 | });
96 |
97 | if(iqid)
98 | ok(true,
99 | "subscribed to " + Strophe.Test._node_name);
100 | });
101 |
102 | test("publish to a node",function() {
103 | var iqid = Strophe.Test.connection.pubsub
104 | .publish(Strophe.Test.connection.jid,
105 | Strophe.Test.PUBSUB_COMPONENT,
106 | Strophe.Test._node_name,
107 | {test:'test'},
108 | function(stanza) {
109 | var error = $(stanza).find("error");
110 |
111 | test("handled published item",
112 | function() {
113 | if(error.length == 0)
114 | {
115 | ok(true,"got item");
116 | }
117 | else
118 | {
119 | ok(false,
120 | "no item");
121 | }
122 |
123 | });
124 | });
125 |
126 | if(iqid)
127 | ok(true,
128 | "published to " + Strophe.Test._node_name);
129 |
130 | });
131 |
132 | test("subscribe to a node with options",function() {
133 | var keyword_elem = Strophe.xmlElement("field",
134 | [["var",
135 | "http://stanziq.com/search#keyword"],
136 | ["type",
137 | 'text-single'],
138 | ["label",
139 | "keyword to match"]]);
140 | var value = Strophe.xmlElement("value",[]);
141 | var text = Strophe.xmlTextNode("crazy");
142 | value.appendChild(text);
143 | keyword_elem.appendChild(value);
144 |
145 | var iqid = Strophe.Test.connection.pubsub
146 | .subscribe(Strophe.Test.connection.jid,
147 | Strophe.Test.PUBSUB_COMPONENT,
148 | Strophe.Test._node_name,
149 | [keyword_elem],
150 | function(stanza) {console.log(stanza);},
151 | function(stanza) {
152 |
153 | var error = $(stanza).find("error");
154 |
155 | test("handled subscribe with options",
156 | function() {
157 | if(error.length == 0)
158 | {
159 | ok(true,"search subscribed");
160 | }
161 | else
162 | {
163 | console.log(error.get(0));
164 | ok(false,
165 | "search not subscribed");
166 | }
167 |
168 | });
169 | });
170 |
171 | if(iqid)
172 | ok(true,
173 | "subscribed to search");
174 | });
175 |
176 | test("unsubscribe to a node",function() {
177 | var iqid = Strophe.Test.connection.pubsub
178 | .unsubscribe(Strophe.Test.connection.jid,
179 | Strophe.Test.PUBSUB_COMPONENT,
180 | Strophe.Test._node_name,
181 | function(stanza) {
182 | var error = $(stanza).find("error");
183 |
184 | test("handled unsubscribe",
185 | function() {
186 | if(error.length == 0)
187 | {
188 | ok(true,"unsubscribed");
189 | }
190 | else
191 | {
192 | console.log(error.get(0));
193 | ok(false,
194 | "unable to unsubscribed");
195 | }
196 | });
197 | });
198 |
199 | if(iqid)
200 | ok(true,
201 | "unsubscribed from search with no options.");
202 | });
203 |
204 | test("test items retrieval",function(){
205 | var itemid = Strophe.Test.connection.pubsub
206 | .items(Strophe.Test.connection.jid,
207 | Strophe.Test.PUBSUB_COMPONENT,
208 | Strophe.Test._node_name,
209 | function(stanza) {
210 | ok(true,"item request successful.");
211 | },
212 | function(stanza) {
213 | ok(false,"failed to send request.");
214 | });
215 |
216 | if(itemid)
217 | {
218 | ok(true,"item request sent.");
219 | }
220 | });
221 |
222 | test("test sendIQ interface.",function(){
223 | var sendiq_good = false;
224 | //setup timeout for sendIQ for 3 seconds
225 | setTimeout(function() {
226 | test("Timeout check", function () {
227 | ok(sendiq_good, "The iq didn't timeout.");
228 | });
229 | }, 3000);
230 |
231 | //send a pubsub subscribe stanza
232 |
233 | var sub = $iq({from:Strophe.Test.connection.jid,
234 | to:Strophe.Test.PUBSUB_COMPONENT,
235 | type:'set'})
236 | .c('pubsub', { xmlns:Strophe.NS.PUBSUB })
237 | .c('subscribe',
238 | {node:Strophe.Test._node_name,
239 | jid:Strophe.Test.connection.jid});
240 | var stanza=sub.tree();
241 | //call sendIQ with several call backs
242 | Strophe.Test.connection
243 | .sendIQ(stanza,
244 | function(stanza) {
245 | test("iq sent",function() {
246 | sendiq_good = true;
247 | ok(true,"iq sent succesfully.");
248 | });
249 | },
250 | function(stz) {
251 | test("iq fail",function() {
252 | if (stz)
253 | sendiq_good = true;
254 | console.log(stanza);
255 | ok(true,"failed to send iq.");
256 | });
257 | });
258 | });
259 |
260 | test("test sendIQ failed.",function(){
261 | var sub = $iq({from:Strophe.Test.connection.jid,
262 | to:Strophe.Test.PUBSUB_COMPONENT,
263 | type:'get'});
264 |
265 | //call sendIQ with several call backs
266 | Strophe.Test.connection
267 | .sendIQ(sub.tree(),
268 | function(stanza) {
269 | console.log(stanza);
270 | test("iq sent",function() {
271 | ok(false,
272 | "iq sent succesfully when should have failed.");
273 | });
274 | },
275 | function(stanza) {
276 | test("iq fail",function() {
277 | ok(true,
278 | "success on failure test: failed to send iq.");
279 | });
280 | });
281 | });
282 | });
283 | });
284 | },
285 |
286 | connectCallback: function(status,cond) {
287 | var error_message = null;
288 | if(status == Strophe.Status.CONNECTED)
289 | {
290 | $('#run_tests').show();
291 | $('#disconnect').show();
292 | var bare_jid = Strophe.getBareJidFromJid(Strophe.Test.connection.jid).split("@")[0];
293 | Strophe.Test._node_name = "/home/"+Strophe.Test.XMPP_DOMAIN+"/"+bare_jid;
294 | }
295 | else if (status == Strophe.Status.DISCONNECTED || status == Strophe.Status.DICONNECTING)
296 | {
297 | $('#run_tests').hide();
298 | $('#disconnect').hide();
299 | }
300 | else if ((status == 0) || (status == Strophe.Status.CONNFAIL))
301 | {
302 | error_message = "Failed to connect to xmpp server.";
303 | }
304 | else if (status == Strophe.Status.AUTHFAIL)
305 | {
306 | error_message = "Failed to authenticate to xmpp server.";
307 | }
308 | if(error_message)
309 | {
310 | $('published_item').text(error_message);
311 |
312 | }
313 | }
314 | };
315 |
--------------------------------------------------------------------------------
/tests/strophe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Strophe.js Tests
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/tests/testrunner.js:
--------------------------------------------------------------------------------
1 | /*
2 | * QUnit - jQuery unit testrunner
3 | *
4 | * http://docs.jquery.com/QUnit
5 | *
6 | * Copyright (c) 2008 John Resig, Jörn Zaefferer
7 | * Dual licensed under the MIT (MIT-LICENSE.txt)
8 | * and GPL (GPL-LICENSE.txt) licenses.
9 | *
10 | * $Id$
11 | */
12 |
13 | (function($) {
14 |
15 | // Tests for equality any JavaScript type and structure without unexpected results.
16 | // Discussions and reference: http://philrathe.com/articles/equiv
17 | // Test suites: http://philrathe.com/tests/equiv
18 | // Author: Philippe Rathé
19 | var equiv = function () {
20 |
21 | var innerEquiv; // the real equiv function
22 | var callers = []; // stack to decide between skip/abort functions
23 |
24 | // Determine what is o.
25 | function hoozit(o) {
26 | if (typeof o === "string") {
27 | return "string";
28 |
29 | } else if (typeof o === "boolean") {
30 | return "boolean";
31 |
32 | } else if (typeof o === "number") {
33 |
34 | if (isNaN(o)) {
35 | return "nan";
36 | } else {
37 | return "number";
38 | }
39 |
40 | } else if (typeof o === "undefined") {
41 | return "undefined";
42 |
43 | // consider: typeof null === object
44 | } else if (o === null) {
45 | return "null";
46 |
47 | // consider: typeof [] === object
48 | } else if (o instanceof Array) {
49 | return "array";
50 |
51 | // consider: typeof new Date() === object
52 | } else if (o instanceof Date) {
53 | return "date";
54 |
55 | // consider: /./ instanceof Object;
56 | // /./ instanceof RegExp;
57 | // typeof /./ === "function"; // => false in IE and Opera,
58 | // true in FF and Safari
59 | } else if (o instanceof RegExp) {
60 | return "regexp";
61 |
62 | } else if (typeof o === "object") {
63 | return "object";
64 |
65 | } else if (o instanceof Function) {
66 | return "function";
67 | }
68 | }
69 |
70 | // Call the o related callback with the given arguments.
71 | function bindCallbacks(o, callbacks, args) {
72 | var prop = hoozit(o);
73 | if (prop) {
74 | if (hoozit(callbacks[prop]) === "function") {
75 | return callbacks[prop].apply(callbacks, args);
76 | } else {
77 | return callbacks[prop]; // or undefined
78 | }
79 | }
80 | }
81 |
82 | var callbacks = function () {
83 |
84 | // for string, boolean, number and null
85 | function useStrictEquality(b, a) {
86 | return a === b;
87 | }
88 |
89 | return {
90 | "string": useStrictEquality,
91 | "boolean": useStrictEquality,
92 | "number": useStrictEquality,
93 | "null": useStrictEquality,
94 | "undefined": useStrictEquality,
95 |
96 | "nan": function (b) {
97 | return isNaN(b);
98 | },
99 |
100 | "date": function (b, a) {
101 | return hoozit(b) === "date" && a.valueOf() === b.valueOf();
102 | },
103 |
104 | "regexp": function (b, a) {
105 | return hoozit(b) === "regexp" &&
106 | a.source === b.source && // the regex itself
107 | a.global === b.global && // and its modifers (gmi) ...
108 | a.ignoreCase === b.ignoreCase &&
109 | a.multiline === b.multiline;
110 | },
111 |
112 | // - skip when the property is a method of an instance (OOP)
113 | // - abort otherwise,
114 | // initial === would have catch identical references anyway
115 | "function": function () {
116 | var caller = callers[callers.length - 1];
117 | return caller !== Object &&
118 | typeof caller !== "undefined";
119 | },
120 |
121 | "array": function (b, a) {
122 | var i;
123 | var len;
124 |
125 | // b could be an object literal here
126 | if ( ! (hoozit(b) === "array")) {
127 | return false;
128 | }
129 |
130 | len = a.length;
131 | if (len !== b.length) { // safe and faster
132 | return false;
133 | }
134 | for (i = 0; i < len; i++) {
135 | if( ! innerEquiv(a[i], b[i])) {
136 | return false;
137 | }
138 | }
139 | return true;
140 | },
141 |
142 | "object": function (b, a) {
143 | var i;
144 | var eq = true; // unless we can proove it
145 | var aProperties = [], bProperties = []; // collection of strings
146 |
147 | // comparing constructors is more strict than using instanceof
148 | if ( a.constructor !== b.constructor) {
149 | return false;
150 | }
151 |
152 | // stack constructor before traversing properties
153 | callers.push(a.constructor);
154 |
155 | for (i in a) { // be strict: don't ensures hasOwnProperty and go deep
156 |
157 | aProperties.push(i); // collect a's properties
158 |
159 | if ( ! innerEquiv(a[i], b[i])) {
160 | eq = false;
161 | }
162 | }
163 |
164 | callers.pop(); // unstack, we are done
165 |
166 | for (i in b) {
167 | bProperties.push(i); // collect b's properties
168 | }
169 |
170 | // Ensures identical properties name
171 | return eq && innerEquiv(aProperties.sort(), bProperties.sort());
172 | }
173 | };
174 | }();
175 |
176 | innerEquiv = function () { // can take multiple arguments
177 | var args = Array.prototype.slice.apply(arguments);
178 | if (args.length < 2) {
179 | return true; // end transition
180 | }
181 |
182 | return (function (a, b) {
183 | if (a === b) {
184 | return true; // catch the most you can
185 |
186 | } else if (typeof a !== typeof b || a === null || b === null || typeof a === "undefined" || typeof b === "undefined") {
187 | return false; // don't lose time with error prone cases
188 |
189 | } else {
190 | return bindCallbacks(a, callbacks, [b, a]);
191 | }
192 |
193 | // apply transition with (1..n) arguments
194 | })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1));
195 | };
196 |
197 | return innerEquiv;
198 | }(); // equiv
199 |
200 | var config = {
201 | stats: {
202 | all: 0,
203 | bad: 0
204 | },
205 | queue: [],
206 | // block until document ready
207 | blocking: true,
208 | //restrict modules/tests by get parameters
209 | filters: location.search.length > 1 && $.map( location.search.slice(1).split('&'), decodeURIComponent ),
210 | isLocal: !!(window.location.protocol == 'file:')
211 | };
212 |
213 | // public API as global methods
214 | $.extend(window, {
215 | test: test,
216 | module: module,
217 | expect: expect,
218 | ok: ok,
219 | equals: equals,
220 | start: start,
221 | stop: stop,
222 | reset: reset,
223 | isLocal: config.isLocal,
224 | same: function(a, b, message) {
225 | push(equiv(a, b), a, b, message);
226 | },
227 | QUnit: {
228 | equiv: equiv
229 | },
230 | // legacy methods below
231 | isSet: isSet,
232 | isObj: isObj,
233 | compare: function() {
234 | throw "compare is deprecated - use same() instead";
235 | },
236 | compare2: function() {
237 | throw "compare2 is deprecated - use same() instead";
238 | },
239 | serialArray: function() {
240 | throw "serialArray is deprecated - use jsDump.parse() instead";
241 | },
242 | q: q,
243 | t: t,
244 | url: url,
245 | triggerEvent: triggerEvent
246 | });
247 |
248 | $(window).load(function() {
249 | $('#userAgent').html(navigator.userAgent);
250 | var head = $('').insertAfter("#userAgent");
251 | $('').attr("disabled", true).prependTo(head).click(function() {
252 | $('li.pass')[this.checked ? 'hide' : 'show']();
253 | });
254 | runTest();
255 | });
256 |
257 | function synchronize(callback) {
258 | config.queue.push(callback);
259 | if(!config.blocking) {
260 | process();
261 | }
262 | }
263 |
264 | function process() {
265 | while(config.queue.length && !config.blocking) {
266 | config.queue.shift()();
267 | }
268 | }
269 |
270 | function stop(timeout) {
271 | config.blocking = true;
272 | if (timeout)
273 | config.timeout = setTimeout(function() {
274 | ok( false, "Test timed out" );
275 | start();
276 | }, timeout);
277 | }
278 | function start() {
279 | // A slight delay, to avoid any current callbacks
280 | setTimeout(function() {
281 | if(config.timeout)
282 | clearTimeout(config.timeout);
283 | config.blocking = false;
284 | process();
285 | }, 13);
286 | }
287 |
288 | function validTest( name ) {
289 | var filters = config.filters;
290 | if( !filters )
291 | return true;
292 |
293 | var i = filters.length,
294 | run = false;
295 | while( i-- ){
296 | var filter = filters[i],
297 | not = filter.charAt(0) == '!';
298 | if( not )
299 | filter = filter.slice(1);
300 | if( name.indexOf(filter) != -1 )
301 | return !not;
302 | if( not )
303 | run = true;
304 | }
305 | return run;
306 | }
307 |
308 | function runTest() {
309 | config.blocking = false;
310 | var started = +new Date;
311 | config.fixture = document.getElementById('main').innerHTML;
312 | config.ajaxSettings = $.ajaxSettings;
313 | synchronize(function() {
314 | $('').html(['Tests completed in ',
315 | +new Date - started, ' milliseconds.
',
316 | '', config.stats.bad, ' tests of ', config.stats.all, ' failed.
']
317 | .join(''))
318 | .appendTo("body");
319 | $("#banner").addClass(config.stats.bad ? "fail" : "pass");
320 | });
321 | }
322 |
323 | function test(name, callback) {
324 | if(config.currentModule)
325 | name = config.currentModule + " module: " + name;
326 | var lifecycle = $.extend({
327 | setup: function() {},
328 | teardown: function() {}
329 | }, config.moduleLifecycle);
330 |
331 | if ( !validTest(name) )
332 | return;
333 |
334 | synchronize(function() {
335 | config.assertions = [];
336 | config.expected = null;
337 | try {
338 | lifecycle.setup();
339 | callback();
340 | lifecycle.teardown();
341 | } catch(e) {
342 | if( typeof console != "undefined" && console.error && console.warn ) {
343 | console.error("Test " + name + " died, exception and test follows");
344 | console.error(e);
345 | console.warn(callback.toString());
346 | }
347 | config.assertions.push( {
348 | result: false,
349 | message: "Died on test #" + (config.assertions.length + 1) + ": " + e.message
350 | });
351 | }
352 | });
353 | synchronize(function() {
354 | try {
355 | reset();
356 | } catch(e) {
357 | if( typeof console != "undefined" && console.error && console.warn ) {
358 | console.error("reset() failed, following Test " + name + ", exception and reset fn follows");
359 | console.error(e);
360 | console.warn(reset.toString());
361 | }
362 | }
363 |
364 | if(config.expected && config.expected != config.assertions.length) {
365 | config.assertions.push({
366 | result: false,
367 | message: "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run"
368 | });
369 | }
370 |
371 | var good = 0, bad = 0;
372 | var ol = $("
").hide();
373 | config.stats.all += config.assertions.length;
374 | for ( var i = 0; i < config.assertions.length; i++ ) {
375 | var assertion = config.assertions[i];
376 | $("").addClass(assertion.result ? "pass" : "fail").text(assertion.message || "(no message)").appendTo(ol);
377 | assertion.result ? good++ : bad++;
378 | }
379 | config.stats.bad += bad;
380 |
381 | var b = $("").html(name + " (" + bad + ", " + good + ", " + config.assertions.length + ")")
382 | .click(function(){
383 | $(this).next().toggle();
384 | })
385 | .dblclick(function(event) {
386 | var target = $(event.target).filter("strong").clone();
387 | if ( target.length ) {
388 | target.children().remove();
389 | location.href = location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent($.trim(target.text()));
390 | }
391 | });
392 |
393 | $("").addClass(bad ? "fail" : "pass").append(b).append(ol).appendTo("#tests");
394 |
395 | if(bad) {
396 | $("#filter").attr("disabled", null);
397 | }
398 | });
399 | }
400 |
401 | // call on start of module test to prepend name to all tests
402 | function module(name, lifecycle) {
403 | config.currentModule = name;
404 | config.moduleLifecycle = lifecycle;
405 | }
406 |
407 | /**
408 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
409 | */
410 | function expect(asserts) {
411 | config.expected = asserts;
412 | }
413 |
414 | /**
415 | * Resets the test setup. Useful for tests that modify the DOM.
416 | */
417 | function reset() {
418 | $("#main").html( config.fixture );
419 | $.event.global = {};
420 | $.ajaxSettings = $.extend({}, config.ajaxSettings);
421 | }
422 |
423 | /**
424 | * Asserts true.
425 | * @example ok( $("a").size() > 5, "There must be at least 5 anchors" );
426 | */
427 | function ok(a, msg) {
428 | config.assertions.push({
429 | result: !!a,
430 | message: msg
431 | });
432 | }
433 |
434 | /**
435 | * Asserts that two arrays are the same
436 | */
437 | function isSet(a, b, msg) {
438 | function serialArray( a ) {
439 | var r = [];
440 |
441 | if ( a && a.length )
442 | for ( var i = 0; i < a.length; i++ ) {
443 | var str = a[i].nodeName;
444 | if ( str ) {
445 | str = str.toLowerCase();
446 | if ( a[i].id )
447 | str += "#" + a[i].id;
448 | } else
449 | str = a[i];
450 | r.push( str );
451 | }
452 |
453 | return "[ " + r.join(", ") + " ]";
454 | }
455 | var ret = true;
456 | if ( a && b && a.length != undefined && a.length == b.length ) {
457 | for ( var i = 0; i < a.length; i++ )
458 | if ( a[i] != b[i] )
459 | ret = false;
460 | } else
461 | ret = false;
462 | config.assertions.push({
463 | result: ret,
464 | message: !ret ? (msg + " expected: " + serialArray(b) + " result: " + serialArray(a)) : msg
465 | });
466 | }
467 |
468 | /**
469 | * Asserts that two objects are equivalent
470 | */
471 | function isObj(a, b, msg) {
472 | var ret = true;
473 |
474 | if ( a && b ) {
475 | for ( var i in a )
476 | if ( a[i] != b[i] )
477 | ret = false;
478 |
479 | for ( i in b )
480 | if ( a[i] != b[i] )
481 | ret = false;
482 | } else
483 | ret = false;
484 |
485 | config.assertions.push({
486 | result: ret,
487 | message: msg
488 | });
489 | }
490 |
491 | /**
492 | * Returns an array of elements with the given IDs, eg.
493 | * @example q("main", "foo", "bar")
494 | * @result [,
, ]
495 | */
496 | function q() {
497 | var r = [];
498 | for ( var i = 0; i < arguments.length; i++ )
499 | r.push( document.getElementById( arguments[i] ) );
500 | return r;
501 | }
502 |
503 | /**
504 | * Asserts that a select matches the given IDs
505 | * @example t("Check for something", "//[a]", ["foo", "baar"]);
506 | * @result returns true if "//[a]" return two elements with the IDs 'foo' and 'baar'
507 | */
508 | function t(a,b,c) {
509 | var f = $(b);
510 | var s = "";
511 | for ( var i = 0; i < f.length; i++ )
512 | s += (s && ",") + '"' + f[i].id + '"';
513 | isSet(f, q.apply(q,c), a + " (" + b + ")");
514 | }
515 |
516 | /**
517 | * Add random number to url to stop IE from caching
518 | *
519 | * @example url("data/test.html")
520 | * @result "data/test.html?10538358428943"
521 | *
522 | * @example url("data/test.php?foo=bar")
523 | * @result "data/test.php?foo=bar&10538358345554"
524 | */
525 | function url(value) {
526 | return value + (/\?/.test(value) ? "&" : "?") + new Date().getTime() + "" + parseInt(Math.random()*100000);
527 | }
528 |
529 | /**
530 | * Checks that the first two arguments are equal, with an optional message.
531 | * Prints out both actual and expected values.
532 | *
533 | * Prefered to ok( actual == expected, message )
534 | *
535 | * @example equals( $.format("Received {0} bytes.", 2), "Received 2 bytes." );
536 | *
537 | * @param Object actual
538 | * @param Object expected
539 | * @param String message (optional)
540 | */
541 | function equals(actual, expected, message) {
542 | push(expected == actual, actual, expected, message);
543 | }
544 |
545 | function push(result, actual, expected, message) {
546 | message = message || (result ? "okay" : "failed");
547 | config.assertions.push({
548 | result: result,
549 | message: result ? message + ": " + expected : message + ", expected: " + jsDump.parse(expected) + " result: " + jsDump.parse(actual)
550 | });
551 | }
552 |
553 | /**
554 | * Trigger an event on an element.
555 | *
556 | * @example triggerEvent( document.body, "click" );
557 | *
558 | * @param DOMElement elem
559 | * @param String type
560 | */
561 | function triggerEvent( elem, type, event ) {
562 | if ( $.browser.mozilla || $.browser.opera ) {
563 | event = document.createEvent("MouseEvents");
564 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
565 | 0, 0, 0, 0, 0, false, false, false, false, 0, null);
566 | elem.dispatchEvent( event );
567 | } else if ( $.browser.msie ) {
568 | elem.fireEvent("on"+type);
569 | }
570 | }
571 |
572 | })(jQuery);
573 |
574 | /**
575 | * jsDump
576 | * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
577 | * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
578 | * Date: 5/15/2008
579 | * @projectDescription Advanced and extensible data dumping for Javascript.
580 | * @version 1.0.0
581 | * @author Ariel Flesler
582 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
583 | */
584 | (function(){
585 | function quote( str ){
586 | return '"' + str.toString().replace(/"/g, '\\"') + '"';
587 | };
588 | function literal( o ){
589 | return o + '';
590 | };
591 | function join( pre, arr, post ){
592 | var s = jsDump.separator(),
593 | base = jsDump.indent();
594 | inner = jsDump.indent(1);
595 | if( arr.join )
596 | arr = arr.join( ',' + s + inner );
597 | if( !arr )
598 | return pre + post;
599 | return [ pre, inner + arr, base + post ].join(s);
600 | };
601 | function array( arr ){
602 | var i = arr.length, ret = Array(i);
603 | this.up();
604 | while( i-- )
605 | ret[i] = this.parse( arr[i] );
606 | this.down();
607 | return join( '[', ret, ']' );
608 | };
609 |
610 | var reName = /^function (\w+)/;
611 |
612 | var jsDump = window.jsDump = {
613 | parse:function( obj, type ){//type is used mostly internally, you can fix a (custom)type in advance
614 | var parser = this.parsers[ type || this.typeOf(obj) ];
615 | type = typeof parser;
616 |
617 | return type == 'function' ? parser.call( this, obj ) :
618 | type == 'string' ? parser :
619 | this.parsers.error;
620 | },
621 | typeOf:function( obj ){
622 | var type = typeof obj,
623 | f = 'function';//we'll use it 3 times, save it
624 | return type != 'object' && type != f ? type :
625 | !obj ? 'null' :
626 | obj.exec ? 'regexp' :// some browsers (FF) consider regexps functions
627 | obj.getHours ? 'date' :
628 | obj.scrollBy ? 'window' :
629 | obj.nodeName == '#document' ? 'document' :
630 | obj.nodeName ? 'node' :
631 | obj.item ? 'nodelist' : // Safari reports nodelists as functions
632 | obj.callee ? 'arguments' :
633 | obj.call || obj.constructor != Array && //an array would also fall on this hack
634 | (obj+'').indexOf(f) != -1 ? f : //IE reports functions like alert, as objects
635 | 'length' in obj ? 'array' :
636 | type;
637 | },
638 | separator:function(){
639 | return this.multiline ? this.HTML ? '
' : '\n' : this.HTML ? ' ' : ' ';
640 | },
641 | indent:function( extra ){// extra can be a number, shortcut for increasing-calling-decreasing
642 | if( !this.multiline )
643 | return '';
644 | var chr = this.indentChar;
645 | if( this.HTML )
646 | chr = chr.replace(/\t/g,' ').replace(/ /g,' ');
647 | return Array( this._depth_ + (extra||0) ).join(chr);
648 | },
649 | up:function( a ){
650 | this._depth_ += a || 1;
651 | },
652 | down:function( a ){
653 | this._depth_ -= a || 1;
654 | },
655 | setParser:function( name, parser ){
656 | this.parsers[name] = parser;
657 | },
658 | // The next 3 are exposed so you can use them
659 | quote:quote,
660 | literal:literal,
661 | join:join,
662 | //
663 | _depth_: 1,
664 | // This is the list of parsers, to modify them, use jsDump.setParser
665 | parsers:{
666 | window: '[Window]',
667 | document: '[Document]',
668 | error:'[ERROR]', //when no parser is found, shouldn't happen
669 | unknown: '[Unknown]',
670 | 'null':'null',
671 | undefined:'undefined',
672 | 'function':function( fn ){
673 | var ret = 'function',
674 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
675 | if( name )
676 | ret += ' ' + name;
677 | ret += '(';
678 |
679 | ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join('');
680 | return join( ret, this.parse(fn,'functionCode'), '}' );
681 | },
682 | array: array,
683 | nodelist: array,
684 | arguments: array,
685 | object:function( map ){
686 | var ret = [ ];
687 | this.up();
688 | for( var key in map )
689 | ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) );
690 | this.down();
691 | return join( '{', ret, '}' );
692 | },
693 | node:function( node ){
694 | var open = this.HTML ? '<' : '<',
695 | close = this.HTML ? '>' : '>';
696 |
697 | var tag = node.nodeName.toLowerCase(),
698 | ret = open + tag;
699 |
700 | for( var a in this.DOMAttrs ){
701 | var val = node[this.DOMAttrs[a]];
702 | if( val )
703 | ret += ' ' + a + '=' + this.parse( val, 'attribute' );
704 | }
705 | return ret + close + open + '/' + tag + close;
706 | },
707 | functionArgs:function( fn ){//function calls it internally, it's the arguments part of the function
708 | var l = fn.length;
709 | if( !l ) return '';
710 |
711 | var args = Array(l);
712 | while( l-- )
713 | args[l] = String.fromCharCode(97+l);//97 is 'a'
714 | return ' ' + args.join(', ') + ' ';
715 | },
716 | key:quote, //object calls it internally, the key part of an item in a map
717 | functionCode:'[code]', //function calls it internally, it's the content of the function
718 | attribute:quote, //node calls it internally, it's an html attribute value
719 | string:quote,
720 | date:quote,
721 | regexp:literal, //regex
722 | number:literal,
723 | 'boolean':literal
724 | },
725 | DOMAttrs:{//attributes to dump from nodes, name=>realName
726 | id:'id',
727 | name:'name',
728 | 'class':'className'
729 | },
730 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
731 | indentChar:' ',//indentation unit
732 | multiline:true //if true, items in a collection, are separated by a \n, else just a space.
733 | };
734 |
735 | })();
736 |
--------------------------------------------------------------------------------
/tests/tests.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 | module("JIDs");
3 |
4 | test("Normal JID", function () {
5 | var jid = "darcy@pemberley.lit/library";
6 | equal(Strophe.getNodeFromJid(jid), "darcy",
7 | "Node should be 'darcy'");
8 | equal(Strophe.getDomainFromJid(jid), "pemberley.lit",
9 | "Domain should be 'pemberley.lit'");
10 | equal(Strophe.getResourceFromJid(jid), "library",
11 | "Node should be 'library'");
12 | equal(Strophe.getBareJidFromJid(jid),
13 | "darcy@pemberley.lit",
14 | "Bare JID should be 'darcy@pemberley.lit'");
15 | });
16 |
17 | test("Weird node (unescaped)", function () {
18 | var jid = "darcy@netherfield.lit@pemberley.lit/library";
19 | equal(Strophe.getNodeFromJid(jid), "darcy",
20 | "Node should be 'darcy'");
21 | equal(Strophe.getDomainFromJid(jid),
22 | "netherfield.lit@pemberley.lit",
23 | "Domain should be 'netherfield.lit@pemberley.lit'");
24 | equal(Strophe.getResourceFromJid(jid), "library",
25 | "Resource should be 'library'");
26 | equal(Strophe.getBareJidFromJid(jid),
27 | "darcy@netherfield.lit@pemberley.lit",
28 | "Bare JID should be 'darcy@netherfield.lit@pemberley.lit'");
29 | });
30 |
31 | test("Weird node (escaped)", function () {
32 | var escapedNode = Strophe.escapeNode("darcy@netherfield.lit");
33 | var jid = escapedNode + "@pemberley.lit/library";
34 | equal(Strophe.getNodeFromJid(jid), "darcy\\40netherfield.lit",
35 | "Node should be 'darcy\\40netherfield.lit'");
36 | equal(Strophe.getDomainFromJid(jid),
37 | "pemberley.lit",
38 | "Domain should be 'pemberley.lit'");
39 | equal(Strophe.getResourceFromJid(jid), "library",
40 | "Resource should be 'library'");
41 | equal(Strophe.getBareJidFromJid(jid),
42 | "darcy\\40netherfield.lit@pemberley.lit",
43 | "Bare JID should be 'darcy\\40netherfield.lit@pemberley.lit'");
44 | });
45 |
46 | test("Weird resource", function () {
47 | var jid = "books@chat.pemberley.lit/darcy@pemberley.lit/library";
48 | equal(Strophe.getNodeFromJid(jid), "books",
49 | "Node should be 'books'");
50 | equal(Strophe.getDomainFromJid(jid), "chat.pemberley.lit",
51 | "Domain should be 'chat.pemberley.lit'");
52 | equal(Strophe.getResourceFromJid(jid),
53 | "darcy@pemberley.lit/library",
54 | "Resource should be 'darcy@pemberley.lit/library'");
55 | equal(Strophe.getBareJidFromJid(jid),
56 | "books@chat.pemberley.lit",
57 | "Bare JID should be 'books@chat.pemberley.lit'");
58 | });
59 |
60 | module("Builder");
61 |
62 | test("Correct namespace (#32)", function () {
63 | var stanzas = [new Strophe.Builder("message", {foo: "asdf"}).tree(),
64 | $build("iq", {}).tree(),
65 | $pres().tree()];
66 | $.each(stanzas, function () {
67 | equal($(this).attr('xmlns'), Strophe.NS.CLIENT,
68 | "Namespace should be '" + Strophe.NS.CLIENT + "'");
69 | });
70 | });
71 |
72 | test("send() accepts Builders (#27)", function () {
73 | var stanza = $pres();
74 | var conn = new Strophe.Connection("");
75 | // fake connection callback to avoid errors
76 | conn.connect_callback = function () {};
77 |
78 | ok(conn._data.length === 0, "Output queue is clean");
79 | try {
80 | conn.send(stanza);
81 | } catch (e) {}
82 | ok(conn._data.length === 1, "Output queue contains an element");
83 | });
84 |
85 | test("send() does not accept strings", function () {
86 | var stanza = "";
87 | var conn = new Strophe.Connection("");
88 | // fake connection callback to avoid errors
89 | conn.connect_callback = function () {};
90 | expect(1);
91 | try {
92 | conn.send(stanza);
93 | } catch (e) {
94 | equal(e.name, "StropheError", "send() should throw exception");
95 | }
96 | });
97 |
98 | test("Builder with XML attribute escaping test", function () {
99 | var text = "";
100 | var expected = "";
101 | var pres = $pres({to: text});
102 | equal(pres.toString(), expected, "< should be escaped");
103 |
104 | text = "foo&bar";
105 | expected = "";
106 | pres = $pres({to: text});
107 | equal(pres.toString(), expected, "& should be escaped");
108 | });
109 |
110 | test("c() accepts text and passes it to xmlElement", function () {
111 | var pres = $pres({from: "darcy@pemberley.lit", to: "books@chat.pemberley.lit"})
112 | .c("nick", {xmlns: "http://jabber.org/protocol/nick"}, "Darcy");
113 | var expected = "Darcy";
114 | equal(pres.toString(), expected, "'Darcy' should be a child of ");
115 | });
116 |
117 | module("XML");
118 |
119 | test("XML escaping test", function () {
120 | var text = "s & p";
121 | var textNode = Strophe.xmlTextNode(text);
122 | equal(Strophe.getText(textNode), "s & p", "should be escaped");
123 | var text0 = "s < & > p";
124 | var textNode0 = Strophe.xmlTextNode(text0);
125 | equal(Strophe.getText(textNode0), "s < & > p", "should be escaped");
126 | var text1 = "s's or \"p\"";
127 | var textNode1 = Strophe.xmlTextNode(text1);
128 | equal(Strophe.getText(textNode1), "s's or "p"", "should be escaped");
129 | var text2 = "]]>";
130 | var textNode2 = Strophe.xmlTextNode(text2);
131 | equal(Strophe.getText(textNode2), "<![CDATA[<foo>]]>", "should be escaped");
132 | var text3 = "]]>";
133 | var textNode3 = Strophe.xmlTextNode(text3);
134 | equal(Strophe.getText(textNode3), "<![CDATA[]]]]><![CDATA[>]]>", "should be escaped");
135 | var text4 = "<foo>]]>";
136 | var textNode4 = Strophe.xmlTextNode(text4);
137 | equal(Strophe.getText(textNode4), "<foo><![CDATA[<foo>]]>", "should be escaped");
138 | });
139 |
140 | test("XML element creation", function () {
141 | var elem = Strophe.xmlElement("message");
142 | equal(elem.tagName, "message", "Element name should be the same");
143 | });
144 |
145 | test("copyElement() double escape bug", function() {
146 | var cloned = Strophe.copyElement(Strophe.xmlGenerator()
147 | .createTextNode('<><>'));
148 | equal(cloned.nodeValue, '<><>');
149 | });
150 |
151 | test("XML serializing", function() {
152 | var parser = new DOMParser();
153 | // Attributes
154 | var element1 = parser.parseFromString("bar","text/xml").documentElement;
155 | equal(Strophe.serialize(element1), "bar", "should be serialized");
156 | var element2 = parser.parseFromString("bar","text/xml").documentElement;
157 | equal(Strophe.serialize(element2), "bar", "should be serialized");
158 | // Escaping values
159 | var element3 = parser.parseFromString("a > 'b' & "b" < c","text/xml").documentElement;
160 | equal(Strophe.serialize(element3), "a > 'b' & "b" < c", "should be serialized");
161 | // Escaping attributes
162 | var element4 = parser.parseFromString("bar","text/xml").documentElement;
163 | equal(Strophe.serialize(element4), "bar", "should be serialized");
164 | var element5 = parser.parseFromString(" "b"\">bar","text/xml").documentElement;
165 | equal(Strophe.serialize(element5), "bar", "should be serialized");
166 | // Empty elements
167 | var element6 = parser.parseFromString("","text/xml").documentElement;
168 | equal(Strophe.serialize(element6), "", "should be serialized");
169 | // Children
170 | var element7 = parser.parseFromString("ab","text/xml").documentElement;
171 | equal(Strophe.serialize(element7), "ab", "should be serialized");
172 | var element8 = parser.parseFromString("abcd","text/xml").documentElement;
173 | equal(Strophe.serialize(element8), "abcd", "should be serialized");
174 | // CDATA
175 | var element9 = parser.parseFromString("]]>","text/xml").documentElement;
176 | equal(Strophe.serialize(element9), "]]>", "should be serialized");
177 | var element10 = parser.parseFromString("]]>","text/xml").documentElement;
178 | equal(Strophe.serialize(element10), "]]>", "should be serialized");
179 | var element11 = parser.parseFromString("<foo>]]>","text/xml").documentElement;
180 | equal(Strophe.serialize(element11), "<foo>]]>", "should be serialized");
181 | });
182 |
183 | module("Handler");
184 |
185 | test("Full JID matching", function () {
186 | var elem = $msg({from: 'darcy@pemberley.lit/library'}).tree();
187 |
188 | var hand = new Strophe.Handler(null, null, null, null, null,
189 | 'darcy@pemberley.lit/library');
190 | equal(hand.isMatch(elem), true, "Full JID should match");
191 |
192 | hand = new Strophe.Handler(null, null, null, null, null,
193 | 'darcy@pemberley.lit')
194 | equal(hand.isMatch(elem), false, "Bare JID shouldn't match");
195 | });
196 |
197 | test("Bare JID matching", function () {
198 | var elem = $msg({from: 'darcy@pemberley.lit/library'}).tree();
199 |
200 | var hand = new Strophe.Handler(null, null, null, null, null,
201 | 'darcy@pemberley.lit/library',
202 | {matchBare: true});
203 | equal(hand.isMatch(elem), true, "Full JID should match");
204 |
205 | hand = new Strophe.Handler(null, null, null, null, null,
206 | 'darcy@pemberley.lit',
207 | {matchBare: true});
208 | equal(hand.isMatch(elem), true, "Bare JID should match");
209 | });
210 |
211 | module("Misc");
212 |
213 | test("Quoting strings", function () {
214 | var input = '"beep \\40"';
215 | var conn = new Strophe.Connection();
216 | var output = conn._quote(input);
217 | equal(output, "\"\\\"beep \\\\40\\\"\"",
218 | "string should be quoted and escaped");
219 | });
220 |
221 | test("Function binding", function () {
222 | var spy = sinon.spy();
223 | var obj = {};
224 | var arg1 = "foo";
225 | var arg2 = "bar";
226 | var arg3 = "baz";
227 |
228 | var f = spy.bind(obj, arg1, arg2);
229 | f(arg3);
230 | equal(spy.called, true, "bound function should be called");
231 | equal(spy.calledOn(obj), true,
232 | "bound function should have correct context");
233 | equal(spy.alwaysCalledWithExactly(arg1, arg2, arg3),
234 | true,
235 | "bound function should get all arguments");
236 | });
237 |
238 | module("XHR error handling");
239 |
240 | // Note that these tests are pretty dependent on the actual code.
241 |
242 | test("Aborted requests do nothing", function () {
243 | Strophe.Connection.prototype._onIdle = function () {};
244 | var conn = new Strophe.Connection("http://fake");
245 |
246 | // simulate a finished but aborted request
247 | var req = {id: 43,
248 | sends: 1,
249 | xhr: {
250 | readyState: 4
251 | },
252 | abort: true};
253 |
254 | conn._requests = [req];
255 |
256 | var spy = sinon.spy();
257 |
258 | conn._onRequestStateChange(spy, req);
259 |
260 | equal(req.abort, false, "abort flag should be toggled");
261 | equal(conn._requests.length, 1, "_requests should be same length");
262 | equal(spy.called, false, "callback should not be called");
263 | });
264 |
265 | test("Incomplete requests do nothing", function () {
266 | Strophe.Connection.prototype._onIdle = function () {};
267 | var conn = new Strophe.Connection("http://fake");
268 |
269 | // simulate a finished but aborted request
270 | var req = {id: 44,
271 | sends: 1,
272 | xhr: {
273 | readyState: 3
274 | }};
275 |
276 | conn._requests = [req];
277 |
278 | var spy = sinon.spy();
279 |
280 | conn._onRequestStateChange(spy, req);
281 |
282 | equal(conn._requests.length, 1, "_requests should be same length");
283 | equal(spy.called, false, "callback should not be called");
284 | });
285 | });
286 |
--------------------------------------------------------------------------------
/tests/testsuite.css:
--------------------------------------------------------------------------------
1 | body, div, h1 { font-family: 'trebuchet ms', verdana, arial; margin: 0; padding: 0 }
2 | body {font-size: 10pt; }
3 | h1 { padding: 15px; font-size: large; background-color: #06b; color: white; }
4 | h1 a { color: white; }
5 | h2 { padding: 10px; background-color: #eee; color: black; margin: 0; font-size: small; font-weight: normal }
6 |
7 | .pass { color: green; }
8 | .fail { color: red; }
9 | p.result { margin-left: 1em; }
10 |
11 | #banner { height: 2em; border-bottom: 1px solid white; }
12 | h2.pass { background-color: green; }
13 | h2.fail { background-color: red; }
14 |
15 | div.testrunner-toolbar { background: #eee; border-top: 1px solid black; padding: 10px; }
16 |
17 | ol#tests > li > strong { cursor:pointer; }
18 |
19 | div#fx-tests h4 {
20 | background: red;
21 | }
22 |
23 | div#fx-tests h4.pass {
24 | background: green;
25 | }
26 |
27 | div#fx-tests div.box {
28 | background: red url(data/cow.jpg) no-repeat;
29 | overflow: hidden;
30 | border: 2px solid #000;
31 | }
32 |
33 | div#fx-tests div.overflow {
34 | overflow: visible;
35 | }
36 |
37 | div.inline {
38 | display: inline;
39 | }
40 |
41 | div.autoheight {
42 | height: auto;
43 | }
44 |
45 | div.autowidth {
46 | width: auto;
47 | }
48 |
49 | div.autoopacity {
50 | opacity: auto;
51 | }
52 |
53 | div.largewidth {
54 | width: 100px;
55 | }
56 |
57 | div.largeheight {
58 | height: 100px;
59 | }
60 |
61 | div.largeopacity {
62 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=100);
63 | }
64 |
65 | div.medwidth {
66 | width: 50px;
67 | }
68 |
69 | div.medheight {
70 | height: 50px;
71 | }
72 |
73 | div.medopacity {
74 | opacity: 0.5;
75 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=50);
76 | }
77 |
78 | div.nowidth {
79 | width: 0px;
80 | }
81 |
82 | div.noheight {
83 | height: 0px;
84 | }
85 |
86 | div.noopacity {
87 | opacity: 0;
88 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0);
89 | }
90 |
91 | div.hidden {
92 | display: none;
93 | }
94 |
95 | div#fx-tests div.widewidth {
96 | background-repeat: repeat-x;
97 | }
98 |
99 | div#fx-tests div.wideheight {
100 | background-repeat: repeat-y;
101 | }
102 |
103 | div#fx-tests div.widewidth.wideheight {
104 | background-repeat: repeat;
105 | }
106 |
107 | div#fx-tests div.noback {
108 | background-image: none;
109 | }
110 |
111 | div.chain, div.chain div { width: 100px; height: 20px; position: relative; float: left; }
112 | div.chain div { position: absolute; top: 0px; left: 0px; }
113 |
114 | div.chain.test { background: red; }
115 | div.chain.test div { background: green; }
116 |
117 | div.chain.out { background: green; }
118 | div.chain.out div { background: red; display: none; }
119 |
120 | div#show-tests * { display: none; }
121 |
--------------------------------------------------------------------------------