RADIUSdesk

Extra Fields To Click-To-Connect Pop-Up

Introduction

  • The Dynamic Login Pages applet has a dedicated section for Click-To-Connect.
  • By default there is an option for the user to first supply their email before they can connect to the Internet.
  • This can be set with and additional expiry date.
  • When these are selected the following happens:
    • The Click-ToConnect is displayed as normal.
    • When the user clicks the button, a check is done if there is a need for the user to supply anything (Based on his MAC Address and if there is an expiry date which now is active with this login page)
    • If there is such a need there is a pop-up window asking the user to provide their email before the Internet connectivity will commence.
  • There might however be the need to also provide additional info besides the email address.
  • This document will show which files needs to be tweaked for that.
  • We will add the following fields
    • A Phone number that needs to be 8 digits long.
    • A field called DN which is also a number that needs to be 7 digits long.

Files To Edit

ExtJs side (Dynamic Details Applet)

  • Locate the file pnlDynamicDetailClickToConnect.js.
  • It is typically under /var/www/rdcore/rd/classic/src/view/dynamicDetails/pnlDynamicDetailClickToConnect.js
  • We will define two combo boxes below cmbReSupply
var cmbReSupplyP = Ext.create('Ext.form.ComboBox', {
    fieldLabel      : 'Re-Supply Phone Interval',
    store           : reSupply,
    queryMode       : 'local',
    displayField    : 'name',
    valueField      : 'id',
    name            : 'ctc_resupply_phone_interval',
    itemId          : 'cmbReSupplyPhone',
    labelCls        : 'lblRd',
    allowBlank      : false,
    forceSelection  : true,
    value           : 0
});
 
var cmbReSupplyDn = Ext.create('Ext.form.ComboBox', {
    fieldLabel      : 'Re-Supply DN Interval',
    store           : reSupply,
    queryMode       : 'local',
    displayField    : 'name',
    valueField      : 'id',
    name            : 'ctc_resupply_dn_interval',
    itemId          : 'cmbReSupplyDn',
    labelCls        : 'lblRd',
    allowBlank      : false,
    forceSelection  : true,
    value           : 0
});
  • Then we will add check boxes for these below the email checkbox
{
    xtype       : 'checkbox',      
    fieldLabel  : 'Require Phone To Connect',
    itemId      : 'chkCtcRequirePhone',
    name        : 'ctc_require_phone',
    inputValue  : 'ctc_require_phone',
    checked     : false,
    disabled    : true
},
cmbReSupplyP,
{
    xtype       : 'checkbox',      
    fieldLabel  : 'Require DN To Connect',
    itemId      : 'chkCtcRequireDn',
    name        : 'ctc_require_dn',
    inputValue  : 'ctc_require_dn',
    checked     : false,
    disabled    : true
},
cmbReSupplyDn
  • You will notice that the items we added are disabled by default.
  • We need to also modify the View Controller to include these new components when certain action are done.
  • Locate the file vcDynamicDetailClickToConnect.js.
  • It is typically under /var/www/rdcore/rd/classic/src/view/dynamicDetails/vcDynamicDetailClickToConnect.js
  • Compare the following with your original code.
