Accountify: Add Accounts To Your Web Site

Current version: 2.02

Accountify is out of date. It was a nice system, but it is based on PHP 4 and Pear::DB, both of which are definitely deprecated at this point. If you want to build web applications in PHP in 2012, we recommend that you check out Symfony or Drupal. Symfony is your best bet if you are a programmer, Drupal may work better for you if you are not.

What Is Accountify?

Accountify is a library of PHP code that lets you add accounts to a new or existing PHP-based web site. (Some people call them logins.) In other words, Accountify solves the identity management problem for many different types of sites.

With Accountify, users who wish to do so can apply for accounts in which to store their personal settings. Accountify does this by extending PHP's built-in "sessions" feature, storing session data in a database and taking care of the nuts and bolts of applying for, verifying, and maintaining user accounts. Accountify also plays nicely with those who want to store their data in separate database tables.

You can use Accountify to restrict access to certain content or features of your web site. Creating an invitation-only and/or manual-account-approval web site is straightforward with Accountify.

Although Accountify does not currently provide built-in support for limiting access to content based on subscription payments, that can easily be implemented "on top of" the features provided by Accountify.

What Are The License Terms?

Accountify is available under a dual license:.

1. Accountify can be used under the GNU General Public License, version 3.0, OR the MIT License. The MIT license is very permissive and is probably what you want. The GPL is provided as an option for those who are already relying on it.

What Are The Requirements?

To use Accountify, you must have a web site on a server with the following features. All of these features are standard on well-run web hosts:

1. A PHP-enabled website (PHP 4 or 5). Accountify is based on PHP. There are ways to achieve similar results with other dynamic web programming languages. For example, visit MSDN for more information about built-in login support in Microsoft's ASP.NET.

2. A MySQL database, or another SQL-capable database such as PostgreSQL. Most PHP hosts provide MySQL.

3. The Pear::DB library, which allows PHP to work with many different databases in a portable and secure way. Many Linux distributions, including Red Hat Enterprise Linux 4, include Pear::DB as standard equipment. You can test this easily with the following tiny test page:


<?php
require 'DB.php';
echo("If you see this, DB.php is correctly installed.");
?>

Put this code in a page called test.php, on your web server (not on your hard drive). If you see the message "If you see this, DB.php is correctly installed," then your web host has Pear::DB installed. If not, ask your web host to install the Pear::DB package, which is standard equipment on most well-run sites today. If they refuse, find a better PHP host! Or, if you really want to, rewrite all of my functions beginning with "db" to use MySQL directly. But trust me, you really don't want to do that. Just find a competent host instead. Good web hosting is cheap these days.

4. Optional: support for the GD graphics library, including Freetype text output, in your build of PHP. This allows Accountify to generate a CAPTCHA image to make sure your users are really people, rather than automated programs generating thousands of unwanted accounts. Accountify also provides an audio CAPTCHA.

You can test for this feature with the following simple PHP test page. Save this as test.php and access it on your web site:

<?php
phpinfo();
?>

Scroll down to the gd heading. If you have such a heading, and it includes the lines "GD Support: Enabled" and "FreeType Support: Enabled," then you are ready to go. If not, ask your web host to enable these features. If they refuse, get a better web host! Or, if you insist, set the captcha option to false in login_config.php.

If You Administer Your Own Linux Server (If You Are root)

(Psst! If your web host is a little confused about what you need, you might send them this section so they can do it for you.)

Under Red Hat Enterprise Linux and many other distributions, you may need to install some optional packages to make all of this work. It's not hard and the built-in package manager does the job for you. Under Red Hat Enterprise 5, you want these packages (package names for other Linux distributions are often similar though this cannot be guaranteed):

mysql-server
php-pear
php-gd
php-pear-DB
php-mysql

Use the command yum install packagename to install a package. Be sure to start the MySQL server (service mysqld start) and restart the web server (service httpd restart) after installing missing packages. If you are installing the MySQL server for the first time you will need to set a root password for it. The yum command will tell you how to do that. Then you'll need to create a database for an individual user. You do that like this:

mysql -u root -p

Answer the password prompt

Then, at the mysql> prompt:

create database MYDATABASE;
grant all privileges on MYDATABASE.* to USER@localhost identified by 'PASSWORD';

You should substitute your own database name, username and password. For security reasons, the password should not be the same as a Unix account password. Write down the database name, username and password you used; you will need them again when you edit login_config.php.

If you are compiling things by hand, you are definitely trying much too hard and creating maintenance hassles for yourself. So don't do that. Use packages instead.

Recent Changes and Upgrade Instructions

Accountify 2.02

A fromAddress parameter in login_config.php is now supported. This allows verification emails to be sent "from" a custom email address, preventing user confusion. If your verification emails are coming from an address your customers do not recognize, use this feature to fix the problem. Only reasonable "From:" addresses for domains hosted on the server are likely to work well.

Also, an eye-catching yellow background is now used by default when presenting error messages during the account creation and editing process. Formerly users complained that the error messages were too easy to miss. You can change this as you see fit by editing login.css.

Only the login_defaults.php, login.php and login.css files have changed. There is also an example of the new option, commented out, at the end of login_config.php (please do not unzip directly over your existing installation as this will crush your settings).

Accountify 2.01

Ensures that when an account is deleted or closed, any sessions already in progress are logged out. Only the login.php file has changed.

Accountify 2.0

Too many improvements to count! Accountify 2.0 made major improvements in the areas of customizability, invitation management and a great deal more. Most files have changed in accountify 2.0.

How to Upgrade

To upgrade, follow these steps:

1. Replace your copy of login.php with the current one— don't crush your copy of login_config.php, you can keep your settings although you will probably wish to add new options. See the changelog in login.php for details. Also look carefully at the new login_config.php to see if there are new configuration options you might wish to set explicitly in your own copy, instead of accepting the built-in defaults.

2. Copy the new file login_defaults.php to your Accountify folder.

3. If you have customized the look and feel of Accountify by making changes in the chrome folder, don't crush those either. However, check out the latest chrome folder for recently modified files. In version 2.0 many files have undergone major changes, so you'll need to copy over quite a few files. Check the modification times on the new chrome files. chrome/admin_tools.php and chrome/settings.php have been changed quite a bit and many new files have been added.

