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.

N2F Yverdon: The mod.ext.php File

Filed under: Development, Yverdon — Tags: , , , , — z|Andrew @ 10:27 am

One of the new features introduced with v0.2 is the mod.ext.php feature.  This feature was a request from a few users and can be a useful way to add functionality to your system.

The premise behind mod.ext.php is similar to that of the sys.ext.php file.  However, instead of being included globally, the mod.ext.php file is only included when a specific module is brought into scope.  This is particularly helpful if you have large/complicated modules and need some classes or global information that is only necessary when that module is in use.

As an example, let’s pretend that we have a set of classes that are particularly large.  We only need these classes inside of our documentation module, so we use a mod.ext.php file to make that possible.  Our mod.ext.php is saved in the module directory at ~/modules/documentation/mod.ext.php, just like a sys.ext.php file.  The code in the file might look like this:

<?php

require('cls/docmgr.cls.php');
require('dat/docs.dat.php');

?>

Just as with the sys.ext.php files we don’t have to do anything special to use mod.ext.php.  Once you create one and the module is called, it will be automatically called into scope before the module’s code is executed.

- Andrew

January 25, 2010

Yverdon v0.2 Changes/Additions

Filed under: Uncategorized — z|Andrew @ 2:12 pm

Now that we’ve finally released v0.2 officially, it seems appropriate to outline the various changes and additions that were done between v0.1 and v0.2.  Lots of testing and planning went into v0.2’s release, so I’ll do my best to summarize in a clear way what we have for the end result.

Bug Fixes

  • Fixed bug with n2f_paginate::listPages() incorrectly listing page numbers
  • Fixed bug with n2f_database::close() trying to close non-objects
  • Fixed bug with n2f_events::hitEvent() throwing error from null arguments
  • Corrected cache method used for dynamic template extension
  • Fixed numerous bugs with MySQLi db extension (complete rewrite)
  • Removed N2F stamp from showing on ?nret=data requests
  • Changed dynamic template parsing method to not use short open tags
  • Fixed relative path issue with n2f-enabled subdirectories
  • Fixed notice thrown by recursive calls to debugEcho()
  • Fixed bug with dynamic template breaking XML declarations
  • Fixed logging bug with setting non-string fields for dynamic templates
  • Changed failure procedure for database extension loading

New Features

  • Added MySQL database extension
  • Added PostgreSQL database extension
  • Added SQLite database extension
  • Added simple meta information system for all extensions
  • Added IsSuccess() function for easily checking n2f_return objects for success/failure status
  • Added notification events to template system
  • Added ability to return cached object from n2f_cache::startCaching()
  • Added N2F version constant (N2F_VERSION)
  • Added port/sock options to database configuration
  • Added n2f_database_query::addParams()
  • Added OS_WINDOWS constant
  • Added ‘mod.ext.php’ modular extension functionality
  • Added optional arguments to n2f_database_query constructor
  • Added ‘global’ skin aliases (for all themes)
  • Created new dynamic argument system for template and database objects
  • Added str_replace_once()
  • Added FirePHP extension
  • Added Memcache capabilities to n2f_cache
  • Added garbage collection to n2f_cache

We’ll do what we can to cover some of the new features here in the coming weeks, as well as to start putting up some better tutorials on the Wiki for how to use the framework in general.

 

- Andrew

Powered by WordPress