Ext.define('Rd.view.dynamicDetails.vcDynamicDetailClickToConnect', {
    extend  : 'Ext.app.ViewController',
    alias   : 'controller.vcDynamicDetailClickToConnect',
    control: {
        '#chkClickToConnect' : {
            change: 'chkClickToConnectChange'
        },
        '#chkCtcRequireEmail' : {
            change:  'chkCtcRequireEmailChange'
        },
        '#chkCtcRequirePhone' : {
            change:  'chkCtcRequirePhoneChange'
        },
        '#chkCtcRequireDn' : {
            change:  'chkCtcRequireDnChange'
        }
    },
    chkClickToConnectChange: function(chk){
        var me      = this;
        var form    = chk.up('form');
        var un      = form.down('#txtConnectUsername');
        var sx      = form.down('#txtConnectSuffix');
        var cd      = form.down('#nrConnectDelay');
        var co      = form.down('#chkConnectOnly');
        var re      = form.down('#chkCtcRequireEmail');
        var rs      = form.down('#cmbReSupply');
        var rp      = form.down('#chkCtcRequirePhone');
        var rsp     = form.down('#cmbReSupplyPhone');
        var rdn     = form.down('#chkCtcRequireDn');
        var rsdn    = form.down('#cmbReSupplyDn');      
        var value   = chk.getValue();
        if(value){
            un.setDisabled(false);
            sx.setDisabled(false);
            cd.setDisabled(false);
            co.setDisabled(false);
            re.setDisabled(false); 
            rs.setDisabled(false);
            rp.setDisabled(false); 
            rsp.setDisabled(false);
            rdn.setDisabled(false); 
            rsdn.setDisabled(false);                   
        }else{
            un.setDisabled(true);
            sx.setDisabled(true);
            cd.setDisabled(true);
            co.setDisabled(true);
            re.setDisabled(true); 
            rs.setDisabled(true);
            rp.setDisabled(true); 
            rsp.setDisabled(true);
            rdn.setDisabled(true); 
            rsdn.setDisabled(true);    
        }
    },
    chkCtcRequireEmailChange: function(chk){
        var me      = this;
        var form    = chk.up('form');
        var value   = chk.getValue();
        var rs      = form.down('#cmbReSupply');
        if(value){
            rs.setDisabled(false);
        }else{
            rs.setDisabled(true);
        }       
    },
    chkCtcRequirePhoneChange: function(chk){
        var me      = this;
        var form    = chk.up('form');
        var value   = chk.getValue();
        var rs      = form.down('#cmbReSupplyPhone');
        if(value){
            rs.setDisabled(false);
        }else{
            rs.setDisabled(true);
        }       
    },
    chkCtcRequireDnChange: function(chk){
        var me      = this;
        var form    = chk.up('form');
        var value   = chk.getValue();
        var rs      = form.down('#cmbReSupplyDn');
        if(value){
            rs.setDisabled(false);
        }else{
            rs.setDisabled(true);
        }       
    }
});
  • Finally we need to add those extra fields to the grid displaying these items
  • Locate the file gridDynamicDetailEmails.js.
  • It is typically under /var/www/rdcore/rd/classic/src/view/dynamicDetails/gridDynamicDetailEmails.js
  • See the following snippet
{ text: 'E-Mail',         dataIndex: 'email',    tdCls: 'gridTree', flex: 1,stateId: 'StateGridDynamicDetailEmails2'},
{ text: 'Phone',          dataIndex: 'phone',    tdCls: 'gridTree', flex: 1,stateId: 'StateGridDynamicDetailEmails2a'},
{ text: 'DN',             dataIndex: 'dn',       tdCls: 'gridTree', flex: 1,stateId: 'StateGridDynamicDetailEmails2b'},
{ 
    text        : 'Captive MAC',
    dataIndex   : 'cp_mac', 
    tdCls       : 'gridTree',
    hidden      : true, 
    flex        : 1,
    stateId		: 'DD_Email_B'
},
  • Remember to run Sencha Command after these to generate the optimized JavaScript code.
  • There is a dedicated Wiki Page in this site for it.
  • Next we will see what we need to modify on the CakePHP3 App Side.

CakePHP3 App Side

  • We need to add extra fields to the dynamic_details sql table inside the rd database.
  • See the following SQL Patch snippet which add those fields if not present
IF NOT EXISTS (SELECT * FROM information_schema.columns
    WHERE column_name = 'ctc_require_phone' AND TABLE_NAME = 'dynamic_details' AND table_schema = 'rd') THEN
    ALTER TABLE dynamic_details ADD COLUMN `ctc_require_phone` tinyint(1) NOT NULL DEFAULT '0';
END IF;
 
