gocodebox/lifterlms

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

Summary

Maintainability
A
25 mins
Test Coverage
A
99%
<?php
/**
 * LLMS_Form_Templates class.
 *
 * @package LifterLMS/Classes
 *
 * @since 5.0.0
 * @version 5.1.1
 */

defined( 'ABSPATH' ) || exit;

/**
 * Manage llms_form post type templates
 *
 * Handles creation of reusable blocks for the core/default fields used
 * by the default checkout, registration, and account edit forms.
 *
 * @since 5.0.0
 */
class LLMS_Form_Templates {

    /**
     * Transform a block definition into a confirm group
     *
     * @since 5.0.0
     *
     * @param array $block A WP_Block definition array.
     * @return array
     */
    private static function add_confirm_group( $block ) {

        $inner = array(
            self::get_confirm_group_controller( $block ),
            self::get_confirm_group_controlled( $block ),
        );

        if ( is_rtl() ) {
            $inner = array_reverse( $inner );
        }

        $attrs = array(
            'fieldLayout' => 'columns',
        );

        if ( ! empty( $block['attrs']['llms_visibility'] ) ) {
            $attrs['llms_visibility'] = $block['attrs']['llms_visibility'];
        }

        return array(
            'blockName'   => 'llms/form-field-confirm-group',
            'innerBlocks' => $inner,
            'attrs'       => $attrs,
        );

    }

    /**
     * Create a wp_block (resuable block) post for a given field id
     *
     * This method will attempt to use an existing reusable block field
     * if it already exists and will only create it if one isn't found.
     *
     * @since 5.0.0
     * @since 5.1.1 Run serialized block content through `wp_slash()` to preserve special characters converted to character codes.
     *
     * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().
     * @return int Returns the WP_Post ID of the the wp_block post type or `0` on failure.
     */
    private static function create_reusable_block( $field_id ) {

        $existing = self::find_reusable_block( $field_id );
        if ( $existing ) {
            return $existing->ID;
        }

        $block_data = self::get_block_data( $field_id );

        $args = array(
            'post_title'   => $block_data['title'],
            'post_content' => wp_slash( serialize_blocks( $block_data['block'] ) ),
            'post_status'  => 'publish',
            'post_type'    => 'wp_block',
            'meta_input'   => array(
                '_is_llms_field' => 'yes',
                '_llms_field_id' => $field_id,
            ),
        );

        return wp_insert_post( $args );

    }

    /**
     * Locates an existing wp_block post by field id
     *
     * @since 5.0.0
     * @since 5.1.0 Method access changed from private to public.
     *
     * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().
     * @return WP_Post|boolean Returns the post object or false if not found.
     */
    public static function find_reusable_block( $field_id ) {

        $query = new WP_Query(
            array(
                'posts_per_page' => 1,
                'no_found_rows'  => true,
                'post_type'      => 'wp_block',
                'meta_key'       => '_llms_field_id',
                'meta_value'     => $field_id,
            )
        );

        return $query->posts ? $query->posts[0] : false;

    }

    /**
     * Retrieve a block array for use in a template
     *
     * Returns a reusable block when `$reusable` is `true` or returns a regular
     * block modified by legacy options for the given location when `$reusable` is `false`.
     *
     * @since 5.0.0
     * @since 5.1.0 Method access set to public.
     *
     * @param string  $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().
     * @param string  $location Form location. Accepts "checkout", "registration", or "account".
     * @param boolean $reusable Whether or not a reusable block should be retrieved.
     * @return array
     */
    public static function get_block( $field_id, $location, $reusable ) {

        if ( $reusable ) {
            return self::get_reusable_block( $field_id );
        }

        $legacy_opt = self::get_legacy_option( $field_id, $location );
        // Add a confirm group for email when confirmation is set or for password fields.
        $confirm    = ( ( 'email' === $field_id && 'yes' === $legacy_opt ) || 'password' === $field_id );
        $block_data = self::get_block_data( $field_id, $confirm );
        if ( 'hidden' === $legacy_opt ) {
            return array();
        }

        $block = $block_data['block'][0];

        if ( in_array( $legacy_opt, array( 'required', 'optional' ), true ) ) {
            $block = self::set_required_atts( $block, ( 'required' === $legacy_opt ) );
        }

        return $block;

    }

    /**
     * Retrieve data for a given field by id
     *
     * @since 5.0.0
     *
     * @param string  $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().
     * @param boolean $confirm  If `true` and the schema includes a confirmation field, will convert the field to a confirm group.
     * @return array Returns an array containing the block data and title.
     */
    private static function get_block_data( $field_id, $confirm = true ) {

        $block = self::get_reusable_block_schema( $field_id );
        $title = $block['title'];
        unset( $block['title'] );

        if ( $confirm && ! empty( $block['confirm'] ) ) {
            $block = self::add_confirm_group( $block );
        }

        $block = self::prepare_blocks( array( $block ) );
        return compact( 'title', 'block' );

    }

