diff --git a/core/05-functions.php b/core/05-functions.php index dcca0b9..5f306f6 100644 --- a/core/05-functions.php +++ b/core/05-functions.php @@ -28,12 +28,27 @@ function url_origin( $s = false, $use_forwarded_host = false ) * @param bool $use_forwarded_host Whether to take the X-Forwarded-Host header into account. * @return string The full url, as requested by the client. */ -function full_url( $s = false, $use_forwarded_host = false ) -{ +function full_url($s = false, $use_forwarded_host = false) { if($s == false) $s = $_SERVER; return url_origin( $s, $use_forwarded_host ) . $s['REQUEST_URI']; } +/** + * Get the stem URL at which this Pepperminty Wiki instance is located + * You can just append ?get_params_here to this and it will be a valid URL. + * Uses full_url() under the hood. + * Note that this is based on the URL of the current request. + * @param array $s The $_SERVER variable (defaults to $_SERVER) + * @param boolean $use_forwarded_host Whether to use the x-forwarded-host header or ignore it. + * @return string The stem url, as described above + */ +function url_stem( $s = false, $use_forwarded_host = false) { + // Calculate the stem from the current full URL by stripping everything after the question mark ('?') + $url_stem = full_url(); + if(mb_strrpos($url_stem, "?") !== false) $url_stem = mb_substr($url_stem, mb_strrpos($url_stem, "?")); + return $url_stem; +} + /** * Converts a filesize into a human-readable string. * @package core @@ -698,9 +713,9 @@ function extract_user_from_userpage($userPagename) { * @param string $body The body of the email. * @return bool Whether the email was sent successfully or not. Currently, this may fail if the user doesn't have a registered email address. */ -function email_user($username, $subject, $body) +function email_user(string $username, string $subject, string $body) : boolean { - global $version, $settings; + global $version, $env, $settings; static $literator = null; if($literator == null) $literator = Transliterator::createFromRules(':: Any-Latin; :: Latin-ASCII; :: NFD; :: [:Nonspacing Mark:] Remove; :: NFC;', Transliterator::FORWARD); @@ -709,6 +724,10 @@ function email_user($username, $subject, $body) if(empty($settings->users->{$username}->emailAddress)) return false; + // If email address verification is required but hasn't been done for this user, skip them + if(empty($env->user_data->emailAddressVerified)) + return false; + $headers = [ "x-mailer" => ini_get("user_agent"), @@ -790,3 +809,16 @@ function delete_recursive($path, $delete_self = true) { } if($delete_self) rmdir($path); } + +/** + * Generates a crytographically-safe random id of the given length. + * @param int $length The length of id to generate. + * @return string The random id. + */ +function crypto_id(int $length) : string { + // It *should* be the right length already, but it doesn't hurt to be safe + return substr(strtr( + base64_encode(random_bytes($length * 0.75)), + [ "=" => "", "+" => "-", "/" => "_"] + ), 0, $length); +} diff --git a/module_index.json b/module_index.json index b79a383..0bf35ba 100755 --- a/module_index.json +++ b/module_index.json @@ -175,7 +175,7 @@ "version": "0.3.4", "author": "Starbeamrainbowlabs", "description": "Adds a user preferences page, letting people do things like change their email address and password.", - "lastupdate": 1577140301, + "lastupdate": 1578257121, "optional": false, "extra_data": [] }, @@ -192,10 +192,10 @@ { "id": "feature-watchlist", "name": "User watchlists", - "version": "0.1", + "version": "0.1.2", "author": "Starbeamrainbowlabs", "description": "Adds per-user watchlists. When a page on a user's watchlist is edited, a notification email is sent.", - "lastupdate": 1577141571, + "lastupdate": 1578255352, "optional": false, "extra_data": [] }, diff --git a/modules/feature-user-preferences.php b/modules/feature-user-preferences.php index 10edf0a..a2b3c29 100644 --- a/modules/feature-user-preferences.php +++ b/modules/feature-user-preferences.php @@ -66,6 +66,9 @@ register_module([ $content .= " \n"; $content .= " \n"; $content .= "

Used to send you notifications etc. Never shared with anyone except $settings->admindetails_name, $settings->sitename's administrator.

\n"; + if($settings->email_user_verify) { + $content .= "

Verification status: ".(!empty($env->user_data->emailAddressVerificationCode) || !$env->user_data->emailAddressVerificationCode ? "not " : "")."verified

"; + } $content .= " \n"; $content .= "\n"; $content .= "

Change Password"; @@ -103,31 +106,91 @@ register_module([ exit(page_renderer::render_main("Error Saving Preferences - $settings->sitename", "

You aren't logged in, so you can't save your preferences. Try logging in first.

")); } - if(isset($_POST["email-address"])) - { - if(mb_strlen($_POST["email-address"]) > 320) - { + if(isset($_POST["email-address"])) { + if(mb_strlen($_POST["email-address"]) > 320) { http_response_code(413); exit(page_renderer::render_main("Error Saving Email Address - $settings->sitename", "

The email address you supplied ({$_POST['email-address']}) is too long. Email addresses can only be 320 characters long. Go back.")); } - if(mb_strpos($_POST["email-address"], "@") === false) - { + if(mb_strpos($_POST["email-address"], "@") === false) { http_response_code(422); exit(page_renderer::render_main("Error Saving Email Address - $settings->sitename", "

The email address you supplied ({$_POST['email-address']}) doesn't appear to be valid. Go back.")); } - + $old_address = $env->user_data->emailAddress ?? null; $env->user_data->emailAddress = $_POST["email-address"]; + // If email address verification is required and the email + // address has changed, send a verification email now + if($settings->email_user_verify && $old_address !== $_POST["email-address"]) { + $env->user_data->emailAddressVerified = false; + if(!email_user_verify($env->user)) { + http_response_code(503); + exit(page_renderer::render_main("Server error sending verification code - $settings->sitename", "

$settings->sitename tried to send you an email to verify your email address, but was unable to do so. The changes to your settings have not been saved. Please contact $settings->admindetails_name, whose email address can be found at the bottom of this page.

")); + } + } } // Save the user's preferences - if(!save_userdata()) - { + if(!save_userdata()) { http_response_code(503); exit(page_renderer::render_main("Error Saving Preferences - $settings->sitename", "

$settings->sitename had some trouble saving your preferences! Please contact $settings->admindetails_name, $settings->sitename's administrator and tell them about this error if it still occurs in 5 minutes. They can be contacted by email at this address: " . hide_email($settings->admindetails_email) . ".

")); } - exit(page_renderer::render_main("Preferences Saved Successfully - $settings->sitename", "

Your preferences have been saved successfully! You could go back your preferences page, or on to the $settings->defaultpage.

")); + exit(page_renderer::render_main("Preferences Saved Successfully - $settings->sitename", "

Your preferences have been saved successfully! You could go back your preferences page, or on to the $settings->defaultpage.

+

If you changed your email address, a verification code will have been sent to the email address you specified. Click on the link provided to verify your new email address.

")); + }); + + /** + * @api {get} ?action=email-address-verify&code={code} Verify the current user's email address + * @apiName EmailAddressVerify + * @apiGroup Settings + * @apiPermission User + * + * @apiParam {string} code The verfication code. + * + * @apiError VerificationCodeIncorrect The supplied verification code is not correct. + */ + add_action("email-address-verify", function() { + global $env, $settings; + + if(!$env->is_logged_in) { + http_response_code(307); + header("x-status: failed"); + header("x-problem: not-logged-in"); + exit(page_renderer::render_main("Not logged in - $settings->sitename", "

You aren't logged in, so you can't verify your email address. Try logging in.

")); + } + + if($env->user_data->emailAddressVerified) { + header("x-status: success"); + exit(page_renderer::render_main("Already verified - $settings->sitename", "

Your email address is already verified, so you don't need to verify it again.

\n

Go to the main page.

")); + } + + if(empty($_GET["code"])) { + http_response_code(400); + header("x-status: failed"); + header("x-problem: no-code-specified"); + exit(page_renderer::render_main("No verification code specified - $settings->sitename", "

No verification code specified. Do so with the code GET parameter.

")); + } + + if($env->user_data->emailAddressVerificationCode !== $_GET["code"]) { + http_resonse_code(400); + header("x-status: failed"); + header("x-problem: code-incorrect"); + exit(page_renderer::render_main("Verification code incorrect", "

That verification code was incorrect. Try specifying another one, or going to your user preferences and changing your email address to re-send another code (you can change it to the same address).

")); + } + + // The code supplied must be valid + unset($env->user_data->emailAddressVerificationCode); + $env->user_data->emailAddressVerified = true; + + if(!save_settings()) { + http_response_code(503); + header("x-status: failed"); + header("x-problem: server-error-disk-io"); + exit(page_renderer::render_main("Server error - $settings->sitename", "

Your verification code was correct, but $settings->sitename was unable to update your user details because it failed to write the changes to disk. Please contact $settings->admindetails_name, whose email address can be found at the bottom of the page.

")); + } + + header("x-status: success"); + exit(page_renderer::render_main("Email Address Verified - $settings->sitename", "

Your email address was verified successfully. Go to the main page

, or to your user preferences to make further changes.

")); }); /** @@ -223,17 +286,52 @@ register_module([ // Display a help section on the user preferences, but only if the user // is logged in and so able to access them - if($env->is_logged_in) - { + if($env->is_logged_in) { add_help_section("910-user-preferences", "User Preferences", "

As you are logged in, $settings->sitename lets you configure a selection of personal preferences. These can be viewed and tweaked to you liking over on the preferences page, which can be accessed at any time by clicking the cog icon (it looks something like this: $settings->user_preferences_button_text), though the administrator of $settings->sitename ($settings->admindetails_name) may have changed its appearance.

"); } - if($settings->avatars_show) - { + if($settings->avatars_show) { add_help_section("915-avatars", "Avatars", "

$settings->sitename allows you to upload an avatar and have it displayed next to your name. If you don't have an avatar uploaded yet, then $settings->sitename will take a hash of your email address and ask Gravatar for for your Gravatar instead. If you haven't told $settings->sitename what your email address is either, a hash of your username is used instead. If you don't have a gravatar, then $settings->sitename asks Gravatar for an identicon instead.

Your avatar on $settings->sitename currently looks like this: " . ($settings->upload_enabled ? " - you can upload a new one by going to your preferences, or clicking here." : ", but $settings->sitename currently has uploads disabled, so you can't upload a new one directly to $settings->sitename. You can, however, set your email address in your preferences and create a Gravatar, and then it should show up here on $settings->sitename shortly.") . "

"); } } ]); +/** + * Sends a verification email to the specified user, assuming they need to + * verify their email address. + * If a user does not need to verify their email address, no verification email + * is sent and true is returned. + * @param string $username The name of the user to send the verification code to. + * @return boolean Whether the verification code was sent successfully. If a user does not need to verify their email address, this returns true. + */ +function email_user_verify(string $username) : boolean { + global $settings; + + $user_data = $settings->users->$username; + + if(!empty($user_data->emailAddressVerified) && + $user_data->emailAddressVerified === true) { + return true; + } + + // Generate a verification code + $user_data->emailAddressVerificationCode = crypto_id(64); + if(!save_settings()) + return false; + + return email_user( + $username, + "Verify your account - $settings->sitename", + "Hey there! Click this link to verify your account on $settings->sitename: + +".url_stem()."?action=email-address-verify&code=$user_data->emailAddressVerificationCode + +$settings->sitename requires that you verify your email address in order to use it. + +--$settings->sitename +Powered by Pepperminty Wiki" + ); +} + ?> diff --git a/peppermint.guiconfig.json b/peppermint.guiconfig.json index d77ba2a..eacae62 100644 --- a/peppermint.guiconfig.json +++ b/peppermint.guiconfig.json @@ -231,6 +231,7 @@ "email_debug_dontsend": { "type": "checkbox", "description": "If set to true, emails are logged to the standard error instead of being actually sent.", "default": false }, "email_subject_utf8": { "type": "checkbox", "description": "Whether to encode the subject of emails sent to allow them to contain unicode characters. Without this, email subjects will be transliterated to ASCII. If utf-8 email subjects are disabled, page names may not be represented properly.", "default": true }, "email_body_utf8": { "type": "checkbox", "description": "Whether to send emails with utf-8 bodies. If set to false, email bodies will be transliterated to ASCII. If utf-8 email bodies are disabled, page names may not be represented properly.", "default": true }, + "email_user_verify": { "type": "checkbox", "description": "Whether user email addresses must be verified in order to send emails to them.", "default": true }, "updateurl": { "type": "url", "description": "The url from which to fetch updates. Defaults to the master (development) branch. MAKE SURE THAT THIS POINTS TO A *HTTPS* URL, OTHERWISE SOMEONE COULD INJECT A VIRUS INTO YOUR WIKI!", "default": "https://raw.githubusercontent.com/sbrl/pepperminty-wiki/master/index.php" }, "optimize_pages": { "type": "checkbox", "description": "Whether to optimise all webpages generated.", "default": true }, "minify_pageindex": { "type": "checkbox", "description": "Whether to minify the page index when saving it. Improves performance slightly (especially on larger wikis), but can make debugging and quick ninja-edits more awkward. Note that this only takes effect when the page index is next saved.", "default": true },