" required autofocus>
@@ -33,6 +35,7 @@ if (($_formError = RequestUtils::getAndClearFormError("login")) !== null) {
diff --git a/src/application/views/form_new_password.php b/src/application/views/form_new_password.php
new file mode 100644
index 0000000..7431bd5
--- /dev/null
+++ b/src/application/views/form_new_password.php
@@ -0,0 +1,38 @@
+
+
+
+
+ $_formError]);
+}
+?>
+
+
+
diff --git a/src/application/views/form_password_reset.php b/src/application/views/form_password_reset.php
new file mode 100644
index 0000000..57d8ed2
--- /dev/null
+++ b/src/application/views/form_password_reset.php
@@ -0,0 +1,41 @@
+
+
+
+
+ $_formError]);
+}
+?>
+
+
+
diff --git a/src/index.php b/src/index.php
index 801cc07..62874e6 100644
--- a/src/index.php
+++ b/src/index.php
@@ -15,6 +15,7 @@ use mystic\forum\orm\UserPermissions;
use mystic\forum\utils\FileUtils;
use mystic\forum\utils\RequestUtils;
use mystic\forum\utils\ValidationUtils;
+use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
@@ -70,6 +71,41 @@ function generateCaptchaText(): string {
return $phrase;
}
+function generatePasswordResetLink(Database &$db, User &$user): ?string {
+ $token = $db->generateId(20);
+
+ $user->passwordResetToken = $token;
+ $user->passwordResetTokenCreated = new \DateTimeImmutable();
+
+ if (!$db->update($user)) {
+ return null;
+ }
+
+ return env("PUBLIC_URL") . "?_action=pwreset&token=" . urlencode($user->passwordResetToken) . "&sig=" . urlencode(base64_encode(hash("sha256", env("SECRET") . $user->passwordResetToken . $user->id . $user->passwordHash, true)));
+}
+
+function decodePasswordResetLink(Database &$db, string $token, string $signature): ?User {
+ $user = new User();
+ $user->passwordResetToken = $token;
+
+ if (!$db->fetchWhere($user, "password_reset_token")) {
+ return null;
+ }
+
+ $then = $user->passwordResetTokenCreated;
+ $now = new \DateTimeImmutable();
+ if (($now->getTimestamp() - $then->getTimestamp()) >= 3600) {
+ return null;
+ }
+
+ $expectedSignature = base64_encode(hash("sha256", env("SECRET") . $user->passwordResetToken . $user->id . $user->passwordHash, true));
+ if ($expectedSignature !== $signature) {
+ return null;
+ }
+
+ return $user;
+}
+
function getThemeAndLangInfo(): array {
$availableThemes = [];
$currentTheme = $_GET["theme"] ?? $_COOKIE["theme"] ?? env("MYSTIC_FORUM_THEME") ?? "default";
@@ -385,7 +421,7 @@ if ($_action === "auth") {
"forum_copyright" => (env("MYSTIC_FORUM_COPYRIGHT") ?? env("MYSTIC_FORUM_TITLE") ?? "Forum")
]
))
- ->subject("Please activate your account")
+ ->subject(__("Please activate your account"))
);
$db->insert($user);
@@ -1311,6 +1347,120 @@ if ($_action === "auth") {
$next = $_POST["next"] ?? ".";
setcookie("lang", $lang, time()+60*60*24*30);
header("Location: $next");
+} elseif ($_action === "pwreset") {
+ if ($currentUser) {
+ header("Location: .");
+ exit;
+ }
+
+ if (RequestUtils::isRequestMethod("POST")) {
+ $token = $_GET["token"] ?? null;
+ $signature = $_GET["sig"] ?? null;
+
+ if ($token !== null && $signature !== null) {
+ RequestUtils::setFormErrorDestination("?_action=pwreset&token=" . urlencode($token) . "&sig=" . urlencode($signature));
+ $formId = "pwnew";
+ $newPassword = RequestUtils::getRequiredField("new_password", $formId);
+ $retypePassword = RequestUtils::getRequiredField("retype_password", $formId);
+ $resetUser = decodePasswordResetLink($db, $token, $signature);
+
+ if ($resetUser === null) {
+ http_response_code(400);
+ msg_error(__("The password reset link is either invalid or it expired"), true);
+ exit;
+ }
+
+ if ($newPassword !== $retypePassword) {
+ RequestUtils::triggerFormError(__("New passwords don't match"), $formId);
+ }
+
+ if (strlen($newPassword) < 8) {
+ RequestUtils::triggerFormError(__("Password too short! Your password must consist of 8 or more characters"), $formId);
+ }
+
+ $resetUser->passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
+ $resetUser->passwordResetToken = null;
+ $resetUser->passwordResetTokenCreated = null;
+
+ if (!$db->update($resetUser)) {
+ RequestUtils::triggerFormError(__("Failed to update password"), $formId);
+ }
+
+ msg_info(__("Password reset successfully!"), true);
+ } else {
+ $formId = "pwreset";
+ $email = RequestUtils::getRequiredField("email", $formId);
+
+ $user = new User();
+ $user->email = $email;
+
+ if ($db->fetchWhere($user, "email")) {
+ try {
+ Transport::fromDsn(env("MAILER_DSN"))->send(
+ (new Email())
+ ->from(env("MAILER_FROM"))
+ ->to(new Address($user->email, $user->displayName))
+ ->text(__(
+ "Hello, %user_display_name%!\n" .
+ "\n" .
+ "a password reset has been requested successfully! Please click the link below to set a new password:\n" .
+ "%reset_link%\n" .
+ "\n" .
+ "If this wasn't you, you can safely ignore this email. The link will only be valid for one hour.\n" .
+ "\n" .
+ "Kind regards,\n" .
+ "%forum_copyright%",
+ params: [
+ "forum_title" => (env("MYSTIC_FORUM_TITLE") ?? "Forum"),
+ "user_display_name" => $user->displayName,
+ "reset_link" => generatePasswordResetLink($db, $user),
+ "forum_copyright" => (env("MYSTIC_FORUM_COPYRIGHT") ?? env("MYSTIC_FORUM_TITLE") ?? "Forum")
+ ]
+ ))
+ ->subject(__("Forgot your password? No problem!"))
+ );
+ } catch (TransportException $_) {
+ // fail silently
+ }
+ } else {
+ // don't make the delay difference too obvious
+ usleep(random_int(900, 4500) * 1000);
+
+ // ideally, at some point we would just want to queue up the email
+ // and send it asynchronously, but this'll have to do for now
+ }
+
+ msg_info(__("If an account exists with the given email address, we will have sent a password reset link to that email address."), true);
+ }
+ } else {
+ $token = $_GET["token"] ?? null;
+ $signature = $_GET["sig"] ?? null;
+
+ if ($token !== null && $signature !== null) {
+ $resetUser = decodePasswordResetLink($db, $token, $signature);
+ if ($resetUser === null) {
+ http_response_code(400);
+ msg_error(__("The password reset link is either invalid or it expired"), true);
+ exit;
+ }
+
+ _view("template_start", [ "_title" => __("Reset password") ]);
+ _view("template_navigation_start");
+ _view("template_navigation_end");
+ _view("form_new_password", [
+ "token" => $token,
+ "signature" => $signature,
+ ]);
+ _view("template_end", [...getThemeAndLangInfo()]);
+ } else {
+ _view("template_start", [ "_title" => __("Reset password") ]);
+ _view("template_navigation_start");
+ _view("template_navigation", ["user" => null]);
+ _view("template_navigation_end");
+ _view("form_password_reset");
+ _view("template_end", [...getThemeAndLangInfo()]);
+ }
+ }
} elseif ($_action === "ctheme") {
// options
$enableLogging = true;
--
cgit v1.2.3