    /**
     * Creates a WP_Block array definition for the confirmation (controlled) block in a confirm group
     *
     * @since 5.0.0
     *
     * @param array $block A WP_Block definition array for the primary/default block in the group.
     * @return array A new WP_Block definition array for the controlled block.
     */
    private static function get_confirm_group_controlled( $block ) {

        $block['blockName'] = 'llms/form-field-text';

        $block['attrs'] = wp_parse_args(
            array(
                'field'               => $block['confirm'],
                'id'                  => $block['attrs']['id'] . '_confirm',
                'name'                => $block['attrs']['id'] . '_confirm',
                'label'               => sprintf( __( 'Confirm %s', 'lifterlms' ), $block['attrs']['label'] ),
                'columns'             => 6,
                'last_column'         => is_rtl() ? false : true,
                'isConfirmationField' => true,
                'llms_visibility'     => 'off',
                'match'               => $block['attrs']['id'],
                'data_store'          => false,
                'data_store_key'      => false,
            ),
            $block['attrs']
        );

        return $block;

    }

    /**
     * Creates a WP_Block array definition for the primary (controller) block in a confirm group
     *
     * @since 5.0.0
     *
     * @param array $block A WP_Block definition array for the primary/default block in the group.
     * @return array A new WP_Block definition array for the controller block.
     */
    private static function get_confirm_group_controller( $block ) {

        $block['attrs'] = wp_parse_args(
            array(
                'columns'                    => 6,
                'last_column'                => is_rtl() ? true : false,
                'isConfirmationControlField' => true,
                'llms_visibility'            => 'off',
                'match'                      => $block['attrs']['id'] . '_confirm',
            ),
            $block['attrs']
        );

        return $block;

    }

    /**
     * Retrieves legacy option's value for a given field and location
     *
     * @since 5.0.0
     *
     * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().
     * @param string $location Form location. Accepts "checkout", "registration", or "account".
     * @return string
     */
    private static function get_legacy_option( $field_id, $location ) {

        $name_map = array(
            'address' => 'address',
            'email'   => 'email_confirmation',
            'name'    => 'names',
            'phone'   => 'phone',
        );

        $val = '';

        if ( array_key_exists( $field_id, $name_map ) ) {

            $key = sprintf( 'lifterlms_user_info_field_%1$s_%2$s_visibility', $name_map[ $field_id ], $location );
            $val = get_option( $key );

        }

        return $val;

    }

    /**
     * Retrieves a core/block WP_Block array for a given default/core field
     *
     * This method will attempt to use an existing wp_block for the given field id
     * if it exists, and when not found creates a new one.
     *
     * @since 5.0.0
     *
     * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().
     * @return array A WP_Block definition array.
     */
    private static function get_reusable_block( $field_id ) {

        $ref = self::create_reusable_block( $field_id );

        return array(
            'blockName'    => 'core/block',
            'attrs'        => compact( 'ref' ),
            'innerContent' => array(),
        );
    }

    /**
     * Retrieves the schema definition for a default/core reusable block
     *
     * @since 5.0.0
     *
     * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().
     * @return array The block definition schema. This is a WP_Block array definition but missing some data that is automatically populated before serialization.
     */
    private static function get_reusable_block_schema( $field_id ) {

        $list = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-reusable-blocks.php';

        $definition = empty( $list[ $field_id ] ) ? array() : self::prepare_block_attrs( $list[ $field_id ] );

        /**
         * Filters the result of a schema definition.
         *
         * This hook can be used to add definitions for custom (non-core) fields or to modify a core definition.
         *
         * @since 5.0.0
         *
         * @param array  $definition The schema definition.
         * @param string $field_id   The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().
         */
        return apply_filters( 'llms_get_reusable_block_schema', $definition, $field_id );

    }

    /**
     * Retrieve the block template HTML for a given location.
     *
     * @since 5.0.0
     *
     * @param string $location Form location. Accepts "checkout", "registration", or "account".
     * @return string
     */
    public static function get_template( $location ) {

        /**
         * Filters whether or not reusable blocks should be used when generating a form template
         *
         * By default when migrating from 4.x, non-reusable blocks will be used in order to ensure legacy settings
         * are transferred during an upgrade to 5.x. However, on a "clean" install of 5.x, reusable blocks will be
         * used in favor of regular blocks.
         *
         * @since 5.0.0
         *
         * @param boolean $use_reusable Whether or not to use reusable blocks.
         */
        $use_reusable = apply_filters( 'llms_blocks_template_use_reusable_blocks', ( 'not-set' === get_option( 'lifterlms_registration_generate_username', 'not-set' ) ) );

        $blocks = self::get_template_blocks( $location, $use_reusable );

        return serialize_blocks( $blocks );

    }

