Merge pull request #52 from edx/nate/simple-chat
Simple port of Class2Go's chat feature
@@ -192,9 +192,8 @@ class CourseFields(object):
|
||||
}},
|
||||
scope=Scope.content)
|
||||
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
|
||||
display_name = String(
|
||||
help="Display name for this module", default="Empty",
|
||||
display_name="Display Name", scope=Scope.settings)
|
||||
display_name = String(help="Display name for this module", default="Empty", display_name="Display Name", scope=Scope.settings)
|
||||
show_chat = Boolean(help="Whether to show the chat widget in this course", default=False, scope=Scope.settings)
|
||||
tabs = List(help="List of tabs to enable in this course", scope=Scope.settings)
|
||||
end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings)
|
||||
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.http import Http404
|
||||
from django.test.utils import override_settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import RequestFactory
|
||||
from django.conf import settings
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -124,3 +125,26 @@ class ViewsTestCase(TestCase):
|
||||
self.assertContains(result, expected_end_text)
|
||||
else:
|
||||
self.assertNotContains(result, "Classes End")
|
||||
|
||||
def test_chat_settings(self):
|
||||
mock_user = MagicMock()
|
||||
mock_user.username = "johndoe"
|
||||
|
||||
mock_course = MagicMock()
|
||||
mock_course.id = "a/b/c"
|
||||
|
||||
# Stub this out in the case that it's not in the settings
|
||||
domain = "jabber.edx.org"
|
||||
settings.JABBER_DOMAIN = domain
|
||||
|
||||
chat_settings = views.chat_settings(mock_course, mock_user)
|
||||
|
||||
# Test the proper format of all chat settings
|
||||
self.assertEquals(chat_settings['domain'], domain)
|
||||
self.assertEquals(chat_settings['room'], "a-b-c_class")
|
||||
self.assertEquals(chat_settings['username'], "johndoe@%s" % domain)
|
||||
|
||||
# TODO: this needs to be changed once we figure out how to
|
||||
# generate/store a real password.
|
||||
self.assertEquals(chat_settings['password'], "johndoe@%s" % domain)
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ log = logging.getLogger("mitx.courseware")
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
|
||||
def user_groups(user):
|
||||
"""
|
||||
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
|
||||
@@ -236,6 +235,36 @@ def update_timelimit_module(user, course_id, model_data_cache, timelimit_descrip
|
||||
return context
|
||||
|
||||
|
||||
def chat_settings(course, user):
|
||||
"""
|
||||
Returns a dict containing the settings required to connect to a
|
||||
Jabber chat server and room.
|
||||
"""
|
||||
domain = getattr(settings, "JABBER_DOMAIN", None)
|
||||
if domain is None:
|
||||
log.warning('You must set JABBER_DOMAIN in the settings to '
|
||||
'enable the chat widget')
|
||||
return None
|
||||
|
||||
return {
|
||||
'domain': domain,
|
||||
|
||||
# Jabber doesn't like slashes, so replace with dashes
|
||||
'room': "{ID}_class".format(ID=course.id.replace('/', '-')),
|
||||
|
||||
'username': "{USER}@{DOMAIN}".format(
|
||||
USER=user.username, DOMAIN=domain
|
||||
),
|
||||
|
||||
# TODO: clearly this needs to be something other than the username
|
||||
# should also be something that's not necessarily tied to a
|
||||
# particular course
|
||||
'password': "{USER}@{DOMAIN}".format(
|
||||
USER=user.username, DOMAIN=domain
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@@ -300,6 +329,18 @@ def index(request, course_id, chapter=None, section=None,
|
||||
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa')
|
||||
}
|
||||
|
||||
# Only show the chat if it's enabled by the course and in the
|
||||
# settings.
|
||||
show_chat = course.show_chat and settings.MITX_FEATURES['ENABLE_CHAT']
|
||||
if show_chat:
|
||||
context['chat'] = chat_settings(course, user)
|
||||
# If we couldn't load the chat settings, then don't show
|
||||
# the widget in the courseware.
|
||||
if context['chat'] is None:
|
||||
show_chat = False
|
||||
|
||||
context['show_chat'] = show_chat
|
||||
|
||||
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
|
||||
if chapter_descriptor is not None:
|
||||
save_child_position(course_module, chapter)
|
||||
|
||||
@@ -144,6 +144,10 @@ MITX_FEATURES = {
|
||||
|
||||
# Allow use of the hint managment instructor view.
|
||||
'ENABLE_HINTER_INSTRUCTOR_VIEW': False,
|
||||
|
||||
# Toggle to enable chat availability (configured on a per-course
|
||||
# basis in Studio)
|
||||
'ENABLE_CHAT': False
|
||||
}
|
||||
|
||||
# Used for A/B testing
|
||||
|
||||
BIN
lms/static/candy_res/audioplayer.swf
Normal file
606
lms/static/candy_res/candy_full.css
Normal file
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* Chat CSS
|
||||
*
|
||||
* @author Michael <michael.weibel@gmail.com>
|
||||
* @author Patrick <patrick.stadler@gmail.com>
|
||||
* @copyright 2011 Amiado Group AG, All rights reserved.
|
||||
*/
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#candy {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
background-color: #bdb7a1;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
border: 1px solid #bdb7a1;
|
||||
border-top-right-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#candy.poppedOut {
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
#chatPopin {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
font-size: 24px;
|
||||
height: 18px !important;
|
||||
width: 22px;
|
||||
color: #666;
|
||||
}
|
||||
#chatPopin:hover {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#chat-tabs {
|
||||
margin: 0 0 0 28px;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
border-left: 1px solid #bdb7a1;
|
||||
}
|
||||
|
||||
#chat-pane.collapsed-message-pane #chat-tabs {
|
||||
height: 100%;
|
||||
width: 29px;
|
||||
border-left: 0;
|
||||
margin: 30px 0 0 0;
|
||||
}
|
||||
|
||||
#chat-tabs #chat-expand-arrow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 27px;
|
||||
height: 15px;
|
||||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
#chat-tabs #chat-expand-arrow em {
|
||||
font-size: 18px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#chat-tabs li {
|
||||
margin: 0;
|
||||
float: left;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#chat-tabs li a {
|
||||
background-color: #e9e6df;
|
||||
padding: 6px 20px 4px 1px;
|
||||
display: inline-block;
|
||||
color: #999;
|
||||
height: 19px;
|
||||
font-size: 22px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#chat-tabs li a.label {
|
||||
border-right: 1px solid #bdb7a1;
|
||||
border-bottom: 1px solid #bdb7a1;
|
||||
}
|
||||
|
||||
#chat-pane.collapsed-message-pane #chat-tabs li a.label {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
#chat-tabs li.active a {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#chat-tabs li.active a.label {
|
||||
border-bottom: 1px solid white;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#chat-tabs li a.transition {
|
||||
display: none; /* JRBL */
|
||||
}
|
||||
|
||||
#chat-tabs li a.close {
|
||||
background-color: transparent;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 7px;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #999;
|
||||
}
|
||||
#chat-tabs li a.close:hover, #chat-tabs li.active a.close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#chat-tabs li .unread {
|
||||
color: white;
|
||||
background-color: #8C1515;
|
||||
padding: 1px 2px;
|
||||
font-weight: normal;
|
||||
font-size: 10px;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#chat-tabs li.offline a.label {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
#chat-toolbar {
|
||||
/*
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
width: 200px;
|
||||
height: 24px;
|
||||
padding-top: 7px;
|
||||
border-top: 1px solid #e9e6df;
|
||||
background-color: #d3cec0;
|
||||
*/
|
||||
display: none !important;
|
||||
}
|
||||
#chat-toolbar li {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 5px;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#chat-toolbar #emoticons-icon {
|
||||
background-image: url(img/action/emoticons.png);
|
||||
}
|
||||
#chat-toolbar .context {
|
||||
background-image: url(img/action/settings.png);
|
||||
display: none;
|
||||
}
|
||||
.role-moderator #chat-toolbar .context, .affiliation-owner #chat-toolbar .context {
|
||||
display: inline-block;
|
||||
}
|
||||
#chat-sound-control {
|
||||
background-image: url(img/action/sound-off.png);
|
||||
}
|
||||
#chat-sound-control.checked {
|
||||
background-image: url(img/action/sound-on.png);
|
||||
}
|
||||
#chat-autoscroll-control {
|
||||
background-image: url(img/action/autoscroll-off.png);
|
||||
}
|
||||
#chat-autoscroll-control.checked {
|
||||
background-image: url(img/action/autoscroll-on.png);
|
||||
}
|
||||
#chat-statusmessage-control {
|
||||
background: url(img/action/statusmessage-off.png);
|
||||
}
|
||||
#chat-statusmessage-control.checked {
|
||||
background: url(img/action/statusmessage-on.png);
|
||||
}
|
||||
#chat-toolbar .usercount {
|
||||
background-image: url(img/action/usercount.png);
|
||||
cursor: default;
|
||||
padding-left: 20px;
|
||||
width: auto;
|
||||
margin-right: 5px;
|
||||
float: right;
|
||||
}
|
||||
.usercount span {
|
||||
display: inline-block;
|
||||
padding: 1px 3px;
|
||||
background-color: #ccc;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.room-pane {
|
||||
/* display: none; */
|
||||
}
|
||||
.roster-pane {
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
top: 30px;
|
||||
right: 0;
|
||||
bottom: 30px;
|
||||
width: 198px;
|
||||
margin: 0;
|
||||
}
|
||||
.roster-pane .user {
|
||||
cursor: pointer;
|
||||
width: 90%;
|
||||
padding: 6px 5%;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
/* display: none; */
|
||||
color: #666;
|
||||
float: left;
|
||||
clear: both;
|
||||
height: 17px;
|
||||
background-color: #d3cec0;
|
||||
border-bottom: 1px solid #bdb7a1;
|
||||
}
|
||||
.roster-pane .user.me {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
.roster-pane .user:hover,
|
||||
.roster-pane .user.me:hover {
|
||||
background-color: #e9e6df;
|
||||
}
|
||||
.roster-pane .user.status-ignored {
|
||||
cursor: default;
|
||||
color: #999;
|
||||
opacity: .5 !important;
|
||||
}
|
||||
.roster-pane .label {
|
||||
float: left;
|
||||
width: 125px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.roster-pane li {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
float: right;
|
||||
display: block;
|
||||
margin-left: 3px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.roster-pane li.role {
|
||||
cursor: default;
|
||||
display: none;
|
||||
}
|
||||
.roster-pane li.role-moderator {
|
||||
background-image: url(img/roster/role-moderator.png);
|
||||
display: block;
|
||||
}
|
||||
.roster-pane li.affiliation-owner {
|
||||
background-image: url(img/roster/affiliation-owner.png);
|
||||
display: block;
|
||||
}
|
||||
.roster-pane li.ignore {
|
||||
background-image: url(img/roster/ignore.png);
|
||||
display: none;
|
||||
}
|
||||
.roster-pane .status-ignored li.ignore {
|
||||
display: block;
|
||||
}
|
||||
.roster-pane .me li.context {
|
||||
display: none;
|
||||
}
|
||||
.roster-pane li.context {
|
||||
background-image: url(img/action/menu.png);
|
||||
cursor: pointer;
|
||||
}
|
||||
.roster-pane li.context:hover {
|
||||
background-color: #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message-pane-wrapper {
|
||||
clear: both;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: auto;
|
||||
width: auto;
|
||||
margin: 30px 199px 32px 0;
|
||||
background-color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
.message-pane {
|
||||
margin: 0;
|
||||
padding: 5px 10px 2px 10px;
|
||||
}
|
||||
.message-pane dt {
|
||||
width: 55px;
|
||||
float: left;
|
||||
color: #888;
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.message-pane dd {
|
||||
overflow: auto;
|
||||
padding: 2px 0 1px 100px;
|
||||
margin: 0 0 2px 0;
|
||||
white-space: -o-pre-wrap; /* Opera */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
|
||||
.message-pane dd .label {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
margin-left: -90px;
|
||||
width: 90px;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-pane .subject {
|
||||
color: #a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-pane .adminmessage {
|
||||
color: #a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-pane .infomessage {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.message-pane .emoticon {
|
||||
vertical-align: text-bottom;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.message-form-wrapper {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: auto;
|
||||
margin-right: 199px;
|
||||
background-color: #e9e6df;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.message-form {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 199px 0 0;
|
||||
padding: 0;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.message-form input {
|
||||
border: 0 none;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
outline-width: 0;
|
||||
padding: 0 50px 0 5px;
|
||||
background-color: transparent;
|
||||
}
|
||||
.message-form input:focus {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.message-form input.submit {
|
||||
cursor: pointer;
|
||||
background-color: #333;
|
||||
color: #e9e6df;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 3px;
|
||||
padding: 5px;
|
||||
width: 40px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
height: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
margin: 18px -18px 2px -2px;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
padding: 5px 0;
|
||||
background: url(img/tooltip-arrows.gif) no-repeat left bottom;
|
||||
}
|
||||
|
||||
#tooltip div {
|
||||
background-color: black;
|
||||
padding: 2px 5px;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
#context-menu {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
padding: 15px 10px;
|
||||
margin: 8px -28px -8px -12px;
|
||||
background: url(img/context-arrows.gif) no-repeat left bottom;
|
||||
}
|
||||
|
||||
#context-menu ul {
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
#context-menu li {
|
||||
padding: 3px 5px 3px 20px;
|
||||
line-height: 12px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 2px;
|
||||
background: 1px no-repeat;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#context-menu li:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
#context-menu li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#context-menu .private {
|
||||
background-image: url(img/action/private.png);
|
||||
}
|
||||
|
||||
#context-menu .ignore {
|
||||
background-image: url(img/action/ignore.png);
|
||||
}
|
||||
|
||||
#context-menu .unignore {
|
||||
background-image: url(img/action/unignore.png);
|
||||
}
|
||||
|
||||
#context-menu .kick {
|
||||
background-image: url(img/action/kick.png);
|
||||
}
|
||||
|
||||
#context-menu .ban {
|
||||
background-image: url(img/action/ban.png);
|
||||
}
|
||||
|
||||
#context-menu .subject {
|
||||
background-image: url(img/action/subject.png);
|
||||
}
|
||||
|
||||
#context-menu .emoticons {
|
||||
padding-left: 5px;
|
||||
width: 85px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#context-menu .emoticons:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#context-menu .emoticons img {
|
||||
cursor: pointer;
|
||||
margin: 3px;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
#chat-modal {
|
||||
background: url(img/modal-bg.png);
|
||||
width: 300px;
|
||||
padding: 20px 5px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -155px;
|
||||
margin-top: -45px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#chat-modal-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 90;
|
||||
background-image: url(img/overlay.png);
|
||||
border-top-left-radius: 8px;
|
||||
}
|
||||
|
||||
#chat-modal.modal-login {
|
||||
display: block;
|
||||
margin-top: -100px;
|
||||
}
|
||||
|
||||
#chat-modal-spinner {
|
||||
display: none;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
#chat-modal form {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
#chat-modal label, #chat-modal input, #chat-modal select {
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 26px;
|
||||
font-size: 16px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
#chat-modal input, #chat-modal select {
|
||||
padding: 2px;
|
||||
line-height: 16px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#chat-modal label {
|
||||
text-align: right;
|
||||
padding-right: 1em;
|
||||
clear: both;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#chat-modal input.button {
|
||||
float: none;
|
||||
display: block;
|
||||
margin: 5px auto;
|
||||
clear: both;
|
||||
position: relative;
|
||||
top: 10px;
|
||||
/* width: 200px; */
|
||||
width: 120px; /* JRBL */
|
||||
}
|
||||
|
||||
#chat-modal .close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: none;
|
||||
padding: 0 5px;
|
||||
margin: -17px 3px 0 0;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#chat-modal .close:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
BIN
lms/static/candy_res/img/action/autoscroll-off.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
lms/static/candy_res/img/action/autoscroll-on.png
Normal file
|
After Width: | Height: | Size: 223 B |
BIN
lms/static/candy_res/img/action/ban.png
Normal file
|
After Width: | Height: | Size: 796 B |
BIN
lms/static/candy_res/img/action/emoticons.png
Executable file
|
After Width: | Height: | Size: 725 B |
BIN
lms/static/candy_res/img/action/ignore.png
Normal file
|
After Width: | Height: | Size: 715 B |
BIN
lms/static/candy_res/img/action/kick.png
Normal file
|
After Width: | Height: | Size: 859 B |
BIN
lms/static/candy_res/img/action/menu.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
lms/static/candy_res/img/action/private.png
Normal file
|
After Width: | Height: | Size: 557 B |
BIN
lms/static/candy_res/img/action/settings.png
Executable file
|
After Width: | Height: | Size: 744 B |
BIN
lms/static/candy_res/img/action/sound-off.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
lms/static/candy_res/img/action/sound-on.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
lms/static/candy_res/img/action/statusmessage-off.png
Normal file
|
After Width: | Height: | Size: 764 B |
BIN
lms/static/candy_res/img/action/statusmessage-on.png
Normal file
|
After Width: | Height: | Size: 659 B |
BIN
lms/static/candy_res/img/action/subject.png
Normal file
|
After Width: | Height: | Size: 413 B |
BIN
lms/static/candy_res/img/action/unignore.png
Normal file
|
After Width: | Height: | Size: 781 B |
BIN
lms/static/candy_res/img/action/usercount.png
Executable file
|
After Width: | Height: | Size: 753 B |
BIN
lms/static/candy_res/img/context-arrows.gif
Normal file
|
After Width: | Height: | Size: 91 B |
BIN
lms/static/candy_res/img/emoticons/Angel.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Angry.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Aww.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Aww_2.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Blushing.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Childish.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Confused.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Creepy.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Crying.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Cthulhu.png
Executable file
|
After Width: | Height: | Size: 775 B |
BIN
lms/static/candy_res/img/emoticons/Cute.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Cute_Winking.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Devil.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Gah.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Gah_2.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Gasping.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Greedy.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Grinning.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Grinning_Winking.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Happy.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Happy_2.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Happy_3.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Heart.png
Executable file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
lms/static/candy_res/img/emoticons/Huh.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Huh_2.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Laughing.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Lips_Sealed.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Madness.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Malicious.png
Executable file
|
After Width: | Height: | Size: 751 B |
2
lms/static/candy_res/img/emoticons/README
Normal file
@@ -0,0 +1,2 @@
|
||||
Simple Smileys is a set of 49 clean, free as in freedom, Public Domain smileys.
|
||||
For more packages or older versions, visit http://simplesmileys.org
|
||||
BIN
lms/static/candy_res/img/emoticons/Sick.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Smiling.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Speechless.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Spiteful.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Stupid.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Sunglasses.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Terrified.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Thumb_Down.png
Executable file
|
After Width: | Height: | Size: 572 B |
BIN
lms/static/candy_res/img/emoticons/Thumb_Up.png
Executable file
|
After Width: | Height: | Size: 530 B |
BIN
lms/static/candy_res/img/emoticons/Tired.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Tongue_Out.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Tongue_Out_Laughing.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/candy_res/img/emoticons/Tongue_Out_Left.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Tongue_Out_Up.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Tongue_Out_Up_Left.png
Executable file
|
After Width: | Height: | Size: 704 B |
BIN
lms/static/candy_res/img/emoticons/Tongue_Out_Winking.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Uncertain.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Uncertain_2.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Unhappy.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/emoticons/Winking.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
lms/static/candy_res/img/favicon.png
Normal file
|
After Width: | Height: | Size: 490 B |
BIN
lms/static/candy_res/img/modal-bg.png
Normal file
|
After Width: | Height: | Size: 109 B |
BIN
lms/static/candy_res/img/modal-spinner.gif
Normal file
|
After Width: | Height: | Size: 723 B |
BIN
lms/static/candy_res/img/overlay.png
Normal file
|
After Width: | Height: | Size: 109 B |
BIN
lms/static/candy_res/img/roster/affiliation-owner.png
Normal file
|
After Width: | Height: | Size: 670 B |
BIN
lms/static/candy_res/img/roster/ignore.png
Normal file
|
After Width: | Height: | Size: 715 B |
BIN
lms/static/candy_res/img/roster/role-moderator.png
Normal file
|
After Width: | Height: | Size: 594 B |
BIN
lms/static/candy_res/img/tab-transitions.png
Normal file
|
After Width: | Height: | Size: 490 B |
BIN
lms/static/candy_res/img/tooltip-arrows.gif
Normal file
|
After Width: | Height: | Size: 66 B |
BIN
lms/static/candy_res/notify.mp3
Normal file
1
lms/static/js/vendor/candy.min.js
vendored
Normal file
127
lms/static/js/vendor/candy_libs/dateformat/dateFormat.js
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Date Format 1.2.3
|
||||
* (c) 2007-2009 Steven Levithan <stevenlevithan.com>
|
||||
* MIT license
|
||||
*
|
||||
* Includes enhancements by Scott Trenda <scott.trenda.net>
|
||||
* and Kris Kowal <cixar.com/~kris.kowal/>
|
||||
*
|
||||
* Accepts a date, a mask, or a date and a mask.
|
||||
* Returns a formatted version of the given date.
|
||||
* The date defaults to the current date/time.
|
||||
* The mask defaults to dateFormat.masks.default.
|
||||
*
|
||||
* @link http://blog.stevenlevithan.com/archives/date-time-format
|
||||
*/
|
||||
|
||||
var dateFormat = function () {
|
||||
var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
|
||||
timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
|
||||
timezoneClip = /[^-+\dA-Z]/g,
|
||||
pad = function (val, len) {
|
||||
val = String(val);
|
||||
len = len || 2;
|
||||
while (val.length < len) val = "0" + val;
|
||||
return val;
|
||||
};
|
||||
|
||||
// Regexes and supporting functions are cached through closure
|
||||
return function (date, mask, utc) {
|
||||
var dF = dateFormat;
|
||||
|
||||
// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
|
||||
if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
|
||||
mask = date;
|
||||
date = undefined;
|
||||
}
|
||||
|
||||
// Passing date through Date applies Date.parse, if necessary
|
||||
date = date ? new Date(date) : new Date;
|
||||
if (isNaN(date)) throw SyntaxError("invalid date");
|
||||
|
||||
mask = String(dF.masks[mask] || mask || dF.masks["default"]);
|
||||
|
||||
// Allow setting the utc argument via the mask
|
||||
if (mask.slice(0, 4) == "UTC:") {
|
||||
mask = mask.slice(4);
|
||||
utc = true;
|
||||
}
|
||||
|
||||
var _ = utc ? "getUTC" : "get",
|
||||
d = date[_ + "Date"](),
|
||||
D = date[_ + "Day"](),
|
||||
m = date[_ + "Month"](),
|
||||
y = date[_ + "FullYear"](),
|
||||
H = date[_ + "Hours"](),
|
||||
M = date[_ + "Minutes"](),
|
||||
s = date[_ + "Seconds"](),
|
||||
L = date[_ + "Milliseconds"](),
|
||||
o = utc ? 0 : date.getTimezoneOffset(),
|
||||
flags = {
|
||||
d: d,
|
||||
dd: pad(d),
|
||||
ddd: dF.i18n.dayNames[D],
|
||||
dddd: dF.i18n.dayNames[D + 7],
|
||||
m: m + 1,
|
||||
mm: pad(m + 1),
|
||||
mmm: dF.i18n.monthNames[m],
|
||||
mmmm: dF.i18n.monthNames[m + 12],
|
||||
yy: String(y).slice(2),
|
||||
yyyy: y,
|
||||
h: H % 12 || 12,
|
||||
hh: pad(H % 12 || 12),
|
||||
H: H,
|
||||
HH: pad(H),
|
||||
M: M,
|
||||
MM: pad(M),
|
||||
s: s,
|
||||
ss: pad(s),
|
||||
l: pad(L, 3),
|
||||
L: pad(L > 99 ? Math.round(L / 10) : L),
|
||||
t: H < 12 ? "a" : "p",
|
||||
tt: H < 12 ? "am" : "pm",
|
||||
T: H < 12 ? "A" : "P",
|
||||
TT: H < 12 ? "AM" : "PM",
|
||||
Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
|
||||
o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
|
||||
S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
|
||||
};
|
||||
|
||||
return mask.replace(token, function ($0) {
|
||||
return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
|
||||
});
|
||||
};
|
||||
}();
|
||||
|
||||
// Some common format strings
|
||||
dateFormat.masks = {
|
||||
"default": "ddd mmm dd yyyy HH:MM:ss",
|
||||
shortDate: "m/d/yy",
|
||||
mediumDate: "mmm d, yyyy",
|
||||
longDate: "mmmm d, yyyy",
|
||||
fullDate: "dddd, mmmm d, yyyy",
|
||||
shortTime: "h:MM TT",
|
||||
mediumTime: "h:MM:ss TT",
|
||||
longTime: "h:MM:ss TT Z",
|
||||
isoDate: "yyyy-mm-dd",
|
||||
isoTime: "HH:MM:ss",
|
||||
isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
|
||||
isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
|
||||
};
|
||||
|
||||
// Internationalization strings
|
||||
dateFormat.i18n = {
|
||||
dayNames: [
|
||||
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
|
||||
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
|
||||
],
|
||||
monthNames: [
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
|
||||
]
|
||||
};
|
||||
|
||||
// For convenience...
|
||||
Date.prototype.format = function (mask, utc) {
|
||||
return dateFormat(this, mask, utc);
|
||||
};
|
||||
4521
lms/static/js/vendor/candy_libs/libs.bundle.js
vendored
Normal file
@@ -0,0 +1,4521 @@
|
||||
// This code was written by Tyler Akins and has been placed in the
|
||||
// public domain. It would be nice if you left this header intact.
|
||||
// Base64 code from Tyler Akins -- http://rumkin.com
|
||||
|
||||
var Base64 = (function () {
|
||||
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
|
||||
var obj = {
|
||||
/**
|
||||
* Encodes a string in base64
|
||||
* @param {String} input The string to encode in base64.
|
||||
*/
|
||||
encode: function (input) {
|
||||
var output = "";
|
||||
var chr1, chr2, chr3;
|
||||
var enc1, enc2, enc3, enc4;
|
||||
var i = 0;
|
||||
|
||||
do {
|
||||
chr1 = input.charCodeAt(i++);
|
||||
chr2 = input.charCodeAt(i++);
|
||||
chr3 = input.charCodeAt(i++);
|
||||
|
||||
enc1 = chr1 >> 2;
|
||||
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
||||
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
||||
enc4 = chr3 & 63;
|
||||
|
||||
if (isNaN(chr2)) {
|
||||
enc3 = enc4 = 64;
|
||||
} else if (isNaN(chr3)) {
|
||||
enc4 = 64;
|
||||
}
|
||||
|
||||
output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
|
||||
keyStr.charAt(enc3) + keyStr.charAt(enc4);
|
||||
} while (i < input.length);
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
/**
|
||||
* Decodes a base64 string.
|
||||
* @param {String} input The string to decode.
|
||||
*/
|
||||
decode: function (input) {
|
||||
var output = "";
|
||||
var chr1, chr2, chr3;
|
||||
var enc1, enc2, enc3, enc4;
|
||||
var i = 0;
|
||||
|
||||
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
|
||||
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
||||
|
||||
do {
|
||||
enc1 = keyStr.indexOf(input.charAt(i++));
|
||||
enc2 = keyStr.indexOf(input.charAt(i++));
|
||||
enc3 = keyStr.indexOf(input.charAt(i++));
|
||||
enc4 = keyStr.indexOf(input.charAt(i++));
|
||||
|
||||
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||
chr3 = ((enc3 & 3) << 6) | enc4;
|
||||
|
||||
output = output + String.fromCharCode(chr1);
|
||||
|
||||
if (enc3 != 64) {
|
||||
output = output + String.fromCharCode(chr2);
|
||||
}
|
||||
if (enc4 != 64) {
|
||||
output = output + String.fromCharCode(chr3);
|
||||
}
|
||||
} while (i < input.length);
|
||||
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
return obj;
|
||||
})();
|
||||
/*
|
||||
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
||||
* Digest Algorithm, as defined in RFC 1321.
|
||||
* Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
|
||||
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
||||
* Distributed under the BSD License
|
||||
* See http://pajhome.org.uk/crypt/md5 for more info.
|
||||
*/
|
||||
|
||||
var MD5 = (function () {
|
||||
/*
|
||||
* Configurable variables. You may need to tweak these to be compatible with
|
||||
* the server-side, but the defaults work in most cases.
|
||||
*/
|
||||
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
|
||||
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
|
||||
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
|
||||
|
||||
/*
|
||||
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
||||
* to work around bugs in some JS interpreters.
|
||||
*/
|
||||
var safe_add = function (x, y) {
|
||||
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
|
||||
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||
return (msw << 16) | (lsw & 0xFFFF);
|
||||
};
|
||||
|
||||
/*
|
||||
* Bitwise rotate a 32-bit number to the left.
|
||||
*/
|
||||
var bit_rol = function (num, cnt) {
|
||||
return (num << cnt) | (num >>> (32 - cnt));
|
||||
};
|
||||
|
||||
/*
|
||||
* Convert a string to an array of little-endian words
|
||||
* If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
|
||||
*/
|
||||
var str2binl = function (str) {
|
||||
var bin = [];
|
||||
var mask = (1 << chrsz) - 1;
|
||||
for(var i = 0; i < str.length * chrsz; i += chrsz)
|
||||
{
|
||||
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
|
||||
}
|
||||
return bin;
|
||||
};
|
||||
|
||||
/*
|
||||
* Convert an array of little-endian words to a string
|
||||
*/
|
||||
var binl2str = function (bin) {
|
||||
var str = "";
|
||||
var mask = (1 << chrsz) - 1;
|
||||
for(var i = 0; i < bin.length * 32; i += chrsz)
|
||||
{
|
||||
str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
/*
|
||||
* Convert an array of little-endian words to a hex string.
|
||||
*/
|
||||
var binl2hex = function (binarray) {
|
||||
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
|
||||
var str = "";
|
||||
for(var i = 0; i < binarray.length * 4; i++)
|
||||
{
|
||||
str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
|
||||
hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
/*
|
||||
* Convert an array of little-endian words to a base-64 string
|
||||
*/
|
||||
var binl2b64 = function (binarray) {
|
||||
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
var str = "";
|
||||
var triplet, j;
|
||||
for(var i = 0; i < binarray.length * 4; i += 3)
|
||||
{
|
||||
triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) |
|
||||
(((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) |
|
||||
((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
|
||||
for(j = 0; j < 4; j++)
|
||||
{
|
||||
if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
|
||||
else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
|
||||
}
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
/*
|
||||
* These functions implement the four basic operations the algorithm uses.
|
||||
*/
|
||||
var md5_cmn = function (q, a, b, x, s, t) {
|
||||
return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b);
|
||||
};
|
||||
|
||||
var md5_ff = function (a, b, c, d, x, s, t) {
|
||||
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
|
||||
};
|
||||
|
||||
var md5_gg = function (a, b, c, d, x, s, t) {
|
||||
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
|
||||
};
|
||||
|
||||
var md5_hh = function (a, b, c, d, x, s, t) {
|
||||
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
|
||||
};
|
||||
|
||||
var md5_ii = function (a, b, c, d, x, s, t) {
|
||||
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
|
||||
};
|
||||
|
||||
/*
|
||||
* Calculate the MD5 of an array of little-endian words, and a bit length
|
||||
*/
|
||||
var core_md5 = function (x, len) {
|
||||
/* append padding */
|
||||
x[len >> 5] |= 0x80 << ((len) % 32);
|
||||
x[(((len + 64) >>> 9) << 4) + 14] = len;
|
||||
|
||||
var a = 1732584193;
|
||||
var b = -271733879;
|
||||
var c = -1732584194;
|
||||
var d = 271733878;
|
||||
|
||||
var olda, oldb, oldc, oldd;
|
||||
for (var i = 0; i < x.length; i += 16)
|
||||
{
|
||||
olda = a;
|
||||
oldb = b;
|
||||
oldc = c;
|
||||
oldd = d;
|
||||
|
||||
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
|
||||
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
|
||||
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
|
||||
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
|
||||
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
|
||||
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
|
||||
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
|
||||
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
|
||||
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
|
||||
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
|
||||
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
|
||||
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
|
||||
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
|
||||
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
|
||||
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
|
||||
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
|
||||
|
||||
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
|
||||
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
|
||||
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
|
||||
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
|
||||
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
|
||||
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
|
||||
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
|
||||
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
|
||||
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
|
||||
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
|
||||
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
|
||||
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
|
||||
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
|
||||
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
|
||||
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
|
||||
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
|
||||
|
||||
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
|
||||
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
|
||||
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
|
||||
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
|
||||
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
|
||||
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
|
||||
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
|
||||
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
|
||||
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
|
||||
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
|
||||
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
|
||||
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
|
||||
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
|
||||
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
|
||||
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
|
||||
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
|
||||
|
||||
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
|
||||
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
|
||||
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
|
||||
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
|
||||
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
|
||||
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
|
||||
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
|
||||
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
|
||||
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
|
||||
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
|
||||
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
|
||||
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
|
||||
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
|
||||
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
|
||||
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
|
||||
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
|
||||
|
||||
a = safe_add(a, olda);
|
||||
b = safe_add(b, oldb);
|
||||
c = safe_add(c, oldc);
|
||||
d = safe_add(d, oldd);
|
||||
}
|
||||
return [a, b, c, d];
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Calculate the HMAC-MD5, of a key and some data
|
||||
*/
|
||||
var core_hmac_md5 = function (key, data) {
|
||||
var bkey = str2binl(key);
|
||||
if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); }
|
||||
|
||||
var ipad = new Array(16), opad = new Array(16);
|
||||
for(var i = 0; i < 16; i++)
|
||||
{
|
||||
ipad[i] = bkey[i] ^ 0x36363636;
|
||||
opad[i] = bkey[i] ^ 0x5C5C5C5C;
|
||||
}
|
||||
|
||||
var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
|
||||
return core_md5(opad.concat(hash), 512 + 128);
|
||||
};
|
||||
|
||||
var obj = {
|
||||
/*
|
||||
* These are the functions you'll usually want to call.
|
||||
* They take string arguments and return either hex or base-64 encoded
|
||||
* strings.
|
||||
*/
|
||||
hexdigest: function (s) {
|
||||
return binl2hex(core_md5(str2binl(s), s.length * chrsz));
|
||||
},
|
||||
|
||||
b64digest: function (s) {
|
||||
return binl2b64(core_md5(str2binl(s), s.length * chrsz));
|
||||
},
|
||||
|
||||
hash: function (s) {
|
||||
return binl2str(core_md5(str2binl(s), s.length * chrsz));
|
||||
},
|
||||
|
||||
hmac_hexdigest: function (key, data) {
|
||||
return binl2hex(core_hmac_md5(key, data));
|
||||
},
|
||||
|
||||
hmac_b64digest: function (key, data) {
|
||||
return binl2b64(core_hmac_md5(key, data));
|
||||
},
|
||||
|
||||
hmac_hash: function (key, data) {
|
||||
return binl2str(core_hmac_md5(key, data));
|
||||
},
|
||||
|
||||
/*
|
||||
* Perform a simple self-test to see if the VM is working
|
||||
*/
|
||||
test: function () {
|
||||
return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72";
|
||||
}
|
||||
};
|
||||
|
||||
return obj;
|
||||
})();
|
||||
/*
|
||||
This program is distributed under the terms of the MIT license.
|
||||
Please see the LICENSE file for details.
|
||||
|
||||
Copyright 2006-2008, OGG, LLC
|
||||
*/
|
||||
|
||||
/* jslint configuration: */
|
||||
/*global document, window, setTimeout, clearTimeout, console,
|
||||
XMLHttpRequest, ActiveXObject,
|
||||
Base64, MD5,
|
||||
Strophe, $build, $msg, $iq, $pres */
|
||||
|
||||
/** File: strophe.js
|
||||
* A JavaScript library for XMPP BOSH.
|
||||
*
|
||||
* This is the JavaScript version of the Strophe library. Since JavaScript
|
||||
* has no facilities for persistent TCP connections, this library uses
|
||||
* Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate
|
||||
* a persistent, stateful, two-way connection to an XMPP server. More
|
||||
* information on BOSH can be found in XEP 124.
|
||||
*/
|
||||
|
||||
/** PrivateFunction: Function.prototype.bind
|
||||
* Bind a function to an instance.
|
||||
*
|
||||
* This Function object extension method creates a bound method similar
|
||||
* to those in Python. This means that the 'this' object will point
|
||||
* to the instance you want. See
|
||||
* <a href='https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind'>MDC's bind() documentation</a> and
|
||||
* <a href='http://benjamin.smedbergs.us/blog/2007-01-03/bound-functions-and-function-imports-in-javascript/'>Bound Functions and Function Imports in JavaScript</a>
|
||||
* for a complete explanation.
|
||||
*
|
||||
* This extension already exists in some browsers (namely, Firefox 3), but
|
||||
* we provide it to support those that don't.
|
||||
*
|
||||
* Parameters:
|
||||
* (Object) obj - The object that will become 'this' in the bound function.
|
||||
* (Object) argN - An option argument that will be prepended to the
|
||||
* arguments given for the function call
|
||||
*
|
||||
* Returns:
|
||||
* The bound function.
|
||||
*/
|
||||
if (!Function.prototype.bind) {
|
||||
Function.prototype.bind = function (obj /*, arg1, arg2, ... */)
|
||||
{
|
||||
var func = this;
|
||||
var _slice = Array.prototype.slice;
|
||||
var _concat = Array.prototype.concat;
|
||||
var _args = _slice.call(arguments, 1);
|
||||
|
||||
return function () {
|
||||
return func.apply(obj ? obj : this,
|
||||
_concat.call(_args,
|
||||
_slice.call(arguments, 0)));
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** PrivateFunction: Array.prototype.indexOf
|
||||
* Return the index of an object in an array.
|
||||
*
|
||||
* This function is not supplied by some JavaScript implementations, so
|
||||
* we provide it if it is missing. This code is from:
|
||||
* http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
|
||||
*
|
||||
* Parameters:
|
||||
* (Object) elt - The object to look for.
|
||||
* (Integer) from - The index from which to start looking. (optional).
|
||||
*
|
||||
* Returns:
|
||||
* The index of elt in the array or -1 if not found.
|
||||
*/
|
||||
if (!Array.prototype.indexOf)
|
||||
{
|
||||
Array.prototype.indexOf = function(elt /*, from*/)
|
||||
{
|
||||
var len = this.length;
|
||||
|
||||
var from = Number(arguments[1]) || 0;
|
||||
from = (from < 0) ? Math.ceil(from) : Math.floor(from);
|
||||
if (from < 0) {
|
||||
from += len;
|
||||
}
|
||||
|
||||
for (; from < len; from++) {
|
||||
if (from in this && this[from] === elt) {
|
||||
return from;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
/* All of the Strophe globals are defined in this special function below so
|
||||
* that references to the globals become closures. This will ensure that
|
||||
* on page reload, these references will still be available to callbacks
|
||||
* that are still executing.
|
||||
*/
|
||||
|
||||
(function (callback) {
|
||||
var Strophe;
|
||||
|
||||
/** Function: $build
|
||||
* Create a Strophe.Builder.
|
||||
* This is an alias for 'new Strophe.Builder(name, attrs)'.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) name - The root element name.
|
||||
* (Object) attrs - The attributes for the root element in object notation.
|
||||
*
|
||||
* Returns:
|
||||
* A new Strophe.Builder object.
|
||||
*/
|
||||
function $build(name, attrs) { return new Strophe.Builder(name, attrs); }
|
||||
/** Function: $msg
|
||||
* Create a Strophe.Builder with a <message/> element as the root.
|
||||
*
|
||||
* Parmaeters:
|
||||
* (Object) attrs - The <message/> element attributes in object notation.
|
||||
*
|
||||
* Returns:
|
||||
* A new Strophe.Builder object.
|
||||
*/
|
||||
function $msg(attrs) { return new Strophe.Builder("message", attrs); }
|
||||
/** Function: $iq
|
||||
* Create a Strophe.Builder with an <iq/> element as the root.
|
||||
*
|
||||
* Parameters:
|
||||
* (Object) attrs - The <iq/> element attributes in object notation.
|
||||
*
|
||||
* Returns:
|
||||
* A new Strophe.Builder object.
|
||||
*/
|
||||
function $iq(attrs) { return new Strophe.Builder("iq", attrs); }
|
||||
/** Function: $pres
|
||||
* Create a Strophe.Builder with a <presence/> element as the root.
|
||||
*
|
||||
* Parameters:
|
||||
* (Object) attrs - The <presence/> element attributes in object notation.
|
||||
*
|
||||
* Returns:
|
||||
* A new Strophe.Builder object.
|
||||
*/
|
||||
function $pres(attrs) { return new Strophe.Builder("presence", attrs); }
|
||||
|
||||
/** Class: Strophe
|
||||
* An object container for all Strophe library functions.
|
||||
*
|
||||
* This class is just a container for all the objects and constants
|
||||
* used in the library. It is not meant to be instantiated, but to
|
||||
* provide a namespace for library objects, constants, and functions.
|
||||
*/
|
||||
Strophe = {
|
||||
/** Constant: VERSION
|
||||
* The version of the Strophe library. Unreleased builds will have
|
||||
* a version of head-HASH where HASH is a partial revision.
|
||||
*/
|
||||
VERSION: "8d27954",
|
||||
|
||||
/** Constants: XMPP Namespace Constants
|
||||
* Common namespace constants from the XMPP RFCs and XEPs.
|
||||
*
|
||||
* NS.HTTPBIND - HTTP BIND namespace from XEP 124.
|
||||
* NS.BOSH - BOSH namespace from XEP 206.
|
||||
* NS.CLIENT - Main XMPP client namespace.
|
||||
* NS.AUTH - Legacy authentication namespace.
|
||||
* NS.ROSTER - Roster operations namespace.
|
||||
* NS.PROFILE - Profile namespace.
|
||||
* NS.DISCO_INFO - Service discovery info namespace from XEP 30.
|
||||
* NS.DISCO_ITEMS - Service discovery items namespace from XEP 30.
|
||||
* NS.MUC - Multi-User Chat namespace from XEP 45.
|
||||
* NS.SASL - XMPP SASL namespace from RFC 3920.
|
||||
* NS.STREAM - XMPP Streams namespace from RFC 3920.
|
||||
* NS.BIND - XMPP Binding namespace from RFC 3920.
|
||||
* NS.SESSION - XMPP Session namespace from RFC 3920.
|
||||
*/
|
||||
NS: {
|
||||
HTTPBIND: "http://jabber.org/protocol/httpbind",
|
||||
BOSH: "urn:xmpp:xbosh",
|
||||
CLIENT: "jabber:client",
|
||||
AUTH: "jabber:iq:auth",
|
||||
ROSTER: "jabber:iq:roster",
|
||||
PROFILE: "jabber:iq:profile",
|
||||
DISCO_INFO: "http://jabber.org/protocol/disco#info",
|
||||
DISCO_ITEMS: "http://jabber.org/protocol/disco#items",
|
||||
MUC: "http://jabber.org/protocol/muc",
|
||||
SASL: "urn:ietf:params:xml:ns:xmpp-sasl",
|
||||
STREAM: "http://etherx.jabber.org/streams",
|
||||
BIND: "urn:ietf:params:xml:ns:xmpp-bind",
|
||||
SESSION: "urn:ietf:params:xml:ns:xmpp-session",
|
||||
VERSION: "jabber:iq:version",
|
||||
STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas"
|
||||
},
|
||||
|
||||
/** Function: addNamespace
|
||||
* This function is used to extend the current namespaces in
|
||||
* Strophe.NS. It takes a key and a value with the key being the
|
||||
* name of the new namespace, with its actual value.
|
||||
* For example:
|
||||
* Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub");
|
||||
*
|
||||
* Parameters:
|
||||
* (String) name - The name under which the namespace will be
|
||||
* referenced under Strophe.NS
|
||||
* (String) value - The actual namespace.
|
||||
*/
|
||||
addNamespace: function (name, value)
|
||||
{
|
||||
Strophe.NS[name] = value;
|
||||
},
|
||||
|
||||
/** Constants: Connection Status Constants
|
||||
* Connection status constants for use by the connection handler
|
||||
* callback.
|
||||
*
|
||||
* Status.ERROR - An error has occurred
|
||||
* Status.CONNECTING - The connection is currently being made
|
||||
* Status.CONNFAIL - The connection attempt failed
|
||||
* Status.AUTHENTICATING - The connection is authenticating
|
||||
* Status.AUTHFAIL - The authentication attempt failed
|
||||
* Status.CONNECTED - The connection has succeeded
|
||||
* Status.DISCONNECTED - The connection has been terminated
|
||||
* Status.DISCONNECTING - The connection is currently being terminated
|
||||
* Status.ATTACHED - The connection has been attached
|
||||
*/
|
||||
Status: {
|
||||
ERROR: 0,
|
||||
CONNECTING: 1,
|
||||
CONNFAIL: 2,
|
||||
AUTHENTICATING: 3,
|
||||
AUTHFAIL: 4,
|
||||
CONNECTED: 5,
|
||||
DISCONNECTED: 6,
|
||||
DISCONNECTING: 7,
|
||||
ATTACHED: 8
|
||||
},
|
||||
|
||||
/** Constants: Log Level Constants
|
||||
* Logging level indicators.
|
||||
*
|
||||
* LogLevel.DEBUG - Debug output
|
||||
* LogLevel.INFO - Informational output
|
||||
* LogLevel.WARN - Warnings
|
||||
* LogLevel.ERROR - Errors
|
||||
* LogLevel.FATAL - Fatal errors
|
||||
*/
|
||||
LogLevel: {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
FATAL: 4
|
||||
},
|
||||
|
||||
/** PrivateConstants: DOM Element Type Constants
|
||||
* DOM element types.
|
||||
*
|
||||
* ElementType.NORMAL - Normal element.
|
||||
* ElementType.TEXT - Text data element.
|
||||
*/
|
||||
ElementType: {
|
||||
NORMAL: 1,
|
||||
TEXT: 3,
|
||||
CDATA: 4
|
||||
},
|
||||
|
||||
/** PrivateConstants: Timeout Values
|
||||
* Timeout values for error states. These values are in seconds.
|
||||
* These should not be changed unless you know exactly what you are
|
||||
* doing.
|
||||
*
|
||||
* TIMEOUT - Timeout multiplier. A waiting request will be considered
|
||||
* failed after Math.floor(TIMEOUT * wait) seconds have elapsed.
|
||||
* This defaults to 1.1, and with default wait, 66 seconds.
|
||||
* SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where
|
||||
* Strophe can detect early failure, it will consider the request
|
||||
* failed if it doesn't return after
|
||||
* Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed.
|
||||
* This defaults to 0.1, and with default wait, 6 seconds.
|
||||
*/
|
||||
TIMEOUT: 1.1,
|
||||
SECONDARY_TIMEOUT: 0.1,
|
||||
|
||||
/** Function: forEachChild
|
||||
* Map a function over some or all child elements of a given element.
|
||||
*
|
||||
* This is a small convenience function for mapping a function over
|
||||
* some or all of the children of an element. If elemName is null, all
|
||||
* children will be passed to the function, otherwise only children
|
||||
* whose tag names match elemName will be passed.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The element to operate on.
|
||||
* (String) elemName - The child element tag name filter.
|
||||
* (Function) func - The function to apply to each child. This
|
||||
* function should take a single argument, a DOM element.
|
||||
*/
|
||||
forEachChild: function (elem, elemName, func)
|
||||
{
|
||||
var i, childNode;
|
||||
|
||||
for (i = 0; i < elem.childNodes.length; i++) {
|
||||
childNode = elem.childNodes[i];
|
||||
if (childNode.nodeType == Strophe.ElementType.NORMAL &&
|
||||
(!elemName || this.isTagEqual(childNode, elemName))) {
|
||||
func(childNode);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** Function: isTagEqual
|
||||
* Compare an element's tag name with a string.
|
||||
*
|
||||
* This function is case insensitive.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) el - A DOM element.
|
||||
* (String) name - The element name.
|
||||
*
|
||||
* Returns:
|
||||
* true if the element's tag name matches _el_, and false
|
||||
* otherwise.
|
||||
*/
|
||||
isTagEqual: function (el, name)
|
||||
{
|
||||
return el.tagName.toLowerCase() == name.toLowerCase();
|
||||
},
|
||||
|
||||
/** PrivateVariable: _xmlGenerator
|
||||
* _Private_ variable that caches a DOM document to
|
||||
* generate elements.
|
||||
*/
|
||||
_xmlGenerator: null,
|
||||
|
||||
/** PrivateFunction: _makeGenerator
|
||||
* _Private_ function that creates a dummy XML DOM document to serve as
|
||||
* an element and text node generator.
|
||||
*/
|
||||
_makeGenerator: function () {
|
||||
var doc;
|
||||
|
||||
if (document.implementation.createDocument === undefined) {
|
||||
doc = this._getIEXmlDom();
|
||||
doc.appendChild(doc.createElement('strophe'));
|
||||
} else {
|
||||
doc = document.implementation
|
||||
.createDocument('jabber:client', 'strophe', null);
|
||||
}
|
||||
|
||||
return doc;
|
||||
},
|
||||
|
||||
/** Function: xmlGenerator
|
||||
* Get the DOM document to generate elements.
|
||||
*
|
||||
* Returns:
|
||||
* The currently used DOM document.
|
||||
*/
|
||||
xmlGenerator: function () {
|
||||
if (!Strophe._xmlGenerator) {
|
||||
Strophe._xmlGenerator = Strophe._makeGenerator();
|
||||
}
|
||||
return Strophe._xmlGenerator;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _getIEXmlDom
|
||||
* Gets IE xml doc object
|
||||
*
|
||||
* Returns:
|
||||
* A Microsoft XML DOM Object
|
||||
* See Also:
|
||||
* http://msdn.microsoft.com/en-us/library/ms757837%28VS.85%29.aspx
|
||||
*/
|
||||
_getIEXmlDom : function() {
|
||||
var doc = null;
|
||||
var docStrings = [
|
||||
"Msxml2.DOMDocument.6.0",
|
||||
"Msxml2.DOMDocument.5.0",
|
||||
"Msxml2.DOMDocument.4.0",
|
||||
"MSXML2.DOMDocument.3.0",
|
||||
"MSXML2.DOMDocument",
|
||||
"MSXML.DOMDocument",
|
||||
"Microsoft.XMLDOM"
|
||||
];
|
||||
|
||||
for (var d = 0; d < docStrings.length; d++) {
|
||||
if (doc === null) {
|
||||
try {
|
||||
doc = new ActiveXObject(docStrings[d]);
|
||||
} catch (e) {
|
||||
doc = null;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return doc;
|
||||
},
|
||||
|
||||
/** Function: xmlElement
|
||||
* Create an XML DOM element.
|
||||
*
|
||||
* This function creates an XML DOM element correctly across all
|
||||
* implementations. Note that these are not HTML DOM elements, which
|
||||
* aren't appropriate for XMPP stanzas.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) name - The name for the element.
|
||||
* (Array|Object) attrs - An optional array or object containing
|
||||
* key/value pairs to use as element attributes. The object should
|
||||
* be in the format {'key': 'value'} or {key: 'value'}. The array
|
||||
* should have the format [['key1', 'value1'], ['key2', 'value2']].
|
||||
* (String) text - The text child data for the element.
|
||||
*
|
||||
* Returns:
|
||||
* A new XML DOM element.
|
||||
*/
|
||||
xmlElement: function (name)
|
||||
{
|
||||
if (!name) { return null; }
|
||||
|
||||
var node = Strophe.xmlGenerator().createElement(name);
|
||||
|
||||
// FIXME: this should throw errors if args are the wrong type or
|
||||
// there are more than two optional args
|
||||
var a, i, k;
|
||||
for (a = 1; a < arguments.length; a++) {
|
||||
if (!arguments[a]) { continue; }
|
||||
if (typeof(arguments[a]) == "string" ||
|
||||
typeof(arguments[a]) == "number") {
|
||||
node.appendChild(Strophe.xmlTextNode(arguments[a]));
|
||||
} else if (typeof(arguments[a]) == "object" &&
|
||||
typeof(arguments[a].sort) == "function") {
|
||||
for (i = 0; i < arguments[a].length; i++) {
|
||||
if (typeof(arguments[a][i]) == "object" &&
|
||||
typeof(arguments[a][i].sort) == "function") {
|
||||
node.setAttribute(arguments[a][i][0],
|
||||
arguments[a][i][1]);
|
||||
}
|
||||
}
|
||||
} else if (typeof(arguments[a]) == "object") {
|
||||
for (k in arguments[a]) {
|
||||
if (arguments[a].hasOwnProperty(k)) {
|
||||
node.setAttribute(k, arguments[a][k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
/* Function: xmlescape
|
||||
* Excapes invalid xml characters.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) text - text to escape.
|
||||
*
|
||||
* Returns:
|
||||
* Escaped text.
|
||||
*/
|
||||
xmlescape: function(text)
|
||||
{
|
||||
text = text.replace(/\&/g, "&");
|
||||
text = text.replace(/</g, "<");
|
||||
text = text.replace(/>/g, ">");
|
||||
text = text.replace(/'/g, "'");
|
||||
text = text.replace(/"/g, """);
|
||||
return text;
|
||||
},
|
||||
|
||||
/** Function: xmlTextNode
|
||||
* Creates an XML DOM text node.
|
||||
*
|
||||
* Provides a cross implementation version of document.createTextNode.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) text - The content of the text node.
|
||||
*
|
||||
* Returns:
|
||||
* A new XML DOM text node.
|
||||
*/
|
||||
xmlTextNode: function (text)
|
||||
{
|
||||
//ensure text is escaped
|
||||
text = Strophe.xmlescape(text);
|
||||
|
||||
return Strophe.xmlGenerator().createTextNode(text);
|
||||
},
|
||||
|
||||
/** Function: getText
|
||||
* Get the concatenation of all text children of an element.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - A DOM element.
|
||||
*
|
||||
* Returns:
|
||||
* A String with the concatenated text of all text element children.
|
||||
*/
|
||||
getText: function (elem)
|
||||
{
|
||||
if (!elem) { return null; }
|
||||
|
||||
var str = "";
|
||||
if (elem.childNodes.length === 0 && elem.nodeType ==
|
||||
Strophe.ElementType.TEXT) {
|
||||
str += elem.nodeValue;
|
||||
}
|
||||
|
||||
for (var i = 0; i < elem.childNodes.length; i++) {
|
||||
if (elem.childNodes[i].nodeType == Strophe.ElementType.TEXT) {
|
||||
str += elem.childNodes[i].nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
/** Function: copyElement
|
||||
* Copy an XML DOM element.
|
||||
*
|
||||
* This function copies a DOM element and all its descendants and returns
|
||||
* the new copy.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - A DOM element.
|
||||
*
|
||||
* Returns:
|
||||
* A new, copied DOM element tree.
|
||||
*/
|
||||
copyElement: function (elem)
|
||||
{
|
||||
var i, el;
|
||||
if (elem.nodeType == Strophe.ElementType.NORMAL) {
|
||||
el = Strophe.xmlElement(elem.tagName);
|
||||
|
||||
for (i = 0; i < elem.attributes.length; i++) {
|
||||
el.setAttribute(elem.attributes[i].nodeName.toLowerCase(),
|
||||
elem.attributes[i].value);
|
||||
}
|
||||
|
||||
for (i = 0; i < elem.childNodes.length; i++) {
|
||||
el.appendChild(Strophe.copyElement(elem.childNodes[i]));
|
||||
}
|
||||
} else if (elem.nodeType == Strophe.ElementType.TEXT) {
|
||||
el = Strophe.xmlGenerator().createTextNode(elem.nodeValue);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
/** Function: escapeNode
|
||||
* Escape the node part (also called local part) of a JID.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) node - A node (or local part).
|
||||
*
|
||||
* Returns:
|
||||
* An escaped node (or local part).
|
||||
*/
|
||||
escapeNode: function (node)
|
||||
{
|
||||
return node.replace(/^\s+|\s+$/g, '')
|
||||
.replace(/\\/g, "\\5c")
|
||||
.replace(/ /g, "\\20")
|
||||
.replace(/\"/g, "\\22")
|
||||
.replace(/\&/g, "\\26")
|
||||
.replace(/\'/g, "\\27")
|
||||
.replace(/\//g, "\\2f")
|
||||
.replace(/:/g, "\\3a")
|
||||
.replace(/</g, "\\3c")
|
||||
.replace(/>/g, "\\3e")
|
||||
.replace(/@/g, "\\40");
|
||||
},
|
||||
|
||||
/** Function: unescapeNode
|
||||
* Unescape a node part (also called local part) of a JID.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) node - A node (or local part).
|
||||
*
|
||||
* Returns:
|
||||
* An unescaped node (or local part).
|
||||
*/
|
||||
unescapeNode: function (node)
|
||||
{
|
||||
return node.replace(/\\20/g, " ")
|
||||
.replace(/\\22/g, '"')
|
||||
.replace(/\\26/g, "&")
|
||||
.replace(/\\27/g, "'")
|
||||
.replace(/\\2f/g, "/")
|
||||
.replace(/\\3a/g, ":")
|
||||
.replace(/\\3c/g, "<")
|
||||
.replace(/\\3e/g, ">")
|
||||
.replace(/\\40/g, "@")
|
||||
.replace(/\\5c/g, "\\");
|
||||
},
|
||||
|
||||
/** Function: getNodeFromJid
|
||||
* Get the node portion of a JID String.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid - A JID.
|
||||
*
|
||||
* Returns:
|
||||
* A String containing the node.
|
||||
*/
|
||||
getNodeFromJid: function (jid)
|
||||
{
|
||||
if (jid.indexOf("@") < 0) { return null; }
|
||||
return jid.split("@")[0];
|
||||
},
|
||||
|
||||
/** Function: getDomainFromJid
|
||||
* Get the domain portion of a JID String.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid - A JID.
|
||||
*
|
||||
* Returns:
|
||||
* A String containing the domain.
|
||||
*/
|
||||
getDomainFromJid: function (jid)
|
||||
{
|
||||
var bare = Strophe.getBareJidFromJid(jid);
|
||||
if (bare.indexOf("@") < 0) {
|
||||
return bare;
|
||||
} else {
|
||||
var parts = bare.split("@");
|
||||
parts.splice(0, 1);
|
||||
return parts.join('@');
|
||||
}
|
||||
},
|
||||
|
||||
/** Function: getResourceFromJid
|
||||
* Get the resource portion of a JID String.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid - A JID.
|
||||
*
|
||||
* Returns:
|
||||
* A String containing the resource.
|
||||
*/
|
||||
getResourceFromJid: function (jid)
|
||||
{
|
||||
var s = jid.split("/");
|
||||
if (s.length < 2) { return null; }
|
||||
s.splice(0, 1);
|
||||
return s.join('/');
|
||||
},
|
||||
|
||||
/** Function: getBareJidFromJid
|
||||
* Get the bare JID from a JID String.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid - A JID.
|
||||
*
|
||||
* Returns:
|
||||
* A String containing the bare JID.
|
||||
*/
|
||||
getBareJidFromJid: function (jid)
|
||||
{
|
||||
return jid ? jid.split("/")[0] : null;
|
||||
},
|
||||
|
||||
/** Function: log
|
||||
* User overrideable logging function.
|
||||
*
|
||||
* This function is called whenever the Strophe library calls any
|
||||
* of the logging functions. The default implementation of this
|
||||
* function does nothing. If client code wishes to handle the logging
|
||||
* messages, it should override this with
|
||||
* > Strophe.log = function (level, msg) {
|
||||
* > (user code here)
|
||||
* > };
|
||||
*
|
||||
* Please note that data sent and received over the wire is logged
|
||||
* via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput().
|
||||
*
|
||||
* The different levels and their meanings are
|
||||
*
|
||||
* DEBUG - Messages useful for debugging purposes.
|
||||
* INFO - Informational messages. This is mostly information like
|
||||
* 'disconnect was called' or 'SASL auth succeeded'.
|
||||
* WARN - Warnings about potential problems. This is mostly used
|
||||
* to report transient connection errors like request timeouts.
|
||||
* ERROR - Some error occurred.
|
||||
* FATAL - A non-recoverable fatal error occurred.
|
||||
*
|
||||
* Parameters:
|
||||
* (Integer) level - The log level of the log message. This will
|
||||
* be one of the values in Strophe.LogLevel.
|
||||
* (String) msg - The log message.
|
||||
*/
|
||||
log: function (level, msg)
|
||||
{
|
||||
return;
|
||||
},
|
||||
|
||||
/** Function: debug
|
||||
* Log a message at the Strophe.LogLevel.DEBUG level.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) msg - The log message.
|
||||
*/
|
||||
debug: function(msg)
|
||||
{
|
||||
this.log(this.LogLevel.DEBUG, msg);
|
||||
},
|
||||
|
||||
/** Function: info
|
||||
* Log a message at the Strophe.LogLevel.INFO level.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) msg - The log message.
|
||||
*/
|
||||
info: function (msg)
|
||||
{
|
||||
this.log(this.LogLevel.INFO, msg);
|
||||
},
|
||||
|
||||
/** Function: warn
|
||||
* Log a message at the Strophe.LogLevel.WARN level.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) msg - The log message.
|
||||
*/
|
||||
warn: function (msg)
|
||||
{
|
||||
this.log(this.LogLevel.WARN, msg);
|
||||
},
|
||||
|
||||
/** Function: error
|
||||
* Log a message at the Strophe.LogLevel.ERROR level.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) msg - The log message.
|
||||
*/
|
||||
error: function (msg)
|
||||
{
|
||||
this.log(this.LogLevel.ERROR, msg);
|
||||
},
|
||||
|
||||
/** Function: fatal
|
||||
* Log a message at the Strophe.LogLevel.FATAL level.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) msg - The log message.
|
||||
*/
|
||||
fatal: function (msg)
|
||||
{
|
||||
this.log(this.LogLevel.FATAL, msg);
|
||||
},
|
||||
|
||||
/** Function: serialize
|
||||
* Render a DOM element and all descendants to a String.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - A DOM element.
|
||||
*
|
||||
* Returns:
|
||||
* The serialized element tree as a String.
|
||||
*/
|
||||
serialize: function (elem)
|
||||
{
|
||||
var result;
|
||||
|
||||
if (!elem) { return null; }
|
||||
|
||||
if (typeof(elem.tree) === "function") {
|
||||
elem = elem.tree();
|
||||
}
|
||||
|
||||
var nodeName = elem.nodeName;
|
||||
var i, child;
|
||||
|
||||
if (elem.getAttribute("_realname")) {
|
||||
nodeName = elem.getAttribute("_realname");
|
||||
}
|
||||
|
||||
result = "<" + nodeName;
|
||||
for (i = 0; i < elem.attributes.length; i++) {
|
||||
if(elem.attributes[i].nodeName != "_realname") {
|
||||
result += " " + elem.attributes[i].nodeName.toLowerCase() +
|
||||
"='" + elem.attributes[i].value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/\'/g, "'")
|
||||
.replace(/</g, "<") + "'";
|
||||
}
|
||||
}
|
||||
|
||||
if (elem.childNodes.length > 0) {
|
||||
result += ">";
|
||||
for (i = 0; i < elem.childNodes.length; i++) {
|
||||
child = elem.childNodes[i];
|
||||
switch( child.nodeType ){
|
||||
case Strophe.ElementType.NORMAL:
|
||||
// normal element, so recurse
|
||||
result += Strophe.serialize(child);
|
||||
break;
|
||||
case Strophe.ElementType.TEXT:
|
||||
// text element to escape values
|
||||
result += child.nodeValue;
|
||||
break;
|
||||
case Strophe.ElementType.CDATA:
|
||||
// cdata section so don't escape values
|
||||
result += "<![CDATA["+child.nodeValue+"]]>";
|
||||
}
|
||||
}
|
||||
result += "</" + nodeName + ">";
|
||||
} else {
|
||||
result += "/>";
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/** PrivateVariable: _requestId
|
||||
* _Private_ variable that keeps track of the request ids for
|
||||
* connections.
|
||||
*/
|
||||
_requestId: 0,
|
||||
|
||||
/** PrivateVariable: Strophe.connectionPlugins
|
||||
* _Private_ variable Used to store plugin names that need
|
||||
* initialization on Strophe.Connection construction.
|
||||
*/
|
||||
_connectionPlugins: {},
|
||||
|
||||
/** Function: addConnectionPlugin
|
||||
* Extends the Strophe.Connection object with the given plugin.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) name - The name of the extension.
|
||||
* (Object) ptype - The plugin's prototype.
|
||||
*/
|
||||
addConnectionPlugin: function (name, ptype)
|
||||
{
|
||||
Strophe._connectionPlugins[name] = ptype;
|
||||
}
|
||||
};
|
||||
|
||||
/** Class: Strophe.Builder
|
||||
* XML DOM builder.
|
||||
*
|
||||
* This object provides an interface similar to JQuery but for building
|
||||
* DOM element easily and rapidly. All the functions except for toString()
|
||||
* and tree() return the object, so calls can be chained. Here's an
|
||||
* example using the $iq() builder helper.
|
||||
* > $iq({to: 'you', from: 'me', type: 'get', id: '1'})
|
||||
* > .c('query', {xmlns: 'strophe:example'})
|
||||
* > .c('example')
|
||||
* > .toString()
|
||||
* The above generates this XML fragment
|
||||
* > <iq to='you' from='me' type='get' id='1'>
|
||||
* > <query xmlns='strophe:example'>
|
||||
* > <example/>
|
||||
* > </query>
|
||||
* > </iq>
|
||||
* The corresponding DOM manipulations to get a similar fragment would be
|
||||
* a lot more tedious and probably involve several helper variables.
|
||||
*
|
||||
* Since adding children makes new operations operate on the child, up()
|
||||
* is provided to traverse up the tree. To add two children, do
|
||||
* > builder.c('child1', ...).up().c('child2', ...)
|
||||
* The next operation on the Builder will be relative to the second child.
|
||||
*/
|
||||
|
||||
/** Constructor: Strophe.Builder
|
||||
* Create a Strophe.Builder object.
|
||||
*
|
||||
* The attributes should be passed in object notation. For example
|
||||
* > var b = new Builder('message', {to: 'you', from: 'me'});
|
||||
* or
|
||||
* > var b = new Builder('messsage', {'xml:lang': 'en'});
|
||||
*
|
||||
* Parameters:
|
||||
* (String) name - The name of the root element.
|
||||
* (Object) attrs - The attributes for the root element in object notation.
|
||||
*
|
||||
* Returns:
|
||||
* A new Strophe.Builder.
|
||||
*/
|
||||
Strophe.Builder = function (name, attrs)
|
||||
{
|
||||
// Set correct namespace for jabber:client elements
|
||||
if (name == "presence" || name == "message" || name == "iq") {
|
||||
if (attrs && !attrs.xmlns) {
|
||||
attrs.xmlns = Strophe.NS.CLIENT;
|
||||
} else if (!attrs) {
|
||||
attrs = {xmlns: Strophe.NS.CLIENT};
|
||||
}
|
||||
}
|
||||
|
||||
// Holds the tree being built.
|
||||
this.nodeTree = Strophe.xmlElement(name, attrs);
|
||||
|
||||
// Points to the current operation node.
|
||||
this.node = this.nodeTree;
|
||||
};
|
||||
|
||||
Strophe.Builder.prototype = {
|
||||
/** Function: tree
|
||||
* Return the DOM tree.
|
||||
*
|
||||
* This function returns the current DOM tree as an element object. This
|
||||
* is suitable for passing to functions like Strophe.Connection.send().
|
||||
*
|
||||
* Returns:
|
||||
* The DOM tree as a element object.
|
||||
*/
|
||||
tree: function ()
|
||||
{
|
||||
return this.nodeTree;
|
||||
},
|
||||
|
||||
/** Function: toString
|
||||
* Serialize the DOM tree to a String.
|
||||
*
|
||||
* This function returns a string serialization of the current DOM
|
||||
* tree. It is often used internally to pass data to a
|
||||
* Strophe.Request object.
|
||||
*
|
||||
* Returns:
|
||||
* The serialized DOM tree in a String.
|
||||
*/
|
||||
toString: function ()
|
||||
{
|
||||
return Strophe.serialize(this.nodeTree);
|
||||
},
|
||||
|
||||
/** Function: up
|
||||
* Make the current parent element the new current element.
|
||||
*
|
||||
* This function is often used after c() to traverse back up the tree.
|
||||
* For example, to add two children to the same element
|
||||
* > builder.c('child1', {}).up().c('child2', {});
|
||||
*
|
||||
* Returns:
|
||||
* The Stophe.Builder object.
|
||||
*/
|
||||
up: function ()
|
||||
{
|
||||
this.node = this.node.parentNode;
|
||||
return this;
|
||||
},
|
||||
|
||||
/** Function: attrs
|
||||
* Add or modify attributes of the current element.
|
||||
*
|
||||
* The attributes should be passed in object notation. This function
|
||||
* does not move the current element pointer.
|
||||
*
|
||||
* Parameters:
|
||||
* (Object) moreattrs - The attributes to add/modify in object notation.
|
||||
*
|
||||
* Returns:
|
||||
* The Strophe.Builder object.
|
||||
*/
|
||||
attrs: function (moreattrs)
|
||||
{
|
||||
for (var k in moreattrs) {
|
||||
if (moreattrs.hasOwnProperty(k)) {
|
||||
this.node.setAttribute(k, moreattrs[k]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/** Function: c
|
||||
* Add a child to the current element and make it the new current
|
||||
* element.
|
||||
*
|
||||
* This function moves the current element pointer to the child,
|
||||
* unless text is provided. If you need to add another child, it
|
||||
* is necessary to use up() to go back to the parent in the tree.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) name - The name of the child.
|
||||
* (Object) attrs - The attributes of the child in object notation.
|
||||
* (String) text - The text to add to the child.
|
||||
*
|
||||
* Returns:
|
||||
* The Strophe.Builder object.
|
||||
*/
|
||||
c: function (name, attrs, text)
|
||||
{
|
||||
var child = Strophe.xmlElement(name, attrs, text);
|
||||
this.node.appendChild(child);
|
||||
if (!text) {
|
||||
this.node = child;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/** Function: cnode
|
||||
* Add a child to the current element and make it the new current
|
||||
* element.
|
||||
*
|
||||
* This function is the same as c() except that instead of using a
|
||||
* name and an attributes object to create the child it uses an
|
||||
* existing DOM element object.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - A DOM element.
|
||||
*
|
||||
* Returns:
|
||||
* The Strophe.Builder object.
|
||||
*/
|
||||
cnode: function (elem)
|
||||
{
|
||||
var xmlGen = Strophe.xmlGenerator();
|
||||
try {
|
||||
var impNode = (xmlGen.importNode !== undefined);
|
||||
}
|
||||
catch (e) {
|
||||
var impNode = false;
|
||||
}
|
||||
var newElem = impNode ?
|
||||
xmlGen.importNode(elem, true) :
|
||||
Strophe.copyElement(elem);
|
||||
this.node.appendChild(newElem);
|
||||
this.node = newElem;
|
||||
return this;
|
||||
},
|
||||
|
||||
/** Function: t
|
||||
* Add a child text element.
|
||||
*
|
||||
* This *does not* make the child the new current element since there
|
||||
* are no children of text elements.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) text - The text data to append to the current element.
|
||||
*
|
||||
* Returns:
|
||||
* The Strophe.Builder object.
|
||||
*/
|
||||
t: function (text)
|
||||
{
|
||||
var child = Strophe.xmlTextNode(text);
|
||||
this.node.appendChild(child);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** PrivateClass: Strophe.Handler
|
||||
* _Private_ helper class for managing stanza handlers.
|
||||
*
|
||||
* A Strophe.Handler encapsulates a user provided callback function to be
|
||||
* executed when matching stanzas are received by the connection.
|
||||
* Handlers can be either one-off or persistant depending on their
|
||||
* return value. Returning true will cause a Handler to remain active, and
|
||||
* returning false will remove the Handler.
|
||||
*
|
||||
* Users will not use Strophe.Handler objects directly, but instead they
|
||||
* will use Strophe.Connection.addHandler() and
|
||||
* Strophe.Connection.deleteHandler().
|
||||
*/
|
||||
|
||||
/** PrivateConstructor: Strophe.Handler
|
||||
* Create and initialize a new Strophe.Handler.
|
||||
*
|
||||
* Parameters:
|
||||
* (Function) handler - A function to be executed when the handler is run.
|
||||
* (String) ns - The namespace to match.
|
||||
* (String) name - The element name to match.
|
||||
* (String) type - The element type to match.
|
||||
* (String) id - The element id attribute to match.
|
||||
* (String) from - The element from attribute to match.
|
||||
* (Object) options - Handler options
|
||||
*
|
||||
* Returns:
|
||||
* A new Strophe.Handler object.
|
||||
*/
|
||||
Strophe.Handler = function (handler, ns, name, type, id, from, options)
|
||||
{
|
||||
this.handler = handler;
|
||||
this.ns = ns;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.options = options || {matchbare: false};
|
||||
|
||||
// default matchBare to false if undefined
|
||||
if (!this.options.matchBare) {
|
||||
this.options.matchBare = false;
|
||||
}
|
||||
|
||||
if (this.options.matchBare) {
|
||||
this.from = from ? Strophe.getBareJidFromJid(from) : null;
|
||||
} else {
|
||||
this.from = from;
|
||||
}
|
||||
|
||||
// whether the handler is a user handler or a system handler
|
||||
this.user = true;
|
||||
};
|
||||
|
||||
Strophe.Handler.prototype = {
|
||||
/** PrivateFunction: isMatch
|
||||
* Tests if a stanza matches the Strophe.Handler.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The XML element to test.
|
||||
*
|
||||
* Returns:
|
||||
* true if the stanza matches and false otherwise.
|
||||
*/
|
||||
isMatch: function (elem)
|
||||
{
|
||||
var nsMatch;
|
||||
var from = null;
|
||||
|
||||
if (this.options.matchBare) {
|
||||
from = Strophe.getBareJidFromJid(elem.getAttribute('from'));
|
||||
} else {
|
||||
from = elem.getAttribute('from');
|
||||
}
|
||||
|
||||
nsMatch = false;
|
||||
if (!this.ns) {
|
||||
nsMatch = true;
|
||||
} else {
|
||||
var that = this;
|
||||
Strophe.forEachChild(elem, null, function (elem) {
|
||||
if (elem.getAttribute("xmlns") == that.ns) {
|
||||
nsMatch = true;
|
||||
}
|
||||
});
|
||||
|
||||
nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns;
|
||||
}
|
||||
|
||||
if (nsMatch &&
|
||||
(!this.name || Strophe.isTagEqual(elem, this.name)) &&
|
||||
(!this.type || elem.getAttribute("type") == this.type) &&
|
||||
(!this.id || elem.getAttribute("id") == this.id) &&
|
||||
(!this.from || from == this.from)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: run
|
||||
* Run the callback on a matching stanza.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The DOM element that triggered the
|
||||
* Strophe.Handler.
|
||||
*
|
||||
* Returns:
|
||||
* A boolean indicating if the handler should remain active.
|
||||
*/
|
||||
run: function (elem)
|
||||
{
|
||||
var result = null;
|
||||
try {
|
||||
result = this.handler(elem);
|
||||
} catch (e) {
|
||||
if (e.sourceURL) {
|
||||
Strophe.fatal("error: " + this.handler +
|
||||
" " + e.sourceURL + ":" +
|
||||
e.line + " - " + e.name + ": " + e.message);
|
||||
} else if (e.fileName) {
|
||||
if (typeof(console) != "undefined") {
|
||||
console.trace();
|
||||
console.error(this.handler, " - error - ", e, e.message);
|
||||
}
|
||||
Strophe.fatal("error: " + this.handler + " " +
|
||||
e.fileName + ":" + e.lineNumber + " - " +
|
||||
e.name + ": " + e.message);
|
||||
} else {
|
||||
Strophe.fatal("error: " + this.handler);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/** PrivateFunction: toString
|
||||
* Get a String representation of the Strophe.Handler object.
|
||||
*
|
||||
* Returns:
|
||||
* A String.
|
||||
*/
|
||||
toString: function ()
|
||||
{
|
||||
return "{Handler: " + this.handler + "(" + this.name + "," +
|
||||
this.id + "," + this.ns + ")}";
|
||||
}
|
||||
};
|
||||
|
||||
/** PrivateClass: Strophe.TimedHandler
|
||||
* _Private_ helper class for managing timed handlers.
|
||||
*
|
||||
* A Strophe.TimedHandler encapsulates a user provided callback that
|
||||
* should be called after a certain period of time or at regular
|
||||
* intervals. The return value of the callback determines whether the
|
||||
* Strophe.TimedHandler will continue to fire.
|
||||
*
|
||||
* Users will not use Strophe.TimedHandler objects directly, but instead
|
||||
* they will use Strophe.Connection.addTimedHandler() and
|
||||
* Strophe.Connection.deleteTimedHandler().
|
||||
*/
|
||||
|
||||
/** PrivateConstructor: Strophe.TimedHandler
|
||||
* Create and initialize a new Strophe.TimedHandler object.
|
||||
*
|
||||
* Parameters:
|
||||
* (Integer) period - The number of milliseconds to wait before the
|
||||
* handler is called.
|
||||
* (Function) handler - The callback to run when the handler fires. This
|
||||
* function should take no arguments.
|
||||
*
|
||||
* Returns:
|
||||
* A new Strophe.TimedHandler object.
|
||||
*/
|
||||
Strophe.TimedHandler = function (period, handler)
|
||||
{
|
||||
this.period = period;
|
||||
this.handler = handler;
|
||||
|
||||
this.lastCalled = new Date().getTime();
|
||||
this.user = true;
|
||||
};
|
||||
|
||||
Strophe.TimedHandler.prototype = {
|
||||
/** PrivateFunction: run
|
||||
* Run the callback for the Strophe.TimedHandler.
|
||||
*
|
||||
* Returns:
|
||||
* true if the Strophe.TimedHandler should be called again, and false
|
||||
* otherwise.
|
||||
*/
|
||||
run: function ()
|
||||
{
|
||||
this.lastCalled = new Date().getTime();
|
||||
return this.handler();
|
||||
},
|
||||
|
||||
/** PrivateFunction: reset
|
||||
* Reset the last called time for the Strophe.TimedHandler.
|
||||
*/
|
||||
reset: function ()
|
||||
{
|
||||
this.lastCalled = new Date().getTime();
|
||||
},
|
||||
|
||||
/** PrivateFunction: toString
|
||||
* Get a string representation of the Strophe.TimedHandler object.
|
||||
*
|
||||
* Returns:
|
||||
* The string representation.
|
||||
*/
|
||||
toString: function ()
|
||||
{
|
||||
return "{TimedHandler: " + this.handler + "(" + this.period +")}";
|
||||
}
|
||||
};
|
||||
|
||||
/** PrivateClass: Strophe.Request
|
||||
* _Private_ helper class that provides a cross implementation abstraction
|
||||
* for a BOSH related XMLHttpRequest.
|
||||
*
|
||||
* The Strophe.Request class is used internally to encapsulate BOSH request
|
||||
* information. It is not meant to be used from user's code.
|
||||
*/
|
||||
|
||||
/** PrivateConstructor: Strophe.Request
|
||||
* Create and initialize a new Strophe.Request object.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The XML data to be sent in the request.
|
||||
* (Function) func - The function that will be called when the
|
||||
* XMLHttpRequest readyState changes.
|
||||
* (Integer) rid - The BOSH rid attribute associated with this request.
|
||||
* (Integer) sends - The number of times this same request has been
|
||||
* sent.
|
||||
*/
|
||||
Strophe.Request = function (elem, func, rid, sends)
|
||||
{
|
||||
this.id = ++Strophe._requestId;
|
||||
this.xmlData = elem;
|
||||
this.data = Strophe.serialize(elem);
|
||||
// save original function in case we need to make a new request
|
||||
// from this one.
|
||||
this.origFunc = func;
|
||||
this.func = func;
|
||||
this.rid = rid;
|
||||
this.date = NaN;
|
||||
this.sends = sends || 0;
|
||||
this.abort = false;
|
||||
this.dead = null;
|
||||
this.age = function () {
|
||||
if (!this.date) { return 0; }
|
||||
var now = new Date();
|
||||
return (now - this.date) / 1000;
|
||||
};
|
||||
this.timeDead = function () {
|
||||
if (!this.dead) { return 0; }
|
||||
var now = new Date();
|
||||
return (now - this.dead) / 1000;
|
||||
};
|
||||
this.xhr = this._newXHR();
|
||||
};
|
||||
|
||||
Strophe.Request.prototype = {
|
||||
/** PrivateFunction: getResponse
|
||||
* Get a response from the underlying XMLHttpRequest.
|
||||
*
|
||||
* This function attempts to get a response from the request and checks
|
||||
* for errors.
|
||||
*
|
||||
* Throws:
|
||||
* "parsererror" - A parser error occured.
|
||||
*
|
||||
* Returns:
|
||||
* The DOM element tree of the response.
|
||||
*/
|
||||
getResponse: function ()
|
||||
{
|
||||
var node = null;
|
||||
if (this.xhr.responseXML && this.xhr.responseXML.documentElement) {
|
||||
node = this.xhr.responseXML.documentElement;
|
||||
if (node.tagName == "parsererror") {
|
||||
Strophe.error("invalid response received");
|
||||
Strophe.error("responseText: " + this.xhr.responseText);
|
||||
Strophe.error("responseXML: " +
|
||||
Strophe.serialize(this.xhr.responseXML));
|
||||
throw "parsererror";
|
||||
}
|
||||
} else if (this.xhr.responseText) {
|
||||
Strophe.error("invalid response received");
|
||||
Strophe.error("responseText: " + this.xhr.responseText);
|
||||
Strophe.error("responseXML: " +
|
||||
Strophe.serialize(this.xhr.responseXML));
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _newXHR
|
||||
* _Private_ helper function to create XMLHttpRequests.
|
||||
*
|
||||
* This function creates XMLHttpRequests across all implementations.
|
||||
*
|
||||
* Returns:
|
||||
* A new XMLHttpRequest.
|
||||
*/
|
||||
_newXHR: function ()
|
||||
{
|
||||
var xhr = null;
|
||||
if (window.XMLHttpRequest) {
|
||||
xhr = new XMLHttpRequest();
|
||||
if (xhr.overrideMimeType) {
|
||||
xhr.overrideMimeType("text/xml");
|
||||
}
|
||||
} else if (window.ActiveXObject) {
|
||||
xhr = new ActiveXObject("Microsoft.XMLHTTP");
|
||||
}
|
||||
|
||||
// use Function.bind() to prepend ourselves as an argument
|
||||
xhr.onreadystatechange = this.func.bind(null, this);
|
||||
|
||||
return xhr;
|
||||
}
|
||||
};
|
||||
|
||||
/** Class: Strophe.Connection
|
||||
* XMPP Connection manager.
|
||||
*
|
||||
* Thie class is the main part of Strophe. It manages a BOSH connection
|
||||
* to an XMPP server and dispatches events to the user callbacks as
|
||||
* data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy
|
||||
* authentication.
|
||||
*
|
||||
* After creating a Strophe.Connection object, the user will typically
|
||||
* call connect() with a user supplied callback to handle connection level
|
||||
* events like authentication failure, disconnection, or connection
|
||||
* complete.
|
||||
*
|
||||
* The user will also have several event handlers defined by using
|
||||
* addHandler() and addTimedHandler(). These will allow the user code to
|
||||
* respond to interesting stanzas or do something periodically with the
|
||||
* connection. These handlers will be active once authentication is
|
||||
* finished.
|
||||
*
|
||||
* To send data to the connection, use send().
|
||||
*/
|
||||
|
||||
/** Constructor: Strophe.Connection
|
||||
* Create and initialize a Strophe.Connection object.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) service - The BOSH service URL.
|
||||
*
|
||||
* Returns:
|
||||
* A new Strophe.Connection object.
|
||||
*/
|
||||
Strophe.Connection = function (service)
|
||||
{
|
||||
/* The path to the httpbind service. */
|
||||
this.service = service;
|
||||
/* The connected JID. */
|
||||
this.jid = "";
|
||||
/* request id for body tags */
|
||||
this.rid = Math.floor(Math.random() * 4294967295);
|
||||
/* The current session ID. */
|
||||
this.sid = null;
|
||||
this.streamId = null;
|
||||
/* stream:features */
|
||||
this.features = null;
|
||||
|
||||
// SASL
|
||||
this.do_session = false;
|
||||
this.do_bind = false;
|
||||
|
||||
// handler lists
|
||||
this.timedHandlers = [];
|
||||
this.handlers = [];
|
||||
this.removeTimeds = [];
|
||||
this.removeHandlers = [];
|
||||
this.addTimeds = [];
|
||||
this.addHandlers = [];
|
||||
|
||||
this._idleTimeout = null;
|
||||
this._disconnectTimeout = null;
|
||||
|
||||
this.authenticated = false;
|
||||
this.disconnecting = false;
|
||||
this.connected = false;
|
||||
|
||||
this.errors = 0;
|
||||
|
||||
this.paused = false;
|
||||
|
||||
// default BOSH values
|
||||
this.hold = 1;
|
||||
this.wait = 60;
|
||||
this.window = 5;
|
||||
|
||||
this._data = [];
|
||||
this._requests = [];
|
||||
this._uniqueId = Math.round(Math.random() * 10000);
|
||||
|
||||
this._sasl_success_handler = null;
|
||||
this._sasl_failure_handler = null;
|
||||
this._sasl_challenge_handler = null;
|
||||
|
||||
// setup onIdle callback every 1/10th of a second
|
||||
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
|
||||
|
||||
// initialize plugins
|
||||
for (var k in Strophe._connectionPlugins) {
|
||||
if (Strophe._connectionPlugins.hasOwnProperty(k)) {
|
||||
var ptype = Strophe._connectionPlugins[k];
|
||||
// jslint complaints about the below line, but this is fine
|
||||
var F = function () {};
|
||||
F.prototype = ptype;
|
||||
this[k] = new F();
|
||||
this[k].init(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Strophe.Connection.prototype = {
|
||||
/** Function: reset
|
||||
* Reset the connection.
|
||||
*
|
||||
* This function should be called after a connection is disconnected
|
||||
* before that connection is reused.
|
||||
*/
|
||||
reset: function ()
|
||||
{
|
||||
this.rid = Math.floor(Math.random() * 4294967295);
|
||||
|
||||
this.sid = null;
|
||||
this.streamId = null;
|
||||
|
||||
// SASL
|
||||
this.do_session = false;
|
||||
this.do_bind = false;
|
||||
|
||||
// handler lists
|
||||
this.timedHandlers = [];
|
||||
this.handlers = [];
|
||||
this.removeTimeds = [];
|
||||
this.removeHandlers = [];
|
||||
this.addTimeds = [];
|
||||
this.addHandlers = [];
|
||||
|
||||
this.authenticated = false;
|
||||
this.disconnecting = false;
|
||||
this.connected = false;
|
||||
|
||||
this.errors = 0;
|
||||
|
||||
this._requests = [];
|
||||
this._uniqueId = Math.round(Math.random()*10000);
|
||||
},
|
||||
|
||||
/** Function: pause
|
||||
* Pause the request manager.
|
||||
*
|
||||
* This will prevent Strophe from sending any more requests to the
|
||||
* server. This is very useful for temporarily pausing while a lot
|
||||
* of send() calls are happening quickly. This causes Strophe to
|
||||
* send the data in a single request, saving many request trips.
|
||||
*/
|
||||
pause: function ()
|
||||
{
|
||||
this.paused = true;
|
||||
},
|
||||
|
||||
/** Function: resume
|
||||
* Resume the request manager.
|
||||
*
|
||||
* This resumes after pause() has been called.
|
||||
*/
|
||||
resume: function ()
|
||||
{
|
||||
this.paused = false;
|
||||
},
|
||||
|
||||
/** Function: getUniqueId
|
||||
* Generate a unique ID for use in <iq/> elements.
|
||||
*
|
||||
* All <iq/> stanzas are required to have unique id attributes. This
|
||||
* function makes creating these easy. Each connection instance has
|
||||
* a counter which starts from zero, and the value of this counter
|
||||
* plus a colon followed by the suffix becomes the unique id. If no
|
||||
* suffix is supplied, the counter is used as the unique id.
|
||||
*
|
||||
* Suffixes are used to make debugging easier when reading the stream
|
||||
* data, and their use is recommended. The counter resets to 0 for
|
||||
* every new connection for the same reason. For connections to the
|
||||
* same server that authenticate the same way, all the ids should be
|
||||
* the same, which makes it easy to see changes. This is useful for
|
||||
* automated testing as well.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) suffix - A optional suffix to append to the id.
|
||||
*
|
||||
* Returns:
|
||||
* A unique string to be used for the id attribute.
|
||||
*/
|
||||
getUniqueId: function (suffix)
|
||||
{
|
||||
if (typeof(suffix) == "string" || typeof(suffix) == "number") {
|
||||
return ++this._uniqueId + ":" + suffix;
|
||||
} else {
|
||||
return ++this._uniqueId + "";
|
||||
}
|
||||
},
|
||||
|
||||
/** Function: connect
|
||||
* Starts the connection process.
|
||||
*
|
||||
* As the connection process proceeds, the user supplied callback will
|
||||
* be triggered multiple times with status updates. The callback
|
||||
* should take two arguments - the status code and the error condition.
|
||||
*
|
||||
* The status code will be one of the values in the Strophe.Status
|
||||
* constants. The error condition will be one of the conditions
|
||||
* defined in RFC 3920 or the condition 'strophe-parsererror'.
|
||||
*
|
||||
* Please see XEP 124 for a more detailed explanation of the optional
|
||||
* parameters below.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid - The user's JID. This may be a bare JID,
|
||||
* or a full JID. If a node is not supplied, SASL ANONYMOUS
|
||||
* authentication will be attempted.
|
||||
* (String) pass - The user's password.
|
||||
* (Function) callback - The connect callback function.
|
||||
* (Integer) wait - The optional HTTPBIND wait value. This is the
|
||||
* time the server will wait before returning an empty result for
|
||||
* a request. The default setting of 60 seconds is recommended.
|
||||
* Other settings will require tweaks to the Strophe.TIMEOUT value.
|
||||
* (Integer) hold - The optional HTTPBIND hold value. This is the
|
||||
* number of connections the server will hold at one time. This
|
||||
* should almost always be set to 1 (the default).
|
||||
*/
|
||||
connect: function (jid, pass, callback, wait, hold)
|
||||
{
|
||||
this.jid = jid;
|
||||
this.pass = pass;
|
||||
this.connect_callback = callback;
|
||||
this.disconnecting = false;
|
||||
this.connected = false;
|
||||
this.authenticated = false;
|
||||
this.errors = 0;
|
||||
|
||||
this.wait = wait || this.wait;
|
||||
this.hold = hold || this.hold;
|
||||
|
||||
// parse jid for domain and resource
|
||||
this.domain = Strophe.getDomainFromJid(this.jid);
|
||||
|
||||
// build the body tag
|
||||
var body = this._buildBody().attrs({
|
||||
to: this.domain,
|
||||
"xml:lang": "en",
|
||||
wait: this.wait,
|
||||
hold: this.hold,
|
||||
content: "text/xml; charset=utf-8",
|
||||
ver: "1.6",
|
||||
"xmpp:version": "1.0",
|
||||
"xmlns:xmpp": Strophe.NS.BOSH
|
||||
});
|
||||
|
||||
this._changeConnectStatus(Strophe.Status.CONNECTING, null);
|
||||
|
||||
this._requests.push(
|
||||
new Strophe.Request(body.tree(),
|
||||
this._onRequestStateChange.bind(
|
||||
this, this._connect_cb.bind(this)),
|
||||
body.tree().getAttribute("rid")));
|
||||
this._throttledRequestHandler();
|
||||
},
|
||||
|
||||
/** Function: attach
|
||||
* Attach to an already created and authenticated BOSH session.
|
||||
*
|
||||
* This function is provided to allow Strophe to attach to BOSH
|
||||
* sessions which have been created externally, perhaps by a Web
|
||||
* application. This is often used to support auto-login type features
|
||||
* without putting user credentials into the page.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid - The full JID that is bound by the session.
|
||||
* (String) sid - The SID of the BOSH session.
|
||||
* (String) rid - The current RID of the BOSH session. This RID
|
||||
* will be used by the next request.
|
||||
* (Function) callback The connect callback function.
|
||||
* (Integer) wait - The optional HTTPBIND wait value. This is the
|
||||
* time the server will wait before returning an empty result for
|
||||
* a request. The default setting of 60 seconds is recommended.
|
||||
* Other settings will require tweaks to the Strophe.TIMEOUT value.
|
||||
* (Integer) hold - The optional HTTPBIND hold value. This is the
|
||||
* number of connections the server will hold at one time. This
|
||||
* should almost always be set to 1 (the default).
|
||||
* (Integer) wind - The optional HTTBIND window value. This is the
|
||||
* allowed range of request ids that are valid. The default is 5.
|
||||
*/
|
||||
attach: function (jid, sid, rid, callback, wait, hold, wind)
|
||||
{
|
||||
this.jid = jid;
|
||||
this.sid = sid;
|
||||
this.rid = rid;
|
||||
this.connect_callback = callback;
|
||||
|
||||
this.domain = Strophe.getDomainFromJid(this.jid);
|
||||
|
||||
this.authenticated = true;
|
||||
this.connected = true;
|
||||
|
||||
this.wait = wait || this.wait;
|
||||
this.hold = hold || this.hold;
|
||||
this.window = wind || this.window;
|
||||
|
||||
this._changeConnectStatus(Strophe.Status.ATTACHED, null);
|
||||
},
|
||||
|
||||
/** Function: xmlInput
|
||||
* User overrideable function that receives XML data coming into the
|
||||
* connection.
|
||||
*
|
||||
* The default function does nothing. User code can override this with
|
||||
* > Strophe.Connection.xmlInput = function (elem) {
|
||||
* > (user code)
|
||||
* > };
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The XML data received by the connection.
|
||||
*/
|
||||
xmlInput: function (elem)
|
||||
{
|
||||
return;
|
||||
},
|
||||
|
||||
/** Function: xmlOutput
|
||||
* User overrideable function that receives XML data sent to the
|
||||
* connection.
|
||||
*
|
||||
* The default function does nothing. User code can override this with
|
||||
* > Strophe.Connection.xmlOutput = function (elem) {
|
||||
* > (user code)
|
||||
* > };
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The XMLdata sent by the connection.
|
||||
*/
|
||||
xmlOutput: function (elem)
|
||||
{
|
||||
return;
|
||||
},
|
||||
|
||||
/** Function: rawInput
|
||||
* User overrideable function that receives raw data coming into the
|
||||
* connection.
|
||||
*
|
||||
* The default function does nothing. User code can override this with
|
||||
* > Strophe.Connection.rawInput = function (data) {
|
||||
* > (user code)
|
||||
* > };
|
||||
*
|
||||
* Parameters:
|
||||
* (String) data - The data received by the connection.
|
||||
*/
|
||||
rawInput: function (data)
|
||||
{
|
||||
return;
|
||||
},
|
||||
|
||||
/** Function: rawOutput
|
||||
* User overrideable function that receives raw data sent to the
|
||||
* connection.
|
||||
*
|
||||
* The default function does nothing. User code can override this with
|
||||
* > Strophe.Connection.rawOutput = function (data) {
|
||||
* > (user code)
|
||||
* > };
|
||||
*
|
||||
* Parameters:
|
||||
* (String) data - The data sent by the connection.
|
||||
*/
|
||||
rawOutput: function (data)
|
||||
{
|
||||
return;
|
||||
},
|
||||
|
||||
/** Function: send
|
||||
* Send a stanza.
|
||||
*
|
||||
* This function is called to push data onto the send queue to
|
||||
* go out over the wire. Whenever a request is sent to the BOSH
|
||||
* server, all pending data is sent and the queue is flushed.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement |
|
||||
* [XMLElement] |
|
||||
* Strophe.Builder) elem - The stanza to send.
|
||||
*/
|
||||
send: function (elem)
|
||||
{
|
||||
if (elem === null) { return ; }
|
||||
if (typeof(elem.sort) === "function") {
|
||||
for (var i = 0; i < elem.length; i++) {
|
||||
this._queueData(elem[i]);
|
||||
}
|
||||
} else if (typeof(elem.tree) === "function") {
|
||||
this._queueData(elem.tree());
|
||||
} else {
|
||||
this._queueData(elem);
|
||||
}
|
||||
|
||||
this._throttledRequestHandler();
|
||||
clearTimeout(this._idleTimeout);
|
||||
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
|
||||
},
|
||||
|
||||
/** Function: flush
|
||||
* Immediately send any pending outgoing data.
|
||||
*
|
||||
* Normally send() queues outgoing data until the next idle period
|
||||
* (100ms), which optimizes network use in the common cases when
|
||||
* several send()s are called in succession. flush() can be used to
|
||||
* immediately send all pending data.
|
||||
*/
|
||||
flush: function ()
|
||||
{
|
||||
// cancel the pending idle period and run the idle function
|
||||
// immediately
|
||||
clearTimeout(this._idleTimeout);
|
||||
this._onIdle();
|
||||
},
|
||||
|
||||
/** Function: sendIQ
|
||||
* Helper function to send IQ stanzas.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The stanza to send.
|
||||
* (Function) callback - The callback function for a successful request.
|
||||
* (Function) errback - The callback function for a failed or timed
|
||||
* out request. On timeout, the stanza will be null.
|
||||
* (Integer) timeout - The time specified in milliseconds for a
|
||||
* timeout to occur.
|
||||
*
|
||||
* Returns:
|
||||
* The id used to send the IQ.
|
||||
*/
|
||||
sendIQ: function(elem, callback, errback, timeout) {
|
||||
var timeoutHandler = null;
|
||||
var that = this;
|
||||
|
||||
if (typeof(elem.tree) === "function") {
|
||||
elem = elem.tree();
|
||||
}
|
||||
var id = elem.getAttribute('id');
|
||||
|
||||
// inject id if not found
|
||||
if (!id) {
|
||||
id = this.getUniqueId("sendIQ");
|
||||
elem.setAttribute("id", id);
|
||||
}
|
||||
|
||||
var handler = this.addHandler(function (stanza) {
|
||||
// remove timeout handler if there is one
|
||||
if (timeoutHandler) {
|
||||
that.deleteTimedHandler(timeoutHandler);
|
||||
}
|
||||
|
||||
var iqtype = stanza.getAttribute('type');
|
||||
if (iqtype == 'result') {
|
||||
if (callback) {
|
||||
callback(stanza);
|
||||
}
|
||||
} else if (iqtype == 'error') {
|
||||
if (errback) {
|
||||
errback(stanza);
|
||||
}
|
||||
} else {
|
||||
throw {
|
||||
name: "StropheError",
|
||||
message: "Got bad IQ type of " + iqtype
|
||||
};
|
||||
}
|
||||
}, null, 'iq', null, id);
|
||||
|
||||
// if timeout specified, setup timeout handler.
|
||||
if (timeout) {
|
||||
timeoutHandler = this.addTimedHandler(timeout, function () {
|
||||
// get rid of normal handler
|
||||
that.deleteHandler(handler);
|
||||
|
||||
// call errback on timeout with null stanza
|
||||
if (errback) {
|
||||
errback(null);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
this.send(elem);
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _queueData
|
||||
* Queue outgoing data for later sending. Also ensures that the data
|
||||
* is a DOMElement.
|
||||
*/
|
||||
_queueData: function (element) {
|
||||
if (element === null ||
|
||||
!element.tagName ||
|
||||
!element.childNodes) {
|
||||
throw {
|
||||
name: "StropheError",
|
||||
message: "Cannot queue non-DOMElement."
|
||||
};
|
||||
}
|
||||
|
||||
this._data.push(element);
|
||||
},
|
||||
|
||||
/** PrivateFunction: _sendRestart
|
||||
* Send an xmpp:restart stanza.
|
||||
*/
|
||||
_sendRestart: function ()
|
||||
{
|
||||
this._data.push("restart");
|
||||
|
||||
this._throttledRequestHandler();
|
||||
clearTimeout(this._idleTimeout);
|
||||
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
|
||||
},
|
||||
|
||||
/** Function: addTimedHandler
|
||||
* Add a timed handler to the connection.
|
||||
*
|
||||
* This function adds a timed handler. The provided handler will
|
||||
* be called every period milliseconds until it returns false,
|
||||
* the connection is terminated, or the handler is removed. Handlers
|
||||
* that wish to continue being invoked should return true.
|
||||
*
|
||||
* Because of method binding it is necessary to save the result of
|
||||
* this function if you wish to remove a handler with
|
||||
* deleteTimedHandler().
|
||||
*
|
||||
* Note that user handlers are not active until authentication is
|
||||
* successful.
|
||||
*
|
||||
* Parameters:
|
||||
* (Integer) period - The period of the handler.
|
||||
* (Function) handler - The callback function.
|
||||
*
|
||||
* Returns:
|
||||
* A reference to the handler that can be used to remove it.
|
||||
*/
|
||||
addTimedHandler: function (period, handler)
|
||||
{
|
||||
var thand = new Strophe.TimedHandler(period, handler);
|
||||
this.addTimeds.push(thand);
|
||||
return thand;
|
||||
},
|
||||
|
||||
/** Function: deleteTimedHandler
|
||||
* Delete a timed handler for a connection.
|
||||
*
|
||||
* This function removes a timed handler from the connection. The
|
||||
* handRef parameter is *not* the function passed to addTimedHandler(),
|
||||
* but is the reference returned from addTimedHandler().
|
||||
*
|
||||
* Parameters:
|
||||
* (Strophe.TimedHandler) handRef - The handler reference.
|
||||
*/
|
||||
deleteTimedHandler: function (handRef)
|
||||
{
|
||||
// this must be done in the Idle loop so that we don't change
|
||||
// the handlers during iteration
|
||||
this.removeTimeds.push(handRef);
|
||||
},
|
||||
|
||||
/** Function: addHandler
|
||||
* Add a stanza handler for the connection.
|
||||
*
|
||||
* This function adds a stanza handler to the connection. The
|
||||
* handler callback will be called for any stanza that matches
|
||||
* the parameters. Note that if multiple parameters are supplied,
|
||||
* they must all match for the handler to be invoked.
|
||||
*
|
||||
* The handler will receive the stanza that triggered it as its argument.
|
||||
* The handler should return true if it is to be invoked again;
|
||||
* returning false will remove the handler after it returns.
|
||||
*
|
||||
* As a convenience, the ns parameters applies to the top level element
|
||||
* and also any of its immediate children. This is primarily to make
|
||||
* matching /iq/query elements easy.
|
||||
*
|
||||
* The options argument contains handler matching flags that affect how
|
||||
* matches are determined. Currently the only flag is matchBare (a
|
||||
* boolean). When matchBare is true, the from parameter and the from
|
||||
* attribute on the stanza will be matched as bare JIDs instead of
|
||||
* full JIDs. To use this, pass {matchBare: true} as the value of
|
||||
* options. The default value for matchBare is false.
|
||||
*
|
||||
* The return value should be saved if you wish to remove the handler
|
||||
* with deleteHandler().
|
||||
*
|
||||
* Parameters:
|
||||
* (Function) handler - The user callback.
|
||||
* (String) ns - The namespace to match.
|
||||
* (String) name - The stanza name to match.
|
||||
* (String) type - The stanza type attribute to match.
|
||||
* (String) id - The stanza id attribute to match.
|
||||
* (String) from - The stanza from attribute to match.
|
||||
* (String) options - The handler options
|
||||
*
|
||||
* Returns:
|
||||
* A reference to the handler that can be used to remove it.
|
||||
*/
|
||||
addHandler: function (handler, ns, name, type, id, from, options)
|
||||
{
|
||||
var hand = new Strophe.Handler(handler, ns, name, type, id, from, options);
|
||||
this.addHandlers.push(hand);
|
||||
return hand;
|
||||
},
|
||||
|
||||
/** Function: deleteHandler
|
||||
* Delete a stanza handler for a connection.
|
||||
*
|
||||
* This function removes a stanza handler from the connection. The
|
||||
* handRef parameter is *not* the function passed to addHandler(),
|
||||
* but is the reference returned from addHandler().
|
||||
*
|
||||
* Parameters:
|
||||
* (Strophe.Handler) handRef - The handler reference.
|
||||
*/
|
||||
deleteHandler: function (handRef)
|
||||
{
|
||||
// this must be done in the Idle loop so that we don't change
|
||||
// the handlers during iteration
|
||||
this.removeHandlers.push(handRef);
|
||||
},
|
||||
|
||||
/** Function: disconnect
|
||||
* Start the graceful disconnection process.
|
||||
*
|
||||
* This function starts the disconnection process. This process starts
|
||||
* by sending unavailable presence and sending BOSH body of type
|
||||
* terminate. A timeout handler makes sure that disconnection happens
|
||||
* even if the BOSH server does not respond.
|
||||
*
|
||||
* The user supplied connection callback will be notified of the
|
||||
* progress as this process happens.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) reason - The reason the disconnect is occuring.
|
||||
*/
|
||||
disconnect: function (reason)
|
||||
{
|
||||
this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason);
|
||||
|
||||
Strophe.info("Disconnect was called because: " + reason);
|
||||
if (this.connected) {
|
||||
// setup timeout handler
|
||||
this._disconnectTimeout = this._addSysTimedHandler(
|
||||
3000, this._onDisconnectTimeout.bind(this));
|
||||
this._sendTerminate();
|
||||
}
|
||||
},
|
||||
|
||||
/** PrivateFunction: _changeConnectStatus
|
||||
* _Private_ helper function that makes sure plugins and the user's
|
||||
* callback are notified of connection status changes.
|
||||
*
|
||||
* Parameters:
|
||||
* (Integer) status - the new connection status, one of the values
|
||||
* in Strophe.Status
|
||||
* (String) condition - the error condition or null
|
||||
*/
|
||||
_changeConnectStatus: function (status, condition)
|
||||
{
|
||||
// notify all plugins listening for status changes
|
||||
for (var k in Strophe._connectionPlugins) {
|
||||
if (Strophe._connectionPlugins.hasOwnProperty(k)) {
|
||||
var plugin = this[k];
|
||||
if (plugin.statusChanged) {
|
||||
try {
|
||||
plugin.statusChanged(status, condition);
|
||||
} catch (err) {
|
||||
Strophe.error("" + k + " plugin caused an exception " +
|
||||
"changing status: " + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// notify the user's callback
|
||||
if (this.connect_callback) {
|
||||
try {
|
||||
this.connect_callback(status, condition);
|
||||
} catch (e) {
|
||||
Strophe.error("User connection callback caused an " +
|
||||
"exception: " + e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** PrivateFunction: _buildBody
|
||||
* _Private_ helper function to generate the <body/> wrapper for BOSH.
|
||||
*
|
||||
* Returns:
|
||||
* A Strophe.Builder with a <body/> element.
|
||||
*/
|
||||
_buildBody: function ()
|
||||
{
|
||||
var bodyWrap = $build('body', {
|
||||
rid: this.rid++,
|
||||
xmlns: Strophe.NS.HTTPBIND
|
||||
});
|
||||
|
||||
if (this.sid !== null) {
|
||||
bodyWrap.attrs({sid: this.sid});
|
||||
}
|
||||
|
||||
return bodyWrap;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _removeRequest
|
||||
* _Private_ function to remove a request from the queue.
|
||||
*
|
||||
* Parameters:
|
||||
* (Strophe.Request) req - The request to remove.
|
||||
*/
|
||||
_removeRequest: function (req)
|
||||
{
|
||||
Strophe.debug("removing request");
|
||||
|
||||
var i;
|
||||
for (i = this._requests.length - 1; i >= 0; i--) {
|
||||
if (req == this._requests[i]) {
|
||||
this._requests.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// IE6 fails on setting to null, so set to empty function
|
||||
req.xhr.onreadystatechange = function () {};
|
||||
|
||||
this._throttledRequestHandler();
|
||||
},
|
||||
|
||||
/** PrivateFunction: _restartRequest
|
||||
* _Private_ function to restart a request that is presumed dead.
|
||||
*
|
||||
* Parameters:
|
||||
* (Integer) i - The index of the request in the queue.
|
||||
*/
|
||||
_restartRequest: function (i)
|
||||
{
|
||||
var req = this._requests[i];
|
||||
if (req.dead === null) {
|
||||
req.dead = new Date();
|
||||
}
|
||||
|
||||
this._processRequest(i);
|
||||
},
|
||||
|
||||
/** PrivateFunction: _processRequest
|
||||
* _Private_ function to process a request in the queue.
|
||||
*
|
||||
* This function takes requests off the queue and sends them and
|
||||
* restarts dead requests.
|
||||
*
|
||||
* Parameters:
|
||||
* (Integer) i - The index of the request in the queue.
|
||||
*/
|
||||
_processRequest: function (i)
|
||||
{
|
||||
var req = this._requests[i];
|
||||
var reqStatus = -1;
|
||||
|
||||
try {
|
||||
if (req.xhr.readyState == 4) {
|
||||
reqStatus = req.xhr.status;
|
||||
}
|
||||
} catch (e) {
|
||||
Strophe.error("caught an error in _requests[" + i +
|
||||
"], reqStatus: " + reqStatus);
|
||||
}
|
||||
|
||||
if (typeof(reqStatus) == "undefined") {
|
||||
reqStatus = -1;
|
||||
}
|
||||
|
||||
// make sure we limit the number of retries
|
||||
if (req.sends > 5) {
|
||||
this._onDisconnectTimeout();
|
||||
return;
|
||||
}
|
||||
|
||||
var time_elapsed = req.age();
|
||||
var primaryTimeout = (!isNaN(time_elapsed) &&
|
||||
time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait));
|
||||
var secondaryTimeout = (req.dead !== null &&
|
||||
req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait));
|
||||
var requestCompletedWithServerError = (req.xhr.readyState == 4 &&
|
||||
(reqStatus < 1 ||
|
||||
reqStatus >= 500));
|
||||
if (primaryTimeout || secondaryTimeout ||
|
||||
requestCompletedWithServerError) {
|
||||
if (secondaryTimeout) {
|
||||
Strophe.error("Request " +
|
||||
this._requests[i].id +
|
||||
" timed out (secondary), restarting");
|
||||
}
|
||||
req.abort = true;
|
||||
req.xhr.abort();
|
||||
// setting to null fails on IE6, so set to empty function
|
||||
req.xhr.onreadystatechange = function () {};
|
||||
this._requests[i] = new Strophe.Request(req.xmlData,
|
||||
req.origFunc,
|
||||
req.rid,
|
||||
req.sends);
|
||||
req = this._requests[i];
|
||||
}
|
||||
|
||||
if (req.xhr.readyState === 0) {
|
||||
Strophe.debug("request id " + req.id +
|
||||
"." + req.sends + " posting");
|
||||
|
||||
try {
|
||||
var async = !('sync' in this && this.sync === true);
|
||||
req.xhr.open("POST", this.service, async);
|
||||
} catch (e2) {
|
||||
Strophe.error("XHR open failed.");
|
||||
if (!this.connected) {
|
||||
this._changeConnectStatus(Strophe.Status.CONNFAIL,
|
||||
"bad-service");
|
||||
}
|
||||
this.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fires the XHR request -- may be invoked immediately
|
||||
// or on a gradually expanding retry window for reconnects
|
||||
var sendFunc = function () {
|
||||
req.date = new Date();
|
||||
req.xhr.send(req.data);
|
||||
};
|
||||
|
||||
// Implement progressive backoff for reconnects --
|
||||
// First retry (send == 1) should also be instantaneous
|
||||
if (req.sends > 1) {
|
||||
// Using a cube of the retry number creates a nicely
|
||||
// expanding retry window
|
||||
var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait),
|
||||
Math.pow(req.sends, 3)) * 1000;
|
||||
setTimeout(sendFunc, backoff);
|
||||
} else {
|
||||
sendFunc();
|
||||
}
|
||||
|
||||
req.sends++;
|
||||
|
||||
if (this.xmlOutput !== Strophe.Connection.prototype.xmlOutput) {
|
||||
this.xmlOutput(req.xmlData);
|
||||
}
|
||||
if (this.rawOutput !== Strophe.Connection.prototype.rawOutput) {
|
||||
this.rawOutput(req.data);
|
||||
}
|
||||
} else {
|
||||
Strophe.debug("_processRequest: " +
|
||||
(i === 0 ? "first" : "second") +
|
||||
" request has readyState of " +
|
||||
req.xhr.readyState);
|
||||
}
|
||||
},
|
||||
|
||||
/** PrivateFunction: _throttledRequestHandler
|
||||
* _Private_ function to throttle requests to the connection window.
|
||||
*
|
||||
* This function makes sure we don't send requests so fast that the
|
||||
* request ids overflow the connection window in the case that one
|
||||
* request died.
|
||||
*/
|
||||
_throttledRequestHandler: function ()
|
||||
{
|
||||
if (!this._requests) {
|
||||
Strophe.debug("_throttledRequestHandler called with " +
|
||||
"undefined requests");
|
||||
} else {
|
||||
Strophe.debug("_throttledRequestHandler called with " +
|
||||
this._requests.length + " requests");
|
||||
}
|
||||
|
||||
if (!this._requests || this._requests.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._requests.length > 0) {
|
||||
this._processRequest(0);
|
||||
}
|
||||
|
||||
if (this._requests.length > 1 &&
|
||||
Math.abs(this._requests[0].rid -
|
||||
this._requests[1].rid) < this.window) {
|
||||
this._processRequest(1);
|
||||
}
|
||||
},
|
||||
|
||||
/** PrivateFunction: _onRequestStateChange
|
||||
* _Private_ handler for Strophe.Request state changes.
|
||||
*
|
||||
* This function is called when the XMLHttpRequest readyState changes.
|
||||
* It contains a lot of error handling logic for the many ways that
|
||||
* requests can fail, and calls the request callback when requests
|
||||
* succeed.
|
||||
*
|
||||
* Parameters:
|
||||
* (Function) func - The handler for the request.
|
||||
* (Strophe.Request) req - The request that is changing readyState.
|
||||
*/
|
||||
_onRequestStateChange: function (func, req)
|
||||
{
|
||||
Strophe.debug("request id " + req.id +
|
||||
"." + req.sends + " state changed to " +
|
||||
req.xhr.readyState);
|
||||
|
||||
if (req.abort) {
|
||||
req.abort = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// request complete
|
||||
var reqStatus;
|
||||
if (req.xhr.readyState == 4) {
|
||||
reqStatus = 0;
|
||||
try {
|
||||
reqStatus = req.xhr.status;
|
||||
} catch (e) {
|
||||
// ignore errors from undefined status attribute. works
|
||||
// around a browser bug
|
||||
}
|
||||
|
||||
if (typeof(reqStatus) == "undefined") {
|
||||
reqStatus = 0;
|
||||
}
|
||||
|
||||
if (this.disconnecting) {
|
||||
if (reqStatus >= 400) {
|
||||
this._hitError(reqStatus);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var reqIs0 = (this._requests[0] == req);
|
||||
var reqIs1 = (this._requests[1] == req);
|
||||
|
||||
if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) {
|
||||
// remove from internal queue
|
||||
this._removeRequest(req);
|
||||
Strophe.debug("request id " +
|
||||
req.id +
|
||||
" should now be removed");
|
||||
}
|
||||
|
||||
// request succeeded
|
||||
if (reqStatus == 200) {
|
||||
// if request 1 finished, or request 0 finished and request
|
||||
// 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to
|
||||
// restart the other - both will be in the first spot, as the
|
||||
// completed request has been removed from the queue already
|
||||
if (reqIs1 ||
|
||||
(reqIs0 && this._requests.length > 0 &&
|
||||
this._requests[0].age() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) {
|
||||
this._restartRequest(0);
|
||||
}
|
||||
// call handler
|
||||
Strophe.debug("request id " +
|
||||
req.id + "." +
|
||||
req.sends + " got 200");
|
||||
func(req);
|
||||
this.errors = 0;
|
||||
} else {
|
||||
Strophe.error("request id " +
|
||||
req.id + "." +
|
||||
req.sends + " error " + reqStatus +
|
||||
" happened");
|
||||
if (reqStatus === 0 ||
|
||||
(reqStatus >= 400 && reqStatus < 600) ||
|
||||
reqStatus >= 12000) {
|
||||
this._hitError(reqStatus);
|
||||
if (reqStatus >= 400 && reqStatus < 500) {
|
||||
this._changeConnectStatus(Strophe.Status.DISCONNECTING,
|
||||
null);
|
||||
this._doDisconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!((reqStatus > 0 && reqStatus < 500) ||
|
||||
req.sends > 5)) {
|
||||
this._throttledRequestHandler();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** PrivateFunction: _hitError
|
||||
* _Private_ function to handle the error count.
|
||||
*
|
||||
* Requests are resent automatically until their error count reaches
|
||||
* 5. Each time an error is encountered, this function is called to
|
||||
* increment the count and disconnect if the count is too high.
|
||||
*
|
||||
* Parameters:
|
||||
* (Integer) reqStatus - The request status.
|
||||
*/
|
||||
_hitError: function (reqStatus)
|
||||
{
|
||||
this.errors++;
|
||||
Strophe.warn("request errored, status: " + reqStatus +
|
||||
", number of errors: " + this.errors);
|
||||
if (this.errors > 4) {
|
||||
this._onDisconnectTimeout();
|
||||
}
|
||||
},
|
||||
|
||||
/** PrivateFunction: _doDisconnect
|
||||
* _Private_ function to disconnect.
|
||||
*
|
||||
* This is the last piece of the disconnection logic. This resets the
|
||||
* connection and alerts the user's connection callback.
|
||||
*/
|
||||
_doDisconnect: function ()
|
||||
{
|
||||
Strophe.info("_doDisconnect was called");
|
||||
this.authenticated = false;
|
||||
this.disconnecting = false;
|
||||
this.sid = null;
|
||||
this.streamId = null;
|
||||
this.rid = Math.floor(Math.random() * 4294967295);
|
||||
|
||||
// tell the parent we disconnected
|
||||
if (this.connected) {
|
||||
this._changeConnectStatus(Strophe.Status.DISCONNECTED, null);
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
// delete handlers
|
||||
this.handlers = [];
|
||||
this.timedHandlers = [];
|
||||
this.removeTimeds = [];
|
||||
this.removeHandlers = [];
|
||||
this.addTimeds = [];
|
||||
this.addHandlers = [];
|
||||
},
|
||||
|
||||
/** PrivateFunction: _dataRecv
|
||||
* _Private_ handler to processes incoming data from the the connection.
|
||||
*
|
||||
* Except for _connect_cb handling the initial connection request,
|
||||
* this function handles the incoming data for all requests. This
|
||||
* function also fires stanza handlers that match each incoming
|
||||
* stanza.
|
||||
*
|
||||
* Parameters:
|
||||
* (Strophe.Request) req - The request that has data ready.
|
||||
*/
|
||||
_dataRecv: function (req)
|
||||
{
|
||||
try {
|
||||
var elem = req.getResponse();
|
||||
} catch (e) {
|
||||
if (e != "parsererror") { throw e; }
|
||||
this.disconnect("strophe-parsererror");
|
||||
}
|
||||
if (elem === null) { return; }
|
||||
|
||||
if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) {
|
||||
this.xmlInput(elem);
|
||||
}
|
||||
if (this.rawInput !== Strophe.Connection.prototype.rawInput) {
|
||||
this.rawInput(Strophe.serialize(elem));
|
||||
}
|
||||
|
||||
// remove handlers scheduled for deletion
|
||||
var i, hand;
|
||||
while (this.removeHandlers.length > 0) {
|
||||
hand = this.removeHandlers.pop();
|
||||
i = this.handlers.indexOf(hand);
|
||||
if (i >= 0) {
|
||||
this.handlers.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// add handlers scheduled for addition
|
||||
while (this.addHandlers.length > 0) {
|
||||
this.handlers.push(this.addHandlers.pop());
|
||||
}
|
||||
|
||||
// handle graceful disconnect
|
||||
if (this.disconnecting && this._requests.length === 0) {
|
||||
this.deleteTimedHandler(this._disconnectTimeout);
|
||||
this._disconnectTimeout = null;
|
||||
this._doDisconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
var typ = elem.getAttribute("type");
|
||||
var cond, conflict;
|
||||
if (typ !== null && typ == "terminate") {
|
||||
// Don't process stanzas that come in after disconnect
|
||||
if (this.disconnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// an error occurred
|
||||
cond = elem.getAttribute("condition");
|
||||
conflict = elem.getElementsByTagName("conflict");
|
||||
if (cond !== null) {
|
||||
if (cond == "remote-stream-error" && conflict.length > 0) {
|
||||
cond = "conflict";
|
||||
}
|
||||
this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
|
||||
} else {
|
||||
this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
|
||||
}
|
||||
this.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// send each incoming stanza through the handler chain
|
||||
var that = this;
|
||||
Strophe.forEachChild(elem, null, function (child) {
|
||||
var i, newList;
|
||||
// process handlers
|
||||
newList = that.handlers;
|
||||
that.handlers = [];
|
||||
for (i = 0; i < newList.length; i++) {
|
||||
var hand = newList[i];
|
||||
// encapsulate 'handler.run' not to lose the whole handler list if
|
||||
// one of the handlers throws an exception
|
||||
try {
|
||||
if (hand.isMatch(child) &&
|
||||
(that.authenticated || !hand.user)) {
|
||||
if (hand.run(child)) {
|
||||
that.handlers.push(hand);
|
||||
}
|
||||
} else {
|
||||
that.handlers.push(hand);
|
||||
}
|
||||
} catch(e) {
|
||||
//if the handler throws an exception, we consider it as false
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** PrivateFunction: _sendTerminate
|
||||
* _Private_ function to send initial disconnect sequence.
|
||||
*
|
||||
* This is the first step in a graceful disconnect. It sends
|
||||
* the BOSH server a terminate body and includes an unavailable
|
||||
* presence if authentication has completed.
|
||||
*/
|
||||
_sendTerminate: function ()
|
||||
{
|
||||
Strophe.info("_sendTerminate was called");
|
||||
var body = this._buildBody().attrs({type: "terminate"});
|
||||
|
||||
if (this.authenticated) {
|
||||
body.c('presence', {
|
||||
xmlns: Strophe.NS.CLIENT,
|
||||
type: 'unavailable'
|
||||
});
|
||||
}
|
||||
|
||||
this.disconnecting = true;
|
||||
|
||||
var req = new Strophe.Request(body.tree(),
|
||||
this._onRequestStateChange.bind(
|
||||
this, this._dataRecv.bind(this)),
|
||||
body.tree().getAttribute("rid"));
|
||||
|
||||
this._requests.push(req);
|
||||
this._throttledRequestHandler();
|
||||
},
|
||||
|
||||
/** PrivateFunction: _connect_cb
|
||||
* _Private_ handler for initial connection request.
|
||||
*
|
||||
* This handler is used to process the initial connection request
|
||||
* response from the BOSH server. It is used to set up authentication
|
||||
* handlers and start the authentication process.
|
||||
*
|
||||
* SASL authentication will be attempted if available, otherwise
|
||||
* the code will fall back to legacy authentication.
|
||||
*
|
||||
* Parameters:
|
||||
* (Strophe.Request) req - The current request.
|
||||
*/
|
||||
_connect_cb: function (req)
|
||||
{
|
||||
Strophe.info("_connect_cb was called");
|
||||
|
||||
this.connected = true;
|
||||
var bodyWrap = req.getResponse();
|
||||
if (!bodyWrap) { return; }
|
||||
|
||||
if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) {
|
||||
this.xmlInput(bodyWrap);
|
||||
}
|
||||
if (this.rawInput !== Strophe.Connection.prototype.rawInput) {
|
||||
this.rawInput(Strophe.serialize(bodyWrap));
|
||||
}
|
||||
|
||||
var typ = bodyWrap.getAttribute("type");
|
||||
var cond, conflict;
|
||||
if (typ !== null && typ == "terminate") {
|
||||
// an error occurred
|
||||
cond = bodyWrap.getAttribute("condition");
|
||||
conflict = bodyWrap.getElementsByTagName("conflict");
|
||||
if (cond !== null) {
|
||||
if (cond == "remote-stream-error" && conflict.length > 0) {
|
||||
cond = "conflict";
|
||||
}
|
||||
this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
|
||||
} else {
|
||||
this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// check to make sure we don't overwrite these if _connect_cb is
|
||||
// called multiple times in the case of missing stream:features
|
||||
if (!this.sid) {
|
||||
this.sid = bodyWrap.getAttribute("sid");
|
||||
}
|
||||
if (!this.stream_id) {
|
||||
this.stream_id = bodyWrap.getAttribute("authid");
|
||||
}
|
||||
var wind = bodyWrap.getAttribute('requests');
|
||||
if (wind) { this.window = parseInt(wind, 10); }
|
||||
var hold = bodyWrap.getAttribute('hold');
|
||||
if (hold) { this.hold = parseInt(hold, 10); }
|
||||
var wait = bodyWrap.getAttribute('wait');
|
||||
if (wait) { this.wait = parseInt(wait, 10); }
|
||||
|
||||
|
||||
var do_sasl_plain = false;
|
||||
var do_sasl_digest_md5 = false;
|
||||
var do_sasl_anonymous = false;
|
||||
|
||||
var mechanisms = bodyWrap.getElementsByTagName("mechanism");
|
||||
var i, mech, auth_str, hashed_auth_str;
|
||||
if (mechanisms.length > 0) {
|
||||
for (i = 0; i < mechanisms.length; i++) {
|
||||
mech = Strophe.getText(mechanisms[i]);
|
||||
if (mech == 'DIGEST-MD5') {
|
||||
do_sasl_digest_md5 = true;
|
||||
} else if (mech == 'PLAIN') {
|
||||
do_sasl_plain = true;
|
||||
} else if (mech == 'ANONYMOUS') {
|
||||
do_sasl_anonymous = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we didn't get stream:features yet, so we need wait for it
|
||||
// by sending a blank poll request
|
||||
var body = this._buildBody();
|
||||
this._requests.push(
|
||||
new Strophe.Request(body.tree(),
|
||||
this._onRequestStateChange.bind(
|
||||
this, this._connect_cb.bind(this)),
|
||||
body.tree().getAttribute("rid")));
|
||||
this._throttledRequestHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Strophe.getNodeFromJid(this.jid) === null &&
|
||||
do_sasl_anonymous) {
|
||||
this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
|
||||
this._sasl_success_handler = this._addSysHandler(
|
||||
this._sasl_success_cb.bind(this), null,
|
||||
"success", null, null);
|
||||
this._sasl_failure_handler = this._addSysHandler(
|
||||
this._sasl_failure_cb.bind(this), null,
|
||||
"failure", null, null);
|
||||
|
||||
this.send($build("auth", {
|
||||
xmlns: Strophe.NS.SASL,
|
||||
mechanism: "ANONYMOUS"
|
||||
}).tree());
|
||||
} else if (Strophe.getNodeFromJid(this.jid) === null) {
|
||||
// we don't have a node, which is required for non-anonymous
|
||||
// client connections
|
||||
this._changeConnectStatus(Strophe.Status.CONNFAIL,
|
||||
'x-strophe-bad-non-anon-jid');
|
||||
this.disconnect();
|
||||
} else if (do_sasl_digest_md5) {
|
||||
this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
|
||||
this._sasl_challenge_handler = this._addSysHandler(
|
||||
this._sasl_challenge1_cb.bind(this), null,
|
||||
"challenge", null, null);
|
||||
this._sasl_failure_handler = this._addSysHandler(
|
||||
this._sasl_failure_cb.bind(this), null,
|
||||
"failure", null, null);
|
||||
|
||||
this.send($build("auth", {
|
||||
xmlns: Strophe.NS.SASL,
|
||||
mechanism: "DIGEST-MD5"
|
||||
}).tree());
|
||||
} else if (do_sasl_plain) {
|
||||
// Build the plain auth string (barejid null
|
||||
// username null password) and base 64 encoded.
|
||||
auth_str = unescape(encodeURIComponent(Strophe.getBareJidFromJid(this.jid)));
|
||||
auth_str = auth_str + "\u0000";
|
||||
auth_str = auth_str + unescape(encodeURIComponent(Strophe.getNodeFromJid(this.jid)));
|
||||
auth_str = auth_str + "\u0000";
|
||||
auth_str = auth_str + this.pass;
|
||||
|
||||
this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
|
||||
this._sasl_success_handler = this._addSysHandler(
|
||||
this._sasl_success_cb.bind(this), null,
|
||||
"success", null, null);
|
||||
this._sasl_failure_handler = this._addSysHandler(
|
||||
this._sasl_failure_cb.bind(this), null,
|
||||
"failure", null, null);
|
||||
|
||||
hashed_auth_str = Base64.encode(auth_str);
|
||||
this.send($build("auth", {
|
||||
xmlns: Strophe.NS.SASL,
|
||||
mechanism: "PLAIN"
|
||||
}).t(hashed_auth_str).tree());
|
||||
} else {
|
||||
this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
|
||||
this._addSysHandler(this._auth1_cb.bind(this), null, null,
|
||||
null, "_auth_1");
|
||||
|
||||
this.send($iq({
|
||||
type: "get",
|
||||
to: this.domain,
|
||||
id: "_auth_1"
|
||||
}).c("query", {
|
||||
xmlns: Strophe.NS.AUTH
|
||||
}).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree());
|
||||
}
|
||||
},
|
||||
|
||||
/** PrivateFunction: _sasl_challenge1_cb
|
||||
* _Private_ handler for DIGEST-MD5 SASL authentication.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The challenge stanza.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_sasl_challenge1_cb: function (elem)
|
||||
{
|
||||
var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/;
|
||||
|
||||
var challenge = Base64.decode(Strophe.getText(elem));
|
||||
var cnonce = MD5.hexdigest("" + (Math.random() * 1234567890));
|
||||
var realm = "";
|
||||
var host = null;
|
||||
var nonce = "";
|
||||
var qop = "";
|
||||
var matches;
|
||||
|
||||
// remove unneeded handlers
|
||||
this.deleteHandler(this._sasl_failure_handler);
|
||||
|
||||
while (challenge.match(attribMatch)) {
|
||||
matches = challenge.match(attribMatch);
|
||||
challenge = challenge.replace(matches[0], "");
|
||||
matches[2] = matches[2].replace(/^"(.+)"$/, "$1");
|
||||
switch (matches[1]) {
|
||||
case "realm":
|
||||
realm = matches[2];
|
||||
break;
|
||||
case "nonce":
|
||||
nonce = matches[2];
|
||||
break;
|
||||
case "qop":
|
||||
qop = matches[2];
|
||||
break;
|
||||
case "host":
|
||||
host = matches[2];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var digest_uri = "xmpp/" + this.domain;
|
||||
if (host !== null) {
|
||||
digest_uri = digest_uri + "/" + host;
|
||||
}
|
||||
|
||||
var A1 = MD5.hash(unescape(encodeURIComponent(Strophe.getNodeFromJid(this.jid))) +
|
||||
":" + realm + ":" + this.pass) +
|
||||
":" + nonce + ":" + cnonce;
|
||||
var A2 = 'AUTHENTICATE:' + digest_uri;
|
||||
|
||||
var responseText = "";
|
||||
responseText += 'username=' +
|
||||
this._quote(unescape(encodeURIComponent(Strophe.getNodeFromJid(this.jid)))) + ',';
|
||||
responseText += 'realm=' + this._quote(realm) + ',';
|
||||
responseText += 'nonce=' + this._quote(nonce) + ',';
|
||||
responseText += 'cnonce=' + this._quote(cnonce) + ',';
|
||||
responseText += 'nc="00000001",';
|
||||
responseText += 'qop="auth",';
|
||||
responseText += 'digest-uri=' + this._quote(digest_uri) + ',';
|
||||
responseText += 'response=' + this._quote(
|
||||
MD5.hexdigest(MD5.hexdigest(A1) + ":" +
|
||||
nonce + ":00000001:" +
|
||||
cnonce + ":auth:" +
|
||||
MD5.hexdigest(A2))) + ',';
|
||||
responseText += 'charset="utf-8"';
|
||||
|
||||
this._sasl_challenge_handler = this._addSysHandler(
|
||||
this._sasl_challenge2_cb.bind(this), null,
|
||||
"challenge", null, null);
|
||||
this._sasl_success_handler = this._addSysHandler(
|
||||
this._sasl_success_cb.bind(this), null,
|
||||
"success", null, null);
|
||||
this._sasl_failure_handler = this._addSysHandler(
|
||||
this._sasl_failure_cb.bind(this), null,
|
||||
"failure", null, null);
|
||||
|
||||
this.send($build('response', {
|
||||
xmlns: Strophe.NS.SASL
|
||||
}).t(Base64.encode(responseText)).tree());
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _quote
|
||||
* _Private_ utility function to backslash escape and quote strings.
|
||||
*
|
||||
* Parameters:
|
||||
* (String) str - The string to be quoted.
|
||||
*
|
||||
* Returns:
|
||||
* quoted string
|
||||
*/
|
||||
_quote: function (str)
|
||||
{
|
||||
return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
||||
//" end string workaround for emacs
|
||||
},
|
||||
|
||||
|
||||
/** PrivateFunction: _sasl_challenge2_cb
|
||||
* _Private_ handler for second step of DIGEST-MD5 SASL authentication.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The challenge stanza.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_sasl_challenge2_cb: function (elem)
|
||||
{
|
||||
// remove unneeded handlers
|
||||
this.deleteHandler(this._sasl_success_handler);
|
||||
this.deleteHandler(this._sasl_failure_handler);
|
||||
|
||||
this._sasl_success_handler = this._addSysHandler(
|
||||
this._sasl_success_cb.bind(this), null,
|
||||
"success", null, null);
|
||||
this._sasl_failure_handler = this._addSysHandler(
|
||||
this._sasl_failure_cb.bind(this), null,
|
||||
"failure", null, null);
|
||||
this.send($build('response', {xmlns: Strophe.NS.SASL}).tree());
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _auth1_cb
|
||||
* _Private_ handler for legacy authentication.
|
||||
*
|
||||
* This handler is called in response to the initial <iq type='get'/>
|
||||
* for legacy authentication. It builds an authentication <iq/> and
|
||||
* sends it, creating a handler (calling back to _auth2_cb()) to
|
||||
* handle the result
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The stanza that triggered the callback.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_auth1_cb: function (elem)
|
||||
{
|
||||
// build plaintext auth iq
|
||||
var iq = $iq({type: "set", id: "_auth_2"})
|
||||
.c('query', {xmlns: Strophe.NS.AUTH})
|
||||
.c('username', {}).t(Strophe.getNodeFromJid(this.jid))
|
||||
.up()
|
||||
.c('password').t(this.pass);
|
||||
|
||||
if (!Strophe.getResourceFromJid(this.jid)) {
|
||||
// since the user has not supplied a resource, we pick
|
||||
// a default one here. unlike other auth methods, the server
|
||||
// cannot do this for us.
|
||||
this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe';
|
||||
}
|
||||
iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid));
|
||||
|
||||
this._addSysHandler(this._auth2_cb.bind(this), null,
|
||||
null, null, "_auth_2");
|
||||
|
||||
this.send(iq.tree());
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _sasl_success_cb
|
||||
* _Private_ handler for succesful SASL authentication.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The matching stanza.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_sasl_success_cb: function (elem)
|
||||
{
|
||||
Strophe.info("SASL authentication succeeded.");
|
||||
|
||||
// remove old handlers
|
||||
this.deleteHandler(this._sasl_failure_handler);
|
||||
this._sasl_failure_handler = null;
|
||||
if (this._sasl_challenge_handler) {
|
||||
this.deleteHandler(this._sasl_challenge_handler);
|
||||
this._sasl_challenge_handler = null;
|
||||
}
|
||||
|
||||
this._addSysHandler(this._sasl_auth1_cb.bind(this), null,
|
||||
"stream:features", null, null);
|
||||
|
||||
// we must send an xmpp:restart now
|
||||
this._sendRestart();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _sasl_auth1_cb
|
||||
* _Private_ handler to start stream binding.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The matching stanza.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_sasl_auth1_cb: function (elem)
|
||||
{
|
||||
// save stream:features for future usage
|
||||
this.features = elem;
|
||||
|
||||
var i, child;
|
||||
|
||||
for (i = 0; i < elem.childNodes.length; i++) {
|
||||
child = elem.childNodes[i];
|
||||
if (child.nodeName == 'bind') {
|
||||
this.do_bind = true;
|
||||
}
|
||||
|
||||
if (child.nodeName == 'session') {
|
||||
this.do_session = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.do_bind) {
|
||||
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
|
||||
return false;
|
||||
} else {
|
||||
this._addSysHandler(this._sasl_bind_cb.bind(this), null, null,
|
||||
null, "_bind_auth_2");
|
||||
|
||||
var resource = Strophe.getResourceFromJid(this.jid);
|
||||
if (resource) {
|
||||
this.send($iq({type: "set", id: "_bind_auth_2"})
|
||||
.c('bind', {xmlns: Strophe.NS.BIND})
|
||||
.c('resource', {}).t(resource).tree());
|
||||
} else {
|
||||
this.send($iq({type: "set", id: "_bind_auth_2"})
|
||||
.c('bind', {xmlns: Strophe.NS.BIND})
|
||||
.tree());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _sasl_bind_cb
|
||||
* _Private_ handler for binding result and session start.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The matching stanza.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_sasl_bind_cb: function (elem)
|
||||
{
|
||||
if (elem.getAttribute("type") == "error") {
|
||||
Strophe.info("SASL binding failed.");
|
||||
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO - need to grab errors
|
||||
var bind = elem.getElementsByTagName("bind");
|
||||
var jidNode;
|
||||
if (bind.length > 0) {
|
||||
// Grab jid
|
||||
jidNode = bind[0].getElementsByTagName("jid");
|
||||
if (jidNode.length > 0) {
|
||||
this.jid = Strophe.getText(jidNode[0]);
|
||||
|
||||
if (this.do_session) {
|
||||
this._addSysHandler(this._sasl_session_cb.bind(this),
|
||||
null, null, null, "_session_auth_2");
|
||||
|
||||
this.send($iq({type: "set", id: "_session_auth_2"})
|
||||
.c('session', {xmlns: Strophe.NS.SESSION})
|
||||
.tree());
|
||||
} else {
|
||||
this.authenticated = true;
|
||||
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Strophe.info("SASL binding failed.");
|
||||
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/** PrivateFunction: _sasl_session_cb
|
||||
* _Private_ handler to finish successful SASL connection.
|
||||
*
|
||||
* This sets Connection.authenticated to true on success, which
|
||||
* starts the processing of user handlers.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The matching stanza.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_sasl_session_cb: function (elem)
|
||||
{
|
||||
if (elem.getAttribute("type") == "result") {
|
||||
this.authenticated = true;
|
||||
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
|
||||
} else if (elem.getAttribute("type") == "error") {
|
||||
Strophe.info("Session creation failed.");
|
||||
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _sasl_failure_cb
|
||||
* _Private_ handler for SASL authentication failure.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The matching stanza.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_sasl_failure_cb: function (elem)
|
||||
{
|
||||
// delete unneeded handlers
|
||||
if (this._sasl_success_handler) {
|
||||
this.deleteHandler(this._sasl_success_handler);
|
||||
this._sasl_success_handler = null;
|
||||
}
|
||||
if (this._sasl_challenge_handler) {
|
||||
this.deleteHandler(this._sasl_challenge_handler);
|
||||
this._sasl_challenge_handler = null;
|
||||
}
|
||||
|
||||
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _auth2_cb
|
||||
* _Private_ handler to finish legacy authentication.
|
||||
*
|
||||
* This handler is called when the result from the jabber:iq:auth
|
||||
* <iq/> stanza is returned.
|
||||
*
|
||||
* Parameters:
|
||||
* (XMLElement) elem - The stanza that triggered the callback.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_auth2_cb: function (elem)
|
||||
{
|
||||
if (elem.getAttribute("type") == "result") {
|
||||
this.authenticated = true;
|
||||
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
|
||||
} else if (elem.getAttribute("type") == "error") {
|
||||
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _addSysTimedHandler
|
||||
* _Private_ function to add a system level timed handler.
|
||||
*
|
||||
* This function is used to add a Strophe.TimedHandler for the
|
||||
* library code. System timed handlers are allowed to run before
|
||||
* authentication is complete.
|
||||
*
|
||||
* Parameters:
|
||||
* (Integer) period - The period of the handler.
|
||||
* (Function) handler - The callback function.
|
||||
*/
|
||||
_addSysTimedHandler: function (period, handler)
|
||||
{
|
||||
var thand = new Strophe.TimedHandler(period, handler);
|
||||
thand.user = false;
|
||||
this.addTimeds.push(thand);
|
||||
return thand;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _addSysHandler
|
||||
* _Private_ function to add a system level stanza handler.
|
||||
*
|
||||
* This function is used to add a Strophe.Handler for the
|
||||
* library code. System stanza handlers are allowed to run before
|
||||
* authentication is complete.
|
||||
*
|
||||
* Parameters:
|
||||
* (Function) handler - The callback function.
|
||||
* (String) ns - The namespace to match.
|
||||
* (String) name - The stanza name to match.
|
||||
* (String) type - The stanza type attribute to match.
|
||||
* (String) id - The stanza id attribute to match.
|
||||
*/
|
||||
_addSysHandler: function (handler, ns, name, type, id)
|
||||
{
|
||||
var hand = new Strophe.Handler(handler, ns, name, type, id);
|
||||
hand.user = false;
|
||||
this.addHandlers.push(hand);
|
||||
return hand;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _onDisconnectTimeout
|
||||
* _Private_ timeout handler for handling non-graceful disconnection.
|
||||
*
|
||||
* If the graceful disconnect process does not complete within the
|
||||
* time allotted, this handler finishes the disconnect anyway.
|
||||
*
|
||||
* Returns:
|
||||
* false to remove the handler.
|
||||
*/
|
||||
_onDisconnectTimeout: function ()
|
||||
{
|
||||
Strophe.info("_onDisconnectTimeout was called");
|
||||
|
||||
// cancel all remaining requests and clear the queue
|
||||
var req;
|
||||
while (this._requests.length > 0) {
|
||||
req = this._requests.pop();
|
||||
req.abort = true;
|
||||
req.xhr.abort();
|
||||
// jslint complains, but this is fine. setting to empty func
|
||||
// is necessary for IE6
|
||||
req.xhr.onreadystatechange = function () {};
|
||||
}
|
||||
|
||||
// actually disconnect
|
||||
this._doDisconnect();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** PrivateFunction: _onIdle
|
||||
* _Private_ handler to process events during idle cycle.
|
||||
*
|
||||
* This handler is called every 100ms to fire timed handlers that
|
||||
* are ready and keep poll requests going.
|
||||
*/
|
||||
_onIdle: function ()
|
||||
{
|
||||
var i, thand, since, newList;
|
||||
|
||||
// add timed handlers scheduled for addition
|
||||
// NOTE: we add before remove in the case a timed handler is
|
||||
// added and then deleted before the next _onIdle() call.
|
||||
while (this.addTimeds.length > 0) {
|
||||
this.timedHandlers.push(this.addTimeds.pop());
|
||||
}
|
||||
|
||||
// remove timed handlers that have been scheduled for deletion
|
||||
while (this.removeTimeds.length > 0) {
|
||||
thand = this.removeTimeds.pop();
|
||||
i = this.timedHandlers.indexOf(thand);
|
||||
if (i >= 0) {
|
||||
this.timedHandlers.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// call ready timed handlers
|
||||
var now = new Date().getTime();
|
||||
newList = [];
|
||||
for (i = 0; i < this.timedHandlers.length; i++) {
|
||||
thand = this.timedHandlers[i];
|
||||
if (this.authenticated || !thand.user) {
|
||||
since = thand.lastCalled + thand.period;
|
||||
if (since - now <= 0) {
|
||||
if (thand.run()) {
|
||||
newList.push(thand);
|
||||
}
|
||||
} else {
|
||||
newList.push(thand);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.timedHandlers = newList;
|
||||
|
||||
var body, time_elapsed;
|
||||
|
||||
// if no requests are in progress, poll
|
||||
if (this.authenticated && this._requests.length === 0 &&
|
||||
this._data.length === 0 && !this.disconnecting) {
|
||||
Strophe.info("no requests during idle cycle, sending " +
|
||||
"blank request");
|
||||
this._data.push(null);
|
||||
}
|
||||
|
||||
if (this._requests.length < 2 && this._data.length > 0 &&
|
||||
!this.paused) {
|
||||
body = this._buildBody();
|
||||
for (i = 0; i < this._data.length; i++) {
|
||||
if (this._data[i] !== null) {
|
||||
if (this._data[i] === "restart") {
|
||||
body.attrs({
|
||||
to: this.domain,
|
||||
"xml:lang": "en",
|
||||
"xmpp:restart": "true",
|
||||
"xmlns:xmpp": Strophe.NS.BOSH
|
||||
});
|
||||
} else {
|
||||
body.cnode(this._data[i]).up();
|
||||
}
|
||||
}
|
||||
}
|
||||
delete this._data;
|
||||
this._data = [];
|
||||
this._requests.push(
|
||||
new Strophe.Request(body.tree(),
|
||||
this._onRequestStateChange.bind(
|
||||
this, this._dataRecv.bind(this)),
|
||||
body.tree().getAttribute("rid")));
|
||||
this._processRequest(this._requests.length - 1);
|
||||
}
|
||||
|
||||
if (this._requests.length > 0) {
|
||||
time_elapsed = this._requests[0].age();
|
||||
if (this._requests[0].dead !== null) {
|
||||
if (this._requests[0].timeDead() >
|
||||
Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) {
|
||||
this._throttledRequestHandler();
|
||||
}
|
||||
}
|
||||
|
||||
if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) {
|
||||
Strophe.warn("Request " +
|
||||
this._requests[0].id +
|
||||
" timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) +
|
||||
" seconds since last activity");
|
||||
this._throttledRequestHandler();
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(this._idleTimeout);
|
||||
|
||||
// reactivate the timer only if connected
|
||||
if (this.connected) {
|
||||
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
callback(Strophe, $build, $msg, $iq, $pres);
|
||||
}
|
||||
|
||||
})(function () {
|
||||
window.Strophe = arguments[0];
|
||||
window.$build = arguments[1];
|
||||
window.$msg = arguments[2];
|
||||
window.$iq = arguments[3];
|
||||
window.$pres = arguments[4];
|
||||
});
|
||||
/*
|
||||
Plugin to implement the MUC extension. http://xmpp.org/extensions/xep-0045.html
|
||||
*/
|
||||
/* jslint configuration: */
|
||||
/* global document, window, setTimeout, clearTimeout, console,
|
||||
XMLHttpRequest, ActiveXObject,
|
||||
Base64, MD5,
|
||||
Strophe, $build, $msg, $iq, $pres
|
||||
*/
|
||||
|
||||
Strophe.addConnectionPlugin('muc', {
|
||||
_connection: null,
|
||||
// The plugin must have the init function
|
||||
/***Function
|
||||
Initialize the MUC plugin. Sets the correct connection object and
|
||||
extends the namesace.
|
||||
*/
|
||||
init: function(conn) {
|
||||
this._connection = conn;
|
||||
/* extend name space
|
||||
* NS.MUC - XMPP Multi-user chat namespace
|
||||
* from XEP 45.
|
||||
*
|
||||
*/
|
||||
Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC+"#owner");
|
||||
Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC+"#admin");
|
||||
},
|
||||
/***Function
|
||||
Join a multi-user chat room
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room to join.
|
||||
(String) nick - The nickname to use in the chat room. Optional
|
||||
(Function) msg_handler_cb - The function call to handle messages from the
|
||||
specified chat room.
|
||||
(Function) pres_handler_cb - The function call back to handle presence
|
||||
in the chat room.
|
||||
(String) password - The optional password to use. (password protected
|
||||
rooms only)
|
||||
*/
|
||||
join: function(room, nick, msg_handler_cb, pres_handler_cb, password) {
|
||||
var room_nick = this.test_append_nick(room, nick);
|
||||
var msg = $pres({from: this._connection.jid,
|
||||
to: room_nick})
|
||||
.c("x",{xmlns: Strophe.NS.MUC});
|
||||
if (password)
|
||||
{
|
||||
var password_elem = Strophe.xmlElement("password",
|
||||
[],
|
||||
password);
|
||||
msg.cnode(password_elem);
|
||||
}
|
||||
if (msg_handler_cb)
|
||||
{
|
||||
this._connection.addHandler(function(stanza) {
|
||||
var from = stanza.getAttribute('from');
|
||||
var roomname = from.split("/");
|
||||
// filter on room name
|
||||
if (roomname[0] == room)
|
||||
{
|
||||
return msg_handler_cb(stanza);
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
},
|
||||
null,
|
||||
"message",
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
if (pres_handler_cb)
|
||||
{
|
||||
this._connection.addHandler(function(stanza) {
|
||||
var xquery = stanza.getElementsByTagName("x");
|
||||
if (xquery.length > 0)
|
||||
{
|
||||
//Handle only MUC user protocol
|
||||
for (var i = 0; i < xquery.length; i++)
|
||||
{
|
||||
var xmlns = xquery[i].getAttribute("xmlns");
|
||||
|
||||
if (xmlns && xmlns.match(Strophe.NS.MUC))
|
||||
{
|
||||
return pres_handler_cb(stanza);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
null,
|
||||
"presence",
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
this._connection.send(msg);
|
||||
},
|
||||
/***Function
|
||||
Leave a multi-user chat room
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room to leave.
|
||||
(String) nick - The nick name used in the room.
|
||||
(Function) handler_cb - Optional function to handle the successful leave.
|
||||
Returns:
|
||||
iqid - The unique id for the room leave.
|
||||
*/
|
||||
leave: function(room, nick, handler_cb) {
|
||||
var room_nick = this.test_append_nick(room, nick);
|
||||
var presenceid = this._connection.getUniqueId();
|
||||
var presence = $pres({type: "unavailable",
|
||||
id: presenceid,
|
||||
from: this._connection.jid,
|
||||
to: room_nick})
|
||||
.c("x",{xmlns: Strophe.NS.MUC});
|
||||
this._connection.addHandler(handler_cb,
|
||||
null,
|
||||
"presence",
|
||||
null,
|
||||
presenceid,
|
||||
null);
|
||||
this._connection.send(presence);
|
||||
return presenceid;
|
||||
},
|
||||
/***Function
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room name.
|
||||
(String) nick - The nick name used in the chat room.
|
||||
(String) message - The message to send to the room.
|
||||
(String) type - "groupchat" for group chat messages or "chat" for private chat messages
|
||||
Returns:
|
||||
msgiq - the unique id used to send the message
|
||||
*/
|
||||
message: function(room, nick, message, type) {
|
||||
var room_nick = this.test_append_nick(room, nick);
|
||||
type = type || "groupchat";
|
||||
var msgid = this._connection.getUniqueId();
|
||||
var msg = $msg({to: room_nick,
|
||||
from: this._connection.jid,
|
||||
type: type,
|
||||
id: msgid}).c("body",
|
||||
{xmlns: Strophe.NS.CLIENT}).t(message);
|
||||
msg.up().c("x", {xmlns: "jabber:x:event"}).c("composing");
|
||||
this._connection.send(msg);
|
||||
return msgid;
|
||||
},
|
||||
/***Function
|
||||
Start a room configuration.
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room name.
|
||||
Returns:
|
||||
id - the unique id used to send the configuration request
|
||||
*/
|
||||
configure: function(room) {
|
||||
//send iq to start room configuration
|
||||
var config = $iq({to:room,
|
||||
type: "get"}).c("query",
|
||||
{xmlns: Strophe.NS.MUC_OWNER});
|
||||
var stanza = config.tree();
|
||||
return this._connection.sendIQ(stanza,
|
||||
function(){},
|
||||
function(){});
|
||||
},
|
||||
/***Function
|
||||
Cancel the room configuration
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room name.
|
||||
Returns:
|
||||
id - the unique id used to cancel the configuration.
|
||||
*/
|
||||
cancelConfigure: function(room) {
|
||||
//send iq to start room configuration
|
||||
var config = $iq({to: room,
|
||||
type: "set"})
|
||||
.c("query", {xmlns: Strophe.NS.MUC_OWNER})
|
||||
.c("x", {xmlns: "jabber:x:data", type: "cancel"});
|
||||
var stanza = config.tree();
|
||||
return this._connection.sendIQ(stanza,
|
||||
function(){},
|
||||
function(){});
|
||||
},
|
||||
/***Function
|
||||
Save a room configuration.
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room name.
|
||||
(Array) configarray - an array of form elements used to configure the room.
|
||||
Returns:
|
||||
id - the unique id used to save the configuration.
|
||||
*/
|
||||
saveConfiguration: function(room, configarray) {
|
||||
var config = $iq({to: room,
|
||||
type: "set"})
|
||||
.c("query", {xmlns: Strophe.NS.MUC_OWNER})
|
||||
.c("x", {xmlns: "jabber:x:data", type: "submit"});
|
||||
for (var i = 0; i < configarray.length; i++) {
|
||||
config.cnode(configarray[i]);
|
||||
config.up();
|
||||
}
|
||||
var stanza = config.tree();
|
||||
return this._connection.sendIQ(stanza,
|
||||
function(){},
|
||||
function(){});
|
||||
},
|
||||
/***Function
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room name.
|
||||
Returns:
|
||||
id - the unique id used to create the chat room.
|
||||
*/
|
||||
createInstantRoom: function(room) {
|
||||
var roomiq = $iq({to: room,
|
||||
type: "set"})
|
||||
.c("query", {xmlns: Strophe.NS.MUC_OWNER})
|
||||
.c("x", {xmlns: "jabber:x:data",
|
||||
type: "submit"});
|
||||
return this._connection.sendIQ(roomiq.tree(),
|
||||
function() {},
|
||||
function() {});
|
||||
},
|
||||
/***
|
||||
Set the topic of the chat room.
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room name.
|
||||
(String) topic - Topic message.
|
||||
*/
|
||||
setTopic: function(room, topic) {
|
||||
var msg = $msg({to: room,
|
||||
from: this._connection.jid,
|
||||
type: "groupchat"})
|
||||
.c("subject", {xmlns: "jabber:client"}).t(topic);
|
||||
this._connection.send(msg.tree());
|
||||
},
|
||||
/***Function
|
||||
Changes the role and affiliation of a member of a MUC room.
|
||||
The modification can only be done by a room moderator. An error will be
|
||||
returned if the user doesn't have permission.
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room name.
|
||||
(String) nick - The nick name of the user to modify.
|
||||
(String) role - The new role of the user.
|
||||
(String) affiliation - The new affiliation of the user.
|
||||
(String) reason - The reason for the change.
|
||||
Returns:
|
||||
iq - the id of the mode change request.
|
||||
*/
|
||||
modifyUser: function(room, nick, role, affiliation, reason) {
|
||||
var item_attrs = {nick: Strophe.escapeNode(nick)};
|
||||
if (role !== null)
|
||||
{
|
||||
item_attrs.role = role;
|
||||
}
|
||||
if (affiliation !== null)
|
||||
{
|
||||
item_attrs.affiliation = affiliation;
|
||||
}
|
||||
var item = $build("item", item_attrs);
|
||||
if (reason !== null)
|
||||
{
|
||||
item.cnode(Strophe.xmlElement("reason", reason));
|
||||
}
|
||||
var roomiq = $iq({to: room,
|
||||
type: "set"})
|
||||
.c("query", {xmlns: Strophe.NS.MUC_OWNER}).cnode(item.tree());
|
||||
return this._connection.sendIQ(roomiq.tree(),
|
||||
function() {},
|
||||
function() {});
|
||||
},
|
||||
/***Function
|
||||
Change the current users nick name.
|
||||
Parameters:
|
||||
(String) room - The multi-user chat room name.
|
||||
(String) user - The new nick name.
|
||||
*/
|
||||
changeNick: function(room, user) {
|
||||
var room_nick = this.test_append_nick(room, user);
|
||||
var presence = $pres({from: this._connection.jid,
|
||||
to: room_nick})
|
||||
.c("x",{xmlns: Strophe.NS.MUC});
|
||||
this._connection.send(presence.tree());
|
||||
},
|
||||
/***Function
|
||||
List all chat room available on a server.
|
||||
Parameters:
|
||||
(String) server - name of chat server.
|
||||
(String) handle_cb - Function to call for room list return.
|
||||
*/
|
||||
listRooms: function(server, handle_cb) {
|
||||
var iq = $iq({to: server,
|
||||
from: this._connection.jid,
|
||||
type: "get"})
|
||||
.c("query",{xmlns: Strophe.NS.DISCO_ITEMS});
|
||||
this._connection.sendIQ(iq, handle_cb, function(){});
|
||||
},
|
||||
test_append_nick: function(room, nick) {
|
||||
var room_nick = room;
|
||||
if (nick)
|
||||
{
|
||||
room_nick += "/" + Strophe.escapeNode(nick);
|
||||
}
|
||||
return room_nick;
|
||||
}
|
||||
});
|
||||
/*
|
||||
mustache.js — Logic-less templates in JavaScript
|
||||
|
||||
See http://mustache.github.com/ for more info.
|
||||
*/
|
||||
|
||||
var Mustache = function() {
|
||||
var Renderer = function() {};
|
||||
|
||||
Renderer.prototype = {
|
||||
otag: "{{",
|
||||
ctag: "}}",
|
||||
pragmas: {},
|
||||
buffer: [],
|
||||
pragmas_implemented: {
|
||||
"IMPLICIT-ITERATOR": true
|
||||
},
|
||||
context: {},
|
||||
|
||||
render: function(template, context, partials, in_recursion) {
|
||||
// reset buffer & set context
|
||||
if(!in_recursion) {
|
||||
this.context = context;
|
||||
this.buffer = []; // TODO: make this non-lazy
|
||||
}
|
||||
|
||||
// fail fast
|
||||
if(!this.includes("", template)) {
|
||||
if(in_recursion) {
|
||||
return template;
|
||||
} else {
|
||||
this.send(template);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
template = this.render_pragmas(template);
|
||||
var html = this.render_section(template, context, partials);
|
||||
if(in_recursion) {
|
||||
return this.render_tags(html, context, partials, in_recursion);
|
||||
}
|
||||
|
||||
this.render_tags(html, context, partials, in_recursion);
|
||||
},
|
||||
|
||||
/*
|
||||
Sends parsed lines
|
||||
*/
|
||||
send: function(line) {
|
||||
if(line !== "") {
|
||||
this.buffer.push(line);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
Looks for %PRAGMAS
|
||||
*/
|
||||
render_pragmas: function(template) {
|
||||
// no pragmas
|
||||
if(!this.includes("%", template)) {
|
||||
return template;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
|
||||
this.ctag, "g");
|
||||
return template.replace(regex, function(match, pragma, options) {
|
||||
if(!that.pragmas_implemented[pragma]) {
|
||||
throw({message:
|
||||
"This implementation of mustache doesn't understand the '" +
|
||||
pragma + "' pragma"});
|
||||
}
|
||||
that.pragmas[pragma] = {};
|
||||
if(options) {
|
||||
var opts = options.split("=");
|
||||
that.pragmas[pragma][opts[0]] = opts[1];
|
||||
}
|
||||
return "";
|
||||
// ignore unknown pragmas silently
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
Tries to find a partial in the curent scope and render it
|
||||
*/
|
||||
render_partial: function(name, context, partials) {
|
||||
name = this.trim(name);
|
||||
if(!partials || partials[name] === undefined) {
|
||||
throw({message: "unknown_partial '" + name + "'"});
|
||||
}
|
||||
if(typeof(context[name]) != "object") {
|
||||
return this.render(partials[name], context, partials, true);
|
||||
}
|
||||
return this.render(partials[name], context[name], partials, true);
|
||||
},
|
||||
|
||||
/*
|
||||
Renders inverted (^) and normal (#) sections
|
||||
*/
|
||||
render_section: function(template, context, partials) {
|
||||
if(!this.includes("#", template) && !this.includes("^", template)) {
|
||||
return template;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
// CSW - Added "+?" so it finds the tighest bound, not the widest
|
||||
var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
|
||||
"\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
|
||||
"\\s*", "mg");
|
||||
|
||||
// for each {{#foo}}{{/foo}} section do...
|
||||
return template.replace(regex, function(match, type, name, content) {
|
||||
var value = that.find(name, context);
|
||||
if(type == "^") { // inverted section
|
||||
if(!value || that.is_array(value) && value.length === 0) {
|
||||
// false or empty list, render it
|
||||
return that.render(content, context, partials, true);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} else if(type == "#") { // normal section
|
||||
if(that.is_array(value)) { // Enumerable, Let's loop!
|
||||
return that.map(value, function(row) {
|
||||
return that.render(content, that.create_context(row),
|
||||
partials, true);
|
||||
}).join("");
|
||||
} else if(that.is_object(value)) { // Object, Use it as subcontext!
|
||||
return that.render(content, that.create_context(value),
|
||||
partials, true);
|
||||
} else if(typeof value === "function") {
|
||||
// higher order section
|
||||
return value.call(context, content, function(text) {
|
||||
return that.render(text, context, partials, true);
|
||||
});
|
||||
} else if(value) { // boolean section
|
||||
return that.render(content, context, partials, true);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
Replace {{foo}} and friends with values from our view
|
||||
*/
|
||||
render_tags: function(template, context, partials, in_recursion) {
|
||||
// tit for tat
|
||||
var that = this;
|
||||
|
||||
var new_regex = function() {
|
||||
return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
|
||||
that.ctag + "+", "g");
|
||||
};
|
||||
|
||||
var regex = new_regex();
|
||||
var tag_replace_callback = function(match, operator, name) {
|
||||
switch(operator) {
|
||||
case "!": // ignore comments
|
||||
return "";
|
||||
case "=": // set new delimiters, rebuild the replace regexp
|
||||
that.set_delimiters(name);
|
||||
regex = new_regex();
|
||||
return "";
|
||||
case ">": // render partial
|
||||
return that.render_partial(name, context, partials);
|
||||
case "{": // the triple mustache is unescaped
|
||||
return that.find(name, context);
|
||||
default: // escape the value
|
||||
return that.escape(that.find(name, context));
|
||||
}
|
||||
};
|
||||
var lines = template.split("\n");
|
||||
for(var i = 0; i < lines.length; i++) {
|
||||
lines[i] = lines[i].replace(regex, tag_replace_callback, this);
|
||||
if(!in_recursion) {
|
||||
this.send(lines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if(in_recursion) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
},
|
||||
|
||||
set_delimiters: function(delimiters) {
|
||||
var dels = delimiters.split(" ");
|
||||
this.otag = this.escape_regex(dels[0]);
|
||||
this.ctag = this.escape_regex(dels[1]);
|
||||
},
|
||||
|
||||
escape_regex: function(text) {
|
||||
// thank you Simon Willison
|
||||
if(!arguments.callee.sRE) {
|
||||
var specials = [
|
||||
'/', '.', '*', '+', '?', '|',
|
||||
'(', ')', '[', ']', '{', '}', '\\'
|
||||
];
|
||||
arguments.callee.sRE = new RegExp(
|
||||
'(\\' + specials.join('|\\') + ')', 'g'
|
||||
);
|
||||
}
|
||||
return text.replace(arguments.callee.sRE, '\\$1');
|
||||
},
|
||||
|
||||
/*
|
||||
find `name` in current `context`. That is find me a value
|
||||
from the view object
|
||||
*/
|
||||
find: function(name, context) {
|
||||
name = this.trim(name);
|
||||
|
||||
// Checks whether a value is thruthy or false or 0
|
||||
function is_kinda_truthy(bool) {
|
||||
return bool === false || bool === 0 || bool;
|
||||
}
|
||||
|
||||
var value;
|
||||
if(is_kinda_truthy(context[name])) {
|
||||
value = context[name];
|
||||
} else if(is_kinda_truthy(this.context[name])) {
|
||||
value = this.context[name];
|
||||
}
|
||||
|
||||
if(typeof value === "function") {
|
||||
return value.apply(context);
|
||||
}
|
||||
if(value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
// silently ignore unkown variables
|
||||
return "";
|
||||
},
|
||||
|
||||
// Utility methods
|
||||
|
||||
/* includes tag */
|
||||
includes: function(needle, haystack) {
|
||||
return haystack.indexOf(this.otag + needle) != -1;
|
||||
},
|
||||
|
||||
/*
|
||||
Does away with nasty characters
|
||||
*/
|
||||
escape: function(s) {
|
||||
s = String(s === null ? "" : s);
|
||||
return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
|
||||
switch(s) {
|
||||
case "&": return "&";
|
||||
case "\\": return "\\\\";
|
||||
case '"': return '"';
|
||||
case "'": return ''';
|
||||
case "<": return "<";
|
||||
case ">": return ">";
|
||||
default: return s;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// by @langalex, support for arrays of strings
|
||||
create_context: function(_context) {
|
||||
if(this.is_object(_context)) {
|
||||
return _context;
|
||||
} else {
|
||||
var iterator = ".";
|
||||
if(this.pragmas["IMPLICIT-ITERATOR"]) {
|
||||
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
|
||||
}
|
||||
var ctx = {};
|
||||
ctx[iterator] = _context;
|
||||
return ctx;
|
||||
}
|
||||
},
|
||||
|
||||
is_object: function(a) {
|
||||
return a && typeof a == "object";
|
||||
},
|
||||
|
||||
is_array: function(a) {
|
||||
return Object.prototype.toString.call(a) === '[object Array]';
|
||||
},
|
||||
|
||||
/*
|
||||
Gets rid of leading and trailing whitespace
|
||||
*/
|
||||
trim: function(s) {
|
||||
return s.replace(/^\s*|\s*$/g, "");
|
||||
},
|
||||
|
||||
/*
|
||||
Why, why, why? Because IE. Cry, cry cry.
|
||||
*/
|
||||
map: function(array, fn) {
|
||||
if (typeof array.map == "function") {
|
||||
return array.map(fn);
|
||||
} else {
|
||||
var r = [];
|
||||
var l = array.length;
|
||||
for(var i = 0; i < l; i++) {
|
||||
r.push(fn(array[i]));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return({
|
||||
name: "mustache.js",
|
||||
version: "0.3.1-dev",
|
||||
|
||||
/*
|
||||
Turns a template and view into HTML
|
||||
*/
|
||||
to_html: function(template, view, partials, send_fun) {
|
||||
var renderer = new Renderer();
|
||||
if(send_fun) {
|
||||
renderer.send = send_fun;
|
||||
}
|
||||
renderer.render(template, view, partials);
|
||||
if(!send_fun) {
|
||||
return renderer.buffer.join("\n");
|
||||
}
|
||||
}
|
||||
});
|
||||
}();
|
||||
/*
|
||||
* jQuery i18n plugin
|
||||
* @requires jQuery v1.1 or later
|
||||
*
|
||||
* See http://recursive-design.com/projects/jquery-i18n/
|
||||
*
|
||||
* Licensed under the MIT license:
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* Version: 0.9.2 (201111281039)
|
||||
*/
|
||||
(function($) {
|
||||
/**
|
||||
* i18n provides a mechanism for translating strings using a jscript dictionary.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* i18n property list
|
||||
*/
|
||||
$.i18n = {
|
||||
|
||||
dict: null,
|
||||
|
||||
/**
|
||||
* setDictionary()
|
||||
* Initialise the dictionary and translate nodes
|
||||
*
|
||||
* @param property_list i18n_dict : The dictionary to use for translation
|
||||
*/
|
||||
setDictionary: function(i18n_dict) {
|
||||
this.dict = i18n_dict;
|
||||
},
|
||||
|
||||
/**
|
||||
* _()
|
||||
* The actual translation function. Looks the given string up in the
|
||||
* dictionary and returns the translation if one exists. If a translation
|
||||
* is not found, returns the original word
|
||||
*
|
||||
* @param string str : The string to translate
|
||||
* @param property_list params : params for using printf() on the string
|
||||
* @return string : Translated word
|
||||
*
|
||||
*/
|
||||
_: function (str, params) {
|
||||
var transl = str;
|
||||
if (this.dict && this.dict[str]) {
|
||||
transl = this.dict[str];
|
||||
}
|
||||
return this.printf(transl, params);
|
||||
},
|
||||
|
||||
/**
|
||||
* toEntity()
|
||||
* Change non-ASCII characters to entity representation
|
||||
*
|
||||
* @param string str : The string to transform
|
||||
* @return string result : Original string with non-ASCII content converted to entities
|
||||
*
|
||||
*/
|
||||
toEntity: function (str) {
|
||||
var result = '';
|
||||
for (var i=0;i<str.length; i++) {
|
||||
if (str.charCodeAt(i) > 128)
|
||||
result += "&#"+str.charCodeAt(i)+";";
|
||||
else
|
||||
result += str.charAt(i);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* stripStr()
|
||||
*
|
||||
* @param string str : The string to strip
|
||||
* @return string result : Stripped string
|
||||
*
|
||||
*/
|
||||
stripStr: function(str) {
|
||||
return str.replace(/^\s*/, "").replace(/\s*$/, "");
|
||||
},
|
||||
|
||||
/**
|
||||
* stripStrML()
|
||||
*
|
||||
* @param string str : The multi-line string to strip
|
||||
* @return string result : Stripped string
|
||||
*
|
||||
*/
|
||||
stripStrML: function(str) {
|
||||
// Split because m flag doesn't exist before JS1.5 and we need to
|
||||
// strip newlines anyway
|
||||
var parts = str.split('\n');
|
||||
for (var i=0; i<parts.length; i++)
|
||||
parts[i] = stripStr(parts[i]);
|
||||
|
||||
// Don't join with empty strings, because it "concats" words
|
||||
// And strip again
|
||||
return stripStr(parts.join(" "));
|
||||
},
|
||||
|
||||
/*
|
||||
* printf()
|
||||
* C-printf like function, which substitutes %s with parameters
|
||||
* given in list. %%s is used to escape %s.
|
||||
*
|
||||
* Doesn't work in IE5.0 (splice)
|
||||
*
|
||||
* @param string S : string to perform printf on.
|
||||
* @param string L : Array of arguments for printf()
|
||||
*/
|
||||
printf: function(S, L) {
|
||||
if (!L) return S;
|
||||
|
||||
var nS = "",
|
||||
search = /%(\d+)\$s/g;
|
||||
// replace %n1$ where n is a number
|
||||
while (result = search.exec(S)) {
|
||||
var index = parseInt(result[1], 10) -1;
|
||||
S = S.replace('%' + result[1] + '\$s', (L[index]));
|
||||
L.splice(index, 1);
|
||||
}
|
||||
var tS = S.split("%s");
|
||||
if (tS.length > 1) {
|
||||
for(var i = 0; i < L.length; i++) {
|
||||
if (tS[i].lastIndexOf('%') == tS[i].length-1 && i != L.length-1)
|
||||
tS[i] += "s"+tS.splice(i+1,1)[0];
|
||||
nS += tS[i] + L[i];
|
||||
}
|
||||
}
|
||||
return nS + tS[tS.length-1];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* _t
|
||||
* Allows you to translate a jQuery selector
|
||||
*
|
||||
* eg $('h1')._t('some text')
|
||||
*
|
||||
* @param string str : The string to translate
|
||||
* @param property_list params : params for using printf() on the string
|
||||
* @return element : chained and translated element(s)
|
||||
*/
|
||||
$.fn._t = function(str, params) {
|
||||
return $(this).text($.i18n._(str, params));
|
||||
};
|
||||
|
||||
|
||||
})(jQuery);
|
||||
/*
|
||||
* Date Format 1.2.3
|
||||
* (c) 2007-2009 Steven Levithan <stevenlevithan.com>
|
||||
* MIT license
|
||||
*
|
||||
* Includes enhancements by Scott Trenda <scott.trenda.net>
|
||||
* and Kris Kowal <cixar.com/~kris.kowal/>
|
||||
*
|
||||
* Accepts a date, a mask, or a date and a mask.
|
||||
* Returns a formatted version of the given date.
|
||||
* The date defaults to the current date/time.
|
||||
* The mask defaults to dateFormat.masks.default.
|
||||
*
|
||||
* @link http://blog.stevenlevithan.com/archives/date-time-format
|
||||
*/
|
||||
|
||||
var dateFormat = function () {
|
||||
var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
|
||||
timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
|
||||
timezoneClip = /[^-+\dA-Z]/g,
|
||||
pad = function (val, len) {
|
||||
val = String(val);
|
||||
len = len || 2;
|
||||
while (val.length < len) val = "0" + val;
|
||||
return val;
|
||||
};
|
||||
|
||||
// Regexes and supporting functions are cached through closure
|
||||
return function (date, mask, utc) {
|
||||
var dF = dateFormat;
|
||||
|
||||
// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
|
||||
if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
|
||||
mask = date;
|
||||
date = undefined;
|
||||
}
|
||||
|
||||
// Passing date through Date applies Date.parse, if necessary
|
||||
date = date ? new Date(date) : new Date;
|
||||
if (isNaN(date)) throw SyntaxError("invalid date");
|
||||
|
||||
mask = String(dF.masks[mask] || mask || dF.masks["default"]);
|
||||
|
||||
// Allow setting the utc argument via the mask
|
||||
if (mask.slice(0, 4) == "UTC:") {
|
||||
mask = mask.slice(4);
|
||||
utc = true;
|
||||
}
|
||||
|
||||
var _ = utc ? "getUTC" : "get",
|
||||
d = date[_ + "Date"](),
|
||||
D = date[_ + "Day"](),
|
||||
m = date[_ + "Month"](),
|
||||
y = date[_ + "FullYear"](),
|
||||
H = date[_ + "Hours"](),
|
||||
M = date[_ + "Minutes"](),
|
||||
s = date[_ + "Seconds"](),
|
||||
L = date[_ + "Milliseconds"](),
|
||||
o = utc ? 0 : date.getTimezoneOffset(),
|
||||
flags = {
|
||||
d: d,
|
||||
dd: pad(d),
|
||||
ddd: dF.i18n.dayNames[D],
|
||||
dddd: dF.i18n.dayNames[D + 7],
|
||||
m: m + 1,
|
||||
mm: pad(m + 1),
|
||||
mmm: dF.i18n.monthNames[m],
|
||||
mmmm: dF.i18n.monthNames[m + 12],
|
||||
yy: String(y).slice(2),
|
||||
yyyy: y,
|
||||
h: H % 12 || 12,
|
||||
hh: pad(H % 12 || 12),
|
||||
H: H,
|
||||
HH: pad(H),
|
||||
M: M,
|
||||
MM: pad(M),
|
||||
s: s,
|
||||
ss: pad(s),
|
||||
l: pad(L, 3),
|
||||
L: pad(L > 99 ? Math.round(L / 10) : L),
|
||||
t: H < 12 ? "a" : "p",
|
||||
tt: H < 12 ? "am" : "pm",
|
||||
T: H < 12 ? "A" : "P",
|
||||
TT: H < 12 ? "AM" : "PM",
|
||||
Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
|
||||
o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
|
||||
S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
|
||||
};
|
||||
|
||||
return mask.replace(token, function ($0) {
|
||||
return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
|
||||
});
|
||||
};
|
||||
}();
|
||||
|
||||
// Some common format strings
|
||||
dateFormat.masks = {
|
||||
"default": "ddd mmm dd yyyy HH:MM:ss",
|
||||
shortDate: "m/d/yy",
|
||||
mediumDate: "mmm d, yyyy",
|
||||
longDate: "mmmm d, yyyy",
|
||||
fullDate: "dddd, mmmm d, yyyy",
|
||||
shortTime: "h:MM TT",
|
||||
mediumTime: "h:MM:ss TT",
|
||||
longTime: "h:MM:ss TT Z",
|
||||
isoDate: "yyyy-mm-dd",
|
||||
isoTime: "HH:MM:ss",
|
||||
isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
|
||||
isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
|
||||
};
|
||||
|
||||
// Internationalization strings
|
||||
dateFormat.i18n = {
|
||||
dayNames: [
|
||||
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
|
||||
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
|
||||
],
|
||||
monthNames: [
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
|
||||
]
|
||||
};
|
||||
|
||||
// For convenience...
|
||||
Date.prototype.format = function (mask, utc) {
|
||||
return dateFormat(this, mask, utc);
|
||||
};
|
||||
1
lms/static/js/vendor/candy_libs/libs.min.js
vendored
Normal file
49
lms/static/js/vendor/candy_ui.js
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
$(window).load(function() {
|
||||
var isInIframe = (window.location != window.parent.location) ? true : false;
|
||||
|
||||
if (isInIframe) {
|
||||
$('#chat-pane #chat-tabs').prepend('<div id="chat-expand-arrow"><em class="icon-chevron-right"></em></div>');
|
||||
} else {
|
||||
$('#candy').addClass('poppedOut').append('<a href="#" onclick="event.preventDefault();" title="Pop-In Chat Window" class="icon-signin" id="chatPopin"></a>');
|
||||
}
|
||||
|
||||
var collapseMessageForm = function() {
|
||||
$('#candy').animate({width: '230px'}, 'slow', function() {
|
||||
$('#chat-expand-arrow em').toggleClass('icon-chevron-left').toggleClass('icon-chevron-right');
|
||||
$('#chat-pane').toggleClass('collapsed-message-pane');
|
||||
});
|
||||
$('#chat-pane .roster-pane').animate({top: '0px'}, 'slow');
|
||||
$('#chat-rooms .message-pane-wrapper, #chat-rooms .message-form-wrapper, form.message-form').fadeOut('slow');
|
||||
}
|
||||
|
||||
var expandMessageForm = function() {
|
||||
$('#chat-pane').toggleClass('collapsed-message-pane');
|
||||
$('#candy').animate({width: '100%'}, 'slow', function() {
|
||||
$('#chat-expand-arrow em').toggleClass('icon-chevron-left').toggleClass('icon-chevron-right');
|
||||
});
|
||||
$('#chat-pane .roster-pane').animate({top: '30px'}, 'slow');
|
||||
$('#chat-rooms .message-pane-wrapper, #chat-rooms .message-form-wrapper, form.message-form').fadeIn('slow');
|
||||
}
|
||||
|
||||
var activeTab;
|
||||
$('#chat-expand-arrow').click(function() {
|
||||
if ($('#chat-pane').hasClass('collapsed-message-pane')) {
|
||||
activeTab.addClass('active');
|
||||
expandMessageForm();
|
||||
} else {
|
||||
activeTab = $('#chat-tabs li.active');
|
||||
$('#chat-tabs li').removeClass('active');
|
||||
collapseMessageForm();
|
||||
}
|
||||
});
|
||||
|
||||
$('#chat-tabs').click(function(event) {
|
||||
if ($(this).has(event.target).length && $('#chat-pane').hasClass('collapsed-message-pane')) {
|
||||
expandMessageForm();
|
||||
}
|
||||
});
|
||||
|
||||
$('#chatPopin').click(function() {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,7 @@
|
||||
@import 'course/courseware/amplifier';
|
||||
@import 'course/layout/calculator';
|
||||
@import 'course/layout/timer';
|
||||
@import 'course/layout/chat';
|
||||
|
||||
// course-specific courseware (all styles in these files should be gated by a
|
||||
// course-specific class). This should be replaced with a better way of
|
||||
|
||||
57
lms/static/sass/course/layout/_chat.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
/* Chat
|
||||
-------------------------------------------------- */
|
||||
#chat-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#chat-toggle {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -45px;
|
||||
height: 25px;
|
||||
width: 170px;
|
||||
padding: 10px 15px;
|
||||
background: #333;
|
||||
border-top-right-radius: 10px;
|
||||
|
||||
span {
|
||||
color: #dcdcdc;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#chat-toggle:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#chat-open {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#chat-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chat-toggle em {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 12px;
|
||||
font-size: 28px !important;
|
||||
}
|
||||
|
||||
#chat-block {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 600px;
|
||||
height: 0px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
display: none;
|
||||
}
|
||||
@@ -6,6 +6,13 @@
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
<%include file="../discussion/_js_head_dependencies.html" />
|
||||
% if show_chat:
|
||||
<link rel="stylesheet" href="${static.url('css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css')}" />
|
||||
## It'd be better to have this in a place like lms/css/vendor/candy,
|
||||
## but the candy_res/ folder contains images and other junk, and it
|
||||
## all needs to stay together for the Candy.js plugin to work.
|
||||
<link rel="stylesheet" href="${static.url('candy_res/candy_full.css')}" />
|
||||
% endif
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
@@ -108,6 +115,40 @@
|
||||
</script>
|
||||
% endif
|
||||
|
||||
% if show_chat:
|
||||
<script type="text/javascript" src="${static.url('js/vendor/candy_libs/libs.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/candy.min.js')}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
// initialize the Candy.js plugin
|
||||
$(document).ready(function() {
|
||||
Candy.init("http://${chat['domain']}:5280/http-bind/", {
|
||||
core: { debug: true, autojoin: ["${chat['room']}@conference.${chat['domain']}"] },
|
||||
view: { resources: "${static.url('candy_res/')}"}
|
||||
});
|
||||
Candy.Core.connect("${chat['username']}", "${chat['password']}");
|
||||
|
||||
// show/hide the chat widget
|
||||
$('#chat-toggle').click(function() {
|
||||
var toggle = $(this);
|
||||
if (toggle.hasClass('closed')) {
|
||||
$('#chat-block').show().animate({height: '400px'}, 'slow', function() {
|
||||
$('#chat-open').hide();
|
||||
$('#chat-close').show();
|
||||
});
|
||||
} else {
|
||||
$('#chat-block').animate({height: '0px'}, 'slow', function() {
|
||||
$('#chat-open').show();
|
||||
$('#chat-close').hide();
|
||||
$(this).hide(); // do this at the very end
|
||||
});
|
||||
}
|
||||
toggle.toggleClass('closed');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
|
||||
</%block>
|
||||
|
||||
% if timer_expiration_duration:
|
||||
@@ -148,6 +189,18 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
% if show_chat:
|
||||
<div id="chat-wrapper">
|
||||
<div id="chat-toggle" class="closed">
|
||||
<span id="chat-open">Open Chat <em class="icon-chevron-up"></em></span>
|
||||
<span id="chat-close">Close Chat <em class="icon-chevron-down"></em></span>
|
||||
</div>
|
||||
<div id="chat-block">
|
||||
## The Candy.js plugin wants to render in an element with #candy
|
||||
<div id="candy"></div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if course.show_calculator:
|
||||
<div class="calc-main">
|
||||
|
||||