id = $row['UserID']; if ($row['DOB'] && $row['DOB'] !== '0000-00-00') { $this->birthdate = $row['DOB']; } $this->cityID = $row['CityID']; $this->countryID = $row['CountryID']; $this->gender = $row['Gender']; $this->email = $row['Email']; $this->name = $row['Name']; $this->stateID = $row['StateID']; $this->username = $row['Username']; $this->organization = $row['SchoolName']; } /************************************************************************** * CRUD Methods **************************************************************************/ /** * Creates a basic user * * @param string $emailAddress * @param string $languageIso A ISO639-format language identifier * @return int The id of the newly created user */ public static function create($emailAddress, $languageIso) { if ($emailAddress != 'eqwiphubs+temp@example.com') { if (self::checkEmail($emailAddress)) { throw new \InvalidArgumentException("Invalid email: '$emailAddress'"); } } $languageID = languageIsoToId($languageIso); if (!$languageID) { throw new \InvalidArgumentException("Invalid language: $languageIso"); } $newUserID = safe_dbwrite("INSERT INTO tig.Users SET Email = '" . mysqlescape($emailAddress) . "', DateJoined = NOW(), LastLogin = NOW(), IP = '{$_SERVER['REMOTE_ADDR']}', flDOBGender = 1 # The v6 Signup form hid birthdate and gender so by default v8 does too"); // Save Preferred Language safe_dbwrite("INSERT INTO tig.UserSpoken SET UserID = $newUserID, LanguageID = $languageID, flPreferred = 1"); // Save Default Email Preferences (none) safe_dbwrite("INSERT INTO tig.UserEmailPrefs SET MemberID = $newUserID, flDispatch = 0, flWeekly = 0, flOpps = 0"); // Add to 'User Settings' table safe_dbwrite("INSERT INTO tig.UserSettings SET MemberID = $newUserID"); // Associate Account with Email-Only Group Memberships safe_dbwrite("UPDATE tig.GroupMembers SET MemberID = $newUserID, flSignupdate = 1, flConfirm = 1 WHERE Email = '". mysqlescape($emailAddress) . "' AND MemberID = 0"); // Initialize Active Rank safe_dbwrite("UPDATE tig.Users SET ActiveRank = $newUserID WHERE UserID = $newUserID"); return $newUserID; } /** * Loads a User from the database (or the in-memory cache of users) * * @param int $id Identifies the user to load * @param boolean $fromDatabaseMaster If true, the user is loaded from the master database. Useful on signup pages when the user was just created. * @return \self * @throws InvalidArgumentException * @throws OutOfBoundsException */ public static function readByID($id, $fromDatabaseMaster = false) { if (!filter_var($id, FILTER_VALIDATE_INT)) { throw new \InvalidArgumentException("Invalid User ID: $id"); } // Check Cache if (self::$loadedUsers[$id]) { return self::$loadedUsers[$id]; } $row = query("SELECT * FROM tig.Users WHERE UserID = $id", '', $fromDatabaseMaster); if (!$row) { throw new \OutOfBoundsException("User ID not found: $id"); } $user = new self($row); // Add to Cache self::$loadedUsers[$id] = $user; return $user; } /** * Loads a group of Users from the database (or the in-memory cache of users) * * @param int[] $ids Identifies the users to load * @return \self[] * @throws InvalidArgumentException */ public static function readByIDs($ids) { if (!is_array($ids)) { throw new \InvalidArgumentException("Not an array."); } if (count($ids) == 0) { return array(); } $users = array(); $rowSet = safe_dbread("SELECT * FROM tig.Users WHERE UserID IN (" . implode(', ', $ids) . ")"); while ($row = safe_fetch_assoc($rowSet)) { $users[] = new self($row); } return $users; } /** * Persists changes to the user in the database * * The various setters do not store their information until this method is * called. */ public function update() { // Handle NULL values $sqlBirthdate = is_null($this->birthdate) ? 'NULL' : ("'" . mysqlescape($this->birthdate) . "'"); $sqlCityID = is_null($this->cityID) ? 'NULL' : ("'" . mysqlescape($this->cityID) . "'"); $sqlCountryID = is_null($this->countryID) ? 'NULL' : ("'" . mysqlescape($this->countryID) . "'"); $sqlStateID = is_null($this->stateID) ? 'NULL' : ("'" . mysqlescape($this->stateID) . "'"); $sqlUsername = is_null($this->username) ? 'NULL' : ("'" . mysqlescape($this->username) . "'"); $sqlOrganization = is_null($this->organization) ? 'NULL' : ("'" . mysqlescape($this->organization) . "'"); safe_dbwrite("UPDATE tig.Users SET CityID = $sqlCityID, CountryID = $sqlCountryID, DOB = $sqlBirthdate, Email = '" . mysqlescape($this->email) . "', Gender = '" . mysqlescape($this->gender) . "', Name = '" . mysqlescape($this->name) . "', StateID = $sqlStateID, Username = $sqlUsername, Organization = $sqlOrganization WHERE UserID = " . mysqlescape($this->id)); } /****************************************************************************** * Getters \ Setters \ Error Checkers ******************************************************************************/ /** * Returns the ID of the User * * @return int */ public function getID() { return $this->id; } /** * Returns the number of years the user has been alive * * @param boolean $anyAge Return the age, regardless of how old the user is * @return int */ public function getAge($anyAge = false) { if (!$this->birthdate) { return; } $birthdate = new \DateTime($this->birthdate); $today = new \DateTime(); $interval = $birthdate->diff($today, true); $age = $interval->y; if (!$anyAge && $age < 14) { return; } return $age; } /** * Returns the full URL from the user's avatar image * * @param int $minimumSize The image will be this large, if an image this large is available. * @return string The URL of the user's avatar image * @throws InvalidArgumentException */ public function getAvatarUrl($minimumSize) { if (!filter_var($minimumSize, FILTER_VALIDATE_INT)) { throw new \InvalidArgumentException("Invalid size: $minimumSize"); } if ($minimumSize > 64) { $size = 128; } elseif ($minimumSize > 32) { $size = 64; } else { $size = 32; } return "https://avatar.tigweb.org/{$this->username}/{$size}"; } /** * Validates a prospective birthdate * * @param string $isoDate Date formatted in ISO 8601 format (ex. 2014-10-03) * @return int|boolean On error, an "ERROR_" constant indicating the type of error. False if the birthdate passes validation. */ public function checkBirthdate($isoDate) { if (mb_strlen($isoDate) === 0) { return self::ERROR_MISSING; } // This regex is a modified version of the regex supplied with the // validation plugin we use: // https://github.com/jzaefferer/jquery-validation/blob/9f4ba10ea79b4cf59225468d6ec29911f0e53a0a/src/core.js#L1137 // It was modified to disallow slashes, which are not allowed by the // ISO format. $formatRegex = '/^\d{4}[\-](0?[1-9]|1[012])[\-](0?[1-9]|[12][0-9]|3[01])$/'; if (!preg_match($formatRegex, $isoDate)) { return self::ERROR_INVALID; } // Split the date list($year, $month, $day) = explode('-', $isoDate); // February 30th? if (!checkdate($month, $day, $year)) { return self::ERROR_INVALID; } // In future? $time = mktime(0, 0, 0, $day, $month, $year); if ($time > time()) { return self::ERROR_TOOBIG; } return false; } /** * Sets the user's birthdate * * @param string $isoDate Date formatted in ISO 8601 format (ex. 2014-10-03) * @throws InvalidArgumentException */ public function setBirthdate($isoDate) { if (self::checkBirthdate($isoDate)) { throw new \InvalidArgumentException("Invalid birthdate: $isoDate"); } $this->birthdate = $isoDate; } /** * Gets the user's birthdate * * @param boolean $anyAge Return the birthdate, even if the user is under 14 years old. * @return string Birthdate in ISO format */ public function getBirthdate($anyAge = false) { if ($this->birthdate === '0000-00-00') { return; } // Prevent the birthdate from being displayed if the user is under 14 if (!$anyAge && $this->getAge(true) < 14) { return; } return $this->birthdate; } /** * Returns an object that can be used to set and retrieve the user's location (ie. city, state, and country) * * @return \TIG\Common\User\Location */ public function getLocation() { return \TIG\Common\User\Location::readByUserID($this->id); } /** * Validates a prospective email address * * @param string $emailAddress The email address to validate * $param int $forUserID The user this email address is for. Optional. * @return int|boolean On error, a "ERROR_" constant indicating the type of error. False if the email address passes validation. */ public static function checkEmail($emailAddress, $forUserID = null) { $length = mb_strlen($emailAddress, 'UTF-8'); if ($length === 0) { return self::ERROR_MISSING; } if ($length > self::EMAIL_MAXLENGTH) { return self::ERROR_TOOBIG; } if (!filter_var($emailAddress, FILTER_VALIDATE_EMAIL)) { return self::ERROR_INVALID; } $currentEmailOwner = self::isEmailTaken($emailAddress); if ($currentEmailOwner && $currentEmailOwner != $forUserID) { return self::ERROR_TAKEN; } return false; } public function setEmail($emailAddress) { if (self::checkEmail($emailAddress, $this->id)) { throw new \InvalidArgumentException("Invalid email: '$emailAddress'"); } $this->email = $emailAddress; } /** * Returns the user's primary email address * * @returns string */ public function getEmail() { return $this->email; } /** * Validates a prospective gender * * @param string $gender Single character indicating the user's gender * @return int|boolean On error, a "ERROR_" constant indicating the type of error. False if the validation passes. */ public static function checkGender($gender) { if (mb_strlen($gender) === 0) { return self::ERROR_MISSING; } $validCharacters = explode(',', self::GENDER_CHARACTERS); if (!in_array($gender, $validCharacters)) { return self::ERROR_INVALID; } return false; } public function setGender($gender) { if (self::checkGender($gender)) { throw new \InvalidArgumentException("Invalid gender character: '$gender'"); } $this->gender = $gender; } public function getGender() { return $this->gender; } /** * Validates a prospective name * * @param sting $name The name to validate * @return int|boolean On error, a "ERROR_" constant indicating the type of error. False if the name passes validation. */ public static function checkName($name) { return self::checkBasicField($name, self::NAME_MAXLENGTH, true); } /** * Sets the user's name * * @param string $name * @throws InvalidArgumentException */ public function setName($name) { if (self::checkName($name)) { throw new \InvalidArgumentException("Invalid name: $name"); } $this->name = $name; } /** * Returns the user's name (or the username, if the name is not available) * * @param boolean $raw If true, the name is returned as it is stored in the Name field of the Users table. * @returns string */ public function getName($raw = false) { if ($raw) { return $this->name; } if ($this->name) { return $this->name; } if ($this->username) { return $this->username; } return '(no name)'; } /** * Validates a prospective username * * @param string $username The username to validate * @return int|boolean On error, a "ERROR_" constant indicating the type of error. False if the username passes validation. */ public static function checkUsername($username) { $length = mb_strlen($username, 'UTF-8'); if ($length === 0) { return self::ERROR_MISSING; } if ($length > self::USERNAME_MAXLENGTH) { return self::ERROR_TOOBIG; } if (!preg_match(self::USERNAME_VALIDCHARACTERREGEX, $username)) { return self::ERROR_INVALID; } $reservedUsernames = explode('|', self::USERNAME_RESERVEDUSERNAMES); foreach ($reservedUsernames as $reserved) { if (stristr($username, $reserved)) { return self::ERROR_TAKEN; } } if (hasSwear($username)) { return self::ERROR_SWEARWORD; } if (self::isUsernameTaken($username)) { return self::ERROR_TAKEN; } return false; } /** * Validates an organization * * @param string $organization The organization to validate * @return int|boolean On error, a "ERROR_" constant indicating the type of error. False if the username passes validation. */ public static function checkOrganization($organization) { $length = mb_strlen($organization, 'UTF-8'); if ($length === 0) { return self::ERROR_MISSING; } return false; } /** * Sets the user's username * * @param string $username * @throws InvalidArgumentException */ public function setUsername($username) { if (self::checkUsername($username)) { throw new \InvalidArgumentException("Invalid username: $username"); } $this->username = $username; } /** * Returns the user's username * * @return string */ public function getUsername() { return $this->username; } /** * Sets the user's organization * * @param string $organization */ public function setOrganization($organization) { $this->organization = $organization; } /** * Returns the user's organization * * @return string */ public function getOrganization() { return $this->organization; } /****************************************************************************** * Utility Functions ******************************************************************************/ /** * Checks if a field is blank or too big * * @param string $value The user input * @param integer $maxLength The maximum number of characters allowed in the field * @param boolean $isRequired Should an error be returned if the field is blank? * @return int|boolean On error, a "ERROR_" constant indicating the type of error. False if the street value passes validation. */ private static function checkBasicField($value, $maxLength, $isRequired) { $length = mb_strlen($value, 'UTF-8'); if ($isRequired && ($length === 0)) { return self::ERROR_MISSING; } if (mb_strlen($value, 'UTF-8') > $maxLength) { return self::ERROR_TOOBIG; } return false; } /** * Checks if an email address is already associated with a user and, if it is, fetches the associated UserID * * @param string $emailAddress * @return int|boolean The ID of the user connected to the email address. False if none. */ public static function isEmailTaken($emailAddress) { if (!filter_var($emailAddress, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException("Invalid email address: $emailAddress"); } $sqlEmailAddress = mysqlescape($emailAddress); // This is always fetched from the master because the contexts (ex. // signup) where this method is used require a up-to-date, definitive // answer. $matches = query(" SELECT UserID FROM tig.Users WHERE Email = '$sqlEmailAddress' OR Email2 = '$sqlEmailAddress' OR Email3 = '$sqlEmailAddress' LIMIT 1 ", "", true); return $matches['UserID'] ?: false; } /** * Checks if a username is being already in use\exists * * @param string $username To check * @return int|boolean The ID of the user with the username. False if none. */ public static function isUsernameTaken($username) { if (mb_strlen($username) === 0) { throw new \InvalidArgumentException("Invalid username: $username"); } $userWithUsername = query("SELECT UserID FROM tig.Users WHERE Username = '" . mysqlescape($username) . "' LIMIT 1"); return $userWithUsername['UserID'] ?: false; } }