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