IF NOT EXISTS (SELECT * FROM information_schema.columns
    WHERE column_name = 'ctc_resupply_phone_interval' AND TABLE_NAME = 'dynamic_details' AND table_schema = 'rd') THEN
    ALTER TABLE dynamic_details ADD COLUMN `ctc_resupply_phone_interval` INT(4) NOT NULL DEFAULT '0';
END IF;
 
IF NOT EXISTS (SELECT * FROM information_schema.columns
    WHERE column_name = 'ctc_require_dn' AND TABLE_NAME = 'dynamic_details' AND table_schema = 'rd') THEN
    ALTER TABLE dynamic_details ADD COLUMN `ctc_require_dn` tinyint(1) NOT NULL DEFAULT '0';
END IF;
 
IF NOT EXISTS (SELECT * FROM information_schema.columns
    WHERE column_name = 'ctc_resupply_dn_interval' AND TABLE_NAME = 'dynamic_details' AND table_schema = 'rd') THEN
    ALTER TABLE dynamic_details ADD COLUMN `ctc_resupply_dn_interval` INT(4) NOT NULL DEFAULT '0';
END IF;
  • We also need to add extra field to the data_collectors sql table inside the rd database.
  • See the following SQL Patch snippet which add those fields if not present
IF NOT EXISTS (SELECT * FROM information_schema.columns
    WHERE column_name = 'phone' AND TABLE_NAME = 'data_collectors' AND table_schema = 'rd') THEN
    ALTER TABLE data_collectors ADD COLUMN `phone` VARCHAR(36) NOT NULL DEFAULT '';
END IF;
 
IF NOT EXISTS (SELECT * FROM information_schema.columns
    WHERE column_name = 'dn' AND TABLE_NAME = 'data_collectors' AND table_schema = 'rd') THEN
    ALTER TABLE data_collectors ADD COLUMN `dn` VARCHAR(36) NOT NULL DEFAULT '';
END IF;
  • After taking care of the database lets see what needs to be modified to the CakePHP 3 controllers
  • Locate the file DynamicDetailsController.php.
  • It is typically under /var/www/rdcore/cake3/rd_cake/src/Controller/DynamicDetailsController.php
  • There is not much that needs modification since CakePHP takes care of most things behind the scenes.
  • Just locate the editClickToConnect method and modify $check_items
$check_items = [
	'connect_check',
	'connect_only',
	'ctc_require_email',
	'ctc_require_phone',
	'ctc_require_dn',
];
  • Locate the file DataCollectorsController.php.
  • It is typically under /var/www/rdcore/cake3/rd_cake/src/Controller/DataCollectorsController.php
  • We will modify the login page eventually to have various options displayed in the Pop-Up for Click-To-Connect so we need to modify the DataCollectorsController to only check the email field IF PRESENT
  • See the following snippets
