initial commit

This commit is contained in:
2025-11-24 14:06:57 +01:00
commit 4fce91b055
81 changed files with 7718 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
<?php
/**
* Handles all data manipulation of the admin part
*/
class AdminModel
{
/**
* Sets the deletion and suspension values
*
* @param $suspensionInDays
* @param $softDelete
* @param $userId
*/
public static function setAccountSuspensionAndDeletionStatus($suspensionInDays, $softDelete, $userId)
{
// Prevent to suspend or delete own account.
// If admin suspend or delete own account will not be able to do any action.
if ($userId == Session::get('user_id')) {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_CANT_DELETE_SUSPEND_OWN'));
return false;
}
if ($suspensionInDays > 0) {
$suspensionTime = time() + ($suspensionInDays * 60 * 60 * 24);
} else {
$suspensionTime = null;
}
// FYI "on" is what a checkbox delivers by default when submitted. Didn't know that for a long time :)
if ($softDelete == "on") {
$delete = 1;
} else {
$delete = 0;
}
// write the above info to the database
self::writeDeleteAndSuspensionInfoToDatabase($userId, $suspensionTime, $delete);
// if suspension or deletion should happen, then also kick user out of the application instantly by resetting
// the user's session :)
if ($suspensionTime != null OR $delete = 1) {
self::resetUserSession($userId);
}
}
/**
* Simply write the deletion and suspension info for the user into the database, also puts feedback into session
*
* @param $userId
* @param $suspensionTime
* @param $delete
* @return bool
*/
private static function writeDeleteAndSuspensionInfoToDatabase($userId, $suspensionTime, $delete)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_suspension_timestamp = :user_suspension_timestamp, user_deleted = :user_deleted WHERE user_id = :user_id LIMIT 1");
$query->execute(array(
':user_suspension_timestamp' => $suspensionTime,
':user_deleted' => $delete,
':user_id' => $userId
));
if ($query->rowCount() == 1) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_SUSPENSION_DELETION_STATUS'));
return true;
}
}
/**
* Kicks the selected user out of the system instantly by resetting the user's session.
* This means, the user will be "logged out".
*
* @param $userId
* @return bool
*/
private static function resetUserSession($userId)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET session_id = :session_id WHERE user_id = :user_id LIMIT 1");
$query->execute(array(
':session_id' => null,
':user_id' => $userId
));
if ($query->rowCount() == 1) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_USER_SUCCESSFULLY_KICKED'));
return true;
}
}
}

View File

