<?php /** * Register and manage LifterLMS user forms * * @package LifterLMS/Classes * * @since 5.0.0 * @version 5.9.0 */ defined( 'ABSPATH' ) || exit; /** * LLMS_Forms class * * @since 5.0.0 * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`. */ class LLMS_Forms { use LLMS_Trait_Singleton; /** * Minimum Supported WP Version required to manage forms with the block editor UI. */ const MIN_WP_VERSION = '5.7.0'; /** * Provide access to the post type manager class * * @var LLMS_Forms_Post_Type */ public $post_type_manager = null; /** * Private Constructor * * @since 5.0.0 * * @return void */ private function __construct() { $this->post_type_manager = new LLMS_Form_Post_Type( $this ); add_filter( 'render_block', array( $this, 'render_field_block' ), 10, 2 ); add_filter( 'llms_get_form_post', array( $this, 'maybe_load_preview' ) ); } /** * Determines if the WP core requirements are met * * This is used to determine if the block editor can be used to manage forms and fields, * all frontend and server-side handling works on all core supported WP versions. * * @since 5.0.0 * * @return boolean */ public function are_requirements_met() { global $wp_version; return version_compare( $wp_version, self::MIN_WP_VERSION, '>=' ) || is_plugin_active( 'gutenberg/gutenberg.php' ); } /** * Determine if usernames are enabled on the site. * * This method is used to determine if a username can be used to login / reset a user's password. * * A reference to every form with a username block is stored in an option. The option is an array * of integers, the WP_Post IDs of all the form posts containing a username block. * * If the array is empty, there are no forms with username blocks and, therefore, usernames are disabled. * If the array contains at least one item that means there is a form with a username block in it and, * we therefore consider usernames to be enabled for the site. * * This isn't perfect. We're well aware. But usernames are kind of silly anyway, right? Just use the email * address like your average website owner and stop pretending usernames matter. * * @since 5.0.0 * * @return bool */ public function are_usernames_enabled() { $locations = get_option( 'llms_forms_username_locations', array() ); /** * Use this to explicitly enable of disable username fields. * * Note that usage of this filter will not actually disable the llms/form-field-username block. * It's possible to create a confusing user experience by explicitly disabling usernames and * leaving username field blocks on one or more forms. If you decide to explicitly disable via * this filter you should also remove all the username blocks from all of your forms. * * @since 5.0.0 * * @param boolean $enabled Whether or not usernames are enabled. */ return apply_filters( 'llms_are_usernames_enabled', ! empty( $locations ) ); } /** * Converts a block to settings understandable by `llms_form_field()` * * @since 5.0.0 * @since 5.1.0 Added logic to remove invisible fields. * Added `$block_list` param. * * @param array $block A WP Block array. * @param array[] $block_list Optional. The list of WP Block array `$block` comes from. Default is empty array. * @return array */ private function block_to_field_settings( $block, $block_list = array() ) { $is_visible = $this->is_block_visible_in_list( $block, $block_list ); /** * Filters whether or not invisible fields should be included * * If the block is not visible (according to LLMS block-level visibility settings) * it will return an empty array (signaling the field to be removed). * * @since 5.1.0 * * @param boolean $filter Whether or not invisible fields should be included. Default is `false`. * @param array $block A WP Block array. * @param array[] $block_list The list of WP Block array `$block` comes from. */ if ( ! $is_visible && apply_filters( 'llms_forms_remove_invisible_field', false, $block, $block_list ) ) { return array(); } $attrs = $this->convert_settings_format( $block['attrs'], 'block' ); // If the field is required and hidden it's impossible for the user to fill it out so it gets marked as optional at runtime. if ( ! empty( $attrs['required'] ) && ! $is_visible ) { $attrs['required'] = false; } /** * Filter an LLMS_Form_Field settings array after conversion from a field block * * @since 5.0.0 * @since 5.1.0 Added `$block_list` param. * * @param array $attrs An array of LLMS_Form_Field settings. * @param array $block A WP Block array. * @param array[] $block_list The list of WP Block array `$block` comes from. */ return apply_filters( 'llms_forms_block_to_field_settings', $attrs, $block, $block_list ); } /** * Cascade all llms_visibility attributes down into inner blocks. * * If a parent block has a visibility setting this will apply that visibility to a chlid block *if* * the child block does not have a visibility setting of its own. * * Ultimately this ensures that a field block that's not visible can be marked as "optional" so that * form validation can take place. * * For example, if a columns block is displayed only to logged out users and it's child fields are marked * as required that means that it's required only to logged out users and the field becomes "optional" * (for validation purposes) to logged in users. * * @since 5.0.0 * * @param array[] $blocks Array of parsed block arrays. * @param string|null $visibility The llms_visibility attribute of the parent block which is applied to all innerBlocks * if the innerBlock does not already have it's own visibility attribute. * @return array[] */ private function cascade_visibility_attrs( $blocks, $visibility = null ) { foreach ( $blocks as &$block ) { // If a visibility setting has been passed from the parent and the block does not have visibility setting of it's own. if ( $visibility && ( empty( $block['attrs']['llms_visibility'] ) || 'off' === $block['attrs']['llms_visibility'] ) ) { $block['attrs']['llms_visibility'] = $visibility; } // This block has a visibility attribute and it should be applied it to all the innerBlocks. if ( ! empty( $block['attrs']['llms_visibility'] ) && ! empty( $block['innerBlocks'] ) ) { $block['innerBlocks'] = $this->cascade_visibility_attrs( $block['innerBlocks'], $block['attrs']['llms_visibility'] ); } } return $blocks; } /** * Converts field settings formats * * There are small differences between the LLMS_Form_Fields settings array * and the WP_Block settings array. * * This method accepts an associative array * in one format or the other and converts it from the original format to the opposite format. * * @since 5.0.0 * * @param array $map Associative array of settings. * @param string $orignal_format The original format of the submitted `$map`. Either "field" for * an array of LLMS_Form_Field settings or `block` for an array * of WP_Block attributes. * @return [type] [description] */ private function convert_settings_format( $map, $orignal_format ) { // Block attributes to LLMS_Form_Field settings. $keys = array( 'field' => 'type', 'className' => 'classes', 'html_attrs' => 'attributes', ); // LLMS_Form_Field settings to block attributes. if ( 'field' === $orignal_format ) { $keys = array_flip( $keys ); } // Loop through the original map and rename the necessary keys. foreach ( $keys as $orig_key => $new_key ) { if ( isset( $map[ $orig_key ] ) ) { $map[ $new_key ] = $map[ $orig_key ]; unset( $map[ $orig_key ] ); } } return $map; } /** * Converts an array of LLMS_Form_Field settings to a block attributes array * * @since 5.0.0 * * @param array $settings An array of LLMS_Form_Field settings. * @return array An array of WP_Block attributes. */ public function convert_settings_to_block_attrs( $settings ) { return $this->convert_settings_format( $settings, 'field' ); } /** * Create a form for a given location with the provided data. * * @since 5.0.0 * * @param string $location_id Location id. * @param bool $recreate If `true` and the form already exists, will recreate the existing form using the existing form's id. * @return int|false Returns the created/update form post ID on success. * If the location doesn't exist, returns `false`. * If the form already exists and `$recreate` is `false` will return `false`. */ public function create( $location_id, $recreate = false ) { if ( ! $this->is_location_valid( $location_id ) ) { return false; } $locs = $this->get_locations(); $data = $locs[ $location_id ]; $existing = $this->get_form_post( $location_id ); // Form already exists and we haven't requested an update. if ( false !== $existing && ! $recreate ) { return false; } $args = array( 'ID' => $existing ? $existing->ID : 0, 'post_content' => LLMS_Form_Templates::get_template( $location_id ), 'post_status' => 'publish', 'post_title' => $data['title'], 'post_type' => $this->get_post_type(), 'meta_input' => $data['meta'], 'post_author' => $existing ? $existing->post_author : LLMS_Install::get_can_install_user_id(), ); /** * Filter arguments used to install a new form. * * @since 5.0.0 * * @param array $args Array of arguments to be passed to wp_insert_post * @param string $location_id Location ID/name. * @param array $data Array of location information from LLMS_Forms::get_locations(). */ $args = apply_filters( 'llms_forms_install_post_args', $args, $location_id, $data ); return wp_insert_post( $args ); } /** * Retrieve the form management user capability. * * @since 5.0.0 * * @return string */ public function get_capability() { return $this->post_type_manager->capability; } /** * Pull LifterLMS Form Field blocks from an array of parsed WP Blocks. * * Searches innerBlocks arrays recursively. * * @since 5.0.0 * @since 5.1.0 First check block's innerBlock attribute exists when checking for inner blocks. * Also made the access visibility public. * @since 5.9.0 Pass an empty string to `strpos()` instead of `null`. * * @param array $blocks Array of WP Block arrays from `parse_blocks()`. * @return array */ public function get_field_blocks( $blocks ) { $fields = array(); foreach ( $blocks as $block ) { if ( ! empty( $block['innerBlocks'] ) ) { $fields = array_merge( $fields, $this->get_field_blocks( $block['innerBlocks'] ) ); } elseif ( false !== strpos( $block['blockName'] ?? '', 'llms/form-field-' ) ) { $fields[] = $block; } elseif ( 'core/html' === $block['blockName'] && ! empty( $block['attrs']['type'] ) ) { $fields[] = $block; } } return $fields; } /** * Returns a list of field names used by LifterLMS forms * * Used to validate uniqueness of custom field data. * * @since 5.0.0 * * @return string[] */ public function get_field_names() { $names = array( 'user_login', 'user_login_confirm', 'email_address', 'email_address_confirm', 'password', 'password_confirm', 'first_name', 'last_name', 'display_name', 'llms_billing_address_1', 'llms_billing_address_2', 'llms_billing_city', 'llms_billing_country', 'llms_billing_state', 'llms_billing_zip', 'llms_phone', ); /** * Filters the list of field names used by LifterLMS forms * * @since 5.0.0 * * @param string[] $names List of registered field names. */ return apply_filters( 'llms_forms_field_names', $names ); } /** * Retrieve an array of parsed blocks for the form at a given location. * * @since 5.0.0 * * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter. * @return array|false */ public function get_form_blocks( $location, $args = array() ) { $post = $this->get_form_post( $location, $args ); if ( ! $post ) { return false; } $content = $post->post_content; $content .= $this->get_additional_fields_html( $location, $args ); $blocks = $this->parse_blocks( $content ); /** * Filters the parsed block list for a given LifterLMS form * * This hook can be used to programmatically modify, insert, or remove * blocks (fields) from a form. * * @since 5.0.0 * * @param array[] $blocks Array of parsed WP_Block arrays. * @param string $location The request form location ID. * @param array $args Additional arguments passed to the short-circuit filter. */ return apply_filters( 'llms_get_form_blocks', $blocks, $location, $args ); } /** * Retrieve an array of LLMS_Form_Fields settings arrays for the form at a given location. * * This method is used by the LLMS_Form_Handler to perform validations on user-submitted data. * * @since 5.0.0 * * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter in `get_form_post()`. * @return false|array */ public function get_form_fields( $location, $args = array() ) { $blocks = $this->get_form_blocks( $location, $args ); if ( false === $blocks ) { return false; } $fields = $this->get_fields_settings_from_blocks( $blocks ); /** * Modify the parsed array of LifterLMS Form Fields * * @since 5.0.0 * * @param array[] $fields Array of LifterLMS Form Field settings data. * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter in `get_form_post()`. */ return apply_filters( 'llms_get_form_fields', $fields, $location, $args ); } /** * Retrieve an array of LLMS_Form_Fields settings arrays from an array of blocks * * @since 5.0.0 * @since 5.1.0 Pass the whole list of blocks to the `$this->block_to_field_settings()` method * To better check whether a block is visible. * * @param array $blocks Array of WP Block arrays from `parse_blocks()`. * @return false|array */ public function get_fields_settings_from_blocks( $blocks ) { $fields = array(); $blocks = $this->get_field_blocks( $blocks ); foreach ( $blocks as $block ) { $settings = $this->block_to_field_settings( $block, $blocks ); if ( $settings ) { $field = new LLMS_Form_Field( $settings ); $fields[] = $field->get_settings(); } } return $fields; } /** * Retrieve a field item from a list of fields by a key/value pair. * * @since 5.0.0 * * @param array[] $fields List of LifterLMS Form Fields. * @param string $key Setting key to search for. * @param mixed $val Setting valued to search for. * @param string $return Determine the return value. Use "field" to return the field settings * array. Use "index" to return the index of the field in the $fields array. * @return array|int|false `false` when the field isn't found in $fields, otherwise returns the field settings * as an array when `$return` is "field". Otherwise returns the field's index as an int. */ public function get_field_by( $fields, $key, $val, $return = 'field' ) { foreach ( $fields as $index => $field ) { if ( isset( $field[ $key ] ) && $val === $field[ $key ] ) { return 'field' === $return ? $field : $index; } } return false; } /** * Retrieve the rendered HTML for the form at a given location. * * @since 5.0.0 * * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter in `get_form_post()`. * @return string */ public function get_form_html( $location, $args = array() ) { $blocks = $this->get_form_blocks( $location, $args ); if ( ! $blocks ) { return ''; } $disable_visibility = ( 'checkout' !== $location ); // Force fields to display regardless of visibility settings when viewing account/registration forms. if ( $disable_visibility ) { add_filter( 'llms_blocks_visibility_should_filter_block', '__return_false', 999 ); } $html = ''; foreach ( $blocks as $block ) { $html .= render_block( $block ); } if ( $disable_visibility ) { remove_filter( 'llms_blocks_visibility_should_filter_block', '__return_false', 999 ); } /** * Modify the parsed array of LifterLMS Form Fields. * * @since 5.0.0 * * @param string $html Form fields HTML. * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter in `get_form_post()`. */ return apply_filters( 'llms_get_form_html', $html, $location, $args ); } /** * Retrieve the WP Post for the form at a given location. * * @since 5.0.0 * * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter. * @return WP_Post|false */ public function get_form_post( $location, $args = array() ) { // @todo Add caching. This runs twice on some page loads. /** * Skip core lookup of the form for the request location and return a custom form post. * * @since 5.0.0 * * @param null|WP_Post $post Return a WP_Post object to short-circuit default lookup query. * @param string $location Form location. Either "checkout", "registration", or "account". * @param array $args Additional custom arguments. */ $post = apply_filters( 'llms_get_form_post_pre_query', null, $location, $args ); if ( is_a( $post, 'WP_Post' ) ) { return $post; } $query = new WP_Query( array( 'post_type' => $this->get_post_type(), 'posts_per_page' => 1, 'no_found_rows' => true, // Only show published forms to end users but allow admins to "preview" drafts. 'post_status' => current_user_can( $this->get_capability() ) ? array( 'publish', 'draft' ) : 'publish', 'meta_query' => array( 'relation' => 'AND', array( 'key' => '_llms_form_location', 'value' => $location, ), array( 'key' => '_llms_form_is_core', 'value' => 'yes', ), ), ) ); $post = $query->have_posts() ? $query->posts[0] : false; /** * Filters the returned `llms_form` post object * * @since 5.0.0 * * @param WP_Post|boolean $post The post object of the form or `false` if no form could be located. * @param string $location Form location. Either "checkout", "registration", or "account". * @param array $args Additional custom arguments. */ return apply_filters( 'llms_get_form_post', $post, $location, $args ); } /** * Retrieve additional fields added to the form programmatically. * * @since 5.0.0 * * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter. * @return array[] */ private function get_additional_fields( $location, $args = array() ) { /** * Filter to add custom fields to a form programmatically. * * @since 3.0.0 * @since 5.0.0 Moved from deprecated function `LLMS_Person_Handler::get_available_fields()`. * * @param array[] $fields Array of field array suitable to pass to `llms_form_field()`. * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter. */ return apply_filters( 'lifterlms_get_person_fields', array(), $location, $args ); } /** * Retrieve HTML for the form's additional programmatically-added fields. * * Gets the HTML for each field from `llms_form_field()` and wraps it as a `wp/html` block. * * @since 5.0.0 * * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter. * @return string */ private function get_additional_fields_html( $location, $args = array() ) { $html = ''; $fields = $this->get_additional_fields( $location, $args ); foreach ( $fields as $field ) { $html .= "\r" . $this->get_custom_field_block_markup( $field ); } return $html; } /** * Retrieve the HTML markup for a custom form field block * * Retrieves an array of `LLMS_Form_Field` settings, generates the HTML * for the field, and wraps it in a `wp:html` block. * * @since 5.0.0 * * @param array $settings Form field settings (passed to `llms_form_field()`). * @return string */ public function get_custom_field_block_markup( $settings ) { return sprintf( '<!-- wp:html %1$s -->%2$s%3$s%2$s<!-- /wp:html -->', wp_json_encode( $settings ), "\r", llms_form_field( $settings, false ) ); } /** * Retrieve an array of form fields used for the "free enrollment" form * * This is the "one-click" enrollment form used when a logged-in user clicks the "checkout" button * from an access plan. * * This function converts the checkout form to hidden fields, the result is that users with all required fields * will be enrolled into the course with a single click (no need to head to the checkout page) and users * who are missing required information will be directed to the checkout page. * * @since 5.0.0 * @since 5.1.0 Specifiy to pass the new 3rd param to the `llms_forms_block_to_field_settings` filter callback. * @since 5.9.0 Fix php 8.1 deprecation warnings when `get_form_fields()` returns `false`. * * @param LLMS_Access_Plan $plan Access plan being used for enrollment. * @return array[] List of LLMS_Form_Field settings arrays. */ public function get_free_enroll_form_fields( $plan ) { // Convert all fields to hidden fields and remove any fields hidden by LLMS block-level visibility settings. add_filter( 'llms_forms_block_to_field_settings', array( $this, 'prepare_field_for_free_enroll_form' ), 999, 3 ); $fields = $this->get_form_fields( 'checkout', compact( 'plan' ) ); remove_filter( 'llms_forms_block_to_field_settings', array( $this, 'prepare_field_for_free_enroll_form' ), 999, 3 ); // If no fields are found, ensure we add to an array instead of casting false to an array (causing a PHP 8.1 deprecation warning). $fields = ! is_array( $fields ) ? array() : $fields; // Add additional fields required for form processing. $fields[] = array( 'name' => 'free_checkout_redirect', 'type' => 'hidden', 'value' => $plan->get_redirection_url(), 'data_store_key' => false, ); $fields[] = array( 'id' => 'llms-plan-id', 'name' => 'llms_plan_id', 'type' => 'hidden', 'value' => $plan->get( 'id' ), 'data_store_key' => false, ); /** * Filter the list of LLMS_Form_Fields used to generate the "free enrollment" form * * @since 5.0.0 * * @param array[] $fields List of LLMS_Form_Field settings arrays. * @param LLMS_Access_Plan $plan Access plan being used for enrollment. */ return apply_filters( 'llms_forms_get_free_enroll_form_fields', $fields, $plan ); } /** * Retrieve the HTML of form fields used for the "free enrollment" form * * @since 5.0.0 * * @see LLMS_Forms::get_free_enroll_form_fields() * * @param LLMS_Access_Plan $plan Access plan being used for enrollment. * @return string */ public function get_free_enroll_form_html( $plan ) { $html = ''; foreach ( $this->get_free_enroll_form_fields( $plan ) as $field ) { $html .= llms_form_field( $field, false ); } return $html; } /** * Retrieve information on all the available form locations. * * @since 5.0.0 * * @return array[] { * An associative array. The array key is the location ID and each array is a location definition array. * * @type string $name The human-readable location name (as displayed on the admin panel). * @type string $description A description of the form (as displayed on the admin panel). * @type string $title The form's post title. This is displayed to the end user when the "Show Form Title" option is enabled. * @type array $meta An associative array of postmeta information for the form. The array key is the meta key and the value is the meta value. * @type string $template A string used to generate the post content of the form post, usually retrieve from `LLMS_Form_Templates`. * @type array $meta Array of meta data used when generating the form. The array key is the meta key and array value is the meta value. * @type array[] $required Array of arrays defining required fields for each form. * } */ public function get_locations() { $locations = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-form-locations.php'; /** * Filter the available form locations. * * NOTE: Removing core forms (as well as modifying the ids / keys) may cause areas of LifterLMS to stop working. * * @since 5.0.0 * * @param array[] $locations Associative array of form location information. */ return apply_filters( 'llms_forms_get_locations', $locations ); } /** * Retrieve the forms post type name. * * @since 5.0.0 * * @return string */ public function get_post_type() { return $this->post_type_manager->post_type; } /** * Determine if a block is visible based on LifterLMS Visibility Settings. * * @since 5.0.0 * * @param array $block Parsed block array. * @return bool */ private function is_block_visible( $block ) { // Make the block return `true` if it's visible, it will already automatically return an empty string if it's invisible. add_filter( 'render_block', '__return_true', 5 ); // Don't run this classes render function on the block during this test. remove_filter( 'render_block', array( $this, 'render_field_block' ), 10, 2 ); // Render the block. $render = render_block( $block ); // Cleanup / reapply filters. add_filter( 'render_block', array( $this, 'render_field_block' ), 10, 2 ); remove_filter( 'render_block', '__return_true', 5 ); /** * Filter whether or not the block is visible. * * @since 5.0.0 * * @param bool $visible Whether or not the block is visible. * @param array $block Parsed block array. */ return apply_filters( 'llms_forms_is_block_visible', llms_parse_bool( $render ), $block ); } /** * Determine if a block is visible in the list it's contained based on LifterLMS Visibility Settings * * Fall back on `$this->is_block_visible()` if empty `$block_list` is provided. * * @since 5.1.0 * * @param array $block Parsed block array. * @param array[] $block_list The list of WP Block array `$block` comes from. * @return bool Returns `true` if `$block` (and all its parents) are visible. Returns `false` when `$block` * or any of its parents are hidden or when `$block` is not found within `$block_list`. */ public function is_block_visible_in_list( $block, $block_list ) { if ( empty( $block_list ) ) { return $this->is_block_visible( $block ); } $path = $this->get_block_path( $block, $block_list ); $is_visible = ! empty( $path ); // Assume the block is visible until proven hidden, except when path is empty. foreach ( $path as $block ) { if ( ! $this->is_block_visible( $block ) ) { $is_visible = false; break; } } /** * Filter whether or not the block is visible in the list of blocks it's contained. * * @since 5.1.0 * * @param bool $is_visible Whether or not the block is visible. * @param array $block Parsed block array. * @param array[] $block_list The list of WP Block array `$block` comes from. */ return apply_filters( 'llms_forms_is_block_visible', $is_visible, $block, $block_list ); } /** * Returns a list of block parents plus the block itself in reverse order * * @since 5.1.0 * * @param array $block Parsed block array. * @param array[] $block_list The list of WP Block array `$block` comes from. * @param int $iterations Stores the number of iterations. * @return array[] List of WP_Block arrays or an empty array if `$block` cannot be found within `$block_list`. */ private function get_block_path( $block, $block_list, $iterations = 0 ) { foreach ( $block_list as $_block ) { // Found the block. if ( $block === $_block ) { return array( $block ); } // No innerblocks, proceed to the next block. if ( empty( $_block['innerBlocks'] ) ) { continue; } // Look in innerblocks for the block. foreach ( $_block['innerBlocks'] as $inner_block ) { // The inner block needs to be merged to the path. $to_merge = array( $inner_block ); if ( $block === $inner_block ) { // Inner block is the one we're looking for. $path = array( $block ); $to_merge = array(); // Inner block equals the path, no need to merge it. } else { $path = $this->get_block_path( $block, array( $inner_block ), $iterations + 1 ); } if ( $path ) { // First iteration, append first block too. if ( ! $iterations ) { $to_merge[] = $_block; } // Merge. return array_merge( $path, $to_merge ); } } } // Block not found in the list. return array(); } /** * Returns a filtered version of `$block_list` containing only the passed `$block` and its parents. * * @since 5.1.0 * * @param array $block Parsed block array. * @param array[] $block_list The list of WP Block array `$block` comes from. * @return array[] Filtered version of `$block_list` containing only the passed `$block` and its parents. * Or an empty array if `$block` cannot be found within `$block_list`. */ private function get_block_tree( $block, $block_list ) { foreach ( $block_list as &$_block ) { // Found the block. if ( $block === $_block ) { return array( $block ); } if ( ! empty( $_block['innerBlocks'] ) ) { $tree = $this->get_block_tree( $block, $_block['innerBlocks'] ); } if ( ! empty( $tree ) ) { // Break as soon as the desired block is removed from one of the innerBlocks. if ( $_block['innerBlocks'] !== $tree ) { // Update innerBlocks/innerContent structure if needed. $_block['innerBlocks'] = $tree; // Update innerContent to reflect the innerBlocks changes = only 1 innerBlock. $inner_block_in_content_index = 0; foreach ( $_block['innerContent'] as $index => $chunk ) { if ( ! is_string( $chunk ) && $inner_block_in_content_index++ ) { unset( $_block['innerContent'][ $index ] ); } } // Re-index. $_block['innerContent'] = array_values( $_block['innerContent'] ); } return array( $_block ); } } return array(); } /** * Installation function to install core forms. * * @since 5.0.0 * * @param bool $recreate Whether or not to recreate an existing form. This is passed to `LLMS_Forms::create()`. * @return WP_Post[] Array of created posts. Array key is the location id and array value is the WP_Post object. */ public function install( $recreate = false ) { $installed = array(); foreach ( array_keys( $this->get_locations() ) as $location ) { $installed[ $location ] = $this->create( $location, $recreate ); } return $installed; } /** * Determines if a location is a valid & registered form location * * @since 5.0.0 * * @param string $location The location id. * @return boolean */ public function is_location_valid( $location ) { return in_array( $location, array_keys( $this->get_locations() ), true ); } /** * Loads reusable blocks into a block list * * By default, a reusable block contains a reference to the block post (which will be * loaded during rendering). This is problematic for us since we want to review then * entire block list so we can see all fields for validation purposes and so on. * * This function will replace each reusable block with the parsed blocks * from it's reference post. * * @since 5.0.0 * @since 5.1.0 Access turned to public. * * @param array[] $blocks List of WP_Block arrays. * @return array[] */ public function load_reusable_blocks( $blocks ) { $loaded = array(); foreach ( $blocks as $index => $block ) { if ( 'core/block' === $block['blockName'] ) { $post = get_post( $block['attrs']['ref'] ); if ( ! $post || 'publish' !== get_post_status( $post ) ) { continue; } $loaded = array_merge( $loaded, $this->parse_blocks( $post->post_content ) ); continue; } if ( $block['innerBlocks'] ) { $block['innerBlocks'] = $this->load_reusable_blocks( $block['innerBlocks'] ); } $loaded[] = $block; } return $loaded; } /** * Load form autosaves when previewing a form * * @since 5.0.0 * * @param WP_Post|boolean $post WP_Post object for the llms_form post or `false` if no form found. * @return WP_Post|boolean */ public function maybe_load_preview( $post ) { // No form post found. if ( ! is_object( $post ) ) { return $post; } // The `_set_preview()` method is marked as private but has existed since 2.7 and my guess is that we can use this safely. if ( ! function_exists( '_set_preview' ) ) { return $post; } $is_preview = ( is_preview() && current_user_can( $this->get_capability(), $post->ID ) ); return $is_preview ? _set_preview( $post ) : $post; } /** * Parse the post_content of a form into a list of WP_Block arrays. * * This method parses the blocks, loads block data from any reusable blocks, * and cascades visibility attributes onto a block's innerBlocks. * * @since 5.0.0 * * @param string $content Post content HTML. * @return array[] Array of parsed block arrays. */ public function parse_blocks( $content ) { $blocks = parse_blocks( $content ); $blocks = $this->load_reusable_blocks( $blocks ); $blocks = $this->cascade_visibility_attrs( $blocks ); return $blocks; } /** * Modifies a field for usage in the "free enrollment" checkout form * * If the block is not visible (according to LLMS block-level visibility settings) * it will return an empty array (signaling the field to be removed). * * Otherwise the block will be converted to a hidden field. * * This method is a filter callback and is intended for internal use only. * * Backwards incompatible changes and/or method removal may occur without notice. * * @since 5.0.0 * @since [versino] Added `$block_list` param. * @access private * * @param array $attrs LLMS_Form_Field settings array for the field. * @param array $block WP_Block settings array. * @param array[] $block_list The list of WP Block array `$block` comes from. * @return array */ public function prepare_field_for_free_enroll_form( $attrs, $block, $block_list ) { if ( ! $this->is_block_visible_in_list( $block, $block_list ) ) { return array(); } $attrs['type'] = 'hidden'; return $attrs; } /** * Render form field blocks. * * @since 5.0.0 * @since 5.9.0 Pass an empty string to `strpos()` instead of `null`. * * @param string $html Block HTML. * @param array $block Array of block information. * @return string */ public function render_field_block( $html, $block ) { // Return HTML for any non llms/form-field blocks. if ( false === strpos( $block['blockName'] ?? '', 'llms/form-field-' ) ) { return $html; } if ( ! empty( $block['innerBlocks'] ) ) { $inner_blocks = array_map( 'render_block', $block['innerBlocks'] ); return implode( "\n", $inner_blocks ); } $attrs = $this->block_to_field_settings( $block ); return llms_form_field( $attrs, false ); } } return LLMS_Forms::instance();