gocodebox/lifterlms

View on GitHub
includes/class.llms.person.handler.php

Summary

Maintainability
B
6 hrs
Test Coverage
A
97%
<?php
/**
 * User Handling for login and registration (mostly)
 *
 * @package LifterLMS/Classes
 *
 * @since 3.0.0
 * @version 6.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_Person_Handler class.
 *
 * @since 3.0.0
 * @since 3.35.0 Sanitize field data when filling field with user-submitted data.
 * @since 5.0.0 Private methods `LLMS_Person_Handler::fill_fields()` and `LLMS_Person_Handler::insert_data()` were removed.
 * @since 6.0.0 Removed deprecated items.
 *              - `LLMS_Person_Handler::register()` method
 *              - `LLMS_Person_Handler::sanitize_field() method`
 *              - `LLMS_Person_Handler::update()` method
 *              - `LLMS_Person_Handler::validate_fields()` method
 *              - `LLMS_Person_Handler::voucher_toggle_script()` method
 */
class LLMS_Person_Handler {

    /**
     * Prefix for all user meta field keys
     *
     * @var string
     */
    private static $meta_prefix = 'llms_';

    /**
     * Prevents the hacky voucher script from being output multiple times
     *
     * @var boolean
     */
    private static $voucher_script_output = false;

    /**
     * Locate password fields from a given form location.
     *
     * @since 5.0.0
     *
     * @param string $location From location.
     * @return false|array[]
     */
    protected static function find_password_fields( $location ) {

        $forms = LLMS_Forms::instance();
        $all   = $forms->get_form_fields( $location );

        $pwd = $forms->get_field_by( (array) $all, 'id', 'password' );

        // If we don't have a password in the form return early.
        if ( ! $pwd ) {
            return false;
        }

        // Setup the return array.
        $fields = array( $pwd );

        // Add confirmation and strength meter if they exist.
        foreach ( array( 'password_confirm', 'llms-password-strength-meter' ) as $id ) {

            $field = $forms->get_field_by( $all, 'id', $id );
            if ( $field ) {

                // If we have a confirmation field ensure that the fields sit side by side.
                if ( 'password_confirm' === $id ) {

                    $fields[0]['columns']         = 6;
                    $fields[0]['last_column']     = false;
                    $fields[0]['wrapper_classes'] = array();

                    $field['columns']         = 6;
                    $field['last_column']     = true;
                    $field['wrapper_classes'] = array();

                }

                $fields[] = $field;
            }
        }

        return $fields;

    }

    /**
     * Generate a unique login based on the user's email address
     *
     * @since 3.0.0
     * @since 3.19.4 Unknown.
     *
     * @param string $email User's email address.
     * @return string
     */
    public static function generate_username( $email ) {

        /**
         * Allow custom username generation
         *
         * @since 3.0.0
         *
         * @param string $custom_username The custom-generated username. If the filter returns a truthy string it will be used in favor
         *                                of the automatically generated username.
         * @param string $email           User's email address.
         */
        $custom_username = apply_filters( 'lifterlms_generate_username', null, $email );
        if ( $custom_username && is_string( $custom_username ) ) {
            return $custom_username;
        }

        $username      = sanitize_user( current( explode( '@', $email ) ), true );
        $orig_username = $username;
        $i             = 1;
        while ( username_exists( $username ) ) {

            $username = $orig_username . $i;
            $i++;

        }

        /**
         * Modify an auto-generated username before it is used
         *
         * @since 3.0.0
         *
         * @param string $username The generated user name.
         * @param string $email    User's email address which was used to generate the username.
         */
        return apply_filters( 'lifterlms_generated_username', $username, $email );

    }

