Move converse-register plugin to folder
This commit is contained in:
parent
828eac1e72
commit
e31fa1f7dd
@ -27,7 +27,7 @@ import "./plugins/notifications.js";
|
|||||||
import "./plugins/omemo.js";
|
import "./plugins/omemo.js";
|
||||||
import "./plugins/profile.js";
|
import "./plugins/profile.js";
|
||||||
import "./plugins/push.js"; // XEP-0357 Push Notifications
|
import "./plugins/push.js"; // XEP-0357 Push Notifications
|
||||||
import "./plugins/register.js"; // XEP-0077 In-band registration
|
import "./plugins/register/index.js"; // XEP-0077 In-band registration
|
||||||
import "./plugins/roomslist/index.js"; // Show currently open chat rooms
|
import "./plugins/roomslist/index.js"; // Show currently open chat rooms
|
||||||
import "./plugins/rosterview/index.js";
|
import "./plugins/rosterview/index.js";
|
||||||
import "./plugins/singleton.js";
|
import "./plugins/singleton.js";
|
||||||
|
@ -1,644 +0,0 @@
|
|||||||
/**
|
|
||||||
* @module converse-register
|
|
||||||
* @description
|
|
||||||
* This is a Converse.js plugin which add support for in-band registration
|
|
||||||
* as specified in XEP-0077.
|
|
||||||
* @copyright 2020, the Converse.js contributors
|
|
||||||
* @license Mozilla Public License (MPLv2)
|
|
||||||
*/
|
|
||||||
import "./controlbox/index.js";
|
|
||||||
import log from "@converse/headless/log";
|
|
||||||
import tpl_form_input from "../templates/form_input.js";
|
|
||||||
import tpl_form_url from "../templates/form_url.js";
|
|
||||||
import tpl_form_username from "../templates/form_username.js";
|
|
||||||
import tpl_register_panel from "../templates/register_panel.html";
|
|
||||||
import tpl_registration_form from "../templates/registration_form.js";
|
|
||||||
import tpl_registration_request from "../templates/registration_request.html";
|
|
||||||
import tpl_spinner from "../templates/spinner.js";
|
|
||||||
import utils from "@converse/headless/utils/form";
|
|
||||||
import { View } from "@converse/skeletor/src/view";
|
|
||||||
import { __ } from 'i18n';
|
|
||||||
import { _converse, api, converse } from "@converse/headless/core";
|
|
||||||
import { pick } from "lodash-es";
|
|
||||||
import { render } from 'lit-html';
|
|
||||||
|
|
||||||
// Strophe methods for building stanzas
|
|
||||||
const { Strophe, sizzle, $iq } = converse.env;
|
|
||||||
const u = converse.env.utils;
|
|
||||||
|
|
||||||
// Add Strophe Namespaces
|
|
||||||
Strophe.addNamespace('REGISTER', 'jabber:iq:register');
|
|
||||||
|
|
||||||
// Add Strophe Statuses
|
|
||||||
const i = Object.keys(Strophe.Status).reduce((max, k) => Math.max(max, Strophe.Status[k]), 0);
|
|
||||||
Strophe.Status.REGIFAIL = i + 1;
|
|
||||||
Strophe.Status.REGISTERED = i + 2;
|
|
||||||
Strophe.Status.CONFLICT = i + 3;
|
|
||||||
Strophe.Status.NOTACCEPTABLE = i + 5;
|
|
||||||
|
|
||||||
|
|
||||||
converse.plugins.add('converse-register', {
|
|
||||||
|
|
||||||
enabled () {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
overrides: {
|
|
||||||
// Overrides mentioned here will be picked up by converse.js's
|
|
||||||
// plugin architecture they will replace existing methods on the
|
|
||||||
// relevant objects or classes.
|
|
||||||
//
|
|
||||||
// New functions which don't exist yet can also be added.
|
|
||||||
|
|
||||||
ControlBoxView: {
|
|
||||||
renderLoginPanel () {
|
|
||||||
/* Also render a registration panel, when rendering the
|
|
||||||
* login panel.
|
|
||||||
*/
|
|
||||||
this.__super__.renderLoginPanel.apply(this, arguments);
|
|
||||||
this.renderRegistrationPanel();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize () {
|
|
||||||
/* The initialize function gets called as soon as the plugin is
|
|
||||||
* loaded by converse.js's plugin machinery.
|
|
||||||
*/
|
|
||||||
_converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL';
|
|
||||||
_converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED';
|
|
||||||
_converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT';
|
|
||||||
_converse.CONNECTION_STATUS[Strophe.Status.NOTACCEPTABLE] = 'NOTACCEPTABLE';
|
|
||||||
|
|
||||||
api.settings.extend({
|
|
||||||
'allow_registration': true,
|
|
||||||
'domain_placeholder': __(" e.g. conversejs.org"), // Placeholder text shown in the domain input on the registration form
|
|
||||||
'providers_link': 'https://compliance.conversations.im/', // Link to XMPP providers shown on registration page
|
|
||||||
'registration_domain': ''
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Object.assign(_converse.ControlBoxView.prototype, {
|
|
||||||
|
|
||||||
showLoginOrRegisterForm () {
|
|
||||||
if (!this.registerpanel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.model.get('active-form') == "register") {
|
|
||||||
this.loginpanel.el.classList.add('hidden');
|
|
||||||
this.registerpanel.el.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
this.loginpanel.el.classList.remove('hidden');
|
|
||||||
this.registerpanel.el.classList.add('hidden');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderRegistrationPanel () {
|
|
||||||
if (api.settings.get('allow_registration')) {
|
|
||||||
this.registerpanel = new _converse.RegisterPanel({
|
|
||||||
'model': this.model
|
|
||||||
});
|
|
||||||
this.registerpanel.render();
|
|
||||||
this.registerpanel.el.classList.add('hidden');
|
|
||||||
const login_panel = this.el.querySelector('#converse-login-panel');
|
|
||||||
if (login_panel) {
|
|
||||||
login_panel.insertAdjacentElement('afterend', this.registerpanel.el);
|
|
||||||
}
|
|
||||||
this.showLoginOrRegisterForm();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function setActiveForm (value) {
|
|
||||||
api.waitUntil('controlBoxInitialized').then(() => {
|
|
||||||
const controlbox = _converse.chatboxes.get('controlbox')
|
|
||||||
controlbox.set({'active-form': value});
|
|
||||||
}).catch(e => log.fatal(e));
|
|
||||||
}
|
|
||||||
_converse.router.route('converse/login', () => setActiveForm('login'));
|
|
||||||
_converse.router.route('converse/register', () => setActiveForm('register'));
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class
|
|
||||||
* @namespace _converse.RegisterPanel
|
|
||||||
* @memberOf _converse
|
|
||||||
*/
|
|
||||||
_converse.RegisterPanel = View.extend({
|
|
||||||
tagName: 'div',
|
|
||||||
id: "converse-register-panel",
|
|
||||||
className: 'controlbox-pane fade-in',
|
|
||||||
events: {
|
|
||||||
'submit form#converse-register': 'onFormSubmission',
|
|
||||||
'click .button-cancel': 'renderProviderChoiceForm',
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize () {
|
|
||||||
this.reset();
|
|
||||||
api.listen.on('connectionInitialized', () => this.registerHooks());
|
|
||||||
},
|
|
||||||
|
|
||||||
render () {
|
|
||||||
this.model.set('registration_form_rendered', false);
|
|
||||||
this.el.innerHTML = tpl_register_panel({
|
|
||||||
'__': __,
|
|
||||||
'default_domain': api.settings.get('registration_domain'),
|
|
||||||
'label_register': __('Fetch registration form'),
|
|
||||||
'help_providers': __('Tip: A list of public XMPP providers is available'),
|
|
||||||
'help_providers_link': __('here'),
|
|
||||||
'href_providers': api.settings.get('providers_link'),
|
|
||||||
'domain_placeholder': api.settings.get('domain_placeholder')
|
|
||||||
});
|
|
||||||
if (api.settings.get('registration_domain')) {
|
|
||||||
this.fetchRegistrationForm(api.settings.get('registration_domain'));
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
registerHooks () {
|
|
||||||
/* Hook into Strophe's _connect_cb, so that we can send an IQ
|
|
||||||
* requesting the registration fields.
|
|
||||||
*/
|
|
||||||
const conn = _converse.connection;
|
|
||||||
const connect_cb = conn._connect_cb.bind(conn);
|
|
||||||
conn._connect_cb = (req, callback, raw) => {
|
|
||||||
if (!this._registering) {
|
|
||||||
connect_cb(req, callback, raw);
|
|
||||||
} else {
|
|
||||||
if (this.getRegistrationFields(req, callback)) {
|
|
||||||
this._registering = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an IQ stanza to the XMPP server asking for the registration fields.
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#getRegistrationFields
|
|
||||||
* @param { Strophe.Request } req - The current request
|
|
||||||
* @param { Function } callback - The callback function
|
|
||||||
*/
|
|
||||||
getRegistrationFields (req, _callback) {
|
|
||||||
const conn = _converse.connection;
|
|
||||||
conn.connected = true;
|
|
||||||
|
|
||||||
const body = conn._proto._reqToData(req);
|
|
||||||
if (!body) { return; }
|
|
||||||
if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) {
|
|
||||||
this.showValidationError(
|
|
||||||
__("Sorry, we're unable to connect to your chosen provider.")
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const register = body.getElementsByTagName("register");
|
|
||||||
const mechanisms = body.getElementsByTagName("mechanism");
|
|
||||||
if (register.length === 0 && mechanisms.length === 0) {
|
|
||||||
conn._proto._no_auth_received(_callback);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (register.length === 0) {
|
|
||||||
conn._changeConnectStatus(Strophe.Status.REGIFAIL);
|
|
||||||
this.showValidationError(
|
|
||||||
__("Sorry, the given provider does not support in "+
|
|
||||||
"band account registration. Please try with a "+
|
|
||||||
"different provider."))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Send an IQ stanza to get all required data fields
|
|
||||||
conn._addSysHandler(this.onRegistrationFields.bind(this), null, "iq", null, null);
|
|
||||||
const stanza = $iq({type: "get"}).c("query", {xmlns: Strophe.NS.REGISTER}).tree();
|
|
||||||
stanza.setAttribute("id", conn.getUniqueId("sendIQ"));
|
|
||||||
conn.send(stanza);
|
|
||||||
conn.connected = false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for {@link _converse.RegisterPanel#getRegistrationFields}
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#onRegistrationFields
|
|
||||||
* @param { XMLElement } stanza - The query stanza.
|
|
||||||
*/
|
|
||||||
onRegistrationFields (stanza) {
|
|
||||||
if (stanza.getAttribute("type") === "error") {
|
|
||||||
_converse.connection._changeConnectStatus(
|
|
||||||
Strophe.Status.REGIFAIL,
|
|
||||||
__('Something went wrong while establishing a connection with "%1$s". '+
|
|
||||||
'Are you sure it exists?', this.domain)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (stanza.getElementsByTagName("query").length !== 1) {
|
|
||||||
_converse.connection._changeConnectStatus(
|
|
||||||
Strophe.Status.REGIFAIL,
|
|
||||||
"unknown"
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.setFields(stanza);
|
|
||||||
if (!this.model.get('registration_form_rendered')) {
|
|
||||||
this.renderRegistrationForm(stanza);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
reset (settings) {
|
|
||||||
const defaults = {
|
|
||||||
fields: {},
|
|
||||||
urls: [],
|
|
||||||
title: "",
|
|
||||||
instructions: "",
|
|
||||||
registered: false,
|
|
||||||
_registering: false,
|
|
||||||
domain: null,
|
|
||||||
form_type: null
|
|
||||||
};
|
|
||||||
Object.assign(this, defaults);
|
|
||||||
if (settings) {
|
|
||||||
Object.assign(this, pick(settings, Object.keys(defaults)));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onFormSubmission (ev) {
|
|
||||||
/* Event handler when the #converse-register form is
|
|
||||||
* submitted.
|
|
||||||
*
|
|
||||||
* Depending on the available input fields, we delegate to
|
|
||||||
* other methods.
|
|
||||||
*/
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
if (ev.target.querySelector('input[name=domain]') === null) {
|
|
||||||
this.submitRegistrationForm(ev.target);
|
|
||||||
} else {
|
|
||||||
this.onProviderChosen(ev.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback method that gets called when the user has chosen an XMPP provider
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#onProviderChosen
|
|
||||||
* @param { HTMLElement } form - The form that was submitted
|
|
||||||
*/
|
|
||||||
onProviderChosen (form) {
|
|
||||||
const domain_input = form.querySelector('input[name=domain]'),
|
|
||||||
domain = domain_input?.value;
|
|
||||||
if (!domain) {
|
|
||||||
// TODO: add validation message
|
|
||||||
domain_input.classList.add('error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
form.querySelector('input[type=submit]').classList.add('hidden');
|
|
||||||
this.fetchRegistrationForm(domain.trim());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a registration form from the requested domain
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#fetchRegistrationForm
|
|
||||||
* @param { String } domain_name - XMPP server domain
|
|
||||||
*/
|
|
||||||
async fetchRegistrationForm (domain_name) {
|
|
||||||
if (!this.model.get('registration_form_rendered')) {
|
|
||||||
this.renderRegistrationRequest();
|
|
||||||
}
|
|
||||||
this.reset({
|
|
||||||
'domain': Strophe.getDomainFromJid(domain_name),
|
|
||||||
'_registering': true
|
|
||||||
});
|
|
||||||
await _converse.initConnection(this.domain);
|
|
||||||
// When testing, the test tears down before the async function
|
|
||||||
// above finishes. So we use optional chaining here
|
|
||||||
_converse.connection?.connect(this.domain, "", status => this.onConnectStatusChanged(status));
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderRegistrationRequest () {
|
|
||||||
/* Clear the form and inform the user that the registration
|
|
||||||
* form is being fetched.
|
|
||||||
*/
|
|
||||||
this.clearRegistrationForm().insertAdjacentHTML(
|
|
||||||
'beforeend',
|
|
||||||
tpl_registration_request({
|
|
||||||
'__': _converse.__,
|
|
||||||
'cancel': api.settings.get('registration_domain'),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
giveFeedback (message, klass) {
|
|
||||||
let feedback = this.el.querySelector('.reg-feedback');
|
|
||||||
if (feedback !== null) {
|
|
||||||
feedback.parentNode.removeChild(feedback);
|
|
||||||
}
|
|
||||||
const form = this.el.querySelector('form');
|
|
||||||
form.insertAdjacentHTML('afterbegin', '<span class="reg-feedback"></span>');
|
|
||||||
feedback = form.querySelector('.reg-feedback');
|
|
||||||
feedback.textContent = message;
|
|
||||||
if (klass) {
|
|
||||||
feedback.classList.add(klass);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clearRegistrationForm () {
|
|
||||||
const form = this.el.querySelector('form');
|
|
||||||
form.innerHTML = '';
|
|
||||||
this.model.set('registration_form_rendered', false);
|
|
||||||
return form;
|
|
||||||
},
|
|
||||||
|
|
||||||
showSpinner () {
|
|
||||||
const form = this.el.querySelector('form');
|
|
||||||
render(tpl_spinner(), form);
|
|
||||||
this.model.set('registration_form_rendered', false);
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback function called by Strophe whenever the connection status changes.
|
|
||||||
* Passed to Strophe specifically during a registration attempt.
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#onConnectStatusChanged
|
|
||||||
* @param { integer } status_code - The Strophe.Status status code
|
|
||||||
*/
|
|
||||||
onConnectStatusChanged(status_code) {
|
|
||||||
log.debug('converse-register: onConnectStatusChanged');
|
|
||||||
if ([Strophe.Status.DISCONNECTED,
|
|
||||||
Strophe.Status.CONNFAIL,
|
|
||||||
Strophe.Status.REGIFAIL,
|
|
||||||
Strophe.Status.NOTACCEPTABLE,
|
|
||||||
Strophe.Status.CONFLICT
|
|
||||||
].includes(status_code)) {
|
|
||||||
|
|
||||||
log.error(
|
|
||||||
`Problem during registration: Strophe.Status is ${_converse.CONNECTION_STATUS[status_code]}`
|
|
||||||
);
|
|
||||||
this.abortRegistration();
|
|
||||||
} else if (status_code === Strophe.Status.REGISTERED) {
|
|
||||||
log.debug("Registered successfully.");
|
|
||||||
_converse.connection.reset();
|
|
||||||
this.showSpinner();
|
|
||||||
|
|
||||||
if (["converse/login", "converse/register"].includes(_converse.router.history.getFragment())) {
|
|
||||||
_converse.router.navigate('', {'replace': true});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.fields.password && this.fields.username) {
|
|
||||||
// automatically log the user in
|
|
||||||
_converse.connection.connect(
|
|
||||||
this.fields.username.toLowerCase()+'@'+this.domain.toLowerCase(),
|
|
||||||
this.fields.password,
|
|
||||||
_converse.onConnectStatusChanged
|
|
||||||
);
|
|
||||||
this.giveFeedback(__('Now logging you in'), 'info');
|
|
||||||
} else {
|
|
||||||
_converse.chatboxviews.get('controlbox').renderLoginPanel();
|
|
||||||
_converse.giveFeedback(__('Registered successfully'));
|
|
||||||
}
|
|
||||||
this.reset();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getLegacyFormFields () {
|
|
||||||
const input_fields = Object.keys(this.fields).map(key => {
|
|
||||||
if (key === "username") {
|
|
||||||
return tpl_form_username({
|
|
||||||
'domain': ` @${this.domain}`,
|
|
||||||
'name': key,
|
|
||||||
'type': "text",
|
|
||||||
'label': key,
|
|
||||||
'value': '',
|
|
||||||
'required': true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return tpl_form_input({
|
|
||||||
'label': key,
|
|
||||||
'name': key,
|
|
||||||
'placeholder': key,
|
|
||||||
'required': true,
|
|
||||||
'type': (key === 'password' || key === 'email') ? key : "text",
|
|
||||||
'value': ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const urls = this.urls.map(u => tpl_form_url({'label': '', 'value': u}));
|
|
||||||
return [...input_fields, ...urls];
|
|
||||||
},
|
|
||||||
|
|
||||||
getFormFields (stanza) {
|
|
||||||
if (this.form_type === 'xform') {
|
|
||||||
return Array.from(stanza.querySelectorAll('field')).map(field =>
|
|
||||||
utils.xForm2TemplateResult(field, stanza, {'domain': this.domain})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return this.getLegacyFormFields();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the registration form based on the XForm fields
|
|
||||||
* received from the XMPP server.
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#renderRegistrationForm
|
|
||||||
* @param { XMLElement } stanza - The IQ stanza received from the XMPP server.
|
|
||||||
*/
|
|
||||||
renderRegistrationForm (stanza) {
|
|
||||||
const form = this.el.querySelector('form');
|
|
||||||
const tpl = tpl_registration_form({
|
|
||||||
'domain': this.domain,
|
|
||||||
'title': this.title,
|
|
||||||
'instructions': this.instructions,
|
|
||||||
'fields': this.fields,
|
|
||||||
'form_fields': this.getFormFields(stanza)
|
|
||||||
});
|
|
||||||
render(tpl, form);
|
|
||||||
form.classList.remove('hidden');
|
|
||||||
this.model.set('registration_form_rendered', true);
|
|
||||||
},
|
|
||||||
|
|
||||||
showValidationError (message) {
|
|
||||||
const form = this.el.querySelector('form');
|
|
||||||
let flash = form.querySelector('.form-errors');
|
|
||||||
if (flash === null) {
|
|
||||||
flash = '<div class="form-errors hidden"></div>';
|
|
||||||
const instructions = form.querySelector('p.instructions');
|
|
||||||
if (instructions === null) {
|
|
||||||
form.insertAdjacentHTML('afterbegin', flash);
|
|
||||||
} else {
|
|
||||||
instructions.insertAdjacentHTML('afterend', flash);
|
|
||||||
}
|
|
||||||
flash = form.querySelector('.form-errors');
|
|
||||||
} else {
|
|
||||||
flash.innerHTML = '';
|
|
||||||
}
|
|
||||||
flash.insertAdjacentHTML(
|
|
||||||
'beforeend',
|
|
||||||
'<p class="form-help error">'+message+'</p>'
|
|
||||||
);
|
|
||||||
flash.classList.remove('hidden');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Report back to the user any error messages received from the
|
|
||||||
* XMPP server after attempted registration.
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#reportErrors
|
|
||||||
* @param { XMLElement } stanza - The IQ stanza received from the XMPP server
|
|
||||||
*/
|
|
||||||
reportErrors (stanza) {
|
|
||||||
const errors = stanza.querySelectorAll('error');
|
|
||||||
errors.forEach(e => this.showValidationError(e.textContent));
|
|
||||||
if (!errors.length) {
|
|
||||||
const message = __('The provider rejected your registration attempt. '+
|
|
||||||
'Please check the values you entered for correctness.');
|
|
||||||
this.showValidationError(message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderProviderChoiceForm (ev) {
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
_converse.connection._proto._abortAllRequests();
|
|
||||||
_converse.connection.reset();
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
abortRegistration () {
|
|
||||||
_converse.connection._proto._abortAllRequests();
|
|
||||||
_converse.connection.reset();
|
|
||||||
if (this.model.get('registration_form_rendered')) {
|
|
||||||
if (api.settings.get('registration_domain') && this.model.get('registration_form_rendered')) {
|
|
||||||
this.fetchRegistrationForm(
|
|
||||||
api.settings.get('registration_domain')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler, when the user submits the registration form.
|
|
||||||
* Provides form error feedback or starts the registration process.
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#submitRegistrationForm
|
|
||||||
* @param { HTMLElement } form - The HTML form that was submitted
|
|
||||||
*/
|
|
||||||
submitRegistrationForm (form) {
|
|
||||||
const has_empty_inputs = Array.from(this.el.querySelectorAll('input.required'))
|
|
||||||
.reduce((result, input) => {
|
|
||||||
if (input.value === '') {
|
|
||||||
input.classList.add('error');
|
|
||||||
return result + 1;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, 0);
|
|
||||||
if (has_empty_inputs) { return; }
|
|
||||||
|
|
||||||
const inputs = sizzle(':input:not([type=button]):not([type=submit])', form);
|
|
||||||
const iq = $iq({'type': 'set', 'id': u.getUniqueId()})
|
|
||||||
.c("query", {xmlns:Strophe.NS.REGISTER});
|
|
||||||
|
|
||||||
if (this.form_type === 'xform') {
|
|
||||||
iq.c("x", {xmlns: Strophe.NS.XFORM, type: 'submit'});
|
|
||||||
|
|
||||||
const xml_nodes = inputs.map(i => utils.webForm2xForm(i)).filter(n => n);
|
|
||||||
xml_nodes.forEach(n => iq.cnode(n).up());
|
|
||||||
} else {
|
|
||||||
inputs.forEach(input => iq.c(input.getAttribute('name'), {}, input.value));
|
|
||||||
}
|
|
||||||
_converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null);
|
|
||||||
_converse.connection.send(iq);
|
|
||||||
this.setFields(iq.tree());
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Stores the values that will be sent to the XMPP server during attempted registration.
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#setFields
|
|
||||||
* @param { XMLElement } stanza - the IQ stanza that will be sent to the XMPP server.
|
|
||||||
*/
|
|
||||||
setFields (stanza) {
|
|
||||||
const query = stanza.querySelector('query');
|
|
||||||
const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query);
|
|
||||||
if (xform.length > 0) {
|
|
||||||
this._setFieldsFromXForm(xform.pop());
|
|
||||||
} else {
|
|
||||||
this._setFieldsFromLegacy(query);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_setFieldsFromLegacy (query) {
|
|
||||||
[].forEach.call(query.children, field => {
|
|
||||||
if (field.tagName.toLowerCase() === 'instructions') {
|
|
||||||
this.instructions = Strophe.getText(field);
|
|
||||||
return;
|
|
||||||
} else if (field.tagName.toLowerCase() === 'x') {
|
|
||||||
if (field.getAttribute('xmlns') === 'jabber:x:oob') {
|
|
||||||
this.urls.concat(sizzle('url', field).map(u => u.textContent));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.fields[field.tagName.toLowerCase()] = Strophe.getText(field);
|
|
||||||
});
|
|
||||||
this.form_type = 'legacy';
|
|
||||||
},
|
|
||||||
|
|
||||||
_setFieldsFromXForm (xform) {
|
|
||||||
this.title = xform.querySelector('title')?.textContent;
|
|
||||||
this.instructions = xform.querySelector('instructions')?.textContent;
|
|
||||||
xform.querySelectorAll('field').forEach(field => {
|
|
||||||
const _var = field.getAttribute('var');
|
|
||||||
if (_var) {
|
|
||||||
this.fields[_var.toLowerCase()] = field.querySelector('value')?.textContent ?? '';
|
|
||||||
} else {
|
|
||||||
// TODO: other option seems to be type="fixed"
|
|
||||||
log.warn("Found field we couldn't parse");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.form_type = 'xform';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback method that gets called when a return IQ stanza
|
|
||||||
* is received from the XMPP server, after attempting to
|
|
||||||
* register a new user.
|
|
||||||
* @private
|
|
||||||
* @method _converse.RegisterPanel#reportErrors
|
|
||||||
* @param { XMLElement } stanza - The IQ stanza.
|
|
||||||
*/
|
|
||||||
_onRegisterIQ (stanza) {
|
|
||||||
if (stanza.getAttribute("type") === "error") {
|
|
||||||
log.error("Registration failed.");
|
|
||||||
this.reportErrors(stanza);
|
|
||||||
|
|
||||||
let error = stanza.getElementsByTagName("error");
|
|
||||||
if (error.length !== 1) {
|
|
||||||
_converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
error = error[0].firstElementChild.tagName.toLowerCase();
|
|
||||||
if (error === 'conflict') {
|
|
||||||
_converse.connection._changeConnectStatus(Strophe.Status.CONFLICT, error);
|
|
||||||
} else if (error === 'not-acceptable') {
|
|
||||||
_converse.connection._changeConnectStatus(Strophe.Status.NOTACCEPTABLE, error);
|
|
||||||
} else {
|
|
||||||
_converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/************************ BEGIN Event Handlers ************************/
|
|
||||||
api.listen.on('controlBoxInitialized', view => {
|
|
||||||
view.model.on('change:active-form', view.showLoginOrRegisterForm, view);
|
|
||||||
});
|
|
||||||
/************************ END Event Handlers ************************/
|
|
||||||
}
|
|
||||||
});
|
|
34
src/plugins/register/controlbox-mixin.js
Normal file
34
src/plugins/register/controlbox-mixin.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { _converse, api } from '@converse/headless/core';
|
||||||
|
|
||||||
|
const ControlBoxRegistrationMixin = {
|
||||||
|
showLoginOrRegisterForm () {
|
||||||
|
if (!this.registerpanel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.model.get('active-form') == 'register') {
|
||||||
|
this.loginpanel.el.classList.add('hidden');
|
||||||
|
this.registerpanel.el.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
this.loginpanel.el.classList.remove('hidden');
|
||||||
|
this.registerpanel.el.classList.add('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderRegistrationPanel () {
|
||||||
|
if (api.settings.get('allow_registration')) {
|
||||||
|
this.registerpanel = new _converse.RegisterPanel({
|
||||||
|
'model': this.model
|
||||||
|
});
|
||||||
|
this.registerpanel.render();
|
||||||
|
this.registerpanel.el.classList.add('hidden');
|
||||||
|
const login_panel = this.el.querySelector('#converse-login-panel');
|
||||||
|
if (login_panel) {
|
||||||
|
login_panel.insertAdjacentElement('afterend', this.registerpanel.el);
|
||||||
|
}
|
||||||
|
this.showLoginOrRegisterForm();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ControlBoxRegistrationMixin;
|
82
src/plugins/register/index.js
Normal file
82
src/plugins/register/index.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* @module converse-register
|
||||||
|
* @description
|
||||||
|
* This is a Converse.js plugin which add support for in-band registration
|
||||||
|
* as specified in XEP-0077.
|
||||||
|
* @copyright 2020, the Converse.js contributors
|
||||||
|
* @license Mozilla Public License (MPLv2)
|
||||||
|
*/
|
||||||
|
import '../controlbox/index.js';
|
||||||
|
import ControlBoxRegistrationMixin from './controlbox-mixin.js';
|
||||||
|
import RegisterPanel from './panel.js';
|
||||||
|
import log from '@converse/headless/log';
|
||||||
|
import { __ } from 'i18n';
|
||||||
|
import { _converse, api, converse } from '@converse/headless/core';
|
||||||
|
|
||||||
|
// Strophe methods for building stanzas
|
||||||
|
const { Strophe } = converse.env;
|
||||||
|
|
||||||
|
// Add Strophe Namespaces
|
||||||
|
Strophe.addNamespace('REGISTER', 'jabber:iq:register');
|
||||||
|
|
||||||
|
// Add Strophe Statuses
|
||||||
|
const i = Object.keys(Strophe.Status).reduce((max, k) => Math.max(max, Strophe.Status[k]), 0);
|
||||||
|
Strophe.Status.REGIFAIL = i + 1;
|
||||||
|
Strophe.Status.REGISTERED = i + 2;
|
||||||
|
Strophe.Status.CONFLICT = i + 3;
|
||||||
|
Strophe.Status.NOTACCEPTABLE = i + 5;
|
||||||
|
|
||||||
|
converse.plugins.add('converse-register', {
|
||||||
|
enabled () {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
overrides: {
|
||||||
|
// Overrides mentioned here will be picked up by converse.js's
|
||||||
|
// plugin architecture they will replace existing methods on the
|
||||||
|
// relevant objects or classes.
|
||||||
|
|
||||||
|
ControlBoxView: {
|
||||||
|
renderLoginPanel () {
|
||||||
|
// Also render a registration panel, when rendering the login panel.
|
||||||
|
this.__super__.renderLoginPanel.apply(this, arguments);
|
||||||
|
this.renderRegistrationPanel();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize () {
|
||||||
|
_converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL';
|
||||||
|
_converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED';
|
||||||
|
_converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT';
|
||||||
|
_converse.CONNECTION_STATUS[Strophe.Status.NOTACCEPTABLE] = 'NOTACCEPTABLE';
|
||||||
|
|
||||||
|
api.settings.extend({
|
||||||
|
'allow_registration': true,
|
||||||
|
'domain_placeholder': __(' e.g. conversejs.org'), // Placeholder text shown in the domain input on the registration form
|
||||||
|
'providers_link': 'https://compliance.conversations.im/', // Link to XMPP providers shown on registration page
|
||||||
|
'registration_domain': ''
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(_converse.ControlBoxView.prototype, ControlBoxRegistrationMixin);
|
||||||
|
|
||||||
|
_converse.RegisterPanel = RegisterPanel;
|
||||||
|
|
||||||
|
function setActiveForm (value) {
|
||||||
|
api.waitUntil('controlBoxInitialized')
|
||||||
|
.then(() => {
|
||||||
|
const controlbox = _converse.chatboxes.get('controlbox');
|
||||||
|
controlbox.set({ 'active-form': value });
|
||||||
|
})
|
||||||
|
.catch(e => log.fatal(e));
|
||||||
|
}
|
||||||
|
_converse.router.route('converse/login', () => setActiveForm('login'));
|
||||||
|
_converse.router.route('converse/register', () => setActiveForm('register'));
|
||||||
|
|
||||||
|
|
||||||
|
api.listen.on('controlBoxInitialized', view => {
|
||||||
|
view.model.on('change:active-form', view.showLoginOrRegisterForm, view);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
536
src/plugins/register/panel.js
Normal file
536
src/plugins/register/panel.js
Normal file
@ -0,0 +1,536 @@
|
|||||||
|
import log from "@converse/headless/log";
|
||||||
|
import tpl_form_input from "templates/form_input.js";
|
||||||
|
import tpl_form_url from "templates/form_url.js";
|
||||||
|
import tpl_form_username from "templates/form_username.js";
|
||||||
|
import tpl_register_panel from "./templates/register_panel.html";
|
||||||
|
import tpl_registration_form from "./templates/registration_form.js";
|
||||||
|
import tpl_registration_request from "./templates/registration_request.html";
|
||||||
|
import tpl_spinner from "templates/spinner.js";
|
||||||
|
import utils from "@converse/headless/utils/form";
|
||||||
|
import { View } from "@converse/skeletor/src/view";
|
||||||
|
import { __ } from 'i18n';
|
||||||
|
import { _converse, api, converse } from "@converse/headless/core";
|
||||||
|
import { pick } from "lodash-es";
|
||||||
|
import { render } from 'lit-html';
|
||||||
|
|
||||||
|
// Strophe methods for building stanzas
|
||||||
|
const { Strophe, sizzle, $iq } = converse.env;
|
||||||
|
const u = converse.env.utils;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @namespace _converse.RegisterPanel
|
||||||
|
* @memberOf _converse
|
||||||
|
*/
|
||||||
|
const RegisterPanel = View.extend({
|
||||||
|
tagName: 'div',
|
||||||
|
id: "converse-register-panel",
|
||||||
|
className: 'controlbox-pane fade-in',
|
||||||
|
events: {
|
||||||
|
'submit form#converse-register': 'onFormSubmission',
|
||||||
|
'click .button-cancel': 'renderProviderChoiceForm',
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize () {
|
||||||
|
this.reset();
|
||||||
|
api.listen.on('connectionInitialized', () => this.registerHooks());
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
this.model.set('registration_form_rendered', false);
|
||||||
|
this.el.innerHTML = tpl_register_panel({
|
||||||
|
'__': __,
|
||||||
|
'default_domain': api.settings.get('registration_domain'),
|
||||||
|
'label_register': __('Fetch registration form'),
|
||||||
|
'help_providers': __('Tip: A list of public XMPP providers is available'),
|
||||||
|
'help_providers_link': __('here'),
|
||||||
|
'href_providers': api.settings.get('providers_link'),
|
||||||
|
'domain_placeholder': api.settings.get('domain_placeholder')
|
||||||
|
});
|
||||||
|
if (api.settings.get('registration_domain')) {
|
||||||
|
this.fetchRegistrationForm(api.settings.get('registration_domain'));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
registerHooks () {
|
||||||
|
/* Hook into Strophe's _connect_cb, so that we can send an IQ
|
||||||
|
* requesting the registration fields.
|
||||||
|
*/
|
||||||
|
const conn = _converse.connection;
|
||||||
|
const connect_cb = conn._connect_cb.bind(conn);
|
||||||
|
conn._connect_cb = (req, callback, raw) => {
|
||||||
|
if (!this._registering) {
|
||||||
|
connect_cb(req, callback, raw);
|
||||||
|
} else {
|
||||||
|
if (this.getRegistrationFields(req, callback)) {
|
||||||
|
this._registering = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an IQ stanza to the XMPP server asking for the registration fields.
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#getRegistrationFields
|
||||||
|
* @param { Strophe.Request } req - The current request
|
||||||
|
* @param { Function } callback - The callback function
|
||||||
|
*/
|
||||||
|
getRegistrationFields (req, _callback) {
|
||||||
|
const conn = _converse.connection;
|
||||||
|
conn.connected = true;
|
||||||
|
|
||||||
|
const body = conn._proto._reqToData(req);
|
||||||
|
if (!body) { return; }
|
||||||
|
if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) {
|
||||||
|
this.showValidationError(
|
||||||
|
__("Sorry, we're unable to connect to your chosen provider.")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const register = body.getElementsByTagName("register");
|
||||||
|
const mechanisms = body.getElementsByTagName("mechanism");
|
||||||
|
if (register.length === 0 && mechanisms.length === 0) {
|
||||||
|
conn._proto._no_auth_received(_callback);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (register.length === 0) {
|
||||||
|
conn._changeConnectStatus(Strophe.Status.REGIFAIL);
|
||||||
|
this.showValidationError(
|
||||||
|
__("Sorry, the given provider does not support in "+
|
||||||
|
"band account registration. Please try with a "+
|
||||||
|
"different provider."))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Send an IQ stanza to get all required data fields
|
||||||
|
conn._addSysHandler(this.onRegistrationFields.bind(this), null, "iq", null, null);
|
||||||
|
const stanza = $iq({type: "get"}).c("query", {xmlns: Strophe.NS.REGISTER}).tree();
|
||||||
|
stanza.setAttribute("id", conn.getUniqueId("sendIQ"));
|
||||||
|
conn.send(stanza);
|
||||||
|
conn.connected = false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for {@link _converse.RegisterPanel#getRegistrationFields}
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#onRegistrationFields
|
||||||
|
* @param { XMLElement } stanza - The query stanza.
|
||||||
|
*/
|
||||||
|
onRegistrationFields (stanza) {
|
||||||
|
if (stanza.getAttribute("type") === "error") {
|
||||||
|
_converse.connection._changeConnectStatus(
|
||||||
|
Strophe.Status.REGIFAIL,
|
||||||
|
__('Something went wrong while establishing a connection with "%1$s". '+
|
||||||
|
'Are you sure it exists?', this.domain)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (stanza.getElementsByTagName("query").length !== 1) {
|
||||||
|
_converse.connection._changeConnectStatus(
|
||||||
|
Strophe.Status.REGIFAIL,
|
||||||
|
"unknown"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.setFields(stanza);
|
||||||
|
if (!this.model.get('registration_form_rendered')) {
|
||||||
|
this.renderRegistrationForm(stanza);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset (settings) {
|
||||||
|
const defaults = {
|
||||||
|
fields: {},
|
||||||
|
urls: [],
|
||||||
|
title: "",
|
||||||
|
instructions: "",
|
||||||
|
registered: false,
|
||||||
|
_registering: false,
|
||||||
|
domain: null,
|
||||||
|
form_type: null
|
||||||
|
};
|
||||||
|
Object.assign(this, defaults);
|
||||||
|
if (settings) {
|
||||||
|
Object.assign(this, pick(settings, Object.keys(defaults)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler when the #converse-register form is submitted.
|
||||||
|
* Depending on the available input fields, we delegate to other methods.
|
||||||
|
* @private
|
||||||
|
* @param { Event } ev
|
||||||
|
*/
|
||||||
|
onFormSubmission (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
if (ev.target.querySelector('input[name=domain]') === null) {
|
||||||
|
this.submitRegistrationForm(ev.target);
|
||||||
|
} else {
|
||||||
|
this.onProviderChosen(ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method that gets called when the user has chosen an XMPP provider
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#onProviderChosen
|
||||||
|
* @param { HTMLElement } form - The form that was submitted
|
||||||
|
*/
|
||||||
|
onProviderChosen (form) {
|
||||||
|
const domain_input = form.querySelector('input[name=domain]'),
|
||||||
|
domain = domain_input?.value;
|
||||||
|
if (!domain) {
|
||||||
|
// TODO: add validation message
|
||||||
|
domain_input.classList.add('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form.querySelector('input[type=submit]').classList.add('hidden');
|
||||||
|
this.fetchRegistrationForm(domain.trim());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a registration form from the requested domain
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#fetchRegistrationForm
|
||||||
|
* @param { String } domain_name - XMPP server domain
|
||||||
|
*/
|
||||||
|
async fetchRegistrationForm (domain_name) {
|
||||||
|
if (!this.model.get('registration_form_rendered')) {
|
||||||
|
this.renderRegistrationRequest();
|
||||||
|
}
|
||||||
|
this.reset({
|
||||||
|
'domain': Strophe.getDomainFromJid(domain_name),
|
||||||
|
'_registering': true
|
||||||
|
});
|
||||||
|
await _converse.initConnection(this.domain);
|
||||||
|
// When testing, the test tears down before the async function
|
||||||
|
// above finishes. So we use optional chaining here
|
||||||
|
_converse.connection?.connect(this.domain, "", status => this.onConnectStatusChanged(status));
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the form and inform the user that the registration
|
||||||
|
* form is being fetched.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
renderRegistrationRequest () {
|
||||||
|
this.clearRegistrationForm().insertAdjacentHTML(
|
||||||
|
'beforeend',
|
||||||
|
tpl_registration_request({
|
||||||
|
'__': _converse.__,
|
||||||
|
'cancel': api.settings.get('registration_domain'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
giveFeedback (message, klass) {
|
||||||
|
let feedback = this.el.querySelector('.reg-feedback');
|
||||||
|
if (feedback !== null) {
|
||||||
|
feedback.parentNode.removeChild(feedback);
|
||||||
|
}
|
||||||
|
const form = this.el.querySelector('form');
|
||||||
|
form.insertAdjacentHTML('afterbegin', '<span class="reg-feedback"></span>');
|
||||||
|
feedback = form.querySelector('.reg-feedback');
|
||||||
|
feedback.textContent = message;
|
||||||
|
if (klass) {
|
||||||
|
feedback.classList.add(klass);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearRegistrationForm () {
|
||||||
|
const form = this.el.querySelector('form');
|
||||||
|
form.innerHTML = '';
|
||||||
|
this.model.set('registration_form_rendered', false);
|
||||||
|
return form;
|
||||||
|
},
|
||||||
|
|
||||||
|
showSpinner () {
|
||||||
|
const form = this.el.querySelector('form');
|
||||||
|
render(tpl_spinner(), form);
|
||||||
|
this.model.set('registration_form_rendered', false);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function called by Strophe whenever the connection status changes.
|
||||||
|
* Passed to Strophe specifically during a registration attempt.
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#onConnectStatusChanged
|
||||||
|
* @param { integer } status_code - The Strophe.Status status code
|
||||||
|
*/
|
||||||
|
onConnectStatusChanged(status_code) {
|
||||||
|
log.debug('converse-register: onConnectStatusChanged');
|
||||||
|
if ([Strophe.Status.DISCONNECTED,
|
||||||
|
Strophe.Status.CONNFAIL,
|
||||||
|
Strophe.Status.REGIFAIL,
|
||||||
|
Strophe.Status.NOTACCEPTABLE,
|
||||||
|
Strophe.Status.CONFLICT
|
||||||
|
].includes(status_code)) {
|
||||||
|
|
||||||
|
log.error(
|
||||||
|
`Problem during registration: Strophe.Status is ${_converse.CONNECTION_STATUS[status_code]}`
|
||||||
|
);
|
||||||
|
this.abortRegistration();
|
||||||
|
} else if (status_code === Strophe.Status.REGISTERED) {
|
||||||
|
log.debug("Registered successfully.");
|
||||||
|
_converse.connection.reset();
|
||||||
|
this.showSpinner();
|
||||||
|
|
||||||
|
if (["converse/login", "converse/register"].includes(_converse.router.history.getFragment())) {
|
||||||
|
_converse.router.navigate('', {'replace': true});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fields.password && this.fields.username) {
|
||||||
|
// automatically log the user in
|
||||||
|
_converse.connection.connect(
|
||||||
|
this.fields.username.toLowerCase()+'@'+this.domain.toLowerCase(),
|
||||||
|
this.fields.password,
|
||||||
|
_converse.onConnectStatusChanged
|
||||||
|
);
|
||||||
|
this.giveFeedback(__('Now logging you in'), 'info');
|
||||||
|
} else {
|
||||||
|
_converse.chatboxviews.get('controlbox').renderLoginPanel();
|
||||||
|
_converse.giveFeedback(__('Registered successfully'));
|
||||||
|
}
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getLegacyFormFields () {
|
||||||
|
const input_fields = Object.keys(this.fields).map(key => {
|
||||||
|
if (key === "username") {
|
||||||
|
return tpl_form_username({
|
||||||
|
'domain': ` @${this.domain}`,
|
||||||
|
'name': key,
|
||||||
|
'type': "text",
|
||||||
|
'label': key,
|
||||||
|
'value': '',
|
||||||
|
'required': true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return tpl_form_input({
|
||||||
|
'label': key,
|
||||||
|
'name': key,
|
||||||
|
'placeholder': key,
|
||||||
|
'required': true,
|
||||||
|
'type': (key === 'password' || key === 'email') ? key : "text",
|
||||||
|
'value': ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const urls = this.urls.map(u => tpl_form_url({'label': '', 'value': u}));
|
||||||
|
return [...input_fields, ...urls];
|
||||||
|
},
|
||||||
|
|
||||||
|
getFormFields (stanza) {
|
||||||
|
if (this.form_type === 'xform') {
|
||||||
|
return Array.from(stanza.querySelectorAll('field')).map(field =>
|
||||||
|
utils.xForm2TemplateResult(field, stanza, {'domain': this.domain})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.getLegacyFormFields();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the registration form based on the XForm fields
|
||||||
|
* received from the XMPP server.
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#renderRegistrationForm
|
||||||
|
* @param { XMLElement } stanza - The IQ stanza received from the XMPP server.
|
||||||
|
*/
|
||||||
|
renderRegistrationForm (stanza) {
|
||||||
|
const form = this.el.querySelector('form');
|
||||||
|
const tpl = tpl_registration_form({
|
||||||
|
'domain': this.domain,
|
||||||
|
'title': this.title,
|
||||||
|
'instructions': this.instructions,
|
||||||
|
'fields': this.fields,
|
||||||
|
'form_fields': this.getFormFields(stanza)
|
||||||
|
});
|
||||||
|
render(tpl, form);
|
||||||
|
form.classList.remove('hidden');
|
||||||
|
this.model.set('registration_form_rendered', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
showValidationError (message) {
|
||||||
|
const form = this.el.querySelector('form');
|
||||||
|
let flash = form.querySelector('.form-errors');
|
||||||
|
if (flash === null) {
|
||||||
|
flash = '<div class="form-errors hidden"></div>';
|
||||||
|
const instructions = form.querySelector('p.instructions');
|
||||||
|
if (instructions === null) {
|
||||||
|
form.insertAdjacentHTML('afterbegin', flash);
|
||||||
|
} else {
|
||||||
|
instructions.insertAdjacentHTML('afterend', flash);
|
||||||
|
}
|
||||||
|
flash = form.querySelector('.form-errors');
|
||||||
|
} else {
|
||||||
|
flash.innerHTML = '';
|
||||||
|
}
|
||||||
|
flash.insertAdjacentHTML(
|
||||||
|
'beforeend',
|
||||||
|
'<p class="form-help error">'+message+'</p>'
|
||||||
|
);
|
||||||
|
flash.classList.remove('hidden');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report back to the user any error messages received from the
|
||||||
|
* XMPP server after attempted registration.
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#reportErrors
|
||||||
|
* @param { XMLElement } stanza - The IQ stanza received from the XMPP server
|
||||||
|
*/
|
||||||
|
reportErrors (stanza) {
|
||||||
|
const errors = stanza.querySelectorAll('error');
|
||||||
|
errors.forEach(e => this.showValidationError(e.textContent));
|
||||||
|
if (!errors.length) {
|
||||||
|
const message = __('The provider rejected your registration attempt. '+
|
||||||
|
'Please check the values you entered for correctness.');
|
||||||
|
this.showValidationError(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderProviderChoiceForm (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
_converse.connection._proto._abortAllRequests();
|
||||||
|
_converse.connection.reset();
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
abortRegistration () {
|
||||||
|
_converse.connection._proto._abortAllRequests();
|
||||||
|
_converse.connection.reset();
|
||||||
|
if (this.model.get('registration_form_rendered')) {
|
||||||
|
if (api.settings.get('registration_domain') && this.model.get('registration_form_rendered')) {
|
||||||
|
this.fetchRegistrationForm(
|
||||||
|
api.settings.get('registration_domain')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler, when the user submits the registration form.
|
||||||
|
* Provides form error feedback or starts the registration process.
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#submitRegistrationForm
|
||||||
|
* @param { HTMLElement } form - The HTML form that was submitted
|
||||||
|
*/
|
||||||
|
submitRegistrationForm (form) {
|
||||||
|
const has_empty_inputs = Array.from(this.el.querySelectorAll('input.required'))
|
||||||
|
.reduce((result, input) => {
|
||||||
|
if (input.value === '') {
|
||||||
|
input.classList.add('error');
|
||||||
|
return result + 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, 0);
|
||||||
|
if (has_empty_inputs) { return; }
|
||||||
|
|
||||||
|
const inputs = sizzle(':input:not([type=button]):not([type=submit])', form);
|
||||||
|
const iq = $iq({'type': 'set', 'id': u.getUniqueId()})
|
||||||
|
.c("query", {xmlns:Strophe.NS.REGISTER});
|
||||||
|
|
||||||
|
if (this.form_type === 'xform') {
|
||||||
|
iq.c("x", {xmlns: Strophe.NS.XFORM, type: 'submit'});
|
||||||
|
|
||||||
|
const xml_nodes = inputs.map(i => utils.webForm2xForm(i)).filter(n => n);
|
||||||
|
xml_nodes.forEach(n => iq.cnode(n).up());
|
||||||
|
} else {
|
||||||
|
inputs.forEach(input => iq.c(input.getAttribute('name'), {}, input.value));
|
||||||
|
}
|
||||||
|
_converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null);
|
||||||
|
_converse.connection.send(iq);
|
||||||
|
this.setFields(iq.tree());
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Stores the values that will be sent to the XMPP server during attempted registration.
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#setFields
|
||||||
|
* @param { XMLElement } stanza - the IQ stanza that will be sent to the XMPP server.
|
||||||
|
*/
|
||||||
|
setFields (stanza) {
|
||||||
|
const query = stanza.querySelector('query');
|
||||||
|
const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query);
|
||||||
|
if (xform.length > 0) {
|
||||||
|
this._setFieldsFromXForm(xform.pop());
|
||||||
|
} else {
|
||||||
|
this._setFieldsFromLegacy(query);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_setFieldsFromLegacy (query) {
|
||||||
|
[].forEach.call(query.children, field => {
|
||||||
|
if (field.tagName.toLowerCase() === 'instructions') {
|
||||||
|
this.instructions = Strophe.getText(field);
|
||||||
|
return;
|
||||||
|
} else if (field.tagName.toLowerCase() === 'x') {
|
||||||
|
if (field.getAttribute('xmlns') === 'jabber:x:oob') {
|
||||||
|
this.urls.concat(sizzle('url', field).map(u => u.textContent));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.fields[field.tagName.toLowerCase()] = Strophe.getText(field);
|
||||||
|
});
|
||||||
|
this.form_type = 'legacy';
|
||||||
|
},
|
||||||
|
|
||||||
|
_setFieldsFromXForm (xform) {
|
||||||
|
this.title = xform.querySelector('title')?.textContent;
|
||||||
|
this.instructions = xform.querySelector('instructions')?.textContent;
|
||||||
|
xform.querySelectorAll('field').forEach(field => {
|
||||||
|
const _var = field.getAttribute('var');
|
||||||
|
if (_var) {
|
||||||
|
this.fields[_var.toLowerCase()] = field.querySelector('value')?.textContent ?? '';
|
||||||
|
} else {
|
||||||
|
// TODO: other option seems to be type="fixed"
|
||||||
|
log.warn("Found field we couldn't parse");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.form_type = 'xform';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method that gets called when a return IQ stanza
|
||||||
|
* is received from the XMPP server, after attempting to
|
||||||
|
* register a new user.
|
||||||
|
* @private
|
||||||
|
* @method _converse.RegisterPanel#reportErrors
|
||||||
|
* @param { XMLElement } stanza - The IQ stanza.
|
||||||
|
*/
|
||||||
|
_onRegisterIQ (stanza) {
|
||||||
|
if (stanza.getAttribute("type") === "error") {
|
||||||
|
log.error("Registration failed.");
|
||||||
|
this.reportErrors(stanza);
|
||||||
|
|
||||||
|
let error = stanza.getElementsByTagName("error");
|
||||||
|
if (error.length !== 1) {
|
||||||
|
_converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
error = error[0].firstElementChild.tagName.toLowerCase();
|
||||||
|
if (error === 'conflict') {
|
||||||
|
_converse.connection._changeConnectStatus(Strophe.Status.CONFLICT, error);
|
||||||
|
} else if (error === 'not-acceptable') {
|
||||||
|
_converse.connection._changeConnectStatus(Strophe.Status.NOTACCEPTABLE, error);
|
||||||
|
} else {
|
||||||
|
_converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default RegisterPanel;
|
Loading…
Reference in New Issue
Block a user