How do I prevent password field from changing every time a row is edited?

How do I prevent password field from changing every time a row is edited?

bsukbsuk Posts: 92Questions: 26Answers: 2

I've followed the instructions as per here: https://www.datatables.net/blog/2015-10-02#Password-fields
And I've reread this thread many times over: https://www.datatables.net/forums/discussion/27868/why-is-password-field-updated-when-updating-other-fields

However, I just can't get it working. When this line of code is added:

->on( 'preEdit', function ( $e, $id, $values ) {
            if ( $values['Password'] === '' ) {
                $e->field( 'Password' )->set( false );
            }
        } )

I'm getting this error in the PHP: <b>Notice</b>: Undefined index: Password in line...

Ideally, the password data (which is hashed) wouldn't be pulled into the field at all, when read by the standard edit window.
Strangely this seems to work fine with inline editing, but not when using the standard edit.
If I don't attempt the above code, the password updates each time (giving a false result, as it's rehashing the already hashed password).

Additionally, the field can't be empty, as I have client side validation preventing this from happening (so that I can enforce a password policy). But even when the validation is removed, the error still occurs.
For the record, the field (and DB field) is called Password !

In the blog post, it says this:
"but this is not the case with passwords, where you don't want to (and shouldn't be able to) read the actual values (they should after all be hashed"
But how to not pull in this information?!

Finally I have a test site set up, so you can easily reproduce the error if required (edit a row, then try and make a change):
http://benshaw.org.uk/misc/editortest/users.html

PHP:

Editor::inst( $db, 'LAF' )
    ->fields(
        Field::inst( 'UserName' )->validator( 'Validate::notEmpty' )
        ->validator( 'Validate::unique', array(
                    "message" => "This User Name is already in use"
                ) ),
        Field::inst( 'FullName' )->validator( 'Validate::notEmpty' ),
        Field::inst( 'UserEmail' )->validator( 'Validate::notEmpty' )
        ->validator( 'Validate::email' )
        ->validator( 'Validate::unique', array(
                    "message" => "This Email address is already in use"
                ) ),
        Field::inst( 'Password' )
            ->validator( function ( $val, $data, $opts ) {
                return strlen( $val ) > 60 ?
                    'Password must be less than 60 characters' :
                    true;
            } )     
        
            
            
        
        ->setFormatter( function ( $val ) {
                //return sha1( $val );
                return password_hash($val, PASSWORD_BCRYPT);
        } ),
        Field::inst( 'Access' ),
        
        Field::inst( 'EndDate' )
        ->validator( 'Validate::dateFormat', array(
            "format"  => Format::DATE_ISO_8601,
            "message" => "Please enter a date in the format yyyy-mm-dd"
        ) )
        ->getFormatter( 'Format::date_sql_to_format', Format::DATE_ISO_8601 )
        ->setFormatter( 'Format::date_format_to_sql', Format::DATE_ISO_8601 ),
        Field::inst( 'ChangePass' )
        ->setFormatter( function ( $val, $data, $opts ) {
            return ! $val ? 0 : 1;
            } )
                        
)
->on( 'preEdit', function ( $e, $id, $values ) {
            if ( $values['Password'] === '' ) {
                $e->field( 'Password' )->set( false );
            }
        } )

    ->process( $_POST )
    ->json();
    

In the JS, I've also added:

        formOptions: {
                main: {
                    submit: 'changed'
                }
            },
        

This question has an accepted answers - jump to answer

