* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Dominic Sayers nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @package ezUser
* @author Dominic Sayers
* @copyright 2009 Dominic Sayers
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
* @link http://code.google.com/p/ezuser/
* @version 0.20 - Password reset now working
*/
// The quality of this code has been improved greatly by using PHPLint
// Copyright (c) 2009 Umberto Salsi
// This is free software; see the license for copying conditions.
// More info: http://www.icosaedro.it/phplint/
/*.
require_module 'dom';
require_module 'pcre';
require_module 'hash';
require_module 'session';
.*/
/* Comment out profiling statements if not needed
function ezUser_time() {list($usec, $sec) = explode(" ",microtime()); return ((float)$usec + (float)$sec);}
$ezUser_profile = array();
$ezUser_profile['REQUEST_TIME'] = $_SERVER['REQUEST_TIME'];
$ezUser_profile['received'] = ezUser_time();
*/
/**
* Common utility functions for ezUser
*
* @package ezUser
*/
interface I_ezUser_common {
const HASH_FUNCTION = 'SHA256';
public static /*.string.*/ function getFileContents(/*.string.*/ $filename, /*.int.*/ $flags = 0, /*.object.*/ $context = NULL, /*.int.*/ $offset = -1, /*.int.*/ $maxLen = -1);
public static /*.string.*/ function makeID();
public static /*.string.*/ function makeUniqueKey(/*.string.*/ $id);
public static /*.boolean.*/ function is_email(/*.string.*/ $email, $checkDNS = false);
}
/**
* Common utility functions for ezUser
*
* @package ezUser
*/
abstract class ezUser_common implements I_ezUser_common {
/**
* Return file contents as a string. Fail silently if the file can't be opened.
*
* The parameters are the same as the built-in PHP function {@link http://www.php.net/file_get_contents file_get_contents}
*/
public static /*.string.*/ function getFileContents(/*.string.*/ $filename, /*.int.*/ $flags = 0, /*.object.*/ $context = NULL, /*.int.*/ $offset = -1, /*.int.*/ $maxLen = -1) {
$contents = @file_get_contents($filename, $flags, $context, $offset, $maxLen);
if ($contents === false) $contents = '';
return $contents;
}
/**
* Make a unique ID based on the current date and time
*/
public static /*.string.*/ function makeID() {
list($usec, $sec) = explode(" ", (string) microtime());
return base_convert($sec, 10, 36) . base_convert((string) mt_rand(0, 35), 10, 36) . str_pad(base_convert(($usec * 1000000), 10, 36), 4, '_', STR_PAD_LEFT);
}
/**
* Make a unique hash key from a string (usually an ID)
*/
public static /*.string.*/ function makeUniqueKey(/*.string.*/ $id) {
return hash(self::HASH_FUNCTION, $_SERVER['REQUEST_TIME'] . $id);
}
/**
* Check that an email address conforms to RFC5322 and other RFCs
*
* @param boolean $checkDNS If true then a DNS check for A and MX records will be made
*/
public static /*.boolean.*/ function is_email(/*.string.*/ $email, $checkDNS = false) {
// Check that $email is a valid address. Read the following RFCs to understand the constraints:
// (http://tools.ietf.org/html/rfc5322)
// (http://tools.ietf.org/html/rfc3696)
// (http://tools.ietf.org/html/rfc5321)
// (http://tools.ietf.org/html/rfc4291#section-2.2)
// (http://tools.ietf.org/html/rfc1123#section-2.1)
// the upper limit on address lengths should normally be considered to be 256
// (http://www.rfc-editor.org/errata_search.php?rfc=3696)
// NB I think John Klensin is misreading RFC 5321 and the the limit should actually be 254
// However, I will stick to the published number until it is changed.
//
// The maximum total length of a reverse-path or forward-path is 256
// characters (including the punctuation and element separators)
// (http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3)
$emailLength = strlen($email);
if ($emailLength > 256) return false; // Too long
// Contemporary email addresses consist of a "local part" separated from
// a "domain part" (a fully-qualified domain name) by an at-sign ("@").
// (http://tools.ietf.org/html/rfc3696#section-3)
$atIndex = strrpos($email,'@');
if ($atIndex === false) return false; // No at-sign
if ($atIndex === 0) return false; // No local part
if ($atIndex === $emailLength) return false; // No domain part
// Sanitize comments
// - remove nested comments, quotes and dots in comments
// - remove parentheses and dots from quoted strings
$braceDepth = 0;
$inQuote = false;
$escapeThisChar = false;
for ($i = 0; $i < $emailLength; ++$i) {
$char = $email[$i];
$replaceChar = false;
if ($char === '\\') {
$escapeThisChar = !$escapeThisChar; // Escape the next character?
} else {
switch ($char) {
case '(':
if ($escapeThisChar) {
$replaceChar = true;
} else {
if ($inQuote) {
$replaceChar = true;
} else {
if ($braceDepth++ > 0) $replaceChar = true; // Increment brace depth
}
}
break;
case ')':
if ($escapeThisChar) {
$replaceChar = true;
} else {
if ($inQuote) {
$replaceChar = true;
} else {
if (--$braceDepth > 0) $replaceChar = true; // Decrement brace depth
if ($braceDepth < 0) $braceDepth = 0;
}
}
break;
case '"':
if ($escapeThisChar) {
$replaceChar = true;
} else {
if ($braceDepth === 0) {
$inQuote = !$inQuote; // Are we inside a quoted string?
} else {
$replaceChar = true;
}
}
break;
case '.': // Dots don't help us either
if ($escapeThisChar) {
$replaceChar = true;
} else {
if ($braceDepth > 0) $replaceChar = true;
}
break;
default:
}
$escapeThisChar = false;
// if ($replaceChar) $email[$i] = 'x'; // Replace the offending character with something harmless
// revision 1.12: Line above replaced because PHPLint doesn't like that syntax
if ($replaceChar) $email = (string) substr_replace($email, 'x', $i, 1); // Replace the offending character with something harmless
}
}
$localPart = substr($email, 0, $atIndex);
$domain = substr($email, $atIndex + 1);
$FWS = "(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t]+)|(?:[ \\t]+(?:(?:\\r\\n)[ \\t]+)*))"; // Folding white space
// Let's check the local part for RFC compliance...
//
// local-part = dot-atom / quoted-string / obs-local-part
// obs-local-part = word *("." word)
// (http://tools.ietf.org/html/rfc5322#section-3.4.1)
//
// Problem: need to distinguish between "first.last" and "first"."last"
// (i.e. one element or two). And I suck at regexes.
$dotArray = /*. (array[int]string) .*/ preg_split('/\\.(?=(?:[^\\"]*\\"[^\\"]*\\")*(?![^\\"]*\\"))/m', $localPart);
$partLength = 0;
foreach ($dotArray as $element) {
// Remove any leading or trailing FWS
$element = preg_replace("/^$FWS|$FWS\$/", '', $element);
// Then we need to remove all valid comments (i.e. those at the start or end of the element
$elementLength = strlen($element);
if ($element[0] === '(') {
$indexBrace = strpos($element, ')');
if ($indexBrace !== false) {
if (preg_match('/(? 0) {
return false; // Illegal characters in comment
}
$element = substr($element, $indexBrace + 1, $elementLength - $indexBrace - 1);
$elementLength = strlen($element);
}
}
if ($element[$elementLength - 1] === ')') {
$indexBrace = strrpos($element, '(');
if ($indexBrace !== false) {
if (preg_match('/(? 0) {
return false; // Illegal characters in comment
}
$element = substr($element, 0, $indexBrace);
$elementLength = strlen($element);
}
}
// Remove any leading or trailing FWS around the element (inside any comments)
$element = preg_replace("/^$FWS|$FWS\$/", '', $element);
// What's left counts towards the maximum length for this part
if ($partLength > 0) $partLength++; // for the dot
$partLength += strlen($element);
// Each dot-delimited component can be an atom or a quoted string
// (because of the obs-local-part provision)
if (preg_match('/^"(?:.)*"$/s', $element) > 0) {
// Quoted-string tests:
//
// Remove any FWS
$element = preg_replace("/(? 0) return false; // ", CR, LF and NUL must be escaped, "" is too short
} else {
// Unquoted string tests:
//
// Period (".") may...appear, but may not be used to start or end the
// local part, nor may two or more consecutive periods appear.
// (http://tools.ietf.org/html/rfc3696#section-3)
//
// A zero-length element implies a period at the beginning or end of the
// local part, or two periods together. Either way it's not allowed.
if ($element === '') return false; // Dots in wrong place
// Any ASCII graphic (printing) character other than the
// at-sign ("@"), backslash, double quote, comma, or square brackets may
// appear without quoting. If any of that list of excluded characters
// are to appear, they must be quoted
// (http://tools.ietf.org/html/rfc3696#section-3)
//
// Any excluded characters? i.e. 0x00-0x20, (, ), <, >, [, ], :, ;, @, \, comma, period, "
if (preg_match('/[\\x00-\\x20\\(\\)<>\\[\\]:;@\\\\,\\."]/', $element) > 0) return false; // These characters must be in a quoted string
}
}
if ($partLength > 64) return false; // Local part must be 64 characters or less
// Now let's check the domain part...
// The domain name can also be replaced by an IP address in square brackets
// (http://tools.ietf.org/html/rfc3696#section-3)
// (http://tools.ietf.org/html/rfc5321#section-4.1.3)
// (http://tools.ietf.org/html/rfc4291#section-2.2)
if (preg_match('/^\\[(.)+]$/', $domain) === 1) {
// It's an address-literal
$addressLiteral = substr($domain, 1, strlen($domain) - 2);
$matchesIP = array();
// Extract IPv4 part from the end of the address-literal (if there is one)
if (preg_match('/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/', $addressLiteral, $matchesIP) > 0) {
$index = strrpos($addressLiteral, $matchesIP[0]);
if ($index === 0) {
// Nothing there except a valid IPv4 address, so...
return true;
} else {
// Assume it's an attempt at a mixed address (IPv6 + IPv4)
if ($addressLiteral[$index - 1] !== ':') return false; // Character preceding IPv4 address must be ':'
if (substr($addressLiteral, 0, 5) !== 'IPv6:') return false; // RFC5321 section 4.1.3
$IPv6 = substr($addressLiteral, 5, ($index ===7) ? 2 : $index - 6);
$groupMax = 6;
}
} else {
// It must be an attempt at pure IPv6
if (substr($addressLiteral, 0, 5) !== 'IPv6:') return false; // RFC5321 section 4.1.3
$IPv6 = substr($addressLiteral, 5);
$groupMax = 8;
}
$groupCount = preg_match_all('/^[0-9a-fA-F]{0,4}|\\:[0-9a-fA-F]{0,4}|(.)/', $IPv6, $matchesIP);
$index = strpos($IPv6,'::');
if ($index === false) {
// We need exactly the right number of groups
if ($groupCount !== $groupMax) return false; // RFC5321 section 4.1.3
} else {
if ($index !== strrpos($IPv6,'::')) return false; // More than one '::'
$groupMax = ($index === 0 || $index === (strlen($IPv6) - 2)) ? $groupMax : $groupMax - 1;
if ($groupCount > $groupMax) return false; // Too many IPv6 groups in address
}
// Check for unmatched characters
array_multisort($matchesIP[1], SORT_DESC);
if ($matchesIP[1][0] !== '') return false; // Illegal characters in address
// It's a valid IPv6 address, so...
return true;
} else {
// It's a domain name...
// The syntax of a legal Internet host name was specified in RFC-952
// One aspect of host name syntax is hereby changed: the
// restriction on the first character is relaxed to allow either a
// letter or a digit.
// (http://tools.ietf.org/html/rfc1123#section-2.1)
//
// NB RFC 1123 updates RFC 1035, but this is not currently apparent from reading RFC 1035.
//
// Most common applications, including email and the Web, will generally not
// permit...escaped strings
// (http://tools.ietf.org/html/rfc3696#section-2)
//
// the better strategy has now become to make the "at least one period" test,
// to verify LDH conformance (including verification that the apparent TLD name
// is not all-numeric)
// (http://tools.ietf.org/html/rfc3696#section-2)
//
// Characters outside the set of alphabetic characters, digits, and hyphen MUST NOT appear in domain name
// labels for SMTP clients or servers
// (http://tools.ietf.org/html/rfc5321#section-4.1.2)
//
// RFC5321 precludes the use of a trailing dot in a domain name for SMTP purposes
// (http://tools.ietf.org/html/rfc5321#section-4.1.2)
$dotArray = /*. (array[int]string) .*/ preg_split('/\\.(?=(?:[^\\"]*\\"[^\\"]*\\")*(?![^\\"]*\\"))/m', $domain);
$partLength = 0;
$element = ''; // Since we use $element after the foreach loop let's make sure it has a value
if (count($dotArray) === 1) return false; // Mail host can't be a TLD
foreach ($dotArray as $element) {
// Remove any leading or trailing FWS
$element = preg_replace("/^$FWS|$FWS\$/", '', $element);
// Then we need to remove all valid comments (i.e. those at the start or end of the element
$elementLength = strlen($element);
if ($element[0] === '(') {
$indexBrace = strpos($element, ')');
if ($indexBrace !== false) {
if (preg_match('/(? 0) {
return false; // Illegal characters in comment
}
$element = substr($element, $indexBrace + 1, $elementLength - $indexBrace - 1);
$elementLength = strlen($element);
}
}
if ($element[$elementLength - 1] === ')') {
$indexBrace = strrpos($element, '(');
if ($indexBrace !== false) {
if (preg_match('/(? 0) {
return false; // Illegal characters in comment
}
$element = substr($element, 0, $indexBrace);
$elementLength = strlen($element);
}
}
// Remove any leading or trailing FWS around the element (inside any comments)
$element = preg_replace("/^$FWS|$FWS\$/", '', $element);
// What's left counts towards the maximum length for this part
if ($partLength > 0) $partLength++; // for the dot
$partLength += strlen($element);
// The DNS defines domain name syntax very generally -- a
// string of labels each containing up to 63 8-bit octets,
// separated by dots, and with a maximum total of 255
// octets.
// (http://tools.ietf.org/html/rfc1123#section-6.1.3.5)
if ($elementLength > 63) return false; // Label must be 63 characters or less
// Each dot-delimited component must be atext
// A zero-length element implies a period at the beginning or end of the
// local part, or two periods together. Either way it's not allowed.
if ($elementLength === 0) return false; // Dots in wrong place
// Any ASCII graphic (printing) character other than the
// at-sign ("@"), backslash, double quote, comma, or square brackets may
// appear without quoting. If any of that list of excluded characters
// are to appear, they must be quoted
// (http://tools.ietf.org/html/rfc3696#section-3)
//
// If the hyphen is used, it is not permitted to appear at
// either the beginning or end of a label.
// (http://tools.ietf.org/html/rfc3696#section-2)
//
// Any excluded characters? i.e. 0x00-0x20, (, ), <, >, [, ], :, ;, @, \, comma, period, "
if (preg_match('/[\\x00-\\x20\\(\\)<>\\[\\]:;@\\\\,\\."]|^-|-$/', $element) > 0) {
return false;
}
}
if ($partLength > 255) return false; // Local part must be 64 characters or less
if (preg_match('/^[0-9]+$/', $element) > 0) return false; // TLD can't be all-numeric
// Check DNS?
if ($checkDNS && function_exists('checkdnsrr')) {
if (!(checkdnsrr($domain, 'A') || checkdnsrr($domain, 'MX'))) {
return false; // Domain doesn't actually exist
}
}
}
// Eliminate all other factors, and the one which remains must be the truth.
// (Sherlock Holmes, The Sign of Four)
return true;
}
}
// End of class ezUser_common
/**
* Password reset handling for ezUser
*
* @package ezUser
*/
interface I_ezUser_reset {
// Methods may be commented out to reduce the attack surface when they are
// not required. Uncomment them if you need them.
// public /*.string.*/ function id();
public /*.string.*/ function resetKey();
// public /*.DateTime.*/ function expires();
public /*.string.*/ function data();
public /*.void.*/ function initialize();
public /*.void.*/ function setID(/*.string.*/ $id);
public /*.void.*/ function setData(/*.string.*/ $data);
}
/**
* Password reset handling for ezUser
*
* @package ezUser
*/
class ezUser_reset extends ezUser_common implements I_ezUser_reset {
private /*.string.*/ $id;
private /*.string.*/ $resetKey;
private /*.DateTime.*/ $expires;
// Methods may be commented out to reduce the attack surface when they are
// not required. Uncomment them if you need them.
// public /*.string.*/ function id() {return $this->id;}
public /*.string.*/ function resetKey() {return $this->resetKey;}
// public /*.DateTime.*/ function expires() {return $this->expires;}
public /*.string.*/ function data() {return serialize(array($this->id, $this->resetKey, serialize($this->expires)));}
public /*.void.*/ function initialize() {
$date = new DateTime();
$date->modify('+1 day');
$this->resetKey = self::makeUniqueKey($this->id);
$this->expires = $date;
}
public /*.void.*/ function setID(/*.string.*/ $id) {
$this->id = $id;
$this->initialize();
}
public /*.void.*/ function setData(/*.string.*/ $data) {
list($this->id, $this->resetKey, $expiresString) = /*.(array[int]string).*/ unserialize($data);
$this->expires = /*.(DateTime).*/ unserialize($expiresString);
}
}
// End of class ezUser_reset
/**
* This class encapsulates all the functions needed for an app to interact
* with a user. It has no knowledge of how user information is persisted.
*
* @package ezUser
*/
interface I_ezUser_base extends I_ezUser_common {
const PACKAGE = 'ezuser',
// REST interface actions
ACTION = 'action',
ACTION_ABOUT = 'about',
ACTION_ACCOUNT = 'account',
ACTION_ACCOUNTFORM = 'accountform',
ACTION_ACCOUNTWIZARD = 'accountwizard',
ACTION_BODY = 'body',
ACTION_CANCEL = 'cancel',
ACTION_CONTAINER = 'container',
ACTION_DASHBOARD = 'dashboard',
ACTION_JAVASCRIPT = 'js',
ACTION_MAIN = 'controlpanel',
ACTION_RESEND = 'resend', // Resend verification email
ACTION_RESET = 'reset', // Process password reset link
ACTION_RESETPASSWORD = 'resetpassword', // Initiate password reset processing
ACTION_RESETREQUEST = 'resetrequest', // Request password reset form
ACTION_RESULTFORM = 'resultform',
ACTION_RESULTTEXT = 'resulttext',
ACTION_SIGNIN = 'signin',
ACTION_SIGNOUT = 'signout',
ACTION_SOURCECODE = 'code',
ACTION_STATUSTEXT = 'statustext',
ACTION_STYLESHEET = 'css',
ACTION_VALIDATE = 'validate', // Validate registration form details
ACTION_VERIFY = 'verify', // Verify verification email
// Keys for the user data array members
TAGNAME_CONFIRM = 'confirm',
TAGNAME_DATA = 'data',
TAGNAME_EMAIL = 'email',
TAGNAME_FIRSTNAME = 'firstname',
TAGNAME_FULLNAME = 'fullname',
TAGNAME_ID = 'id',
TAGNAME_LASTNAME = 'lastname',
TAGNAME_NEWUSER = 'newuser',
TAGNAME_PASSWORD = 'password',
TAGNAME_REMEMBERME = 'rememberme',
TAGNAME_RESETKEY = 'resetkey',
TAGNAME_RESETDATA = 'resetdata',
TAGNAME_SAVEDPASSWORD = 'usesavedpassword',
TAGNAME_STATUS = 'status',
TAGNAME_STAYSIGNEDIN = 'staysignedin',
TAGNAME_USER = 'user',
TAGNAME_USERNAME = 'username',
TAGNAME_VERIFICATIONKEY = 'verificationkey',
TAGNAME_VERBOSE = 'verbose',
TAGNAME_WIZARD = 'wizard',
// Registration status codes
STATUS_UNKNOWN = 0,
STATUS_PENDING = 1,
STATUS_CONFIRMED = 2,
STATUS_INACTIVE = 3,
// Authentication result codes
RESULT_UNDEFINED = 0,
RESULT_SUCCESS = 1,
RESULT_UNKNOWNUSER = 2,
RESULT_BADPASSWORD = 3,
RESULT_UNKNOWNACTION = 4,
RESULT_NOACTION = 5,
RESULT_NOSESSION = 6,
RESULT_NOSESSIONCOOKIES = 7,
RESULT_STORAGEERR = 8,
RESULT_EMAILERR = 9,
// Validation result codes
RESULT_VALIDATED = 32,
RESULT_NOID = 33,
RESULT_NOUSERNAME = 34,
RESULT_NOEMAIL = 35,
RESULT_EMAILFORMATERR = 36,
RESULT_NOPASSWORD = 37,
RESULT_NULLPASSWORD = 38,
RESULT_STATUSNAN = 39,
RESULT_RESULTNAN = 40,
RESULT_CONFIGNOTARRAY = 41,
RESULT_USERNAMEEXISTS = 42,
RESULT_EMAILEXISTS = 43,
RESULT_NOTSIGNEDIN = 44,
RESULT_INCOMPLETE = 45,
// Miscellaneous constants
DELIMITER_SPACE = ' ',
STRING_TRUE = 'true',
STRING_FALSE = 'false';
public /*.int.*/ function authenticate($passwordHash = '');
public /*.string.*/ function username();
public /*.string.*/ function firstName();
public /*.string.*/ function lastName();
public /*.string.*/ function fullName();
public /*.string.*/ function email();
public /*.int.*/ function status();
public /*.boolean.*/ function authenticated();
public /*.void.*/ function setFirstName(/*.string.*/ $name);
public /*.void.*/ function setLastName(/*.string.*/ $name);
}
// End of interface I_ezUser_base
/**
* This class encapsulates all the functions needed for an app to interact
* with a user. It has no knowledge of how user information is persisted.
*
* @package ezUser
*/
class ezUser_base extends ezUser_common implements I_ezUser_base {
// User data
private $keys = array (
self::TAGNAME_USERNAME ,
self::TAGNAME_EMAIL ,
self::TAGNAME_ID ,
self::TAGNAME_PASSWORD ,
self::TAGNAME_STATUS ,
self::TAGNAME_FIRSTNAME ,
self::TAGNAME_LASTNAME ,
self::TAGNAME_FULLNAME ,
self::TAGNAME_VERIFICATIONKEY,
);
private $values = array (
self::TAGNAME_USERNAME => '',
self::TAGNAME_EMAIL => '',
self::TAGNAME_ID => '',
self::TAGNAME_PASSWORD => '',
self::TAGNAME_STATUS => '0',
self::TAGNAME_FIRSTNAME => '',
self::TAGNAME_LASTNAME => '',
self::TAGNAME_FULLNAME => '',
self::TAGNAME_VERIFICATIONKEY => ''
);
// State and derived data
private $authenticated = false; // User is signed in
private $usernameIsDefault = true; // username === firstName.lastName
private $isChanged = false; // Unsaved changes?
private $result = self::RESULT_UNDEFINED; // Result of any change operation
private $config = /*.(array[string]string).*/ array(); // Configuration settings
private $errors = /*.(array[string]string).*/ array(); // Validation errors
private $signOutActions = /*.(array[int]string).*/ array(); // Things to do on signing out
// ---------------------------------------------------------------------------
// Helper methods
// ---------------------------------------------------------------------------
private /*.boolean.*/ function setValue(/*.string.*/ $key, /*.string.*/ $value) {
if ($value !== $this->values[$key]) {
$this->values[$key] = $value;
$this->isChanged = true;
return true;
} else {
return false;
}
}
private /*.string.*/ function getValue(/*.string.*/ $key) {
$value = '';
if (!in_array($key, $this->keys)) return $value;
switch ($key) {
case self::TAGNAME_VERIFICATIONKEY:
if ((int) $this->getValue(self::TAGNAME_STATUS) === self::STATUS_PENDING) $value = $this->values[$key];
break;
case self::TAGNAME_ID:
if ($this->values[$key] === '') $this->setValue($key, self::makeID());
$value = $this->values[$key];
break;
default:
$value = $this->values[$key];
break;
}
return $value;
}
// ---------------------------------------------------------------------------
// Substantive methods
// ---------------------------------------------------------------------------
public /*.int.*/ function authenticate($passwordHash = '') {
if (empty($passwordHash)) {
// Sign out
$this->authenticated = false;
$result = self::RESULT_SUCCESS;
} else {
// Sign in
$sessionHash = hash(self::HASH_FUNCTION, session_id() . hash(self::HASH_FUNCTION, $_SERVER['REMOTE_ADDR'] . $this->values[self::TAGNAME_PASSWORD]));
$this->authenticated = ($passwordHash === $sessionHash);
$result = ($this->authenticated) ? self::RESULT_SUCCESS : self::RESULT_BADPASSWORD;
}
$this->result = $result;
return $result;
}
// ---------------------------------------------------------------------------
// "Get" methods
// ---------------------------------------------------------------------------
protected /*.string.*/ function data() {return serialize($this->values);}
protected /*.string.*/ function id() {return $this->getValue(self::TAGNAME_ID);}
public /*.string.*/ function username() {return $this->getValue(self::TAGNAME_USERNAME);}
protected /*.string.*/ function passwordHash() {return $this->getValue(self::TAGNAME_PASSWORD);}
public /*.string.*/ function firstName() {return $this->getValue(self::TAGNAME_FIRSTNAME);}
public /*.string.*/ function lastName() {return $this->getValue(self::TAGNAME_LASTNAME);}
public /*.string.*/ function fullName() {return $this->getValue(self::TAGNAME_FULLNAME);}
public /*.string.*/ function email() {return $this->getValue(self::TAGNAME_EMAIL);}
protected /*.string.*/ function verificationKey() {return $this->getValue(self::TAGNAME_VERIFICATIONKEY);}
public /*.int.*/ function status() {return (int) $this->getValue(self::TAGNAME_STATUS);}
public /*.boolean.*/ function authenticated() {return $this->authenticated;}
protected /*.int.*/ function result() {return $this->result;}
protected /*.array[string]string.*/ function config() {return $this->config;}
protected /*.array[string]string.*/ function errors() {return $this->errors;}
protected /*.string.*/ function signOutActions() {return implode(self::DELIMITER_SPACE, $this->signOutActions);}
protected /*.boolean.*/ function isChanged() {return $this->isChanged;}
protected /*.boolean.*/ function incomplete() {
return ( empty($this->values[self::TAGNAME_USERNAME]) ||
empty($this->values[self::TAGNAME_EMAIL]) ||
empty($this->values[self::TAGNAME_ID])
);
}
// ---------------------------------------------------------------------------
// Name manipulation
// ---------------------------------------------------------------------------
private /*.string.*/ function getDefaultUsername() {
$lastName = $this->values[self::TAGNAME_LASTNAME];
$firstName = $this->values[self::TAGNAME_FIRSTNAME];
$username = strtolower($firstName . $lastName);
$username = preg_replace('/[^0-9a-z_-]/', '', $username);
return $username;
}
private /*.void.*/ function setFullName() {
$firstName = $this->values[self::TAGNAME_FIRSTNAME];
$lastName = $this->values[self::TAGNAME_LASTNAME];
$separator = (empty($firstName) || empty($lastName)) ? '' : self::DELIMITER_SPACE;
$this->setValue(self::TAGNAME_FULLNAME, $firstName . $separator . $lastName);
if ($this->usernameIsDefault) {$this->setValue(self::TAGNAME_USERNAME, $this->getDefaultUsername());}
}
private /*.void.*/ function setNamePart(/*.string.*/ $key, /*.string.*/ $name) {
$name = trim($name);
if ($this->setValue($key, $name)) $this->setFullName();
}
// ---------------------------------------------------------------------------
// "Set" methods
// ---------------------------------------------------------------------------
protected /*.void.*/ function setData(/*.string.*/ $data) {
$this->values = /*.(array[string]string).*/ unserialize($data);
}
protected /*.void.*/ function clearErrors() {
$this->errors = /*.(array[string]string).*/ array();
}
protected /*.int.*/ function setStatus(/*.int.*/ $status) {
if (!is_numeric($status)) return self::RESULT_STATUSNAN;
// If we're setting this user to Pending then generate a verification key
if ($status === self::STATUS_PENDING && $this->status() !== self::STATUS_PENDING) {
// Use the ID to generate a verification key
$this->setValue(self::TAGNAME_VERIFICATIONKEY, self::makeUniqueKey($this->id()));
}
$this->setValue(self::TAGNAME_STATUS, (string) $status);
return self::RESULT_VALIDATED;
}
protected /*.int.*/ function setResult(/*.int.*/ $result) {
if (!is_numeric($result)) return self::RESULT_RESULTNAN;
$this->result = $result;
return self::RESULT_VALIDATED;
}
protected /*.int.*/ function setConfig(/*.array[string]string.*/ $config) {
if (!is_array($config)) return self::RESULT_CONFIGNOTARRAY;
$this->config = $config;
return self::RESULT_VALIDATED;
}
public /*.void.*/ function setFirstName(/*.string.*/ $name) {$this->setNamePart(self::TAGNAME_FIRSTNAME, $name);}
public /*.void.*/ function setLastName(/*.string.*/ $name) {$this->setNamePart(self::TAGNAME_LASTNAME, $name);}
protected /*.int.*/ function setUsername($name = '') {
$this->usernameIsDefault = empty($name);
if ($this->usernameIsDefault) $name = $this->getDefaultUsername();
if (empty($name)) return self::RESULT_NOUSERNAME;
$this->setValue(self::TAGNAME_USERNAME, $name);
return self::RESULT_VALIDATED;
}
protected /*.int.*/ function setEmail(/*.string.*/ $email) {
if (empty($email)) return self::RESULT_NOEMAIL;
if (!self::is_email($email)) {
$this->errors[self::TAGNAME_EMAIL] = $email;
return self::RESULT_EMAILFORMATERR;
}
$this->setValue(self::TAGNAME_EMAIL, $email);
return self::RESULT_VALIDATED;
}
protected /*.int.*/ function setPasswordHash(/*.string.*/ $passwordHash) {
if (empty($passwordHash)) return self::RESULT_NOPASSWORD;
if ($passwordHash === hash(self::HASH_FUNCTION, '')) return self::RESULT_NULLPASSWORD;
$this->setValue(self::TAGNAME_PASSWORD, $passwordHash);
return self::RESULT_VALIDATED;
}
protected /*.void.*/ function addSignOutAction(/*.string.*/ $action) {
if (!in_array($action, $this->signOutActions)) $this->signOutActions[] = $action;
}
protected /*.void.*/ function clearSignOutActions() {
$this->signOutActions = /*.(array[int]string).*/ array();
}
protected /*.void.*/ function clearChanges() {
$this->isChanged = false;
}
// ---------------------------------------------------------------------------
// Password reset handling
// ---------------------------------------------------------------------------
private $passwordResetFlag = false;
private /*.ezUser_reset.*/ $passwordReset;
public /*.boolean.*/ function hasPasswordReset() {return $this->passwordResetFlag;}
public /*.ezUser_reset.*/ function passwordReset($terminate = false) {
$passwordReset = new ezUser_reset();
if ($terminate) {
unset($this->passwordReset);
$this->passwordResetFlag = false;
return $passwordReset; // empty
} else {
$passwordReset->setID($this->id());
$this->passwordReset = $passwordReset;
$this->passwordResetFlag = true;
return $this->passwordReset;
}
}
}
// End of class ezUser_base
/**
* This class encapsulates all the functions needed to manage the collection
* of stored users. It interacts with the storage mechanism (e.g. database or
* XML file).
*
* @package ezUser
*/
interface I_ezUser_environment extends I_ezUser_base {
// Cookie names
const COOKIE_USERNAME = 'ezuser1',
COOKIE_PASSWORD = 'ezuser2',
COOKIE_AUTOSIGN = 'ezuser3',
// Storage locations
STORAGE = '.ezuser-data.php',
SETTINGS = '.ezuser-settings.php',
// Keys for the configuration settings
SETTINGS_ADMINEMAIL = 'adminEmail',
SETTINGS_PERSISTED = 'persisted',
SETTINGS_EMPTY = 'empty',
SETTINGS_ACCOUNTPAGE = 'accountPage',
SETTINGS_SECUREFOLDER = 'secureFolder',
// Miscellaneous constants
DELIMITER_EMAIL = '@';
public static /*.ezUser_base.*/ function getSessionObject($instance = self::PACKAGE);
public static /*.ezUser_base.*/ function save(/*.array[string]mixed.*/ $userData);
public static /*.ezUser_base.*/ function lookup($needle = '', $tagName = '');
}
/**
* This class encapsulates all the functions needed to manage the collection
* of stored users. It interacts with the storage mechanism (e.g. database or
* XML file).
*
* @package ezUser
*/
class ezUser_environment extends ezUser_base implements I_ezUser_environment {
// ---------------------------------------------------------------------------
// Configuration settings
// ---------------------------------------------------------------------------
protected static /*.string.*/ function getInstanceId($container = self::PACKAGE) {
return ($container === self::ACTION_MAIN || $container === self::PACKAGE) ? self::PACKAGE : self::PACKAGE . "-$container";
}
public static /*.ezUser_base.*/ function getSessionObject($instance = self::PACKAGE) {
$instanceId = self::getInstanceId($instance);
if (!array_key_exists($instanceId, $_SESSION)) $_SESSION[$instanceId] = new ezUser_base();
return /*.(ezUser_base).*/ $_SESSION[$instanceId];
}
protected static /*.void.*/ function setSessionObject(/*.ezUser_base.*/ $ezUser, $instance = self::PACKAGE) {
$instanceId = self::getInstanceId($instance);
$_SESSION[$instanceId] = $ezUser;
}
protected static /*.string.*/ function thisURL() {
$package = self::PACKAGE;
// Find out the URL of this script so we can call it later
$file = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? (string) str_replace("\\", '/' , __FILE__) : __FILE__;
return dirname(substr($file, strpos(strtolower($_SERVER['SCRIPT_FILENAME']), strtolower($_SERVER['SCRIPT_NAME'])))) . "/$package.php";
}
private static /*.array[string]string.*/ function loadConfig() {
$ezUser = self::getSessionObject();
$config = $ezUser->config();
$settingsFile = realpath(dirname(__FILE__) . "/" . self::SETTINGS);
// If configuration settings file doesn't exist then use default settings
if (($settingsFile === false) || !is_file($settingsFile)) {
$config[self::SETTINGS_EMPTY] = self::STRING_TRUE;
} else {
// Open the vessel
$storage = new DOMDocument();
$storage->load($settingsFile);
$nodeList = $storage->getElementsByTagName('settings')->item(0)->childNodes;
for ($i = 0; $i < $nodeList->length; $i++) {
$node = $nodeList->item($i);
if ($node->nodeType == XML_ELEMENT_NODE) {
$config[$node->nodeName] = $node->nodeValue;
}
}
}
$config[self::SETTINGS_PERSISTED] = self::STRING_TRUE;
$ezUser->setConfig($config);
return $config;
}
private static /*.array[string]string.*/ function getSettings() {
$ezUser = self::getSessionObject();
$config = $ezUser->config();
if (!is_array($config)) {$config = self::loadConfig();}
if (!array_key_exists(self::SETTINGS_PERSISTED, $config)) {$config = self::loadConfig();}
if ($config[self::SETTINGS_PERSISTED] !== self::STRING_TRUE) {$config = self::loadConfig();}
return $config;
}
protected static /*.string.*/ function getSetting(/*.string.*/ $setting) {
$config = self::getSettings();
$thisSetting = (array_key_exists($setting, $config)) ? $config[$setting] : '';
return $thisSetting;
}
// ---------------------------------------------------------------------------
// Helper methods
// ---------------------------------------------------------------------------
private static /*.DOMDocument.*/ function openStorage() {
// Connect to database or whatever our storage mechanism is in this version
// Where is the storage container?
$storage_file = dirname(__FILE__) . '/' . self::STORAGE;
// If storage container doesn't exist then create it
if (!is_file($storage_file)) {
$query = '?';
$html = <<
HTML;
$handle = @fopen($storage_file, 'wb');
if ($handle === false) exit(self::RESULT_STORAGEERR);
fwrite($handle, $html);
fclose($handle);
chmod($storage_file, 0600);
}
// Open the container for use
$storage = new DOMDocument();
$storage->load($storage_file);
return $storage;
}
// ---------------------------------------------------------------------------
private static /*.void.*/ function closeStorage(/*.DOMDocument.*/ $storage) {
$storage_file = dirname(__FILE__) . '/' . self::STORAGE;
for ($attempt = 0; $attempt < 3; $attempt++) {
$count = @$storage->save($storage_file);
if ((bool) $count) break;
sleep(1); // File may occasionally be locked by indexing/backups etc.
}
}
// ---------------------------------------------------------------------------
private static /*.DOMElement.*/ function findUser(/*.DOMDocument.*/ $storage, $needle = '', $tagName = '') {
if ($needle === '') return $storage->createElement(self::TAGNAME_USER);
if ($tagName === '') $tagName = ((bool) strpos($needle,self::DELIMITER_EMAIL)) ? self::TAGNAME_EMAIL : self::TAGNAME_USERNAME;
$nodeList = $storage->getElementsByTagName($tagName);
$found = false;
for ($i = 0; $i < $nodeList->length; $i++) {
$node = $nodeList->item($i);
$found = (strcasecmp($node->nodeValue, $needle) === 0);
if ($found) break;
}
if ($found && isset($node)) {
/*.object.*/ $userElement_PHPLint = $node->parentNode; // PHPLint-compliant typecasting (yawn)
$userElement = /*.(DOMElement).*/ $userElement_PHPLint;
return $userElement;
} else {
return $storage->createElement(self::TAGNAME_USER);
}
}
// ---------------------------------------------------------------------------
// Substantive methods
// ---------------------------------------------------------------------------
public static /*.ezUser_base.*/ function lookup($needle = '', $tagName = '') {
$ezUser = new ezUser_base();
if ($needle === '') return $ezUser;
if ($tagName === '' || $tagName === self::TAGNAME_USERNAME || $tagName === self::TAGNAME_EMAIL) {
$ezUser->setUsername($needle); // Will get overwritten if we successfully find the user in the database
}
$storage = self::openStorage();
$record = self::findUser($storage, $needle, $tagName);
if ($record->hasChildNodes()) {
$data = $record->getElementsByTagName(self::TAGNAME_DATA)->item(0)->nodeValue;
if (!empty($data)) $ezUser->setData($data);
$nodeList = $record->getElementsByTagName(self::TAGNAME_RESETDATA);
if ((bool) $nodeList->length) {
$data = $nodeList->item(0)->nodeValue;
if (!empty($data)) {
$passwordReset = $ezUser->passwordReset();
$passwordReset->setData($data);
}
}
}
return $ezUser;
}
// ---------------------------------------------------------------------------
private static /*.boolean.*/ function sendEmail($to = '', $subject = '', $message = '', $additional_headers = '') {
if ($to === '') return false; // Can't send to an empty address
if ($subject.$message === '') return false; // Can't send empty subject and message - that's just creepy
$from = self::getSetting(self::SETTINGS_ADMINEMAIL);
$from = ($from === '') ? 'webmaster' : $from;
// If there's no domain, then assume same as this host
if (strpos($from, self::DELIMITER_EMAIL) === false) {
$host = $_SERVER['HTTP_HOST'];
$domain = (substr_count($host, '.') > 1) ? substr($host, strpos($host, '.') + 1) : $host;
$from .= self::DELIMITER_EMAIL . $domain;
}
// Extra headers
$additional_headers .= "From: $from\r\n";
date_default_timezone_set(@date_default_timezone_get()); // E_STRICT needs this or it complains about the mail function
return @mail($to, $subject, $message, $additional_headers);
}
// ---------------------------------------------------------------------------
private static /*.int.*/ function is_duplicate(/*.string.*/ $username_or_email, /*.string.*/ $id) {
$resultCode = ((bool) strpos($username_or_email,self::DELIMITER_EMAIL)) ? self::RESULT_EMAILEXISTS : self::RESULT_USERNAMEEXISTS;
$ezUser = self::lookup($username_or_email);
return ($ezUser->status() === self::STATUS_UNKNOWN) || ($ezUser->id() === $id) ? self::RESULT_VALIDATED : $resultCode;
}
// ---------------------------------------------------------------------------
// Storage methods
// ---------------------------------------------------------------------------
private static /*.void.*/ function addElement (/*.DOMDocument.*/ $storage, /*.DOMElement.*/ $record, /*.string.*/ $tagName, /*.string.*/ $value) {
$record->appendChild($storage->createTextNode("\n\t\t")); // XML formatting
$record->appendChild($storage->createElement($tagName, $value));
}
private static /*.DOMElement.*/ function createRecord(/*.DOMDocument.*/ $storage, /*.ezUser_base.*/ $ezUser) {
$record = $storage->createElement(self::TAGNAME_USER);
self::addElement($storage, $record, self::TAGNAME_USERNAME, $ezUser->username()); // Add username
self::addElement($storage, $record, self::TAGNAME_EMAIL, $ezUser->email()); // Add email address
self::addElement($storage, $record, self::TAGNAME_ID, $ezUser->id()); // Add id
self::addElement($storage, $record, self::TAGNAME_DATA, $ezUser->data()); // Add data blob
// Add verification key if necessary
$verificationKey = $ezUser->verificationKey();
if (!empty($verificationKey)) {
self::addElement($storage, $record, self::TAGNAME_VERIFICATIONKEY, $verificationKey); // Add verification key
}
// Add password reset data if necessary
if ($ezUser->hasPasswordReset()) {
$passwordReset = $ezUser->passwordReset();
self::addElement($storage, $record, self::TAGNAME_RESETKEY, $passwordReset->resetKey()); // Add password reset key
self::addElement($storage, $record, self::TAGNAME_RESETDATA, $passwordReset->data()); // Add password reset data
}
self::addElement($storage, $record, 'updated', gmdate("Y-m-d H:i:s (T)")); // Note when the record was updated
$record->appendChild($storage->createTextNode("\n\t")); // XML formatting
return $record;
}
// ---------------------------------------------------------------------------
private static /*.int.*/ function add(/*.ezUser_base.*/ $ezUser) {
$storage = self::openStorage();
$record = self::createRecord($storage, $ezUser);
$users = $storage->getElementsByTagName('users')->item(0);
$users->appendChild($storage->createTextNode("\t")); // XML formatting
$users->appendChild($record);
$users->appendChild($storage->createTextNode("\n")); // XML formatting
self::closeStorage($storage);
return self::RESULT_SUCCESS;
}
// ---------------------------------------------------------------------------
private static /*.int.*/ function update(/*.ezUser_base.*/ $ezUser) {
$storage = self::openStorage();
$oldRecord = self::findUser($storage, $ezUser->id(), self::TAGNAME_ID);
if (!$oldRecord->hasChildNodes()) return self::RESULT_STORAGEERR;
$newRecord = self::createRecord($storage, $ezUser);
$oldRecord->parentNode->replaceChild($newRecord, $oldRecord);
self::closeStorage($storage);
return self::RESULT_SUCCESS;
}
// ---------------------------------------------------------------------------
// Account verification
// ---------------------------------------------------------------------------
protected static /*.boolean.*/ function verify_notify($username_or_email = '') {
$ezUser = self::lookup($username_or_email);
if ($ezUser->status() !== self::STATUS_PENDING) return false; // Only send confirmation email to users who are pending verification
// Bits of plumbing
$URL = self::thisURL();
$host = $_SERVER['HTTP_HOST'];
$s = ($_SERVER['SERVER_PROTOCOL'] === 'HTTPS') ? 's' : '';
// Message - SMTP needs CRLF not a bare LF (http://cr.yp.to/docs/smtplf.html)
$message = "Somebody calling themselves " . $ezUser->fullName() . " created an account at http$s://$host using this email address.\r\n";
$message .= "If it was you please click on the following link to verify the account.\r\n\r\n";
$message .= "http$s://$host$URL?" . self::ACTION_VERIFY . "=" . $ezUser->verificationKey() . "\r\n\r\n";
$message .= "After you click the link your account will be fully functional.\r\n";
// Send it
return self::sendEmail($ezUser->email(), 'New account confirmation', $message);
}
protected static /*.void.*/ function verify_update(/*.ezUser_base.*/ $ezUser, /*.string.*/ $verificationKey) {
if ($ezUser->status() === self::STATUS_PENDING && $ezUser->verificationKey() === $verificationKey) {
$ezUser->setStatus(self::STATUS_CONFIRMED);
self::update($ezUser);
}
}
// ---------------------------------------------------------------------------
public static /*.ezUser_base.*/ function save(/*.array[string]mixed.*/ $userData) {
$result = self::RESULT_VALIDATED;
$newUser = (array_key_exists(self::TAGNAME_NEWUSER, $userData) && ($userData[self::TAGNAME_NEWUSER] === self::STRING_TRUE)) ? true : false;
$emailChanged = false;
$usernameChanged = false;
$ezUser = self::getSessionObject(self::ACTION_ACCOUNT);
if (!$newUser && $ezUser->authenticated()) $ezUser->clearErrors(); else $newUser = true;
if ($newUser) {
$ezUser = new ezUser_base();
self::setSessionObject($ezUser, self::ACTION_ACCOUNT);
}
// Update email address
if (array_key_exists(self::TAGNAME_EMAIL, $userData)) {
$email = (string) $userData[self::TAGNAME_EMAIL];
$emailChanged = ($email !== $ezUser->email());
$thisResult = $ezUser->setEmail($email);
$result = ($result === self::RESULT_VALIDATED) ? $thisResult : $result;
} else $email = '';
// Update username
if (array_key_exists(self::COOKIE_USERNAME, $userData)) {
$username = (string) $userData[self::COOKIE_USERNAME];
$usernameChanged = ($username !== $ezUser->username());
$thisResult = $ezUser->setUsername($username);
$result = ($result === self::RESULT_VALIDATED) ? $thisResult : $result;
} else $username = '';
// Update password
if (array_key_exists(self::COOKIE_PASSWORD, $userData)) {
$passwordHash = (string) $userData[self::COOKIE_PASSWORD];
$thisResult = $ezUser->setPasswordHash($passwordHash);
$result = ($result === self::RESULT_VALIDATED) ? $thisResult : $result;
}
// Update first name and last name
if (array_key_exists(self::TAGNAME_FIRSTNAME, $userData)) $ezUser->setFirstName((string) $userData[self::TAGNAME_FIRSTNAME]);
if (array_key_exists(self::TAGNAME_LASTNAME, $userData)) $ezUser->setLastName ((string) $userData[self::TAGNAME_LASTNAME]);
// Check for duplicates
$id = $ezUser->id();
if (($result === self::RESULT_VALIDATED) && $emailChanged) $result = self::is_duplicate($email, $id);
if (($result === self::RESULT_VALIDATED) && $usernameChanged) $result = self::is_duplicate($username, $id);
// Final checks and update
if ($result === self::RESULT_VALIDATED) {
if ($ezUser->isChanged()) {
if ($newUser || $emailChanged) $ezUser->setStatus(self::STATUS_PENDING);
if ($ezUser->incomplete()) {
$result = self::RESULT_INCOMPLETE;
} else {
$result = ($newUser) ? self::add($ezUser) : self::update($ezUser);
if ($result === self::RESULT_SUCCESS) $ezUser->clearChanges();
if ($newUser || $emailChanged) self::verify_notify($email);
}
} else {
$result = self::RESULT_SUCCESS;
}
}
$ezUser->setResult($result);
return $ezUser;
}
// ---------------------------------------------------------------------------
// Password reset handling
// ---------------------------------------------------------------------------
private static /*.boolean.*/ function passwordReset_notify(/*.ezUser_base.*/ $ezUser) {
$passwordReset = $ezUser->passwordReset();
// Bits of plumbing
$URL = self::thisURL();
$host = $_SERVER['HTTP_HOST'];
$s = ($_SERVER['SERVER_PROTOCOL'] === 'HTTPS') ? 's' : '';
// Message
$message = "A password reset was requested for an account at http$s://$host using this email address.\r\n";
$message .= "If you want to reset your password please click on the following link.\r\n\r\n";
$message .= "http$s://$host$URL?" . self::ACTION_RESET . "=" . $passwordReset->resetKey() . "\r\n\r\n";
$message .= "If nothing happens when you click on the link then please copy it into your browser's address bar.\r\n";
// Send it
return self::sendEmail($ezUser->email(), 'Account maintenance', $message);
}
protected static /*.boolean.*/ function passwordReset_initialize(/*.string.*/ $username_or_email) {
$ezUser = self::lookup($username_or_email);
if ($ezUser->status() === self::STATUS_UNKNOWN) return false;
$passwordReset = $ezUser->passwordReset();
$passwordReset->initialize();
return ((bool) self::update($ezUser)) ? self::passwordReset_notify($ezUser) : false;
}
protected static /*.void.*/ function passwordReset_update(/*.ezUser_base.*/ $ezUser, /*.string.*/$passwordHash) {
if ($ezUser->hasPasswordReset()) {
$ezUser->setPasswordHash($passwordHash);
$ezUser->passwordReset(true); // Clear password reset data
self::update($ezUser);
}
}
}
// End of class ezUser_environment
/**
* This class manages the HTML, CSS and Javascript that you can include in
* your web pages to support user registration and authentication.
*
* @package ezUser
*/
interface I_ezUser extends I_ezUser_environment {
// Modes for account form
const ACCOUNT_MODE_NEW = 'new',
ACCOUNT_MODE_EDIT = 'edit',
ACCOUNT_MODE_DISPLAY = 'display',
ACCOUNT_MODE_RESULT = 'result',
ACCOUNT_MODE_CANCEL = 'cancel',
// Button types
BUTTON_TYPE_ACTION = 'action',
BUTTON_TYPE_PREFERENCE = 'preference',
BUTTON_TYPE_HIDDEN = 'hidden',
// Message types
MESSAGE_TYPE_DEFAULT = 'message',
MESSAGE_TYPE_TEXT = 'text',
// Message styles
MESSAGE_STYLE_DEFAULT = 'info',
MESSAGE_STYLE_FAIL = 'fail',
MESSAGE_STYLE_TEXT = 'text',
MESSAGE_STYLE_PLAIN = 'plain',
// Miscellaneous constants
DELIMITER_PLUS = '+',
PASSWORD_MASK = '************',
STRING_LEFT = 'left',
STRING_RIGHT = 'right';
// Methods may be commented out to reduce the attack surface when they are
// not required. Uncomment them if you need them.
// public static /*.void.*/ function getStatusText (/*.int.*/ $status, $more = '');
// public static /*.void.*/ function getResultText (/*.int.*/ $result, $more = '');
// public static /*.void.*/ function getStatusDescription (/*.int.*/ $status, $more = '');
// public static /*.void.*/ function getResultDescription (/*.int.*/ $result, $more = '');
public static /*.void.*/ function getResultForm (/*.int.*/ $result, $more = '');
public static /*.void.*/ function fatalError (/*.int.*/ $result, $more = '');
public static /*.void.*/ function getAccountForm ($mode = '', $newUser = false);
// public static /*.void.*/ function getDashboard ();
// public static /*.void.*/ function getSignInForm ();
// public static /*.void.*/ function getControlPanel ($username = '');
// public static /*.void.*/ function getStyleSheet ();
// public static /*.void.*/ function getJavascript ($containerList = '');
public static /*.void.*/ function getContainer ($action = self::ACTION_MAIN);
public static /*.void.*/ function getAbout ();
// public static /*.void.*/ function getSourceCode ();
public static /*.void.*/ function signIn ($userData = /*.(array[string]mixed).*/ array());
}
/**
* This class manages the HTML, CSS and Javascript that you can include in
* your web pages to support user registration and authentication.
*
* @package ezUser
*/
class ezUser extends ezUser_environment implements I_ezUser {
// ---------------------------------------------------------------------------
// Functions for sending stuff to the browser
// ---------------------------------------------------------------------------
private static /*.string.*/ function containerHeader() {return self::PACKAGE . '-container';}
private static /*.void.*/ function sendContent(/*.string.*/ $content, $container = self::PACKAGE, $contentType = 'text/html') {
// Send headers first
if (!headers_sent()) {
$package = self::PACKAGE;
if ($container === '') $container = $package;
header("Package: $package");
header(self::containerHeader() . ": $container");
header("Content-type: $contentType");
}
// Send content
echo $content;
/* Comment out profiling statements if not needed
// Send profiling data as a comment
global $ezUser_profile;
if (count($ezUser_profile) > 0) {
$ezUser_profile['response'] = ezUser_time();
if ($contentType === 'text/javascript' || $contentType === 'text/css') {
$commentStart = '/' . '*';
$commentEnd = '*' . '/';
} else {
$commentStart = '';
}
echo "\n$commentStart\n";
$previous = reset($ezUser_profile);
while (list($key, $value) = each($ezUser_profile)) {
$elapsed = round($value - $previous, 4);
$previous = $value;
echo "$key\t$value\t$elapsed\n";
}
echo "$commentEnd\n";
}
*/
}
private static /*.string.*/ function getXML($html = '', $container = self::PACKAGE) {
$package = self::PACKAGE;
if (is_numeric($container)) $container = $package; // If passed to sendXML as an array
return "<$package container=\"$container\">$package>";
}
private static /*.void.*/ function sendXML(/*.mixed.*/ $content = '', $container = self::PACKAGE) {
if (is_array($content)) {
// Expected array format is $content['container'] = ''
$package = self::PACKAGE;
$contentArray = /*.(array[]string).*/ $content;
$xmlArray = /*.(array[]string).*/ array_map('ezUser::getXML', $contentArray, array_keys($contentArray)); // wrap each element
$xml = implode('', $xmlArray);
$xml = "<$package>$xml$package>";
} else {
$xml = self::getXML((string) $content, $container);
}
self::sendContent($xml, $container, 'text/xml');
}
// ---------------------------------------------------------------------------
// Functions that build common HTML fragments
// ---------------------------------------------------------------------------
private static /*.string.*/ function htmlPage($body = '', $title = '', $sendToBrowser = false) {
$package = self::PACKAGE;
$URL = self::thisURL();
$actionJs = self::ACTION_JAVASCRIPT;
$actionCSS = self::ACTION_STYLESHEET;
$html = <<
$title
$body
HTML;
if ($sendToBrowser) {self::sendContent($html); return '';} else return $html;
}
private static /*.string.*/ function htmlContainer($action = self::ACTION_MAIN, $sendToBrowser = false) {
$package = self::PACKAGE;
$baseAction = explode('=', $action);
$container = self::getInstanceId($baseAction[0]);
$actionCommand = self::ACTION;
$actionJs = self::ACTION_JAVASCRIPT;
$URL = self::thisURL();
$js = $package . '_ajax';
$js .= "[$js.push(new C_$js()) - 1].execute('$action')";
$html = <<
HTML;
if ($sendToBrowser) {self::sendContent($html); return '';} else return $html;
}
private static /*.string.*/ function htmlInputText($styleFloat = self::STRING_RIGHT) {
$package = self::PACKAGE;
$onKeyUp = $package . '_keyUp';
return <<$message
";
$id = ($container === '') ? "$package-$type" : "$container-$type";
$onClick = $package . '_click';
return <<$message
HTML;
}
// ---------------------------------------------------------------------------
// Text versions of status and result codes
// ---------------------------------------------------------------------------
private static /*.string.*/ function statusText(/*.int.*/ $status, $more = '', $sendToBrowser = false) {
switch ($status) {
case self::STATUS_UNKNOWN: $text = "Unknown status"; break;
case self::STATUS_PENDING: $text = "Awaiting confirmation"; break;
case self::STATUS_CONFIRMED: $text = "Confirmed and active"; break;
case self::STATUS_INACTIVE: $text = "Inactive"; break;
default: $text = "Unknown status code"; break;
}
if ($more !== '') $text .= ": $more";
if ($sendToBrowser) {self::sendContent($text); return '';} else return $text;
}
private static /*.string.*/ function resultText(/*.int.*/ $result, $more = '', $sendToBrowser = false) {
switch ($result) {
// Authentication results
case self::RESULT_UNDEFINED: $text = "Undefined"; break;
case self::RESULT_SUCCESS: $text = "Success"; break;
case self::RESULT_UNKNOWNUSER: $text = "Username not recognised"; break;
case self::RESULT_BADPASSWORD: $text = "Password is wrong"; break;
case self::RESULT_UNKNOWNACTION: $text = "Unrecognised action"; break;
case self::RESULT_NOACTION: $text = "No action specified"; break;
case self::RESULT_NOSESSION: $text = "No session data available"; break;
case self::RESULT_NOSESSIONCOOKIES: $text = "Session cookies are not enabled"; break;
case self::RESULT_STORAGEERR: $text = "Error with stored user details"; break;
case self::RESULT_EMAILERR: $text = "Error sending email"; break;
// Registration and validation results
case self::RESULT_VALIDATED: $text = "Validation was successful"; break;
case self::RESULT_NOID: $text = "ID cannot be blank"; break;
case self::RESULT_NOUSERNAME: $text = "The username cannot be blank"; break;
case self::RESULT_NOEMAIL: $text = "Please provide an email address"; break;
case self::RESULT_EMAILFORMATERR: $text = "Incorrect email address format"; break;
case self::RESULT_NOPASSWORD: $text = "Password hash cannot be blank"; break;
case self::RESULT_NULLPASSWORD: $text = "Password cannot be blank"; break;
case self::RESULT_STATUSNAN: $text = "Status code must be numeric"; break;
case self::RESULT_RESULTNAN: $text = "Result code must be numeric"; break;
case self::RESULT_CONFIGNOTARRAY: $text = "Configuration settings must be an array"; break;
case self::RESULT_USERNAMEEXISTS: $text = "This username already exists"; break;
case self::RESULT_EMAILEXISTS: $text = "Email address is already registered"; break;
case self::RESULT_NOTSIGNEDIN: $text = "You must be signed in to update your account"; break;
case self::RESULT_INCOMPLETE: $text = "Not enough information to update the account"; break;
default: $text = "Unknown result code"; break;
}
if ($more !== '') $text .= ": $more";
if ($sendToBrowser) {self::sendContent($text); return '';} else return $text;
}
private static /*.string.*/ function statusDescription(/*.int.*/ $status, $more = '', $sendToBrowser = false) {
switch ($status) {
case self::STATUS_PENDING: $text = "Your account has been created and a confirmation email has been sent. Please click on the link in the confirmation email to verify your account.";
break;
default: $text = self::statusText($status); break;
}
if ($more !== '') $text .= ": $more";
if ($sendToBrowser) {self::sendContent($text); return '';} else return $text;
}
private static /*.string.*/ function resultDescription(/*.int.*/ $result, $more = '', $sendToBrowser = false) {
switch ($result) {
case self::RESULT_EMAILFORMATERR: $text = "The format of the email address you entered was incorrect. Email addresses should be in the form joe.smith@example.com";
break;
default: $text = self::resultText($result); break;
}
if ($more !== '') $text .= ": $more";
if ($sendToBrowser) {self::sendContent($text); return '';} else return $text;
}
// ---------------------------------------------------------------------------
// HTML for UI Forms
// ---------------------------------------------------------------------------
/**
* Render the HTML for the account maintenance form
*
* $newUser indicates whether this is an existing user from the
* database, or a new registration that we are processing. If the user
* enters invalid data we might render this form a number of times
* until validation is successful. $newUser should persist until
* registration is successful.
*
* The form can also operate as a "wizard" (with Next and Back buttons). This
* allows it to work in the confined space of the ezUser control panel
*
* This function is also driven by the mode parameter as follows:
*
* Mode Behaviour
* ------- --------------------------------------------------------------
* - (none) Infer mode from current ezUser object - if it's authenticated
* then display the account page for that user. If not then
* display a registration form for a new user. Inferred mode will
* be 'display' or 'new'
*
* - new Register a new user. Input controls are blank but available.
* Button says Register.
*
* - display View account details. Input controls are populated but
* unavailable. Button says Edit.
*
* - edit Edit an existing account or correct a failed registration.
* Input controls are populated with existing data. Buttons say
* OK and Cancel
*
* - result Infer actual mode from result of validation. If validated then
* display account details, otherwise allow them to be corrected.
* Inferred mode will be either 'display' or 'edit'.
*
* - cancel Infer actual mode from $newUser. If we're cancelling a new
* registration then clear the form. If we're cancelling editing
* an existing user then redisplay details from the database.
* Inferred mode will be either 'new' or 'display'.
*
* So, the difference between $mode = 'new' and $newUser = true is as
* follows:
*
* - $mode = 'new' means this is a blank form for a new registration
*
* - $newUser = true means we are processing a new registration but we
* might be asking the user to re-enter certain values:
* the form might therefore need to be populated with the
* attempted registration details.
*
* @param string $mode See above
* @param boolean $newUser Is this a new or existing user?
* @param boolean $wizard Display as a wizard within control panel
* @param boolean $sendToBrowser Send HTML to browser?
*/
private static /*.string.*/ function htmlAccountForm($mode = '', $newUser = false, $wizard = false, $sendToBrowser = false) {
/* Comment out profiling statements if not needed
global $ezUser_profile;
$ezUser_profile[self::ACTION_ACCOUNT . '-start'] = ezUser_time();
*/
$package = self::PACKAGE;
$action = self::ACTION_ACCOUNT;
$actionResend = self::ACTION_RESEND;
$actionValidate = self::ACTION_VALIDATE;
$formId = self::getInstanceId($action);
$container = ($wizard) ? $package : $formId;
$tagFirstName = self::TAGNAME_FIRSTNAME;
$tagLastName = self::TAGNAME_LASTNAME;
$tagEmail = self::TAGNAME_EMAIL;
$tagUsername = self::TAGNAME_USERNAME;
$tagPassword = self::TAGNAME_PASSWORD;
$tagConfirm = self::TAGNAME_CONFIRM;
$tagNewUser = self::TAGNAME_NEWUSER;
$tagUseSavedPassword = self::TAGNAME_SAVEDPASSWORD;
$tagWizard = self::TAGNAME_WIZARD;
$modeNew = self::ACCOUNT_MODE_NEW;
$modeEdit = self::ACCOUNT_MODE_EDIT;
$modeDisplay = self::ACCOUNT_MODE_DISPLAY;
$modeResult = self::ACCOUNT_MODE_RESULT;
$modeCancel = self::ACCOUNT_MODE_CANCEL;
$stringRight = self::STRING_RIGHT;
$htmlButtonAction = self::htmlButton(self::BUTTON_TYPE_ACTION);
$htmlButtonHidden = self::htmlButton(self::BUTTON_TYPE_HIDDEN);
$passwordOnFocus = $package . '_passwordFocus';
$passwordOnBlur = $package . '_passwordBlur';
$htmlInputText = self::htmlInputText();
$messageShort = self::htmlMessage('* reqd', self::MESSAGE_STYLE_PLAIN, $formId);
$resendButton = '';
if (!isset($mode) || empty($mode)) $mode = '';
$modeInfo = ($newUser) ? self::STRING_TRUE : self::STRING_FALSE;
$modeInfo = "(originally mode was '$mode', new flag was $modeInfo) -->";
if ($mode === '') {
$ezUser = self::getSessionObject();
$result = self::RESULT_SUCCESS;
if ($ezUser->authenticated()) {
$mode = $modeDisplay;
$ezUser->addSignOutAction($action);
self::setSessionObject($ezUser, $action);
} else {
$mode = $modeNew;
$ezUser->clearSignOutActions();
}
} else {
$ezUser = self::getSessionObject($action);
$result = $ezUser->result();
}
if ($mode === $modeCancel) $ezUser->clearErrors();
// Some raw logic - think carefully about these lines before amending
if (!isset($newUser)) $newUser = false;
if ($mode === $modeNew) $newUser = true;
if ($mode === $modeCancel) $mode = ($newUser) ? $modeNew : $modeDisplay;
if ($mode === $modeResult) $mode = ($result === self::RESULT_SUCCESS) ? $modeDisplay : $modeEdit;
switch ($mode) {
case self::ACCOUNT_MODE_NEW:
$email = '';
$firstName = '';
$lastName = '';
$username = '';
$password = '';
$buttonID = $actionValidate;
$buttonText = 'Register';
$buttonAction = $actionValidate;
$disabled = '';
$htmlOtherButton = "\t\t\t\t\n";
$useSavedPassword = false;
$messageLong = self::htmlMessage('', self::MESSAGE_STYLE_TEXT, $formId, self::MESSAGE_TYPE_TEXT);
break;
case self::ACCOUNT_MODE_DISPLAY:
$errors = $ezUser->errors();
$email = (array_key_exists(self::TAGNAME_EMAIL, $errors)) ? $errors[self::TAGNAME_EMAIL] : $ezUser->email();
$firstName = (array_key_exists(self::TAGNAME_FIRSTNAME, $errors)) ? $errors[self::TAGNAME_FIRSTNAME] : $ezUser->firstName();
$lastName = (array_key_exists(self::TAGNAME_LASTNAME, $errors)) ? $errors[self::TAGNAME_LASTNAME] : $ezUser->lastName();
$username = (array_key_exists(self::TAGNAME_USERNAME, $errors)) ? $errors[self::TAGNAME_USERNAME] : $ezUser->username();
$password = ($ezUser->passwordHash() === '') ? '' : self::PASSWORD_MASK;
$buttonID = $modeEdit;
$buttonText = 'Edit';
$buttonAction = "$action=$modeEdit";
$disabled = "\t\t\t\t\tdisabled\t=\t\"disabled\"\r\n";
$htmlOtherButton = "\t\t\t\t\n";
$useSavedPassword = false;
$newUser = false;
if ($result === self::RESULT_SUCCESS || $result === self::RESULT_UNDEFINED) {
// Show status information
$status = $ezUser->status();
$messageLong = ($status === self::STATUS_CONFIRMED) ? '' : self::statusDescription($status);
$messageLong = self::htmlMessage($messageLong, self::MESSAGE_STYLE_TEXT, $formId, self::MESSAGE_TYPE_TEXT);
if ($status === self::STATUS_PENDING) $resendButton = "\n\t\t\t\t";
} else {
// Show result information
$messageLong = self::resultDescription($result);
$messageLong = self::htmlMessage($messageLong, self::MESSAGE_STYLE_FAIL, $formId, self::MESSAGE_TYPE_TEXT);
}
break;
case self::ACCOUNT_MODE_EDIT:
$errors = $ezUser->errors();
$email = (array_key_exists(self::TAGNAME_EMAIL, $errors)) ? $errors[self::TAGNAME_EMAIL] : $ezUser->email();
$firstName = (array_key_exists(self::TAGNAME_FIRSTNAME, $errors)) ? $errors[self::TAGNAME_FIRSTNAME] : $ezUser->firstName();
$lastName = (array_key_exists(self::TAGNAME_LASTNAME, $errors)) ? $errors[self::TAGNAME_LASTNAME] : $ezUser->lastName();
$username = (array_key_exists(self::TAGNAME_USERNAME, $errors)) ? $errors[self::TAGNAME_USERNAME] : $ezUser->username();
$password = ($ezUser->passwordHash() === '') ? '' : self::PASSWORD_MASK;
$buttonID = $actionValidate;
$buttonText = 'OK';
$buttonAction = $actionValidate;
$disabled = '';
$htmlOtherButton = "\t\t\t\t\n";
$useSavedPassword = $newUser;
if ($result === self::RESULT_SUCCESS || $result === self::RESULT_UNDEFINED) {
$messageLong = self::htmlMessage('', self::MESSAGE_STYLE_TEXT, $formId, self::MESSAGE_TYPE_TEXT);
} else {
// Show result information
$messageLong = self::resultDescription($result);
$messageLong = self::htmlMessage($messageLong, self::MESSAGE_STYLE_FAIL, $formId, self::MESSAGE_TYPE_TEXT);
}
break;
default:
$useSavedPassword = false;
$email = '';
$disabled = '';
$firstName = '';
$lastName = '';
$username = '';
$password = '';
$buttonID = '';
$buttonAction = '';
$buttonText = '';
$htmlOtherButton = '';
$messageLong = '';
break;
}
if ($wizard) {
$wizardString = self::STRING_TRUE;
$styleHidden = " $package-hidden";
$htmlNavigation = <<