Browse Source

Moved all the registration code into a plugin

JC Brand 9 năm trước cách đây
mục cha
commit
cb241dd594
8 tập tin đã thay đổi với 566 bổ sung467 xóa
  1. 2 1
      converse.js
  2. 3 3
      docs/CHANGES.md
  3. 1 0
      main.js
  4. 4 446
      src/converse-core.js
  5. 7 2
      src/converse-muc.js
  6. 13 6
      src/converse-otr.js
  7. 527 0
      src/converse-register.js
  8. 9 9
      src/utils.js

+ 2 - 1
converse.js

@@ -6,10 +6,11 @@
 define("converse", [
     /* Removable components
      * --------------------
-     * Any of the following components can be removed if they're not needed.
+     * Any of the following components may be removed if they're not needed.
      */
     "converse-muc", // XEP-0045 Multi-user chat
     "converse-otr", // Off-the-record encryption for one-on-one messages
+    "converse-register", // XEP-0077 In-band registration
     /* End: Removable components */
 
     "converse-core"

+ 3 - 3
docs/CHANGES.md

@@ -2,9 +2,9 @@
 
 ## 0.11.0 (Unreleased)
 
-- Split converse.js into different modules. The code for the OTR and MUC
-  features are now in separate modules and these can be removed completely from
-  the build. [jcbrand]
+- Split converse.js into different modules.
+  The code for the OTR, MUC and registration features are now in separate
+  modules and these can be removed completely from the build. [jcbrand]
 - Don't play sound notifications for OTR messages which are setting up an
   encrypted session. [jcbrand]
 - Removed the `account.logout` API, instead use `user.logout`. [jcbrand]

+ 1 - 0
main.js

@@ -47,6 +47,7 @@ require.config({
         "converse-core":            "src/converse-core",
         "converse-muc":             "src/converse-muc",
         "converse-otr":             "src/converse-otr",
+        "converse-register":        "src/converse-register",
         "converse-dependencies":    "src/deps-full",
         "converse-templates":       "src/templates",
 

+ 4 - 446
src/converse-core.js

@@ -131,21 +131,10 @@
         Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
         Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
         Strophe.addNamespace('MAM', 'urn:xmpp:mam:0');
-        Strophe.addNamespace('REGISTER', 'jabber:iq:register');
         Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
         Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
         Strophe.addNamespace('XFORM', 'jabber:x:data');
 
-        // Add Strophe Statuses
-        var i = 0;
-        Object.keys(Strophe.Status).forEach(function (key) {
-            i = Math.max(i, Strophe.Status[key]);
-        });
-        Strophe.Status.REGIFAIL        = i + 1;
-        Strophe.Status.REGISTERED      = i + 2;
-        Strophe.Status.CONFLICT        = i + 3;
-        Strophe.Status.NOTACCEPTABLE   = i + 5;
-
         // Constants
         // ---------
         var LOGIN = "login";
@@ -258,7 +247,6 @@
             moment.locale = moment.lang;
         }
         moment.locale(this.detectLocale(this.isMomentLocale));
-        this.i18n = settings.i18n ? settings.i18n : locales[this.detectLocale(this.isConverseLocale)];
 
         // Translation machinery
         // ---------------------
@@ -272,7 +260,6 @@
             allow_contact_requests: true,
             allow_dragresize: true,
             allow_logout: true,
-            allow_registration: true,
             animate: true,
             archived_messages_page_size: '20',
             authentication: 'login', // Available values are "login", "prebind", "anonymous".
@@ -286,7 +273,6 @@
             csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
             debug: false,
             default_domain: undefined,
-            domain_placeholder: __(" e.g. conversejs.org"),  // Placeholder text shown in the domain input on the registration form
             expose_rid_and_sid: false,
             forward_messages: false,
             hide_offline_users: false,
@@ -302,7 +288,6 @@
             play_sounds: false,
             prebind: false, // XXX: Deprecated, use "authentication" instead.
             prebind_url: null,
-            providers_link: 'https://xmpp.net/directory.php', // Link to XMPP providers shown on registration page
             rid: undefined,
             roster_groups: false,
             show_controlbox_by_default: false,
@@ -2032,22 +2017,16 @@
             renderLoginPanel: function () {
                 var $feedback = this.$('.conn-feedback'); // we want to still show any existing feedback.
                 this.$el.html(converse.templates.controlbox(this.model.toJSON()));
-                var cfg = {'$parent': this.$el.find('.controlbox-panes'), 'model': this};
+                var cfg = {
+                    '$parent': this.$el.find('.controlbox-panes'),
+                    'model': this
+                };
                 if (!this.loginpanel) {
                     this.loginpanel = new converse.LoginPanel(cfg);
-                    if (converse.allow_registration) {
-                        this.registerpanel = new converse.RegisterPanel(cfg);
-                    }
                 } else {
                     this.loginpanel.delegateEvents().initialize(cfg);
-                    if (converse.allow_registration) {
-                        this.registerpanel.delegateEvents().initialize(cfg);
-                    }
                 }
                 this.loginpanel.render();
-                if (converse.allow_registration) {
-                    this.registerpanel.render().$el.hide();
-                }
                 this.initDragResize().setDimensions();
                 if ($feedback.length && $feedback.text() !== __('Connecting')) {
                     this.$('.conn-feedback').replaceWith($feedback);
@@ -4059,427 +4038,6 @@
             }
         });
 
-        this.RegisterPanel = Backbone.View.extend({
-            tagName: 'div',
-            id: "register",
-            className: 'controlbox-pane',
-            events: {
-                'submit form#converse-register': 'onProviderChosen'
-            },
-
-            initialize: function (cfg) {
-                this.reset();
-                this.$parent = cfg.$parent;
-                this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
-                this.registerHooks();
-            },
-
-            render: function () {
-                this.$parent.append(this.$el.html(
-                    converse.templates.register_panel({
-                        'label_domain': __("Your XMPP provider's domain name:"),
-                        'label_register': __('Fetch registration form'),
-                        'help_providers': __('Tip: A list of public XMPP providers is available'),
-                        'help_providers_link': __('here'),
-                        'href_providers': converse.providers_link,
-                        'domain_placeholder': converse.domain_placeholder
-                    })
-                ));
-                this.$tabs.append(converse.templates.register_tab({label_register: __('Register')}));
-                return this;
-            },
-
-            registerHooks: function () {
-                /* Hook into Strophe's _connect_cb, so that we can send an IQ
-                 * requesting the registration fields.
-                 */
-                var conn = converse.connection;
-                var connect_cb = conn._connect_cb.bind(conn);
-                conn._connect_cb = function (req, callback, raw) {
-                    if (!this._registering) {
-                        connect_cb(req, callback, raw);
-                    } else {
-                        if (this.getRegistrationFields(req, callback, raw)) {
-                            this._registering = false;
-                        }
-                    }
-                }.bind(this);
-            },
-
-            getRegistrationFields: function (req, _callback, raw) {
-                /*  Send an IQ stanza to the XMPP server asking for the
-                 *  registration fields.
-                 *  Parameters:
-                 *    (Strophe.Request) req - The current request
-                 *    (Function) callback
-                 */
-                converse.log("sendQueryStanza was called");
-                var conn = converse.connection;
-                conn.connected = true;
-
-                var body = conn._proto._reqToData(req);
-                if (!body) { return; }
-                if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) {
-                    return false;
-                }
-                var register = body.getElementsByTagName("register");
-                var 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,
-                        __('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);
-                conn.send($iq({type: "get"}).c("query", {xmlns: Strophe.NS.REGISTER}).tree());
-                return true;
-            },
-
-            onRegistrationFields: function (stanza) {
-                /*  Handler for Registration Fields Request.
-                 *
-                 *  Parameters:
-                 *    (XMLElement) elem - The query stanza.
-                 */
-                if (stanza.getElementsByTagName("query").length !== 1) {
-                    converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
-                    return false;
-                }
-                this.setFields(stanza);
-                this.renderRegistrationForm(stanza);
-                return false;
-            },
-
-            reset: function (settings) {
-                var defaults = {
-                    fields: {},
-                    urls: [],
-                    title: "",
-                    instructions: "",
-                    registered: false,
-                    _registering: false,
-                    domain: null,
-                    form_type: null
-                };
-                _.extend(this, defaults);
-                if (settings) {
-                    _.extend(this, _.pick(settings, Object.keys(defaults)));
-                }
-            },
-
-            onProviderChosen: function (ev) {
-                /* Callback method that gets called when the user has chosen an
-                 * XMPP provider.
-                 *
-                 * Parameters:
-                 *      (Submit Event) ev - Form submission event.
-                 */
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                var $form = $(ev.target),
-                    $domain_input = $form.find('input[name=domain]'),
-                    domain = $domain_input.val();
-                if (!domain) {
-                    $domain_input.addClass('error');
-                    return;
-                }
-                $form.find('input[type=submit]').hide()
-                    .after(converse.templates.registration_request({
-                        cancel: __('Cancel'),
-                        info_message: __('Requesting a registration form from the XMPP server')
-                    }));
-                $form.find('button.cancel').on('click', this.cancelRegistration.bind(this));
-                this.reset({
-                    domain: Strophe.getDomainFromJid(domain),
-                    _registering: true
-                });
-                converse.connection.connect(this.domain, "", this.onRegistering.bind(this));
-                return false;
-            },
-
-            giveFeedback: function (message, klass) {
-                this.$('.reg-feedback').attr('class', 'reg-feedback').text(message);
-                if (klass) {
-                    $('.reg-feedback').addClass(klass);
-                }
-            },
-
-            onRegistering: function (status, error) {
-                var that;
-                converse.log('onRegistering');
-                if (_.contains([
-                            Strophe.Status.DISCONNECTED,
-                            Strophe.Status.CONNFAIL,
-                            Strophe.Status.REGIFAIL,
-                            Strophe.Status.NOTACCEPTABLE,
-                            Strophe.Status.CONFLICT
-                        ], status)) {
-
-                    converse.log('Problem during registration: Strophe.Status is: '+status);
-                    this.cancelRegistration();
-                    if (error) {
-                        this.giveFeedback(error, 'error');
-                    } else {
-                        this.giveFeedback(__(
-                                'Something went wrong while establishing a connection with "%1$s". Are you sure it exists?',
-                                this.domain
-                            ), 'error');
-                    }
-                } else if (status === Strophe.Status.REGISTERED) {
-                    converse.log("Registered successfully.");
-                    converse.connection.reset();
-                    that = this;
-                    this.$('form').hide(function () {
-                        $(this).replaceWith('<span class="spinner centered"/>');
-                        if (that.fields.password && that.fields.username) {
-                            // automatically log the user in
-                            converse.connection.connect(
-                                that.fields.username.toLowerCase()+'@'+that.domain.toLowerCase(),
-                                that.fields.password,
-                                converse.onConnectStatusChanged
-                            );
-                            converse.chatboxviews.get('controlbox')
-                                .switchTab({target: that.$tabs.find('.current')})
-                                .giveFeedback(__('Now logging you in'));
-                        } else {
-                            converse.chatboxviews.get('controlbox')
-                                .renderLoginPanel()
-                                .giveFeedback(__('Registered successfully'));
-                        }
-                        that.reset();
-                    });
-                }
-            },
-
-            renderRegistrationForm: function (stanza) {
-                /* Renders the registration form based on the XForm fields
-                 * received from the XMPP server.
-                 *
-                 * Parameters:
-                 *      (XMLElement) stanza - The IQ stanza received from the XMPP server.
-                 */
-                var $form= this.$('form'),
-                    $stanza = $(stanza),
-                    $fields, $input;
-                $form.empty().append(converse.templates.registration_form({
-                    'domain': this.domain,
-                    'title': this.title,
-                    'instructions': this.instructions
-                }));
-                if (this.form_type === 'xform') {
-                    $fields = $stanza.find('field');
-                    _.each($fields, function (field) {
-                        $form.append(utils.xForm2webForm.bind(this, $(field), $stanza));
-                    }.bind(this));
-                } else {
-                    // Show fields
-                    _.each(Object.keys(this.fields), function (key) {
-                        if (key === "username") {
-                            $input = templates.form_username({
-                                domain: ' @'+this.domain,
-                                name: key,
-                                type: "text",
-                                label: key,
-                                value: '',
-                                required: 1
-                            });
-                        } else {
-                            $form.append('<label>'+key+'</label>');
-                            $input = $('<input placeholder="'+key+'" name="'+key+'"></input>');
-                            if (key === 'password' || key === 'email') {
-                                $input.attr('type', key);
-                            }
-                        }
-                        $form.append($input);
-                    }.bind(this));
-                    // Show urls
-                    _.each(this.urls, function (url) {
-                        $form.append($('<a target="blank"></a>').attr('href', url).text(url));
-                    }.bind(this));
-                }
-                if (this.fields) {
-                    $form.append('<input type="submit" class="pure-button button-primary" value="'+__('Register')+'"/>');
-                    $form.on('submit', this.submitRegistrationForm.bind(this));
-                    $form.append('<input type="button" class="pure-button button-cancel" value="'+__('Cancel')+'"/>');
-                    $form.find('input[type=button]').on('click', this.cancelRegistration.bind(this));
-                } else {
-                    $form.append('<input type="button" class="submit" value="'+__('Return')+'"/>');
-                    $form.find('input[type=button]').on('click', this.cancelRegistration.bind(this));
-                }
-            },
-
-            reportErrors: function (stanza) {
-                /* Report back to the user any error messages received from the
-                 * XMPP server after attempted registration.
-                 *
-                 * Parameters:
-                 *      (XMLElement) stanza - The IQ stanza received from the
-                 *      XMPP server.
-                 */
-                var $form= this.$('form'), flash;
-                var $errmsgs = $(stanza).find('error text');
-                var $flash = $form.find('.form-errors');
-                if (!$flash.length) {
-                   flash = '<legend class="form-errors"></legend>';
-                    if ($form.find('p.instructions').length) {
-                        $form.find('p.instructions').append(flash);
-                    } else {
-                        $form.prepend(flash);
-                    }
-                    $flash = $form.find('.form-errors');
-                } else {
-                    $flash.empty();
-                }
-                $errmsgs.each(function (idx, txt) {
-                    $flash.append($('<p>').text($(txt).text()));
-                });
-                if (!$errmsgs.length) {
-                    $flash.append($('<p>').text(
-                        __('The provider rejected your registration attempt. '+
-                           'Please check the values you entered for correctness.')));
-                }
-                $flash.show();
-            },
-
-            cancelRegistration: function (ev) {
-                /* Handler, when the user cancels the registration form.
-                 */
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                converse.connection.reset();
-                this.render();
-            },
-
-            submitRegistrationForm : function (ev) {
-                /* Handler, when the user submits the registration form.
-                 * Provides form error feedback or starts the registration
-                 * process.
-                 *
-                 * Parameters:
-                 *      (Event) ev - the submit event.
-                 */
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                var $empty_inputs = this.$('input.required:emptyVal');
-                if ($empty_inputs.length) {
-                    $empty_inputs.addClass('error');
-                    return;
-                }
-                var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
-                    iq = $iq({type: "set"}).c("query", {xmlns:Strophe.NS.REGISTER});
-
-                if (this.form_type === 'xform') {
-                    iq.c("x", {xmlns: Strophe.NS.XFORM, type: 'submit'});
-                    $inputs.each(function () {
-                        iq.cnode(utils.webForm2xForm(this)).up();
-                    });
-                } else {
-                    $inputs.each(function () {
-                        var $input = $(this);
-                        iq.c($input.attr('name'), {}, $input.val());
-                    });
-                }
-                converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null);
-                converse.connection.send(iq);
-                this.setFields(iq.tree());
-            },
-
-            setFields: function (stanza) {
-                /* Stores the values that will be sent to the XMPP server
-                 * during attempted registration.
-                 *
-                 * Parameters:
-                 *      (XMLElement) stanza - the IQ stanza that will be sent to the XMPP server.
-                 */
-                var $query = $(stanza).find('query'), $xform;
-                if ($query.length > 0) {
-                    $xform = $query.find('x[xmlns="'+Strophe.NS.XFORM+'"]');
-                    if ($xform.length > 0) {
-                        this._setFieldsFromXForm($xform);
-                    } else {
-                        this._setFieldsFromLegacy($query);
-                    }
-                }
-            },
-
-            _setFieldsFromLegacy: function ($query) {
-                $query.children().each(function (idx, field) {
-                    var $field = $(field);
-                    if (field.tagName.toLowerCase() === 'instructions') {
-                        this.instructions = Strophe.getText(field);
-                        return;
-                    } else if (field.tagName.toLowerCase() === 'x') {
-                        if ($field.attr('xmlns') === 'jabber:x:oob') {
-                            $field.find('url').each(function (idx, url) {
-                                this.urls.push($(url).text());
-                            }.bind(this));
-                        }
-                        return;
-                    }
-                    this.fields[field.tagName.toLowerCase()] = Strophe.getText(field);
-                }.bind(this));
-                this.form_type = 'legacy';
-            },
-
-            _setFieldsFromXForm: function ($xform) {
-                this.title = $xform.find('title').text();
-                this.instructions = $xform.find('instructions').text();
-                $xform.find('field').each(function (idx, field) {
-                    var _var = field.getAttribute('var');
-                    if (_var) {
-                        this.fields[_var.toLowerCase()] = $(field).children('value').text();
-                    } else {
-                        // TODO: other option seems to be type="fixed"
-                        converse.log("WARNING: Found field we couldn't parse");
-                    }
-                }.bind(this));
-                this.form_type = 'xform';
-            },
-
-            _onRegisterIQ: function (stanza) {
-                /* Callback method that gets called when a return IQ stanza
-                 * is received from the XMPP server, after attempting to
-                 * register a new user.
-                 *
-                 * Parameters:
-                 *      (XMLElement) stanza - The IQ stanza.
-                 */
-                var error = null,
-                    query = stanza.getElementsByTagName("query");
-                if (query.length > 0) {
-                    query = query[0];
-                }
-                if (stanza.getAttribute("type") === "error") {
-                    converse.log("Registration failed.");
-                    error = stanza.getElementsByTagName("error");
-                    if (error.length !== 1) {
-                        converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
-                        return false;
-                    }
-                    error = error[0].firstChild.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);
-                    }
-                    this.reportErrors(stanza);
-                } else {
-                    converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null);
-                }
-                return false;
-            },
-
-            remove: function () {
-                this.$tabs.empty();
-                this.$el.parent().empty();
-            }
-        });
-
         this.LoginPanel = Backbone.View.extend({
             tagName: 'div',
             id: "login-dialog",

+ 7 - 2
src/converse-muc.js

@@ -22,6 +22,7 @@
         factory(converse, utils);
     }
 }(this, function (converse_api, utils) {
+    "use strict";
     // Strophe methods for building stanzas
     var Strophe = converse_api.env.Strophe,
         $iq = converse_api.env.$iq,
@@ -36,7 +37,10 @@
 
     // Translation machinery
     // ---------------------
-    var __ = utils.__.bind(this);
+    // __ is just a placeholder for now, we need to bind the utils.__ method
+    // to the inner converse object, which we can't here, so we do it in the
+    // initialize method.
+    var __ =  function () {};
     var ___ = utils.___;
     
     // Add Strophe Namespaces
@@ -194,8 +198,9 @@
             /* The initialize function gets called as soon as the plugin is
              * loaded by converse.js's plugin machinery.
              */
-
             var converse = this.converse;
+            // For translations
+            __ = utils.__.bind(converse);
             // Configuration values for this plugin
             var settings = {
                 allow_muc: true,

+ 13 - 6
src/converse-otr.js

@@ -23,6 +23,7 @@
         factory(otr, converse, utils);
     }
 }(this, function (otr, converse_api, utils) {
+    "use strict";
     // Strophe methods for building stanzas
     var Strophe = converse_api.env.Strophe,
         b64_sha1 = converse_api.env.b64_sha1;
@@ -30,10 +31,6 @@
     var $ = converse_api.env.jQuery,
         _ = converse_api.env._;
 
-    // Translation machinery
-    // ---------------------
-    var __ = utils.__.bind(this);
-
     var HAS_CSPRNG = ((typeof crypto !== 'undefined') &&
         ((typeof crypto.randomBytes === 'function') ||
             (typeof crypto.getRandomValues === 'function')
@@ -49,20 +46,28 @@
     var VERIFIED= 2;
     var FINISHED = 3;
 
-    // Translation aware constants
-    // ---------------------------
+
     var OTR_CLASS_MAPPING = {};
     OTR_CLASS_MAPPING[UNENCRYPTED] = 'unencrypted';
     OTR_CLASS_MAPPING[UNVERIFIED] = 'unverified';
     OTR_CLASS_MAPPING[VERIFIED] = 'verified';
     OTR_CLASS_MAPPING[FINISHED] = 'finished';
 
+    // Translation aware constants
+    // ---------------------------
+
+    // Just a placeholder for now, we need to bind the utils.__ method to the
+    // inner converse object, which we can't here, so we do it in the
+    // initialize method.
+    var __ =  function () {};
+
     var OTR_TRANSLATED_MAPPING  = {};
     OTR_TRANSLATED_MAPPING[UNENCRYPTED] = __('unencrypted');
     OTR_TRANSLATED_MAPPING[UNVERIFIED] = __('unverified');
     OTR_TRANSLATED_MAPPING[VERIFIED] = __('verified');
     OTR_TRANSLATED_MAPPING[FINISHED] = __('finished');
 
+
     converse_api.plugins.add('otr', {
 
         overrides: {
@@ -486,6 +491,8 @@
              * loaded by converse.js's plugin machinery.
              */
             var converse = this.converse;
+            // For translations
+            __ = utils.__.bind(converse);
             // Configuration values for this plugin
             var settings = {
                 allow_otr: true,

+ 527 - 0
src/converse-register.js

@@ -0,0 +1,527 @@
+// Converse.js (A browser based XMPP chat client)
+// http://conversejs.org
+//
+// Copyright (c) 2012-2016, Jan-Carel Brand <jc@opkode.com>
+// Licensed under the Mozilla Public License (MPLv2)
+//
+/*global converse, utils, Backbone, define */
+
+/* This is a Converse.js plugin which add support for in-band registration
+ * as specified in XEP-0077.
+ */
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        // AMD module loading
+        define("converse-register", ["converse-core", "utils"], factory);
+    } else {
+        // When not using a module loader
+        // -------------------------------
+        // In this case, the dependencies need to be available already as
+        // global variables, and should be loaded separately via *script* tags.
+        // See the file **non_amd.html** for an example of this usecase.
+        factory(converse, utils);
+    }
+}(this, function (converse_api, utils) {
+    "use strict";
+    // Strophe methods for building stanzas
+    var Strophe = converse_api.env.Strophe,
+        $iq = converse_api.env.$iq;
+    // Other necessary globals
+    var $ = converse_api.env.jQuery,
+        _ = converse_api.env._;
+
+    // Translation machinery
+    // ---------------------
+    // Just a placeholder for now, we need to bind the utils.__ method to the
+    // inner converse object, which we can't here, so we do it in the
+    // initialize method.
+    var __ =  function () {};
+    
+    // Add Strophe Namespaces
+    Strophe.addNamespace('REGISTER', 'jabber:iq:register');
+
+    // Add Strophe Statuses
+    var i = 0;
+    Object.keys(Strophe.Status).forEach(function (key) {
+        i = Math.max(i, Strophe.Status[key]);
+    });
+    Strophe.Status.REGIFAIL        = i + 1;
+    Strophe.Status.REGISTERED      = i + 2;
+    Strophe.Status.CONFLICT        = i + 3;
+    Strophe.Status.NOTACCEPTABLE   = i + 5;
+
+    converse_api.plugins.add('register', {
+
+        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: function () {
+                    /* Also render a registration panel, when rendering the
+                     * login panel.
+                     */
+                    this._super.renderLoginPanel.apply(this, arguments);
+                    var converse = this._super.converse,
+                        cfg;
+                    if (converse.allow_registration) {
+                        cfg = {
+                            '$parent': this.$el.find('.controlbox-panes'),
+                            'model': this
+                        };
+                        if (typeof this.registerpanel === 'undefined') {
+                            this.registerpanel = new converse.RegisterPanel(cfg);
+                        } else {
+                            this.registerpanel.delegateEvents().initialize(cfg);
+                        }
+                        this.registerpanel.render().$el.hide();
+                    }
+                    return this;
+                }
+            }
+        },
+
+        initialize: function () {
+            /* The initialize function gets called as soon as the plugin is
+             * loaded by converse.js's plugin machinery.
+             */
+            var converse = this.converse;
+            // For translations
+            __ = utils.__.bind(converse);
+            // Configuration values for this plugin
+            var settings = {
+                allow_registration: true,
+                domain_placeholder: __(" e.g. conversejs.org"),  // Placeholder text shown in the domain input on the registration form
+                providers_link: 'https://xmpp.net/directory.php', // Link to XMPP providers shown on registration page
+            };
+            _.extend(converse, settings);
+            _.extend(converse, _.pick(converse.user_settings, Object.keys(settings)));
+
+
+            converse.RegisterPanel = Backbone.View.extend({
+                tagName: 'div',
+                id: "register",
+                className: 'controlbox-pane',
+                events: {
+                    'submit form#converse-register': 'onProviderChosen'
+                },
+
+                initialize: function (cfg) {
+                    this.reset();
+                    this.$parent = cfg.$parent;
+                    this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
+                    this.registerHooks();
+                },
+
+                render: function () {
+                    this.$parent.append(this.$el.html(
+                        converse.templates.register_panel({
+                            'label_domain': __("Your XMPP provider's domain name:"),
+                            'label_register': __('Fetch registration form'),
+                            'help_providers': __('Tip: A list of public XMPP providers is available'),
+                            'help_providers_link': __('here'),
+                            'href_providers': converse.providers_link,
+                            'domain_placeholder': converse.domain_placeholder
+                        })
+                    ));
+                    this.$tabs.append(converse.templates.register_tab({label_register: __('Register')}));
+                    return this;
+                },
+
+                registerHooks: function () {
+                    /* Hook into Strophe's _connect_cb, so that we can send an IQ
+                    * requesting the registration fields.
+                    */
+                    var conn = converse.connection;
+                    var connect_cb = conn._connect_cb.bind(conn);
+                    conn._connect_cb = function (req, callback, raw) {
+                        if (!this._registering) {
+                            connect_cb(req, callback, raw);
+                        } else {
+                            if (this.getRegistrationFields(req, callback, raw)) {
+                                this._registering = false;
+                            }
+                        }
+                    }.bind(this);
+                },
+
+                getRegistrationFields: function (req, _callback, raw) {
+                    /*  Send an IQ stanza to the XMPP server asking for the
+                    *  registration fields.
+                    *  Parameters:
+                    *    (Strophe.Request) req - The current request
+                    *    (Function) callback
+                    */
+                    converse.log("sendQueryStanza was called");
+                    var conn = converse.connection;
+                    conn.connected = true;
+
+                    var body = conn._proto._reqToData(req);
+                    if (!body) { return; }
+                    if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) {
+                        return false;
+                    }
+                    var register = body.getElementsByTagName("register");
+                    var 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,
+                            __('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);
+                    conn.send($iq({type: "get"}).c("query", {xmlns: Strophe.NS.REGISTER}).tree());
+                    return true;
+                },
+
+                onRegistrationFields: function (stanza) {
+                    /*  Handler for Registration Fields Request.
+                    *
+                    *  Parameters:
+                    *    (XMLElement) elem - The query stanza.
+                    */
+                    if (stanza.getElementsByTagName("query").length !== 1) {
+                        converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
+                        return false;
+                    }
+                    this.setFields(stanza);
+                    this.renderRegistrationForm(stanza);
+                    return false;
+                },
+
+                reset: function (settings) {
+                    var defaults = {
+                        fields: {},
+                        urls: [],
+                        title: "",
+                        instructions: "",
+                        registered: false,
+                        _registering: false,
+                        domain: null,
+                        form_type: null
+                    };
+                    _.extend(this, defaults);
+                    if (settings) {
+                        _.extend(this, _.pick(settings, Object.keys(defaults)));
+                    }
+                },
+
+                onProviderChosen: function (ev) {
+                    /* Callback method that gets called when the user has chosen an
+                    * XMPP provider.
+                    *
+                    * Parameters:
+                    *      (Submit Event) ev - Form submission event.
+                    */
+                    if (ev && ev.preventDefault) { ev.preventDefault(); }
+                    var $form = $(ev.target),
+                        $domain_input = $form.find('input[name=domain]'),
+                        domain = $domain_input.val();
+                    if (!domain) {
+                        $domain_input.addClass('error');
+                        return;
+                    }
+                    $form.find('input[type=submit]').hide()
+                        .after(converse.templates.registration_request({
+                            cancel: __('Cancel'),
+                            info_message: __('Requesting a registration form from the XMPP server')
+                        }));
+                    $form.find('button.cancel').on('click', this.cancelRegistration.bind(this));
+                    this.reset({
+                        domain: Strophe.getDomainFromJid(domain),
+                        _registering: true
+                    });
+                    converse.connection.connect(this.domain, "", this.onRegistering.bind(this));
+                    return false;
+                },
+
+                giveFeedback: function (message, klass) {
+                    this.$('.reg-feedback').attr('class', 'reg-feedback').text(message);
+                    if (klass) {
+                        $('.reg-feedback').addClass(klass);
+                    }
+                },
+
+                onRegistering: function (status, error) {
+                    var that;
+                    converse.log('onRegistering');
+                    if (_.contains([
+                                Strophe.Status.DISCONNECTED,
+                                Strophe.Status.CONNFAIL,
+                                Strophe.Status.REGIFAIL,
+                                Strophe.Status.NOTACCEPTABLE,
+                                Strophe.Status.CONFLICT
+                            ], status)) {
+
+                        converse.log('Problem during registration: Strophe.Status is: '+status);
+                        this.cancelRegistration();
+                        if (error) {
+                            this.giveFeedback(error, 'error');
+                        } else {
+                            this.giveFeedback(__(
+                                    'Something went wrong while establishing a connection with "%1$s". Are you sure it exists?',
+                                    this.domain
+                                ), 'error');
+                        }
+                    } else if (status === Strophe.Status.REGISTERED) {
+                        converse.log("Registered successfully.");
+                        converse.connection.reset();
+                        that = this;
+                        this.$('form').hide(function () {
+                            $(this).replaceWith('<span class="spinner centered"/>');
+                            if (that.fields.password && that.fields.username) {
+                                // automatically log the user in
+                                converse.connection.connect(
+                                    that.fields.username.toLowerCase()+'@'+that.domain.toLowerCase(),
+                                    that.fields.password,
+                                    converse.onConnectStatusChanged
+                                );
+                                converse.chatboxviews.get('controlbox')
+                                    .switchTab({target: that.$tabs.find('.current')})
+                                    .giveFeedback(__('Now logging you in'));
+                            } else {
+                                converse.chatboxviews.get('controlbox')
+                                    .renderLoginPanel()
+                                    .giveFeedback(__('Registered successfully'));
+                            }
+                            that.reset();
+                        });
+                    }
+                },
+
+                renderRegistrationForm: function (stanza) {
+                    /* Renders the registration form based on the XForm fields
+                    * received from the XMPP server.
+                    *
+                    * Parameters:
+                    *      (XMLElement) stanza - The IQ stanza received from the XMPP server.
+                    */
+                    var $form= this.$('form'),
+                        $stanza = $(stanza),
+                        $fields, $input;
+                    $form.empty().append(converse.templates.registration_form({
+                        'domain': this.domain,
+                        'title': this.title,
+                        'instructions': this.instructions
+                    }));
+                    if (this.form_type === 'xform') {
+                        $fields = $stanza.find('field');
+                        _.each($fields, function (field) {
+                            $form.append(utils.xForm2webForm.bind(this, $(field), $stanza));
+                        }.bind(this));
+                    } else {
+                        // Show fields
+                        _.each(Object.keys(this.fields), function (key) {
+                            if (key === "username") {
+                                $input = converse.templates.form_username({
+                                    domain: ' @'+this.domain,
+                                    name: key,
+                                    type: "text",
+                                    label: key,
+                                    value: '',
+                                    required: 1
+                                });
+                            } else {
+                                $form.append('<label>'+key+'</label>');
+                                $input = $('<input placeholder="'+key+'" name="'+key+'"></input>');
+                                if (key === 'password' || key === 'email') {
+                                    $input.attr('type', key);
+                                }
+                            }
+                            $form.append($input);
+                        }.bind(this));
+                        // Show urls
+                        _.each(this.urls, function (url) {
+                            $form.append($('<a target="blank"></a>').attr('href', url).text(url));
+                        }.bind(this));
+                    }
+                    if (this.fields) {
+                        $form.append('<input type="submit" class="pure-button button-primary" value="'+__('Register')+'"/>');
+                        $form.on('submit', this.submitRegistrationForm.bind(this));
+                        $form.append('<input type="button" class="pure-button button-cancel" value="'+__('Cancel')+'"/>');
+                        $form.find('input[type=button]').on('click', this.cancelRegistration.bind(this));
+                    } else {
+                        $form.append('<input type="button" class="submit" value="'+__('Return')+'"/>');
+                        $form.find('input[type=button]').on('click', this.cancelRegistration.bind(this));
+                    }
+                },
+
+                reportErrors: function (stanza) {
+                    /* Report back to the user any error messages received from the
+                    * XMPP server after attempted registration.
+                    *
+                    * Parameters:
+                    *      (XMLElement) stanza - The IQ stanza received from the
+                    *      XMPP server.
+                    */
+                    var $form= this.$('form'), flash;
+                    var $errmsgs = $(stanza).find('error text');
+                    var $flash = $form.find('.form-errors');
+                    if (!$flash.length) {
+                    flash = '<legend class="form-errors"></legend>';
+                        if ($form.find('p.instructions').length) {
+                            $form.find('p.instructions').append(flash);
+                        } else {
+                            $form.prepend(flash);
+                        }
+                        $flash = $form.find('.form-errors');
+                    } else {
+                        $flash.empty();
+                    }
+                    $errmsgs.each(function (idx, txt) {
+                        $flash.append($('<p>').text($(txt).text()));
+                    });
+                    if (!$errmsgs.length) {
+                        $flash.append($('<p>').text(
+                            __('The provider rejected your registration attempt. '+
+                            'Please check the values you entered for correctness.')));
+                    }
+                    $flash.show();
+                },
+
+                cancelRegistration: function (ev) {
+                    /* Handler, when the user cancels the registration form.
+                    */
+                    if (ev && ev.preventDefault) { ev.preventDefault(); }
+                    converse.connection.reset();
+                    this.render();
+                },
+
+                submitRegistrationForm : function (ev) {
+                    /* Handler, when the user submits the registration form.
+                    * Provides form error feedback or starts the registration
+                    * process.
+                    *
+                    * Parameters:
+                    *      (Event) ev - the submit event.
+                    */
+                    if (ev && ev.preventDefault) { ev.preventDefault(); }
+                    var $empty_inputs = this.$('input.required:emptyVal');
+                    if ($empty_inputs.length) {
+                        $empty_inputs.addClass('error');
+                        return;
+                    }
+                    var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
+                        iq = $iq({type: "set"}).c("query", {xmlns:Strophe.NS.REGISTER});
+
+                    if (this.form_type === 'xform') {
+                        iq.c("x", {xmlns: Strophe.NS.XFORM, type: 'submit'});
+                        $inputs.each(function () {
+                            iq.cnode(utils.webForm2xForm(this)).up();
+                        });
+                    } else {
+                        $inputs.each(function () {
+                            var $input = $(this);
+                            iq.c($input.attr('name'), {}, $input.val());
+                        });
+                    }
+                    converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null);
+                    converse.connection.send(iq);
+                    this.setFields(iq.tree());
+                },
+
+                setFields: function (stanza) {
+                    /* Stores the values that will be sent to the XMPP server
+                    * during attempted registration.
+                    *
+                    * Parameters:
+                    *      (XMLElement) stanza - the IQ stanza that will be sent to the XMPP server.
+                    */
+                    var $query = $(stanza).find('query'), $xform;
+                    if ($query.length > 0) {
+                        $xform = $query.find('x[xmlns="'+Strophe.NS.XFORM+'"]');
+                        if ($xform.length > 0) {
+                            this._setFieldsFromXForm($xform);
+                        } else {
+                            this._setFieldsFromLegacy($query);
+                        }
+                    }
+                },
+
+                _setFieldsFromLegacy: function ($query) {
+                    $query.children().each(function (idx, field) {
+                        var $field = $(field);
+                        if (field.tagName.toLowerCase() === 'instructions') {
+                            this.instructions = Strophe.getText(field);
+                            return;
+                        } else if (field.tagName.toLowerCase() === 'x') {
+                            if ($field.attr('xmlns') === 'jabber:x:oob') {
+                                $field.find('url').each(function (idx, url) {
+                                    this.urls.push($(url).text());
+                                }.bind(this));
+                            }
+                            return;
+                        }
+                        this.fields[field.tagName.toLowerCase()] = Strophe.getText(field);
+                    }.bind(this));
+                    this.form_type = 'legacy';
+                },
+
+                _setFieldsFromXForm: function ($xform) {
+                    this.title = $xform.find('title').text();
+                    this.instructions = $xform.find('instructions').text();
+                    $xform.find('field').each(function (idx, field) {
+                        var _var = field.getAttribute('var');
+                        if (_var) {
+                            this.fields[_var.toLowerCase()] = $(field).children('value').text();
+                        } else {
+                            // TODO: other option seems to be type="fixed"
+                            converse.log("WARNING: Found field we couldn't parse");
+                        }
+                    }.bind(this));
+                    this.form_type = 'xform';
+                },
+
+                _onRegisterIQ: function (stanza) {
+                    /* Callback method that gets called when a return IQ stanza
+                    * is received from the XMPP server, after attempting to
+                    * register a new user.
+                    *
+                    * Parameters:
+                    *      (XMLElement) stanza - The IQ stanza.
+                    */
+                    var error = null,
+                        query = stanza.getElementsByTagName("query");
+                    if (query.length > 0) {
+                        query = query[0];
+                    }
+                    if (stanza.getAttribute("type") === "error") {
+                        converse.log("Registration failed.");
+                        error = stanza.getElementsByTagName("error");
+                        if (error.length !== 1) {
+                            converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
+                            return false;
+                        }
+                        error = error[0].firstChild.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);
+                        }
+                        this.reportErrors(stanza);
+                    } else {
+                        converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null);
+                    }
+                    return false;
+                },
+
+                remove: function () {
+                    this.$tabs.empty();
+                    this.$el.parent().empty();
+                }
+            });
+        }
+    });
+}));

+ 9 - 9
src/utils.js

@@ -1,11 +1,11 @@
-/*global jQuery, templates, escape, Jed, _, locales */
+/*global jQuery, templates, escape, _, locales */
 (function (root, factory) {
     if (typeof define === 'function' && define.amd) {
-        define(["jquery", "underscore", "converse-templates", "locales"], factory);
+        define(["jquery", "underscore", "jed", "converse-templates", "locales"], factory);
     } else {
         root.utils = factory(jQuery, _, templates, locales);
     }
-}(this, function ($, _, templates, locales) {
+}(this, function ($, _, Jed, templates, locales) {
     "use strict";
 
     var XFORM_TYPE_MAP = {
@@ -110,12 +110,12 @@
 
         ___: function (str) {
             /* XXX: This is part of a hack to get gettext to scan strings to be
-                * translated. Strings we cannot send to the function above because
-                * they require variable interpolation and we don't yet have the
-                * variables at scan time.
-                *
-                * See actionInfoMessages
-                */
+             * translated. Strings we cannot send to the function above because
+             * they require variable interpolation and we don't yet have the
+             * variables at scan time.
+             *
+             * See actionInfoMessages in src/converse-muc.js
+             */
             return str;
         },