N2F Yverdon Cookbook: A Useful $user
Almost every site needs some security, or maybe user specific settings. The n2f $user object exists for this purpose. Since every site’s needs are different, the default $user is more of a template than anything else. Luckily, expanding it is pretty easy.
The default user (see code here) only manages two properties, and only stores them in the session. This minimalist approach is enough to track a user through the site for the length of their session, with code like:
global $user;
if($user->user_id === 0) {
// New user session
$user->user_id = now().rand();
} else {
// This is an existing session - do something about it
}
What if you want your users to be able to log in and you want to record it in a database? Now we need to upgrade the user extension. Let’s take a look at the steps.
First we add a simple table in the database:
CREATE TABLE IF NOT EXISTS `login` ( `user_id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(30) NOT NULL, `password` varchar(50) NOT NULL, `lasttime` int(11) DEFAULT NULL, `name` varchar(100) NOT NULL, `email` varchar(100) NOT NULL, `admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1 = admin 0 = regular', `active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1 = active 0 = inactive', `loggedin` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`user_id`) );
Now we add the attributes to the n2f_user:
class n2f_user {
public $user_id;
public $username;
public $lasttime;
public $name;
public $email;
public $admin; // 1 = admin 0 = regular
public $active; // 1 = active 0 = inactive
public $loggedin; // 1 = logged in
private $errors; // records any error messages
I also added a private variable to store any potential issues our object might experience. We need to initialize the new attributes in the constructor like so:
public function __construct($user_id = null) {
// Initialize the properties
$this->user_id = 0;
$this->username = '';
$this->lasttime = time();
$this->name = '';
$this->email = '';
$this->admin = 0; // 1 = admin 0 = regular
$this->active = 0; // 1 = active 0 = inactive
$this->loggedin = 0; // 0 = logged out
$this->errors = array(); // a empty array to hold error messages
if($user_id !== null) {
// Try to fetch the user from the database
$this->fetch($user_id);
}
// Return ourself for chaining
return ($this);
}
I made the constructor take an optional user id and try to do a fetch, to simplify steps in the code later. Here is the code for fetch():
public function fetch($user_id) {
global $db;
$sql = "SELECT * FROM `login` WHERE `user_id` = ?";
$query = $db->query($sql);
$query->addParam('user_id', $user_id, MYSQLIDB_TYPE_INTEGER);
$query->execQuery();
if($query->isError()) {
// Nevermind, we seem to have a db issue
$this->errors[] = "User database not available";
} elseif($query->numRows() !== 1) {
// Nevermind, we didn't find the user
$this->errors[] = "User not found in database";
} else {
// Load the user with the data
$row = $query->fetchRow();
$this->user_id = $row['user_id'];
$this->username = $row['username'];
$this->lasttime = $row['lasttime'];
$this->name = $row['name'];
$this->email = $row['email'];
$this->admin = $row['admin'];
$this->active = $row['active'];
$this->loggedin = $row['loggedin'];
}
// Return ourself for chaining
return ($this);
}
We can’t fetch anything that isn’t in the database so we need a function to register users like so:
public function register($username, $password, $name, $email, $admin, $active) {
global $db;
// Check that this username is not already in use
$sql = "SELECT * FROM `login` WHERE `username`=?";
$query = $db->query($sql);
$query->addParam('username', $username, MYSQLIDB_TYPE_STRING);
$query->execQuery();
if($query->isError()) {
// We failed because of a db problem
$this->errors[] = "User database not available";
} elseif($query->numRows() > 0) {
// The name is already taken
$this->errors[] = "Username is not available";
} else {
// Encrypt the password
$password = encStr($password);
// Add the new user
$sql = "INSERT INTO `login` SET `username`=?, `password`=?, `name`=?, ";
$sql .= "`email`=?, `admin`=?, `active`=?";
$query = $db->query($sql);
$query->addParam('username', $username, MYSQLIDB_TYPE_STRING);
$query->addParam('password', $password, MYSQLIDB_TYPE_STRING);
$query->addParam('name', $name, MYSQLIDB_TYPE_STRING);
$query->addParam('email', $email, MYSQLIDB_TYPE_STRING);
$query->addParam('admin', $admin, MYSQLIDB_TYPE_INTEGER);
$query->addParam('active', $active, MYSQLIDB_TYPE_INTEGER);
$query->execQuery();
if($query->isError()) {
// We failed because of a db problem
$this->errors[] = "User database not available";
} else {
// Update user_id
$this->user_id = $query->fetchInc();
}
}
// Return ourself for chaining
return ($this);
}
And we’ll also need to be able to update those attributes in the database, so:
public function store($password = null) {
global $db;
$sql = "UPDATE `login` SET `username`=?, `name`=?, `email`=?, ";
$sql .= "`admin`=?, `active`=?, `lasttime`=?, `loggedin`=?";
if($passsword !== null) {
$password = encStr($password);
$sql .= ", `password`=?";
}
$sql .= " WHERE `user_id`=?";
$query = $db->query($sql);
$query->addParam('username', $this->username, MYSQLIDB_TYPE_STRING);
$query->addParam('name', $this->name, MYSQLIDB_TYPE_STRING);
$query->addParam('email', $this->email, MYSQLIDB_TYPE_STRING);
$query->addParam('admin', $this->admin, MYSQLIDB_TYPE_INTEGER);
$query->addParam('active', $this->active, MYSQLIDB_TYPE_INTEGER);
$query->addParam('lasttime', $this->lasttime, MYSQLIDB_TYPE_INTEGER);
$query->addParam('loggedin', $this->loggedin, MYSQLIDB_TYPE_INTEGER);
if($passsword !== null) {
$query->addParam('password', $password, MYSQLIDB_TYPE_STRING);
}
$query->addParam('user_id', $this->user_id, MYSQLIDB_TYPE_INTEGER);
$query->execQuery();
if($query->isError()) {
// We failed because of a db problem
$this->errors[] = "User data was not stored: ".$query->fetchError();
}
// Return ourself for chaining
return ($this);
}
Of course, part of the point is to be able to login and logout. Notice that they update the $sess object. We’ll see why in a moment.
public function login($username,$password) {
global $db, $sess;
if($username == null || $password == null) {
// We failed because of invalid parameters
$this->errors[] = "Username and password are required for login";
} else {
// Encrypt the password
$password = encStr($password);
$sql = "SELECT * FROM `login` WHERE `username`=? AND `password`=? AND `active`=?";
$query = $db->query($sql);
$query->addParam('username', $username, MYSQLIDB_TYPE_STRING);
$query->addParam('password', $password, MYSQLIDB_TYPE_STRING);
$query->addParam('active', 1, MYSQLIDB_TYPE_INTEGER);
$query->execQuery();
if($query->isError()) {
// We failed because of a db problem
$this->errors[] = "User database not available";
} elseif($query->numRows() === 0) {
// Username or Password does not match
$this->errors[] = "Incorrect username or password";
} elseif($query->numRows() > 1) {
// To prevent this, never allow two users to end up with the same username
$this->errors[] = "Duplicate users found for login";
} else {
// Load the data from the row
$row = $query->fetchRow();
$this->user_id = $row['user_id'];
$this->username = $row['username'];
$this->name = $row['name'];
$this->email = $row['email'];
$this->admin = $row['admin'];
$this->active = $row['active'];
// Update their lasttime and loggedin
$this->lasttime = time();
$this->loggedin = 1;
$this->store();
// Update the session
$sess->set('n2f_sess_user', $this->user_id);
}
}
// Return ourself for chaining
return ($this);
}
public function logout() {
global $db, $sess;
// Update their lasttime and loggedin
$this->lasttime = time();
$this->loggedin = 0;
$this->store();
// Update the session
$sess->set('n2f_sess_user', 0);
// Return ourself for chaining
return ($this);
}
The last things in our new n2f_user class are the mindlessly obvious error managing functions:
public function hasError() {
return (count($this->errors)>0);
}
public function getError() {
return $this->errors[count($this->errors)-1];
}
public function getErrors() {
return $this->errors;
}
public function clearErrors() {
$this->errors = array();
// Return ourself for chaining
return ($this);
}
public function addError($error) {
$this->errors[] = $error;
// Return ourself for chaining
return ($this);
}
}
And now for the magic that keeps a user recognized for the life of their session. This is also the reason that login and logout touched the $sess object.
// Hook the N2F_EVT_CORE_LOADED event
$n2f->hookEvent(N2F_EVT_CORE_LOADED, 'init_user');
function init_user(n2f_cls &$n2f, $results) {
// Check if there was a massive failure or if the session extension isn't loaded
if ($results === false || $n2f->hasExtension('session') === false) {
// And if either is the case, just stop here
return(null);
}
// Pull in global variable(s)
global $user, $sess;
// Initialize the timeout stamp
$timeout = (time() - 300);
// Check if there is a session user
if ($sess->exists('n2f_sess_user')) {
// There is, so pull them out
$user_id = $sess->get('n2f_sess_user');
// Fetch them from the db
$user = new n2f_user($user_id);
// First check if they've timed out
if ($user->lasttime < $timeout) {
// And if so, reset their properties
$user->user_id = 0;
$user->active = 0;
$user->admin = 0;
$user->addError("User found, but session timed out!");
// And log them out
$user->logout();
// If we're supposed to track warnings..
if ($n2f->debug->showLevel(N2F_DEBUG_WARN)) {
// Throw a warning to the main debug object
$n2f->debug->throwWarning(N2F_WARN_USER_TIMEOUT, S('N2F_WARN_USER_TIMEOUT'), 'system/extensions/user.ext.php');
}
// Else if they are logged in
} elseif($user->loggedin == 1) {
// Update their lasttime
$user->lasttime = time();
$user->store();
}
} else {
// Otherwise, initialize a new user and set the session
$user = new n2f_user();
$sess->set('n2f_sess_user', $user_id);
}
// If we're supposed to track notices..
if ($n2f->debug->showLevel(N2F_DEBUG_NOTICE)) {
// Throw a notice to the main debug object
$n2f->debug->throwNotice(N2F_NOTICE_USER_INIT, S('N2F_NOTICE_USER_INIT'), 'system/extensions/user.ext.php');
}
// And stop processing, we've got nothing left to do!
return(null);
}
Since that function is registered to be called on every page load, it handles keeping users logged in across page loads, and also logging them out after the timeout. With all this in place, you can now go to a page.php in a module and write code like:
// If user is logged in
if($user->loggedin == 1 &amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp; $user->user_id > 0) {
// If the user as an admin
if($user->admin == 1) {
// Set up and send them to the admin page
} else {
// Set up and send them to the regular users' page
}
} else {
// Send visitor to the login page
}
Isn’t that nice? If you’d like a full copy of the code, I’ve uploaded the SQL and PHP here.
[...] make this easy, we’ll start by using the Useful $user extension.
Pingback by Yverdon N2F Yverdon Cookbook: Ajax login with data.php and jQuery - N2F Training Team Blogtorials — February 4, 2010 @ 11:37 am