@@ -0,0 +1,258 @@
<?php
class AvatarModel
{
/**
* Gets a gravatar image link from given email address
*
* Gravatar is the #1 (free) provider for email address based global avatar hosting.
* The URL (or image) returns always a .jpg file ! For deeper info on the different parameter possibilities:
* @see http://gravatar.com/site/implement/images/
* @source http://gravatar.com/site/implement/images/php/
*
* This method will return something like http://www.gravatar.com/avatar/79e2e5b48aec07710c08d50?s=80&d=mm&r=g
* Note: the url does NOT have something like ".jpg" ! It works without.
*
* Set the configs inside the application/config/ files.
*
* @param string $email The email address
* @return string
*/
public static function getGravatarLinkByEmail($email)
{
return 'http://www.gravatar.com/avatar/' .
md5(strtolower(trim($email))) .
'?s=' . Config::get('AVATAR_SIZE') . '&d=' . Config::get('GRAVATAR_DEFAULT_IMAGESET') . '&r=' . Config::get('GRAVATAR_RATING');
}
/**
* Gets the user's avatar file path
* @param int $user_has_avatar Marker from database
* @param int $user_id User's id
* @return string Avatar file path
*/
public static function getPublicAvatarFilePathOfUser($user_has_avatar, $user_id)
{
if ($user_has_avatar) {
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . $user_id . '.jpg';
}
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . Config::get('AVATAR_DEFAULT_IMAGE');
}
/**
* Gets the user's avatar file path
* @param $user_id integer The user's id
* @return string avatar picture path
*/
public static function getPublicUserAvatarFilePathByUserId($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_has_avatar FROM users WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_id' => $user_id));
if ($query->fetch()->user_has_avatar) {
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . $user_id . '.jpg';
}
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . Config::get('AVATAR_DEFAULT_IMAGE');
}
/**
* Create an avatar picture (and checks all necessary things too)
* TODO decouple
* TODO total rebuild
*/
public static function createAvatar()
{
// check avatar folder writing rights, check if upload fits all rules
if (self::isAvatarFolderWritable() AND self::validateImageFile()) {
// create a jpg file in the avatar folder, write marker to database
$target_file_path = Config::get('PATH_AVATARS') . Session::get('user_id');
self::resizeAvatarImage($_FILES['avatar_file']['tmp_name'], $target_file_path, Config::get('AVATAR_SIZE'), Config::get('AVATAR_SIZE'));
self::writeAvatarToDatabase(Session::get('user_id'));
Session::set('user_avatar_file', self::getPublicUserAvatarFilePathByUserId(Session::get('user_id')));
Session::add('feedback_positive', Text::get('FEEDBACK_AVATAR_UPLOAD_SUCCESSFUL'));
}
}
/**
* Checks if the avatar folder exists and is writable
*
* @return bool success status
*/
public static function isAvatarFolderWritable()
{
if (is_dir(Config::get('PATH_AVATARS')) AND is_writable(Config::get('PATH_AVATARS'))) {
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_FOLDER_DOES_NOT_EXIST_OR_NOT_WRITABLE'));
return false;
}
/**
* Validates the image
* Only accepts gif, jpg, png types
* @see http://php.net/manual/en/function.image-type-to-mime-type.php
*
* @return bool
*/
public static function validateImageFile()
{
if (!isset($_FILES['avatar_file'])) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_IMAGE_UPLOAD_FAILED'));
return false;
}
// if input file too big (>5MB)
if ($_FILES['avatar_file']['size'] > 5000000) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_UPLOAD_TOO_BIG'));
return false;
}
// get the image width, height and mime type
$image_proportions = getimagesize($_FILES['avatar_file']['tmp_name']);
// if input file too small, [0] is the width, [1] is the height
if ($image_proportions[0] < Config::get('AVATAR_SIZE') OR $image_proportions[1] < Config::get('AVATAR_SIZE')) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_UPLOAD_TOO_SMALL'));
return false;
}
// if file type is not jpg, gif or png
if (!in_array($image_proportions['mime'], array('image/jpeg', 'image/gif', 'image/png'))) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_UPLOAD_WRONG_TYPE'));
return false;
}
return true;
}
/**
* Writes marker to database, saying user has an avatar now
*
* @param $user_id
*/
public static function writeAvatarToDatabase($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_has_avatar = TRUE WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_id' => $user_id));
}
/**
* Resize avatar image (while keeping aspect ratio and cropping it off in a clean way).
* Only works with gif, jpg and png file types. If you want to change this also have a look into
* method validateImageFile() inside this model.
*
* TROUBLESHOOTING: You don't see the new image ? Press F5 or CTRL-F5 to refresh browser cache.
*
* @param string $source_image The location to the original raw image
* @param string $destination The location to save the new image
* @param int $final_width The desired width of the new image
* @param int $final_height The desired height of the new image
*
* @return bool success state
*/
public static function resizeAvatarImage($source_image, $destination, $final_width = 44, $final_height = 44)
{
$imageData = getimagesize($source_image);
$width = $imageData[0];
$height = $imageData[1];
$mimeType = $imageData['mime'];
if (!$width || !$height) {
return false;
}
switch ($mimeType) {
case 'image/jpeg': $myImage = imagecreatefromjpeg($source_image); break;
case 'image/png': $myImage = imagecreatefrompng($source_image); break;
case 'image/gif': $myImage = imagecreatefromgif($source_image); break;
default: return false;
}
// calculating the part of the image to use for thumbnail
if ($width > $height) {
$verticalCoordinateOfSource = 0;
$horizontalCoordinateOfSource = ($width - $height) / 2;
$smallestSide = $height;
} else {
$horizontalCoordinateOfSource = 0;
$verticalCoordinateOfSource = ($height - $width) / 2;
$smallestSide = $width;
}
// copying the part into thumbnail, maybe edit this for square avatars
$thumb = imagecreatetruecolor($final_width, $final_height);
imagecopyresampled($thumb, $myImage, 0, 0, $horizontalCoordinateOfSource, $verticalCoordinateOfSource, $final_width, $final_height, $smallestSide, $smallestSide);
// add '.jpg' to file path, save it as a .jpg file with our $destination_filename parameter
imagejpeg($thumb, $destination . '.jpg', Config::get('AVATAR_JPEG_QUALITY'));
imagedestroy($thumb);
if (file_exists($destination)) {
return true;
}
return false;
}
/**
* Delete a user's avatar
*
* @param int $userId
* @return bool success
*/
public static function deleteAvatar($userId)
{
if (!ctype_digit($userId)) {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_FAILED"));
return false;
}
// try to delete image, but still go on regardless of file deletion result
self::deleteAvatarImageFile($userId);
$database = DatabaseFactory::getFactory()->getConnection();
$sth = $database->prepare("UPDATE users SET user_has_avatar = 0 WHERE user_id = :user_id LIMIT 1");
$sth->bindValue(":user_id", (int)$userId, PDO::PARAM_INT);
$sth->execute();
if ($sth->rowCount() == 1) {
Session::set('user_avatar_file', self::getPublicUserAvatarFilePathByUserId($userId));
Session::add("feedback_positive", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_SUCCESSFUL"));
return true;
} else {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_FAILED"));
return false;
}
}
/**
* Removes the avatar image file from the filesystem
*
* @param integer $userId
* @return bool
*/
public static function deleteAvatarImageFile($userId)
{
// Check if file exists
if (!file_exists(Config::get('PATH_AVATARS') . $userId . ".jpg")) {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_NO_FILE"));
return false;
}
// Delete avatar file
if (!unlink(Config::get('PATH_AVATARS') . $userId . ".jpg")) {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_FAILED"));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Class CaptchaModel
*
* This model class handles all the captcha stuff.
* Currently this uses the excellent Captcha generator lib from https://github.com/Gregwar/Captcha
* Have a look there for more options etc.
*/
class CaptchaModel
{
/**
* Generates the captcha, "returns" a real image, this is why there is header('Content-type: image/jpeg')
* Note: This is a very special method, as this is echoes out binary data.
*/
public static function generateAndShowCaptcha()
{
// create a captcha with the CaptchaBuilder lib (loaded via Composer)
$captcha = new Gregwar\Captcha\CaptchaBuilder;
$captcha->build(
Config::get('CAPTCHA_WIDTH'),
Config::get('CAPTCHA_HEIGHT')
);
// write the captcha character into session
Session::set('captcha', $captcha->getPhrase());
// render an image showing the characters (=the captcha)
header('Content-type: image/jpeg');
$captcha->output();
}
/**
* Checks if the entered captcha is the same like the one from the rendered image which has been saved in session
* @param $captcha string The captcha characters
* @return bool success of captcha check
*/
public static function checkCaptcha($captcha)
{
if (Session::get('captcha') && ($captcha == Session::get('captcha'))) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,382 @@
<?php
/**
* LoginModel
*
* The login part of the model: Handles the login / logout stuff
*/
class LoginModel
{
/**
* Login process (for DEFAULT user accounts).
*
* @param $user_name string The user's name
* @param $user_password string The user's password
* @param $set_remember_me_cookie mixed Marker for usage of remember-me cookie feature
*
* @return bool success state
*/
public static function login($user_name, $user_password, $set_remember_me_cookie = null)
{
// we do negative-first checks here, for simplicity empty username and empty password in one line
if (empty($user_name) OR empty($user_password)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_FIELD_EMPTY'));
return false;
}
// checks if user exists, if login is not blocked (due to failed logins) and if password fits the hash
$result = self::validateAndGetUser($user_name, $user_password);
// check if that user exists. We don't give back a cause in the feedback to avoid giving an attacker details.
if (!$result) {
//No Need to give feedback here since whole validateAndGetUser controls gives a feedback
return false;
}
// stop the user's login if account has been soft deleted
if ($result->user_deleted == 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_DELETED'));
return false;
}
// stop the user from logging in if user has a suspension, display how long they have left in the feedback.
if ($result->user_suspension_timestamp != null && $result->user_suspension_timestamp - time() > 0) {
$suspensionTimer = Text::get('FEEDBACK_ACCOUNT_SUSPENDED') . round(abs($result->user_suspension_timestamp - time())/60/60, 2) . " hours left";
Session::add('feedback_negative', $suspensionTimer);
return false;
}
// reset the failed login counter for that user (if necessary)
if ($result->user_last_failed_login > 0) {
self::resetFailedLoginCounterOfUser($result->user_name);
}
// save timestamp of this login in the database line of that user
self::saveTimestampOfLoginOfUser($result->user_name);
// if user has checked the "remember me" checkbox, then write token into database and into cookie
if ($set_remember_me_cookie) {
self::setRememberMeInDatabaseAndCookie($result->user_id);
}
// successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true
self::setSuccessfulLoginIntoSession(
$result->user_id, $result->user_name, $result->user_email, $result->user_account_type
);
// return true to make clear the login was successful
// maybe do this in dependence of setSuccessfulLoginIntoSession ?
return true;
}
/**
* Validates the inputs of the users, checks if password is correct etc.
* If successful, user is returned
*
* @param $user_name
* @param $user_password
*
* @return bool|mixed
*/
private static function validateAndGetUser($user_name, $user_password)
{
// brute force attack mitigation: use session failed login count and last failed login for not found users.
// block login attempt if somebody has already failed 3 times and the last login attempt is less than 30sec ago
// (limits user searches in database)
if (Session::get('failed-login-count') >= 3 AND (Session::get('last-failed-login') > (time() - 30))) {
Session::add('feedback_negative', Text::get('FEEDBACK_LOGIN_FAILED_3_TIMES'));
return false;
}
// get all data of that user (to later check if password and password_hash fit)
$result = UserModel::getUserDataByUsername($user_name);
// check if that user exists. We don't give back a cause in the feedback to avoid giving an attacker details.
// brute force attack mitigation: reset failed login counter because of found user
if (!$result) {
// increment the user not found count, helps mitigate user enumeration
self::incrementUserNotFoundCounter();
// user does not exist, but we won't to give a potential attacker this details, so we just use a basic feedback message
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_WRONG'));
return false;
}
// block login attempt if somebody has already failed 3 times and the last login attempt is less than 30sec ago
if (($result->user_failed_logins >= 3) AND ($result->user_last_failed_login > (time() - 30))) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_WRONG_3_TIMES'));
return false;
}
// if hash of provided password does NOT match the hash in the database: +1 failed-login counter
if (!password_verify($user_password, $result->user_password_hash)) {
self::incrementFailedLoginCounterOfUser($result->user_name);
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_WRONG'));
return false;
}
// if user is not active (= has not verified account by verification mail)
if ($result->user_active != 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_NOT_ACTIVATED_YET'));
return false;
}
// reset the user not found counter
self::resetUserNotFoundCounter();
return $result;
}
/**
* Reset the failed-login-count to 0.
* Reset the last-failed-login to an empty string.
*/
private static function resetUserNotFoundCounter()
{
Session::set('failed-login-count', 0);
Session::set('last-failed-login', '');
}
/**
* Increment the failed-login-count by 1.
* Add timestamp to last-failed-login.
*/
private static function incrementUserNotFoundCounter()
{
// Username enumeration prevention: set session failed login count and last failed login for users not found
Session::set('failed-login-count', Session::get('failed-login-count') + 1);
Session::set('last-failed-login', time());
}
/**
* performs the login via cookie (for DEFAULT user account, FACEBOOK-accounts are handled differently)
* TODO add throttling here ?
*
* @param $cookie string The cookie "remember_me"
*
* @return bool success state
*/
public static function loginWithCookie($cookie)
{
// do we have a cookie ?
if (!$cookie) {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
// before list(), check it can be split into 3 strings.
if (count (explode(':', $cookie)) !== 3) {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
// check cookie's contents, check if cookie contents belong together or token is empty
list ($user_id, $token, $hash) = explode(':', $cookie);
// decrypt user id
$user_id = Encryption::decrypt($user_id);
if ($hash !== hash('sha256', $user_id . ':' . $token) OR empty($token) OR empty($user_id)) {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
// get data of user that has this id and this token
$result = UserModel::getUserDataByUserIdAndToken($user_id, $token);
// if user with that id and exactly that cookie token exists in database
if ($result) {
// successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true
self::setSuccessfulLoginIntoSession($result->user_id, $result->user_name, $result->user_email, $result->user_account_type);
// save timestamp of this login in the database line of that user
self::saveTimestampOfLoginOfUser($result->user_name);
// NOTE: we don't set another remember_me-cookie here as the current cookie should always
// be invalid after a certain amount of time, so the user has to login with username/password
// again from time to time. This is good and safe ! ;)
Session::add('feedback_positive', Text::get('FEEDBACK_COOKIE_LOGIN_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
}
/**
* Log out process: delete cookie, delete session
*/
public static function logout()
{
$user_id = Session::get('user_id');
self::deleteCookie($user_id);
Session::destroy();
Session::updateSessionId($user_id);
}
/**
* The real login process: The user's data is written into the session.
* Cheesy name, maybe rename. Also maybe refactoring this, using an array.
*
* @param $user_id
* @param $user_name
* @param $user_email
* @param $user_account_type
*/
public static function setSuccessfulLoginIntoSession($user_id, $user_name, $user_email, $user_account_type)
{
Session::init();
// remove old and regenerate session ID.
// It's important to regenerate session on sensitive actions,
// and to avoid fixated session.
// e.g. when a user logs in
session_regenerate_id(true);
$_SESSION = array();
Session::set('user_id', $user_id);
Session::set('user_name', $user_name);
Session::set('user_email', $user_email);
Session::set('user_account_type', $user_account_type);
Session::set('user_provider_type', 'DEFAULT');
// get and set avatars
Session::set('user_avatar_file', AvatarModel::getPublicUserAvatarFilePathByUserId($user_id));
Session::set('user_gravatar_image_url', AvatarModel::getGravatarLinkByEmail($user_email));
// finally, set user as logged-in
Session::set('user_logged_in', true);
// update session id in database
Session::updateSessionId($user_id, session_id());
// set session cookie setting manually,
// Why? because you need to explicitly set session expiry, path, domain, secure, and HTTP.
// @see https://www.owasp.org/index.php/PHP_Security_Cheat_Sheet#Cookies
setcookie(session_name(), session_id(), time() + Config::get('SESSION_RUNTIME'), Config::get('COOKIE_PATH'),
Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
}
/**
* Increments the failed-login counter of a user
*
* @param $user_name
*/
public static function incrementFailedLoginCounterOfUser($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users
SET user_failed_logins = user_failed_logins+1, user_last_failed_login = :user_last_failed_login
WHERE user_name = :user_name OR user_email = :user_name
LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_name' => $user_name, ':user_last_failed_login' => time() ));
}
/**
* Resets the failed-login counter of a user back to 0
*
* @param $user_name
*/
public static function resetFailedLoginCounterOfUser($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users
SET user_failed_logins = 0, user_last_failed_login = NULL
WHERE user_name = :user_name AND user_failed_logins != 0
LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_name' => $user_name));
}
/**
* Write timestamp of this login into database (we only write a "real" login via login form into the database,
* not the session-login on every page request
*
* @param $user_name
*/
public static function saveTimestampOfLoginOfUser($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_last_login_timestamp = :user_last_login_timestamp
WHERE user_name = :user_name LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_name' => $user_name, ':user_last_login_timestamp' => time()));
}
/**
* Write remember-me token into database and into cookie
* Maybe splitting this into database and cookie part ?
*
* @param $user_id
*/
public static function setRememberMeInDatabaseAndCookie($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
// generate 64 char random string
$random_token_string = hash('sha256', mt_rand());
// write that token into database
$sql = "UPDATE users SET user_remember_me_token = :user_remember_me_token WHERE user_id = :user_id LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_remember_me_token' => $random_token_string, ':user_id' => $user_id));
// generate cookie string that consists of user id, random string and combined hash of both
// never expose the original user id, instead, encrypt it.
$cookie_string_first_part = Encryption::encrypt($user_id) . ':' . $random_token_string;
$cookie_string_hash = hash('sha256', $user_id . ':' . $random_token_string);
$cookie_string = $cookie_string_first_part . ':' . $cookie_string_hash;
// set cookie, and make it available only for the domain created on (to avoid XSS attacks, where the
// attacker could steal your remember-me cookie string and would login itself).
// If you are using HTTPS, then you should set the "secure" flag (the second one from right) to true, too.
// @see http://www.php.net/manual/en/function.setcookie.php
setcookie('remember_me', $cookie_string, time() + Config::get('COOKIE_RUNTIME'), Config::get('COOKIE_PATH'),
Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
}
/**
* Deletes the cookie
* It's necessary to split deleteCookie() and logout() as cookies are deleted without logging out too!
* Sets the remember-me-cookie to ten years ago (3600sec * 24 hours * 365 days * 10).
* that's obviously the best practice to kill a cookie @see http://stackoverflow.com/a/686166/1114320
*
* @param string $user_id
*/
public static function deleteCookie($user_id = null)
{
// is $user_id was set, then clear remember_me token in database
if (isset($user_id)) {
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_remember_me_token = :user_remember_me_token WHERE user_id = :user_id LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_remember_me_token' => null, ':user_id' => $user_id));
}
// delete remember_me cookie in browser
setcookie('remember_me', false, time() - (3600 * 24 * 3650), Config::get('COOKIE_PATH'),
Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
}
/**
* Returns the current state of the user's login
*
* @return bool user's login status
*/
public static function isUserLoggedIn()
{
return Session::userIsLoggedIn();
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* NoteModel
* This is basically a simple CRUD (Create/Read/Update/Delete) demonstration.
*/
class NoteModel
{
/**
* Get all notes (notes are just example data that the user has created)
* @return array an array with several objects (the results)
*/
public static function getAllNotes()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => Session::get('user_id')));
// fetchAll() is the PDO method that gets all result rows
return $query->fetchAll();
}
/**
* Get a single note
* @param int $note_id id of the specific note
* @return object a single object (the result)
*/
public static function getNote($note_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id AND note_id = :note_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => Session::get('user_id'), ':note_id' => $note_id));
// fetch() is the PDO method that gets a single result
return $query->fetch();
}
/**
* Set a note (create a new one)
* @param string $note_text note text that will be created
* @return bool feedback (was the note created properly ?)
*/
public static function createNote($note_text)
{
if (!$note_text || strlen($note_text) == 0) {
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_CREATION_FAILED'));
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "INSERT INTO notes (note_text, user_id) VALUES (:note_text, :user_id)";
$query = $database->prepare($sql);
$query->execute(array(':note_text' => $note_text, ':user_id' => Session::get('user_id')));
if ($query->rowCount() == 1) {
return true;
}
// default return
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_CREATION_FAILED'));
return false;
}
/**
* Update an existing note
* @param int $note_id id of the specific note
* @param string $note_text new text of the specific note
* @return bool feedback (was the update successful ?)
*/
public static function updateNote($note_id, $note_text)
{
if (!$note_id || !$note_text) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE notes SET note_text = :note_text WHERE note_id = :note_id AND user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':note_id' => $note_id, ':note_text' => $note_text, ':user_id' => Session::get('user_id')));
if ($query->rowCount() == 1) {
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_EDITING_FAILED'));
return false;
}
/**
* Delete a specific note
* @param int $note_id id of the note
* @return bool feedback (was the note deleted properly ?)
*/
public static function deleteNote($note_id)
{
if (!$note_id) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "DELETE FROM notes WHERE note_id = :note_id AND user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':note_id' => $note_id, ':user_id' => Session::get('user_id')));
if ($query->rowCount() == 1) {
return true;
}
// default return
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_DELETION_FAILED'));
return false;
}
}

View File

@@ -0,0 +1,365 @@
<?php
/**
* Class PasswordResetModel
*
* Handles all the stuff that is related to the password-reset process
*/
class PasswordResetModel
{
/**
* Perform the necessary actions to send a password reset mail
*
* @param $user_name_or_email string Username or user's email
* @param $captcha string Captcha string
*
* @return bool success status
*/
public static function requestPasswordReset($user_name_or_email, $captcha)
{
if (!CaptchaModel::checkCaptcha($captcha)) {
Session::add('feedback_negative', Text::get('FEEDBACK_CAPTCHA_WRONG'));
return false;
}
if (empty($user_name_or_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_EMAIL_FIELD_EMPTY'));
return false;
}
// check if that username exists
$result = UserModel::getUserDataByUserNameOrEmail($user_name_or_email);
if (!$result) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST'));
return false;
}
// generate integer-timestamp (to see when exactly the user (or an attacker) requested the password reset mail)
// generate random hash for email password reset verification (40 bytes)
$temporary_timestamp = time();
$user_password_reset_hash = bin2hex(random_bytes(40));
// set token (= a random hash string and a timestamp) into database ...
$token_set = self::setPasswordResetDatabaseToken($result->user_name, $user_password_reset_hash, $temporary_timestamp);
if (!$token_set) {
return false;
}
// ... and send a mail to the user, containing a link with username and token hash string
$mail_sent = self::sendPasswordResetMail($result->user_name, $user_password_reset_hash, $result->user_email);
if ($mail_sent) {
return true;
}
// default return
return false;
}
/**
* Set password reset token in database (for DEFAULT user accounts)
*
* @param string $user_name username
* @param string $user_password_reset_hash password reset hash
* @param int $temporary_timestamp timestamp
*
* @return bool success status
*/
public static function setPasswordResetDatabaseToken($user_name, $user_password_reset_hash, $temporary_timestamp)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users
SET user_password_reset_hash = :user_password_reset_hash, user_password_reset_timestamp = :user_password_reset_timestamp
WHERE user_name = :user_name AND user_provider_type = :provider_type LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_reset_hash' => $user_password_reset_hash, ':user_name' => $user_name,
':user_password_reset_timestamp' => $temporary_timestamp, ':provider_type' => 'DEFAULT'
));
// check if exactly one row was successfully changed
if ($query->rowCount() == 1) {
return true;
}
// fallback
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_FAIL'));
return false;
}
/**
* Send the password reset mail
*
* @param string $user_name username
* @param string $user_password_reset_hash password reset hash
* @param string $user_email user email
*
* @return bool success status
*/
public static function sendPasswordResetMail($user_name, $user_password_reset_hash, $user_email)
{
// create email body
$body = Config::get('EMAIL_PASSWORD_RESET_CONTENT') . ' ' . Config::get('URL') .
Config::get('EMAIL_PASSWORD_RESET_URL') . '/' . urlencode($user_name) . '/' . urlencode($user_password_reset_hash);
// create instance of Mail class, try sending and check
$mail = new Mail;
$mail_sent = $mail->sendMail($user_email, Config::get('EMAIL_PASSWORD_RESET_FROM_EMAIL'),
Config::get('EMAIL_PASSWORD_RESET_FROM_NAME'), Config::get('EMAIL_PASSWORD_RESET_SUBJECT'), $body
);
if ($mail_sent) {
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_RESET_MAIL_SENDING_SUCCESSFUL'));
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_MAIL_SENDING_ERROR') . $mail->getError() );
return false;
}
/**
* Verifies the password reset request via the verification hash token (that's only valid for one hour)
* @param string $user_name Username
* @param string $verification_code Hash token
* @return bool Success status
*/
public static function verifyPasswordReset($user_name, $verification_code)
{
$database = DatabaseFactory::getFactory()->getConnection();
// check if user-provided username + verification code combination exists
$sql = "SELECT user_id, user_password_reset_timestamp
FROM users
WHERE user_name = :user_name
AND user_password_reset_hash = :user_password_reset_hash
AND user_provider_type = :user_provider_type
LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_reset_hash' => $verification_code, ':user_name' => $user_name,
':user_provider_type' => 'DEFAULT'
));
// if this user with exactly this verification hash code does NOT exist
if ($query->rowCount() != 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_COMBINATION_DOES_NOT_EXIST'));
return false;
}
// get result row (as an object)
$result_user_row = $query->fetch();
// 3600 seconds are 1 hour
$timestamp_one_hour_ago = time() - 3600;
// if password reset request was sent within the last hour (this timeout is for security reasons)
if ($result_user_row->user_password_reset_timestamp > $timestamp_one_hour_ago) {
// verification was successful
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_RESET_LINK_VALID'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_LINK_EXPIRED'));
return false;
}
}
/**
* Writes the new password to the database
*
* @param string $user_name username
* @param string $user_password_hash
* @param string $user_password_reset_hash
*
* @return bool
*/
public static function saveNewUserPassword($user_name, $user_password_hash, $user_password_reset_hash)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_password_hash = :user_password_hash, user_password_reset_hash = NULL,
user_password_reset_timestamp = NULL
WHERE user_name = :user_name AND user_password_reset_hash = :user_password_reset_hash
AND user_provider_type = :user_provider_type LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_hash' => $user_password_hash, ':user_name' => $user_name,
':user_password_reset_hash' => $user_password_reset_hash, ':user_provider_type' => 'DEFAULT'
));
// if one result exists, return true, else false. Could be written even shorter btw.
return ($query->rowCount() == 1 ? true : false);
}
/**
* Set the new password (for DEFAULT user, FACEBOOK-users don't have a password)
* Please note: At this point the user has already pre-verified via verifyPasswordReset() (within one hour),
* so we don't need to check again for the 60min-limit here. In this method we authenticate
* via username & password-reset-hash from (hidden) form fields.
*
* @param string $user_name
* @param string $user_password_reset_hash
* @param string $user_password_new
* @param string $user_password_repeat
*
* @return bool success state of the password reset
*/
public static function setNewPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)
{
// validate the password
if (!self::validateResetPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)) {
return false;
}
// crypt the password (with the PHP 5.5+'s password_hash() function, result is a 60 character hash string)
$user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT);
// write the password to database (as hashed and salted string), reset user_password_reset_hash
if (self::saveNewUserPassword($user_name, $user_password_hash, $user_password_reset_hash)) {
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CHANGE_FAILED'));
return false;
}
}
/**
* Validate the password submission
*
* @param $user_name
* @param $user_password_reset_hash
* @param $user_password_new
* @param $user_password_repeat
*
* @return bool
*/
public static function validateResetPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)
{
if (empty($user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_FIELD_EMPTY'));
return false;
} else if (empty($user_password_reset_hash)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_MISSING'));
return false;
} else if (empty($user_password_new) || empty($user_password_repeat)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY'));
return false;
} else if ($user_password_new !== $user_password_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG'));
return false;
} else if (strlen($user_password_new) < 6) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT'));
return false;
}
return true;
}
/**
* Writes the new password to the database
*
* @param string $user_name
* @param string $user_password_hash
*
* @return bool
*/
public static function saveChangedPassword($user_name, $user_password_hash)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_password_hash = :user_password_hash
WHERE user_name = :user_name
AND user_provider_type = :user_provider_type LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_hash' => $user_password_hash, ':user_name' => $user_name,
':user_provider_type' => 'DEFAULT'
));
// if one result exists, return true, else false. Could be written even shorter btw.
return ($query->rowCount() == 1 ? true : false);
}
/**
* Validates fields, hashes new password, saves new password
*
* @param string $user_name
* @param string $user_password_current
* @param string $user_password_new
* @param string $user_password_repeat
*
* @return bool
*/
public static function changePassword($user_name, $user_password_current, $user_password_new, $user_password_repeat)
{
// validate the passwords
if (!self::validatePasswordChange($user_name, $user_password_current, $user_password_new, $user_password_repeat)) {
return false;
}
// crypt the password (with the PHP 5.5+'s password_hash() function, result is a 60 character hash string)
$user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT);
// write the password to database (as hashed and salted string)
if (self::saveChangedPassword($user_name, $user_password_hash)) {
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CHANGE_FAILED'));
return false;
}
}
/**
* Validates current and new passwords
*
* @param string $user_name
* @param string $user_password_current
* @param string $user_password_new
* @param string $user_password_repeat
*
* @return bool
*/
public static function validatePasswordChange($user_name, $user_password_current, $user_password_new, $user_password_repeat)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_password_hash, user_failed_logins FROM users WHERE user_name = :user_name LIMIT 1;";
$query = $database->prepare($sql);
$query->execute(array(
':user_name' => $user_name
));
$user = $query->fetch();
if ($query->rowCount() == 1) {
$user_password_hash = $user->user_password_hash;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST'));
return false;
}
if (!password_verify($user_password_current, $user_password_hash)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CURRENT_INCORRECT'));
return false;
} else if (empty($user_password_new) || empty($user_password_repeat)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY'));
return false;
} else if ($user_password_new !== $user_password_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG'));
return false;
} else if (strlen($user_password_new) < 6) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT'));
return false;
} else if ($user_password_current == $user_password_new){
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_NEW_SAME_AS_CURRENT'));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,293 @@
<?php
/**
* Class RegistrationModel
*
* Everything registration-related happens here.
*/
class RegistrationModel
{
/**
* Handles the entire registration process for DEFAULT users (not for people who register with
* 3rd party services, like facebook) and creates a new user in the database if everything is fine
*
* @return boolean Gives back the success status of the registration
*/
public static function registerNewUser()
{
// clean the input
$user_name = strip_tags(Request::post('user_name'));
$user_email = strip_tags(Request::post('user_email'));
$user_email_repeat = strip_tags(Request::post('user_email_repeat'));
$user_password_new = Request::post('user_password_new');
$user_password_repeat = Request::post('user_password_repeat');
// stop registration flow if registrationInputValidation() returns false (= anything breaks the input check rules)
$validation_result = self::registrationInputValidation(Request::post('captcha'), $user_name, $user_password_new, $user_password_repeat, $user_email, $user_email_repeat);
if (!$validation_result) {
return false;
}
// crypt the password with the PHP 5.5's password_hash() function, results in a 60 character hash string.
// @see php.net/manual/en/function.password-hash.php for more, especially for potential options
$user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT);
// make return a bool variable, so both errors can come up at once if needed
$return = true;
// check if username already exists
if (UserModel::doesUsernameAlreadyExist($user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_ALREADY_TAKEN'));
$return = false;
}
// check if email already exists
if (UserModel::doesEmailAlreadyExist($user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_EMAIL_ALREADY_TAKEN'));
$return = false;
}
// if Username or Email were false, return false
if (!$return) return false;
// generate random hash for email verification (40 bytes)
$user_activation_hash = bin2hex(random_bytes(40));
// write user data to database
if (!self::writeNewUserToDatabase($user_name, $user_password_hash, $user_email, time(), $user_activation_hash)) {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_CREATION_FAILED'));
return false; // no reason not to return false here
}
// get user_id of the user that has been created, to keep things clean we DON'T use lastInsertId() here
$user_id = UserModel::getUserIdByUsername($user_name);
if (!$user_id) {
Session::add('feedback_negative', Text::get('FEEDBACK_UNKNOWN_ERROR'));
return false;
}
// send verification email
if (self::sendVerificationEmail($user_id, $user_email, $user_activation_hash)) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_SUCCESSFULLY_CREATED'));
return true;
}
// if verification email sending failed: instantly delete the user
self::rollbackRegistrationByUserId($user_id);
Session::add('feedback_negative', Text::get('FEEDBACK_VERIFICATION_MAIL_SENDING_FAILED'));
return false;
}
/**
* Validates the registration input
*
* @param $captcha
* @param $user_name
* @param $user_password_new
* @param $user_password_repeat
* @param $user_email
* @param $user_email_repeat
*
* @return bool
*/
public static function registrationInputValidation($captcha, $user_name, $user_password_new, $user_password_repeat, $user_email, $user_email_repeat)
{
$return = true;
// perform all necessary checks
if (!CaptchaModel::checkCaptcha($captcha)) {
Session::add('feedback_negative', Text::get('FEEDBACK_CAPTCHA_WRONG'));
$return = false;
}
// if username, email and password are all correctly validated, but make sure they all run on first sumbit
if (self::validateUserName($user_name) AND self::validateUserEmail($user_email, $user_email_repeat) AND self::validateUserPassword($user_password_new, $user_password_repeat) AND $return) {
return true;
}
// otherwise, return false
return false;
}
/**
* Validates the username
*
* @param $user_name
* @return bool
*/
public static function validateUserName($user_name)
{
if (empty($user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_FIELD_EMPTY'));
return false;
}
// if username is too short (2), too long (64) or does not fit the pattern (aZ09)
if (!preg_match('/^[a-zA-Z0-9]{2,64}$/', $user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN'));
return false;
}
return true;
}
/**
* Validates the email
*
* @param $user_email
* @param $user_email_repeat
* @return bool
*/
public static function validateUserEmail($user_email, $user_email_repeat)
{
if (empty($user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_FIELD_EMPTY'));
return false;
}
if ($user_email !== $user_email_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_REPEAT_WRONG'));
return false;
}
// validate the email with PHP's internal filter
// side-fact: Max length seems to be 254 chars
// @see http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
if (!filter_var($user_email, FILTER_VALIDATE_EMAIL)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_DOES_NOT_FIT_PATTERN'));
return false;
}
return true;
}
/**
* Validates the password
*
* @param $user_password_new
* @param $user_password_repeat
* @return bool
*/
public static function validateUserPassword($user_password_new, $user_password_repeat)
{
if (empty($user_password_new) OR empty($user_password_repeat)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY'));
return false;
}
if ($user_password_new !== $user_password_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG'));
return false;
}
if (strlen($user_password_new) < 6) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT'));
return false;
}
return true;
}
/**
* Writes the new user's data to the database
*
* @param $user_name
* @param $user_password_hash
* @param $user_email
* @param $user_creation_timestamp
* @param $user_activation_hash
*
* @return bool
*/
public static function writeNewUserToDatabase($user_name, $user_password_hash, $user_email, $user_creation_timestamp, $user_activation_hash)
{
$database = DatabaseFactory::getFactory()->getConnection();
// write new users data into database
$sql = "INSERT INTO users (user_name, user_password_hash, user_email, user_creation_timestamp, user_activation_hash, user_provider_type)
VALUES (:user_name, :user_password_hash, :user_email, :user_creation_timestamp, :user_activation_hash, :user_provider_type)";
$query = $database->prepare($sql);
$query->execute(array(':user_name' => $user_name,
':user_password_hash' => $user_password_hash,
':user_email' => $user_email,
':user_creation_timestamp' => $user_creation_timestamp,
':user_activation_hash' => $user_activation_hash,
':user_provider_type' => 'DEFAULT'));
$count = $query->rowCount();
if ($count == 1) {
return true;
}
return false;
}
/**
* Deletes the user from users table. Currently used to rollback a registration when verification mail sending
* was not successful.
*
* @param $user_id
*/
public static function rollbackRegistrationByUserId($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("DELETE FROM users WHERE user_id = :user_id");
$query->execute(array(':user_id' => $user_id));
}
/**
* Sends the verification email (to confirm the account).
* The construction of the mail $body looks weird at first, but it's really just a simple string.
*
* @param int $user_id user's id
* @param string $user_email user's email
* @param string $user_activation_hash user's mail verification hash string
*
* @return boolean gives back true if mail has been sent, gives back false if no mail could been sent
*/
public static function sendVerificationEmail($user_id, $user_email, $user_activation_hash)
{
$body = Config::get('EMAIL_VERIFICATION_CONTENT') . Config::get('URL') . Config::get('EMAIL_VERIFICATION_URL')
. '/' . urlencode($user_id) . '/' . urlencode($user_activation_hash);
$mail = new Mail;
$mail_sent = $mail->sendMail($user_email, Config::get('EMAIL_VERIFICATION_FROM_EMAIL'),
Config::get('EMAIL_VERIFICATION_FROM_NAME'), Config::get('EMAIL_VERIFICATION_SUBJECT'), $body
);
if ($mail_sent) {
Session::add('feedback_positive', Text::get('FEEDBACK_VERIFICATION_MAIL_SENDING_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_VERIFICATION_MAIL_SENDING_ERROR') . $mail->getError() );
return false;
}
}
/**
* checks the email/verification code combination and set the user's activation status to true in the database
*
* @param int $user_id user id
* @param string $user_activation_verification_code verification token
*
* @return bool success status
*/
public static function verifyNewUser($user_id, $user_activation_verification_code)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_active = 1, user_activation_hash = NULL
WHERE user_id = :user_id AND user_activation_hash = :user_activation_hash LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => $user_id, ':user_activation_hash' => $user_activation_verification_code));
if ($query->rowCount() == 1) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_ACTIVATION_SUCCESSFUL'));
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_ACTIVATION_FAILED'));
return false;
}
}