Answers

  • allanallan Posts: 63,175Questions: 1Answers: 10,409 Site admin

    he password updates each time (giving a false result, as it's rehashing the already hashed password).

    This suggests that the hashed password is being read from the database (i.e. it is being read and then submitted, which is why the check for an empty string isn't working) - is that the case? You can check by looking at the JSON data and looking to see if it contains the hashed passwords, but I suspect it is based on the above.

    That's probably something you don't want to do. The server should just return an empty string for the passwords on read, which you could do with a formatter

    For example:

    Field::inst( 'Password' )
      ->getFormatter( function () {
        return '';
      } )
    

    Regards,
    Allan

  • bsukbsuk Posts: 92Questions: 26Answers: 2
    edited March 2016

    Thanks Allan, Good to know.

    I made that change, and removed the client side formatting, but I'm still getting "System Error", and the php in the console is reporting: Undefined index: Password

    Can you think of any way to troubleshoot this further?
    http://benshaw.org.uk/misc/editortest/users.html

    Many thanks.

    Error screengrab:
    http://benshaw.org.uk/misc/editortest/error.png

  • allanallan Posts: 63,175Questions: 1Answers: 10,409 Site admin

    I think you can remove this now:

    formOptions: {
            main: {
                submit: 'changed'
            }
        },
    

    If you want to keep it then you would need to check for the password parameter being submitted using isset().

    Since you are only submitting changed values at the moment, and you don't change the value, then it isn't submitted. Hence the error (from code that is expecting it to always be present).

    Allan

  • bsukbsuk Posts: 92Questions: 26Answers: 2

    Thanks Allan. It's getting closer now, but the problem still exists when trying to edit any of the other fields using inline editing. It works with the main edit modal, but not with inline editing (reports the system error, and doesn't allow changes to be saved).

    Is there anything extra I need to add in order to prevent the error occuring on "blur" submissions? I currently have:

        
    // Enable submitting by clicking out of inline field (rather than having to press return)
    $('#users').on( 'click', 'tbody td:not(:first-child)', function (e) {
        editor.inline( this, {
                                onBlur: 'submit'
                            } );
    } );
    

    (Sorry for my frequent posts on here by the way, I'm still awaiting my company to process my order for professional support. It should happen soon).

    Full JS:

    // Initiate DT Editor into variable
    var editor; 
    
    // Editor and settings
    $(document).ready(function() {
        editor = new $.fn.dataTable.Editor( {
            
            // Get table/DB config
            ajax: "php/users.php",
            
            // Initiate table
            table: "#users",
            
            // Only submit changed data
            
            
            // Customise button and modal wording
            i18n: {
                    create: {
                        button: "New User",
                        title:  "Create New User",
                        submit: "Create"
                    },
                    edit: {
                        button: "Edit User",
                        title:  "Edit User",
                        submit: "Confirm Changes"
                    },
                    remove: {
                        button: "Delete User",
                        title:  "Delete User",
                        submit: "Confirm",
                        confirm: {
                            _: "Are you sure you wish to delete %d users?",
                            1: "Are you sure you wish to delete this user?"
                        }
                    }
                    },
            
            // Field display names and DB fields
            fields: [ 
                    {
                    label: "*Login Name:",
                    name: "UserName"
                }, 
                    {
                    label: "*Full Name:",
                    name: "FullName"
                }, 
                    {
                    label: "*Email:",
                    name: "UserEmail"
                }, 
                    {
                    label: "Password:",
                    name: "Password",
                     type: "password"
                },  
                    {
                    label: "Access:",                
                    name: "Access",
                    type: "select",
                        
                        // Access group options (standard is set as default here and in the DB)
                        options: [
                                    { label: 'Standard User', value: "Standard User" },
                                    { label: 'Administrator', value: 'Administrator' },
                                    { label: 'Super Admin', value: 'Super Admin' }
                                       ]
                },
                    {
                    label: "End Date:",
                    name: "EndDate",
                    type: 'datetime'
                },
                {
                        label:     "",
                        name:      "ChangePass",
                        type:      "checkbox",
                        separator: "|",
                        options:   [
                            { label: 'User must change password at next login', value: 1 }
                        ]
                    }
            ]
    } );
    
    
    
    
        
    // Enable submitting by clicking out of inline field (rather than having to press return)
    $('#users').on( 'click', 'tbody td:not(:first-child)', function (e) {
        editor.inline( this, {
                                onBlur: 'submit'
                            } );
    } );
    
            
    
    
                    
    
    // Intitiate DT Table               
    var table = $('#users').DataTable( {
        
        // Display sensible wording if table is empty
        "language": {
                  "emptyTable": "No local users have been created yet."
                    },
            
        "columnDefs": [
                
                // Display "No Expiry" if end date field is empty/null
                {
                  "data": null,
                  "defaultContent": "No Expiry",
                  "targets": 6
                }
        ],
            
        // Don't allow length change (fixed at 10) - Amend this if desired  
        lengthChange: false,
        
        // Order table by login name
        "order": [[ 1, "asc" ]],
        
        // Pull config from editor initialisation PHP file
        ajax: "php/users.php",
        
        // Column settings
        columns: [
            
            // Select checkbox to allow standard editing modal
                {
                data: null,
                defaultContent: '',
                className: 'select-checkbox',
                orderable: false
                },
    
                { data: "UserName" },
                { data: "FullName" },
                { data: "UserEmail" },
                
                // Display bootstrap reset button instead of hashed password data
                { data: "Password", render: function ( data, type, row) {
                    return "<button type=\"button\" class=\"btn btn-default btn-xs\">Click to Reset</button>";
                }
                },
                
                { data: "Access" },
                
                // Use moment.js to display date in UK format (amend data format as desired)
                { data: "EndDate", render: function ( data, type, row) {
                    if( data ) {
                    return moment(data).format("DD/MM/YYYY");
                    }
                    }
                }
            ],
            
            // Enable selecting of rows to allow standard editing modal
            select: true
            
    } );
        
        
    // Display the buttons
    new $.fn.dataTable.Buttons( table, [
            { extend: "create", editor: editor },
            { extend: "edit",   editor: editor },
            { extend: "remove", editor: editor }
        ] );
    
        table.buttons().container()
            .appendTo( $('.col-sm-6:eq(0)', table.table().container() ) );
    } );
    
    

    Many thanks again for all your help.

  • allanallan Posts: 63,175Questions: 1Answers: 10,409 Site admin

    Hi,

    So the error now is very similar to before - previously in the main editing view only the changed data was being submitted. Removing that so it will submit all fixes the issue.

    With inline editing, by default it will only submit the changed values. You can modify that to submit all parameters using the form-options parameter for inline(), but I'd suggest you simply check for the Password variable being submitted before attempting to use it, using the isset() method I suggested above. e.g.:

    if ( ! isset( $values['Password'] ) || $values['Password'] === '' ) {
       $e->field( 'Password' )->set( false );
    }
    

    Allan

  • bsukbsuk Posts: 92Questions: 26Answers: 2

    Thanks Allan! That's really great, and it's working now.
    However, there's one last burning question: How do I now apply validation to the password field? e.g must be not blank, must contain a number, one capital letter etc?

    I have versions of these validation processes working in PHP and JS, but neither of them now work, presumably because the inline editing is expecting the validation to be met when data is submitted upon the change of other fields.

    Even when I add:

    formOptions: {
            main: {
                submit: 'changed'
            }
        },
    

    the inline editing is still not allowing me to edit other fields when password validation is enabled.. :(

    For example:

    // Client side form validation (prior to submitting to server)          
        editor.on( 'preSubmit', function ( e, o, action ) {
    
            // Don't validate on deletion of row
            if ( action !== 'remove' ) {
    
            // Validate only on password field
            var PassField = editor.field( 'Password' );
            var currentval = PassField.val();
            var validfail = 0;
    
            // Password must include at least 1 number
            var matches = currentval.match(/\d+/g);
            if (matches != null) {
            } else {
            PassField.error( 'Password requires at least one number' );
            var validfail = 1;
            }
    
            // Password must include at least one upper case letter
            var matches = currentval.match('[A-Z]');
            if (matches != null) {
            } else {
            PassField.error( 'Password requires a capital letter' );
            var validfail = 1;
            }
    
            // Password must be at least 6 characters long
            if (currentval.length < 6) {
            PassField.error( 'Password requires at least 6 characters' );
            var validfail = 1;
            }
    
            // Return true if validation has passed (required to prevent double clicking error on first submit)
            if (validfail == 0) {
            return true;
            }
    
            // Echo any errors, and prompt for resubission
            if ( this.inError() ) {
            return false;
            } 
    
            }
        } );            
    

    I don't understand why all fields are being submitted, despite setting the form options to "changed". There are no javascript or PHP errors this time..

  • bsukbsuk Posts: 92Questions: 26Answers: 2

    I think I may have successfully worked around this in a really weird way (though there's probably a better method):
    I've replaced the blank password default with a random string: "sdkfjhHHjj33" that matches the validation requirements.
    I think the length being forced to 6 characters or above was upsetting things.
    It seems to be working now:

    if ( ! isset( $values['Password'] ) || $values['Password'] === 'sdkfjhHHjj33' ) {
       $e->field( 'Password' )->set( false );
    }
    
    Field::inst( 'Password' )
                ->getFormatter( function () {
                    return 'sdkfjhHHjj33';
                  } )   
    

    A bit unorthodox, but seems to work..
    I've also added:

    $( 'input', editor.field('Password').node() ).on( 'focus', function () {
        this.select();
    } );
    

    To make editing of passwords inline easier.
    Feel free to suggest any problems with this method, or better alternatives, but otherwise thanks very much again for all your help, Allan.

  • allanallan Posts: 63,175Questions: 1Answers: 10,409 Site admin
    edited April 2016 Answer ✓

    How do I now apply validation to the password field? e.g must be not blank, must contain a number, one capital letter etc?

    Your best bet would probably be to use a custom validator. You can use the built in validators to check if a value was given if submitted (notEmpty) but there isn't a built in validator for checking a capital letter etc - that would need a one or two liner custom function.

    You are running into complications because it is doing validation on the client-side. Using a server-side validator will side step that and avoid the need for the workaround above.

    Allan

  • bsukbsuk Posts: 92Questions: 26Answers: 2

    Great, I'll investigate that option further. Many thanks Allan, I really appreciate your help.

This discussion has been closed.