From 9c08692d5d2abe7e7198d940a11bd12001568c52 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Fri, 18 Oct 2019 16:56:21 -0700 Subject: [PATCH] New Pledge: Validate submitted form values and generate a draft pledge post (#35) Ensures that a submission to the new pledge form has: * A unique email address compared to existing pledges * A unique domain in the URL, compared to existing pledges * Has at least one valid contributor listed Error messages when one or more of these conditions isn't met are descriptive so that the submitter can correct the issue. Fixes #15 --- plugins/wporg-5ftf/includes/pledge-form.php | 191 ++++++++++++++++-- plugins/wporg-5ftf/includes/pledge-meta.php | 57 +++--- .../views/inputs-pledge-contributors.php | 2 +- .../views/inputs-pledge-new-misc.php | 7 +- 4 files changed, 202 insertions(+), 55 deletions(-) diff --git a/plugins/wporg-5ftf/includes/pledge-form.php b/plugins/wporg-5ftf/includes/pledge-form.php index 0c69a17..263bf04 100755 --- a/plugins/wporg-5ftf/includes/pledge-form.php +++ b/plugins/wporg-5ftf/includes/pledge-form.php @@ -8,7 +8,7 @@ namespace WordPressDotOrg\FiveForTheFuture\PledgeForm; use WordPressDotOrg\FiveForTheFuture; use WordPressDotOrg\FiveForTheFuture\Pledge; use WordPressDotOrg\FiveForTheFuture\PledgeMeta; -use WP_Error; +use WP_Error, WP_Post, WP_User; defined( 'WPINC' ) || die(); @@ -23,9 +23,9 @@ add_shortcode( '5ftf_pledge_form_manage', __NAMESPACE__ . '\render_form_manage' */ function render_form_new() { $action = filter_input( INPUT_POST, 'action' ); + $data = get_form_submission(); $messages = []; $complete = false; - $data = PledgeMeta\get_pledge_meta(); if ( 'Submit Pledge' === $action ) { $processed = process_form_new(); @@ -45,12 +45,12 @@ function render_form_new() { } /** - * + * Process a submission from the New Pledge form. * * @return string|WP_Error String "success" if the form processed correctly. Otherwise WP_Error. */ function process_form_new() { - $submission = filter_input_array( INPUT_POST, PledgeMeta\get_input_filters() ); + $submission = get_form_submission(); $has_required = PledgeMeta\has_required_pledge_meta( $submission ); @@ -58,15 +58,35 @@ function process_form_new() { return $has_required; } + $email = sanitize_meta( + PledgeMeta\META_PREFIX . 'org-pledge-email', + $submission['org-pledge-email'], + 'post', + Pledge\CPT_ID + ); + + if ( has_existing_pledge( $email, 'email' ) ) { + return new WP_Error( + 'existing_pledge_email', + __( 'This email address is already connected to an existing pledge.', 'wporg' ) + ); + } + $domain = PledgeMeta\get_normalized_domain_from_url( $submission['org-url'] ); - if ( has_existing_pledge( $domain ) ) { + if ( has_existing_pledge( $domain, 'domain' ) ) { return new WP_Error( - 'existing_pledge', + 'existing_pledge_domain', __( 'A pledge already exists for this domain.', 'wporg' ) ); } + $contributors = parse_contributors( $submission['pledge-contributors'] ); + + if ( is_wp_error( $contributors ) ) { + return $contributors; + } + $name = sanitize_meta( PledgeMeta\META_PREFIX . 'org-name', $submission['org-name'], @@ -80,8 +100,6 @@ function process_form_new() { return $created; } - PledgeMeta\save_pledge_meta( $created, $submission ); - return 'success'; } @@ -116,12 +134,12 @@ function render_form_manage() { } /** - * + * Process a submission from the Manage Existing Pledge form. * * @return string|WP_Error String "success" if the form processed correctly. Otherwise WP_Error. */ function process_form_manage() { - $submission = filter_input_array( INPUT_POST, PledgeMeta\get_input_filters() ); + $submission = get_form_submission(); $has_required = PledgeMeta\has_required_pledge_meta( $submission ); @@ -129,9 +147,23 @@ function process_form_manage() { return $has_required; } + $email = sanitize_meta( + PledgeMeta\META_PREFIX . 'org-pledge-email', + $submission['org-pledge-email'], + 'post', + Pledge\CPT_ID + ); + + if ( has_existing_pledge( $email, 'email' ) ) { + return new WP_Error( + 'existing_pledge_email', + __( 'This email address is already connected to an existing pledge.', 'wporg' ) + ); + } + $domain = PledgeMeta\get_normalized_domain_from_url( $submission['org-url'] ); - if ( has_existing_pledge( $domain ) ) { + if ( has_existing_pledge( $domain, 'domain' ) ) { return new WP_Error( 'existing_pledge', __( 'A pledge already exists for this domain.', 'wporg' ) @@ -140,25 +172,58 @@ function process_form_manage() { } /** + * Get and sanitize $_POST values from a form submission. * + * @return array|bool + */ +function get_form_submission() { + $input_filters = array_merge( + // Inputs that correspond to meta values. + wp_list_pluck( PledgeMeta\get_pledge_meta_config( 'user_input' ), 'php_filter' ), + // Inputs with no corresponding meta value. + array( + 'pledge-contributors' => FILTER_SANITIZE_STRING, + 'pledge-agreement' => FILTER_VALIDATE_BOOLEAN, + ) + ); + + return filter_input_array( INPUT_POST, $input_filters ); +} + +/** + * Check a key value against existing pledges to see if one already exists. * - * @param string $domain - * @param int $current_pledge_id + * @param string $key The value to match against other pledges. + * @param string $key_type The type of value being matched. `email` or `domain`. + * @param int $current_pledge_id Optional. The post ID of the pledge to compare against others. * * @return bool */ -function has_existing_pledge( $domain, int $current_pledge_id = 0 ) { +function has_existing_pledge( $key, $key_type, int $current_pledge_id = 0 ) { $args = array( 'post_type' => Pledge\CPT_ID, - 'post_status' => array( 'pending', 'publish' ), - 'meta_query' => array( - array( - 'key' => PledgeMeta\META_PREFIX . 'org-domain', - 'value' => $domain, - ), - ), + 'post_status' => array( 'draft', 'pending', 'publish' ), ); + switch ( $key_type ) { + case 'email': + $args['meta_query'] = array( + array( + 'key' => PledgeMeta\META_PREFIX . 'org-pledge-email', + 'value' => $key, + ), + ); + break; + case 'domain': + $args['meta_query'] = array( + array( + 'key' => PledgeMeta\META_PREFIX . 'org-domain', + 'value' => $key, + ), + ); + break; + } + if ( $current_pledge_id ) { $args['exclude'] = array( $current_pledge_id ); } @@ -168,6 +233,90 @@ function has_existing_pledge( $domain, int $current_pledge_id = 0 ) { return ! empty( $matching_pledge ); } +/** + * TODO Move this to the contributor cpt include file. + * + * @param int $pledge_id + * + * @return array + */ +function get_pledge_contributors( $pledge_id = 0 ) { + $contributors = array(); + + // Get POST'd submission, if it exists. + $submission = filter_input( INPUT_POST, 'pledge-contributors', FILTER_SANITIZE_STRING ); + + // Get existing pledge, if it exists. + $pledge = get_post( $pledge_id ); + + if ( ! empty( $submission ) ) { + $contributors = array_map( 'sanitize_user', explode( ',', $submission ) ); + } elseif ( $pledge instanceof WP_Post ) { + // TODO the Contributor post type is being introduced in a separate PR. These details may change. + + $contributor_posts = get_posts( array( + 'post_type' => '', + 'post_status' => array( 'pending', 'publish' ), + 'post_parent' => $pledge_id, + 'numberposts' => -1, + ) ); + + $contributors = wp_list_pluck( $contributor_posts, 'post_title' ); + } + + return $contributors; +} + +/** + * Ensure each item in a list of usernames is valid and corresponds to a user. + * + * @param string $contributors A comma-separated list of username strings. + * + * @return array|WP_Error An array of sanitized wporg usernames on success. Otherwise WP_Error. + */ +function parse_contributors( $contributors ) { + $invalid_contributors = array(); + $sanitized_contributors = array(); + + $contributors = explode( ',', $contributors ); + + foreach ( $contributors as $wporg_username ) { + $sanitized_username = sanitize_user( $wporg_username ); + $user = get_user_by( 'login', $sanitized_username ); + + if ( $user instanceof WP_User ) { + $sanitized_contributors[] = $sanitized_username; + } else { + $invalid_contributors[] = $wporg_username; + } + } + + if ( ! empty( $invalid_contributors ) ) { + /* translators: Used between sponsor names in a list, there is a space after the comma. */ + $item_separator = _x( ', ', 'list item separator', 'wporg' ); + + return new WP_Error( + 'invalid_contributor', + sprintf( + /* translators: %s is a list of usernames. */ + __( 'The following contributor usernames are not valid: %s', 'wporg' ), + implode( $item_separator, $invalid_contributors ) + ) + ); + } + + if ( empty( $sanitized_contributors ) ) { + return new WP_Error( + 'contributor_required', + __( 'The pledge must have at least one contributor username.', 'wporg' ) + ); + } + + $sanitized_contributors = array_unique( $sanitized_contributors ); + + return $sanitized_contributors; +} + /** * * diff --git a/plugins/wporg-5ftf/includes/pledge-meta.php b/plugins/wporg-5ftf/includes/pledge-meta.php index 1c29d22..a0c6194 100755 --- a/plugins/wporg-5ftf/includes/pledge-meta.php +++ b/plugins/wporg-5ftf/includes/pledge-meta.php @@ -7,17 +7,21 @@ namespace WordPressDotOrg\FiveForTheFuture\PledgeMeta; use WordPressDotOrg\FiveForTheFuture; use WordPressDotOrg\FiveForTheFuture\Pledge; +use WordPressDotOrg\FiveForTheFuture\PledgeForm; use WP_Post, WP_Error; defined( 'WPINC' ) || die(); const META_PREFIX = FiveForTheFuture\PREFIX . '_'; -add_action( 'init', __NAMESPACE__ . '\register_pledge_meta' ); -add_action( 'admin_init', __NAMESPACE__ . '\add_meta_boxes' ); -add_action( 'save_post', __NAMESPACE__ . '\save_pledge', 10, 2 ); -add_action( 'updated_' . Pledge\CPT_ID . '_meta', __NAMESPACE__ . '\update_generated_meta', 10, 4 ); -add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' ); +add_action( 'init', __NAMESPACE__ . '\register_pledge_meta' ); +add_action( 'admin_init', __NAMESPACE__ . '\add_meta_boxes' ); +add_action( 'save_post', __NAMESPACE__ . '\save_pledge', 10, 2 ); +add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' ); + +// Both hooks must be used because `updated` doesn't fire if the post meta didn't previously exist. +add_action( 'updated_postmeta', __NAMESPACE__ . '\update_generated_meta', 10, 4 ); +add_action( 'added_post_meta', __NAMESPACE__ . '\update_generated_meta', 10, 4 ); /** * Define pledge meta fields and their properties. @@ -196,8 +200,7 @@ function save_pledge( $pledge_id, $pledge ) { return; } - $definitions = wp_list_pluck( get_pledge_meta_config( 'user_input' ), 'php_filter' ); - $submitted_meta = filter_input_array( INPUT_POST, $definitions ); + $submitted_meta = PledgeForm\get_form_submission(); if ( is_wp_error( has_required_pledge_meta( $submitted_meta ) ) ) { return; @@ -242,7 +245,21 @@ function save_pledge_meta( $pledge_id, $new_values ) { * @return void */ function update_generated_meta( $meta_id, $object_id, $meta_key, $_meta_value ) { + $post_type = get_post_type( $object_id ); + + if ( Pledge\CPT_ID !== $post_type ) { + return; + } + switch ( $meta_key ) { + case META_PREFIX . 'org-name': + if ( 'updated_postmeta' === current_action() ) { + wp_update_post( array( + 'post_title' => $_meta_value, + ) ); + } + break; + case META_PREFIX . 'org-url': $domain = get_normalized_domain_from_url( $_meta_value ); update_post_meta( $object_id, META_PREFIX . 'org-domain', $domain ); @@ -291,26 +308,6 @@ function has_required_pledge_meta( array $submission ) { return true; } -/** - * Get the input filters for submitted content. - * - * @return array - */ -function get_input_filters() { - return array_merge( - // Inputs that correspond to meta values. - wp_list_pluck( get_pledge_meta_config( 'user_input' ), 'php_filter' ), - // Inputs with no corresponding meta value. - array( - 'contributor-wporg-usernames' => [ - 'filter' => FILTER_SANITIZE_STRING, - 'flags' => FILTER_REQUIRE_ARRAY, - ], - 'pledge-agreement' => FILTER_VALIDATE_BOOLEAN, - ) - ); -} - /** * Get the metadata for a given pledge, or a default set if no pledge is provided. * @@ -326,13 +323,13 @@ function get_pledge_meta( $pledge_id = 0, $context = '' ) { $meta = array(); // Get POST'd submission, if it exists. - $submission = filter_input_array( INPUT_POST, get_input_filters() ); + $submission = PledgeForm\get_form_submission(); foreach ( $keys as $key => $config ) { if ( isset( $submission[ $key ] ) ) { $meta[ $key ] = $submission[ $key ]; - } else if ( $pledge instanceof WP_Post ) { - $meta_key = META_PREFIX . $key; + } elseif ( $pledge instanceof WP_Post ) { + $meta_key = META_PREFIX . $key; $meta[ $key ] = get_post_meta( $pledge->ID, $meta_key, true ); } else { $meta[ $key ] = $config['default'] ?: ''; diff --git a/plugins/wporg-5ftf/views/inputs-pledge-contributors.php b/plugins/wporg-5ftf/views/inputs-pledge-contributors.php index bc3fee3..96a13e2 100644 --- a/plugins/wporg-5ftf/views/inputs-pledge-contributors.php +++ b/plugins/wporg-5ftf/views/inputs-pledge-contributors.php @@ -16,7 +16,7 @@ namespace WordPressDotOrg\FiveForTheFuture\View; type="text" id="5ftf-pledge-contributors" name="pledge-contributors" - value="" + value="" required aria-describedby="5ftf-pledge-contributors-help" /> diff --git a/plugins/wporg-5ftf/views/inputs-pledge-new-misc.php b/plugins/wporg-5ftf/views/inputs-pledge-new-misc.php index 2954f53..3b74b78 100644 --- a/plugins/wporg-5ftf/views/inputs-pledge-new-misc.php +++ b/plugins/wporg-5ftf/views/inputs-pledge-new-misc.php @@ -12,11 +12,12 @@ namespace WordPressDotOrg\FiveForTheFuture\View; /> -