diff --git a/TODO.md b/TODO.md index fae32df8..8911bdb5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,9 @@ TODO: * Draaiing van foto's bij importeren goedzetten -* Import asset ownership * Import users +* Import asset ownership * Pagina om één foto te bekijken * Taggen door gebruikers * Uploaden door gebruikers * Album management -* Password reset via e-mail diff --git a/controllers/Login.php b/controllers/Login.php index b75c0b95..700b1715 100644 --- a/controllers/Login.php +++ b/controllers/Login.php @@ -29,9 +29,12 @@ class Login extends HTMLController if (isset($_POST['redirect_url'])) header('Location: ' . base64_decode($_POST['redirect_url'])); elseif (isset($_SESSION['login_url'])) + { + unset($_SESSION['redirect_url']); header('Location: ' . $_SESSION['redirect_url']); + } else - header('Location: ' . BASEURL . '/admin/'); + header('Location: ' . BASEURL . '/'); exit; } else @@ -39,15 +42,28 @@ class Login extends HTMLController } parent::__construct('Log in - ' . SITE_TITLE); - $this->page->appendStylesheet(BASEURL . '/css/admin.css'); $form = new LogInForm('Log in'); if ($login_error) - $form->setErrorMessage('Invalid email address or password.'); + $form->adopt(new Alert('', 'Invalid email address or password.', 'error')); // Tried anything? Be helpful, at least. if (isset($_POST['emailaddress'])) $form->setEmail($_POST['emailaddress']); + // A message from the past/present/future? + if (isset($_SESSION['login_msg'])) + { + $form->adopt(new Alert($_SESSION['login_msg'][0], $_SESSION['login_msg'][1], $_SESSION['login_msg'][2])); + unset($_SESSION['login_msg']); + } + + // Going somewhere? + if (!empty($_GET['redirect']) && ($url = base64_decode($_GET['redirect']))) + { + $_SESSION['login_url'] = $url; + $form->setRedirectUrl($url); + } + $this->page->adopt($form); } } diff --git a/controllers/ResetPassword.php b/controllers/ResetPassword.php new file mode 100644 index 00000000..7153498b --- /dev/null +++ b/controllers/ResetPassword.php @@ -0,0 +1,81 @@ +isLoggedIn()) + throw new UserFacingException('You are already logged in.'); + + // Verifying an existing reset key? + if (isset($_GET['step'], $_GET['email'], $_GET['key']) && $_GET['step'] == 2) + { + $email = rawurldecode($_GET['email']); + $id_user = Authentication::getUserid($email); + if ($id_user === false) + throw new UserFacingException('Invalid email address. Please make sure you copied the full link in the email you received.'); + + $key = $_GET['key']; + if (!Authentication::checkResetKey($id_user, $key)) + throw new UserFacingException('Invalid reset token. Please make sure you copied the full link in the email you received. Note: you cannot use the same token twice.'); + + parent::__construct('Reset password - ' . SITE_TITLE); + $form = new PasswordResetForm($email, $key); + $this->page->adopt($form); + + // Are they trying to set something already? + if (isset($_POST['password1'], $_POST['password2'])) + { + $missing = []; + if (strlen($_POST['password1']) < 6 || !preg_match('~[^A-z]~', $_POST['password1'])) + $missing[] = 'Please fill in a password that is at least six characters long and contains at least one non-alphabetic character (e.g. a number or symbol).'; + if ($_POST['password1'] != $_POST['password2']) + $missing[] = 'The passwords you entered do not match.'; + + // So, are we good to go? + if (empty($missing)) + { + Authentication::updatePassword($id_user, Authentication::computeHash($_POST['password1'])); + $_SESSION['login_msg'] = ['Your password has been reset', 'You can now use the form below to log in to your account.', 'success']; + header('Location: ' . BASEURL . '/login/'); + exit; + } + else + $form->adopt(new Alert('Some fields require your attention', '', 'error')); + } + } + else + { + parent::__construct('Reset password - ' . SITE_TITLE); + $form = new ForgotPasswordForm(); + $this->page->adopt($form); + + // Have they submitted an email address yet? + if (isset($_POST['emailaddress']) && preg_match('~^.+@.+\.[a-z]+$~', trim($_POST['emailaddress']))) + { + $id_user = Authentication::getUserid(trim($_POST['emailaddress'])); + if ($id_user === false) + { + $form->adopt(new Alert('Invalid email address', 'The email address you provided could not be found in our system. Please try again.', 'error')); + return; + } + + Authentication::setResetKey($id_user); + Email::resetMail($id_user); + + // Show the success message + $this->page->clear(); + $box = new DummyBox('An email has been sent'); + $box->adopt(new Alert('', 'We have sent an email to ' . $_POST['emailaddress'] . ' containing details on how to reset your password.', 'success')); + $this->page->adopt($box); + } + } + } +} diff --git a/models/Authentication.php b/models/Authentication.php index 1f0f5055..55f9dc3b 100644 --- a/models/Authentication.php +++ b/models/Authentication.php @@ -44,6 +44,31 @@ class Authentication return empty($res) ? false : $res; } + public static function setResetKey($id_user) + { + return Registry::get('db')->query(' + UPDATE users + SET reset_key = {string:key} + WHERE id_user = {int:id}', + [ + 'id' => $id_user, + 'key' => self::newActivationKey(), + ]); + } + + public static function checkResetKey($id_user, $reset_key) + { + $key = Registry::get('db')->queryValue(' + SELECT reset_key + FROM users + WHERE id_user = {int:id}', + [ + 'id' => $id_user, + ]); + + return $key == $reset_key; + } + /** * Verifies whether the user is currently logged in. */ @@ -62,6 +87,18 @@ class Authentication return isset($_SESSION['user_id']) && self::checkExists($_SESSION['user_id']); } + /** + * Generates a new activation key. + */ + public static function newActivationKey() + { + $alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $string = ''; + for ($i = 0; $i < 16; $i++) + $string .= $alpha[mt_rand(0, strlen($alpha) - 1)]; + return $string; + } + /** * Checks a password for a given username against the database. */ diff --git a/models/Dispatcher.php b/models/Dispatcher.php index 9336d442..3647905c 100644 --- a/models/Dispatcher.php +++ b/models/Dispatcher.php @@ -21,6 +21,7 @@ class Dispatcher 'managetags' => 'ManageTags', 'manageusers' => 'ManageUsers', 'people' => 'ViewPeople', + 'resetpassword' => 'ResetPassword', 'suggest' => 'ProvideAutoSuggest', 'timeline' => 'ViewTimeline', 'uploadmedia' => 'UploadMedia', @@ -77,6 +78,11 @@ class Dispatcher else self::trigger403(); } + catch (UserFacingException $e) + { + $debug_info = ErrorHandler::getDebugInfo($e->getTrace()); + ErrorHandler::display($e->getMessage(), $debug_info, false); + } catch (Exception $e) { ErrorHandler::handleError(E_USER_ERROR, 'Unspecified exception: ' . $e->getMessage(), $e->getFile(), $e->getLine()); @@ -93,7 +99,7 @@ class Dispatcher public static function kickGuest() { $form = new LogInForm('Log in'); - $form->setErrorMessage('Admin access required. Please log in.'); + $form->adopt(new Alert('', 'You need to be logged in to view this page.', 'error')); $form->setRedirectUrl($_SERVER['REQUEST_URI']); $page = new MainTemplate('Login required'); diff --git a/models/Email.php b/models/Email.php new file mode 100644 index 00000000..456b0136 --- /dev/null +++ b/models/Email.php @@ -0,0 +1,100 @@ +\r\n"; + + // Set up headers. + $headers .= "MIME-Version: 1.0\r\n"; + $headers .= "Content-Type: multipart/alternative;boundary=$boundary\r\n"; + + // Start the message with a plaintext version of the mail. + $message = "This is a MIME encoded message."; + $message .= "\r\n\r\n--$boundary\r\n"; + $message .= "Content-type: text/plain;charset=utf-8\r\n\r\n"; + $message .= self::wrapLines($body); + + // Autolink URLs and wrap lines. + $html_body = preg_replace('~\b(?!")https?://[^"\s]+~', '$0', $body); + $html_body = preg_replace('~(\s+)(www\..+\.[^"\s]+?)(\.|\s+)~', '$1$2$3', $html_body); + $html_body = preg_replace('~={10,}~', '
', $html_body); + $html_body = self::wrapLines(str_replace("\r", "
\r", $html_body), 80, "\r\n"); + + // Then, more excitingly, add an HTML version! + $message .= "\r\n\r\n--" . $boundary . "\r\n"; + $message .= "Content-type: text/html;charset=utf-8\r\n\r\n"; + $message .= "\r\n\r\n

" . + $html_body . "

\r\n"; + + // End off with a final boundary. + $message .= "\r\n\r\n--" . $boundary . "--"; + + if (DEBUG) + return file_put_contents(BASEDIR . '/mail_dumps.txt', "To: \"$addressee\" <$address>\r\n$headers\r\nSubject: $subject\r\n" . self::wrapLines($message), FILE_APPEND); + else + return mail("\"$addressee\" <$address>", $subject, $message, $headers, '-fbounces@pics.hashru.nl'); + } + + public static function wrapLines($body, $maxlength = 80, $break = "\r\n") + { + $lines = explode("\n", $body); + $wrapped = ""; + foreach ($lines as $line) + $wrapped .= wordwrap($line, $maxlength, $break) . $break; + return $wrapped; + } + + private static function parseTemplate($template, $replacements) + { + $replacement_keys = array_map(function($el) { return "%$el%"; }, array_keys($replacements)); + $subject = str_replace($replacement_keys, array_values($replacements), $template['subject']); + $body = str_replace($replacement_keys, array_values($replacements), $template['body']); + return [$subject, $body]; + } + + public static function resetMail($id_user) + { + $row = Registry::get('db')->queryAssoc(' + SELECT first_name, surname, emailaddress, reset_key + FROM users + WHERE id_user = {int:id_user}', + [ + 'id_user' => $id_user, + ]); + + if (empty($row)) + return false; + + + list($subject, $body) = self::parseTemplate([ + 'subject' => 'Information on how to reset your HashRU password', + 'body' => str_replace("\n", "\r\n", 'Dear %FIRST_NAME%, + +You are receiving this email because a password reset request was issued on our website. + +If you did not request a password reset, please disregard this email. Otherwise, please follow the link below. + +%RESET_LINK% + +The HashRU Pics team'), + ], [ + 'FIRST_NAME' => $row['first_name'], + 'RESET_LINK' => BASEURL . '/resetpassword/?step=2&email=' . rawurlencode($row['emailaddress']) . '&key=' . $row['reset_key'], + ]); + + $addressee = trim($row['first_name'] . ' ' . $row['surname']); + self::send($row['emailaddress'], $addressee, $subject, $body); + } +} diff --git a/models/ErrorHandler.php b/models/ErrorHandler.php index 214f6ec5..da650e93 100644 --- a/models/ErrorHandler.php +++ b/models/ErrorHandler.php @@ -3,7 +3,7 @@ * ErrorHandler.php * Contains key class ErrorHandler. * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen + * Kabuki CMS (C) 2013-2016, Aaron van Geffen *****************************************************************************/ class ErrorHandler @@ -11,6 +11,18 @@ class ErrorHandler private static $error_count = 0; private static $handling_error; + public static function enable() + { + set_error_handler('ErrorHandler::handleError'); + ini_set("display_errors", DEBUG ? "On" : "Off"); + } + + public static function disable() + { + set_error_handler(NULL); + ini_set("display_errors", "Off"); + } + // Handler for standard PHP error messages. public static function handleError($error_level, $error_message, $file, $line, $context = null) { @@ -121,14 +133,27 @@ class ErrorHandler return $error_message; } - public static function display($message, $debug_info) + public static function display($message, $debug_info, $is_sensitive = true) { + $is_admin = Registry::has('user') && Registry::get('user')->isAdmin(); + // Just show the message if we're running in a console. if (empty($_SERVER['HTTP_HOST'])) { echo $message; exit; } + // JSON request? + elseif (isset($_GET['json']) || isset($_GET['format']) && $_GET['format'] == 'json') + { + if (DEBUG || $is_admin) + echo json_encode(['error' => $message . "\n\n" . $debug_info]); + elseif (!$is_sensitive) + echo json_encode(['error' => $message]); + else + echo json_encode(['error' => 'Our apologies, an error occured while we were processing your request. Please try again later, or contact us if the problem persists.']); + exit; + } // Initialise the main template to present a nice message to the user. $page = new MainTemplate('An error occured!'); @@ -146,6 +171,8 @@ class ErrorHandler $page->adopt(new AdminBar()); } } + elseif (!$is_sensitive) + $page->adopt(new DummyBox('An error occured!', '

