mirror of
https://github.com/WordPress/five-for-the-future.git
synced 2025-07-06 18:55:44 +03:00
Email: Send pledge confirmation with authentication token.
Fixes #34. Fixes #10.
This commit is contained in:
parent
6fe5a92c7b
commit
147da5ae24
7 changed files with 476 additions and 6 deletions
121
plugins/wporg-5ftf/includes/email.php
Normal file
121
plugins/wporg-5ftf/includes/email.php
Normal file
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Helper functions for sending emails, including authentication tokens.
|
||||
*
|
||||
* We don't want pledges connected to individual w.org accounts, because that would encourage people to create
|
||||
* "company" accounts instead of having their contributions show up as real human beings; or because that
|
||||
* individual will likely eventually leave the company, and "ownership" of the pledge would be orphaned; or
|
||||
* because we'd have to tie multiple accounts to the pledge (and all the extra time/UX costs associated with that),
|
||||
* and would still have problems with orphaned ownership, etc.
|
||||
*
|
||||
* So instead, we just ask companies to create pledges using a group email (e.g., support@wordcamp.org), and
|
||||
* we email them time-restricted, once-time-use auth tokens when they want to "log in".
|
||||
*
|
||||
* WP "nonces" aren't ideal for this purpose from a security perspective, because they're less secure. They're
|
||||
* reusable, last up to 24 hours, and have a much smaller search space in brute force attacks. They also create an
|
||||
* inconsistent UX, because a token could be valid for 24 hours, or for 1 second, due to how `wp_nonce_tick()`
|
||||
* works. That would lead to some situations where a nonce had already expired by the time the contributor opened
|
||||
* the email and clicked on the link.
|
||||
*
|
||||
* So instead, true NONCEs are implemented; see `is_valid_authentication_token()` for details.
|
||||
*/
|
||||
|
||||
namespace WordPressDotOrg\FiveForTheFuture\Email;
|
||||
|
||||
defined( 'WPINC' ) || die();
|
||||
|
||||
const TOKEN_PREFIX = '5ftf_auth_token_';
|
||||
|
||||
/**
|
||||
* Wrap `wp_mail()` with shared functionality.
|
||||
*
|
||||
* @param string $to
|
||||
* @param string $subject
|
||||
* @param string $message
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function send_email( $to, $subject, $message ) {
|
||||
$headers = array(
|
||||
'From: WordPress - Five for the Future <donotreply@wordpress.org>',
|
||||
'Reply-To: support@wordcamp.org',
|
||||
// todo update address when new one is created
|
||||
);
|
||||
|
||||
return wp_mail( $to, $subject, $message, $headers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an action URL with a unique authentication token.
|
||||
*
|
||||
* @param int $pledge_id
|
||||
* @param string $action
|
||||
* @param int $action_page_id The ID of the page that the user will be taken back to, in order to process their
|
||||
* verification request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function get_authentication_url( $pledge_id, $action, $action_page_id ) {
|
||||
$auth_token = array(
|
||||
'value' => wp_generate_password( 20, false ), // Similar to `get_password_reset_key()`.
|
||||
// todo should encrypt at rest? core doesn't but others do
|
||||
'expiration' => time() + ( 2 * HOUR_IN_SECONDS ),
|
||||
);
|
||||
|
||||
/*
|
||||
* Tying the token to a specific pledge is important for security, otherwise companies could get a valid token
|
||||
* for their pledge, and use it to edit other company's pledges.
|
||||
*
|
||||
* Similarly, tying it to specific actions is also important, to protect against CSRF attacks.
|
||||
*
|
||||
* This function intentionally requires the caller to pass in a pledge ID and action, so that it can guarantee
|
||||
* that each token will be unique across pledges and actions.
|
||||
*/
|
||||
update_post_meta( $pledge_id, TOKEN_PREFIX . $action, $auth_token );
|
||||
|
||||
$auth_url = add_query_arg(
|
||||
array(
|
||||
'action' => $action,
|
||||
'pledge_id' => $pledge_id,
|
||||
'auth_token' => $auth_token['value'],
|
||||
),
|
||||
get_permalink( $action_page_id )
|
||||
);
|
||||
|
||||
// todo include a "this lnk will expire in 10 hours and after its used once" message too?
|
||||
// probably, but what's the best way to do that DRYly?
|
||||
|
||||
return $auth_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify whether or not a given authentication token is valid.
|
||||
*
|
||||
* These tokens are more secure than WordPress' imitation nonces, because they can only be used once, and expire
|
||||
* in a shorter timeframe. Like WP nonces, though, they must be tied to a specific action and post object in order
|
||||
* to prevent misuse.
|
||||
*
|
||||
* @param $pledge_id
|
||||
* @param $action
|
||||
* @param $unverified_token
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function is_valid_authentication_token( $pledge_id, $action, $unverified_token ) {
|
||||
$verified = false;
|
||||
$valid_token = get_post_meta( $pledge_id, TOKEN_PREFIX . $action, true );
|
||||
|
||||
if ( $valid_token && $valid_token['expiration'] > time() && $unverified_token === $valid_token['value'] ) {
|
||||
$verified = true;
|
||||
|
||||
// Tokens should not be reusable, to increase security.
|
||||
delete_post_meta( $pledge_id, TOKEN_PREFIX . $action );
|
||||
// todo when used to manage pledge, token will probably get deleted when viewing, and then they won't be able to save
|
||||
// fix that when create the manage process, though. for now this works for confirming email address.
|
||||
// maye pass a `context` param to this function, either 'view' or 'update', and only delete if context is 'update' ?
|
||||
// make sure view and update functions checks to make sure have valid token, not create though
|
||||
}
|
||||
|
||||
return $verified;
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
namespace WordPressDotOrg\FiveForTheFuture\PledgeForm;
|
||||
|
||||
use WordPressDotOrg\FiveForTheFuture;
|
||||
use WordPressDotOrg\FiveForTheFuture\{ Pledge, PledgeMeta, Contributor };
|
||||
use WordPressDotOrg\FiveForTheFuture\{ Pledge, PledgeMeta, Contributor, Email };
|
||||
use WP_Error, WP_User;
|
||||
|
||||
defined( 'WPINC' ) || die();
|
||||
|
@ -21,11 +21,12 @@ add_shortcode( '5ftf_pledge_form_manage', __NAMESPACE__ . '\render_form_manage'
|
|||
* @return false|string
|
||||
*/
|
||||
function render_form_new() {
|
||||
$action = filter_input( INPUT_POST, 'action' );
|
||||
$action = isset( $_GET['action'] ) ? filter_input( INPUT_GET, 'action' ) : filter_input( INPUT_POST, 'action' );
|
||||
$data = get_form_submission();
|
||||
$messages = [];
|
||||
$complete = false;
|
||||
$directory_url = get_permalink( get_page_by_path( 'pledges' ) );
|
||||
$view = 'form-pledge-new.php';
|
||||
|
||||
if ( 'Submit Pledge' === $action ) {
|
||||
$processed = process_form_new();
|
||||
|
@ -35,11 +36,16 @@ function render_form_new() {
|
|||
} elseif ( 'success' === $processed ) {
|
||||
$complete = true;
|
||||
}
|
||||
} else if ( 'confirm_pledge_email' === $action ) {
|
||||
$view = 'form-pledge-confirm-email.php';
|
||||
$pledge_id = filter_input( INPUT_GET, 'pledge_id', FILTER_VALIDATE_INT );
|
||||
$unverified_token = filter_input( INPUT_GET, 'auth_token', FILTER_SANITIZE_STRING );
|
||||
$email_confirmed = process_email_confirmation( $pledge_id, $action, $unverified_token );
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$readonly = false;
|
||||
require FiveForTheFuture\PATH . 'views/form-pledge-new.php';
|
||||
require FiveForTheFuture\get_views_path() . $view;
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
@ -111,6 +117,40 @@ function process_form_new() {
|
|||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a request to confirm a company's email address.
|
||||
*
|
||||
* @param int $pledge_id
|
||||
* @param string $action
|
||||
* @param array $unverified_token
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function process_email_confirmation( $pledge_id, $action, $unverified_token ) {
|
||||
$meta_key = PledgeMeta\META_PREFIX . 'pledge-email-confirmed';
|
||||
$already_confirmed = get_post( $pledge_id )->$meta_key;
|
||||
|
||||
if ( $already_confirmed ) {
|
||||
/*
|
||||
* If they refresh the page after confirming, they'd otherwise get an error because the token had been
|
||||
* used, and might be confused and think that the address wasn't confirmed.
|
||||
*
|
||||
* This leaks the fact that the address is confirmed, because it will return true even if the token is
|
||||
* invalid, but there aren't any security/privacy implications of that.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
$email_confirmed = Email\is_valid_authentication_token( $pledge_id, $action, $unverified_token );
|
||||
|
||||
if ( $email_confirmed ) {
|
||||
update_post_meta( $pledge_id, $meta_key, true );
|
||||
wp_update_post( array( 'ID' => $pledge_id, 'post_status' => 'publish' ) );
|
||||
}
|
||||
|
||||
return $email_confirmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the form(s) for managing existing pledges.
|
||||
*
|
||||
|
@ -179,6 +219,12 @@ function process_form_manage() {
|
|||
__( 'A pledge already exists for this domain.', 'wporg' )
|
||||
);
|
||||
}
|
||||
|
||||
// todo email any new contributors for confirmation
|
||||
// notify any removed contributors?
|
||||
// ask them to update their profiles?
|
||||
// automatically update contributor profiles?
|
||||
// anything else?
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -184,10 +184,19 @@ function render_meta_boxes( $pledge, $box ) {
|
|||
* @param WP_Post $pledge
|
||||
*/
|
||||
function save_pledge( $pledge_id, $pledge ) {
|
||||
$action = filter_input( INPUT_GET, 'action' );
|
||||
$get_action = filter_input( INPUT_GET, 'action' );
|
||||
$post_action = $_POST['action'] ?? null;
|
||||
$ignored_actions = array( 'trash', 'untrash', 'restore' );
|
||||
|
||||
if ( $action && in_array( $action, $ignored_actions, true ) ) {
|
||||
/*
|
||||
* This is only intended to run when the front end form and wp-admin forms are submitted, not when posts are
|
||||
* programmatically updated.
|
||||
*/
|
||||
if ( 'Submit Pledge' !== $post_action && 'editpost' !== $get_action ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $get_action && in_array( $get_action, $ignored_actions, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
namespace WordPressDotOrg\FiveForTheFuture\Pledge;
|
||||
use WordPressDotOrg\FiveForTheFuture\Email;
|
||||
|
||||
use WordPressDotOrg\FiveForTheFuture;
|
||||
use WP_Error;
|
||||
|
@ -127,5 +128,48 @@ function create_new_pledge( $name ) {
|
|||
'post_status' => 'draft',
|
||||
);
|
||||
|
||||
return wp_insert_post( $args, true );
|
||||
|
||||
$pledge_id = wp_insert_post( $args, true );
|
||||
// The pledge's meta data is saved at this point via `save_pledge_meta()`, which is a `save_post` callback.
|
||||
|
||||
if ( ! is_wp_error( $pledge_id ) ) {
|
||||
send_pledge_verification_email( $pledge_id, get_post()->ID );
|
||||
send_contributor_verification_email();
|
||||
}
|
||||
|
||||
return $pledge_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email pledge manager to confirm their email address.
|
||||
*
|
||||
* @param int $pledge_id The ID of the pledge.
|
||||
* @param int $action_page_id The ID of the page that the user will be taken back to, in order to process their
|
||||
* verification request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function send_pledge_verification_email( $pledge_id, $action_page_id ) {
|
||||
$pledge = get_post( $pledge_id );
|
||||
|
||||
$message =
|
||||
'Thanks for committing to help keep WordPress sustainable! Please confirm this email address ' .
|
||||
'so that we can accept your pledge:' . "\n\n" .
|
||||
Email\get_authentication_url( $pledge_id, 'confirm_pledge_email', $action_page_id )
|
||||
;
|
||||
|
||||
// todo include a notice that the link will expire in X hours, so they know what to expect
|
||||
// need to make that value DRY across all emails with links
|
||||
// should probably say that on the front end form success message as well, so they know to go check their email now instead of after lunch.
|
||||
|
||||
return Email\send_email(
|
||||
$pledge->{'5ftf_org-pledge-email'},
|
||||
'Please confirm your email address',
|
||||
$message
|
||||
);
|
||||
}
|
||||
|
||||
// todo
|
||||
function send_contributor_verification_email() {
|
||||
// todo
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue