PHP PDO Password Update goes to a coded error screen?

Okay. So you are binding then setting the values. Unusual but I suppose it works. I’d set the values first just to keep the editor happy but that is off-topic.

Next basic question. Your edit form starts with:

include('nav/head.php');

if($role_id != '1') {
	header('Location: error.php');
	exit();
}

Does nav/head.php set the value for $role_id?

More importantly, the code suggests that only an admin can see this form . Is that correct?

And even an admin user is not allowed to change their own role or status?

nav/head.php contains the header code for the page. There is code that mentions the $role_id in it:

<?php

session_start();

require_once('api/dbconnect.php');

if(!isset($_SESSION['loggedin'])) {
	header('Location: login.php');
	exit();	
}

$name_query = $pdo->prepare("SELECT first_name FROM users WHERE id = ?");
$name_query->execute([$_SESSION['id']]);
$first_name = $name_query->fetchColumn();

$role_query = $pdo->prepare("SELECT role_id FROM users WHERE id = ?");
$role_result = $role_query->execute([$_SESSION['id']]);
$role_id = $role_query->fetchColumn();

?>

This is here because in order to access any part of this application, you have to log in first. This code starts the session and checks if the user is logged in. If they are, it proceeds to the next line of code. If not, they are redirected to the login page to login.

After that comes a name query and a role query. The name query is there to be used for a different file that does not pertain to the $role_id so we can skip this. The role query is there to set the role_id as the session id so that the app will know what user role has just logged in.

That is the overall goal. There are two user roles, Operator (or Standard user) and Administrator. Only admins should be able to change user information such as enabling or disabling their account, changing passwords, names, emails, etc. Yes, ideally you would want an operator to change their own information (like a user profile), but that isn’t necessary since I don’t need it for this project.

Correct. If they were able to do that, then the code would break as there always needs to be at least one administrator. That’s why on the users page, the active user (or logged in user) cannot delete their account:

image

That padlock is there to prevent the active admin account from being deleted.

So now lets address the actual problem which is that a disabled select element will not submit a value. Which is what is ultimately causing your nulls. Make a simple test form and submit it:

<?php
if (isset($_POST['btn_save'])) {
    var_dump($_POST);
    die();
}
$status = 33;  // Intentionally fake
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Test</title>
</head>
<body>
    <form method="POST">
        <select disabled class="form-control" id="status" name="status">
            <option selected value="1">Active</option>
            <option value="0">Inactive</option>
        </select>
        <!-- <input type="hidden" name="status" value="<?= $status ?>" /> -->
        <input type="submit" name="btn_save" value="Save">
    </form>
</body>
</html>

See the problem with status not being posted? One fix is to follow the select with a hidden control. The hidden value will be posted unless the select is enabled. Uncomment it and try it. This is what some of the other posters were trying to suggest that you do.

You could also do this in code by checking to see if the value was posted and assigning it if it was not.

I’m trying that. The only difference is that yours

is HTML. Mine is PHP:

	if($_SESSION['id'] == $rowUser['id']) {
		echo '<select disabled class="form-control" id="role_id" name="role_id">
			<option selected value="1">Administrator</option>
			<option value="2">Operator</option>
			</select>';
		echo '<p style="color: #a40000;">The user role cannot be changed</p>';

So I’ll likely run into quotation issues…

Well that is a different topic completely. If you want I can give a little sample showing how to generate html without constantly flipping back and forth. I should point out that your approach is not html escaping your output which means that some perfectly data can easily break your code.

That’d be great. It might help me better understand what exactly my code is doing.

See the following example, that conditionally only uses and operates on the correct data for the current user’s role and in the case of an admin if they are editing their own record or not (database specific statements are commented out and ‘fake’ data is being set in the code for demonstration purposes) -

<?php

session_start();

// fake a user
$_SESSION['id'] = 123;

// require '../dbconnect.php';

// use defined constants instead of literal numbers in the code
define('ROLE_ADMIN', 1);
define('ROLE_OPER', 2);

define('STATUS_DISABLED', 0);
define('STATUS_ENABLED', 1);


// define the role choices - used when dynamically outputting the select/option menu
$role_choices = [ROLE_ADMIN=>'Administrator',ROLE_OPER=>'Operator'];

// define the status choices - used when dynamically outputting the select/option menu
$status_choices = [STATUS_DISABLED=>'Inactive',STATUS_ENABLED=>'Active'];

// you would query to get the current user's information and permissions here...

// fake the user's role
$role_id = ROLE_ADMIN;

$errors = []; // an array to hold user error messages
$post = []; // an array to hold a trimmed working copy of the submitted form data

// form processing, detect if a post method form was submitted
if($_SERVER['REQUEST_METHOD'] == 'POST')
{
	// if there is more than one possible form on this page, add logic here (switch/case) to detect which form was submitted
	
	// save/update code -
	
	// trim all the input data at once
	$post = array_map('trim',$_POST); // if any of the fields can be arrays, use a recursive trim call-back function here instead of php's trim function
	// use elements in $post in the rest of the code on this page
	
	// validate all the inputs here, adding validation error messages in the $errors array, using the field name as the array index

	// the existence of and validation for role_id, status, and id are dependent on the current user's role and if they are editing their own record
	
	// validation of the conform_pwd and password/confirm_pwd match is depended on the password field being not empty
	
	// if there are no errors, use the submitted data
	if(empty($errors))
	{
		$params = []; // an array to hold the prepared query input values
		$set_terms = []; // an array to hold the SET terms
		
		// add the 'always' fields
		$set_terms[] = "first_name = ?";
		$set_terms[] = "last_name = ?";
		$set_terms[] = "email = ?";
		$set_terms[] = "username = ?";
		$params[] = $post['first_name'];
		$params[] = $post['last_name'];
		$params[] = $post['email'];
		$params[] = $post['username'];
		
		// add the password field
		if($post['password'] != '')
		{
			$set_terms[] = "password = ?";
			$params[] = password_hash($post['password'], PASSWORD_DEFAULT);
		}
		
		// add the admin only fields (role_id, status, and WHERE id) if the current user is an admin and is not editing their own record
		if($role_id == ROLE_ADMIN && $_SESSION['id'] != $post['id'])
		{
			$set_terms[] = "role_id = ?";
			$set_terms[] = "status = ?";
			$params[] = $post['role_id'];
			$params[] = $post['status'];
			
			// the WHERE id = ? value
			$params[] = $post['id'];
		}
		else
		{
			// the WHERE id = ? value
			$params[] = $_SESSION['id'];
		}
		
		// build and execute the sql query
		$sql = "UPDATE users SET " . implode(',',$set_terms) . " WHERE id = ?";
		//$stmt = $pdo->prepare($sql);
		//$stmt->execute($params);
		
		// examine the query/data
		echo '<pre>';
		echo $sql; echo '<br>';
		print_r($params);
		echo '</pre>';
		
		// note: because the above query can produce duplicate errors for the username and email fields, you would need to detect that here using whatever query error handling method you are using
	}
	
	// if no errors at this point (the sql query logic can produce duplicate errors), success
	if(empty($errors))
	{
		// redirect to the exact same url of this page to cause a get request - PRG Post, Redirect, Get.
		die(header("Refresh:0"));
		// note: if you want to display a one-time success message, store it in a session variable, then test, display, and clear that variable in the html document
	}
}

// get method business logic - get/produce data needed to display the page

// condition and validate the input(s) - edit_id
$edit_id = isset($_GET['edit_id']) ? (int)trim($_GET['edit_id']) : 0;

if(!$edit_id)
{
	$errors[] = 'A user has not been selected.';
}

// if the form has not been submitted and there is an edit_id, retrieve the initial data to edit, less the password hash, and store it in $post
if(empty($post) && $edit_id)
{
	$sql = "SELECT id, first_name, last_name, email, username, role_id, status FROM users WHERE id = ?";
	//$stmt->prepare($sql);
	//$stmt->execute([$edit_id]);
	//$post = $stmt->fetch();
	
	// fake some data
	$post = ['id'=>$edit_id,'first_name'=>'fn','last_name'=>'ln','email'=>'em','username'=>'un','role_id'=>2,'status'=>0];
}

// the html document starts here...
?>

<?php
// display any errors
if(!empty($errors))
{
	echo implode('<br>',$errors);
}

// output the form
if(!empty($post))
{
?>
	<form method='post'>
	<label>First Name: <input type='text' name='first_name' value='<?php echo htmlentities($post['first_name'] ?? '',ENT_QUOTES); ?>'></label><br>
	<label>Last Name: <input type='text' name='last_name' value='<?php echo htmlentities($post['last_name'] ?? '',ENT_QUOTES); ?>'></label><br>
	<label>Email: <input type='text' name='email' value='<?php echo htmlentities($post['email'] ?? '',ENT_QUOTES); ?>'></label><br>
	<label>Username: <input type='text' name='username' value='<?php echo htmlentities($post['username'] ?? '',ENT_QUOTES); ?>'></label><br>
	<?php // note: depending on any errors in either/both the password and confirm_pwd fields, you would clear the values being output so that the user must reenter one/both values ?>
	<label>Password: <input type='text' name='password' value='<?php echo htmlentities($post['password'] ?? '',ENT_QUOTES); ?>'></label><br>
	<label>Confirm Password: <input type='text' name='confirm_pwd' value='<?php echo htmlentities($post['confirm_pwd'] ?? '',ENT_QUOTES); ?>'></label><br>
	<?php
	// add the admin only fields (role_id, status, id) if the current user is an admin and is not editing their own record
	if($role_id == ROLE_ADMIN && $_SESSION['id'] != $post['id'])
	{
		// output the select/option menus for role_id and status, pre-selecting any existing option choice
	?>
		<select name='role_id'>
		<option value=''>Select the role</option>
		<?php
		foreach($role_choices as $choice_id=>$choice)
		{
			$sel = isset($post['role_id']) && $post['role_id'] == $choice_id ? ' selected' : '';
			echo "<option value='$choice_id'$sel>$choice</option>";
		}
		?>
		</select><br>

		<select name='status'>
		<option value=''>Select the status</option>
		<?php
		foreach($status_choices as $choice_id=>$choice)
		{
			$sel = isset($post['status']) && $post['status'] == $choice_id ? ' selected' : '';
			echo "<option value='$choice_id'$sel>$choice</option>";
		}
		?>
		</select><br>

		<input type='hidden' name='id' value='<?php echo htmlentities($post['id'] ?? '',ENT_QUOTES); ?>'>
	<?php
	}
	?>
	<input type='submit'>
	</form>
<?php
}
?>

This is a classic case of being careful about what you asked for. Going to introduce several concepts here that makes things easier but might prove overwhelming.

First, we have an object named App which process the page using a method called run. By introducing an object we can start to organize the code a bit.

Secondly, we use what is known as the heredoc notation for generating strings. Heredoc eliminates the need to switch in and out of php and reduces the quoting issues. It also makes it easy to implement html escaping.

With further ado:

<?php

$app = new App();
$app->run();

class App
{
    public function run()
    {
        if (isset($_POST['btn_save'])) {
            var_dump($_POST);
            die();
        }
        $currentUserId = 42; // $_SESSION['id'];
        $rowUser = [
            'id' => 42,
            'status' => 33,
            'first_name' => 'Tom <"> Jerry',
        ];
        $html = $this->render($currentUserId,$rowUser);
        echo $html;
    }
    protected function escape(string $value) : string
    {
        return htmlspecialchars($value, ENT_COMPAT);
    }
    private function render($currentUserId,$rowUser)
    {
        $html = <<<EOT
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Test</title>
</head>
<body>
{$this->renderContent($currentUserId,$rowUser)}
</body>
</html>
EOT;
        return $html;
    }
    private function renderContent($currentUserId,$rowUser)
    {
        return <<<EOT
    <form method="POST">
        <input type="text" name="first_name" value="{$this->escape($rowUser['first_name'])}" />
        {$this->renderStatus($currentUserId,$rowUser['id'],$rowUser['status'])})
        <input type="submit" name="btn_save" value="Save">
    </form>
EOT;

    }
    private function renderStatus($currentUserId,$rowUserId,$status)
    {
        if ($currentUserId === $rowUserId) {
            return <<<EOT
<select disabled class="form-control" id="status" name="status">
  <option selected value="1">Active</option>
  <option value="0">Inactive</option>
</select>
<input type="hidden" name="status" value="{$status}" />
EOT;
        }
        // This is very hokey, you really should have a loop here
        // Leave that as an excersize
        if ($status === 1) {
            return <<<EOT
<select class="form-control" id="status" name="status">
  <option selected value="1">Active</option>
  <option value="0">Inactive</option>
</select>';
EOT;
        }
        return <<<EOT
<select class="form-control" id="status" name="status">
  <option value="1">Active</option>
  <option selected value="0">Inactive</option>
</select>';
EOT;
        }
}

So we create an object and call run on it. We then call render to generate the html as one long string. The render method generates the basic html page and then calls renderContent to generate the form. You can adjust things later so you can have the same basic html page but different content.

The first_name value is passed through the html escaper. If you view source in the your browser then you can see that some of the characters such as < " > have been transformed so the browser won’t get confused by them.

A renderStatus method focuses on just the status select element. Hopefully it is easier to understand without a bunch of if/else statements. What you really should have is an array of options and then a loop to generate the list instead of repeating everything. But I figured that might be one step to far.

If you plan on continuing with PHP then get yourself an editor which syntax highlights your code. Plenty of free ones out there. You are making things much harder by typing blind.

And learn to use the browser development tools. Pressing F12 should bring it up. The network tab will show you exactly what is being posted and would quickly uncover some of the problems you are having.

I use Atom for editing my code:

I thought of trying this, but it doesn’t work… why?

if($_SESSION['id'] == $rowUser['id']) {
     echo <<<EOT <input type="text" class="form-control" id="role_id" name="role_id" placeholder="" 
     value="<?php print($rowUser['role_id']); ?>" maxlength="255" autocomplete="off" />; EOT;
     echo '<select disabled class="form-control" id="role_id" name="role_id">
	<option selected value="1">Administrator</option>
	<option value="2">Operator</option>
	</select>';
     echo '<p style="color: #a40000;">The user role cannot be changed</p>';
			}

Why not use something like that instead?

If you’re going to use Heredoc syntax, keep in mind " It is very important to note that the line with the closing identifier must contain no other characters, except a semicolon ( ; ). That means especially that the identifier may not be indented , and there may not be any spaces or tabs before or after the semicolon." Yours seems to be just on the end of a load of other code, unless it’s the way you have formatted it for the post. You also don’t need the opening and closing PHP tags, or the print statement, just the variable name.

In what way does it not work, presuming the paragraph above doesn’t cover it?

The fact that it does not work is a pretty good argument for not using it. Humor. Take a step back and think about what you are trying to do. You are deep down in the mud and not seeing clearly what you are coding. Might be time to take a break.

And Atom should be flagging a bunch of things you are doing wrong.

I get this:

Parse error : syntax error, unexpected ‘<<’ (T_SL) in C:\xampp\htdocs\cascotax\user_edit.php on line 78

I’ve tried different methods using echo, print and return. I get an error each time.

You didn’t mention whether the code is formatted differently in the post than it is in your editor. If it is not, then as well as my note about the closing identifier, there’s "After this operator, an identifier is provided, then a newline. " regarding the opening operator. Your post shows that there is more after the opening identifier.

More info: https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc

And yet the doc and its examples shows that there is no need for any of those, it explains that Heredoc behaves just like a double-quoted string, but without the quotes and with the ability to include quotes without escaping them. Just put the variable name, like you would in a double-quoted string.

I tried it again and still nothing, but I read that you cannot use heredoc on the same line. It has to be spread across multiple lines.

So I tried this:

echo <<<EOT
				<input type="hidden" class="form-control" id="status" name="status" placeholder="" value="<?php print($rowUser['status']); ?>" maxlength="255" autocomplete="off" />
				EOT;

And now I have another syntax problem:

Parse error : syntax error, unexpected ‘’ (T_ENCAPSED_AND_WHITESPACE), expecting ‘-’ or identifier (T_STRING) or variable (T_VARIABLE) or number (T_NUM_STRING) in C:\xampp\htdocs\cascotax\user_edit.php on line 52

(That’s also exactly how my code is formatted in the script:)

The closing EOT; must start all the way over to the left. Your editor should be doing that for you.

Even after fixing it, the error is still there…

Parse error : syntax error, unexpected ‘’ (T_ENCAPSED_AND_WHITESPACE), expecting ‘-’ or identifier (T_STRING) or variable (T_VARIABLE) or number (T_NUM_STRING) in C:\xampp\htdocs\cascotax\user_edit.php on line 52

This problem is with your embedded <?php print statement. Actually while that is a problem the error is probably coming from something completely different. Inserting code as images make it difficult to copy/paste and test.

Again, you are more or less randomly trying too many things. Take a break then focus on the basics. And if you are not going to use heredoc the way it intended to be used then don’t use it.

Heredoc…

O’key, I just wouldn’t talk about common MVC-pattern realising: bootstrap-file / application / routing / controllers / view…

But what you think, what shows your IDE by error in Heredoc content, as in HTML, as in PHP? Your IDE shows nothing! Because all Heredoc contents for your IDE it’s just a string.

…That is very bad idea - to use Heredoc for markup rendering.

What else is there to do? Use the code from post #28?

If you have a small “hand-made” project - the best way for you is no concept change. Don’t touch it, if it works.

If you would to migrate to professional style… I think, to use one of existent PHP-frameworks is optimal variant.