' . $message . '

')); else $page->adopt(new DummyBox('An error occured!', '

Our apologies, an error occured while we were processing your request. Please try again later, or contact us if the problem persists.

')); diff --git a/models/UserFacingException.php b/models/UserFacingException.php new file mode 100644 index 00000000..031aa435 --- /dev/null +++ b/models/UserFacingException.php @@ -0,0 +1,12 @@ + p, +.alert-block > ul { + margin-bottom: 0; +} +.alert-block p + p { + margin-top: 5px; +} + + /* Responsive: smartphone in portrait ---------------------------------------*/ @media only screen and (max-width: 895px) { diff --git a/templates/Alert.php b/templates/Alert.php new file mode 100644 index 00000000..1267ecae --- /dev/null +++ b/templates/Alert.php @@ -0,0 +1,24 @@ +_title = $title; + $this->_message = $message; + $this->_type = in_array($type, ['alert', 'error', 'success', 'info']) ? $type : 'alert'; + } + + protected function html_content() + { + echo ' +
', (!empty($this->_title) ? ' + ' . $this->_title . '
' : ''), $this->_message, '
'; + } +} diff --git a/templates/ForgotPasswordForm.php b/templates/ForgotPasswordForm.php new file mode 100644 index 00000000..7a50660b --- /dev/null +++ b/templates/ForgotPasswordForm.php @@ -0,0 +1,29 @@ + +