    /**
     * Get the fields for the login form
     *
     * @since 3.0.0
     * @since 3.0.4 Unknown.
     * @since 5.0.0 Remove usage of the deprecated `lifterlms_registration_generate_username`.
     *
     * @param string $layout Form layout. Accepts "columns" (default) or "stacked".
     * @return array[] An array of form field arrays.
     */
    public static function get_login_fields( $layout = 'columns' ) {

        $usernames = LLMS_Forms::instance()->are_usernames_enabled();

        /**
         * Customize the fields used to build the user login form
         *
         * @since 3.0.0
         * @param array[] $fields An array of form field arrays.
         */
        return apply_filters(
            'lifterlms_person_login_fields',
            array(
                array(
                    'columns'     => ( 'columns' == $layout ) ? 6 : 12,
                    'id'          => 'llms_login',
                    'label'       => ! $usernames ? __( 'Email Address', 'lifterlms' ) : __( 'Username or Email Address', 'lifterlms' ),
                    'last_column' => ( 'columns' == $layout ) ? false : true,
                    'required'    => true,
                    'type'        => ! $usernames ? 'email' : 'text',
                ),
                array(
                    'columns'     => ( 'columns' == $layout ) ? 6 : 12,
                    'id'          => 'llms_password',
                    'label'       => __( 'Password', 'lifterlms' ),
                    'last_column' => ( 'columns' == $layout ) ? true : true,
                    'required'    => true,
                    'type'        => 'password',
                ),
                array(
                    'columns'     => ( 'columns' == $layout ) ? 3 : 12,
                    'classes'     => 'llms-button-action',
                    'id'          => 'llms_login_button',
                    'value'       => __( 'Login', 'lifterlms' ),
                    'last_column' => ( 'columns' == $layout ) ? false : true,
                    'required'    => false,
                    'type'        => 'submit',
                ),
                array(
                    'columns'     => ( 'columns' == $layout ) ? 6 : 6,
                    'id'          => 'llms_remember',
                    'label'       => __( 'Remember me', 'lifterlms' ),
                    'last_column' => false,
                    'required'    => false,
                    'type'        => 'checkbox',
                ),
                array(
                    'columns'         => ( 'columns' == $layout ) ? 3 : 6,
                    'id'              => 'llms_lost_password',
                    'last_column'     => true,
                    'description'     => '<a href="' . esc_url( llms_lostpassword_url() ) . '">' . __( 'Lost your password?', 'lifterlms' ) . '</a>',
                    'type'            => 'html',
                    'wrapper_classes' => 'align-right',
                ),
            )
        );

    }

    /**
     * Retrieve fields for password recovery
     *
     * Used to generate the form where a username/email is entered to start the password reset process.
     *
     * @since 3.8.0
     * @since 5.0.0 Use LLMS_Forms::are_usernames_enabled() in favor of deprecated option "lifterlms_registration_generate_username".
     *               Remove field values set to the default value for a form field.
     *
     * @return array[] An array of form field arrays.
     */
    public static function get_lost_password_fields() {

        $usernames = LLMS_Forms::instance()->are_usernames_enabled();

        if ( ! $usernames ) {
            $message = __( 'Lost your password? Enter your email address and we will send you a link to reset it.', 'lifterlms' );
        } else {
            $message = __( 'Lost your password? Enter your username or email address and we will send you a link to reset it.', 'lifterlms' );
        }

        /**
         * Filter the message displayed on the lost password form.
         *
         * @since Unknown.
         *
         * @param string $message The message displayed before the form.
         */
        $message = apply_filters( 'lifterlms_lost_password_message', $message );

        /**
         * Filter the form fields displayed for the lost password form.
         *
         * @since 3.8.0
         *
         * @param array[] $fields An array of form field arrays.
         */
        return apply_filters(
            'lifterlms_lost_password_fields',
            array(
                array(
                    'id'    => 'llms_lost_password_message',
                    'type'  => 'html',
                    'value' => $message,
                ),
                array(
                    'id'       => 'llms_login',
                    'label'    => ! $usernames ? __( 'Email Address', 'lifterlms' ) : __( 'Username or Email Address', 'lifterlms' ),
                    'required' => true,
                    'type'     => ! $usernames ? 'email' : 'text',
                ),
                array(
                    'classes' => 'llms-button-action auto',
                    'id'      => 'llms_lost_password_button',
                    'value'   => __( 'Reset Password', 'lifterlms' ),
                    'type'    => 'submit',
                ),
            )
        );

    }