    /**
     * Retrieve a list of blocks for the given template
     *
     * @since 5.0.0
     *
     * @param string  $location Form location id.
     * @param boolean $reusable Whether or not reusable blocks should be used.
     * @return array[]
     */
    private static function get_template_blocks( $location, $reusable ) {

        $blocks = array();

        // Email and password are added in different locations depending on the form.
        $base = array(
            self::get_block( 'email', $location, $reusable ),
            self::get_block( 'password', $location, $reusable ),
        );

        // Username only added when option is off on legacy sites.
        if ( 'account' !== $location && ! llms_parse_bool( get_option( 'lifterlms_registration_generate_username', 'yes' ) ) ) {
            array_unshift( $base, self::get_reusable_block( 'username' ) );
        }

        // Email and password go first on checkout/reg forms.
        if ( 'account' !== $location ) {
            $blocks = array_merge( $base, $blocks );
        }

        $blocks[] = self::get_block( 'name', $location, $reusable );

        // Display name on account only, users can add to other forms if desired.
        if ( 'account' === $location ) {
            $blocks[] = self::get_block( 'display_name', $location, $reusable );
        }

        $blocks[] = self::get_block( 'address', $location, $reusable );
        $blocks[] = self::get_block( 'phone', $location, $reusable );

        if ( 'registration' === $location ) {
            $blocks[] = self::get_voucher_block();
        }

        // Email and password go at the end on the account form.
        if ( 'account' === $location ) {
            $blocks = array_merge( $blocks, $base );
        }

        return array_filter( $blocks );

    }

    /**
     * Retrieve block for the voucher row.
     *
     * @since 5.0.0
     *
     * @return array
     */
    private static function get_voucher_block() {

        // Don't include voucher if legacy option has vouchers hidden.
        $option = get_option( 'lifterlms_voucher_field_registration_visibility', 'optional' );
        if ( 'hidden' === $option ) {
            return array();
        }

        return array(
            'blockName'    => 'llms/form-field-redeem-voucher',
            'attrs'        => array(
                'id'             => 'llms_voucher',
                'label'          => __( 'Have a voucher?', 'lifterlms' ),
                'placeholder'    => __( 'Voucher Code', 'lifterlms' ),
                'required'       => ( 'required' === $option ),
                'toggleable'     => true,
                'data_store'     => false,
                'data_store_key' => false,
            ),
            'innerContent' => array(),
        );

    }

    /**
     * Prepares block attributes for a given reusable block
     *
     * This method loads a reusable block from the blocks schema and attempts to locate a user information field
     * for the given field block from the user information fields schema.
     *
     * The field is matched by the block's "id" attribute which should match a user information field's "name" attribute.
     *
     * When a match is found, the information field data is merged into the block data and the settings are converted from field settings
     * to block attributes.
     *
     * @since 5.0.0
     *
     * @param array $block A partial WP_Block array used to create a reusable block.
     * @return array
     */
    private static function prepare_block_attrs( $block ) {

        if ( ! empty( $block['innerBlocks'] ) ) {
            foreach ( $block['innerBlocks'] as &$inner_block ) {
                $inner_block = self::prepare_block_attrs( $inner_block );
            }
        } elseif ( ! empty( $block['attrs']['id'] ) ) {

            // If we find a field, merge the block into the field and convert it to block attributes.
            $field          = llms_get_user_information_field( $block['attrs']['id'] );
            $block['attrs'] = $field ? LLMS_Forms::instance()->convert_settings_to_block_attrs( wp_parse_args( $field, $block['attrs'] ) ) : $block['attrs'];

        }

        return $block;

    }

    /**
     * Recursively prepare a list of blocks to ensure it can be passed into serialize_blocks() without error
     *
     * @since 5.0.0
     *
     * @param array[] $blocks Array of WP_Block definition arrays.
     * @return array[]
     */
    private static function prepare_blocks( $blocks ) {

        foreach ( $blocks as &$block ) {

            $block = wp_parse_args(
                $block,
                array(
                    'attrs'       => array(),
                    'innerBlocks' => array(),
                )
            );

            if ( ! empty( $block['innerBlocks'] ) ) {
                $block['innerBlocks'] = self::prepare_blocks( $block['innerBlocks'] );
            }

            // WP core serialize_block() doesn't work unless this...
            $block['innerContent'] = array_fill( 0, count( $block['innerBlocks'] ), null );

        }

        return $blocks;

    }

    /**
     * Modifies the `required` block attribute
     *
     * @since 5.0.0
     *
     * @param array   $block    A WP_Block definition array.
     * @param boolean $required Desired value of the required attribute.
     */
    private static function set_required_atts( $block, $required ) {

        if ( isset( $block['attrs']['required'] ) && 'llms/form-field-user-address-street-secondary' !== $block['blockName'] ) {
            $block['attrs']['required'] = $required;
        }

        foreach ( $block['innerBlocks'] as &$inner_block ) {
            $inner_block = self::set_required_atts( $inner_block, $required );
        }

        return $block;

    }

}