* 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\">"; } 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"; } 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 = << HTML; } else { $wizardString = self::STRING_FALSE; $styleHidden = ''; $htmlNavigation = ''; } // At this point we have finished with the result of any prior validation // so we can clear the result field $ezUser->setResult(self::RESULT_UNDEFINED); $newString = ($newUser) ? self::STRING_TRUE : self::STRING_FALSE; $useSavedPasswordString = ($useSavedPassword) ? self::STRING_TRUE : self::STRING_FALSE; $modeInfo = "