    /**
     * Retrieve an array of password fields.
     *
     * This is only used on the password rest form as a fallback
     * when no "custom" password fields can be found in either of the default
     * checkout or registration forms.
     *
     * @since 3.7.0
     * @since 5.0.0 Removed optional parameters
     *
     * @return array[]
     */
    private static function get_password_fields() {

        $fields = array();

        $fields[] = array(
            'columns'     => 6,
            'classes'     => 'llms-password',
            'id'          => 'password',
            'label'       => __( 'Password', 'lifterlms' ),
            'last_column' => false,
            'match'       => 'password_confirm',
            'required'    => true,
            'type'        => 'password',
        );
        $fields[] = array(
            'columns'  => 6,
            'classes'  => 'llms-password-confirm',
            'id'       => 'password_confirm',
            'label'    => __( 'Confirm Password', 'lifterlms' ),
            'match'    => 'password',
            'required' => true,
            'type'     => 'password',
        );

        $fields[] = array(
            'classes'      => 'llms-password-strength-meter',
            'description'  => __( 'A strong password is required. The password must be at least 6 characters in length. Consider adding letters, numbers, and symbols to increase the password strength.', 'lifterlms' ),
            'id'           => 'llms-password-strength-meter',
            'type'         => 'html',
            'min_length'   => 6,
            'min_strength' => 'strong',
        );

        return $fields;

    }

    /**
     * Retrieve form fields used on the password reset form.
     *
     * This method will attempt to the "custom" password fields in the checkout form
     * and then in the registration form. At least a password field must be found. If
     * it cannot be found this function falls back to a set of default fields as defined
     * in the LLMS_Person_Handler::get_password_fields() method.
     *
     * @since Unknown
     * @since 5.0.0 Get fields from the checkout or registration forms before falling back to default fields.
     *              Changed filter on return from "lifterlms_lost_password_fields" to "llms_password_reset_fields".
     *
     * @param string $key User password reset key, usually populated via $_GET vars.
     * @param string $login User login (username), usually populated via $_GET vars.
     * @return array[]
     */
    public static function get_password_reset_fields( $key = '', $login = '' ) {

        $fields = array();
        foreach ( array( 'checkout', 'registration' ) as $location ) {
            $fields = self::find_password_fields( $location );
            if ( $fields ) {
                break;
            }
        }

        // Fallback if no custom fields are found.
        if ( ! $fields ) {
            $location = 'fallback';
            $fields   = self::get_password_fields();
        }

        // Add button.
        $fields[] = array(
            'classes' => 'llms-button-action auto',
            'id'      => 'llms_lost_password_button',
            'type'    => 'submit',
            'value'   => __( 'Reset Password', 'lifterlms' ),
        );

        // Add hidden fields.
        $fields[] = array(
            'id'    => 'llms_reset_key',
            'type'  => 'hidden',
            'value' => $key,
        );
        $fields[] = array(
            'id'    => 'llms_reset_login',
            'type'  => 'hidden',
            'value' => $login,
        );

        /**
         * Filter password reset form fields.
         *
         * @since 5.0.0
         *
         * @param array[] $fields   Array of form field arrays.
         * @param string  $key      User password reset key, usually populated via $_GET vars.
         * @param string  $login    User login (username), usually populated via $_GET vars.
         * @param string  $location Location where the fields were retrieved from. Either "checkout", "registration", or "fallback".
         *                          Fallback denotes that no password field was located in either of the previous forms so a default
         *                          set of fields is generated programmatically.
         */
        return apply_filters( 'llms_password_reset_fields', $fields, $key, $login, $location );

    }

    /**
     * Login a user
     *
     * @since 3.0.0
     * @since 3.29.4 Unknown.
     * @since 5.0.0 Removed email lookup logic since `wp_authenticate()` supports email addresses as `user_login` since WP 4.5.
     *
     * @param array $data {
     *     User login information.
     *
     *     @type string $llms_login User email address or username.
     *     @type string $llms_password User password.
     *     @type string $llms_remember Whether to extend the cookie duration to keep the user logged in for a longer period.
     * }
     * @return WP_Error|int The WP_User ID on login success or an error object on failure.
     */
    public static function login( $data ) {

        /**
         * Run an action prior to user login.
         *
         * @since 3.0.0
         *
         * @param array $data {
         *    User login credentials.
         *
         *    @type string $user_login User's username.
         *    @type string $password User's password.
         *    @type bool $remeber Whether to extend the cookie duration to keep the user logged in for a longer period.
         * }
         */
        do_action( 'lifterlms_before_user_login', $data );

        /**
         * Filter user submitted login data prior to data validation.
         *
         * @since 3.0.0
         *
         * @param array $data {
         *    User login credentials.
         *
         *    @type string $user_login User's username.
         *    @type string $password User's password.
         *    @type bool $remeber Whether to extend the cookie duration to keep the user logged in for a longer period.
         * }
         */
        $data = apply_filters( 'lifterlms_user_login_data', $data );

        // Validate the fields & allow custom validation to occur.
        $valid = self::validate_login_fields( $data );

        // If errors found, return them.
        if ( is_wp_error( $valid ) ) {

            /**
             * Filters the errors found during a LifterLMS user login attempt
             *
             * @since Unknown
             *
             * @param WP_Error       $valid  Error object containing information about the login error.
             * @param array          $data   User submitted login form data.
             * @param WP_Error|false $signon The original WP Error object returned by `wp_signon()` or false if the error
             *                               is encountered prior to the signon attempt.
             */
            return apply_filters( 'lifterlms_user_login_errors', $valid, $data, false );

        }

        $creds = array(
            'user_login'    => wp_unslash( $data['llms_login'] ), // Unslash ensures that an email address with an apostrophe is unescaped for lookups.
            'user_password' => $data['llms_password'],
            'remember'      => isset( $data['llms_remember'] ),
        );

        /**
         * Filter a user's login credentials immediately prior to signing in.
         *
         * @since Unknown
         *
         * @param array $creds {
         *    User login credentials.
         *
         *    @type string $user_login User's username.
         *    @type string $password User's password.
         *    @type bool $remeber Whether to extend the cookie duration to keep the user logged in for a longer period.
         * }
         */
        $creds  = apply_filters( 'lifterlms_login_credentials', $creds );
        $signon = wp_signon( $creds, is_ssl() );

        if ( is_wp_error( $signon ) ) {

            $err = new WP_Error( 'login-error', __( 'Could not find an account with the supplied email address and password combination.', 'lifterlms' ) );
            // This hook is documented in includes/class.llms.person.handler.php.
            return apply_filters( 'lifterlms_user_login_errors', $err, $data, $signon );

        }

        return $signon->ID;

    }

    /**
     * Validate login form fields
     *
     * @since 5.0.0
     *
     * @param array $data Array of user-submitted data, usually from `$_POST`.
     * @return WP_Error|true Returns an error object or `true` if the submission is valid.
     */
    protected static function validate_login_fields( $data ) {

        $err = new WP_Error();

        $fields = self::get_login_fields();

        foreach ( $fields as $field ) {

            $name  = isset( $field['name'] ) ? $field['name'] : $field['id'];
            $label = isset( $field['label'] ) ? $field['label'] : $name;

            $field_type = isset( $field['type'] ) ? $field['type'] : '';
            $val        = isset( $data[ $name ] ) ? $data[ $name ] : '';

            // Ensure required fields are submitted.
            if ( ! empty( $field['required'] ) && empty( $val ) ) {

                $err->add( $field['id'], sprintf( __( '%s is a required field', 'lifterlms' ), $label ), 'required' );
                continue;

            }

            // Email fields must be emails.
            if ( 'email' === $field_type && ! is_email( $val ) ) {
                $err->add( $field['id'], sprintf( __( '%s must be a valid email address', 'lifterlms' ), $label ), 'invalid' );
            }
        }

        $valid = $err->has_errors() ? $err : true;

        /**
         * Filters the validation result of user-submitted login data
         *
         * @since 4.21.0
         *
         * @param WP_Error|boolean $valid An error object containing validation errors or `true` if no validation errors found.
         * @param array            $data  User submitted login data.
         */
        return apply_filters( 'llms_after_user_login_data_validation', $valid, $data );

    }

    /**
     * Retrieve an array of fields for a specific screen
     *
     * @since 3.0.0
     * @since 3.7.0 Unknown.
     * @deprecated 5.0.0 `LLMS_Person_Handler::get_available_fields()` is deprecated in favor of `LLMS_Forms::get_form_fields()`.
     *
     * @param string    $screen Name os the screen [account|checkout|registration].
     * @param array|int $data   Array of data to fill fields with or a WP User ID.
     * @return array
     */
    public static function get_available_fields( $screen = 'registration', $data = array() ) {
        _deprecated_function( 'LLMS_Person_Handler::get_available_fields()', '5.0.0', 'LLMS_Forms::get_form_fields()' );
        return LLMS_Forms::instance()->get_form_fields( $screen );
    }

}