public function addMac(){
    if ($this->request->is('post')) { 
        if($this->request->getData('email')){
            if(!$this->_test_email($this->request->data['email'])){
                return;
            }
        }
        $dd = $this->_find_dynamic_detail_id();
  • And this one
private function _addOrEdit($user,$type= 'add') {
 
    //__ Authentication + Authorization __
 
    $user_id    = $user['id'];
 
    if($this->request->getData('email')){
        if(!$this->_test_email($this->request->data['email'])){
            return;
        }
    }
 
    $this->request->data['public_ip'] = $this->request->clientIp();
  • Also the macCheck method need to give feedback on those new methods in order so we can determine which fields must be shown in the Pop-Up
public function macCheck(){
 
    $data   = $this->request->data;      
    $q_r    = $this->_find_dynamic_detail_id();
 
    $data['ctc_require_email'] = false; // By defaul don't ask for email;     
 
    if($q_r){
 
        //Once we found the Dynamic Login Page; We need to figure out if we need to ask for an email for this person
        //For that we need to look for a combo **dynamic_detail_id** and **mac**
        //IF found look at the modify timestamp and if it 'expired' ask for it again
        //If not found ask for it (ctc_require_email == true)
        //Else we set ctc_require_email == false since we found the combo and it has not expired yet  
        $dd_id          = $q_r->dynamic_detail_id;
        $dd_resuply_int = $q_r->dynamic_detail->ctc_resupply_email_interval;
        $dd_resuply_intP = $q_r->dynamic_detail->ctc_resupply_phone_interval;
        $data['dd_id']  = $dd_id;
 
        if($q_r->dynamic_detail->ctc_require_email == true){
            $q_dd = $this->{$this->main_model}->find()
                ->where([$this->main_model.'.dynamic_detail_id' => $dd_id,$this->main_model.'.mac' => $this->request->data['mac']])
                ->first();
            if($q_dd){
                if($dd_resuply_int > 0){ //This has an expiry date lets compare
 
                    $expiry_time    = $q_dd->modified->toUnixString()+($dd_resuply_int * 24 * 60 *60);
                    $now            = new FrozenTime();
                    if($expiry_time < $now->toUnixString()){
                        //It already expired ask for a new one
                        $data['ctc_require_email'] = true; 
                    }   
                }
            }else{
                $data['ctc_require_email'] = true; //We did not found it so have to supply email
            }
        }
 
        if($q_r->dynamic_detail->ctc_require_phone == true){
            $q_dd = $this->{$this->main_model}->find()
                ->where([$this->main_model.'.dynamic_detail_id' => $dd_id,$this->main_model.'.mac' => $this->request->data['mac']])
                ->first();
            if($q_dd){
                if($dd_resuply_intP > 0){ //This has an expiry date lets compare                  
                    $expiry_time    = $q_dd->modified->toUnixString()+($dd_resuply_intP * 24 * 60 *60);
                    $now            = new FrozenTime();
                    if($expiry_time < $now->toUnixString()){
                        //It already expired ask for a new one
                        $data['ctc_require_email'] = true; 
                    }   
                }
            }else{
                $data['ctc_require_phone'] = true; //We did not found it so have to supply email
            }
        }
 
        if($q_r->dynamic_detail->ctc_require_dn == true){
            $q_dd = $this->{$this->main_model}->find()
                ->where([$this->main_model.'.dynamic_detail_id' => $dd_id,$this->main_model.'.mac' => $this->request->data['mac']])
                ->first();
            if($q_dd){
                if($dd_resuply_intP > 0){ //This has an expiry date lets compare                  
                    $expiry_time    = $q_dd->modified->toUnixString()+($dd_resuply_intP * 24 * 60 *60);
                    $now            = new FrozenTime();
                    if($expiry_time < $now->toUnixString()){
                        //It already expired ask for a new one
                        $data['ctc_require_dn'] = true; 
                    }   
                }
            }else{
                $data['ctc_require_dn'] = true; //We did not found it so have to supply dn
            }
        }      
 
    } 
 
    $this->set(array(
        'data' => $data,
        'success' => true,
        '_serialize' => array('data','success')
    ));
 
}
  • This brings us to the end of the CakePHP 3 modifications.
  • The only remaining part is the login page themselves.

Modify the Login Pages for the Extra Options

  • Locate the file rdConnect.js.
  • It is typically under /var/www/login/cp/js/rdConnect.js
  • We need to add a flag to set if we created the pop-up window (ctcFormDone)
cMinWidth           = 240; //240 mobile 300 desktop
scrollHeight        = 1000;
 
var ctcFormDone     = false;
 
fDebug          = function(message){  
    if(cDebug){
        console.log(message)  
    }
};
  • Next we have to add some OR coonditions when the user click on the Click To Connect button
var email_check = location.protocol+'//'+document.location.hostname+"/cake3/rd_cake/data-collectors/mac-check.json";
 
webix.ajax().timeout(3000).post(
    email_check,formData,{ 
    error   : function(text, data, XmlHttpRequest){
        console.log("ERROR -> Getting Info for MAC");    
    },
    success : function(text, data, XmlHttpRequest){
        if(data.json().success == true){            
            if((data.json().data.ctc_require_email == true)||(data.json().data.ctc_require_phone == true)||(data.json().data.ctc_require_dn == true)){
                if(ctcFormDone == false){ //If not already done 
                    buildClickToConnectForm(data.json().data);
                }                       
                showForm("winEmail", b);
            }else{
                onBtnClickToConnectClick();
            } 
        }else{
            console.log("OTHER ERROR");   
        }
    }
});
  • Lets then see the function that is called buildClickToConnectForm.
  • This is called only once.
  • We also include validation rules for those fields.
var buildClickToConnectForm = function(data){
 
    console.log("Click To Connect Form");
    console.log(data);
    var e1 = {
      view      : "template",
      template  : "Please supply to get <b>Guest Access</b>"
    };
    var e2 = { view:"text", label:'Email', name:"email" };
    var e3 = { view:"text", label:'Phone', name:"phone" };
    var e4 = { view:"text", label:'DN',    name:"dn" };
    var b1 = { view:"button", value: "Submit", click:function(){
	    if (this.getParentView().validate()){ //validate form
	        var button      = this;
	        var formData    = new FormData();
	        var mac_address = getParameterByName('mac');
            formData.append("mac", mac_address);
 
            var values      = this.getParentView().getValues();
            console.log(values);
            if(values.email){
                formData.append("email", values.email);  
            }
            if(values.phone){
                formData.append("phone", values.phone);  
            }
            if(values.dn){
                formData.append("dn", values.dn);  
            }
 
 
            //We also add the following
            var called      = getParameterByName('called');
            formData.append("cp_mac", called);
 
            var nasid       = getParameterByName('nasid');
            formData.append("nasid", nasid);
 
            //This might not always be included
            var ssid        = getParameterByName('ssid');
            if(ssid !== ''){
                formData.append("ssid", ssid);
            }   
 
            var add_mac  = location.protocol+'//'+document.location.hostname+"/cake3/rd_cake/data-collectors/add-mac.json";
            webix.ajax().timeout(3000).post(
                add_mac,
                formData,
                { 
                error   : function(text, data, XmlHttpRequest){
                    console.log("ERROR -> Adding MAC");    
                },
                success : function(text, data, XmlHttpRequest){
                    if(data.json().success == true){
                      //console.log("ADDED MAC NOW TRY TO CONNECT");
                      webix.message("All is correct");
                      button.getTopParentView().hide(); //hide window
                      onBtnClickToConnectClick();
 
                    }else{
                        //console.log("OTHER ERROR");
                        webix.message({ type:"error", text:data.json().message });   
                    }
                }
            });          
 
        }
	    else
		    webix.message({ type:"error", text:"Form data is invalid" });
    }};
 
    var elements = [e1];
    var height   = 140;
    var rules   = {};
 
    if(data.ctc_require_email == true){
        elements.push(e2);
        height = height+60;
        rules.email = webix.rules.isEmail;
    }
    if(data.ctc_require_phone == true){
        elements.push(e3);
        height      = height+60;
        rules.phone = function(value){
            if (! /^[0-9]{8}$/.test(value)) {
              webix.message("Phone number must be 8 digits long"); 
              return false;
            }
            return true;
        };
    }
    if(data.ctc_require_dn == true){
        elements.push(e4);
        height = height+60;
        rules.dn = function(value){
            if (! /^[0-9]{7}$/.test(value)) {
              webix.message("DN number must be 7 digits long"); 
              return false;
            }
            return true;
        };
    }
    elements.push(b1);
 
    frmEmail = {
	    view        :"form",
	    borderless  :true,
	    elements    : elements,
	    rules       :rules,
	    elementsConfig:{
		    labelPosition:"top",
	    }
    };
 
     webix.ui({
        view    : "popup",
        id      : "winEmail",
        head    : false,
        height  : height,
        body    :webix.copy(frmEmail)
    });
 
    ctcFormDone = true;     
}
  • That brings us to the totality of the changes required.
  • These login pages does not need any further optimization. (They are done using Webix)