434 lines
16 KiB
JavaScript
434 lines
16 KiB
JavaScript
import log from "@converse/headless/log";
|
|
import tplFormInput from "templates/form_input.js";
|
|
import tplFormUrl from "templates/form_url.js";
|
|
import tplFormUsername from "templates/form_username.js";
|
|
import tplRegisterPanel from "./templates/register_panel.js";
|
|
import { CustomElement } from 'shared/components/element.js';
|
|
import { __ } from 'i18n';
|
|
import { _converse, api, converse } from "@converse/headless/core.js";
|
|
import { initConnection } from '@converse/headless/utils/init.js';
|
|
import { setActiveForm } from './utils.js';
|
|
import { webForm2xForm } from "@converse/headless/utils/form";
|
|
|
|
import './styles/register.scss';
|
|
|
|
// Strophe methods for building stanzas
|
|
const { Strophe, sizzle, $iq } = converse.env;
|
|
const u = converse.env.utils;
|
|
|
|
|
|
const CHOOSE_PROVIDER = 0;
|
|
const FETCHING_FORM = 1;
|
|
const REGISTRATION_FORM = 2;
|
|
const REGISTRATION_FORM_ERROR = 3;
|
|
|
|
|
|
/**
|
|
* @class
|
|
* @namespace _converse.RegisterPanel
|
|
* @memberOf _converse
|
|
*/
|
|
class RegisterPanel extends CustomElement {
|
|
|
|
static get properties () {
|
|
return {
|
|
status : { type: String },
|
|
alert_message: { type: String },
|
|
alert_type: { type: String },
|
|
}
|
|
}
|
|
|
|
constructor () {
|
|
super();
|
|
this.alert_type = 'info';
|
|
this.setErrorMessage = (m) => this.setMessage(m, 'danger');
|
|
this.setFeedbackMessage = (m) => this.setMessage(m, 'info');
|
|
}
|
|
|
|
initialize () {
|
|
this.reset();
|
|
this.listenTo(_converse, 'connectionInitialized', () => this.registerHooks());
|
|
|
|
const domain = api.settings.get('registration_domain');
|
|
if (domain) {
|
|
this.fetchRegistrationForm(domain);
|
|
} else {
|
|
this.status = CHOOSE_PROVIDER;
|
|
}
|
|
}
|
|
|
|
render () {
|
|
return tplRegisterPanel(this);
|
|
}
|
|
|
|
setMessage(message, type) {
|
|
this.alert_type = type;
|
|
this.alert_message = message;
|
|
}
|
|
|
|
/**
|
|
* Hook into Strophe's _connect_cb, so that we can send an IQ
|
|
* requesting the registration fields.
|
|
*/
|
|
registerHooks () {
|
|
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.
|
|
* @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.status = CHOOSE_PROVIDER;
|
|
this.setErrorMessage(__("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.alert_type = 'danger';
|
|
this.setErrorMessage(
|
|
__("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((s) => this.onRegistrationFields(s), 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}
|
|
* @method _converse.RegisterPanel#onRegistrationFields
|
|
* @param { Element } stanza - The query stanza.
|
|
*/
|
|
onRegistrationFields (stanza) {
|
|
if (stanza.getAttribute("type") === "error") {
|
|
this.reportErrors(stanza);
|
|
if (api.settings.get('registration_domain')) {
|
|
this.status = REGISTRATION_FORM_ERROR;
|
|
} else {
|
|
this.status = CHOOSE_PROVIDER;
|
|
}
|
|
return false;
|
|
}
|
|
this.setFields(stanza);
|
|
if (this.status === FETCHING_FORM) {
|
|
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, settings);
|
|
}
|
|
|
|
/**
|
|
* Event handler when the #converse-register form is submitted.
|
|
* Depending on the available input fields, we delegate to other methods.
|
|
* @param { Event } ev
|
|
*/
|
|
onFormSubmission (ev) {
|
|
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
|
|
* @method _converse.RegisterPanel#onProviderChosen
|
|
* @param { HTMLElement } form - The form that was submitted
|
|
*/
|
|
onProviderChosen (form) {
|
|
const domain = form.querySelector('input[name=domain]')?.value;
|
|
if (domain) this.fetchRegistrationForm(domain.trim());
|
|
}
|
|
|
|
/**
|
|
* Fetch a registration form from the requested domain
|
|
* @method _converse.RegisterPanel#fetchRegistrationForm
|
|
* @param { String } domain_name - XMPP server domain
|
|
*/
|
|
fetchRegistrationForm (domain_name) {
|
|
this.status = FETCHING_FORM;
|
|
this.reset({
|
|
'domain': Strophe.getDomainFromJid(domain_name),
|
|
'_registering': true
|
|
});
|
|
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, "", (s) => this.onConnectStatusChanged(s));
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Callback function called by Strophe whenever the connection status changes.
|
|
* Passed to Strophe specifically during a registration attempt.
|
|
* @method _converse.RegisterPanel#onConnectStatusChanged
|
|
* @param { number } 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();
|
|
|
|
if (["converse/login", "converse/register"].includes(_converse.router.history.getFragment())) {
|
|
_converse.router.navigate('', {'replace': true});
|
|
}
|
|
setActiveForm('login');
|
|
|
|
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.setFeedbackMessage(__('Now logging you in'));
|
|
} else {
|
|
this.setFeedbackMessage(__('Registered successfully'));
|
|
}
|
|
this.reset();
|
|
}
|
|
}
|
|
|
|
getLegacyFormFields () {
|
|
const input_fields = Object.keys(this.fields).map(key => {
|
|
if (key === "username") {
|
|
return tplFormUsername({
|
|
'domain': ` @${this.domain}`,
|
|
'name': key,
|
|
'type': "text",
|
|
'label': key,
|
|
'value': '',
|
|
'required': true
|
|
});
|
|
} else {
|
|
return tplFormInput({
|
|
'label': key,
|
|
'name': key,
|
|
'placeholder': key,
|
|
'required': true,
|
|
'type': (key === 'password' || key === 'email') ? key : "text",
|
|
'value': ''
|
|
})
|
|
}
|
|
});
|
|
const urls = this.urls.map(u => tplFormUrl({'label': '', 'value': u}));
|
|
return [...input_fields, ...urls];
|
|
}
|
|
|
|
getFormFields (stanza) {
|
|
if (this.form_type === 'xform') {
|
|
return Array.from(stanza.querySelectorAll('field')).map(field =>
|
|
u.xForm2TemplateResult(field, stanza, {'domain': this.domain})
|
|
);
|
|
} else {
|
|
return this.getLegacyFormFields();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the registration form based on the XForm fields
|
|
* received from the XMPP server.
|
|
* @method _converse.RegisterPanel#renderRegistrationForm
|
|
* @param { Element } stanza - The IQ stanza received from the XMPP server.
|
|
*/
|
|
renderRegistrationForm (stanza) {
|
|
this.form_fields = this.getFormFields(stanza);
|
|
this.status = REGISTRATION_FORM;
|
|
}
|
|
|
|
/**
|
|
* Report back to the user any error messages received from the
|
|
* XMPP server after attempted registration.
|
|
* @method _converse.RegisterPanel#reportErrors
|
|
* @param { Element } stanza - The IQ stanza received from the XMPP server
|
|
*/
|
|
reportErrors (stanza) {
|
|
const errors = Array.from(stanza.querySelectorAll('error'));
|
|
if (errors.length) {
|
|
this.setErrorMessage(errors.reduce((result, e) => `${result}\n${e.textContent}`, ''));
|
|
} else {
|
|
this.setErrorMessage(__('The provider rejected your registration attempt. '+
|
|
'Please check the values you entered for correctness.'));
|
|
}
|
|
}
|
|
|
|
renderProviderChoiceForm (ev) {
|
|
ev?.preventDefault?.();
|
|
_converse.connection._proto._abortAllRequests();
|
|
_converse.connection.reset();
|
|
this.status = CHOOSE_PROVIDER;
|
|
}
|
|
|
|
abortRegistration () {
|
|
_converse.connection._proto._abortAllRequests();
|
|
_converse.connection.reset();
|
|
if ([FETCHING_FORM, REGISTRATION_FORM].includes(this.status)) {
|
|
if (api.settings.get('registration_domain')) {
|
|
this.fetchRegistrationForm(api.settings.get('registration_domain'));
|
|
}
|
|
} else {
|
|
this.requestUpdate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler, when the user submits the registration form.
|
|
* Provides form error feedback or starts the registration process.
|
|
* @method _converse.RegisterPanel#submitRegistrationForm
|
|
* @param { HTMLElement } form - The HTML form that was submitted
|
|
*/
|
|
submitRegistrationForm (form) {
|
|
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 => 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((iq) => this._onRegisterIQ(iq), 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.
|
|
* @method _converse.RegisterPanel#setFields
|
|
* @param { Element } 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.
|
|
* @method _converse.RegisterPanel#reportErrors
|
|
* @param { Element } 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;
|
|
}
|
|
}
|
|
|
|
api.elements.define('converse-register-panel', RegisterPanel);
|