Commit 9d77a4ef authored by JC Brand's avatar JC Brand

Fixes #129 Add support for XEP-0156.

Only XML is supported for now.
parent 54e9c51a
......@@ -2,11 +2,18 @@
## 6.0.0 (Unreleased)
- #129: Add support for XEP-0156: Disovering Alternative XMPP Connection Methods. Only XML is supported for now.
- #1691 Fix `collection.chatbox is undefined` errors
- Prevent editing of sent file uploads.
### Breaking changes
- In order to add support for XEP-0156, the XMPP connection needs to be created
only once we know the JID of the user that's logging in. This means that the
[connectionInitialized](https://conversejs.org/docs/html/api/-_converse.html#event:connectionInitialized)
event now fires much later than before. Plugins that rely on `connectionInitialized`
being triggered before the user's JID has been provided will need to be updated.
- The following API methods now return promises:
* `_converse.api.chats.get`
* `_converse.api.chats.create`
......
......@@ -639,6 +639,23 @@ The default chat status that the user wil have. If you for example set this to
``'chat'``, then Converse will send out a presence stanza with ``"show"``
set to ``'chat'`` as soon as you've been logged in.
discover_connection_methods
---------------------------
* Default: ``false``
Use `XEP-0156 <https://xmpp.org/extensions/xep-0156.html>`_ to discover whether
the XMPP host for the current user advertises any Websocket or BOSH connection
URLs that can be used.
If this is set to ``false``, then a `websocket_url`_ or `bosh_service_url`_ need to be
set.
Currently only the XML encoded host-meta resource is supported as shown in
`Example 2 under section 3.3 <https://xmpp.org/extensions/xep-0156.html#httpexamples>`_.
domain_placeholder
------------------
......@@ -647,8 +664,6 @@ domain_placeholder
The placeholder text shown in the domain input on the registration form.
emoji_image_path
----------------
......@@ -1624,6 +1639,7 @@ Allows you to show or hide buttons on the chatboxes' toolbars.
.. _`websocket-url`:
websocket_url
-------------
......
......@@ -404,7 +404,7 @@
it("can be retrieved from the XMPP server", mock.initConverse(
{'connection': ['send']}, ['chatBoxesFetched', 'roomsPanelRendered', 'rosterGroupsFetched'], {},
null, ['chatBoxesFetched', 'roomsPanelRendered', 'rosterGroupsFetched'], {},
async function (done, _converse) {
await test_utils.waitUntilDiscoConfirmed(
......@@ -421,25 +421,12 @@
* </pubsub>
* </iq>
*/
let IQ_id;
const call = await u.waitUntil(() =>
_.filter(
_converse.connection.send.calls.all(),
call => {
const stanza = call.args[0];
if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
return;
}
if (sizzle('items[node="storage:bookmarks"]', stanza).length) {
IQ_id = stanza.getAttribute('id');
return true;
}
}
).pop()
);
const IQ_stanzas = _converse.connection.IQ_stanzas;
const sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop());
expect(Strophe.serialize(call.args[0])).toBe(
`<iq from="romeo@montague.lit/orchard" id="${IQ_id}" type="get" xmlns="jabber:client">`+
expect(Strophe.serialize(sent_stanza)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
'<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
'<items node="storage:bookmarks"/>'+
'</pubsub>'+
......@@ -469,7 +456,7 @@
expect(_converse.bookmarks.models.length).toBe(0);
spyOn(_converse.bookmarks, 'onBookmarksReceived').and.callThrough();
var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id})
var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')})
.c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('items', {'node': 'storage:bookmarks'})
.c('item', {'id': 'current'})
......@@ -495,7 +482,7 @@
describe("The rooms panel", function () {
it("shows a list of bookmarks", mock.initConverse(
{'connection': ['send']}, ['rosterGroupsFetched'], {},
null, ['rosterGroupsFetched'], {},
async function (done, _converse) {
await test_utils.waitUntilDiscoConfirmed(
......@@ -505,31 +492,19 @@
);
test_utils.openControlBox();
let IQ_id;
const call = await u.waitUntil(() =>
_.filter(
_converse.connection.send.calls.all(),
call => {
const stanza = call.args[0];
if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
return;
}
if (sizzle('items[node="storage:bookmarks"]', stanza).length) {
IQ_id = stanza.getAttribute('id');
return true;
}
}
).pop()
);
expect(Strophe.serialize(call.args[0])).toBe(
`<iq from="romeo@montague.lit/orchard" id="${IQ_id}" type="get" xmlns="jabber:client">`+
const IQ_stanzas = _converse.connection.IQ_stanzas;
const sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop());
expect(Strophe.serialize(sent_stanza)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
'<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
'<items node="storage:bookmarks"/>'+
'</pubsub>'+
'</iq>'
);
const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id})
const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')})
.c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('items', {'node': 'storage:bookmarks'})
.c('item', {'id': 'current'})
......@@ -583,7 +558,7 @@
it("remembers the toggle state of the bookmarks list", mock.initConverse(
{'connection': ['send']}, ['rosterGroupsFetched'], {},
null, ['rosterGroupsFetched'], {},
async function (done, _converse) {
test_utils.openControlBox();
......@@ -593,31 +568,19 @@
['http://jabber.org/protocol/pubsub#publish-options']
);
let IQ_id;
const call = await u.waitUntil(() =>
_.filter(
_converse.connection.send.calls.all(),
call => {
const stanza = call.args[0];
if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
return;
}
if (sizzle('items[node="storage:bookmarks"]', stanza).length) {
IQ_id = stanza.getAttribute('id');
return true;
}
}
).pop()
);
expect(Strophe.serialize(call.args[0])).toBe(
`<iq from="romeo@montague.lit/orchard" id="${IQ_id}" type="get" xmlns="jabber:client">`+
const IQ_stanzas = _converse.connection.IQ_stanzas;
const sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop());
expect(Strophe.serialize(sent_stanza)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
'<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
'<items node="storage:bookmarks"/>'+
'</pubsub>'+
'</iq>'
);
const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id})
const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')})
.c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('items', {'node': 'storage:bookmarks'})
.c('item', {'id': 'current'})
......
......@@ -8,7 +8,7 @@
it("contains a checkbox to indicate whether the computer is trusted or not",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ auto_login: false,
allow_registration: false },
async function (done, _converse) {
......@@ -42,7 +42,7 @@
it("checkbox can be set to false by default",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ auto_login: false,
trusted: false,
allow_registration: false },
......
......@@ -10,7 +10,7 @@
it("is not available unless allow_registration=true",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ auto_login: false,
allow_registration: false },
async function (done, _converse) {
......@@ -24,7 +24,7 @@
it("can be opened by clicking on the registration tab",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ auto_login: false,
allow_registration: true },
async function (done, _converse) {
......@@ -45,18 +45,18 @@
it("allows the user to choose an XMPP provider's domain",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ auto_login: false,
allow_registration: true },
async function (done, _converse) {
spyOn(Strophe.Connection.prototype, 'connect');
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
test_utils.openControlBox();
const cbview = _converse.chatboxviews.get('controlbox');
const registerview = cbview.registerpanel;
spyOn(registerview, 'onProviderChosen').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
spyOn(_converse.connection, 'connect');
// Open the register panel
cbview.el.querySelector('.toggle-register-login').click();
......@@ -75,17 +75,18 @@
form.querySelector('input[name=domain]').value = 'conversejs.org';
submit_button.click();
expect(registerview.onProviderChosen).toHaveBeenCalled();
expect(_converse.connection.connect).toHaveBeenCalled();
await u.waitUntil(() => _converse.connection.connect.calls.count());
done();
}));
it("will render a registration form as received from the XMPP provider",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ auto_login: false,
allow_registration: true },
async function (done, _converse) {
spyOn(Strophe.Connection.prototype, 'connect');
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
test_utils.openControlBox();
const cbview = _converse.chatboxviews.get('controlbox');
......@@ -97,7 +98,6 @@
spyOn(registerview, 'onRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
spyOn(_converse.connection, 'connect').and.callThrough();
expect(registerview._registering).toBeFalsy();
expect(_converse.connection.connected).toBeFalsy();
......@@ -105,7 +105,7 @@
registerview.el.querySelector('input[type=submit]').click();
expect(registerview.onProviderChosen).toHaveBeenCalled();
expect(registerview._registering).toBeTruthy();
expect(_converse.connection.connect).toHaveBeenCalled();
await u.waitUntil(() => _converse.connection.connect.calls.count());
let stanza = new Strophe.Builder("stream:features", {
'xmlns:stream': "http://etherx.jabber.org/streams",
......@@ -137,7 +137,7 @@
it("will set form_type to legacy and submit it as legacy",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ auto_login: false,
allow_registration: true },
async function (done, _converse) {
......@@ -194,7 +194,7 @@
it("will set form_type to xform and submit it as xform",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ auto_login: false,
allow_registration: true },
async function (done, _converse) {
......@@ -267,7 +267,7 @@
it("renders the account registration form",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ auto_login: false,
view_mode: 'fullscreen',
allow_registration: true },
......
......@@ -53,7 +53,7 @@
it("uses bookmarks to determine groupchat names",
mock.initConverse(
{'connection': ['send']},
null,
['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'],
{'view_mode': 'fullscreen'},
async function (done, _converse) {
......@@ -113,7 +113,7 @@
describe("A groupchat shown in the groupchats list", function () {
it("is highlighted if its currently open", mock.initConverse(
it("is highlighted if it's currently open", mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'],
{ view_mode: 'fullscreen',
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
......@@ -137,8 +137,6 @@
expect(room_els.length).toBe(1);
item = room_els[0];
expect(item.textContent.trim()).toBe('balcony@chat.shakespeare.lit');
const conv_el = document.querySelector('#conversejs');
conv_el.parentElement.removeChild(conv_el);
done();
}));
......
......@@ -11,7 +11,7 @@
it("gets enabled with an <enable> stanza and resumed with a <resume> stanza",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
null, ['chatBoxesInitialized'],
{ 'auto_login': false,
'enable_smacks': true,
'show_controlbox_by_default': true,
......
......@@ -1124,7 +1124,7 @@ converse.plugins.add('converse-chatview', {
if (Backbone.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) {
_converse.router.navigate('');
}
if (_converse.connection.connected) {
if (_converse.api.connection.connected()) {
// Immediately sending the chat state, because the
// model is going to be destroyed afterwards.
this.model.setChatState(_converse.INACTIVE);
......
......@@ -154,6 +154,7 @@ converse.plugins.add('converse-controlbox', {
_converse.api.promises.add('controlBoxInitialized');
const addControlBox = () => _converse.chatboxes.add({'id': 'controlbox'});
_converse.ControlBox = _converse.ChatBox.extend({
......@@ -220,9 +221,9 @@ converse.plugins.add('converse-controlbox', {
} else {
this.hide();
}
if (!_converse.connection.connected ||
!_converse.connection.authenticated ||
_converse.connection.disconnecting) {
const connection = get(_converse, 'connection', {});
if (!connection.connected || !connection.authenticated || connection.disconnecting) {
this.renderLoginPanel();
} else if (this.model.get('connected')) {
this.renderControlBoxPane();
......@@ -296,7 +297,8 @@ converse.plugins.add('converse-controlbox', {
if (_converse.sticky_controlbox) {
return;
}
if (_converse.connection.connected && !_converse.connection.disconnecting) {
const connection = get(_converse, 'connection', {});
if (connection.connected && !connection.disconnecting) {
this.model.save({'closed': true});
} else {
this.model.trigger('hide');
......@@ -319,7 +321,8 @@ converse.plugins.add('converse-controlbox', {
}
u.addClass('hidden', this.el);
_converse.api.trigger('chatBoxClosed', this);
if (!_converse.connection.connected) {
if (!_converse.api.connection.connected()) {
_converse.controlboxtoggle.render();
}
_converse.controlboxtoggle.show(callback);
......@@ -464,7 +467,7 @@ converse.plugins.add('converse-controlbox', {
if (["converse/login", "converse/register"].includes(Backbone.history.getFragment())) {
_converse.router.navigate('', {'replace': true});
}
_converse.connection.reset();
_converse.connection && _converse.connection.reset();
_converse.api.user.login(jid, password);
}
});
......@@ -510,7 +513,7 @@ converse.plugins.add('converse-controlbox', {
// artifacts (i.e. on page load the toggle is shown only to then
// seconds later be hidden in favor of the controlbox).
this.el.innerHTML = tpl_controlbox_toggle({
'label_toggle': _converse.connection.connected ? __('Chat Contacts') : __('Toggle chat')
'label_toggle': _converse.api.connection.connected() ? __('Chat Contacts') : __('Toggle chat')
})
return this;
},
......@@ -529,7 +532,7 @@ converse.plugins.add('converse-controlbox', {
if (!controlbox) {
controlbox = addControlBox();
}
if (_converse.connection.connected) {
if (_converse.api.connection.connected()) {
controlbox.save({'closed': false});
} else {
controlbox.trigger('show');
......@@ -540,7 +543,7 @@ converse.plugins.add('converse-controlbox', {
e.preventDefault();
if (u.isVisible(_converse.root.querySelector("#controlbox"))) {
const controlbox = _converse.chatboxes.get('controlbox');
if (_converse.connection.connected) {
if (_converse.api.connection.connected) {
controlbox.save({closed: true});
} else {
controlbox.trigger('hide');
......@@ -582,10 +585,9 @@ converse.plugins.add('converse-controlbox', {
});
Promise.all([
_converse.api.waitUntil('connectionInitialized'),
_converse.api.waitUntil('chatBoxViewsInitialized')
]).then(addControlBox).catch(e => _converse.log(e, Strophe.LogLevel.FATAL));
.then(addControlBox)
.catch(e => _converse.log(e, Strophe.LogLevel.FATAL));
_converse.api.listen.on('chatBoxesFetched', () => {
const controlbox = _converse.chatboxes.get('controlbox') || addControlBox();
......
......@@ -351,7 +351,7 @@ converse.plugins.add('converse-dragresize', {
_converse.resizing.chatbox.width,
_converse.resizing.chatbox.model.get('default_width')
);
if (_converse.connection.connected) {
if (_converse.api.connection.connected()) {
_converse.resizing.chatbox.model.save({'height': height});
_converse.resizing.chatbox.model.save({'width': width});
} else {
......
......@@ -318,7 +318,7 @@ converse.plugins.add('converse-minimize', {
* @param { _converse.ChatBoxView|_converse.ChatRoomView|_converse.ControlBoxView|_converse.HeadlinesBoxView } [newchat]
*/
async trimChats (newchat) {
if (_converse.no_trimming || !_converse.connection.connected || _converse.view_mode !== 'overlayed') {
if (_converse.no_trimming || !_converse.api.connection.connected() || _converse.view_mode !== 'overlayed') {
return;
}
const shown_chats = this.getShownChats();
......@@ -556,10 +556,7 @@ converse.plugins.add('converse-minimize', {
});
/************************ BEGIN Event Handlers ************************/
Promise.all([
_converse.api.waitUntil('connectionInitialized'),
_converse.api.waitUntil('chatBoxViewsInitialized')
]).then(() => {
_converse.api.waitUntil('chatBoxViewsInitialized').then(() => {
_converse.minimized_chats = new _converse.MinimizedChats({
model: _converse.chatboxes
});
......
......@@ -240,8 +240,8 @@ converse.plugins.add('converse-omemo', {
/* The initialize function gets called as soon as the plugin is
* loaded by Converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
const { _converse } = this;
const { __ } = _converse;
_converse.api.settings.update({
'omemo_default': false,
......
......@@ -175,7 +175,7 @@ converse.plugins.add('converse-register', {
initialize () {
this.reset();
this.registerHooks();
_converse.api.listen.on('connectionInitialized', () => this.registerHooks());
},
render () {
......@@ -340,7 +340,7 @@ converse.plugins.add('converse-register', {
* @method _converse.RegisterPanel#fetchRegistrationForm
* @param { String } domain_name - XMPP server domain
*/
fetchRegistrationForm (domain_name) {
async fetchRegistrationForm (domain_name) {
if (!this.model.get('registration_form_rendered')) {
this.renderRegistrationRequest();
}
......@@ -348,7 +348,8 @@ converse.plugins.add('converse-register', {
'domain': Strophe.getDomainFromJid(domain_name),
'_registering': true
});
_converse.connection.connect(this.domain, "", this.onConnectStatusChanged.bind(this));
await _converse.initConnection(this.domain);
_converse.connection.connect(this.domain, "", status => this.onConnectStatusChanged(status));
return false;
},
......
......@@ -19,6 +19,10 @@ const BOSH_SESSION_ID = 'converse.bosh-session';
converse.plugins.add('converse-bosh', {
enabled () {
return true;
},
initialize () {
const { _converse } = this;
......@@ -35,9 +39,15 @@ converse.plugins.add('converse-bosh', {
_converse.bosh_session.browserStorage = new BrowserStorage.session(id);
await new Promise(resolve => _converse.bosh_session.fetch({'success': resolve, 'error': resolve}));
}
if (_converse.jid && _converse.bosh_session.get('jid') === _converse.jid) {
if (_converse.jid) {
if (_converse.bosh_session.get('jid') !== _converse.jid) {
const jid = await _converse.setUserJID(_converse.jid);
_converse.bosh_session.clear({'silent': true });
_converse.bosh_session.save({'jid': _converse.jid, id});
_converse.bosh_session.save({jid});
}
} else { // Keepalive
const jid = _converse.bosh_session.get('jid');
jid && await _converse.setUserJID();
}
return _converse.bosh_session;
}
......@@ -45,17 +55,17 @@ converse.plugins.add('converse-bosh', {
_converse.startNewPreboundBOSHSession = function () {
if (!_converse.prebind_url) {
throw new Error(
"attemptPreboundSession: If you use prebind then you MUST supply a prebind_url");
throw new Error("startNewPreboundBOSHSession: If you use prebind then you MUST supply a prebind_url");
}
const xhr = new XMLHttpRequest();
xhr.open('GET', _converse.prebind_url, true);
xhr.setRequestHeader('Accept', 'application/json, text/javascript');
xhr.onload = function () {
xhr.onload = async function () {
if (xhr.status >= 200 && xhr.status < 400) {
const data = JSON.parse(xhr.responseText);
const jid = await _converse.setUserJID(data.jid);
_converse.connection.attach(
data.jid,
jid,
data.sid,
data.rid,
_converse.onConnectStatusChanged
......@@ -79,9 +89,6 @@ converse.plugins.add('converse-bosh', {
_converse.restoreBOSHSession = async function () {
if (!_converse.api.connection.isType('bosh')) {
return false;
}
const jid = (await initBOSHSession()).get('jid');
if (jid) {
try {
......@@ -119,9 +126,7 @@ converse.plugins.add('converse-bosh', {
}
});
_converse.api.listen.on('addClientFeatures',
() => _converse.api.disco.own.features.add(Strophe.NS.BOSH)
);
_converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.BOSH));
/************************ END Event Handlers ************************/
......
......@@ -230,6 +230,7 @@ _converse.default_settings = {
csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
debug: false,
default_state: 'online',
discover_connection_methods: false,
geouri_regex: /https\:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([\-0-9.]+)\/([\-0-9.]+)\S*/g,
geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2',
idle_presence_timeout: 300, // Seconds after which an idle presence is sent
......@@ -330,7 +331,7 @@ function addPromise (promise) {
}
_converse.isTestEnv = function () {
return _.get(_converse.connection, 'service') === 'jasmine tests';
return Strophe.Connection.name === 'MockConnection';
}
......@@ -457,7 +458,7 @@ async function attemptNonPreboundSession (credentials, automatic) {
} else if (!_converse.isTestEnv() && window.PasswordCredential) {
connect(await getLoginCredentialsFromBrowser());
} else {
throw new Error("attemptNonPreboundSession: Could not find any credentials to log you in with!");
_converse.log("attemptNonPreboundSession: Could not find any credentials to log in with", Strophe.LogLevel.WARN);
}
} else if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(_converse.authentication) && (!automatic || _converse.auto_login)) {
connect();
......@@ -523,9 +524,7 @@ function reconnect () {
const debouncedReconnect = _.debounce(reconnect, 2000);
_converse.shouldClearCache = function () {
return !_converse.config.get('trusted') || _converse.isTestEnv();
}
_converse.shouldClearCache = () => (!_converse.config.get('trusted') || _converse.isTestEnv());
function clearSession () {
if (_converse.session !== undefined) {
......@@ -548,15 +547,66 @@ function clearSession () {
}
/**
* Creates a new Strophe.Connection instance and if applicable, attempt to
* restore the BOSH session or if `auto_login` is true, attempt to log in.
async function onDomainDiscovered (response) {
const text = await response.text();
const xrd = (new window.DOMParser()).parseFromString(text, "text/xml").firstElementChild;
if (xrd.nodeName != "XRD" || xrd.namespaceURI != "http://docs.oasis-open.org/ns/xri/xrd-1.0") {
return _converse.log("Could not discover XEP-0156 connection methods", Strophe.LogLevel.WARN);
}
const bosh_links = sizzle(`Link[rel="urn:xmpp:alt-connections:xbosh"]`, xrd);
const ws_links = sizzle(`Link[rel="urn:xmpp:alt-connections:websocket"]`, xrd);
const bosh_methods = bosh_links.map(el => el.getAttribute('href'));
const ws_methods = ws_links.map(el => el.getAttribute('href'));
// TODO: support multiple endpoints
_converse.websocket_url = ws_methods.pop();
_converse.bosh_service_url = bosh_methods.pop();
if (bosh_methods.length === 0 && ws_methods.length === 0) {
_converse.log(
"onDomainDiscovered: neither BOSH nor WebSocket connection methods have been specified with XEP-0156.",
Strophe.LogLevel.WARN
);
}
}
/* Use XEP-0156 to check whether this host advertises websocket or BOSH connection methods.
*/
_converse.initConnection = async function () {
if (!_converse.connection) {
if (!_converse.bosh_service_url && ! _converse.websocket_url) {
async function discoverConnectionMethods (domain) {
const options = {
'mode': 'cors',
'headers': {
'Accept': 'application/xrd+xml, text/xml'
}
};
const url = `https://${domain}/.well-known/host-meta`;
let response;
try {
response = await fetch(url, options);
} catch (e) {
_converse.log(`Failed to discover alternative connection methods at ${url}`, Strophe.LogLevel.ERROR);
return _converse.log(e, Strophe.LogLevel.ERROR);
}
if (response.status >= 200 && response.status < 400) {
await onDomainDiscovered(response);
} else {
_converse.log("Could not discover XEP-0156 connection methods", Strophe.LogLevel.WARN);
}
}
_converse.initConnection = async function (domain) {
if (_converse.discover_connection_methods) {
await discoverConnectionMethods(domain);
}
if (! _converse.bosh_service_url) {
if (_converse.authentication === _converse.PREBIND) {
throw new Error("authentication is set to 'prebind' but we don't have a BOSH connection");
}
if (! _converse.websocket_url) {
throw new Error("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both.");
}
}
if (('WebSocket' in window || 'MozWebSocket' in window) && _converse.websocket_url) {
_converse.connection = new Strophe.Connection(
_converse.websocket_url,
......@@ -575,10 +625,6 @@ _converse.initConnection = async function () {
throw new Error("initConnection: this browser does not support "+
"websockets and bosh_service_url wasn't specified.");
}
if (_converse.auto_login || _converse.keepalive) {
await _converse.api.user.login(null, null, true);
}
}
setUpXMLLogging();
/**
* Triggered once the `Strophe.Connection` constructor has been initialized, which
......@@ -587,10 +633,10 @@ _converse.initConnection = async function () {
* @event _converse#connectionInitialized
*/
_converse.api.trigger('connectionInitialized');
};
}
async function setUserJID (jid) {
async function initSession (jid) {
const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase();
const id = `converse.session-${bare_jid}`;
if (!_converse.session || _converse.session.get('id') !== id) {
......@@ -612,23 +658,14 @@ async function setUserJID (jid) {
} else {
saveJIDtoSession(jid);
}
/**
* Triggered whenever the user's JID has been updated
* @event _converse#setUserJID
*/
_converse.api.trigger('setUserJID');
return jid;
}
function saveJIDtoSession (jid) {
jid = _converse.session.get('jid') || jid;
if (_converse.authentication !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) {
jid = jid.toLowerCase() + _converse.generateResource();
}
// Set JID on the connection object so that when we call
// `connection.bind` the new resource is found by Strophe.js
// and sent to the XMPP server.
_converse.connection.jid = jid;
_converse.jid = jid;
_converse.bare_jid = Strophe.getBareJidFromJid(jid);
_converse.resource = Strophe.getResourceFromJid(jid);
......@@ -640,6 +677,37 @@ function saveJIDtoSession (jid) {
'domain': _converse.domain,
'active': true
});
// Set JID on the connection object so that when we call `connection.bind`
// the new resource is found by Strophe.js and sent to the XMPP server.
_converse.connection.jid = jid;
}
/**
* Stores the passed in JID for the current user, potentially creating a
* resource if the JID is bare.
*
* Given that we can only create an XMPP connection if we know the domain of
* the server connect to and we only know this once we know the JID, we also
* call {@link _converse.initConnection } (if necessary) to make sure that the
* connection is set up.
*
* @method _converse#setUserJID
* @emits _converse#setUserJID
* @params { String } jid
*/
_converse.setUserJID = async function (jid) {
if (!_converse.connection || !u.isSameDomain(_converse.connection.jid, jid)) {
const domain = Strophe.getDomainFromJid(jid)
await _converse.initConnection(domain);
}
await initSession(jid);
/**
* Triggered whenever the user's JID has been updated
* @event _converse#setUserJID
*/
_converse.api.trigger('setUserJID');
return jid;
}
......@@ -649,7 +717,7 @@ async function onConnected (reconnecting) {
*/
delete _converse.connection.reconnecting;
_converse.connection.flush(); // Solves problem of returned PubSub BOSH response not received by browser
await setUserJID(_converse.connection.jid);
await _converse.setUserJID(_converse.connection.jid);
/**
* Synchronous event triggered after we've sent an IQ to bind the
* user's JID resource for this session.
......@@ -681,9 +749,9 @@ function setUpXMLLogging () {
async function finishInitialization () {
initClientConfig();
initPlugins();
await _converse.initConnection();
_converse.registerGlobalEventHandlers();
if (!Backbone.history.started) {
if (!Backbone.History.started) {
Backbone.history.start();
}
if (_converse.idle_presence_timeout > 0) {
......@@ -691,6 +759,10 @@ async function finishInitialization () {
_converse.api.disco.own.features.add(Strophe.NS.IDLE);
});
}
if (_converse.auto_login ||
_converse.keepalive && _.invoke(_converse.pluggable.plugins['converse-bosh'], 'enabled')) {
await _converse.api.user.login(null, null, true);
}
}
......@@ -706,6 +778,7 @@ function finishDisconnection () {
_converse.connection.reset();
tearDown();
clearSession();
delete _converse.connection;
/**
* Triggered after converse.js has disconnected from the XMPP server.
* @event _converse#disconnected
......@@ -725,7 +798,7 @@ function fetchLoginCredentials (wait=0) {
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 400) {
const data = JSON.parse(xhr.responseText);
setUserJID(data.jid).then(() => {
_converse.setUserJID(data.jid).then(() => {
resolve({
jid: data.jid,
password: data.password
......@@ -761,7 +834,7 @@ async function getLoginCredentials () {
async function getLoginCredentialsFromBrowser () {
const creds = await navigator.credentials.get({'password': true});
if (creds && creds.type == 'password' && u.isValidJID(creds.id)) {
await setUserJID(creds.id);
await _converse.setUserJID(creds.id);
return {'jid': creds.id, 'password': creds.password};
}
}
......@@ -782,18 +855,20 @@ function cleanup () {
if (_converse.chatboxviews) {
delete _converse.chatboxviews;
}
if (_converse.connection) {
_converse.connection.reset();
}
_converse.stopListening();
_converse.off();
}
_converse.initialize = async function (settings, callback) {
cleanup();
settings = settings !== undefined ? settings : {};
const init_promise = u.getResolveablePromise();
PROMISES.forEach(addPromise);
if (_converse.connection !== undefined) {
cleanup();
}
if ('onpagehide' in window) {
// Pagehide gets thrown in more cases than unload. Specifically it
......@@ -877,7 +952,7 @@ _converse.initialize = async function (settings, callback) {
if (_converse.idle_seconds > 0) {
_converse.idle_seconds = 0;
}
if (!_converse.connection.authenticated) {
if (!_.get(_converse.connection, 'authenticated')) {
// We can't send out any stanzas when there's no authenticated connection.
// This can happen when the connection reconnects.
return;
......@@ -901,7 +976,7 @@ _converse.initialize = async function (settings, callback) {
/* An interval handler running every second.
* Used for CSI and the auto_away and auto_xa features.
*/
if (!_converse.connection.authenticated) {
if (!_.get(_converse.connection, 'authenticated')) {
// We can't send out any stanzas when there's no authenticated connection.
// This can happen when the connection reconnects.
return;
......@@ -1378,7 +1453,7 @@ _converse.api = {
* @returns {boolean} Whether there is an established connection or not.
*/
connected () {
return (_converse.connection && _converse.connection.connected) || false;
return _.get(_converse, 'connection', {}).connected && true;
},
/**
......@@ -1417,7 +1492,7 @@ _converse.api = {
// We also call `_proto._doDisconnect` so that connection event handlers
// for the old transport are removed.
if (_converse.api.connection.isType('websocket') && _converse.bosh_service_url) {
await setUserJID(_converse.bare_jid);
await _converse.setUserJID(_converse.bare_jid);
_converse.connection._proto._doDisconnect();
_converse.connection._proto = new Strophe.Bosh(_converse.connection);
_converse.connection.service = _converse.bosh_service_url;
......@@ -1426,9 +1501,9 @@ _converse.api = {
// When reconnecting anonymously, we need to connect with only
// the domain, not the full JID that we had in our previous
// (now failed) session.
await setUserJID(_converse.settings.jid);
await _converse.setUserJID(_converse.settings.jid);
} else {
await setUserJID(_converse.bare_jid);
await _converse.setUserJID(_converse.bare_jid);
}
_converse.connection._proto._doDisconnect();
_converse.connection._proto = new Strophe.Websocket(_converse.connection);
......@@ -1439,7 +1514,7 @@ _converse.api = {
// When reconnecting anonymously, we need to connect with only
// the domain, not the full JID that we had in our previous
// (now failed) session.
await setUserJID(_converse.settings.jid);
await _converse.setUserJID(_converse.settings.jid);
}
if (_converse.connection.reconnecting) {
debouncedReconnect();
......@@ -1527,20 +1602,19 @@ _converse.api = {
* fails to restore a previous auth'd session.
*/
async login (jid, password, automatic=false) {
if (_converse.api.connection.isType('bosh')) {
if (jid || _converse.jid) {
jid = await _converse.setUserJID(jid || _converse.jid);
}
// See whether there is a BOSH session to re-attach to
if (_.invoke(_converse.pluggable.plugins['converse-bosh'], 'enabled')) {
if (await _converse.restoreBOSHSession()) {
return;
} else if (_converse.authentication === _converse.PREBIND && (!automatic || _converse.auto_login)) {
return _converse.startNewPreboundBOSHSession();
}
} else if (_converse.authentication === _converse.PREBIND) {
throw new Error("authentication is set to 'prebind' but we don't have a BOSH connection");
}
if (jid || _converse.jid) {
// Reassign because we might have gained a resource
jid = await setUserJID(jid || _converse.jid);
}
password = password || _converse.password;
const credentials = (jid && password) ? { jid, password } : null;
attemptNonPreboundSession(credentials, automatic);
......@@ -1866,6 +1940,11 @@ _converse.api = {
* _converse.api.send(msg);
*/
send (stanza) {
if (!_converse.api.connection.connected()) {
_converse.log("Not sending stanza because we're not connected!", Strophe.LogLevel.WARN);
_converse.log(Strophe.serialize(stanza), Strophe.LogLevel.WARN);
return;
}
if (_.isString(stanza)) {
stanza = u.toStanza(stanza);
}
......
......@@ -633,7 +633,7 @@ converse.plugins.add('converse-muc', {
disco_entity.destroy();
}
}
if (_converse.connection.connected) {
if (_converse.api.connection.connected()) {
this.sendUnavailablePresence(exit_msg);
}
u.safeSave(this, {'connection_status': converse.ROOMSTATUS.DISCONNECTED});
......
......@@ -107,6 +107,15 @@ u.isSameBareJID = function (jid1, jid2) {
Strophe.getBareJidFromJid(jid2).toLowerCase();
};
u.isSameDomain = function (jid1, jid2) {
if (!_.isString(jid1) || !_.isString(jid2)) {
return false;
}
return Strophe.getDomainFromJid(jid1).toLowerCase() ===
Strophe.getDomainFromJid(jid2).toLowerCase();
};
u.isNewMessage = function (message) {
/* Given a stanza, determine whether it's a new
* message, i.e. not a MAM archived one.
......
......@@ -9,7 +9,6 @@
const Strophe = converse.env.Strophe;
const dayjs = converse.env.dayjs;
const $iq = converse.env.$iq;
const u = converse.env.utils;
window.libsignal = {
'SignalProtocolAddress': function (name, device_id) {
......@@ -32,7 +31,7 @@
return Promise.resolve(key_and_tag);
}
},
'SessionBuilder': function (storage, remote_address) {
'SessionBuilder': function (storage, remote_address) { // eslint-disable-line no-unused-vars
this.processPreKey = function () {
return Promise.resolve();
}
......@@ -116,15 +115,18 @@
'preventDefault': function () {}
};
mock.mock_connection = function () { // eslint-disable-line wrap-iife
return function () {
const OriginalConnection = Strophe.Connection;
function MockConnection (service, options) {
OriginalConnection.call(this, service, options);
Strophe.Bosh.prototype._processRequest = function () {}; // Don't attempt to send out stanzas
const c = new Strophe.Connection('jasmine tests');
const sendIQ = c.sendIQ;
const sendIQ = this.sendIQ;
c.IQ_stanzas = [];
c.IQ_ids = [];
c.sendIQ = function (iq, callback, errback) {
this.IQ_stanzas = [];
this.IQ_ids = [];
this.sendIQ = function (iq, callback, errback) {
if (!_.isElement(iq)) {
iq = iq.nodeTree;
}
......@@ -134,9 +136,9 @@
return id;
}
const send = c.send;
c.sent_stanzas = [];
c.send = function (stanza) {
const send = this.send;
this.sent_stanzas = [];
this.send = function (stanza) {
if (_.isElement(stanza)) {
this.sent_stanzas.push(stanza);
} else {
......@@ -145,11 +147,11 @@
return send.apply(this, arguments);
}
c.features = Strophe.xmlHtmlNode(
this.features = Strophe.xmlHtmlNode(
'<stream:features xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">'+
'<ver xmlns="urn:xmpp:features:rosterver"/>'+
'<csi xmlns="urn:xmpp:csi:0"/>'+
'<c xmlns="http://jabber.org/protocol/caps" ver="UwBpfJpEt3IoLYfWma/o/p3FFRo=" hash="sha-1" node="http://prosody.im"/>'+
'<this xmlns="http://jabber.org/protocol/caps" ver="UwBpfJpEt3IoLYfWma/o/p3FFRo=" hash="sha-1" node="http://prosody.im"/>'+
'<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">'+
'<required/>'+
'</bind>'+
......@@ -159,52 +161,41 @@
'</session>'+
'</stream:features>').firstChild;
c._proto._connect = function () {
c.connected = true;
c.mock = true;
c.jid = 'romeo@montague.lit/orchard';
c._changeConnectStatus(Strophe.Status.BINDREQUIRED);
this._proto._connect = () => {
this.connected = true;
this.mock = true;
this.jid = 'romeo@montague.lit/orchard';
this._changeConnectStatus(Strophe.Status.BINDREQUIRED);
};
c.bind = function () {
c.authenticated = true;
this.bind = () => {
this.authenticated = true;
c._changeConnectStatus(Strophe.Status.CONNECTED);
this.authenticated = true;
this._changeConnectStatus(Strophe.Status.CONNECTED);
};
c._proto._disconnect = function () {
c._onDisconnectTimeout();
this._proto._disconnect = () => this._onDisconnectTimeout();
this._proto._onDisconnectTimeout = _.noop;
}
c._proto._onDisconnectTimeout = _.noop;
return c;
};
}();
MockConnection.prototype = Object.create(OriginalConnection.prototype);
Strophe.Connection = MockConnection;
async function initConverse (settings, spies={}, promises) {
async function initConverse (settings, spies={}) {
window.localStorage.clear();
window.sessionStorage.clear();
const el = document.querySelector('#conversejs');
if (el) {
el.parentElement.removeChild(el);
}
const connection = mock.mock_connection();
if (spies && spies.connection) {
spies.connection.forEach(method => spyOn(connection, method));
}
const _converse = await converse.initialize(Object.assign({
'i18n': 'en',
'animate': false,
'auto_subscribe': false,
'play_sounds': false,
'bosh_service_url': 'montague.lit/http-bind',
'connection': connection,
'animate': false,
'use_emojione': false,
'debug': false,
'i18n': 'en',
'no_trimming': true,
'play_sounds': false,
'use_emojione': false,
'view_mode': mock.view_mode,
'debug': false
}, settings || {}));
if (spies && spies._converse) {
......@@ -214,7 +205,7 @@
_converse.ChatBoxViews.prototype.trimChat = function () {};
_converse.api.vcard.get = function (model, force) {
return new Promise((resolve, reject) => {
return new Promise(resolve => {
let jid;
if (_.isString(model)) {
jid = model;
......@@ -263,9 +254,13 @@
return async done => {
const _converse = await initConverse(settings, spies);
async function _done () {
if (_converse.api.connection.connected()) {
await _converse.api.user.logout();
}
const el = document.querySelector('#conversejs');
if (el) {
el.parentElement.removeChild(el);
}
done();
}
await Promise.all((promise_names || []).map(_converse.api.waitUntil));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment