gocodebox/lifterlms

View on GitHub
includes/forms/class-llms-form-handler.php

Summary

Maintainability
B
6 hrs
Test Coverage
A
96%
<?php
/**
 * Handle LifterLMS Form submissions.
 *
 * @package  LifterLMS/Classes
 *
 * @since 5.0.0
 * @version 7.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_Form_Handler class.
 *
 * @since 5.0.0
 * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.
 */
class LLMS_Form_Handler {

    use LLMS_Trait_Singleton;

    /**
     * Validation class instance.
     *
     * @var LLMS_Form_Validator
     */
    protected $validator = null;

    /**
     * Private Constructor.
     *
     * @since 5.0.0
     *
     * @return void
     */
    private function __construct() {

        $this->validator = new LLMS_Form_Validator();

        add_action( 'lifterlms_before_user_update', array( $this, 'maybe_modify_edit_account_field_settings' ), 10, 3 );
        add_action( 'lifterlms_before_user_update', array( $this, 'maybe_modify_required_address_fields' ), 10, 3 );
        add_action( 'lifterlms_before_user_registration', array( $this, 'maybe_modify_required_address_fields' ), 10, 3 );

    }

    /**
     * Retrieve fields for a given form.
     *
     * Ensures the form exists and that the current user can access the form.
     *
     * @since 5.0.0
     *
     * @param string $action   User action to be performed. Either "update" (for an existing user) or "registration" for a new user.
     * @param string $location Form location ID.
     * @param array  $args     Additional arguments passed to the short-circuit filter.
     * @return WP_Error|array[] Array of LLMS_Form_Field arrays on success or an error object on failure.
     */
    protected function get_fields( $action, $location, $args = array() ) {

        $fields = LLMS_Forms::instance()->get_form_fields( $location, $args );

        // Form couldn't be located.
        if ( false === $fields ) {
            // Translators: %s = form location ID.
            return new WP_Error( 'llms-form-invalid-location', sprintf( __( 'The form location "%s" is invalid.', 'lifterlms' ), $location ), $args );

        } elseif ( 'account' === $location && 'update' !== $action ) {
            // No logged in user, can't update.
            return new WP_Error( 'llms-form-no-user', __( 'You must be logged in to perform this action.', 'lifterlms' ), $args );
        }

        return $fields;

    }

    /**
     * Insert user data into the database.
     *
     * @since 5.0.0
     *
     * @param string  $action      Type of insert action. Either "registration" for a new user or "update" for an existing one.
     * @param array   $posted_data User-submitted form data.
     * @param array[] $fields      List of LifterLMS Form fields for the form.
     * @return WP_Error|int Error on failure or WP_User ID on success.
     */
    protected function insert( $action, $posted_data, $fields ) {

        $func     = 'registration' === $action ? 'wp_insert_user' : 'wp_update_user';
        $prepared = $this->prepare_data_for_insert( $posted_data, $fields, $action );

        $user_id = $func( $prepared['users'] );
        if ( is_wp_error( $user_id ) ) {
            return $user_id;
        }

        foreach ( $prepared['usermeta'] as $key => $val ) {
            update_user_meta( $user_id, $key, $val );
        }

        return $user_id;

    }

    /**
     * Modify LifterLMS Fields prior to performing submit handler validations.
     *
     * @since 5.0.0
     * @since 5.1.0 Do not allow submitting a password change without providing a `password_current`
     *
     * @param array   $posted_data User submitted form data (passed by reference).
     * @param string  $location    Form location ID.
     * @param array[] $fields      Array of LifterLMS Form Fields (passed by reference).
     * @return void
     */
    public function maybe_modify_edit_account_field_settings( &$posted_data, $location, &$fields ) {

        if ( 'account' !== $location ) {
            return;
        }

        /**
         * If email address and passwords aren't submitted we can mark them as "optional" fields.
         *
         * These fields are dynamically toggled and disabled if they're not modified.
         * Process `password_current` as last as it depends on `password` field submission.
         */
        foreach ( array( 'email_address', 'password', 'password_current' ) as $field_id ) {

            // If the field exists and it's not included (or empty) in the posted data.
            $index = LLMS_Forms::instance()->get_field_by( $fields, 'id', $field_id, 'index' );
            if ( false !== $index && empty( $posted_data[ $fields[ $index ]['name'] ] ) ) {

                // When updating a password, the `password_current` is mandatory.
                if ( 'account' === $location && 'password_current' === $field_id ) {
                    // Get `password` field.
                    $password_index = LLMS_Forms::instance()->get_field_by( $fields, 'id', 'password', 'index' );
                    // If a `passowrd` feld has been submitted then the `password_current` cannot be skipped.
                    if ( false !== $password_index &&
                            ! empty( $posted_data[ $fields[ $password_index ]['name'] ] ) ) {
                        continue;
                    }
                }

                // Remove the field so we don't accidentally save an empty value later.
                unset( $posted_data[ $fields[ $index ]['name'] ] );

                // Mark the field as optional (for validation purposes).
                $fields[ $index ]['required'] = false;

                // Check if there's a confirm field and do the same.
                $con_index = LLMS_Forms::instance()->get_field_by( $fields, 'id', "{$field_id}_confirm", 'index' );
                if ( false !== $con_index && empty( $posted_data[ $fields[ $con_index ]['name'] ] ) ) {
                    unset( $posted_data[ $fields[ $con_index ]['name'] ] );
                    $fields[ $con_index ]['required'] = false;
                }
            }
        }

    }

    /**
     * Modify LifterLMS Fields to allow some address fields to be conditionally required.
     *
     * Uses available country locale information to remove the "required" attribute for state
     * and zip code fields when a user has chosen a country that doesn't use states and/or
     * zip codes.
     *
     * @since 5.0.0
     *
     * @param array   $posted_data User submitted form data (passed by reference).
     * @param string  $location    Form location ID.
     * @param array[] $fields      Array of LifterLMS Form Fields (passed by reference).
     * @return void
     */
    public function maybe_modify_required_address_fields( &$posted_data, $location, &$fields ) {

        // Only proceed if we have a country to review.
        if ( empty( $posted_data['llms_billing_country'] ) ) {
            return;
        }

        $country = $posted_data['llms_billing_country'];
        $info    = llms_get_country_address_info( $country );

        // Fields to chek.
        $check = array(
            'llms_billing_city'  => 'city',
            'llms_billing_state' => 'state',
            'llms_billing_zip'   => 'postcode',
        );

        foreach ( $check as $post_key => $info_key ) {

            $index = LLMS_Forms::instance()->get_field_by( $fields, 'name', $post_key, 'index' );

            // Field exists, no data was posted, and the field is disabled (is `false`) in the address info array.
            if ( false !== $index && empty( $posted_data[ $post_key ] ) && ! $info[ $info_key ] ) {
                $fields[ $index ]['required'] = false;
            }
        }

    }

    /**
     * Prepares user-submitted data for insertion into the database.
     *
     * @since 5.0.0
     *
     * @param array   $posted_data Sanitized & validated user-submitted form data.
     * @param array[] $fields      LifterLMS form fields list.
     * @param string  $action      Insert action, either "registration" for new users or "update" for existing, users.
     * @return array
     */
    protected function prepare_data_for_insert( $posted_data, $fields, $action ) {

        $prepared = array();

        foreach ( $fields as $field ) {

            if ( empty( $field['data_store_key'] ) ) {
                continue;
            }

            // We need to account for fields that are part of the form but are not present in the `$posted_data`
            // e.g. unchecked check boxes.
            if ( isset( $posted_data[ $field['name'] ] ) || 'checkbox' === $field['type'] ) {

                if ( ! isset( $prepared[ $field['data_store'] ] ) ) {
                    $prepared[ $field['data_store'] ] = array();
                }

                $prepared[ $field['data_store'] ][ $field['data_store_key'] ] = isset( $posted_data[ $field['name'] ] ) ? $posted_data[ $field['name'] ] : array();
            }
        }

        if ( 'registration' === $action ) {

            $defaults = array(
                'role'                 => 'student',
                'show_admin_bar_front' => false,
            );

            // Add a username if we don't have a user_login field.
            if ( empty( $prepared['users']['user_login'] ) ) {
                $defaults['user_login'] = LLMS_Person_Handler::generate_username( $posted_data['email_address'] );
            }

            // Add a password if we don't have a password field.
            if ( empty( $prepared['users']['user_pass'] ) ) {
                $defaults['user_pass'] = wp_generate_password( 32, true, true );
            }

            $prepared['users'] = wp_parse_args( $prepared['users'], $defaults );

        } elseif ( 'update' === $action ) {

            $prepared['users']['ID'] = empty( $posted_data['user_id'] ) ? get_current_user_id() : absint( $posted_data['user_id'] );

        }

        // Record an IP Address.
        $prepared['usermeta']['llms_ip_address'] = llms_get_ip_address();

        // If terms have been agreed to, record a time stamp for the agreement.
        if ( isset( $posted_data['llms_agress_to_terms'] ) ) {
            $prepared['usermeta']['llms_agress_to_terms'] = current_time( 'mysql' );
        }

        /**
         * Filter data added to the wp_users data via `wp_insert_user()` or `wp_update_user()`.
         *
         * The dynamic portion of this hook, `$action`, can be either "registration" or "update".
         *
         * @since 3.0.0
         * @since 5.0.0 Moved from `LLMS_Person_Handler::insert_data()`.
         *
         * @param array  $user_data   Array of user data.
         * @param array  $posted_data Array of user-submitted data.
         * @param string $action      Submission action, either "registration" or "update".
         */
        $prepared['users'] = apply_filters( "lifterlms_user_{$action}_insert_user", $prepared['users'], $posted_data, $action );

        /**
         * Filter meta data to be added for the user.
         *
         * The dynamic portion of this hook, `$action`, can be either "registration" or "update".
         *
         * @since 3.0.0
         * @since 5.0.0 Moved from `LLMS_Person_Handler::insert_data()`.
         *
         * @param array  $user_meta   Array of user meta data.
         * @param array  $posted_data Array of user-submitted data.
         * @param string $action      Submission action, either "registration" or "update".
         */
        $prepared['usermeta'] = apply_filters( "lifterlms_user_{$action}_insert_user_meta", $prepared['usermeta'], $posted_data, $action );

        return $prepared;

    }

    /**
     * Form submission handler.
     *
     * @since 5.0.0
     * @since 5.1.0 Remove invisible fields from when loading the checkout form.
     * @since 7.0.0 Allow submission validation only (without actually submitting the fields) using the
     *              `validate_only` flag in the `$args` array.
     *
     * @param array  $posted_data User-submitted form data.
     * @param string $location    Form location ID.
     * @param array  $args        Additional arguments passed to the short-circuit filter.
     * @return integer|boolean|WP_Error On success returns the `WP_User` ID.
     *                                  If the `validate_only` argument is passed returns `true` on success.
     *                                  Returns an error object if any validation or processing errors are encountered.
     */
    public function submit( $posted_data, $location, $args = array() ) {

        // Determine the user action to perform.
        $action = get_current_user_id() ? 'update' : 'registration';

        // Load the form, filtering out invisible fields, only for checkout form.
        if ( 'checkout' === $location ) {
            add_filter( 'llms_forms_remove_invisible_field', '__return_true', 999 );
        }
        $fields = $this->get_fields( $action, $location, $args );
        if ( 'checkout' === $location ) {
            remove_filter( 'llms_forms_remove_invisible_field', '__return_true', 999 );
        }

        if ( is_wp_error( $fields ) ) {
            return $this->submit_error( $fields, $posted_data, $action );
        }

        // Make sure the user id cannot be forced by user submission.
        unset( $posted_data['user_id'] );

        if ( ! empty( $args['validate_only'] ) ) {
            return $this->validate_fields( $posted_data, $location, $fields, $action );
        }

        return $this->submit_fields( $posted_data, $location, $fields, $action );

    }

    /**
     * Form fields submission.
     *
     * @since 5.0.0
     * @since 5.1.0 Added "lifterlms_user_{$action}_required_data" filter, to filter the required fields validity of the form submission.
     * @since 5.4.1 Sanitize filed only after validation. See https://github.com/gocodebox/lifterlms/issues/1829.
     * @since 6.0.0 Notify developers of the deprecated `lifterlms_created_person` action hook.
     * @since 7.0.0 Moved validation logic to the `validate_fields()` method.
     *
     * @param array   $posted_data User-submitted form data.
     * @param string  $location    Form location ID.
     * @param array[] $fields      Array of LifterLMS Form Fields.
     * @param string  $action      User action to perform.
     * @return int|WP_Error WP_User ID on success, error object on failure.
     */
    public function submit_fields( $posted_data, $location, $fields, $action ) {

        $validate = $this->validate_fields( $posted_data, $location, $fields, $action );
        if ( is_wp_error( $validate ) ) {
            return $validate;
        }

        // Sanitize.
        $posted_data = $this->validator->sanitize_fields( $posted_data, $fields );

        $user_id = $this->insert( $action, $posted_data, $fields );
        if ( is_wp_error( $user_id ) ) {
            return $this->submit_error( $user_id, $posted_data, $action );
        }

        if ( 'registration' === $action ) {

            /**
             * Deprecated user creation hook.
             *
             * @since Unknown.
             * @deprecated 5.0.0
             *
             * @param int    $user_id     WP_User ID of the newly created user.
             * @param array  $posted_data Array of user-submitted data.
             * @param string $location    Form location.
             */
            do_action_deprecated(
                'lifterlms_created_person',
                array( $user_id, $posted_data, $location ),
                '5.0.0',
                'lifterlms_user_registered'
            );

            /**
             * Fire an action after a user has been registered.
             *
             * @since 3.0.0
             * @since 5.0.0 Moved from `LLMS_Person_Handler::register()`.
             *
             * @param int    $user_id     WP_User ID of the user.
             * @param array  $posted_data Array of user submitted data.
             * @param string $location    Form location.
             */
            do_action( 'lifterlms_user_registered', $user_id, $posted_data, $location );

        } elseif ( 'update' === $action ) {

            /**
             * Fire an action after a user has been updated.
             *
             * @since 3.0.0
             * @since 5.0.0 Moved from `LLMS_Person_Handler::update()`.
             *
             * @param int    $user_id     WP_User ID of the user.
             * @param array  $posted_data Array of user submitted data.
             * @param string $location    Form location.
             */
            do_action( 'lifterlms_user_updated', $user_id, $posted_data, $location );

        }

        return $user_id;

    }

    /**
     * Ensure all errors objects encountered during form submission are filterable.
     *
     * @since 5.0.0
     *
     * @param WP_Error $error       Error object.
     * @param array    $posted_data User-submitted form data.
     * @param string   $action      Form action, either "registration" or "update".
     * @return WP_Error
     */
    protected function submit_error( $error, $posted_data, $action ) {

        /**
         * Filter the error return when the insert/update fails.
         *
         * The dynamic portion of this hook, `$action`, can be either "registration" or "update".
         *
         * @since 3.0.0
         * @since 5.0.0 Moved from `LLMS_Person_Handler::insert_data()`.
         *
         * @param WP_Error $error       Error object.
         * @param array    $posted_data Array of user-submitted data.
         * @param string   $action      Submission action, either "registration" or "update"!
         */
        return apply_filters( "lifterlms_user_{$action}_failure", $error, $posted_data, $action );

    }

    /**
     * Form fields submission validation.
     *
     * @since 7.0.0
     *
     * @param array   $posted_data User-submitted form data.
     * @param string  $location    Form location ID.
     * @param array[] $fields      Array of LifterLMS Form Fields.
     * @param string  $action      User action to perform.
     * @return boolean|WP_Error Returns `true` on success and an error object on failure.
     */
    protected function validate_fields( $posted_data, $location, $fields, $action ) {

        /**
         * Run an action immediately prior to user registration or update.
         *
         * The dynamic portion of this hook, `$action`, can be either "registration" or "update".
         *
         * @since 3.0.0
         * @since 5.0.0 Moved from `LLMS_Person_Handler::update()` & LLMS_Person_Handler::register().
         *              Added parameters `$fields` and `$args`.
         *              Triggered by `do_action_ref_array()` instead of `do_action()` allowing modification
         *              of `$posted_data` and `$fields` via hooks.
         *
         * @param array   $posted_data Array of user-submitted data (passed by reference).
         * @param string  $location    Form location.
         * @param array[] $fields      Array of LifterLMS Form Fields (passed by reference).
         */
        do_action_ref_array( "lifterlms_before_user_{$action}", array( &$posted_data, $location, &$fields ) );

        // Check for all required fields.
        $required = $this->validator->validate_required_fields( $posted_data, $fields );

        /**
         * Filter the required fields validity of the form submission.
         *
         * The dynamic portion of this hook, `$action`, can be either "registration" or "update".
         *
         * @since 5.0.1
         *
         * @param WP_Error|true $valid       Error object containing required validation errors or true when the data is valid.
         * @param array         $posted_data Array of user-submitted data.
         * @param string        $location    Form location.
         */
        $required = apply_filters( "lifterlms_user_{$action}_required_data", $required, $posted_data, $location );

        if ( is_wp_error( $required ) ) {
            return $this->submit_error( $required, $posted_data, $action );
        }

        $posted_data = wp_unslash( $posted_data );

        $valid = $this->validator->validate_fields( $posted_data, $fields );
        if ( is_wp_error( $valid ) ) {
            return $this->submit_error( $valid, $posted_data, $action );
        }

        // Validate matching fields.
        $matches = $this->validator->validate_matching_fields( $posted_data, $fields );
        if ( is_wp_error( $matches ) ) {
            return $this->submit_error( $matches, $posted_data, $action );
        }

        /**
         * Filter the validity of the form submission.
         *
         * The dynamic portion of this hook, `$action`, can be either "registration" or "update".
         *
         * @since 3.0.0
         * @since 5.0.0 Unknown.
         *
         * @param WP_Error|true $valid       Error object containing validation errors or true when the data is valid.
         * @param array         $posted_data Array of user-submitted data.
         * @param string        $location    Form location.
         */
        $valid = apply_filters( "lifterlms_user_{$action}_data", true, $posted_data, $location );
        if ( is_wp_error( $valid ) ) {
            return $this->submit_error( $valid, $posted_data, $action );
        }

        /**
         * Run an action immediately after user registration/update fields have been validated.
         *
         * The dynamic portion of this hook, `$action`, can be either "registration" or "update".
         *
         * @since 3.0.0
         * @since 5.0.0 Moved from `LLMS_Person_Handler::update()` & LLMS_Person_Handler::register().
         *              Added parameters `$fields` and `$args`.
         *
         * @param array   $posted_data Array of user-submitted data.
         * @param string  $location    Form location.
         * @param array[] $fields      Array of LifterLMS Form Fields.
         */
        do_action( "lifterlms_user_{$action}_after_validation", $posted_data, $location, $fields );

        return true;

    }

}