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 @@
CREATE DATABASE IF NOT EXISTS `huge`;

View File

@@ -0,0 +1,31 @@
CREATE TABLE IF NOT EXISTS `huge`.`users` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'auto incrementing user_id of each user, unique index',
`session_id` varchar(48) DEFAULT NULL COMMENT 'stores session cookie id to prevent session concurrency',
`user_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'user''s name, unique',
`user_password_hash` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s password in salted and hashed format',
`user_email` varchar(254) COLLATE utf8_unicode_ci NOT NULL COMMENT 'user''s email, unique',
`user_active` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'user''s activation status',
`user_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'user''s deletion status',
`user_account_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'user''s account type (basic, premium, etc)',
`user_has_avatar` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1 if user has a local avatar, 0 if not',
`user_remember_me_token` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s remember-me cookie token',
`user_creation_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of the creation of user''s account',
`user_suspension_timestamp` bigint(20) DEFAULT NULL COMMENT 'Timestamp till the end of a user suspension',
`user_last_login_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of user''s last login',
`user_failed_logins` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'user''s failed login attempts',
`user_last_failed_login` int(10) DEFAULT NULL COMMENT 'unix timestamp of last failed login attempt',
`user_activation_hash` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s email verification hash string',
`user_password_reset_hash` char(80) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s password reset code',
`user_password_reset_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of the password reset request',
`user_provider_type` text COLLATE utf8_unicode_ci,
PRIMARY KEY (`user_id`),
UNIQUE KEY `user_name` (`user_name`),
UNIQUE KEY `user_email` (`user_email`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user data';
INSERT INTO `huge`.`users` (`user_id`, `session_id`, `user_name`, `user_password_hash`, `user_email`, `user_active`, `user_deleted`, `user_account_type`,
`user_has_avatar`, `user_remember_me_token`, `user_creation_timestamp`, `user_suspension_timestamp`, `user_last_login_timestamp`,
`user_failed_logins`, `user_last_failed_login`, `user_activation_hash`, `user_password_reset_hash`,
`user_password_reset_timestamp`, `user_provider_type`) VALUES
(1, NULL, 'demo', '$2y$10$OvprunjvKOOhM1h9bzMPs.vuwGIsOqZbw88rzSyGCTJTcE61g5WXi', 'demo@demo.com', 1, 0, 7, 0, NULL, 1422205178, NULL, 1422209189, 0, NULL, NULL, NULL, NULL, 'DEFAULT'),
(2, NULL, 'demo2', '$2y$10$OvprunjvKOOhM1h9bzMPs.vuwGIsOqZbw88rzSyGCTJTcE61g5WXi', 'demo2@demo.com', 1, 0, 1, 0, NULL, 1422205178, NULL, 1422209189, 0, NULL, NULL, NULL, NULL, 'DEFAULT');

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `huge`.`notes` (
`note_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`note_text` text NOT NULL,
`user_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`note_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user notes';

View File

@@ -0,0 +1,156 @@
<?php
/**
* Configuration for DEVELOPMENT environment
* To create another configuration set just copy this file to config.production.php etc. You get the idea :)
*/
/**
* Configuration for: Error reporting
* Useful to show every little problem during development, but only show hard / no errors in production.
* It's a little bit dirty to put this here, but who cares. For development purposes it's totally okay.
*/
error_reporting(E_ALL);
ini_set("display_errors", 1);
/**
* Configuration for cookie security
* Quote from PHP manual: Marks the cookie as accessible only through the HTTP protocol. This means that the cookie
* won't be accessible by scripting languages, such as JavaScript. This setting can effectively help to reduce identity
* theft through XSS attacks (although it is not supported by all browsers).
*
* @see php.net/manual/en/session.configuration.php#ini.session.cookie-httponly
*/
ini_set('session.cookie_httponly', 1);
/**
* Returns the full configuration.
* This is used by the core/Config class.
*/
return array(
/**
* Configuration for: Base URL
* This detects your URL/IP incl. sub-folder automatically. You can also deactivate auto-detection and provide the
* URL manually. This should then look like 'http://192.168.33.44/' ! Note the slash in the end.
*/
'URL' => 'http://' . $_SERVER['HTTP_HOST'] . str_replace('public', '', dirname($_SERVER['SCRIPT_NAME'])),
/**
* Configuration for: Folders
* Usually there's no reason to change this.
*/
'PATH_CONTROLLER' => realpath(dirname(__FILE__) . '/../../') . '/application/controller/',
'PATH_VIEW' => realpath(dirname(__FILE__) . '/../../') . '/application/view/',
/**
* Configuration for: Avatar paths
* Internal path to save avatars. Make sure this folder is writable. The slash at the end is VERY important!
*/
'PATH_AVATARS' => realpath(dirname(__FILE__) . '/../../') . '/public/avatars/',
'PATH_AVATARS_PUBLIC' => 'avatars/',
/**
* Configuration for: Default controller and action
*/
'DEFAULT_CONTROLLER' => 'index',
'DEFAULT_ACTION' => 'index',
/**
* Configuration for: Database
* DB_TYPE The used database type. Note that other types than "mysql" might break the db construction currently.
* DB_HOST The mysql hostname, usually localhost or 127.0.0.1
* DB_NAME The database name
* DB_USER The username
* DB_PASS The password
* DB_PORT The mysql port, 3306 by default (?), find out via phpinfo() and look for mysqli.default_port.
* DB_CHARSET The charset, necessary for security reasons. Check Database.php class for more info.
*/
'DB_TYPE' => 'mysql',
'DB_HOST' => 'localhost',
'DB_NAME' => 'huge',
'DB_USER' => 'root',
'DB_PASS' => 'root',
'DB_PORT' => '3306',
'DB_CHARSET' => 'utf8',
/**
* Configuration for: Captcha size
* The currently used Captcha generator (https://github.com/Gregwar/Captcha) also runs without giving a size,
* so feel free to use ->build(); inside CaptchaModel.
*/
'CAPTCHA_WIDTH' => 359,
'CAPTCHA_HEIGHT' => 100,
/**
* Configuration for: Cookies
* 1209600 seconds = 2 weeks
* COOKIE_PATH is the path the cookie is valid on, usually "/" to make it valid on the whole domain.
* @see http://stackoverflow.com/q/9618217/1114320
* @see php.net/manual/en/function.setcookie.php
*
* COOKIE_DOMAIN: The domain where the cookie is valid for. Usually this does not work with "localhost",
* ".localhost", "127.0.0.1", or ".127.0.0.1". If so, leave it as empty string, false or null.
* When using real domains make sure you have a dot (!) in front of the domain, like ".mydomain.com". This is
* strange, but explained here:
* @see http://stackoverflow.com/questions/2285010/php-setcookie-domain
* @see http://stackoverflow.com/questions/1134290/cookies-on-localhost-with-explicit-domain
* @see http://php.net/manual/en/function.setcookie.php#73107
*
* COOKIE_SECURE: If the cookie will be transferred through secured connection(SSL). It's highly recommended to set it to true if you have secured connection.
* COOKIE_HTTP: If set to true, Cookies that can't be accessed by JS - Highly recommended!
* SESSION_RUNTIME: How long should a session cookie be valid by seconds, 604800 = 1 week.
*/
'COOKIE_RUNTIME' => 1209600,
'COOKIE_PATH' => '/',
'COOKIE_DOMAIN' => "",
'COOKIE_SECURE' => false,
'COOKIE_HTTP' => true,
'SESSION_RUNTIME' => 604800,
/**
* Configuration for: Avatars/Gravatar support
* Set to true if you want to use "Gravatar(s)", a service that automatically gets avatar pictures via using email
* addresses of users by requesting images from the gravatar.com API. Set to false to use own locally saved avatars.
* AVATAR_SIZE set the pixel size of avatars/gravatars (will be 44x44 by default). Avatars are always squares.
* AVATAR_DEFAULT_IMAGE is the default image in public/avatars/
*/
'USE_GRAVATAR' => false,
'GRAVATAR_DEFAULT_IMAGESET' => 'mm',
'GRAVATAR_RATING' => 'pg',
'AVATAR_SIZE' => 44,
'AVATAR_JPEG_QUALITY' => 85,
'AVATAR_DEFAULT_IMAGE' => 'default.jpg',
/**
* Configuration for: Encryption Keys
* ENCRYPTION_KEY, HMAC_SALT: Currently used to encrypt and decrypt publicly visible values, like the user id in
* the cookie. Change these values for increased security, but don't touch if you have no idea what this means.
*/
'ENCRYPTION_KEY' => '6#x0gÊìf^25cL1f$08&',
'HMAC_SALT' => '8qk9c^4L6d#15tM8z7n0%',
/**
* Configuration for: Email server credentials
*
* Here you can define how you want to send emails.
* If you have successfully set up a mail server on your linux server and you know
* what you do, then you can skip this section. Otherwise please set EMAIL_USE_SMTP to true
* and fill in your SMTP provider account data.
*
* EMAIL_USED_MAILER: Check Mail class for alternatives
* EMAIL_USE_SMTP: Use SMTP or not
* EMAIL_SMTP_AUTH: leave this true unless your SMTP service does not need authentication
*/
'EMAIL_USED_MAILER' => 'phpmailer',
'EMAIL_USE_SMTP' => false,
'EMAIL_SMTP_HOST' => 'yourhost',
'EMAIL_SMTP_AUTH' => true,
'EMAIL_SMTP_USERNAME' => 'yourusername',
'EMAIL_SMTP_PASSWORD' => 'yourpassword',
'EMAIL_SMTP_PORT' => 465,
'EMAIL_SMTP_ENCRYPTION' => 'ssl',
/**
* Configuration for: Email content data
*/
'EMAIL_PASSWORD_RESET_URL' => 'login/verifypasswordreset',
'EMAIL_PASSWORD_RESET_FROM_EMAIL' => 'no-reply@example.com',
'EMAIL_PASSWORD_RESET_FROM_NAME' => 'My Project',
'EMAIL_PASSWORD_RESET_SUBJECT' => 'Password reset for PROJECT XY',
'EMAIL_PASSWORD_RESET_CONTENT' => 'Please click on this link to reset your password: ',
'EMAIL_VERIFICATION_URL' => 'register/verify',
'EMAIL_VERIFICATION_FROM_EMAIL' => 'no-reply@example.com',
'EMAIL_VERIFICATION_FROM_NAME' => 'My Project',
'EMAIL_VERIFICATION_SUBJECT' => 'Account activation for PROJECT XY',
'EMAIL_VERIFICATION_CONTENT' => 'Please click on this link to activate your account: ',
);

View File

@@ -0,0 +1,75 @@
<?php
/**
* Texts used in the application.
* These texts are used via Text::get('FEEDBACK_USERNAME_ALREADY_TAKEN').
* Could be extended to i18n etc.
*/
return array(
"FEEDBACK_UNKNOWN_ERROR" => "Unknown error occurred!",
"FEEDBACK_DELETED" => "Your account has been deleted.",
"FEEDBACK_ACCOUNT_SUSPENDED" => "Account Suspended for ",
"FEEDBACK_ACCOUNT_SUSPENSION_DELETION_STATUS" => "This user's suspension / deletion status has been edited.",
"FEEDBACK_ACCOUNT_CANT_DELETE_SUSPEND_OWN" => "You can not delete or suspend your own account.",
"FEEDBACK_ACCOUNT_USER_SUCCESSFULLY_KICKED" => "The selected user has been successfully kicked out of the system (by resetting this user's session)",
"FEEDBACK_PASSWORD_WRONG_3_TIMES" => "You have typed in a wrong password 3 or more times already. Please wait 30 seconds to try again.",
"FEEDBACK_ACCOUNT_NOT_ACTIVATED_YET" => "Your account is not activated yet. Please click on the confirm link in the mail.",
"FEEDBACK_USERNAME_OR_PASSWORD_WRONG" => "The username or password is incorrect. Please try again.",
"FEEDBACK_USER_DOES_NOT_EXIST" => "This user does not exist.",
"FEEDBACK_LOGIN_FAILED" => "Login failed.",
"FEEDBACK_LOGIN_FAILED_3_TIMES" => "Login failed 3 or more times already. Please wait 30 seconds to try again.",
"FEEDBACK_USERNAME_FIELD_EMPTY" => "Username field was empty.",
"FEEDBACK_PASSWORD_FIELD_EMPTY" => "Password field was empty.",
"FEEDBACK_USERNAME_OR_PASSWORD_FIELD_EMPTY" => "Username or password field was empty.",
"FEEDBACK_USERNAME_EMAIL_FIELD_EMPTY" => "Username / email field was empty.",
"FEEDBACK_EMAIL_FIELD_EMPTY" => "Email field was empty.",
"FEEDBACK_EMAIL_REPEAT_WRONG" => "Email and email repeat are not the same",
"FEEDBACK_EMAIL_AND_PASSWORD_FIELDS_EMPTY" => "Email and password fields were empty.",
"FEEDBACK_USERNAME_SAME_AS_OLD_ONE" => "Sorry, that username is the same as your current one. Please choose another one.",
"FEEDBACK_USERNAME_ALREADY_TAKEN" => "Sorry, that username is already taken. Please choose another one.",
"FEEDBACK_USER_EMAIL_ALREADY_TAKEN" => "Sorry, that email is already in use. Please choose another one.",
"FEEDBACK_USERNAME_CHANGE_SUCCESSFUL" => "Your username has been changed successfully.",
"FEEDBACK_USERNAME_AND_PASSWORD_FIELD_EMPTY" => "Username and password fields were empty.",
"FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN" => "Username does not fit the name pattern: only a-Z and numbers are allowed, 2 to 64 characters.",
"FEEDBACK_EMAIL_DOES_NOT_FIT_PATTERN" => "Sorry, your chosen email does not fit into the email naming pattern.",
"FEEDBACK_EMAIL_SAME_AS_OLD_ONE" => "Sorry, that email address is the same as your current one. Please choose another one.",
"FEEDBACK_EMAIL_CHANGE_SUCCESSFUL" => "Your email address has been changed successfully.",
"FEEDBACK_CAPTCHA_WRONG" => "The entered captcha security characters were wrong.",
"FEEDBACK_PASSWORD_REPEAT_WRONG" => "Password and password repeat are not the same.",
"FEEDBACK_PASSWORD_TOO_SHORT" => "Password has a minimum length of 6 characters.",
"FEEDBACK_USERNAME_TOO_SHORT_OR_TOO_LONG" => "Username cannot be shorter than 2 or longer than 64 characters.",
"FEEDBACK_ACCOUNT_SUCCESSFULLY_CREATED" => "Your account has been created successfully and we have sent you an email. Please click the VERIFICATION LINK within that mail.",
"FEEDBACK_VERIFICATION_MAIL_SENDING_FAILED" => "Sorry, we could not send you an verification mail. Your account has NOT been created.",
"FEEDBACK_ACCOUNT_CREATION_FAILED" => "Sorry, your registration failed. Please go back and try again.",
"FEEDBACK_VERIFICATION_MAIL_SENDING_ERROR" => "Verification mail could not be sent due to: ",
"FEEDBACK_VERIFICATION_MAIL_SENDING_SUCCESSFUL" => "A verification mail has been sent successfully.",
"FEEDBACK_ACCOUNT_ACTIVATION_SUCCESSFUL" => "Activation was successful! You can now log in.",
"FEEDBACK_ACCOUNT_ACTIVATION_FAILED" => "Sorry, no such id/verification code combination here! It might be possible that your mail provider (Yahoo? Hotmail?) automatically visits links in emails for anti-scam scanning, so this activation link might been clicked without your action. Please try to log in on the main page.",
"FEEDBACK_AVATAR_UPLOAD_SUCCESSFUL" => "Avatar upload was successful.",
"FEEDBACK_AVATAR_UPLOAD_WRONG_TYPE" => "Only JPEG and PNG files are supported.",
"FEEDBACK_AVATAR_UPLOAD_TOO_SMALL" => "Avatar source file's width/height is too small. Needs to be 100x100 pixel minimum.",
"FEEDBACK_AVATAR_UPLOAD_TOO_BIG" => "Avatar source file is too big. 5 Megabyte is the maximum.",
"FEEDBACK_AVATAR_FOLDER_DOES_NOT_EXIST_OR_NOT_WRITABLE" => "Avatar folder does not exist or is not writable. Please change this via chmod 775 or 777.",
"FEEDBACK_AVATAR_IMAGE_UPLOAD_FAILED" => "Something went wrong with the image upload.",
"FEEDBACK_AVATAR_IMAGE_DELETE_SUCCESSFUL" => "You successfully deleted your avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_NO_FILE" => "You don't have a custom avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_FAILED" => "Something went wrong while deleting your avatar.",
"FEEDBACK_PASSWORD_RESET_TOKEN_FAIL" => "Could not write token to database.",
"FEEDBACK_PASSWORD_RESET_TOKEN_MISSING" => "No password reset token.",
"FEEDBACK_PASSWORD_RESET_MAIL_SENDING_ERROR" => "Password reset mail could not be sent due to: ",
"FEEDBACK_PASSWORD_RESET_MAIL_SENDING_SUCCESSFUL" => "A password reset mail has been sent successfully.",
"FEEDBACK_PASSWORD_RESET_LINK_EXPIRED" => "Your reset link has expired. Please use the reset link within one hour.",
"FEEDBACK_PASSWORD_RESET_COMBINATION_DOES_NOT_EXIST" => "Username/Verification code combination does not exist.",
"FEEDBACK_PASSWORD_RESET_LINK_VALID" => "Password reset validation link is valid. Please change the password now.",
"FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL" => "Password successfully changed.",
"FEEDBACK_PASSWORD_CHANGE_FAILED" => "Sorry, your password changing failed.",
"FEEDBACK_PASSWORD_NEW_SAME_AS_CURRENT" => "New password is the same as the current password.",
"FEEDBACK_PASSWORD_CURRENT_INCORRECT" => "Current password entered was incorrect.",
"FEEDBACK_ACCOUNT_TYPE_CHANGE_SUCCESSFUL" => "Account type change successful",
"FEEDBACK_ACCOUNT_TYPE_CHANGE_FAILED" => "Account type change failed",
"FEEDBACK_NOTE_CREATION_FAILED" => "Note creation failed.",
"FEEDBACK_NOTE_EDITING_FAILED" => "Note editing failed.",
"FEEDBACK_NOTE_DELETION_FAILED" => "Note deletion failed.",
"FEEDBACK_COOKIE_INVALID" => "Your remember-me-cookie is invalid.",
"FEEDBACK_COOKIE_LOGIN_SUCCESSFUL" => "You were successfully logged in via the remember-me-cookie.",
);

View File

@@ -0,0 +1,35 @@
<?php
class AdminController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// special authentication check for the entire controller: Note the check-ADMIN-authentication!
// All methods inside this controller are only accessible for admins (= users that have role type 7)
Auth::checkAdminAuthentication();
}
/**
* This method controls what happens when you move to /admin or /admin/index in your app.
*/
public function index()
{
$this->View->render('admin/index', array(
'users' => UserModel::getPublicProfilesOfAllUsers())
);
}
public function actionAccountSettings()
{
AdminModel::setAccountSuspensionAndDeletionStatus(
Request::post('suspension'), Request::post('softDelete'), Request::post('user_id')
);
Redirect::to("admin");
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* This controller shows an area that's only visible for logged in users (because of Auth::checkAuthentication(); in line 16)
*/
class DashboardController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// this entire controller should only be visible/usable by logged in users, so we put authentication-check here
Auth::checkAuthentication();
}
/**
* This method controls what happens when you move to /dashboard/index in your app.
*/
public function index()
{
$this->View->render('dashboard/index');
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Error
* This controller simply contains some methods that can be used to give proper feedback in certain error scenarios,
* like a proper 404 response with an additional HTML page behind when something does not exist.
*/
class ErrorController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
}
/**
* Use this when something is not found. Gives back a proper 404 header response plus a normal page (where you could
* show a well-designed error message or something more useful for your users).
* You can see this in action in action in /core/Application.php -> __construct
*/
public function error404()
{
header('HTTP/1.0 404 Not Found', true, 404);
$this->View->render('error/404');
}
}

View File

@@ -0,0 +1,21 @@
<?php
class IndexController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
}
/**
* Handles what happens when user moves to URL/index/index - or - as this is the default controller, also
* when user moves to /index or enter your application at base level
*/
public function index()
{
$this->View->render('index/index');
}
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* LoginController
* Controls everything that is authentication-related
*/
class LoginController extends Controller
{
/**
* Construct this object by extending the basic Controller class. The parent::__construct thing is necessary to
* put checkAuthentication in here to make an entire controller only usable for logged-in users (for sure not
* needed in the LoginController).
*/
public function __construct()
{
parent::__construct();
}
/**
* Index, default action (shows the login form), when you do login/index
*/
public function index()
{
// if user is logged in redirect to main-page, if not show the view
if (LoginModel::isUserLoggedIn()) {
Redirect::home();
} else {
$data = array('redirect' => Request::get('redirect') ? Request::get('redirect') : null);
$this->View->render('login/index', $data);
}
}
/**
* The login action, when you do login/login
*/
public function login()
{
// check if csrf token is valid
if (!Csrf::isTokenValid()) {
LoginModel::logout();
Redirect::home();
exit();
}
// perform the login method, put result (true or false) into $login_successful
$login_successful = LoginModel::login(
Request::post('user_name'), Request::post('user_password'), Request::post('set_remember_me_cookie')
);
// check login status: if true, then redirect user to user/index, if false, then to login form again
if ($login_successful) {
if (Request::post('redirect')) {
Redirect::toPreviousViewedPageAfterLogin(ltrim(urldecode(Request::post('redirect')), '/'));
} else {
Redirect::to('user/index');
}
} else {
if (Request::post('redirect')) {
Redirect::to('login?redirect=' . ltrim(urlencode(Request::post('redirect')), '/'));
} else {
Redirect::to('login/index');
}
}
}
/**
* The logout action
* Perform logout, redirect user to main-page
*/
public function logout()
{
LoginModel::logout();
Redirect::home();
exit();
}
/**
* Login with cookie
*/
public function loginWithCookie()
{
// run the loginWithCookie() method in the login-model, put the result in $login_successful (true or false)
$login_successful = LoginModel::loginWithCookie(Request::cookie('remember_me'));
// if login successful, redirect to dashboard/index ...
if ($login_successful) {
Redirect::to('dashboard/index');
} else {
// if not, delete cookie (outdated? attack?) and route user to login form to prevent infinite login loops
LoginModel::deleteCookie();
Redirect::to('login/index');
}
}
/**
* Show the request-password-reset page
*/
public function requestPasswordReset()
{
$this->View->render('login/requestPasswordReset');
}
/**
* The request-password-reset action
* POST-request after form submit
*/
public function requestPasswordReset_action()
{
PasswordResetModel::requestPasswordReset(Request::post('user_name_or_email'), Request::post('captcha'));
Redirect::to('login/index');
}
/**
* Verify the verification token of that user (to show the user the password editing view or not)
* @param string $user_name username
* @param string $verification_code password reset verification token
*/
public function verifyPasswordReset($user_name, $verification_code)
{
// check if this the provided verification code fits the user's verification code
if (PasswordResetModel::verifyPasswordReset($user_name, $verification_code)) {
// pass URL-provided variable to view to display them
$this->View->render('login/resetPassword', array(
'user_name' => $user_name,
'user_password_reset_hash' => $verification_code
));
} else {
Redirect::to('login/index');
}
}
/**
* Set the new password
* Please note that this happens while the user is not logged in. The user identifies via the data provided by the
* password reset link from the email, automatically filled into the <form> fields. See verifyPasswordReset()
* for more. Then (regardless of result) route user to index page (user will get success/error via feedback message)
* POST request !
* TODO this is an _action
*/
public function setNewPassword()
{
PasswordResetModel::setNewPassword(
Request::post('user_name'), Request::post('user_password_reset_hash'),
Request::post('user_password_new'), Request::post('user_password_repeat')
);
Redirect::to('login/index');
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* The note controller: Just an example of simple create, read, update and delete (CRUD) actions.
*/
class NoteController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// VERY IMPORTANT: All controllers/areas that should only be usable by logged-in users
// need this line! Otherwise not-logged in users could do actions. If all of your pages should only
// be usable by logged-in users: Put this line into libs/Controller->__construct
Auth::checkAuthentication();
}
/**
* This method controls what happens when you move to /note/index in your app.
* Gets all notes (of the user).
*/
public function index()
{
$this->View->render('note/index', array(
'notes' => NoteModel::getAllNotes()
));
}
/**
* This method controls what happens when you move to /dashboard/create in your app.
* Creates a new note. This is usually the target of form submit actions.
* POST request.
*/
public function create()
{
NoteModel::createNote(Request::post('note_text'));
Redirect::to('note');
}
/**
* This method controls what happens when you move to /note/edit(/XX) in your app.
* Shows the current content of the note and an editing form.
* @param $note_id int id of the note
*/
public function edit($note_id)
{
$this->View->render('note/edit', array(
'note' => NoteModel::getNote($note_id)
));
}
/**
* This method controls what happens when you move to /note/editSave in your app.
* Edits a note (performs the editing after form submit).
* POST request.
*/
public function editSave()
{
NoteModel::updateNote(Request::post('note_id'), Request::post('note_text'));
Redirect::to('note');
}
/**
* This method controls what happens when you move to /note/delete(/XX) in your app.
* Deletes a note. In a real application a deletion via GET/URL is not recommended, but for demo purposes it's
* totally okay.
* @param int $note_id id of the note
*/
public function delete($note_id)
{
NoteModel::deleteNote($note_id);
Redirect::to('note');
}
}

View File

@@ -0,0 +1,39 @@
<?php
class ProfileController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
}
/**
* This method controls what happens when you move to /overview/index in your app.
* Shows a list of all users.
*/
public function index()
{
$this->View->render('profile/index', array(
'users' => UserModel::getPublicProfilesOfAllUsers())
);
}
/**
* This method controls what happens when you move to /overview/showProfile in your app.
* Shows the (public) details of the selected user.
* @param $user_id int id the the user
*/
public function showProfile($user_id)
{
if (isset($user_id)) {
$this->View->render('profile/showProfile', array(
'user' => UserModel::getPublicProfileOfUser($user_id))
);
} else {
Redirect::home();
}
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* RegisterController
* Register new user
*/
class RegisterController extends Controller
{
/**
* Construct this object by extending the basic Controller class. The parent::__construct thing is necessary to
* put checkAuthentication in here to make an entire controller only usable for logged-in users (for sure not
* needed in the RegisterController).
*/
public function __construct()
{
parent::__construct();
}
/**
* Register page
* Show the register form, but redirect to main-page if user is already logged-in
*/
public function index()
{
if (LoginModel::isUserLoggedIn()) {
Redirect::home();
} else {
$this->View->render('register/index');
}
}
/**
* Register page action
* POST-request after form submit
*/
public function register_action()
{
$registration_successful = RegistrationModel::registerNewUser();
if ($registration_successful) {
Redirect::to('login/index');
} else {
Redirect::to('register/index');
}
}
/**
* Verify user after activation mail link opened
* @param int $user_id user's id
* @param string $user_activation_verification_code user's verification token
*/
public function verify($user_id, $user_activation_verification_code)
{
if (isset($user_id) && isset($user_activation_verification_code)) {
RegistrationModel::verifyNewUser($user_id, $user_activation_verification_code);
$this->View->render('register/verify');
} else {
Redirect::to('login/index');
}
}
/**
* Generate a captcha, write the characters into $_SESSION['captcha'] and returns a real image which will be used
* like this: <img src="......./login/showCaptcha" />
* IMPORTANT: As this action is called via <img ...> AFTER the real application has finished executing (!), the
* SESSION["captcha"] has no content when the application is loaded. The SESSION["captcha"] gets filled at the
* moment the end-user requests the <img .. >
* Maybe refactor this sometime.
*/
public function showCaptcha()
{
CaptchaModel::generateAndShowCaptcha();
}
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* UserController
* Controls everything that is user-related
*/
class UserController extends Controller
{
/**
* Construct this object by extending the basic Controller class.
*/
public function __construct()
{
parent::__construct();
// VERY IMPORTANT: All controllers/areas that should only be usable by logged-in users
// need this line! Otherwise not-logged in users could do actions.
Auth::checkAuthentication();
}
/**
* Show user's PRIVATE profile
*/
public function index()
{
$this->View->render('user/index', array(
'user_name' => Session::get('user_name'),
'user_email' => Session::get('user_email'),
'user_gravatar_image_url' => Session::get('user_gravatar_image_url'),
'user_avatar_file' => Session::get('user_avatar_file'),
'user_account_type' => Session::get('user_account_type')
));
}
/**
* Show edit-my-username page
*/
public function editUsername()
{
$this->View->render('user/editUsername');
}
/**
* Edit user name (perform the real action after form has been submitted)
*/
public function editUsername_action()
{
// check if csrf token is valid
if (!Csrf::isTokenValid()) {
LoginModel::logout();
Redirect::home();
exit();
}
UserModel::editUserName(Request::post('user_name'));
Redirect::to('user/editUsername');
}
/**
* Show edit-my-user-email page
*/
public function editUserEmail()
{
$this->View->render('user/editUserEmail');
}
/**
* Edit user email (perform the real action after form has been submitted)
*/
// make this POST
public function editUserEmail_action()
{
UserModel::editUserEmail(Request::post('user_email'));
Redirect::to('user/editUserEmail');
}
/**
* Edit avatar
*/
public function editAvatar()
{
$this->View->render('user/editAvatar', array(
'avatar_file_path' => AvatarModel::getPublicUserAvatarFilePathByUserId(Session::get('user_id'))
));
}
/**
* Perform the upload of the avatar
* POST-request
*/
public function uploadAvatar_action()
{
AvatarModel::createAvatar();
Redirect::to('user/editAvatar');
}
/**
* Delete the current user's avatar
*/
public function deleteAvatar_action()
{
AvatarModel::deleteAvatar(Session::get("user_id"));
Redirect::to('user/editAvatar');
}
/**
* Show the change-account-type page
*/
public function changeUserRole()
{
$this->View->render('user/changeUserRole');
}
/**
* Perform the account-type changing
* POST-request
*/
public function changeUserRole_action()
{
if (Request::post('user_account_upgrade')) {
// "2" is quick & dirty account type 2, something like "premium user" maybe. you got the idea :)
UserRoleModel::changeUserRole(2);
}
if (Request::post('user_account_downgrade')) {
// "1" is quick & dirty account type 1, something like "basic user" maybe.
UserRoleModel::changeUserRole(1);
}
Redirect::to('user/changeUserRole');
}
/**
* Password Change Page
*/
public function changePassword()
{
$this->View->render('user/changePassword');
}
/**
* Password Change Action
* Submit form, if retured positive redirect to index, otherwise show the changePassword page again
*/
public function changePassword_action()
{
$result = PasswordResetModel::changePassword(
Session::get('user_name'), Request::post('user_password_current'),
Request::post('user_password_new'), Request::post('user_password_repeat')
);
if($result)
Redirect::to('user/index');
else
Redirect::to('user/changePassword');
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* Class Application
* The heart of the application
*/
class Application
{
/** @var mixed Instance of the controller */
private $controller;
/** @var array URL parameters, will be passed to used controller-method */
private $parameters = array();
/** @var string Just the name of the controller, useful for checks inside the view ("where am I ?") */
private $controller_name;
/** @var string Just the name of the controller's method, useful for checks inside the view ("where am I ?") */
private $action_name;
/**
* Start the application, analyze URL elements, call according controller/method or relocate to fallback location
*/
public function __construct()
{
// create array with URL parts in $url
$this->splitUrl();
// creates controller and action names (from URL input)
$this->createControllerAndActionNames();
// does such a controller exist ?
if (file_exists(Config::get('PATH_CONTROLLER') . $this->controller_name . '.php')) {
// load this file and create this controller
// example: if controller would be "car", then this line would translate into: $this->car = new car();
require Config::get('PATH_CONTROLLER') . $this->controller_name . '.php';
$this->controller = new $this->controller_name();
// check are controller and method existing and callable?
if (is_callable(array($this->controller, $this->action_name))) {
if (!empty($this->parameters)) {
// call the method and pass arguments to it
call_user_func_array(array($this->controller, $this->action_name), $this->parameters);
} else {
// if no parameters are given, just call the method without parameters, like $this->index->index();
$this->controller->{$this->action_name}();
}
} else {
// load 404 error page
require Config::get('PATH_CONTROLLER') . 'ErrorController.php';
$this->controller = new ErrorController;
$this->controller->error404();
}
} else {
// load 404 error page
require Config::get('PATH_CONTROLLER') . 'ErrorController.php';
$this->controller = new ErrorController;
$this->controller->error404();
}
}
/**
* Get and split the URL
*/
private function splitUrl()
{
if (Request::get('url')) {
// split URL
$url = trim(Request::get('url'), '/');
$url = filter_var($url, FILTER_SANITIZE_URL);
$url = explode('/', $url);
// put URL parts into according properties
$this->controller_name = isset($url[0]) ? $url[0] : null;
$this->action_name = isset($url[1]) ? $url[1] : null;
// remove controller name and action name from the split URL
unset($url[0], $url[1]);
// rebase array keys and store the URL parameters
$this->parameters = array_values($url);
}
}
/**
* Checks if controller and action names are given. If not, default values are put into the properties.
* Also renames controller to usable name.
*/
private function createControllerAndActionNames()
{
// check for controller: no controller given ? then make controller = default controller (from config)
if (!$this->controller_name) {
$this->controller_name = Config::get('DEFAULT_CONTROLLER');
}
// check for action: no action given ? then make action = default action (from config)
if (!$this->action_name OR (strlen($this->action_name) == 0)) {
$this->action_name = Config::get('DEFAULT_ACTION');
}
// rename controller name to real controller class/file name ("index" to "IndexController")
$this->controller_name = ucwords($this->controller_name) . 'Controller';
}
}

82
application/core/Auth.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
/**
* Class Auth
* Checks if user is logged in, if not then sends the user to "yourdomain.com/login".
* Auth::checkAuthentication() can be used in the constructor of a controller (to make the
* entire controller only visible for logged-in users) or inside a controller-method to make only this part of the
* application available for logged-in users.
*/
class Auth
{
/**
* The normal authentication flow, just check if the user is logged in (by looking into the session).
* If user is not, then he will be redirected to login page and the application is hard-stopped via exit().
*/
public static function checkAuthentication()
{
// initialize the session (if not initialized yet)
Session::init();
// self::checkSessionConcurrency();
// if user is NOT logged in...
// (if user IS logged in the application will not run the code below and therefore just go on)
if (!Session::userIsLoggedIn()) {
// ... then treat user as "not logged in", destroy session, redirect to login page
Session::destroy();
// send the user to the login form page, but also add the current page's URI (the part after the base URL)
// as a parameter argument, making it possible to send the user back to where he/she came from after a
// successful login
header('location: ' . Config::get('URL') . 'login?redirect=' . urlencode($_SERVER['REQUEST_URI']));
// to prevent fetching views via cURL (which "ignores" the header-redirect above) we leave the application
// the hard way, via exit(). @see https://github.com/panique/php-login/issues/453
// this is not optimal and will be fixed in future releases
exit();
}
}
/**
* The admin authentication flow, just check if the user is logged in (by looking into the session) AND has
* user role type 7 (currently there's only type 1 (normal user), type 2 (premium user) and 7 (admin)).
* If user is not, then he will be redirected to login page and the application is hard-stopped via exit().
* Using this method makes only sense in controllers that should only be used by admins.
*/
public static function checkAdminAuthentication()
{
// initialize the session (if not initialized yet)
Session::init();
// self::checkSessionConcurrency();
// if user is not logged in or is not an admin (= not role type 7)
if (!Session::userIsLoggedIn() || Session::get("user_account_type") != 7) {
// ... then treat user as "not logged in", destroy session, redirect to login page
Session::destroy();
header('location: ' . Config::get('URL') . 'login');
// to prevent fetching views via cURL (which "ignores" the header-redirect above) we leave the application
// the hard way, via exit(). @see https://github.com/panique/php-login/issues/453
// this is not optimal and will be fixed in future releases
exit();
}
}
/**
* Detects if there is concurrent session (i.e. another user logged in with the same current user credentials),
* If so, then logout.
*/
public static function checkSessionConcurrency(){
if(Session::userIsLoggedIn()){
if(Session::isConcurrentSessionExists()){
LoginModel::logout();
Redirect::home();
exit();
}
}
}
}

View File

@@ -0,0 +1,23 @@
<?php
class Config
{
// this is public to allow better Unit Testing
public static $config;
public static function get($key)
{
if (!self::$config) {
$config_file = '../application/config/config.' . Environment::get() . '.php';
if (!file_exists($config_file)) {
return false;
}
self::$config = require $config_file;
}
return self::$config[$key];
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* This is the "base controller class". All other "real" controllers extend this class.
* Whenever a controller is created, we also
* 1. initialize a session
* 2. check if the user is not logged in anymore (session timeout) but has a cookie
*/
class Controller
{
/** @var View View The view object */
public $View;
/**
* Construct the (base) controller. This happens when a real controller is constructed, like in
* the constructor of IndexController when it says: parent::__construct();
*/
public function __construct()
{
// always initialize a session
Session::init();
// check session concurrency
Auth::checkSessionConcurrency();
// user is not logged in but has remember-me-cookie ? then try to login with cookie ("remember me" feature)
if (!Session::userIsLoggedIn() AND Request::cookie('remember_me')) {
header('location: ' . Config::get('URL') . 'login/loginWithCookie');
}
// create a view object to be able to use it inside a controller, like $this->View->render();
$this->View = new View();
}
}

60
application/core/Csrf.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
/**
* Cross Site Request Forgery Class
*
*/
/**
* Instructions:
*
* At your form, before the submit button put:
* <input type="hidden" name="csrf_token" value="<?= Csrf::makeToken(); ?>" />
*
* This validation needed in the controller action method to validate CSRF token submitted with the form:
*
* if (!Csrf::isTokenValid()) {
* LoginModel::logout();
* Redirect::home();
* exit();
* }
*
* To get simpler code it might be better to put the logout, redirect, exit into an own (static) method.
*/
class Csrf
{
/**
* get CSRF token and generate a new one if expired
*
* @access public
* @static static method
* @return string
*/
public static function makeToken()
{
// token is valid for 1 day
$max_time = 60 * 60 * 24;
$stored_time = Session::get('csrf_token_time');
$csrf_token = Session::get('csrf_token');
if ($max_time + $stored_time <= time() || empty($csrf_token)) {
Session::set('csrf_token', md5(uniqid(rand(), true)));
Session::set('csrf_token_time', time());
}
return Session::get('csrf_token');
}
/**
* checks if CSRF token in session is same as in the form submitted
*
* @access public
* @static static method
* @return bool
*/
public static function isTokenValid()
{
$token = Request::post('csrf_token');
return $token === Session::get('csrf_token') && !empty($token);
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* Class DatabaseFactory
*
* Use it like this:
* $database = DatabaseFactory::getFactory()->getConnection();
*
* That's my personal favourite when creating a database connection.
* It's a slightly modified version of Jon Raphaelson's excellent answer on StackOverflow:
* http://stackoverflow.com/questions/130878/global-or-singleton-for-database-connection
*
* Full quote from the answer:
*
* "Then, in 6 months when your app is super famous and getting dugg and slashdotted and you decide you need more than
* a single connection, all you have to do is implement some pooling in the getConnection() method. Or if you decide
* that you want a wrapper that implements SQL logging, you can pass a PDO subclass. Or if you decide you want a new
* connection on every invocation, you can do do that. It's flexible, instead of rigid."
*
* Thanks! Big up, mate!
*/
class DatabaseFactory
{
private static $factory;
private $database;
public static function getFactory()
{
if (!self::$factory) {
self::$factory = new DatabaseFactory();
}
return self::$factory;
}
public function getConnection() {
if (!$this->database) {
/**
* Check DB connection in try/catch block. Also when PDO is not constructed properly,
* prevent to exposing database host, username and password in plain text as:
* PDO->__construct('mysql:host=127....', 'root', '12345678', Array)
* by throwing custom error message
*/
try {
$options = array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING);
$this->database = new PDO(
Config::get('DB_TYPE') . ':host=' . Config::get('DB_HOST') . ';dbname=' .
Config::get('DB_NAME') . ';port=' . Config::get('DB_PORT') . ';charset=' . Config::get('DB_CHARSET'),
Config::get('DB_USER'), Config::get('DB_PASS'), $options
);
} catch (PDOException $e) {
// Echo custom message. Echo error code gives you some info.
echo 'Database connection can not be estabilished. Please try again later.' . '<br>';
echo 'Error code: ' . $e->getCode();
// Stop application :(
// No connection, reached limit connections etc. so no point to keep it running
exit;
}
}
return $this->database;
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* Encryption and Decryption Class
*
*/
class Encryption
{
/**
* Cipher algorithm
*
* @var string
*/
const CIPHER = 'aes-256-cbc';
/**
* Hash function
*
* @var string
*/
const HASH_FUNCTION = 'sha256';
/**
* constructor for Encryption object.
*
* @access private
*/
private function __construct()
{
}
/**
* Encrypt a string.
*
* @access public
* @static static method
* @param string $plain
* @return string
* @throws Exception If functions don't exists
*/
public static function encrypt($plain)
{
if (!function_exists('openssl_cipher_iv_length') ||
!function_exists('openssl_random_pseudo_bytes') ||
!function_exists('openssl_encrypt')) {
throw new Exception('Encryption function doesn\'t exist');
}
// generate initialization vector,
// this will make $iv different every time,
// so, encrypted string will be also different.
$iv_size = openssl_cipher_iv_length(self::CIPHER);
$iv = openssl_random_pseudo_bytes($iv_size);
// generate key for authentication using ENCRYPTION_KEY & HMAC_SALT
$key = mb_substr(hash(self::HASH_FUNCTION, Config::get('ENCRYPTION_KEY') . Config::get('HMAC_SALT')), 0, 32, '8bit');
// append initialization vector
$encrypted_string = openssl_encrypt($plain, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
$ciphertext = $iv . $encrypted_string;
// apply the HMAC
$hmac = hash_hmac('sha256', $ciphertext, $key);
return $hmac . $ciphertext;
}
/**
* Decrypted a string.
*
* @access public
* @static static method
* @param string $ciphertext
* @return string
* @throws Exception If $ciphertext is empty, or If functions don't exists
*/
public static function decrypt($ciphertext)
{
if (empty($ciphertext)) {
throw new Exception('The String to decrypt can\'t be empty');
}
if (!function_exists('openssl_cipher_iv_length') ||
!function_exists('openssl_decrypt')) {
throw new Exception('Encryption function doesn\'t exist');
}
// generate key used for authentication using ENCRYPTION_KEY & HMAC_SALT
$key = mb_substr(hash(self::HASH_FUNCTION, Config::get('ENCRYPTION_KEY') . Config::get('HMAC_SALT')), 0, 32, '8bit');
// split cipher into: hmac, cipher & iv
$macSize = 64;
$hmac = mb_substr($ciphertext, 0, $macSize, '8bit');
$iv_cipher = mb_substr($ciphertext, $macSize, null, '8bit');
// generate original hmac & compare it with the one in $ciphertext
$originalHmac = hash_hmac('sha256', $iv_cipher, $key);
if (!self::hashEquals($hmac, $originalHmac)) {
return false;
}
// split out the initialization vector and cipher
$iv_size = openssl_cipher_iv_length(self::CIPHER);
$iv = mb_substr($iv_cipher, 0, $iv_size, '8bit');
$cipher = mb_substr($iv_cipher, $iv_size, null, '8bit');
return openssl_decrypt($cipher, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
}
/**
* A timing attack resistant comparison.
*
* @access private
* @static static method
* @param string $hmac The hmac from the ciphertext being decrypted.
* @param string $compare The comparison hmac.
* @return bool
* @see https://github.com/sarciszewski/php-future/blob/bd6c91fb924b2b35a3e4f4074a642868bd051baf/src/Security.php#L36
*/
private static function hashEquals($hmac, $compare)
{
if (function_exists('hash_equals')) {
return hash_equals($hmac, $compare);
}
// if hash_equals() is not available,
// then use the following snippet.
// It's equivalent to hash_equals() in PHP 5.6.
$hashLength = mb_strlen($hmac, '8bit');
$compareLength = mb_strlen($compare, '8bit');
if ($hashLength !== $compareLength) {
return false;
}
$result = 0;
for ($i = 0; $i < $hashLength; $i++) {
$result |= (ord($hmac[$i]) ^ ord($compare[$i]));
}
return $result === 0;
}
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* Class Environment
*
* Extremely simple way to get the environment, everywhere inside your application.
* Extend this the way you want.
*/
class Environment
{
public static function get()
{
// if APPLICATION_ENV constant exists (set in Apache configs)
// then return content of APPLICATION_ENV
// else return "development"
return (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : "development");
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* Class Filter
*
* This is the place to put filters, usually methods that cleans, sorts and, well, filters stuff.
*/
class Filter
{
/**
* The XSS filter: This simply removes "code" from any data, used to prevent Cross-Site Scripting Attacks.
*
* A very simple introduction: Let's say an attackers changes its username from "John" to these lines:
* "<script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>"
* This means, every user's browser would render "John" anymore, instead interpreting this JavaScript code, calling
* the delete.php, in this case inside the project, in worse scenarios something like performing a bank transaction
* or sending your cookie data (containing your remember-me-token) to somebody else.
*
* What is XSS ?
* @see http://phpsecurity.readthedocs.org/en/latest/Cross-Site-Scripting-%28XSS%29.html
*
* Deeper information:
* @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
*
* XSSFilter expects a value, checks if the value is a string, and if so, encodes typical script tag chars to
* harmless HTML (you'll see the code, it wil not be interpreted). Then the method checks if the value is an array,
* or an object and if so, makes sure all its string content is encoded (recursive call on its values).
* Note that this method uses reference to the assed variable, not a copy, meaning you can use this methods like this:
*
* CORRECT: Filter::XSSFilter($myVariable);
* WRONG: $myVariable = Filter::XSSFilter($myVariable);
*
* This works like some other popular PHP functions, for example sort().
* @see http://php.net/manual/en/function.sort.php
*
* @see http://stackoverflow.com/questions/1676897/what-does-it-mean-to-start-a-php-function-with-an-ampersand
* @see http://php.net/manual/en/language.references.pass.php
*
* FYI: htmlspecialchars() does this (from PHP docs):
*
* '&' (ampersand) becomes '&amp;'
* '"' (double quote) becomes '&quot;' when ENT_NOQUOTES is not set.
* "'" (single quote) becomes '&#039;' (or &apos;) only when ENT_QUOTES is set.
* '<' (less than) becomes '&lt;'
* '>' (greater than) becomes '&gt;'
*
* @see http://www.php.net/manual/en/function.htmlspecialchars.php
*
* @param $value The value to be filtered
* @return mixed
*/
public static function XSSFilter(&$value)
{
// if argument is a string, filters that string
if (is_string($value)) {
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
// if argument is an array or an object,
// recursivly filters its content
} else if (is_array($value) || is_object($value)) {
/**
* Make sure the element is passed by reference,
* In PHP 7, foreach does not use the internal array pointer.
* In order to be able to directly modify array elements within the loop
* precede $value with &. In that case the value will be assigned by reference.
* @see http://php.net/manual/en/control-structures.foreach.php
*/
foreach ($value as &$valueInValue) {
self::XSSFilter($valueInValue);
}
}
// other types are untouched
return $value;
}
}

154
application/core/Mail.php Normal file
View File

@@ -0,0 +1,154 @@
<?php
/* Using PHPMailer's namespace */
use PHPMailer\PHPMailer\PHPMailer;
/**
* Class Mail
*
* Handles everything regarding mail-sending.
*/
class Mail
{
/** @var mixed variable to collect errors */
private $error;
/**
* Try to send a mail by using PHP's native mail() function.
* Please note that not PHP itself will send a mail, it's just a wrapper for Linux's sendmail or other mail tools
*
* Good guideline on how to send mails natively with mail():
* @see http://stackoverflow.com/a/24644450/1114320
* @see http://www.php.net/manual/en/function.mail.php
*/
public function sendMailWithNativeMailFunction()
{
// no code yet, so we just return something to make IDEs and code analyzer tools happy
return false;
}
/**
* Try to send a mail by using SwiftMailer.
* Make sure you have loaded SwiftMailer via Composer.
*
* @return bool
*/
public function sendMailWithSwiftMailer()
{
// no code yet, so we just return something to make IDEs and code analyzer tools happy
return false;
}
/**
* Try to send a mail by using PHPMailer.
* Make sure you have loaded PHPMailer via Composer.
* Depending on your EMAIL_USE_SMTP setting this will work via SMTP credentials or via native mail()
*
* @param $user_email
* @param $from_email
* @param $from_name
* @param $subject
* @param $body
*
* @return bool
* @throws Exception
* @throws phpmailerException
*/
public function sendMailWithPHPMailer($user_email, $from_email, $from_name, $subject, $body)
{
$mail = new PHPMailer;
// you should use UTF-8 to avoid encoding issues
$mail->CharSet = 'UTF-8';
// if you want to send mail via PHPMailer using SMTP credentials
if (Config::get('EMAIL_USE_SMTP')) {
// set PHPMailer to use SMTP
$mail->IsSMTP();
// 0 = off, 1 = commands, 2 = commands and data, perfect to see SMTP errors
$mail->SMTPDebug = 0;
// enable SMTP authentication
$mail->SMTPAuth = Config::get('EMAIL_SMTP_AUTH');
// encryption
if (Config::get('EMAIL_SMTP_ENCRYPTION')) {
$mail->SMTPSecure = Config::get('EMAIL_SMTP_ENCRYPTION');
}
// set SMTP provider's credentials
$mail->Host = Config::get('EMAIL_SMTP_HOST');
$mail->Username = Config::get('EMAIL_SMTP_USERNAME');
$mail->Password = Config::get('EMAIL_SMTP_PASSWORD');
$mail->Port = Config::get('EMAIL_SMTP_PORT');
} else {
$mail->IsMail();
}
// fill mail with data
$mail->From = $from_email;
$mail->FromName = $from_name;
$mail->AddAddress($user_email);
$mail->Subject = $subject;
$mail->Body = $body;
// try to send mail, put result status (true/false into $wasSendingSuccessful)
// I'm unsure if mail->send really returns true or false every time, tis method in PHPMailer is quite complex
$wasSendingSuccessful = $mail->Send();
if ($wasSendingSuccessful) {
return true;
} else {
// if not successful, copy errors into Mail's error property
$this->error = $mail->ErrorInfo;
return false;
}
}
/**
* The main mail sending method, this simply calls a certain mail sending method depending on which mail provider
* you've selected in the application's config.
*
* @param $user_email string email
* @param $from_email string sender's email
* @param $from_name string sender's name
* @param $subject string subject
* @param $body string full mail body text
* @return bool the success status of the according mail sending method
*/
public function sendMail($user_email, $from_email, $from_name, $subject, $body)
{
if (Config::get('EMAIL_USED_MAILER') == "phpmailer") {
// returns true if successful, false if not
return $this->sendMailWithPHPMailer(
$user_email, $from_email, $from_name, $subject, $body
);
}
if (Config::get('EMAIL_USED_MAILER') == "swiftmailer") {
return $this->sendMailWithSwiftMailer();
}
if (Config::get('EMAIL_USED_MAILER') == "native") {
return $this->sendMailWithNativeMailFunction();
}
}
/**
* The different mail sending methods write errors to the error property $this->error,
* this method simply returns this error / error array.
*
* @return mixed
*/
public function getError()
{
return $this->error;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Class Redirect
*
* Simple abstraction for redirecting the user to a certain page
*/
class Redirect
{
/**
* To the last visited page before user logged in (useful when people are on a certain page inside your application
* and then want to log in (to edit or comment something for example) and don't to be redirected to the main page).
*
* This is just a bulletproof version of Redirect::to(), redirecting to an ABSOLUTE URL path like
* "http://www.mydomain.com/user/profile", useful as people had problems with the RELATIVE URL path generated
* by Redirect::to() when using HUGE inside sub-folders.
*
* @param $path string
*/
public static function toPreviousViewedPageAfterLogin($path)
{
header('location: http://' . $_SERVER['HTTP_HOST'] . '/' . $path);
}
/**
* To the homepage
*/
public static function home()
{
header("location: " . Config::get('URL'));
}
/**
* To the defined page, uses a relative path (like "user/profile")
*
* Redirects to a RELATIVE path, like "user/profile" (which works very fine unless you are using HUGE inside tricky
* sub-folder structures)
*
* @see https://github.com/panique/huge/issues/770
* @see https://github.com/panique/huge/issues/754
*
* @param $path string
*/
public static function to($path)
{
header("location: " . Config::get('URL') . $path);
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* This is under development. Expect changes!
* Class Request
* Abstracts the access to $_GET, $_POST and $_COOKIE, preventing direct access to these super-globals.
* This makes PHP code quality analyzer tools very happy.
* @see http://php.net/manual/en/reserved.variables.request.php
*/
class Request
{
/**
* Gets/returns the value of a specific key of the POST super-global.
* When using just Request::post('x') it will return the raw and untouched $_POST['x'], when using it like
* Request::post('x', true) then it will return a trimmed and stripped $_POST['x'] !
*
* @param mixed $key key
* @param bool $clean marker for optional cleaning of the var
* @return mixed the key's value or nothing
*/
public static function post($key, $clean = false)
{
if (isset($_POST[$key])) {
// we use the Ternary Operator here which saves the if/else block
// @see http://davidwalsh.name/php-shorthand-if-else-ternary-operators
return ($clean) ? trim(strip_tags($_POST[$key])) : $_POST[$key];
}
}
/**
* Returns the state of a checkbox.
*
* @param mixed $key key
* @return mixed state of the checkbox
*/
public static function postCheckbox($key)
{
return isset($_POST[$key]) ? 1 : null;
}
/**
* gets/returns the value of a specific key of the GET super-global
* @param mixed $key key
* @return mixed the key's value or nothing
*/
public static function get($key)
{
if (isset($_GET[$key])) {
return $_GET[$key];
}
}
/**
* gets/returns the value of a specific key of the COOKIE super-global
* @param mixed $key key
* @return mixed the key's value or nothing
*/
public static function cookie($key)
{
if (isset($_COOKIE[$key])) {
return $_COOKIE[$key];
}
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* Session class
*
* handles the session stuff. creates session when no one exists, sets and gets values, and closes the session
* properly (=logout). Not to forget the check if the user is logged in or not.
*/
class Session
{
/**
* starts the session
*/
public static function init()
{
// if no session exist, start the session
if (session_id() == '') {
session_start();
}
}
/**
* sets a specific value to a specific key of the session
*
* @param mixed $key key
* @param mixed $value value
*/
public static function set($key, $value)
{
$_SESSION[$key] = $value;
}
/**
* gets/returns the value of a specific key of the session
*
* @param mixed $key Usually a string, right ?
* @return mixed the key's value or nothing
*/
public static function get($key)
{
if (isset($_SESSION[$key])) {
$value = $_SESSION[$key];
// filter the value for XSS vulnerabilities
return Filter::XSSFilter($value);
}
}
/**
* adds a value as a new array element to the key.
* useful for collecting error messages etc
*
* @param mixed $key
* @param mixed $value
*/
public static function add($key, $value)
{
$_SESSION[$key][] = $value;
}
/**
* deletes the session (= logs the user out)
*/
public static function destroy()
{
session_destroy();
}
/**
* update session id in database
*
* @access public
* @static static method
* @param string $userId
* @param string $sessionId
*/
public static function updateSessionId($userId, $sessionId = null)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET session_id = :session_id WHERE user_id = :user_id";
$query = $database->prepare($sql);
$query->execute(array(':session_id' => $sessionId, ":user_id" => $userId));
}
/**
* checks for session concurrency
*
* This is done as the following:
* UserA logs in with his session id('123') and it will be stored in the database.
* Then, UserB logs in also using the same email and password of UserA from another PC,
* and also store the session id('456') in the database
*
* Now, Whenever UserA performs any action,
* You then check the session_id() against the last one stored in the database('456'),
* If they don't match then log both of them out.
*
* @access public
* @static static method
* @return bool
* @see Session::updateSessionId()
* @see http://stackoverflow.com/questions/6126285/php-stop-concurrent-user-logins
*/
public static function isConcurrentSessionExists()
{
$session_id = session_id();
$userId = Session::get('user_id');
if (isset($userId) && isset($session_id)) {
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT session_id FROM users WHERE user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(":user_id" => $userId));
$result = $query->fetch();
$userSessionId = !empty($result)? $result->session_id: null;
return $session_id !== $userSessionId;
}
return false;
}
/**
* Checks if the user is logged in or not
*
* @return bool user's login status
*/
public static function userIsLoggedIn()
{
return (self::get('user_logged_in') ? true : false);
}
}

32
application/core/Text.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
class Text
{
private static $texts;
public static function get($key, $data = null)
{
// if not $key
if (!$key) {
return null;
}
if ($data) {
foreach ($data as $var => $value) {
${$var} = $value;
}
}
// load config file (this is only done once per application lifecycle)
if (!self::$texts) {
self::$texts = require('../application/config/texts.php');
}
// check if array key exists
if (!array_key_exists($key, self::$texts)) {
return null;
}
return self::$texts[$key];
}
}

177
application/core/View.php Normal file
View File

@@ -0,0 +1,177 @@
<?php
/**
* Class View
* The part that handles all the output
*/
class View
{
/**
* simply includes (=shows) the view. this is done from the controller. In the controller, you usually say
* $this->view->render('help/index'); to show (in this example) the view index.php in the folder help.
* Usually the Class and the method are the same like the view, but sometimes you need to show different views.
* @param string $filename Path of the to-be-rendered view, usually folder/file(.php)
* @param array $data Data to be used in the view
*/
public function render($filename, $data = null)
{
if ($data) {
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
require Config::get('PATH_VIEW') . '_templates/header.php';
require Config::get('PATH_VIEW') . $filename . '.php';
require Config::get('PATH_VIEW') . '_templates/footer.php';
}
/**
* Similar to render, but accepts an array of separate views to render between the header and footer. Use like
* the following: $this->view->renderMulti(array('help/index', 'help/banner'));
* @param array $filenames Array of the paths of the to-be-rendered view, usually folder/file(.php) for each
* @param array $data Data to be used in the view
* @return bool
*/
public function renderMulti($filenames, $data = null)
{
if (!is_array($filenames)) {
self::render($filenames, $data);
return false;
}
if ($data) {
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
require Config::get('PATH_VIEW') . '_templates/header.php';
foreach($filenames as $filename) {
require Config::get('PATH_VIEW') . $filename . '.php';
}
require Config::get('PATH_VIEW') . '_templates/footer.php';
}
/**
* Same like render(), but does not include header and footer
* @param string $filename Path of the to-be-rendered view, usually folder/file(.php)
* @param mixed $data Data to be used in the view
*/
public function renderWithoutHeaderAndFooter($filename, $data = null)
{
if ($data) {
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
require Config::get('PATH_VIEW') . $filename . '.php';
}
/**
* Renders pure JSON to the browser, useful for API construction
* @param $data
*/
public function renderJSON($data)
{
header("Content-Type: application/json");
echo json_encode($data);
}
/**
* renders the feedback messages into the view
*/
public function renderFeedbackMessages()
{
// echo out the feedback messages (errors and success messages etc.),
// they are in $_SESSION["feedback_positive"] and $_SESSION["feedback_negative"]
require Config::get('PATH_VIEW') . '_templates/feedback.php';
// delete these messages (as they are not needed anymore and we want to avoid to show them twice
Session::set('feedback_positive', null);
Session::set('feedback_negative', null);
}
/**
* Checks if the passed string is the currently active controller.
* Useful for handling the navigation's active/non-active link.
*
* @param string $filename
* @param string $navigation_controller
*
* @return bool Shows if the controller is used or not
*/
public static function checkForActiveController($filename, $navigation_controller)
{
$split_filename = explode("/", $filename);
$active_controller = $split_filename[0];
if ($active_controller == $navigation_controller) {
return true;
}
return false;
}
/**
* Checks if the passed string is the currently active controller-action (=method).
* Useful for handling the navigation's active/non-active link.
*
* @param string $filename
* @param string $navigation_action
*
* @return bool Shows if the action/method is used or not
*/
public static function checkForActiveAction($filename, $navigation_action)
{
$split_filename = explode("/", $filename);
$active_action = $split_filename[1];
if ($active_action == $navigation_action) {
return true;
}
return false;
}
/**
* Checks if the passed string is the currently active controller and controller-action.
* Useful for handling the navigation's active/non-active link.
*
* @param string $filename
* @param string $navigation_controller_and_action
*
* @return bool
*/
public static function checkForActiveControllerAndAction($filename, $navigation_controller_and_action)
{
$split_filename = explode("/", $filename);
$active_controller = $split_filename[0];
$active_action = $split_filename[1];
$split_filename = explode("/", $navigation_controller_and_action);
$navigation_controller = $split_filename[0];
$navigation_action = $split_filename[1];
if ($active_controller == $navigation_controller AND $active_action == $navigation_action) {
return true;
}
return false;
}
/**
* Converts characters to HTML entities
* This is important to avoid XSS attacks, and attempts to inject malicious code in your page.
*
* @param string $str The string.
* @return string
*/
public function encodeHTML($str)
{
return htmlentities($str, ENT_QUOTES, 'UTF-8');
}
}

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;
}
}

View File

@@ -0,0 +1,19 @@
<?php
// get the feedback (they are arrays, to make multiple positive/negative messages possible)
$feedback_positive = Session::get('feedback_positive');
$feedback_negative = Session::get('feedback_negative');
// echo out positive messages
if (isset($feedback_positive)) {
foreach ($feedback_positive as $feedback) {
echo '<div class="feedback success">'.$feedback.'</div>';
}
}
// echo out negative messages
if (isset($feedback_negative)) {
foreach ($feedback_negative as $feedback) {
echo '<div class="feedback error">'.$feedback.'</div>';
}
}

View File

@@ -0,0 +1,7 @@
<div class="footer"></div>
</div><!-- close class="wrapper" -->
<!-- the support button on the top right -->
<a class="support-button" href="https://affiliates.a2hosting.com/idevaffiliate.php?id=4471&url=579" target="_blank"></a>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!doctype html>
<html>
<head>
<title>HUGE</title>
<!-- META -->
<meta charset="utf-8">
<!-- send empty favicon fallback to prevent user's browser hitting the server for lots of favicon requests resulting in 404s -->
<link rel="icon" href="data:;base64,=">
<!-- CSS -->
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/style.css" />
</head>
<body>
<!-- wrapper, to center website -->
<div class="wrapper">
<!-- logo -->
<div class="logo"></div>
<!-- navigation -->
<ul class="navigation">
<li <?php if (View::checkForActiveController($filename, "index")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>index/index">Index</a>
</li>
<li <?php if (View::checkForActiveController($filename, "profile")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>profile/index">Profiles</a>
</li>
<?php if (Session::userIsLoggedIn()) { ?>
<li <?php if (View::checkForActiveController($filename, "dashboard")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>dashboard/index">Dashboard</a>
</li>
<li <?php if (View::checkForActiveController($filename, "note")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>note/index">My Notes</a>
</li>
<?php } else { ?>
<!-- for not logged in users -->
<li <?php if (View::checkForActiveControllerAndAction($filename, "login/index")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/index">Login</a>
</li>
<li <?php if (View::checkForActiveControllerAndAction($filename, "register/index")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>register/index">Register</a>
</li>
<?php } ?>
</ul>
<!-- my account -->
<ul class="navigation right">
<?php if (Session::userIsLoggedIn()) : ?>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/index">My Account</a>
<ul class="navigation-submenu">
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/changeUserRole">Change account type</a>
</li>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/editAvatar">Edit your avatar</a>
</li>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/editusername">Edit my username</a>
</li>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/edituseremail">Edit my email</a>
</li>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/changePassword">Change Password</a>
</li>
<li <?php if (View::checkForActiveController($filename, "login")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/logout">Logout</a>
</li>
</ul>
</li>
<?php if (Session::get("user_account_type") == 7) : ?>
<li <?php if (View::checkForActiveController($filename, "admin")) {
echo ' class="active" ';
} ?> >
<a href="<?php echo Config::get('URL'); ?>admin/">Admin</a>
</li>
<?php endif; ?>
<?php endif; ?>
</ul>

View File

@@ -0,0 +1,57 @@
<div class="container">
<h1>Admin/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<div>
This controller/action/view shows a list of all users in the system. with the ability to soft delete a user
or suspend a user.
</div>
<div>
<table class="overview-table">
<thead>
<tr>
<td>Id</td>
<td>Avatar</td>
<td>Username</td>
<td>User's email</td>
<td>Activated ?</td>
<td>Link to user's profile</td>
<td>suspension Time in days</td>
<td>Soft delete</td>
<td>Submit</td>
</tr>
</thead>
<?php foreach ($this->users as $user) { ?>
<tr class="<?= ($user->user_active == 0 ? 'inactive' : 'active'); ?>">
<td><?= $user->user_id; ?></td>
<td class="avatar">
<?php if (isset($user->user_avatar_link)) { ?>
<img src="<?= $user->user_avatar_link; ?>"/>
<?php } ?>
</td>
<td><?= $user->user_name; ?></td>
<td><?= $user->user_email; ?></td>
<td><?= ($user->user_active == 0 ? 'No' : 'Yes'); ?></td>
<td>
<a href="<?= Config::get('URL') . 'profile/showProfile/' . $user->user_id; ?>">Profile</a>
</td>
<form action="<?= config::get("URL"); ?>admin/actionAccountSettings" method="post">
<td><input type="number" name="suspension" /></td>
<td><input type="checkbox" name="softDelete" <?php if ($user->user_deleted) { ?> checked <?php } ?> /></td>
<td>
<input type="hidden" name="user_id" value="<?= $user->user_id; ?>" />
<input type="submit" />
</td>
</form>
</tr>
<?php } ?>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<div class="container">
<h1>DashboardController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<p>
This is an area that's only visible for logged in users. Try to log out, an go to /dashboard/ again. You'll
be redirected to /index/ as you are not logged in. You can protect a whole section in your app within the
according controller by placing <i>Auth::handleLogin();</i> into the constructor.
<p>
</div>
</div>

View File

@@ -0,0 +1,6 @@
<div class="container">
<h1>404 - Page not found</h1>
<div class="box">
<p class="red-text">This page does not exist.</p>
</div>
</div>

View File

@@ -0,0 +1,18 @@
<div class="container">
<h1>IndexController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<p>
This is the homepage. As no real URL-route (like /register/index) is provided, the app uses the default
controller and the default action, defined in application/config/config.php, by default it's
IndexController and index()-method. So, the app will load application/controller/IndexController.php and
run index() from that file. Easy. That index()-method (= the action) has just one line of code inside
($this->view->render('index/index');) that loads application/view/index/index.php, which is basically
this text you are reading right now.
</p>
</div>
</div>

View File

@@ -0,0 +1,50 @@
<div class="container">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="login-page-box">
<div class="table-wrapper">
<!-- login box on left side -->
<div class="login-box">
<h2>Login here</h2>
<form action="<?php echo Config::get('URL'); ?>login/login" method="post">
<input type="text" name="user_name" placeholder="Username or email" required />
<input type="password" name="user_password" placeholder="Password" required />
<label for="set_remember_me_cookie" class="remember-me-label">
<input type="checkbox" name="set_remember_me_cookie" class="remember-me-checkbox" />
Remember me for 2 weeks
</label>
<!-- when a user navigates to a page that's only accessible for logged a logged-in user, then
the user is sent to this page here, also having the page he/she came from in the URL parameter
(have a look). This "where did you came from" value is put into this form to sent the user back
there after being logged in successfully.
Simple but powerful feature, big thanks to @tysonlist. -->
<?php if (!empty($this->redirect)) { ?>
<input type="hidden" name="redirect" value="<?php echo $this->encodeHTML($this->redirect); ?>" />
<?php } ?>
<!--
set CSRF token in login form, although sending fake login requests mightn't be interesting gap here.
If you want to get deeper, check these answers:
1. natevw's http://stackoverflow.com/questions/6412813/do-login-forms-need-tokens-against-csrf-attacks?rq=1
2. http://stackoverflow.com/questions/15602473/is-csrf-protection-necessary-on-a-sign-up-form?lq=1
3. http://stackoverflow.com/questions/13667437/how-to-add-csrf-token-to-login-form?lq=1
-->
<input type="hidden" name="csrf_token" value="<?= Csrf::makeToken(); ?>" />
<input type="submit" class="login-submit-button" value="Log in"/>
</form>
<div class="link-forgot-my-password">
<a href="<?php echo Config::get('URL'); ?>login/requestPasswordReset">I forgot my password</a>
</div>
</div>
<!-- register box on right side -->
<div class="register-box">
<h2>No account yet ?</h2>
<a href="<?php echo Config::get('URL'); ?>register/index">Register</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,35 @@
<div class="container">
<h1>Request a password reset</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<!-- request password reset form box -->
<form method="post" action="<?php echo Config::get('URL'); ?>login/requestPasswordReset_action">
<label for="user_name_or_email">
Enter your username or email and you'll get a mail with instructions:
<input type="text" name="user_name_or_email" required />
</label>
<!-- show the captcha by calling the login/showCaptcha-method in the src attribute of the img tag -->
<img id="captcha" src="<?php echo Config::get('URL'); ?>register/showCaptcha" /><br/>
<input type="text" name="captcha" placeholder="Enter captcha above" required />
<!-- quick & dirty captcha reloader -->
<a href="#" style="display: block; font-size: 11px; margin: 5px 0 15px 0;"
onclick="document.getElementById('captcha').src = '<?php echo Config::get('URL'); ?>register/showCaptcha?' + Math.random(); return false">Reload Captcha</a>
<input type="submit" value="Send me a password-reset mail" />
</form>
</div>
</div>
<div class="container">
<p style="display: block; font-size: 11px; color: #999;">
Please note: This captcha will be generated when the img tag requests the captcha-generation
(= a real image) from YOURURL/register/showcaptcha. As this is a client-side triggered request, a
$_SESSION["captcha"] dump will not show the captcha characters. The captcha generation
happens AFTER the request that generates THIS page has been finished.
</p>
</div>

View File

@@ -0,0 +1,27 @@
<div class="container">
<h1>LoginController/resetPassword</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Set new password</h2>
<p>FYI: ... Idenfitication process works via password-reset-token (hidden input field)</p>
<!-- new password form box -->
<form method="post" action="<?php echo Config::get('URL'); ?>login/setNewPassword" name="new_password_form">
<input type='hidden' name='user_name' value='<?php echo $this->user_name; ?>' />
<input type='hidden' name='user_password_reset_hash' value='<?php echo $this->user_password_reset_hash; ?>' />
<label for="reset_input_password_new">New password (min. 6 characters)</label>
<input id="reset_input_password_new" class="reset_input" type="password"
name="user_password_new" pattern=".{6,}" required autocomplete="off" />
<label for="reset_input_password_repeat">Repeat new password</label>
<input id="reset_input_password_repeat" class="reset_input" type="password"
name="user_password_repeat" pattern=".{6,}" required autocomplete="off" />
<input type="submit" name="submit_new_password" value="Submit new password" />
</form>
<a href="<?php echo Config::get('URL'); ?>login/index">Back to Login Page</a>
</div>
</div>

View File

@@ -0,0 +1,22 @@
<div class="container">
<h1>NoteController/edit/:note_id</h1>
<div class="box">
<h2>Edit a note</h2>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<?php if ($this->note) { ?>
<form method="post" action="<?php echo Config::get('URL'); ?>note/editSave">
<label>Change text of note: </label>
<!-- we use htmlentities() here to prevent user input with " etc. break the HTML -->
<input type="hidden" name="note_id" value="<?php echo htmlentities($this->note->note_id); ?>" />
<input type="text" name="note_text" value="<?php echo htmlentities($this->note->note_text); ?>" />
<input type="submit" value='Change' />
</form>
<?php } else { ?>
<p>This note does not exist.</p>
<?php } ?>
</div>
</div>

View File

@@ -0,0 +1,44 @@
<div class="container">
<h1>NoteController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<p>
This is just a simple CRUD implementation. Creating, reading, updating and deleting things.
</p>
<p>
<form method="post" action="<?php echo Config::get('URL');?>note/create">
<label>Text of new note: </label><input type="text" name="note_text" />
<input type="submit" value='Create this note' autocomplete="off" />
</form>
</p>
<?php if ($this->notes) { ?>
<table class="note-table">
<thead>
<tr>
<td>Id</td>
<td>Note</td>
<td>EDIT</td>
<td>DELETE</td>
</tr>
</thead>
<tbody>
<?php foreach($this->notes as $key => $value) { ?>
<tr>
<td><?= $value->note_id; ?></td>
<td><?= htmlentities($value->note_text); ?></td>
<td><a href="<?= Config::get('URL') . 'note/edit/' . $value->note_id; ?>">Edit</a></td>
<td><a href="<?= Config::get('URL') . 'note/delete/' . $value->note_id; ?>">Delete</a></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } else { ?>
<div>No notes yet. Create some !</div>
<?php } ?>
</div>
</div>

View File

@@ -0,0 +1,44 @@
<div class="container">
<h1>ProfileController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<div>
This controller/action/view shows a list of all users in the system. You could use the underlying code to
build things that use profile information of one or multiple/all users.
</div>
<div>
<table class="overview-table">
<thead>
<tr>
<td>Id</td>
<td>Avatar</td>
<td>Username</td>
<td>User's email</td>
<td>Activated ?</td>
<td>Link to user's profile</td>
</tr>
</thead>
<?php foreach ($this->users as $user) { ?>
<tr class="<?= ($user->user_active == 0 ? 'inactive' : 'active'); ?>">
<td><?= $user->user_id; ?></td>
<td class="avatar">
<?php if (isset($user->user_avatar_link)) { ?>
<img src="<?= $user->user_avatar_link; ?>" />
<?php } ?>
</td>
<td><?= $user->user_name; ?></td>
<td><?= $user->user_email; ?></td>
<td><?= ($user->user_active == 0 ? 'No' : 'Yes'); ?></td>
<td>
<a href="<?= Config::get('URL') . 'profile/showProfile/' . $user->user_id; ?>">Profile</a>
</td>
</tr>
<?php } ?>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,41 @@
<div class="container">
<h1>ProfileController/showProfile/:id</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<div>This controller/action/view shows all public information about a certain user.</div>
<?php if ($this->user) { ?>
<div>
<table class="overview-table">
<thead>
<tr>
<td>Id</td>
<td>Avatar</td>
<td>Username</td>
<td>User's email</td>
<td>Activated ?</td>
</tr>
</thead>
<tbody>
<tr class="<?= ($this->user->user_active == 0 ? 'inactive' : 'active'); ?>">
<td><?= $this->user->user_id; ?></td>
<td class="avatar">
<?php if (isset($this->user->user_avatar_link)) { ?>
<img src="<?= $this->user->user_avatar_link; ?>" />
<?php } ?>
</td>
<td><?= $this->user->user_name; ?></td>
<td><?= $this->user->user_email; ?></td>
<td><?= ($this->user->user_active == 0 ? 'No' : 'Yes'); ?></td>
</tr>
</tbody>
</table>
</div>
<?php } ?>
</div>
</div>

View File

@@ -0,0 +1,38 @@
<div class="container">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<!-- login box on left side -->
<div class="login-box" style="width: 50%; display: block;">
<h2>Register a new account</h2>
<!-- register form -->
<form method="post" action="<?php echo Config::get('URL'); ?>register/register_action">
<!-- the user name input field uses a HTML5 pattern check -->
<input type="text" pattern="[a-zA-Z0-9]{2,64}" name="user_name" placeholder="Username (letters/numbers, 2-64 chars)" required />
<input type="text" name="user_email" placeholder="email address (a real address)" required />
<input type="text" name="user_email_repeat" placeholder="repeat email address (to prevent typos)" required />
<input type="password" name="user_password_new" pattern=".{6,}" placeholder="Password (6+ characters)" required autocomplete="off" />
<input type="password" name="user_password_repeat" pattern=".{6,}" required placeholder="Repeat your password" autocomplete="off" />
<!-- show the captcha by calling the login/showCaptcha-method in the src attribute of the img tag -->
<img id="captcha" src="<?php echo Config::get('URL'); ?>register/showCaptcha" />
<input type="text" name="captcha" placeholder="Please enter above characters" required />
<!-- quick & dirty captcha reloader -->
<a href="#" style="display: block; font-size: 11px; margin: 5px 0 15px 0; text-align: center"
onclick="document.getElementById('captcha').src = '<?php echo Config::get('URL'); ?>register/showCaptcha?' + Math.random(); return false">Reload Captcha</a>
<input type="submit" value="Register" />
</form>
</div>
</div>
<div class="container">
<p style="display: block; font-size: 11px; color: #999;">
Please note: This captcha will be generated when the img tag requests the captcha-generation
(= a real image) from YOURURL/register/showcaptcha. As this is a client-side triggered request, a
$_SESSION["captcha"] dump will not show the captcha characters. The captcha generation
happens AFTER the request that generates THIS page has been finished.
</p>
</div>

View File

@@ -0,0 +1,12 @@
<div class="container">
<h1>Verification</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<a href="<?php echo Config::get('URL'); ?>">Go back to home page</a>
</div>
</div>

View File

@@ -0,0 +1,25 @@
<div class="container">
<h1>UserController/changePassword</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Set new password</h2>
<!-- new password form box -->
<form method="post" action="<?php echo Config::get('URL'); ?>user/changePassword_action" name="new_password_form">
<label for="change_input_password_current">Enter Current Password:</label>
<p><input id="change_input_password_current" class="reset_input" type='password'
name='user_password_current' pattern=".{6,}" required autocomplete="off" /></p>
<label for="change_input_password_new">New password (min. 6 characters)</label>
<p><input id="change_input_password_new" class="reset_input" type="password"
name="user_password_new" pattern=".{6,}" required autocomplete="off" /></p>
<label for="change_input_password_repeat">Repeat new password</label>
<p><input id="change_input_password_repeat" class="reset_input" type="password"
name="user_password_repeat" pattern=".{6,}" required autocomplete="off" /></p>
<input type="submit" name="submit_new_password" value="Submit new password" />
</form>
</div>
</div>

View File

@@ -0,0 +1,31 @@
<div class="container">
<h1>UserController/changeUserRole</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Change account type</h2>
<p>
This page is a basic implementation of the upgrade-process.
User can click on that button to upgrade their accounts from
"basic account" to "premium account". This script simple offers
a click-able button that will upgrade/downgrade the account instantly.
In a real world application you would implement something like a
pay-process.
</p>
<p>
Please note: This whole process has been renamed from AccountType (v3.0) to UserRole (v3.1).
</p>
<h2>Currently your account type is: <?php echo Session::get('user_account_type'); ?></h2>
<!-- basic implementation for two account types: type 1 and type 2 -->
<form action="<?php echo Config::get('URL'); ?>user/changeUserRole_action" method="post">
<?php if (Session::get('user_account_type') == 1) { ?>
<input type="submit" name="user_account_upgrade" value="Upgrade my account (to Premium User)" />
<?php } else if (Session::get('user_account_type') == 2) { ?>
<input type="submit" name="user_account_downgrade" value="Downgrade my account (to Basic User)" />
<?php } ?>
</form>
</div>
</div>

View File

@@ -0,0 +1,28 @@
<div class="container">
<h1>Edit your avatar</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h3>Upload an Avatar</h3>
<div class="feedback info">
If you still see the old picture after uploading a new one: Hard-Reload the page with F5! Your browser doesn't
realize there's a new image as new and old one have the same filename.
</div>
<form action="<?php echo Config::get('URL'); ?>user/uploadAvatar_action" method="post" enctype="multipart/form-data">
<label for="avatar_file">Select an avatar image from your hard-disk (will be scaled to 44x44 px, only .jpg currently):</label>
<input type="file" name="avatar_file" required />
<!-- max size 5 MB (as many people directly upload high res pictures from their digital cameras) -->
<input type="hidden" name="MAX_FILE_SIZE" value="5000000" />
<input type="submit" value="Upload image" />
</form>
</div>
<div class="box">
<h3>Delete your avatar</h3>
<p>Click this link to delete your (local) avatar: <a href="<?php echo Config::get('URL'); ?>user/deleteAvatar_action">Delete your avatar</a>
</div>
</div>

View File

@@ -0,0 +1,17 @@
<div class="container">
<h1>UserController/editUserEmail</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Change your email address</h2>
<form action="<?php echo Config::get('URL'); ?>user/editUserEmail_action" method="post">
<label>
New email address: <input type="text" name="user_email" required />
</label>
<input type="submit" value="Submit" />
</form>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<div class="container">
<h1>UserController/editUsername</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Change your username</h2>
<form action="<?php echo Config::get('URL'); ?>user/editUserName_action" method="post">
<!-- btw http://stackoverflow.com/questions/774054/should-i-put-input-tag-inside-label-tag -->
<label>
New username: <input type="text" name="user_name" required />
</label>
<!-- set CSRF token at the end of the form -->
<input type="hidden" name="csrf_token" value="<?= Csrf::makeToken(); ?>" />
<input type="submit" value="Submit" />
</form>
</div>
</div>

View File

@@ -0,0 +1,21 @@
<div class="container">
<h1>UserController/showProfile</h1>
<div class="box">
<h2>Your profile</h2>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div>Your username: <?= $this->user_name; ?></div>
<div>Your email: <?= $this->user_email; ?></div>
<div>Your avatar image:
<?php if (Config::get('USE_GRAVATAR')) { ?>
Your gravatar pic (on gravatar.com): <img src='<?= $this->user_gravatar_image_url; ?>' />
<?php } else { ?>
Your avatar pic (saved locally): <img src='<?= $this->user_avatar_file; ?>' />
<?php } ?>
</div>
<div>Your account type is: <?= $this->user_account_type; ?></div>
</div>
</div>