diff options
Diffstat (limited to 'src/index.php')
| -rw-r--r-- | src/index.php | 152 | 
1 files changed, 151 insertions, 1 deletions
| 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; |