includes/forms/class-llms-form-templates.php
<?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;
}
}