N2F Training Team Blogtorials

January 31, 2010

N2F Yverdon Cookbook: A Useful $user

Filed under: Yverdon — Tags: — z|MattH @ 9:50 pm

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;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.

January 29, 2010

N2F Yverdon Cookbook: Get Records from a Database

Filed under: Yverdon — Tags: — z|MattH @ 3:19 pm

A common task is retrieving records from the database.  Despite the existence of N2F Yverdon Basics: Part 4 - The Database Engine, this common task is often confusing to people used to using the mysql_* functions.  The n2f database system takes care of most of the annoying code for you, and setup is easy.  Getting a batch of records from your database is as simple as the following two steps:

1.  Configure the database in system/config.inc


// name of the db extension you want to use
$cfg['db']['type']    = 'mysqli';
// name of the server that hosts the database
$cfg['db']['host']    = 'localhost';
// name of the database on the server
$cfg['db']['name']    = 'my_database';
// name of the user for this database
$cfg['db']['user']    = 'my_db_user';
// password for the user
$cfg['db']['pass']    = 'my_db_pass';
// file to be used if using the file db extension
$cfg['db']['file']    = '';

2.  Where you want the records, use some code like:


global $db;
$sql = "SELECT * FROM `table` WHERE `field`=?";
$query = $db->query($sql);
$query->addParam('field','value', <DBEXT>_TYPE_CONSTANT);
$query->execQuery();

if($query->isError()) {
// deal with a db error here
} elseif($query->numRows() === 0) {
// deal with no results here
} else {
// do whatever it was you wanted with the data
$records = $query->fetchRows();
}

Remember to replace <DBEXT>_TYPE_CONSTANT with the appropriate constant for the database extension and data type you are using.  For a full list of the methods available on the database and query objects, see the phpDoc for n2f_database and n2f_database_query.

June 26, 2009

N2F Yverdon: v0.2 Improvements

Filed under: Development, Yverdon — Tags: , , , , , , , , , — z|Andrew @ 5:51 pm

With today’s development release of Yverdon v0.2, I decided I would take some time to highlight a few of the smaller changes we’ve implemented in the next iteration of our PHP 5 framework.

MySQLi Database Extension
We were lucky to be working with Waterfall Data Solutions from the start of N2F Yverdon, so we’ve had some great testing done for us on some specific extensions and sets of core functionality.  One of the biggest contributions that we received was massive amounts of test data for the MySQLi database extension.  With all of the data we ended up doing a complete rewrite of the extension, and we still have more improvements that we weren’t able to get into the v0.2 release.  The rewrite concentrated on resolving issues resulting from the buggy implementation of the MySQLi extension in PHP, as well as some irregularities we found when dealing with parametrized queries.  This all leads to more secure interactions with MySQL through the extension, which is never a bad thing.

PHP 5.3 Compatibility
It makes us very proud to say that we sat down with the intention of making Yverdon v0.2 fully compatible with the PHP 5.3 RC’s, and we ended up changing about 5 lines of code in total.  It’s very possible that we’ll be making more changes in the future to account for bugs we haven’t yet found, but at the least we’re already working hard to make sure we don’t fall behind the times in the PHP world.

Memcache Caching
Chris (ctd1500) put in a bit of work to add memcache functionality to our sponsored cache extension.  This means that anything which uses the cache extension can be easily configured to take advantage of a memcache installation, including the template engine.

New Database Extensions
Another set of additions that have been taking a lot of time are the new database extensions we’ve added to our sponsored list.  Now you have access to MySQL, SQLite and PostgreSQL through our new sponsored extensions.  One of the last things we’re doing to the code is running these three new engines through a test routine that will be used on every release moving forward.

There’s a lot more that we’re working on releasing with the first stable release of v0.2, but we’ll save some of them for that time just to keep it that much more of a surprise.  Hopefully you have a better idea of what we’ve been working on with Yverdon over the past 7 or 8 months.

- Andrew

May 22, 2009

N2F Yverdon: What Is A sys.ext.php File?

In Yverdon v0.1, we added the ability to use sys.ext.php files inside of each module.  Their purpose is very simple and yet sometimes beautifully useful.

The Problem
Sometimes we find ourselves working on very large projects.  Inside of those large projects, the tendency is to have each developer concentrate on specific modules or extensions.  The module/extension layout works great in most situations, but it occurred to us that it wouldn’t always be appropriate to put certain system-wide functionality into an extension.  As an example, it would seem odd to build an entire extension just to create some template aliases for one level of the site.

The Solution
Our solution, the sys.ext.php file, allows a developer to include code system-wide on a particular directory level without having to create an extension.  The sys.ext.php files are included after all extensions have been included but before any modules are included.  This gives you the ability to use any of the constructs provided by your extensions and have your code execute before a page.php or data.php is called.  Going back to our example, you will see that the default installation’s main module has a sys.ext.php file inside of it, with the following contents:

<?php

/***********************************************\
* N2F Yverdon v0                              *
* Copyright (c) 2008 Zibings Incorporated     *
*                                             *
* You should have received a copy of the      *
* Microsoft Reciprocal License along with     *
* this program.  If not, see:                 *
* <http://opensource.org/licenses/ms-rl.html> *
\***********************************************/

// Create our template aliases
n2f_template_dynamic::addAlias('header', 'header', 'main', N2F_REL_PATH.'modules', 'default');
n2f_template_dynamic::addAlias('footer', 'footer', 'main', N2F_REL_PATH.'modules', 'default');

?>

This file only needs to be in the main module for all of the other modules in the root level to have access to it’s values.  The result of this code is that now all templates executed in the root level will now be able to use aliases for <%header%> and <%footer%>.

Conclusion
The sys.ext.php files are easy to use and very handy in certain situations.  Hopefully this blogtorial has been helpful in showing you the basic idea behind sys.ext.php files.  Yverdon v0.2 will also be introducing a new file, the mod.ext.php file, but we’ll talk about that when the time comes.  ;)

- Andrew

Powered by WordPress