Password reset procedure

'; + + foreach ($this->_subtemplates as $template) + $template->html_main(); + + echo ' +

Please fill in the email address you used to sign up in the form below. You will be sent a reset link to your email address.

+
+
+ + +
+ '; + } +} diff --git a/templates/LogInForm.php b/templates/LogInForm.php index c051b4d8..c2b8835b 100644 --- a/templates/LogInForm.php +++ b/templates/LogInForm.php @@ -8,15 +8,9 @@ class LogInForm extends SubTemplate { - private $error_message = ''; private $redirect_url = ''; private $emailaddress = ''; - public function setErrorMessage($message) - { - $this->error_message = $message; - } - public function setRedirectUrl($url) { $_SESSION['login_url'] = $url; @@ -32,12 +26,10 @@ class LogInForm extends SubTemplate { echo '
-

Admin login

'; +

Log in

'; - // Invalid login? Show a message. - if (!empty($this->error_message)) - echo ' -

', $this->error_message, '

'; + foreach ($this->_subtemplates as $template) + $template->html_main(); echo '
@@ -54,7 +46,10 @@ class LogInForm extends SubTemplate '; echo ' -
+ Forgotten your password? +
+ +
'; } } diff --git a/templates/PasswordResetForm.php b/templates/PasswordResetForm.php new file mode 100644 index 00000000..4bc9c5e8 --- /dev/null +++ b/templates/PasswordResetForm.php @@ -0,0 +1,46 @@ +email = $email; + $this->key = $key; + } + + protected function html_content() + { + echo ' +
+

Password reset procedure

'; + + foreach ($this->_subtemplates as $template) + $template->html_main(); + + echo ' +

You have successfully confirmed your identify. Please use the form below to set a new password.

+
+

+ + +

+ +

+ + +

+ + +
+
'; + } +}