Version 1.1 added a "remember me on this computer" checkbox; if you are not simply using my HTML and CSS, then you'll need to add that part of the latest chrome/login_prompt.php into your own. It's very easy— just look for "remember me on this computer" in that file and copy and paste the appropriate HTML.

4. Visit the new version of the setup.php script with your browser, as described below. setup.php is used for both first-time setup and upgrades and will automatically add any newer fields that may be missing from your database tables. If you don't do this many new features simply will not work!

Installing Accountify

OK, you're sold on the concept. How do you install Accountify on your own site? Very easily! But first, make sure you have met the requirements mentioned earlier, particularly PHP (4 or 5), MySQL and Pear::DB.

Installation Steps

Got the prerequisites out of the way? Good! Let's install!

Just follow these steps:

1. click here to download accountify.zip.

2. Unpack accountify.zip to a folder on your hard drive.

3. Using Windows Notepad or another text editor of your choice, edit the file login_config.php and change the database-related settings to match your website. Your SQL database name, database username, and database password are all assigned by your web host, so please do not ask me for this information— if you don't know, check your web host's help pages or contact their technical support.

Your database type (the dbtype setting) will probably be mysql, but other databases are also popular. Your host can tell you whether you have MySQL or another type of database. Note that while I have used Pear::DB for maximum portability from one database to another, I have only tested this code personally with MySQL. Incompatibilities may exist.

4. Change the adminemail, adminusername and adminpassword settings in login_config.php to match your email address, preferred username and preferred password! This ensures that when you log into your website, you get administrative powers. This allows you to carry out important functions such as blocking users by email address or email domain, counting the users in the system, purging users who have been idle for six months, and removing accounts that have never been verified. Please choose a secure, unique password for the admin account!

5. Decide whether you want usernames (user IDs) on your site. On sites where users never "see" each other, logging in with an email address and a password is perfectly sufficient and gives the user less to remember. But on a site where users interact by sending email or posting messages, usernames are a good idea. That's because you can display a username to a second user without compromising the first user's privacy. If you decide to require usernames, change the usernames setting from false to true. It's OK to change your mind later— users will then be required to pick a username on their next logon.

6. Decide whether to personally approve each account aplication. If you prefer to review every account application before it "goes live" on the site, set the accountApproval option to true. Once you do this, users who have just verified their accounts will receive an email message explaining that their account won't go live until it is approved. And you, the administrator, will receive an email message inviting you to log on and approve the account. When you approve or deny an application, the user receives one more email message letting them know. You can edit the subject lines and full text of these emails in login_config.php. Take care not to break the PHP code, though— every quoted line of text except the last one in the message should end in a . to concatenate it with the next. Confused? Don't worry— just follow the example of what is already there.

7. New in 2.0: decide whether to let users change their email address. Set the changeEmail option to true or false accordingly. If you allow usernames it almost always makes sense to leave this option set to true. If you do not allow usernames you might want to set it to false to avoid confusion between users (although your PHP code, which identifies users by $_SESSION['id'], should never become confused).

