2
0
Эх сурвалжийг харах

Render service URL input in registration form

If none can be determined via settings of XEP-0156
JC Brand 5 сар өмнө
parent
commit
db4952009a

+ 11 - 6
src/headless/shared/connection/index.js

@@ -84,9 +84,9 @@ export class Connection extends Strophe.Connection {
     async discoverConnectionMethods (domain) {
         // Use XEP-0156 to check whether this host advertises websocket or BOSH connection methods.
         const options = {
-            'mode': /** @type {RequestMode} */('cors'),
-            'headers': {
-                'Accept': 'application/xrd+xml, text/xml'
+            mode: /** @type {RequestMode} */('cors'),
+            headers: {
+                Accept: 'application/xrd+xml, text/xml'
             }
         };
         const url = `https://${domain}/.well-known/host-meta`;
@@ -94,7 +94,7 @@ export class Connection extends Strophe.Connection {
         try {
             response = await fetch(url, options);
         } catch (e) {
-            log.error(`Failed to discover alternative connection methods at ${url}`);
+            log.info(`Failed to discover alternative connection methods at ${url}`);
             log.error(e);
             return;
         }
@@ -114,7 +114,7 @@ export class Connection extends Strophe.Connection {
      * @param {Function} callback
      */
     async connect (jid, password, callback) {
-        const { api } = _converse;
+        const { __, api } = _converse;
 
         if (api.settings.get("discover_connection_methods")) {
             const domain = Strophe.getDomainFromJid(jid);
@@ -124,6 +124,11 @@ export class Connection extends Strophe.Connection {
             // If we don't have a connection URL, we show an input for the user
             // to manually provide it.
             api.settings.set('show_connection_url_input', true);
+            (callback || this.onConnectStatusChanged)(
+                Strophe.Status.DISCONNECTED,
+                __('Could not automatically determine a connection URL')
+            );
+            return;
         }
         super.connect(jid, password, callback || this.onConnectStatusChanged, BOSH_WAIT);
     }
@@ -346,7 +351,7 @@ export class Connection extends Strophe.Connection {
      * through various states while establishing or tearing down a
      * connection.
      * @param {Number} status
-     * @param {String} message
+     * @param {String} [message]
      */
     onConnectStatusChanged (status, message) {
         const { __ } = _converse;

+ 2 - 2
src/headless/types/shared/connection/index.d.ts

@@ -86,9 +86,9 @@ export class Connection extends Connection_base {
      * through various states while establishing or tearing down a
      * connection.
      * @param {Number} status
-     * @param {String} message
+     * @param {String} [message]
      */
-    onConnectStatusChanged(status: number, message: string): void;
+    onConnectStatusChanged(status: number, message?: string): void;
     /**
      * @param {string} type
      */

+ 2 - 0
src/plugins/controlbox/loginform.js

@@ -102,3 +102,5 @@ class LoginForm extends CustomElement {
 }
 
 api.elements.define('converse-login-form', LoginForm);
+
+export default LoginForm;

+ 19 - 13
src/plugins/controlbox/templates/loginform.js

@@ -7,7 +7,10 @@ import { __ } from 'i18n';
 
 const { ANONYMOUS, EXTERNAL, LOGIN, PREBIND, CONNECTION_STATUS } = constants;
 
-const trust_checkbox = (checked) => {
+/**
+ * @param {boolean} checked
+ */
+function tplTrustCheckbox(checked) {
     const i18n_hint_trusted = __(
         'To improve performance, we cache your data in this browser. ' +
             'Uncheck this box if this is a public computer or if you want your data to be deleted when you log out. ' +
@@ -35,7 +38,7 @@ const trust_checkbox = (checked) => {
     `;
 };
 
-const connection_url_input = () => {
+export function tplConnectionURLInput() {
     const i18n_connection_url = __('Connection URL');
     const i18n_form_help = __('HTTP or websocket URL that is used to connect to your XMPP server');
     const i18n_placeholder = __('e.g. wss://example.org/xmpp-websocket');
@@ -44,6 +47,7 @@ const connection_url_input = () => {
             <label for="converse-conn-url" class="form-label">${i18n_connection_url}</label>
             <p class="form-help instructions">${i18n_form_help}</p>
             <input
+                required
                 id="converse-conn-url"
                 class="form-control"
                 type="url"
@@ -54,7 +58,7 @@ const connection_url_input = () => {
     `;
 };
 
-const password_input = () => {
+function tplPasswordInput() {
     const i18n_password = __('Password');
     return html`
         <div class="mb-3">
@@ -72,7 +76,7 @@ const password_input = () => {
     `;
 };
 
-const tplRegisterLink = () => {
+function tplRegisterLink() {
     const i18n_create_account = __('Create an account');
     const i18n_hint_no_account = __("Don't have a chat account?");
     return html`
@@ -85,7 +89,7 @@ const tplRegisterLink = () => {
     `;
 };
 
-const tplShowRegisterLink = () => {
+function tplShowRegisterLink() {
     return (
         api.settings.get('allow_registration') &&
         !api.settings.get('auto_login') &&
@@ -93,7 +97,7 @@ const tplShowRegisterLink = () => {
     );
 };
 
-const auth_fields = (el) => {
+function tplAuthFields() {
     const authentication = api.settings.get('authentication');
     const i18n_login = __('Log in');
     const i18n_xmpp_address = __('XMPP Address');
@@ -109,7 +113,6 @@ const auth_fields = (el) => {
                 <input
                     id="converse-login-jid"
                     ?autofocus=${api.settings.get('auto_focus') ? true : false}
-                    @changed=${el.validate}
                     value="${api.settings.get('jid') ?? ''}"
                     required
                     class="form-control"
@@ -118,9 +121,9 @@ const auth_fields = (el) => {
                     placeholder="${placeholder_username}"
                 />
             </div>
-            ${authentication !== EXTERNAL ? password_input() : ''}
-            ${api.settings.get('show_connection_url_input') ? connection_url_input() : ''}
-            ${show_trust_checkbox ? trust_checkbox(show_trust_checkbox === 'off' ? false : true) : ''}
+            ${authentication !== EXTERNAL ? tplPasswordInput() : ''}
+            ${api.settings.get('show_connection_url_input') ? tplConnectionURLInput() : ''}
+            ${show_trust_checkbox ? tplTrustCheckbox(show_trust_checkbox === 'off' ? false : true) : ''}
         </fieldset>
         <fieldset class="form-group buttons">
             <input class="btn btn-primary" type="submit" value="${i18n_login}" />
@@ -129,12 +132,12 @@ const auth_fields = (el) => {
     `;
 };
 
-const form_fields = (el) => {
+function tplFormFields() {
     const authentication = api.settings.get('authentication');
     const i18n_disconnected = __('Disconnected');
     const i18n_anon_login = __('Click here to log in anonymously');
     return html`
-        ${authentication == LOGIN || authentication == EXTERNAL ? auth_fields(el) : ''}
+        ${authentication == LOGIN || authentication == EXTERNAL ? tplAuthFields() : ''}
         ${authentication == ANONYMOUS
             ? html`<input class="btn btn-primary login-anon" type="submit" value="${i18n_anon_login}" />`
             : ''}
@@ -142,6 +145,9 @@ const form_fields = (el) => {
     `;
 };
 
+/**
+ * @param {import('../loginform.js').default} el
+ */
 export default (el) => {
     const { connfeedback } = _converse.state;
     const connection_status = connfeedback.get('connection_status');
@@ -159,6 +165,6 @@ export default (el) => {
             </div>
             ${CONNECTION_STATUS[connection_status] === 'CONNECTING'
                 ? tplSpinner()
-                : form_fields(el)}
+                : tplFormFields()}
         </form>`;
 };

+ 70 - 21
src/plugins/register/form.js

@@ -27,6 +27,8 @@ class RegistrationForm extends CustomElement {
     static get properties () {
         return {
             status : { type: String },
+            domain: { type: String },
+            service_url: { type: String },
             alert_message: { type: String },
             alert_type: { type: String },
         }
@@ -38,14 +40,17 @@ class RegistrationForm extends CustomElement {
         this.fields = {};
         this.domain = null;
         this.alert_type = 'info';
-        this.setErrorMessage = (m) => this.setMessage(m, 'danger');
-        this.setFeedbackMessage = (m) => this.setMessage(m, 'info');
+        this.setErrorMessage = /** @param {string} m */(m) => this.setMessage(m, 'danger');
+        this.setFeedbackMessage = /** @param {string} m */(m) => this.setMessage(m, 'info');
     }
 
     initialize () {
         this.reset();
         this.listenTo(_converse, 'connectionInitialized', () => this.registerHooks());
 
+        const settings = api.settings.get();
+        this.listenTo(settings, 'change:show_connection_url_input', () => this.requestUpdate());
+
         const domain = api.settings.get('registration_domain');
         if (domain) {
             this.fetchRegistrationForm(domain);
@@ -58,6 +63,10 @@ class RegistrationForm extends CustomElement {
         return tplChooseProvider(this);
     }
 
+    /**
+     * @param {string} message
+     * @param {'info'|'danger'} type
+     */
     setMessage(message, type) {
         this.alert_type = type;
         this.alert_message = message;
@@ -168,61 +177,92 @@ class RegistrationForm extends CustomElement {
     onFormSubmission (ev) {
         ev?.preventDefault?.();
         const form = /** @type {HTMLFormElement} */(ev.target);
-        if (form.querySelector('input[name=domain]') === null) {
+
+        const domain_input = /** @type {HTMLInputElement} */(form.querySelector('input[name=domain]'));
+        if (domain_input === null) {
             this.submitRegistrationForm(form);
         } else {
             this.onProviderChosen(form);
         }
-
     }
 
     /**
      * Callback method that gets called when the user has chosen an XMPP provider
-     * @method _converse.RegistrationForm#onProviderChosen
-     * @param {HTMLElement} form - The form that was submitted
+     * @param {HTMLFormElement} form - The form that was submitted
      */
     onProviderChosen (form) {
         const domain = /** @type {HTMLInputElement} */(form.querySelector('input[name=domain]'))?.value;
-        if (domain) this.fetchRegistrationForm(domain.trim());
+        if (domain) {
+            const form_data = new FormData(form);
+            let service_url = null;
+            if (api.settings.get('show_connection_url_input')) {
+                service_url = /** @type {string} */(form_data.get('connection-url'));
+                if (service_url.startsWith('wss:')) {
+                    api.settings.set("websocket_url", service_url);
+                } else if (service_url.startsWith('https:')) {
+                    api.settings.set('bosh_service_url', service_url);
+                } else {
+                    this.alert_message = __('Invalid connection URL, only HTTPS and WSS accepted');
+                    this.alert_type = 'danger';
+                    this.status = CHOOSE_PROVIDER;
+                    this.requestUpdate();
+                    return;
+                }
+            }
+            this.fetchRegistrationForm(domain.trim(), service_url?.trim());
+        } else {
+            this.status = CHOOSE_PROVIDER;
+        }
     }
 
     /**
      * Fetch a registration form from the requested domain
-     * @method _converse.RegistrationForm#fetchRegistrationForm
      * @param {string} domain_name - XMPP server domain
+     * @param {string|null} [service_url]
      */
-    fetchRegistrationForm (domain_name) {
+    fetchRegistrationForm (domain_name, service_url) {
         this.status = FETCHING_FORM;
         this.reset({
-            'domain': Strophe.getDomainFromJid(domain_name),
-            '_registering': true
+            _registering: true,
+            domain: Strophe.getDomainFromJid(domain_name),
+            service_url,
         });
+
         api.connection.init();
+
         // When testing, the test tears down before the async function
         // above finishes. So we use optional chaining here
-        api.connection.get()?.connect(this.domain, "", (s) => this.onConnectStatusChanged(s));
+        api.connection.get()?.connect(
+            this.domain,
+            '',
+            /**
+             * @param {number} s
+             * @param {string} m
+             */
+            (s, m) => this.onConnectStatusChanged(s, m)
+        );
         return false;
     }
 
     /**
      * Callback function called by Strophe whenever the connection status changes.
      * Passed to Strophe specifically during a registration attempt.
-     * @method _converse.RegistrationForm#onConnectStatusChanged
      * @param {number} status_code - The Strophe.Status status code
+     * @param {string} message
      */
-    onConnectStatusChanged(status_code) {
+    onConnectStatusChanged(status_code, message) {
         log.debug('converse-register: onConnectStatusChanged');
         if ([Strophe.Status.DISCONNECTED,
-             Strophe.Status.CONNFAIL,
-             Strophe.Status.REGIFAIL,
+            Strophe.Status.CONNFAIL,
+            Strophe.Status.REGIFAIL,
              Strophe.Status.NOTACCEPTABLE,
              Strophe.Status.CONFLICT
             ].includes(status_code)) {
 
-            log.error(
+            log.warn(
                 `Problem during registration: Strophe.Status is ${CONNECTION_STATUS[status_code]}`
             );
-            this.abortRegistration();
+            this.abortRegistration(message);
         } else if (status_code === Strophe.Status.REGISTERED) {
             log.debug("Registered successfully.");
             api.connection.get().reset();
@@ -313,6 +353,9 @@ class RegistrationForm extends CustomElement {
         }
     }
 
+    /**
+     * @param {Event} ev
+     */
     renderProviderChoiceForm (ev) {
         ev?.preventDefault?.();
         const connection = api.connection.get();
@@ -321,17 +364,23 @@ class RegistrationForm extends CustomElement {
         this.status = CHOOSE_PROVIDER;
     }
 
-    abortRegistration () {
+    /**
+     * @param {string} message
+     */
+    abortRegistration (message) {
         const connection = api.connection.get();
         connection._proto._abortAllRequests();
         connection.reset();
         if ([FETCHING_FORM, REGISTRATION_FORM].includes(this.status)) {
             if (api.settings.get('registration_domain')) {
                 this.fetchRegistrationForm(api.settings.get('registration_domain'));
+                return;
             }
-        } else {
-            this.requestUpdate();
         }
+        this.alert_message = message;
+        this.alert_type = 'danger';
+        this.status = CHOOSE_PROVIDER;
+        this.requestUpdate();
     }
 
     /**

+ 17 - 8
src/plugins/register/templates/choose_provider.js

@@ -1,9 +1,10 @@
-import tplRegistrationForm from './registration_form.js';
+import { html } from 'lit';
+import { api } from '@converse/headless';
 import tplSpinner from 'templates/spinner.js';
-import tplSwitchForm from './switch_form.js';
 import { __ } from 'i18n';
-import { api } from '@converse/headless';
-import { html } from 'lit';
+import { tplConnectionURLInput } from '../../controlbox/templates/loginform.js';
+import tplSwitchForm from './switch_form.js';
+import tplRegistrationForm from './registration_form.js';
 
 /**
  * @param {import('../form.js').default} el
@@ -24,17 +25,26 @@ function tplFormRequest(el) {
     `;
 };
 
-function tplDomainInput() {
+/**
+ * @param {import('../form.js').default} el
+ */
+function tplDomainInput(el) {
     const domain_placeholder = api.settings.get('domain_placeholder');
     const i18n_providers = __('Tip: A list of public XMPP providers is available');
     const i18n_providers_link = __('here');
     const href_providers = api.settings.get('providers_link');
     return html`
-        <input class="form-control" required="required" type="text" name="domain" placeholder="${domain_placeholder}" />
+        <input class="form-control"
+            required="required"
+            type="text" name="domain"
+            placeholder="${domain_placeholder}"
+            value="${el.domain}"
+        />
         <p class="form-text text-muted">
             ${i18n_providers}
             <a href="${href_providers}" class="url" target="_blank" rel="noopener">${i18n_providers_link}</a>.
         </p>
+        ${api.settings.get('show_connection_url_input') ? tplConnectionURLInput() : ''}
     `;
 };
 
@@ -67,8 +77,7 @@ function tplChooseProvider(el) {
             <legend class="col-form-label">${i18n_create_account}</legend>
             <div>
                 <label class="form-label">${i18n_choose_provider}</label>
-
-                ${default_domain ? default_domain : tplDomainInput()}
+                ${default_domain ? default_domain : tplDomainInput(el)}
             </div>
             ${show_form_buttons ? tplFetchFormButtons() : ''}
         </form>

+ 1 - 1
src/templates/form_captcha.js

@@ -1,7 +1,7 @@
 import { html } from "lit";
 
 export default (o) => html`
-    <fieldset class="pb-2">
+    <fieldset class="mb-3">
         ${o.label ? html`<label class="form-label">${o.label}</label>` : '' }
         <img src="data:${o.type};base64,${o.data}">
         <input name="${o.name}" type="text" ?required="${o.required}" />

+ 1 - 1
src/templates/form_checkbox.js

@@ -1,7 +1,7 @@
 import { html } from "lit";
 
 export default  (o) => html`
-    <fieldset class="pb-2 form-check">
+    <fieldset class="mb-3 form-check">
         <input id="${o.id}"
                name="${o.name}"
                type="checkbox"

+ 1 - 1
src/templates/form_date.js

@@ -1,7 +1,7 @@
 import { html } from "lit";
 
 export default  (o) => html`
-    <div class="pb-2">
+    <div class="mb-3">
         <label for="${o.id}" class="form-label">${o.label}
             ${(o.desc) ? html`<small class="form-text text-muted">${o.desc}</small>` : ''}
         </label>

+ 1 - 1
src/templates/form_input.js

@@ -1,7 +1,7 @@
 import { html } from "lit";
 
 export default  (o) => html`
-    <div class="pb-2">
+    <div class="mb-3">
         ${ o.type !== 'hidden' ? html`<label for="${o.id}" class="form-label">${o.label}
             ${(o.desc) ? html`<small class="form-text text-muted">${o.desc}</small>` : ''}
         </label>` : '' }

+ 1 - 1
src/templates/form_select.js

@@ -3,7 +3,7 @@ import { html } from "lit";
 const tplOption = (o) => html`<option value="${o.value}" ?selected="${o.selected}">${o.label}</option>`;
 
 export default  (o) => html`
-    <div class="pb-2">
+    <div class="mb-3">
         <label for="${o.id}" class="form-label">${o.label}</label>
         <select class="form-control" id="${o.id}" name="${o.name}" ?multiple="${o.multiple}">
             ${o.options?.map(o => tplOption(o))}

+ 1 - 1
src/templates/form_textarea.js

@@ -4,7 +4,7 @@ import { u } from '@converse/headless';
 export default (o) => {
     const id = u.getUniqueId();
     return html`
-        <div class="pb-2">
+        <div class="mb-3">
             <label class="form-label label-ta" for="${o.id}">${o.label}
                 ${(o.desc) ? html`<small class="form-text text-muted">${o.desc}</small>` : ''}
             </label>

+ 1 - 1
src/templates/form_url.js

@@ -1,7 +1,7 @@
 import { html } from "lit";
 
 export default (o) => html`
-    <div class="pb-2">
+    <div class="mb-3">
         <label for="${o.id}" class="form-label">${o.label}
             ${ o.desc ? html`<small id="o.id" class="form-text text-muted">${o.desc}</small>` : '' }
         </label>

+ 1 - 1
src/templates/form_username.js

@@ -1,7 +1,7 @@
 import { html } from 'lit';
 
 export default (o) => html`
-    <div class="pb-2">
+    <div class="mb-3">
         ${
             o.type !== 'hidden'
                 ? html`<label for="${o.id}" class="form-label"

+ 22 - 1
src/types/plugins/controlbox/loginform.d.ts

@@ -1,2 +1,23 @@
-export {};
+export default LoginForm;
+declare class LoginForm extends CustomElement {
+    initialize(): void;
+    handler: () => void;
+    connectedCallback(): void;
+    render(): import("lit").TemplateResult<1>;
+    firstUpdated(): void;
+    /**
+     * @param {SubmitEvent} ev
+     */
+    onLoginFormSubmitted(ev: SubmitEvent): Promise<void>;
+    /**
+     * @param {HTMLFormElement} form
+     */
+    discoverConnectionMethods(form: HTMLFormElement): any;
+    initPopovers(): void;
+    /**
+     * @param {string} [jid]
+     */
+    connect(jid?: string): void;
+}
+import { CustomElement } from 'shared/components/element.js';
 //# sourceMappingURL=loginform.d.ts.map

+ 2 - 1
src/types/plugins/controlbox/templates/loginform.d.ts

@@ -1,3 +1,4 @@
-declare function _default(el: any): import("lit").TemplateResult<1>;
+export function tplConnectionURLInput(): import("lit").TemplateResult<1>;
+declare function _default(el: import("../loginform.js").default): import("lit").TemplateResult<1>;
 export default _default;
 //# sourceMappingURL=loginform.d.ts.map

+ 27 - 12
src/types/plugins/register/form.d.ts

@@ -7,6 +7,12 @@ declare class RegistrationForm extends CustomElement {
         status: {
             type: StringConstructor;
         };
+        domain: {
+            type: StringConstructor;
+        };
+        service_url: {
+            type: StringConstructor;
+        };
         alert_message: {
             type: StringConstructor;
         };
@@ -18,12 +24,16 @@ declare class RegistrationForm extends CustomElement {
     fields: {};
     domain: any;
     alert_type: string;
-    setErrorMessage: (m: any) => void;
-    setFeedbackMessage: (m: any) => void;
+    setErrorMessage: (m: string) => void;
+    setFeedbackMessage: (m: string) => void;
     initialize(): void;
     status: number;
     render(): import("lit").TemplateResult<1>;
-    setMessage(message: any, type: any): void;
+    /**
+     * @param {string} message
+     * @param {'info'|'danger'} type
+     */
+    setMessage(message: string, type: "info" | "danger"): void;
     alert_message: any;
     /**
      * Hook into Strophe's _connect_cb, so that we can send an IQ
@@ -53,23 +63,22 @@ declare class RegistrationForm extends CustomElement {
     onFormSubmission(ev: Event): void;
     /**
      * Callback method that gets called when the user has chosen an XMPP provider
-     * @method _converse.RegistrationForm#onProviderChosen
-     * @param {HTMLElement} form - The form that was submitted
+     * @param {HTMLFormElement} form - The form that was submitted
      */
-    onProviderChosen(form: HTMLElement): void;
+    onProviderChosen(form: HTMLFormElement): void;
     /**
      * Fetch a registration form from the requested domain
-     * @method _converse.RegistrationForm#fetchRegistrationForm
      * @param {string} domain_name - XMPP server domain
+     * @param {string|null} [service_url]
      */
-    fetchRegistrationForm(domain_name: string): boolean;
+    fetchRegistrationForm(domain_name: string, service_url?: string | null): boolean;
     /**
      * Callback function called by Strophe whenever the connection status changes.
      * Passed to Strophe specifically during a registration attempt.
-     * @method _converse.RegistrationForm#onConnectStatusChanged
      * @param {number} status_code - The Strophe.Status status code
+     * @param {string} message
      */
-    onConnectStatusChanged(status_code: number): void;
+    onConnectStatusChanged(status_code: number, message: string): void;
     getLegacyFormFields(): import("lit").TemplateResult<1>[];
     /**
      * @param {Element} stanza
@@ -90,8 +99,14 @@ declare class RegistrationForm extends CustomElement {
      * @param {Element} stanza - The IQ stanza received from the XMPP server
      */
     reportErrors(stanza: Element): void;
-    renderProviderChoiceForm(ev: any): void;
-    abortRegistration(): void;
+    /**
+     * @param {Event} ev
+     */
+    renderProviderChoiceForm(ev: Event): void;
+    /**
+     * @param {string} message
+     */
+    abortRegistration(message: string): void;
     /**
      * Handler, when the user submits the registration form.
      * Provides form error feedback or starts the registration process.