View File

@@ -0,0 +1,343 @@
<?php
/**
* UserModel
* Handles all the PUBLIC profile stuff. This is not for getting data of the logged in user, it's more for handling
* data of all the other users. Useful for display profile information, creating user lists etc.
*/
class UserModel
{
/**
* Gets an array that contains all the users in the database. The array's keys are the user ids.
* Each array element is an object, containing a specific user's data.
* The avatar line is built using Ternary Operators, have a look here for more:
* @see http://davidwalsh.name/php-shorthand-if-else-ternary-operators
*
* @return array The profiles of all users
*/
public static function getPublicProfilesOfAllUsers()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_active, user_has_avatar, user_deleted FROM users";
$query = $database->prepare($sql);
$query->execute();
$all_users_profiles = array();
foreach ($query->fetchAll() as $user) {
// all elements of array passed to Filter::XSSFilter for XSS sanitation, have a look into
// application/core/Filter.php for more info on how to use. Removes (possibly bad) JavaScript etc from
// the user's values
array_walk_recursive($user, 'Filter::XSSFilter');
$all_users_profiles[$user->user_id] = new stdClass();
$all_users_profiles[$user->user_id]->user_id = $user->user_id;
$all_users_profiles[$user->user_id]->user_name = $user->user_name;
$all_users_profiles[$user->user_id]->user_email = $user->user_email;
$all_users_profiles[$user->user_id]->user_active = $user->user_active;
$all_users_profiles[$user->user_id]->user_deleted = $user->user_deleted;
$all_users_profiles[$user->user_id]->user_avatar_link = (Config::get('USE_GRAVATAR') ? AvatarModel::getGravatarLinkByEmail($user->user_email) : AvatarModel::getPublicAvatarFilePathOfUser($user->user_has_avatar, $user->user_id));
}
return $all_users_profiles;
}
/**
* Gets a user's profile data, according to the given $user_id
* @param int $user_id The user's id
* @return mixed The selected user's profile
*/
public static function getPublicProfileOfUser($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_active, user_has_avatar, user_deleted
FROM users WHERE user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => $user_id));
$user = $query->fetch();
if ($query->rowCount() == 1) {
if (Config::get('USE_GRAVATAR')) {
$user->user_avatar_link = AvatarModel::getGravatarLinkByEmail($user->user_email);
} else {
$user->user_avatar_link = AvatarModel::getPublicAvatarFilePathOfUser($user->user_has_avatar, $user->user_id);
}
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST'));
}
// all elements of array passed to Filter::XSSFilter for XSS sanitation, have a look into
// application/core/Filter.php for more info on how to use. Removes (possibly bad) JavaScript etc from
// the user's values
array_walk_recursive($user, 'Filter::XSSFilter');
return $user;
}
/**
* @param $user_name_or_email
*
* @return mixed
*/
public static function getUserDataByUserNameOrEmail($user_name_or_email)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_id, user_name, user_email FROM users
WHERE (user_name = :user_name_or_email OR user_email = :user_name_or_email)
AND user_provider_type = :provider_type LIMIT 1");
$query->execute(array(':user_name_or_email' => $user_name_or_email, ':provider_type' => 'DEFAULT'));
return $query->fetch();
}
/**
* Checks if a username is already taken
*
* @param $user_name string username
*
* @return bool
*/
public static function doesUsernameAlreadyExist($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_id FROM users WHERE user_name = :user_name LIMIT 1");
$query->execute(array(':user_name' => $user_name));
if ($query->rowCount() == 0) {
return false;
}
return true;
}
/**
* Checks if a email is already used
*
* @param $user_email string email
*
* @return bool
*/
public static function doesEmailAlreadyExist($user_email)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_id FROM users WHERE user_email = :user_email LIMIT 1");
$query->execute(array(':user_email' => $user_email));
if ($query->rowCount() == 0) {
return false;
}
return true;
}
/**
* Writes new username to database
*
* @param $user_id int user id
* @param $new_user_name string new username
*
* @return bool
*/
public static function saveNewUserName($user_id, $new_user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_name = :user_name WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_name' => $new_user_name, ':user_id' => $user_id));
if ($query->rowCount() == 1) {
return true;
}
return false;
}
/**
* Writes new email address to database
*
* @param $user_id int user id
* @param $new_user_email string new email address
*
* @return bool
*/
public static function saveNewEmailAddress($user_id, $new_user_email)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_email = :user_email WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_email' => $new_user_email, ':user_id' => $user_id));
$count = $query->rowCount();
if ($count == 1) {
return true;
}
return false;
}
/**
* Edit the user's name, provided in the editing form
*
* @param $new_user_name string The new username
*
* @return bool success status
*/
public static function editUserName($new_user_name)
{
// new username same as old one ?
if ($new_user_name == Session::get('user_name')) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_SAME_AS_OLD_ONE'));
return false;
}
// username cannot be empty and must be azAZ09 and 2-64 characters
if (!preg_match("/^[a-zA-Z0-9]{2,64}$/", $new_user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN'));
return false;
}
// clean the input, strip usernames longer than 64 chars (maybe fix this ?)
$new_user_name = substr(strip_tags($new_user_name), 0, 64);
// check if new username already exists
if (self::doesUsernameAlreadyExist($new_user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_ALREADY_TAKEN'));
return false;
}
$status_of_action = self::saveNewUserName(Session::get('user_id'), $new_user_name);
if ($status_of_action) {
Session::set('user_name', $new_user_name);
Session::add('feedback_positive', Text::get('FEEDBACK_USERNAME_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_UNKNOWN_ERROR'));
return false;
}
}
/**
* Edit the user's email
*
* @param $new_user_email
*
* @return bool success status
*/
public static function editUserEmail($new_user_email)
{
// email provided ?
if (empty($new_user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_FIELD_EMPTY'));
return false;
}
// check if new email is same like the old one
if ($new_user_email == Session::get('user_email')) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_SAME_AS_OLD_ONE'));
return false;
}
// user's email must be in valid email format, also checks the length
// @see http://stackoverflow.com/questions/21631366/php-filter-validate-email-max-length
// @see http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
if (!filter_var($new_user_email, FILTER_VALIDATE_EMAIL)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_DOES_NOT_FIT_PATTERN'));
return false;
}
// strip tags, just to be sure
$new_user_email = substr(strip_tags($new_user_email), 0, 254);
// check if user's email already exists
if (self::doesEmailAlreadyExist($new_user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_EMAIL_ALREADY_TAKEN'));
return false;
}
// write to database, if successful ...
// ... then write new email to session, Gravatar too (as this relies to the user's email address)
if (self::saveNewEmailAddress(Session::get('user_id'), $new_user_email)) {
Session::set('user_email', $new_user_email);
Session::set('user_gravatar_image_url', AvatarModel::getGravatarLinkByEmail($new_user_email));
Session::add('feedback_positive', Text::get('FEEDBACK_EMAIL_CHANGE_SUCCESSFUL'));
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_UNKNOWN_ERROR'));
return false;
}
/**
* Gets the user's id
*
* @param $user_name
*
* @return mixed
*/
public static function getUserIdByUsername($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id FROM users WHERE user_name = :user_name AND user_provider_type = :provider_type LIMIT 1";
$query = $database->prepare($sql);
// DEFAULT is the marker for "normal" accounts (that have a password etc.)
// There are other types of accounts that don't have passwords etc. (FACEBOOK)
$query->execute(array(':user_name' => $user_name, ':provider_type' => 'DEFAULT'));
// return one row (we only have one result or nothing)
return $query->fetch()->user_id;
}
/**
* Gets the user's data
*
* @param $user_name string User's name
*
* @return mixed Returns false if user does not exist, returns object with user's data when user exists
*/
public static function getUserDataByUsername($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_password_hash, user_active,user_deleted, user_suspension_timestamp, user_account_type,
user_failed_logins, user_last_failed_login
FROM users
WHERE (user_name = :user_name OR user_email = :user_name)
AND user_provider_type = :provider_type
LIMIT 1";
$query = $database->prepare($sql);
// DEFAULT is the marker for "normal" accounts (that have a password etc.)
// There are other types of accounts that don't have passwords etc. (FACEBOOK)
$query->execute(array(':user_name' => $user_name, ':provider_type' => 'DEFAULT'));
// return one row (we only have one result or nothing)
return $query->fetch();
}
/**
* Gets the user's data by user's id and a token (used by login-via-cookie process)
*
* @param $user_id
* @param $token
*
* @return mixed Returns false if user does not exist, returns object with user's data when user exists
*/
public static function getUserDataByUserIdAndToken($user_id, $token)
{
$database = DatabaseFactory::getFactory()->getConnection();
// get real token from database (and all other data)
$query = $database->prepare("SELECT user_id, user_name, user_email, user_password_hash, user_active,
user_account_type, user_has_avatar, user_failed_logins, user_last_failed_login
FROM users
WHERE user_id = :user_id
AND user_remember_me_token = :user_remember_me_token
AND user_remember_me_token IS NOT NULL
AND user_provider_type = :provider_type LIMIT 1");
$query->execute(array(':user_id' => $user_id, ':user_remember_me_token' => $token, ':provider_type' => 'DEFAULT'));
// return one row (we only have one result or nothing)
return $query->fetch();
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* Class UserRoleModel
*
* This class contains everything that is related to up- and downgrading accounts.
*/
class UserRoleModel
{
/**
* Upgrades / downgrades the user's account. Currently it's just the field user_account_type in the database that
* can be 1 or 2 (maybe "basic" or "premium"). Put some more complex stuff in here, maybe a pay-process or whatever
* you like.
*
* @param $type
*
* @return bool
*/
public static function changeUserRole($type)
{
if (!$type) {
return false;
}
// save new role to database
if (self::saveRoleToDatabase($type)) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_TYPE_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_TYPE_CHANGE_FAILED'));
return false;
}
}
/**
* Writes the new account type marker to the database and to the session
*
* @param $type
*
* @return bool
*/
public static function saveRoleToDatabase($type)
{
// if $type is not 1 or 2
if (!in_array($type, [1, 2])) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_account_type = :new_type WHERE user_id = :user_id LIMIT 1");
$query->execute(array(
':new_type' => $type,
':user_id' => Session::get('user_id')
));
if ($query->rowCount() == 1) {
// set account type in session
Session::set('user_account_type', $type);
return true;
}
return false;
}
}