8. New in 2.0: do you want to run an "invitation-only" site? An invitation-only site is a site that users cannot join unless they are invited by an existing user who has invitations to offer, or by the administrator (that's you). Invitation-only sites do not offer a "create account" link. You can configure your site to be invitation-only by setting the invitationOnly option to true. Note that, by default, only the administrator can give out invitations. You can change that by setting the initialInvitations option to a number other than zero, and/or by giving out invitations via the admin page. Using the admin page, you can give out any number of invitations to specific users or to all users at once. Users who have invitations to give out will see an "invite more people" link when they logged in.

Since the "invite more people" feature requires quite a bit of space on the page, Accountify generates a separate page for it. By default, this page won't have your site's unique style. You can easily change that by editing chrome/invite_users.php. If you don't know PHP, don't worry— just pay attention to the HTML and leave the PHP code alone.

Whether or not your site is strictly invitation-only, if you take advantage of invitations at all as a way to promote and manage your site you may wish to change the invitationLandingPage option. This indicates the page where a newly invited user should "land" after accepting their invitation. This page must feature an Accountify login prompt.

By default, this is set to SITE_BASE, which automatically expands to your site's home page. That's a good choice for most sites. But if it is not what you want, be sure to change this setting.

9. Accountify sends email messages to users in several situations: when they first create their accounts (and must verify them), when accounts require administrator approval (this message goes to the administrator), when users are invited to join the site, when users forget their passwords and need to change them, and when an account has been approved or rejected.

By default, Accountify will send these messages "from" whatever the default "From:" address is on your web server. This can be confusing. Fortunately, beginning in Accountify 2.02, you can fix this by setting the fromAddress option in login_config.php to any email address (although email addresses in domains not residing on the same company's mail servers are unlikely to work well).

For example, you can set this option as follows:

fromAddress = "Our Staff <staff@example.com>"

Keep in mind that options in login_config.php are separated by commas. I have no trailing comma here because this is the last option in my copy of login_config.php, but if you choose to list the options in another order, don't forget the comma.
The email messages sent in these situations are very generic, because I don't know much about your site! So I strongly recommend that you also edit the verificationBody, resetBody, approvalBody, approvedBody and deniedBody settings in login_config.php to better reflect what your site is about. That way users won't just delete the email. Make sure you keep VERIFICATION_URL, RESET_URL and similar capitalized macro names— these are automatically replaced with the links that users must follow to complete an action.
Prior to version 2.0, Accountify required email verification to change an email address or close an account. Beginning in version 2.0, such actions simply require that the user reenter their password. This is much less tedious for everyone. Of course, we still offer an "I forgot my password" feature that allows the password to be reset by email.
10. Decide whether you want to use Accountify's built-in PHP session manager. While PHP's standard support for sessions does work, it is insecure on shared hosts, and less efficient than the use of a database. Accountify's built-in session manager takes advantage of Pear::DB and MySQL. If you don't want this, you can disable it by setting builtInSessions to false in login_config.php. In most cases, this is only worthwhile for high-traffic sites that wish to use session managers like memcached. If you don't understand the issue, I strongly recommend that you leave this setting alone.

10. Upload the accountify folder to your website, complete with your edited version of login_config.php. Upload the entire accountify folder so that it appears just below your web site's document root. Note that if you prefer another location, you will need to adjust the cssUrl setting in login_config.php.

11. Visit the setup.php page once to create the database tables:


http://www.example.com/accountify/setup.php

Although the table-creation code is harmless to run more than once, it's not a bad idea to remove setup.php after the table has been successfully created. If the table creation code generates an error message, it is likely that your database settings are incorrect in login_config.php. You should fix those before continuing.

12. Modify your pages to load Accountify at the beginning, include the appropriate CSS, and display the login prompt, as described later in this article.

13. Take advantage of Accountify in your own code, as described later in this article.

How To Use Accountify

PHP provides us with a built-in mechanism for handling "sessions." That is, we can easily keep track of a user's choices during a particular visit to a website. But PHP does not have a built-in system for handling accounts. So most PHP developers wind up reinventing it, writing PHP and MySQL code to store account information in a database.

That's a shame, because PHP's sessions are convenient and require very little programmer effort. If only there were a similar solution for accounts...

That's where Accountify comes in handy.

Adding Accounts With Accountify

Your first step is to add the following code at the very beginning of each page, assuming that the page is in the same folder with Accountify. If your page is in a different folder, and it probably will be, adjust your require command accordingly with a relative path or an absolute file system path (not a URL) that is appropriate for your system:

<?php
  # The above line must be THE FIRST LINE in the ENTIRE file

  # The path that follows will work if you have installed
  # Accountify's 'accountify' folder in your document root
  # (where your site's home page lives)
  require $_SERVER['DOCUMENT_ROOT'] . '/accountify/login.php';
?>

You can, of course, install Accountify in a different location, in which case your require command will be different. If you don't understand this, install Accountify in the suggested location.

If you have installed the accountify folder in the suggested location, you will also find it easy to bring in Accountify's style sheet in the head element of your pages:

<link
  href="/accountify/chrome/login.css"
  rel="stylesheet" type="text/css"/>

If you have placed Accountify somewhere else, you will need to alter the URL above accordingly.

The require command above fetches information from the user's account and automatically folds it into the $_SESSION array... much like PHP's normal session support. In fact, since Accountify is built on top of PHP sessions, you'll still get session data storage for users who are not logged in. And if the user has logged into an account, their session is automatically reloaded from a MySQL database. Cookies expire, but accounts are forever.

Note that if you have existing session-based code that sets $_SESSION['email'], this is incompatible with Accountify. You'll need to let Accountify manage the following session fields exclusively (but yes, you may read these fields and take advantage of the information):

email
realname
password
user
closed
id
admin
approved
login_invitations
login_invitedby

Any other session variables you may freely change, and Accountify will helpfully store them with the rest of the user's account information. Please do not prefix session variables with login_ as this prefix is reserved for future expansion of Accountify.

Displaying the Login Prompt

Isn't that a little too good to be true? Well... just a little. We also need to display the login prompt at some point in the page so that users can create accounts, log in, change their settings, log out, and so on.

We can do all that with just a little bit more code. First, in the head element of the page, you'll need a link element to bring in the CSS code that provides a reasonable "look" for the login prompts. The href I've used here assumes that Accountify has been unpacked at the top level of the website. If not, adjust the href accordingly:


<link href="/accountify/chrome/login.css" rel="stylesheet" type="text/css">

Customizing the Chrome

My own version of login.css is very basic. And the text of my logon prompts might not be what you want. Are you stuck? Of course not! Just edit the files in the chrome subfolder to suit your needs. If you see PHP code you don't understand, leave that alone and change only the surrounding text. This is the right away to "localize" Accountify for your own site, and for other languages as well. In future versions of Accountify, I'll try to minimize the need to rewrite your customized chrome files whenever practical.

For quick results, start by editing login.css. Also note that page_head.php and page_tail.php are used to open and close all "free-standing" pages generated by Accountify, such as the account creation pages, the account editing pages, the administration pages and the invitation page. A few edits here and to the CSS file can quickly marry Accountify to the "look and feel" of your site.

Once you have the style sheet, all you have to do is insert the Accountify login prompt at the appropriate point in your page:


... At the point in your page where you want the login prompt to appear
<?php
# Display the login-related prompts etc
$login->prompt();
?>

See the file test.php in the accountify folder for a more complete example of the above.

By default, the Accountify style sheet displays the logon prompt with the "float: right" style. As a result, the logon prompt appears "inset" at the right-hand side of the page. This is a flexible and effective layout that matches the behavior of many popular sites. It also allows for the fact that the size of the "logon prompt" varies as the user creates an account, logs on, edits their password, and performs other tasks within Accountify.

Storing Information With Accountify

So far, so good! But how do we store useful information in these accounts? We have two choices: storing it just as we would with PHP's session support, and using a separate database table.

Accountify extends PHP's built-in session support, storing the information permanently with the account. So anything you store in the $_SESSION array will automatically be saved in the user's account. This is a powerful and flexible way of saving information and doesn't require any SQL knowledge on your part.

For example, if you have just used a select element called color to fetch the user's favorite color, you can save it to their account with one line of PHP code:


$_SESSION['color'] = $_POST['color'];

Each time the user logs in, this data will be restored. So you can still retrieve $_SESSION['color'] many logins from now, even if the user logs in from a completely different computer.

Limitations of $_SESSION

If you make heavy use of this approach you may eventually run into database limitations or ruin the performance of your site. That's because Accountify uses a single SQL "BLOB" object to store your data (under MySQL, Accountify uses MEDIUMBLOB to avoid a severe size limit).

Although using the BLOB type typically allows close to 1MB of user data to be stored and retrieved, in practice unpacking 1MB of user data every time a user accesses one of your pages is very slow. So I strongly advise you to use separate database tables to store bulky data, rather than keeping it in $_SESSION. Use $_SESSION['id'] as your database key, and fetch only the data you need for a particular purpose.

If you just want to keep a few facts about each user around, Accountify's support for permanently storing $_SESSION variables is a great solution. But if you have larger amounts of data to keep, you should use database tables of your own to store it.

"What if I have session variables that I don't want kept permanently?"

Accountify does not attempt to permanently retain any session variable that begins with tmp_. For instance, if you store a value in $SESSION['tmp_color'], that value will stick around for the current session but will be gone when the user logs out.

Storing Information in Separate Database Tables

The $_SESSION approach is highly convenient. But it does does have disadvantages. Since the data is "serialized" to a single SQL "blob" field, you can't easily write a SQL database query that returns all users who like the color blue. And there is also the fact that storing huge amounts of data where it must be fetched on every single page request can slow down your site, as well as possibly bumping into database limitations.

Is there an alternative? Sure! Instead of storing everything in $_SESSION, you can use $_SESSION['id'] as a key in your own database tables. Since Accountify guarantees that every user will have a distinct id field, you can easily use it as a field value in Pear::DB SQL queries like this one:


$db->query("INSERT INTO myinfo (id, color) VALUES (?, ?)",
  array($_SESSION['id'], $_POST['color']));

And then you can fetch the IDs of all of the users who prefer blue with a query like this:

$db->getAll("SELECT FROM myinfo (id) WHERE color = blue");

"But what does an ID look like in my database? What is the appropriate SQL type?"

CHAR(16) is the right choice for an Accountify ID.

Fetching Users By Id

This solution identifies the users who like the color blue, but it doesn't tell you much about them. What are their real names, usernames (user IDs) and email addresses? That information "belongs" to Accountify. So it's not immediately obvious how we access it from our own code.

Fetching that information for the currently logged-in user is straightforward:


Real Name: <?php echo $_SESSION['realname']?><br>
Email: <?php echo $_SESSION['email']?><br>
Username: <?php echo $_SESSION['user']?><br>
ID: <?php echo $_SESSION['id']?><br>

But how do we fetch this information about a different user? Accountify provides the loadUserData function for this purpose:


Users Who Like Blue
<ul>
<?php
$users = $db->getAll("SELECT FROM myinfo (id) WHERE color = blue");
foreach ($users as $id) {
  $data = $login->loadUserData[$id];
  echo("<li>" . $data['user'] . "</li>\n");
}
?>
</ul>

Identifying The Admin

Accountify knows who the administrative user is (that is, who you are). It would be nice if you could take advantage of this information to display certain options of your own only to the administrator. Beginning in Accountify 1.2, this is easy to do. Just check $_SESSION['admin'], like this:

if ($_SESSION['admin']) {
# Put your admin-only code here
}

When A Page Shouldn't Display The Login Prompt

What if a particular page should not display the login prompt? That's OK— just require login.php at the top but leave out the CSS and $login->prompt calls. That page can still access the user's account information as long as the user has already logged in on a separate page.

Mandatory Logins

What if logging in should be mandatory for a particular page... before the user is allowed to access the good stuff? That's easy to implement with a little PHP code:


<?php
  if ($_SESSION['id']) {
?>
... The good stuff goes here. Note that we are still
inside the "if" even though we're back in HTML-land.
You can do anything you like here, including using
'require' to bring in another file:
<?php require '/outside/document/root/privatepage.php';?>
<?php
  } else {
?>
... A message to the user telling them that they
must log in first goes here. Again, we're back
in HTML-land, but we're still inside the "else"
<?php
    # Now we close the else clause
  }
?>

Granting Invitations From Your Own Code

Accountify provides tools to grant invitations on its admin page. But what if you want to give out invitations to a user in a special situation that I have not anticipated? No problem. You can give out more invitations to the current user with code like this:

<?php
# Grant 5 invitations to the logged-in user
$login->grantInvitations(5);
?>

You can also check $_SESSION['login_invitations'] to find out how many invitations the user currently has. Note, however, that you may not directly change login_invitations. Assigning a new value to this session variable will not have the intended effect. Call $login->grantInvitations instead.

Reacting to Invitations

Are you coding up a social networking site? Then you're probably keeping track of "friend" relationships. And when Jane sends an invitation to Bob and Bob decides to act on it, you probably want to make Jane and Bob "friends" right off the bat.

Is there a way to detect this situation? Yes. Just look at $_SESSION['login_invitedby'] to find the id of the user who invited the current user. Then, just insert the appropriate record in your friends table if it does not already exist.

For efficiency, you might want to check whether this is the user's first session before wasting resources on a database query. Here I am assuming that you have already created a SQL table called friends and that you are using Pear::DB for your database operations:

<?php
if (strlen($_SESSION['id']) &&
  ($_SESSION['sessions'] == 1))
{
  # This is the first session
  $db->query("INSERT INTO friends (f1, f2) VALUES (?, ?)",
    array($_SESSION['login_invitedby'], $_SESSION['id']));
}
?>

This technique is efficient but will only work if the above code is guaranteed to be a part of the page where the user first logs in. An alternative is to test a session variable of your own, such as $_SESSION['friendedback'], to see whether the user who invited the current user has been "friended back" yet.

Requesting Extra Information At Account Creation Time

At account creation time, Accountify obtains the user's email address, real name, and username (if you enable usernames). Once an account is live on Accountify, you have these pieces of information. But what if there is additional information you want the user to provide at the time their account is created?

You could wait until the user's account is approved and then ask for their street address, date of birth and shoe size. But this is an awkward process. And if the user's answers imply that they shouldn't have an account anyway, then the email verification process is a waste of the user's time.

While we're at it, although it's not terribly inconvenient to use one button for "Account Settings" (i.e. full name and email address, which are managed by Accountify) and a separate button for your application-specific settings, it might be nice to add additional tabs to the Accountify "Settings" area instead.

Accountify's extraCategories and extraCategoryOrder options allow you to solve both problems by registering your own PHP functions to generate the HTML and validate the data entered for extra account setup pages, which can also appear as tabs when the user edits his or her settings later. You can specify whether a tab should appear at setup time only, at editing time only, or in both situations.

The extraCategories and extraCategoryOrder options typically look like this. This example defines two category pages, one for the user's birthdate and the second for the user's location. Later I'll preesnt full code for the first of these two:

  'extraCategoryOrder' => array(
    'birthdate', 'toc'
  ),  
  'extraCategories' => array(
    'birthdate' => array(
      'label' => 'Birthdate',
      'html' => 'myBirthdateHtml',
      'validator' => 'myBirthdateValidator',
      'writer' => 'myBirthdateWriter',
      'eraser' => 'myBirthdateEraser',
      'php' => 'mycode.php',
    ),
  'toc' => array(
           'label' => 'Terms and Conditions',
           'html' => 'myTocHtml',
           'validator' => 'myTocValidator',
           'php' => 'mycode.php',
           'setup' => true,
           'editable' => false
         )
  ),

First, let's quickly dispense with extraCategoryOrder. This is a simple array which indicates the order in which your category pages should be shown at account setup (or as tabs when the user edits his or her settings later). The values given for this array must match the keys given for the extraCategories option. Note that you can temporarily disable a category by leaving it out of this list. You do not need to list the built-in category pages, nor is it possible to change their order here.

extraCategories is an associative array with one key for each additional category (aka "wizard page" or "settings tab") you wish to add. Key names should contain letters, underscores and digits only. Labels are not restricted in this way, so this shouldn't cramp your style.

The value corresponding to each key is another associative array, with up to eight keys: label, php, html, validator, writer, eraser, setup and editable.

The value corresponding to label is the label that will appear on the tab associated with this category.

The value corresponding to the setup key, which defaults to true if setup is not present, indicates whether that this category should appear during account creation. The value corresponding to the editable key, which also defaults to true, means that this category should appear as a tab when the user clicks the "Settings" button to edit their existing account settings. By default, then, a category appears in both situations. Most of the time, this is what you want.

The values for the html, validator, writer and eraser keys are the names of callback functions written by you. These functions output the HTML form elements for the category page, validate the user's data entry, save the user's data for this category page to the database of your choice, and delete the user's data for this category page from your database. The first two, html and validator, are required. The third and fourth, writer and eraser, is optional.

I'll discuss these callback functions in detail in the next section. But first, you need to know where to put them! And that brings us to the php key.

The last key shown in the example is php. The value corresponding to this key is the name of a PHP file written by you which defines your callback functions. If you do not specify an absolute path, Accountify assumes this file is located in your accountify folder (that is, wherever login.php is). You can easily pick things up from a subfolder called custom by using a path like custom/myfile.php. If you do specify an absolute path, remember that this is a filesystem path, not a URL. However, you can specify paths relative to your main web site folder by beginning them with $_SERVER['DOCUMENT_ROOT'].

"Where do I do global initialization?"

You can initialize global variables, open database handles and so forth at the beginning of your PHP file. However, your PHP file will be evaluated before the global $login object is created. So if you want services provided by that object at initialization time, do your initialization in your callbacks if they have not been called previously. Use a global $init variable, check whether it is set already, and if not, call an initialization function from your callbacks.

"Can I use the same file for multiple categories of callbacks?"

It's OK to list the same file for multiple categories. Your code will only be loaded once. There is no guarantee that the PHP files for various categories will be loaded in a specific order, so use your own require_once statements to bring in any shared code. Don't output anything to the browser at the global level— you only write directly to the browser from inside your html callback function.

Now you know where to put the callback functions. But exactly what do these functions look like? What parameters do they receive and what should they return to Accountify? That's the subject of the next three sections.
"Can I use a method of an object as a callback function?"

Fans of object-oriented programming may prefer to use methods of objects rather than callback functions. Yes, you can do this with Accountify. Instead of a function name in quotes, pass an array consisting of a reference to the object and the name of the method:

  'html' => array(&$myObject, 'htmlGeneratorMethod'),

Note the use of the & symbol. Without this symbol the object would be copied (at least in PHP 4), which is not what you want.

This syntax is also supported by numerous built-in PHP functions that expect callback functions, notably session_set_write_handler.

The html callback

The html callback is responsible for generating the HTML for the category in question. This is a simple enough job: fetch the existing data (if the category is reappearing as a settings tab) and then output appropriate HTML for the form elements the user needs to see.

You can use whatever HTML you wish. Just keep in mind that the form element is already open at this point in the page— you are only responsible for presenting the elements that gather the information you want. If you're confused, see the simple example below. Form fields with names beginning with login_ are reserved for use by Accountify. Do not use names beginning with login_ for your own form fields. Simple names like birthmonth will work just fine.

If the form elements have been successfully written, your html callback should return an array consisting only of one element: the string loginDataValid. If they have not been successfully written, your html callback should return an array consisting of two elements: the string loginDataCancel and an error message to be shown to the user. The account creation or editing process will fail, and the error message will be displayed. Your error message may contain HTML markup.

The html callback receives three parameters: $category, $id and $hints. $category is the name (not the label) of the category page to be shown. $id is the Accountify ID of the current user, or false when a new account is being created. And $hints is an optional associative array of extra information provided by your validator callback. In most cases, you won't need to worry about $hints.

$category allows you to implement more than one category page in a single callback. And $id allows you to fetch the user's current settings and display them as the default values of the form fields.

"But that's not enough... or is it?"

At first glance, something might seem to be missing here. When the user fills out tab A, switches to tab B, and returns to tab A, how do you redisplay the user's previous entries on tab A? After all, since the user hasn't clicked "OK" for the entire account creation or editing process, they haven't been written to the database yet.

The answer is that Accountify automatically remembers what the user's form fields looked like (the contents of the $_POST associative array) at the time the user switches to another tab (or clicks "Next" or "Previous" during account creation). These values are automatically returned to the $_POST array before your html callback is invoked again, and any potentially conflicting $_POST variables are removed.

What it boils down to is that to redisplay a category page, you only need to worry about two things:

1. The current contents of the user's account, which should be your initial defaults. This is only an issue when editing an existing account, not at account creation time.

2. The contents of $_POST, which represent the user's latest data entry attempt. These should override the initial defaults. Note that these must be properly escaped when you redisplay them!

Please note that Accountify uses form fields with names that begin with login_ for internal purposes. You must not use the login_ prefix in your own form fields. Short names like age and shoesize will work just fine.

Sample code for an html callback follows. You can simply use $_SESSION for storage if you want, but to demonstrate the capabilities of category page callbacks more fully, I will use a separate database in these examples. Here I assume that you have created a separate SQL table as follows:

CREATE TABLE birthdates (
  id CHAR(16),
  birthdate INTEGER,
  PRIMARY KEY(id),
  INDEX(birthdate)
)

You'll note that this table allows us to easily look up users whose ages are in a particular range with SQL queries like this one:

SELECT FROM birthdates WHERE birthdate BETWEEN 19700101 AND 19791231

That's a capability that can't be implemented efficiently with $_SESSION. So there are good reasons to use separate databases rather than $_SESSION, at least in certain applications. For instance, on a dating site, users typically express a preference to meet someone in a certain age range. This sort of thing is best done with a query like the one above.

In the following html callback, as well as the other callback examples that follow, I assume that you are storing the user's birthdate information in the above table. I also assume that $mydb is a valid Pear::DB database connection object opened elsewhere in your code (but you don't have to use Pear::DB— you can use anything you want).

function myBirthdateHtml($category, $id, $hints)
{
  global $dbFailure;
  if ($dbFailure) {
    return array('loginDataCancel', 'Database not available');
  }
  # If $id is present, then the user is
  # editing existing choices. Fetch
  # the user's current choices to use them as
  # the defaults, saving the user a lot of time.
  global $mydb;
  $birthDate = false;
  if (strlen($id)) {
    $query = 'SELECT birthdate FROM birthdates WHERE id = ?';
    $qresult = $mydb->getAll($query, array($id));
    if (PEAR::isError($qresult)) {
      return array('loginDataCancel',
        'Database error, try again later');
    }
    # Pull out the first result. If there are no result rows, this
    # user simply hasn't set her birthdate yet, which is fine.
    if (count($qresult)) {
      $birthDate = $qresult[0][0];
    }
  }

  # Parse the previously stored birthdate back into
  # separate form fields with PHP's handy sscanf function
  $birthYear = '';
  $birthMonth = '';
  $birthDay = '';

  if (strlen($birthDate)) {
    list($birthYear, $birthMonth, $birthDay) =
      sscanf($birthDate, "%04d%02d%02d");
  }

  # If the user has already entered something,
  # let that override what is currently stored.
  # This allows the user to move from tab to tab,
  # or correct errors in one or more fields, without
  # starting from scratch. (Accountify automatically
  # repopulates $_POST correctly when you switch category pages.)

  if (isset($_POST['birthyear']) && (strlen($_POST['birthyear']))) {
    $birthYear = $_POST['birthyear'];
  }

  # Take a hint from the validator to fix the birth year
  if (isset($hints['birthyear'])) {
    $birthYear = $hints['birthyear'];
  }

  if (isset($_POST['birthmonth']) && (strlen($_POST['birthmonth']))) {
    $birthMonth = $_POST['birthmonth'];
  }
  if (isset($_POST['birthday']) && (strlen($_POST['birthday']))) {
    $birthDay = $_POST['birthday'];
  }

  # ALWAYS PREVENT XSS ATTACKS by escaping the
  # user's prior input correctly, whether or not
  # < and > are considered valid input!

  $birthYear = htmlspecialchars($birthYear);
  $birthMonth = htmlspecialchars($birthMonth);
  $birthDay = htmlspecialchars($birthDay);
?>
<p>
Users must be at least 18 years of age. When were you born?
</p>
<p>
<input name="birthmonth" size="2" maxlength="2"
  value="<?php echo $birthMonth?>"
/> Month
<input name="birthday" size="2" maxlength="2"
  value="<?php echo $birthDay?>"
/> Day
<input name="birthyear" size="4" maxlength="4"
  value="<?php echo $birthYear?>"
/> Year
</p>
<?php
  return array('loginDataValid');
}

As you can see, this function generates the HTML elements for the birthdate-related information the webmaster wishes to prompt for, in addition to the information that Accountify normally prompts for at account creation time.

But now that the user can see the form, we still have to do something with the information they type in. That's where the validator callback comes into play.

The validator callback

The validator callback is responsible for verifying that the user's submitted information is reasonable and, if it is reasonable, returning that information to Accountify so that it can be stored. If the user enters something unreasonable (such as text instead of a date), or something you do not allow (such as a birthdate that implies they are less than 18 years old), the validator callback returns an error message to be shown to the user. The validator callback can also "flunk" the entire account application, which may be necessary if the user is underage or has refused to accept the terms and conditions of use for the site.

In addition, the callback function can indicate that the returned data is valid, but the changes must not be saved until the user reenters their Accountify password. This is useful when editing sensitive information such as contact email addresses. Instead of reproducing a password prompt on every category page that requires sensitive information, Accountify prompts once for the password after all of the category pages have been successfully validated, which is less tedious for the user. Note that your validator callback may be called more than once before the writer callback is invoked, if indeed the user ever chooses to click "OK" or "Finish" and complete the operation at all.

The validator callback picks up the values submitted by the user simply by lookig at the $_POST associative array in the way all PHP programmers are already familiar with. So the real question is how to return them correctly if they are valid, and how to complain correctly if they are not.

If The Data Is Good

If your validator is satisfied with the submitted data, it should return a value like this:

return array('loginDataValid',
  array("birthdate" -> "19950927"));

That is, the return value is an array containing two elements. The first element is the string loginDataValid, which indicates that the data the user has entered is good. The second is an associative array containing the data.

If A Password Is Required

If the data is valid, but the user should be required to reenter their password before it is saved, the validator should return loginDataValidWithPassword:

return array('loginDataValidWithPassword',
  array("birthdate" -> "19950927"));

If at least one category returns loginDataValidWithPassword, Accountify will prompt the user to reenter their password before saving the data. Note that this occurs only when existing account settings are being edited, not when the account is created for the first time. At account creation time, loginDataValid and loginDataValidWithPassword are equivalent.

Tip: check whether the user's new settings actually differ from their previous settings before returning loginDataValidWithPassword. If no actual changes have been made, return loginDataValid to avoid irritating the user with an unnecessary password prompt.

If The Category Should Not Appear Again

Normally, at account creation time, users can move back and forth between category pages with the "Previous" and "Next" buttons. In some cases, however— such as a "Terms and Conditions" page or a CAPTCHA test— there is nothing to be gained by forcing the user to look at that page again once they have successfully moved past it.

In such cases, return array('loginDataValidDone') instead of array('loginDataValid'). If you return loginDataValidDone for a particular category page, Accountify will not display that page again during the same account creation process. This saves the user the trouble of scrolling through terms and conditions or reading a CAPTCHA code more than once. You can see a working example in the "terms and conditions" category page provided in mycode.php.

If The Data Is Bad

If the user's data entry is not satisfactory, you have two options: you can require the user to make changes, or you can "flunk" (cancel) the user's account creation attempt completely. The second option only makes sense when a new account is being created. You'll know that is the case when the $id argument to the validator is false.

To force the user to make changes, just return an array consisting of the string loginDataInvalid, an error message, and an optional associative array of hints to be provided to your html callback for the user's next try. Your error message may contain HTML code. Here's a simple example:

#The user left a field blank or entered something strange
return array('loginDataInvalid',
  'Your birth month must be a number between 1 and 12.');

A more complex example involving hints might look like this:

#The user entered a 2-digit year
if ($year < 100) {
  if ($year < 50) {
    $year += 2000;
  } else {
    $year += 1900;
  }
  return array('loginDataInvalid',
    'Years must be four digits. Check whether the provided ' .
    'suggestion matches what you had in mind.',
    array('year' => $year));

Your category page will then be displayed again, with the error message appearing before your HTML elements. This gives the user a chance to correct their mistakes. Any information you return in a hints array will be passed to the HTML callback.

To cancel the account creation attempt completely, just return an array consist of the string loginDataCancel and an error message. This error message may also contain HTML code. Here is an example:

return array('loginDataCancel',
  'Sorry, you must be over 18 to access this site.');

"Can't I just save the data to my database table now?"

Please don't do that yet! The user may have satisfied your validator, but Accountify needs to make sure that all of the other data entered for this user is valid before saving anything. Also, if the account is brand new, then it doesn't have an ID yet. And in some cases you might not even need your own database table. Read on to see why.

Here is a complete example of a working validator callback for a category page that deals with birthdates. Note that the validator callback receives the category name and the Accountify ID of the user as parameters. The first is useful if you choose to implement more than one validator in a single function. The second parameter will be false unless the user is editing settings for an existing account. If the user is editing settings for an existing account, you may wish to access their current settings as part of the validation process for new settings. You may also acess the $_SESSION associative array for this purpose.

function myBirthdateValidator($category, $id)
{
  # Deliberately out of range default,
  # make sure they answer with something valid
  $month = 0;
  if (isset($_POST['birthmonth'])) {
    $month = $_POST['birthmonth'];
  }
  $day = 0;
  if (isset($_POST['birthday'])) {
    $day = $_POST['birthday'];
  }
  $year = 0;
  if (isset($_POST['birthyear'])) {
    $year = $_POST['birthyear'];
  }
  if (($month < 1) || ($month > 12)) {
    # Out of range, let them try again.
    return array('loginDataInvalid',
      "The birth month must be between 1 and 12.");
  }
  if (($day < 1) || ($day > 31)) {
    return array('loginDataInvalid',
      "The birth day must be between 1 and 31.");
  }
  if ($year < 1800) {
    if ($year < 100) {
      # Pass back an intelligent guess at the real birth year,
      # as a hint to the html callback's next pass
      if ($year < 50) {
        $year += 2000;
      } else {
        $year += 1900;
      }
      return array('loginDataInvalid',
        "The birth year must be a four-digit number greater than 1800. Did you mean $year?",
        array('birthyear' => $year));
    }
    return array('loginDataInvalid',
      "The birth year must be a four-digit number greater than 1800.");
  }
  $birthdate = sprintf("%04d%02d%02d", $year, $month, $day);
  $now = getdate(time());
  $now['year'] -= 18;  
  $thendate = sprintf("%04d%02d%02d",
    $now['year'], $now['mon'], $now['mday']);
  if ($birthdate > $thendate) {
    # Cancel the account creation, the user is
    # not old enough.
    return array('loginDataCancel',
      "Sorry, you must be at least 18 years old.");
  }
  # Return success
  return array('loginDataValid',
    array("birthdate" => $birthdate));
}

Saving Your Data: The writer Callback

Unlike the first two callbacks, the writer callback is optional. If you do not provide a writer callback, then Accountify will automatically merge your validated data into the $_SESSION associative array when creating or editing an account. That is, if the associative array returned by your validator callback contains a birthdate key, then $_SESSION['birthdate'] will be set for you. In Accountify, $_SESSION variables are permanently saved as part of the account, so this is a "cheap and easy" way to store your data.

But many users will want to store the data differently. For instance, you may wish to find that data again quickly via an indexed SQL table. Also, although up to 1MB can be handled reliably, large amounts of data usually do not belong in $_SESSION. The writer callback gives you the option of storing your data in an alternative way.

If you do provide a writer callback, your data will be passed to it as an associative array once Accountify is satisfied that the new account is ready to be created (or that it is time to update an existing account). Typicallly this means that the user has clicked "Finish" in the account creation process or clicked "OK" after editing their settings on one or more "Settings" tabs.

Returning from the writer callback

The writer callback always returns an array. That array consists of a result code, sometimes followed by an optional error message to be displayed to the user.

When the Writer Succeeds

If your writer callback succeeds, it should return an array as follows:

return array('loginDataValid');

When The Writer Fails

If the writer callback fails due to a situation that can be corrected, such as a request for a username or email address that has been claimed by another user in the time since the validator call was made, the writer callback should return an array as follows:

return array('loginDataInvalid',
  "Sorry, that username has been claimed by another user. Please " .
  "try another.");

The relevant category page or tab will then be redisplayed to the user, with the error message displayed at the beginning.

Note that the error message may contain HTML elements.

If the writer callback fails due to a fatal error that cannot be corrected, such as a database error, the writer callback should return an array as follows:

return array('loginDataCancel',
  "A database error occurred. We apologize for the inconvenience. " .
  "Please try the operation at another time.");

Here is an example of a writer callback that stores birthdate data in the birthdates table we saw earlier:

function myBirthdateWriter($category, $id, $data) {
  global $mydb;
  # Try to store the user's birthdate for the first time
  $qresult = $mydb->query("INSERT INTO birthdates (id, birthdate) " .
      "VALUES (?, ?)", array($id, $data['birthdate']));
  if (PEAR::isError($qresult)) {
    # Error. But is it simply because we already
    # have a record for this user?
    if ($qresult->getMessage() == 'DB Error: already exists') {
      # OK, then just update the existing record
      $qresult =
        $mydb->query("UPDATE birthdates " .
          "SET birthdate = ? " .
          "WHERE id = ?",
          array($data['birthdate'], $id));
    }
  }
  if (PEAR::isError($qresult)) {
    # An error at this point is a real problem
    return array(
      'loginDataCancel',
      'Database failure, sorry');
  }
  return array('loginDataValid');
}

The eraser callback

We've displayed the form, validated the user's birthdate, and written it to a custom database table of our own. So we're done... right?

Welll... not quite! We still need a way to "clean up" and erase the data from our custom database table when an account is deleted or closed. And that's where the eraser callback comes in handy.

Note that the eraser callback is optional. If you don't have a writer callback, you probably don't need an eraser callback either, because your validated data is automatically stored in $_SESSION, which Accountify will automatically clean up when an account is deleted. If you do have a custom writer callback, you will almost certainly want an eraser callback as well.

"Is eraser like a destructor in object-oriented programming?"

Yes, it is very similar in concept. However, the eraser function deals with permanent storage, not with a temporary object in memory. The eraser callback might not get called for months or years, while many login sessions (and objects, if you use objects) come and go. So we gave it a different name to avoid potential confusion.

The eraser callback is called in two types of situations:

1. When the user's account is about to be deleted completely. This can happen right away if a writer callback for a later category fails at account creation time, after your own writer callback has already succeeded. It also happens when the administrator purges idle accounts.

2. When an account is closed. In the current version of Accountify, closing an account does not completely remove it from the database; it simply sets the closed flag to Y and blocks any future logins. Although the current release of Accountify does not have a "restore account" feature, that feature may appear in the future. So you might not want to completely erase your data from custom tables when an account is closed.

For this reason, the eraser callback receives two parameters: $id and $delete. The $id parameter contains the Accountify ID of the user, and you should use it to locate your user's data in your custom database and remove it. The $delete parameter is true only if the account is being completely deleted. If this parameter is false and you are interested in preserving data for closed accounts, you might choose to return without actually erasing anything. And in both cases, depending on your site's policies, you might wish to copy the user's data to a "former users" database of some sort. That's up to you. (Of course, you should never retain personal customer information without permission.)

Note that your eraser callback may be called twice: once to close but not actually delete an account, and later to actually delete it. So your eraser callback must tolerate a situation in which you have already removed the user's data. This is usually not a problem since databases do not regard a DELETE statement that doesn't match any records as an error.

Your eraser callback must not assume that $_SESSION contains data relevant to the account in question. Only $id should be used to identify the relevant user. If you are using $_SESSION as your only storage, you don't need an eraser callback anyway. Accountify will remove that data automatically when an account is permanently deleted.

There are no second chances for eraser callbacks, so there is no return value.

Here is an example of an eraser callback for the birthdates table used in the previous examples:

function myBirthdateEraser($category, $id, $deleting)
{
  global $mydb;  
  if (!$deleting) {
    # Keep this around for un-close operations
    return;
  }
  $mydb->query("DELETE FROM birthdates WHERE id = ?",
    array($id));
}

More Cleanup: When Accounts Die

Your application might need to clean up its own database tables when an account dies. It's easy to do this if your data is associated with a custom category page as described above. But what if you're not using extra Accountify category pages, or simply need to clean up something that isn't associated with one?

There's a solution for you too: general-purpose eraser callbacks. Just add your own eraser callback function to login_config.php, like this:

  'eraserCallbacks' => array(
    # You may list multiple comma-separated associative arrays  
    array(
      'php' => 'mycode.php',
      'function' => 'myEraserCallback'
    )
  ),

Note that you must specify both the PHP file that contains the source code and the name of the eraser callback function itself.

Your callback function will receive two parameters, $id and $deleted. The first parameter is the Accountify ID of the user. The second parameter is true only if the account is to be completely deleted. If the account is simply being closed— which allows for the possibility of reopening it later— this parameter will be false. You may prefer to do nothing when this parameter is false, or you might wish to address the fact that the account has been closed in some way. Just keep in mind that you won't receive a special callback if and when the account is reopened. You could, however, take appropriate action when you see the user logged in again.

Admin Tools: Every Janitor Needs A Mop

Once an account-driven website has become popular, it's certain that the database will become cluttered with unverified accounts, accounts that have been forgotten, and accounts created by spammers. It's a pain, but that's life!

Fortunately Accountify provides simple administrative tools to deal with all this. To use them, follow these steps:

1. Log in with your email address (you set this as the adminEmail setting when you edited login_config.php). If you haven't already created an account for that address, just do it now as any other user would. This is safe because the link to verify the account is sent only to the administrative email address.

2. Click on the "Admin" link that appears for you (and only you!) beneath "Logged On As."

3. Take advantage of the tools provided! In particular, you can delete accounts that have not been used for six months, delete accounts that have gone unverified for a week (the users never responded to the initial email), count the accounts in the system, or block users by email address or email domain. Note that the blocking feature only kicks in on their next logon. Also, take care not to block your administrative email address! If you do, you'll need to clean that up manually in the loginblock SQL table.

You can also grant one, several, or all users the power to issue invitations to other users. Note that this is not the same thing as inviting new users yourself. You do that using the "Invite!" button just as any other user would. Of course, as the administrator, you never run out of invitations.

Finally, you can close and reopen accounts via the "Close Accounts" tab. When you close accounts you have the option of deleting them permanently, which would rule out using "reopen" later. You also have the option of blocking the email addresses involved so that even if you do delete the accounts permanently, the user cannot apply for a new account with the same initial email address.

When you are done working with the account administration features, just click "Exit Administration Page" to return to the site.

How Accountify Works

Interested in the nuts and bolts of Accountify? You can find a detailed discussion of this subject in my WWW FAQ article how do I add accounts to my web site?

Conclusion

Many programmers want account features for their websites. Unlike ASP.NET, PHP does not have a built-in login system. Accountify fills this gap by providing a flexible way of handling user logins, allowing users to create, verify and manage their accounts and "upgrading" PHP's familiar session system to serve as part of an account system as well. Accountify also provides the administrative features needed to effectively manage a system with many accounts.

Follow us on Twitter | Contact Us

Copyright 1994-2012 Boutell.Com, Inc. All Rights Reserved.