Files
hub-insurance/wp-content/plugins/generateblocks-pro/includes/pattern-library/class-pattern-library-rest.php
T
2026-07-02 15:54:39 -06:00

2096 lines
58 KiB
PHP

<?php
/**
* The Pattern library rest class file.
*
* @package GenerateBlocksPro\Pattern_Library
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( class_exists( 'GenerateBlocks_Pattern_Library_Rest' ) ) :
/**
* Main class for the Pattern Library Rest functions.
*
* @since 1.9
*/
class GenerateBlocks_Pro_Pattern_Library_Rest extends GenerateBlocks_Pro_Singleton {
private const ENDPOINT_GLOBAL_STYLE_DATA = 'wp-json/generateblocks-pro/v1/pattern-library/provide-global-style-data';
private const ENDPOINT_FORM_DATA = 'wp-json/generateblocks-pro/v1/pattern-library/provide-form-data';
private const ENDPOINT_LIBRARY_BY_KEY = 'wp-json/generateblocks-pro/v1/pattern-library/get-library-by-public-key';
/**
* Initialize all hooks.
*
* @return void
*/
public function init() {
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}
/**
* Register the REST routes.
*
* @return void
*/
public function register_routes(): void {
$namespace = 'generateblocks-pro/v1';
register_rest_route(
$namespace,
'/pattern-library/categories',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'list_categories' ),
'permission_callback' => array( $this, 'can_list_patterns' ),
)
);
register_rest_route(
$namespace,
'/pattern-library/patterns',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'list_patterns' ),
'permission_callback' => array( $this, 'can_list_patterns' ),
)
);
register_rest_route(
$namespace,
'/pattern-library/get-global-style-data',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_global_style_data' ),
'permission_callback' => function() {
return apply_filters(
'generateblocks_can_view_pattern_library',
$this->edit_posts()
);
},
)
);
register_rest_route(
$namespace,
'/pattern-library/provide-global-style-data',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'provide_global_style_data' ),
'permission_callback' => array( $this, 'can_list_patterns' ),
)
);
register_rest_route(
$namespace,
'/pattern-library/provide-form-data',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'provide_form_data' ),
'permission_callback' => array( $this, 'can_list_patterns' ),
)
);
register_rest_route(
$namespace,
'/pattern-library/import-styles',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'import_styles' ),
'permission_callback' => array( $this, 'can_manage_classes' ),
)
);
register_rest_route(
$namespace,
'/pattern-library/import-forms',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'import_forms' ),
'permission_callback' => array( $this, 'can_import_forms' ),
)
);
register_rest_route(
$namespace,
'/pattern-library/get-library-by-public-key',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_library_by_public_key' ),
'permission_callback' => array( $this, 'has_valid_key' ),
)
);
register_rest_route(
$namespace,
'/pattern-library/add-library',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'add_library' ),
'permission_callback' => array( $this, 'can_manage_classes' ),
)
);
}
/**
* Check to see if we can import components.
*/
public function can_manage_classes(): bool {
return current_user_can( 'manage_options' );
}
/**
* Check to see if the user can import form dependencies for patterns.
*
* @param WP_REST_Request $request The request.
*/
public function can_import_forms( WP_REST_Request $request ): bool {
if ( ! $this->forms_are_available() ) {
return false;
}
// Importing a form creates a published `gblocks_pro_form` post,
// which is a public submission endpoint. Gate behind the same
// `manage` cap the Forms CPT uses, so this route can't be used
// to bypass the standard Forms admin permission model.
$can_import = class_exists( 'GenerateBlocks_Pro_Form_Post_Type' )
&& GenerateBlocks_Pro_Form_Post_Type::current_user_can( 'manage' );
/** This filter is documented in includes/functions.php */
return (bool) apply_filters(
'generateblocks_pattern_library_can_import_forms',
$can_import,
$request
);
}
/**
* Check to see if the user can edit this post.
*/
public function edit_posts(): bool {
return current_user_can( 'edit_posts' );
}
/**
* Check to see if the user can view patterns.
*
* @param WP_REST_Request $request The request.
*
* @return bool
*/
public function can_list_patterns( WP_REST_Request $request ): bool {
$is_local = $request->get_param( 'isLocal' );
if ( $is_local ) {
$current_host = $_SERVER['HTTP_HOST'] ?? '';
$request_host = $request->get_header( 'host' );
return $current_host === $request_host
? $this->edit_posts()
: false;
}
return $this->has_valid_key( $request );
}
/**
* Check to see if the user has a valid public key.
*
* @param WP_REST_Request $request The request.
*
* @return bool
*/
public function has_valid_key( WP_REST_Request $request ): bool {
$public_key = $request->get_header( 'X-GB-Public-Key' );
if ( is_null( $public_key ) || ! class_exists( 'GenerateBlocks_Pro_Pattern_Library' ) ) {
return false;
}
return null !== GenerateBlocks_Pro_Pattern_Library::get_instance()
->get_pattern_permissions_by_public_key( $public_key );
}
/**
* Sanitize a scalar REST request value.
*
* @param mixed $value The request value.
*
* @return string
*/
private function sanitize_request_string( $value ): string {
if ( is_array( $value ) || is_object( $value ) ) {
return '';
}
return sanitize_text_field( (string) $value );
}
/**
* Return the empty global style data shape expected by the editor.
*
* @return array
*/
private function empty_global_style_data(): array {
return [
'css' => '',
'styles' => [],
];
}
/**
* Validate and normalize remote global style data.
*
* @param mixed $data The decoded response data.
*
* @return array|false
*/
private function normalize_global_style_data( $data ) {
if ( ! is_array( $data ) ) {
return false;
}
if ( ! isset( $data['css'] ) || ! is_string( $data['css'] ) ) {
return false;
}
if ( isset( $data['styles'] ) && ! is_array( $data['styles'] ) ) {
return false;
}
return [
'css' => $data['css'],
'styles' => array_values( $data['styles'] ?? [] ),
];
}
/**
* Get a saved remote library from the request id.
*
* @param WP_REST_Request $request The request.
*
* @return GenerateBlocks_Library_DTO|null
*/
private function get_remote_library_from_request( WP_REST_Request $request ) {
$id = $this->sanitize_request_string( $request->get_param( 'id' ) );
if ( '' === $id || ! class_exists( 'GenerateBlocks_Libraries' ) ) {
return null;
}
$library = GenerateBlocks_Libraries::get_instance()->get_one( $id );
if ( ! $library instanceof GenerateBlocks_Library_DTO ) {
return null;
}
$is_local = (bool) $library->is_local;
$domain = (string) $library->domain;
$public_key = (string) $library->public_key;
if ( $is_local || '' === $domain || '' === $public_key ) {
return null;
}
return $library;
}
/**
* Validate a remote library endpoint URL.
*
* @param string $url The endpoint URL.
*
* @return string
*/
private function validate_remote_endpoint_url( string $url ): string {
$url = esc_url_raw( $url );
if ( '' === $url ) {
return '';
}
$parts = wp_parse_url( $url );
if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) {
return '';
}
if ( ! empty( $parts['user'] ) || ! empty( $parts['pass'] ) ) {
return '';
}
if ( ! in_array( strtolower( $parts['scheme'] ), [ 'http', 'https' ], true ) ) {
return '';
}
return $url;
}
/**
* Fetch a remote pattern-library endpoint, following redirects manually.
*
* Provider URLs are saved by administrators. Lower-privileged requests
* receive a library id and resolve the URL from stored library data.
*
* Use wp_safe_remote_get() for WordPress' SSRF URL validation instead of
* maintaining our own host/IP deny list. Automatic redirects stay disabled
* so each Location hop is reduced back to the provider base and rebuilt
* against the known GenerateBlocks endpoint before the next safe request.
* This preserves normal provider moves while keeping the final path under
* our control. Intentional private/local providers can opt in through the
* core HTTP API filters documented for wp_http_validate_url().
*
* Redirects can move the provider's domain or WordPress subdirectory, but
* they do not get to choose the final request path: we always rebuild the
* known pattern-library endpoint on the redirected base before issuing the
* next hop.
*
* @param string $base_url Provider base URL.
* @param string $endpoint_path Known endpoint path.
* @param array $args HTTP request arguments.
* @param array $query_args Query arguments to append.
*
* @return array|WP_Error
*/
private function fetch_remote_endpoint( string $base_url, string $endpoint_path, array $args = [], array $query_args = [] ) {
$url = $this->build_remote_endpoint_url( $base_url, $endpoint_path, $query_args );
if ( '' === $url ) {
return new WP_Error(
'generateblocks_pattern_library_invalid_url',
__( 'Invalid pattern library URL.', 'generateblocks-pro' )
);
}
$args['redirection'] = 0;
$max_hops = 5;
for ( $hop = 0; $hop <= $max_hops; $hop++ ) {
$response = wp_safe_remote_get( $url, $args );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code < 300 || $code >= 400 ) {
return $response;
}
$location = wp_remote_retrieve_header( $response, 'location' );
if ( is_array( $location ) ) {
$location = end( $location );
}
if ( ! is_string( $location ) || '' === trim( $location ) ) {
return $response;
}
if ( $hop === $max_hops ) {
return new WP_Error(
'generateblocks_pattern_library_too_many_redirects',
__( 'Too many pattern library redirects.', 'generateblocks-pro' )
);
}
$redirect_base_url = $this->derive_remote_base_url_from_redirect( $location, $url, $endpoint_path );
if ( '' === $redirect_base_url ) {
return new WP_Error(
'generateblocks_pattern_library_invalid_redirect',
__( 'Invalid pattern library redirect.', 'generateblocks-pro' )
);
}
$url = $this->build_remote_endpoint_url( $redirect_base_url, $endpoint_path, $query_args );
if ( '' === $url ) {
return new WP_Error(
'generateblocks_pattern_library_invalid_redirect',
__( 'Invalid pattern library redirect.', 'generateblocks-pro' )
);
}
}
return new WP_Error(
'generateblocks_pattern_library_redirect_loop',
__( 'Pattern library redirect loop detected.', 'generateblocks-pro' )
);
}
/**
* Build a known pattern-library endpoint URL from a provider base URL.
*
* @param string $base_url Provider base URL.
* @param string $endpoint_path Known endpoint path.
* @param array $query_args Query arguments to append.
*
* @return string
*/
private function build_remote_endpoint_url( string $base_url, string $endpoint_path, array $query_args = [] ): string {
$url = $this->validate_remote_endpoint_url(
trailingslashit( $base_url ) . ltrim( $endpoint_path, '/' )
);
if ( '' === $url ) {
return '';
}
if ( ! empty( $query_args ) ) {
$url = add_query_arg( $query_args, $url );
}
return $this->validate_remote_endpoint_url( $url );
}
/**
* Derive the next provider base URL from a redirect target.
*
* Redirects may move the provider domain or WordPress subdirectory, but
* they do not get to choose the final API path. We only keep the path
* prefix when the redirect target already contains the known endpoint.
*
* @param string $location Redirect location header.
* @param string $current_url URL that produced the redirect.
* @param string $endpoint_path Known endpoint path.
*
* @return string
*/
private function derive_remote_base_url_from_redirect( string $location, string $current_url, string $endpoint_path ): string {
$redirect_url = $this->validate_remote_endpoint_url(
WP_Http::make_absolute_url( $location, $current_url )
);
if ( '' === $redirect_url ) {
return '';
}
$parts = wp_parse_url( $redirect_url );
if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) {
return '';
}
$base_url = strtolower( $parts['scheme'] ) . '://' . $parts['host'];
if ( isset( $parts['port'] ) ) {
$base_url .= ':' . (int) $parts['port'];
}
$path = isset( $parts['path'] ) && is_string( $parts['path'] ) ? $parts['path'] : '';
$endpoint_path = '/' . ltrim( $endpoint_path, '/' );
$endpoint_pos = strpos( $path, $endpoint_path );
if ( false !== $endpoint_pos ) {
$base_path = substr( $path, 0, $endpoint_pos );
$base_url .= '/' . trim( $base_path, '/' );
}
return rtrim( $base_url, '/' );
}
/**
* Returns a list of categories.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function list_categories( WP_REST_Request $request ): WP_REST_Response {
$public_key = $request->get_header( 'X-GB-Public-Key' );
$library_id = $request->get_param( 'libraryId' );
$instance = GenerateBlocks_Pro_Pattern_Library::get_instance();
$allowed_collections = $instance->get_collections_by_public_key( $public_key );
if ( is_null( $allowed_collections ) ) {
$allowed_collections = get_terms(
[
'taxonomy' => 'gblocks_pattern_collections',
'slug' => $library_id,
'fields' => 'ids',
]
);
}
if ( empty( $allowed_collections ) || is_wp_error( $allowed_collections ) ) {
return $this->success( array() );
}
// Only categories that are attached to a pattern in one of the
// allowed collections — keeps category names from leaking across keys.
$pattern_ids = get_posts(
array(
'post_type' => 'wp_block',
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'tax_query' => array(
array(
'taxonomy' => 'gblocks_pattern_collections',
'field' => 'term_id',
'terms' => $allowed_collections,
),
),
)
);
if ( empty( $pattern_ids ) ) {
return $this->success( array() );
}
$term_ids = wp_get_object_terms(
$pattern_ids,
'wp_pattern_category',
array( 'fields' => 'ids' )
);
if ( empty( $term_ids ) || is_wp_error( $term_ids ) ) {
return $this->success( array() );
}
$terms = get_terms(
array(
'taxonomy' => 'wp_pattern_category',
'include' => array_unique( array_map( 'intval', $term_ids ) ),
)
);
if ( is_wp_error( $terms ) ) {
return $this->success( array() );
}
$data = array_map(
function( WP_Term $term ) {
return array(
'id' => $term->term_id,
'name' => $term->name,
);
},
$terms
);
return $this->success( $data );
}
/**
* Returns a list of patterns.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function list_patterns( WP_REST_Request $request ): WP_REST_Response {
$public_key = $request->get_header( 'X-GB-Public-Key' );
$library_id = $request->get_param( 'libraryId' );
$search = $request->get_param( 'search' );
$category_id = $request->get_param( 'categoryId' );
$cat_query = isset( $category_id ) && '' !== $category_id ? array(
'taxonomy' => 'wp_pattern_category',
'field' => 'term_id',
'terms' => $category_id,
) : null;
$instance = GenerateBlocks_Pro_Pattern_Library::get_instance();
$allowed_collections = $instance->get_collections_by_public_key( $public_key );
// If the key is not attached to any collection.
if ( is_null( $allowed_collections ) ) {
$allowed_collections = get_terms(
[
'taxonomy' => 'gblocks_pattern_collections',
'slug' => $library_id,
'fields' => 'ids',
]
);
if ( ! $allowed_collections ) {
return $this->success( array() );
}
}
// Defensive cast against a non-numeric or negative filter return
// (a misconfigured `-1` would scan the entire `wp_block` table).
// Sites that intentionally raise the count above the default
// keep that exact value — no upper clamp.
$pattern_count = apply_filters( 'generateblocks_pattern_library_count', 250 );
$pattern_count = is_numeric( $pattern_count ) ? (int) $pattern_count : 250;
$pattern_count = max( 1, $pattern_count );
$posts = get_posts(
array(
'post_type' => 'wp_block',
// `get_posts()` defaults `post_status` to 'publish' for
// non-attachment types, but stating it explicitly matches
// sibling routes (list_categories, provide_global_style_data)
// and protects against any future override of the default.
'post_status' => 'publish',
'posts_per_page' => $pattern_count, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page -- Filter is opt-in; result is cached on the consumer.
's' => $search,
'tax_query' => array(
$cat_query,
array(
'taxonomy' => 'gblocks_pattern_collections',
'field' => 'term_id',
'terms' => $allowed_collections,
),
),
)
);
$data = array_reduce(
$posts,
function( $patterns, $post ) use ( $category_id ) {
$post_patterns = get_post_meta( $post->ID, 'generateblocks_patterns_tree', true );
// Don't return patterns with no tree.
if ( ! $post_patterns ) {
return $patterns;
}
// Filter patterns by category.
$filter_patterns = array_filter(
$post_patterns,
function( $pattern ) use ( $category_id ) {
if ( isset( $category_id ) && '' !== $category_id ) {
return in_array( $category_id, $pattern['categories'] );
}
return true;
}
);
$form_selector_cache = [];
$filter_patterns = array_map(
function( $pattern ) use ( &$form_selector_cache ) {
if ( ! is_array( $pattern ) ) {
return $pattern;
}
$pattern['formRefs'] = array_values(
array_filter(
array_map( 'absint', (array) ( $pattern['formRefs'] ?? [] ) )
)
);
if ( empty( $pattern['formRefs'] ) && is_string( $pattern['pattern'] ?? null ) ) {
$pattern['formRefs'] = $this->collect_form_refs_from_blocks(
parse_blocks( wp_unslash( $pattern['pattern'] ) )
);
}
if ( ! empty( $pattern['formRefs'] ) ) {
$global_style_selectors = is_array( $pattern['globalStyleSelectors'] ?? null )
? $pattern['globalStyleSelectors']
: [];
$pattern['globalStyleSelectors'] = array_values(
array_unique(
array_filter(
array_merge(
$global_style_selectors,
$this->get_current_form_global_style_selectors( $pattern['formRefs'], $form_selector_cache )
),
static function( $selector ) {
return is_string( $selector ) && '' !== $selector;
}
)
)
);
}
return $pattern;
},
$filter_patterns
);
return array_merge(
$patterns,
$filter_patterns
);
},
[]
);
return $this->success( $data );
}
/**
* Returns a list of required classes from the provider.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function get_global_style_data( WP_REST_Request $request ): WP_REST_Response {
$library = $this->get_remote_library_from_request( $request );
if ( is_null( $library ) ) {
return $this->failed( __( 'Invalid pattern library.', 'generateblocks-pro' ) );
}
$cache_key = $library->id . '_global-style-data';
$cache = GenerateBlocks_Libraries::get_cached_data( $cache_key );
$cache = $this->normalize_global_style_data( $cache );
if ( false !== $cache ) {
return $this->success( $cache );
}
$endpoint = $this->build_remote_endpoint_url(
(string) $library->domain,
self::ENDPOINT_GLOBAL_STYLE_DATA
);
if ( '' === $endpoint ) {
return $this->failed( __( 'Invalid pattern library domain.', 'generateblocks-pro' ) );
}
$response = $this->fetch_remote_endpoint(
(string) $library->domain,
self::ENDPOINT_GLOBAL_STYLE_DATA,
[
'headers' => array(
'X-GB-Public-Key' => sanitize_text_field( $library->public_key ),
),
'timeout' => 15,
]
);
if ( is_wp_error( $response ) ) {
return $this->failed( __( 'Unable to request required classes.', 'generateblocks-pro' ) );
}
$response_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $response_code ) {
return $this->failed( __( 'Unable to request required classes.', 'generateblocks-pro' ) );
}
$body = wp_remote_retrieve_body( $response );
$body = json_decode( $body, true );
if ( ! is_array( $body ) ) {
return $this->failed( __( 'Invalid required classes response.', 'generateblocks-pro' ) );
}
$data = $this->normalize_global_style_data( $body['response']['data'] ?? null );
if ( false === $data ) {
return $this->failed( __( 'Invalid required classes response.', 'generateblocks-pro' ) );
}
// Cache our data.
GenerateBlocks_Libraries::set_cached_data( $data, $cache_key );
return $this->success( $data );
}
/**
* Returns an array of class data.
* This function is called on the providers server.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function provide_global_style_data( WP_REST_Request $request ): WP_REST_Response {
$data = $this->empty_global_style_data();
$public_key = $this->sanitize_request_string( $request->get_header( 'X-GB-Public-Key' ) );
$permissions = GenerateBlocks_Pro_Pattern_Library::get_instance()
->get_pattern_permissions_by_public_key( $public_key );
if ( is_null( $permissions ) ) {
return $this->success( $data );
}
$selectors = $this->get_global_style_selectors_by_collections( $permissions['includes'] );
if ( empty( $selectors ) ) {
return $this->success( $data );
}
$selector_map = array_fill_keys( $selectors, true );
$style_count = apply_filters( 'generateblocks_styles_posts_per_page', 5000 );
$style_count = is_numeric( $style_count ) ? (int) $style_count : 5000;
$style_count = max( 1, $style_count );
$posts = get_posts(
[
'post_type' => 'gblocks_styles',
'post_status' => 'publish',
'posts_per_page' => $style_count, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page -- Defensive cast above; filter is opt-in.
'order' => 'ASC',
'orderby' => 'menu_order',
]
);
foreach ( $posts as $post ) {
$class_name = get_post_meta( $post->ID, 'gb_style_selector', true );
$styles = get_post_meta( $post->ID, 'gb_style_data', true );
$css = get_post_meta( $post->ID, 'gb_style_css', true );
if ( ! is_string( $class_name ) || empty( $selector_map[ $class_name ] ) ) {
continue;
}
if ( ! is_array( $styles ) ) {
$styles = [];
}
if ( ! is_string( $css ) ) {
$css = '';
}
$data['css'] .= $css;
$data['styles'][] = [
'title' => $class_name,
'className' => $class_name,
'styles' => wp_json_encode( $styles ),
'css' => $css,
];
}
return $this->success( $data );
}
/**
* Returns safe form data referenced by allowed patterns.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function provide_form_data( WP_REST_Request $request ): WP_REST_Response {
$data = [ 'forms' => [] ];
if ( ! $this->forms_are_available() ) {
return $this->success( $data );
}
$form_ids = $this->validate_form_ids_payload( $request->get_param( 'formIds' ) );
if ( is_wp_error( $form_ids ) || empty( $form_ids ) ) {
return $this->success( $data );
}
$public_key = $this->sanitize_request_string( $request->get_header( 'X-GB-Public-Key' ) );
$permissions = GenerateBlocks_Pro_Pattern_Library::get_instance()
->get_pattern_permissions_by_public_key( $public_key );
if ( is_null( $permissions ) ) {
return $this->success( $data );
}
$allowed_form_ids = $this->get_form_refs_by_collections( $permissions['includes'] );
if ( empty( $allowed_form_ids ) ) {
return $this->success( $data );
}
$allowed_form_ids = array_fill_keys( $allowed_form_ids, true );
foreach ( $form_ids as $form_id ) {
if ( empty( $allowed_form_ids[ $form_id ] ) ) {
continue;
}
$form = get_post( $form_id );
if (
! $form ||
GenerateBlocks_Pro_Form_Post_Type::POST_TYPE !== $form->post_type ||
'publish' !== $form->post_status
) {
continue;
}
$meta = get_post_meta( $form_id, GenerateBlocks_Pro_Form_Post_Type::META_KEY, true );
$config = is_array( $meta ) && is_array( $meta['config'] ?? null ) ? $meta['config'] : [];
$safe_config = $this->normalize_import_form_config( $config );
if ( is_wp_error( $safe_config ) ) {
continue;
}
$data['forms'][] = [
'sourceId' => $form_id,
'title' => sanitize_text_field( $form->post_title ),
'content' => (string) $form->post_content,
'config' => $safe_config,
];
}
return $this->success( $data );
}
/**
* Import remote forms needed by selected patterns.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function import_forms( WP_REST_Request $request ): WP_REST_Response {
if ( ! $this->forms_are_available() ) {
return $this->failed( __( 'Forms are not available on this site.', 'generateblocks-pro' ) );
}
$form_ids = $this->validate_form_ids_payload( $request->get_param( 'formIds' ) );
if ( is_wp_error( $form_ids ) ) {
return $this->error( 400, $form_ids->get_error_message() );
}
if ( empty( $form_ids ) ) {
return $this->success(
[
'idMap' => [],
'imported' => [],
]
);
}
$library = $this->get_remote_library_from_request( $request );
if ( is_null( $library ) ) {
return $this->failed( __( 'Invalid pattern library.', 'generateblocks-pro' ) );
}
$response = $this->fetch_remote_endpoint(
(string) $library->domain,
self::ENDPOINT_FORM_DATA,
[
'headers' => [
'X-GB-Public-Key' => sanitize_text_field( $library->public_key ),
],
'timeout' => 15,
],
[
'formIds' => implode( ',', $form_ids ),
]
);
if ( is_wp_error( $response ) ) {
return $this->failed( __( 'Unable to request required forms.', 'generateblocks-pro' ) );
}
if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
return $this->failed( __( 'Unable to request required forms.', 'generateblocks-pro' ) );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $body ) ) {
return $this->failed( __( 'Invalid required forms response.', 'generateblocks-pro' ) );
}
$forms = $this->normalize_import_forms_payload( $body['response']['data'] ?? null, $form_ids );
if ( is_wp_error( $forms ) ) {
return $this->error( 400, $forms->get_error_message() );
}
$id_map = [];
$imported = [];
foreach ( $form_ids as $source_id ) {
if ( ! isset( $forms[ $source_id ] ) ) {
$this->rollback_imported_forms( $imported );
return $this->failed( __( 'A required form was not available from the pattern library.', 'generateblocks-pro' ) );
}
$local_id = $this->insert_imported_form( $forms[ $source_id ] );
if ( is_wp_error( $local_id ) ) {
$this->rollback_imported_forms( $imported );
return $this->error( 400, $local_id->get_error_message() );
}
$id_map[ $source_id ] = $local_id;
$imported[] = $local_id;
}
return $this->success(
[
'idMap' => $id_map,
'imported' => $imported,
]
);
}
/**
* Check whether the Forms system is available.
*
* @return bool
*/
private function forms_are_available(): bool {
return class_exists( 'GenerateBlocks_Pro_Form_Post_Type' ) &&
class_exists( 'GenerateBlocks_Pro_Form_Rest' );
}
/**
* Validate form IDs from REST input.
*
* @param mixed $form_ids Raw form IDs.
*
* @return array|WP_Error
*/
private function validate_form_ids_payload( $form_ids ) {
if ( is_string( $form_ids ) ) {
$form_ids = explode( ',', $form_ids );
}
if ( ! is_array( $form_ids ) ) {
return new WP_Error(
'invalid_form_ids_payload',
__( 'Invalid form IDs payload.', 'generateblocks-pro' )
);
}
$form_ids = array_values(
array_filter(
array_unique(
array_map( 'absint', $form_ids )
)
)
);
sort( $form_ids, SORT_NUMERIC );
$max_forms = apply_filters( 'generateblocks_pattern_library_max_form_imports', 50 );
$max_forms = is_numeric( $max_forms ) ? max( 1, (int) $max_forms ) : 50;
if ( count( $form_ids ) > $max_forms ) {
return new WP_Error(
'too_many_form_ids',
__( 'Too many forms requested.', 'generateblocks-pro' )
);
}
return $form_ids;
}
/**
* Validate and normalize remote form data.
*
* @param mixed $data Requested data.
* @param array $requested_ids Requested source form IDs.
*
* @return array|WP_Error
*/
private function normalize_import_forms_payload( $data, array $requested_ids ) {
if ( ! is_array( $data ) || ! is_array( $data['forms'] ?? null ) ) {
return new WP_Error(
'invalid_forms_payload',
__( 'Invalid forms payload.', 'generateblocks-pro' )
);
}
$requested_ids = array_fill_keys( $requested_ids, true );
$forms = [];
foreach ( $data['forms'] as $form ) {
if (
! is_array( $form ) ||
! is_string( $form['content'] ?? null ) ||
! is_array( $form['config'] ?? null )
) {
return new WP_Error(
'invalid_form_payload',
__( 'Invalid form item.', 'generateblocks-pro' )
);
}
$source_id = absint( $form['sourceId'] ?? 0 );
if ( $source_id <= 0 || empty( $requested_ids[ $source_id ] ) ) {
continue;
}
$safe_config = $this->normalize_import_form_config( $form['config'] );
if ( is_wp_error( $safe_config ) ) {
return $safe_config;
}
$forms[ $source_id ] = [
'sourceId' => $source_id,
'title' => sanitize_text_field( $form['title'] ?? '' ),
'content' => $form['content'],
'config' => $safe_config,
];
}
return $forms;
}
/**
* Normalize form config for safe library transport.
*
* @param mixed $config Raw form config.
*
* @return array|WP_Error
*/
private function normalize_import_form_config( $config ) {
$config = is_array( $config ) ? $config : [];
$as_text = static function( $key ) use ( $config ) {
return isset( $config[ $key ] ) && is_scalar( $config[ $key ] )
? (string) $config[ $key ]
: '';
};
$raw_actions = [];
$actions = [];
$allowed_actions = [ 'email', 'email-signup', 'confirmation-email' ];
foreach ( (array) ( $config['actions'] ?? [] ) as $action ) {
$action = is_string( $action ) ? sanitize_key( $action ) : '';
if ( '' === $action || in_array( $action, $raw_actions, true ) ) {
continue;
}
$raw_actions[] = $action;
if ( in_array( $action, $allowed_actions, true ) ) {
$actions[] = $action;
}
}
$unsupported_actions = array_values( array_diff( $raw_actions, $allowed_actions ) );
if ( ! empty( $unsupported_actions ) ) {
return new WP_Error(
'gb_form_import_unsupported_action',
__( 'Imported forms include an unsupported action.', 'generateblocks-pro' )
);
}
$form_type = sanitize_key( $as_text( 'formType' ) );
$form_type = in_array( $form_type, [ 'contact', 'email-signup', 'contact-email-signup' ], true ) ? $form_type : '';
if ( empty( $actions ) ) {
$actions = 'email-signup' === $form_type ? [ 'email-signup' ] : [ 'email' ];
}
$settings = [];
$source_settings = is_array( $config['actionSettings'] ?? null ) ? $config['actionSettings'] : [];
$email_signup_settings = is_array( $source_settings['email-signup'] ?? null )
? $source_settings['email-signup']
: [];
$has_signup_opt_in = 'contact-email-signup' === $form_type
&& isset( $email_signup_settings['optInField'] )
&& is_scalar( $email_signup_settings['optInField'] );
if ( in_array( 'email-signup', $actions, true ) ) {
$settings['email-signup'] = [
'emailField' => sanitize_key( $email_signup_settings['emailField'] ?? '' ),
'nameField' => sanitize_key( $email_signup_settings['nameField'] ?? '' ),
'optInField' => $has_signup_opt_in
? sanitize_key( (string) $email_signup_settings['optInField'] )
: '',
];
}
$normalized = [
'actions' => array_values( array_unique( $actions ) ),
'actionSettings' => $settings,
'formType' => $form_type,
'emailTo' => '',
'emailFromName' => '',
'emailFromEmail' => '',
'emailSubject' => sanitize_text_field( $as_text( 'emailSubject' ) ),
'emailReplyToField' => sanitize_key( $as_text( 'emailReplyToField' ) ),
'confirmationEmailField' => sanitize_key( $as_text( 'confirmationEmailField' ) ),
'confirmationEmailSubject' => sanitize_text_field( $as_text( 'confirmationEmailSubject' ) ),
'confirmationEmailBody' => sanitize_textarea_field( $as_text( 'confirmationEmailBody' ) ),
'confirmationEmailReplyToEmail' => '',
'confirmationEmailReplyToName' => sanitize_text_field( $as_text( 'confirmationEmailReplyToName' ) ),
'useTurnstile' => false,
'successMessage' => sanitize_text_field( $as_text( 'successMessage' ) ),
'errorMessage' => sanitize_text_field( $as_text( 'errorMessage' ) ),
'redirectUrl' => '',
];
$meta = GenerateBlocks_Pro_Form_Post_Type::sanitize_meta( [ 'config' => $normalized ] );
$safe_config = $meta['config'] ?? $normalized;
// This is a transport payload, not final live form meta. Provider and
// destination IDs are intentionally stripped above, so preserve the
// vetted action list after live-form sanitization removes provider-less
// email-signup actions.
$safe_config['actions'] = $normalized['actions'];
return $safe_config;
}
/**
* Get form IDs referenced by allowed pattern collections.
*
* @param array $collections The allowed collection term IDs.
*
* @return array
*/
private function get_form_refs_by_collections( array $collections ): array {
if ( empty( $collections ) ) {
return [];
}
$pattern_count = apply_filters( 'generateblocks_pattern_library_count', 250 );
$pattern_count = is_numeric( $pattern_count ) ? (int) $pattern_count : 250;
$pattern_count = max( 1, $pattern_count );
$posts = get_posts(
[
'post_type' => 'wp_block',
'post_status' => 'publish',
'posts_per_page' => $pattern_count, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page -- Defensive cast above; filter is opt-in.
'tax_query' => [
[
'taxonomy' => 'gblocks_pattern_collections',
'field' => 'term_id',
'terms' => $collections,
],
],
]
);
if ( ! is_array( $posts ) ) {
return [];
}
$form_ids = [];
foreach ( $posts as $post ) {
if ( ! isset( $post->ID ) ) {
continue;
}
$patterns = get_post_meta( $post->ID, 'generateblocks_patterns_tree', true );
if ( ! is_array( $patterns ) ) {
continue;
}
foreach ( $patterns as $pattern ) {
if ( ! is_array( $pattern ) ) {
continue;
}
$refs = array_values(
array_filter(
array_map( 'absint', (array) ( $pattern['formRefs'] ?? [] ) )
)
);
if ( empty( $refs ) && is_string( $pattern['pattern'] ?? null ) ) {
$refs = $this->collect_form_refs_from_blocks( parse_blocks( wp_unslash( $pattern['pattern'] ) ) );
}
foreach ( $refs as $ref ) {
$form_ids[] = $ref;
}
}
}
return array_values( array_unique( $form_ids ) );
}
/**
* Collect form-render references from parsed blocks.
*
* @param array $blocks Parsed blocks.
* @param array $data Accumulator.
* @param array $reusable_block_ids Reusable block IDs already followed.
*
* @return array
*/
private function collect_form_refs_from_blocks( $blocks, array $data = [], array $reusable_block_ids = [] ): array {
if ( ! is_array( $blocks ) ) {
return $data;
}
foreach ( $blocks as $block ) {
if ( ! is_array( $block ) ) {
continue;
}
if ( 'generateblocks-pro/form-render' === ( $block['blockName'] ?? '' ) ) {
$form_id = absint( $block['attrs']['formId'] ?? 0 );
if ( $form_id > 0 ) {
$data[] = $form_id;
}
}
if ( 'core/block' === ( $block['blockName'] ?? '' ) ) {
$ref = absint( $block['attrs']['ref'] ?? 0 );
if ( $ref > 0 && ! in_array( $ref, $reusable_block_ids, true ) ) {
$reusable_block = get_post( $ref );
if ( $reusable_block && 'wp_block' === $reusable_block->post_type && 'publish' === $reusable_block->post_status ) {
$reusable_block_ids[] = $ref;
$data = $this->collect_form_refs_from_blocks(
parse_blocks( $reusable_block->post_content ),
$data,
$reusable_block_ids
);
}
}
}
if ( ! empty( $block['innerBlocks'] ) ) {
$data = $this->collect_form_refs_from_blocks( $block['innerBlocks'], $data, $reusable_block_ids );
}
}
return array_values( array_unique( $data ) );
}
/**
* Delete forms created by a failed dependency import batch.
*
* Existing matched forms are never passed here. We only roll back posts
* created in the current request, so a partial remote response cannot
* leave published orphan forms behind.
*
* @param array $form_ids Imported local form IDs.
*
* @return void
*/
private function rollback_imported_forms( array $form_ids ): void {
foreach ( $form_ids as $form_id ) {
$form_id = absint( $form_id );
if ( $form_id > 0 ) {
wp_delete_post( $form_id, true );
}
}
}
/**
* Insert a new local form from imported data.
*
* @param array $form Form data.
*
* @return int|WP_Error
*/
private function insert_imported_form( array $form ) {
$schema_check = $this->validate_imported_form_schema( $form['content'] );
if ( is_wp_error( $schema_check ) ) {
return $schema_check;
}
$content = GenerateBlocks_Pro_Form_Rest::regenerate_block_unique_ids( $form['content'] );
$title = '' !== $form['title']
? $form['title']
: __( 'Imported Form', 'generateblocks-pro' );
$post_id = wp_insert_post(
wp_slash(
[
'post_type' => GenerateBlocks_Pro_Form_Post_Type::POST_TYPE,
'post_status' => 'publish',
'post_title' => $title,
'post_content' => $content,
]
),
true
);
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
if ( ! $post_id ) {
return new WP_Error(
'gb_form_import_failed',
__( 'Could not import form.', 'generateblocks-pro' )
);
}
$meta = GenerateBlocks_Pro_Form_Post_Type::sanitize_meta( [ 'config' => $form['config'] ] );
if ( false === update_post_meta( $post_id, GenerateBlocks_Pro_Form_Post_Type::META_KEY, $meta ) ) {
wp_delete_post( $post_id, true );
return new WP_Error(
'gb_form_import_meta_failed',
__( 'Could not store imported form data.', 'generateblocks-pro' )
);
}
return (int) $post_id;
}
/**
* Validate imported form content before creating a local form.
*
* @param string $content Form post content.
*
* @return true|WP_Error
*/
private function validate_imported_form_schema( string $content ) {
if ( ! class_exists( 'GenerateBlocks_Pro_Form_Schema_Builder' ) ) {
return true;
}
$schema = GenerateBlocks_Pro_Form_Schema_Builder::get_instance()
->build_schema_from_content(
$content,
[ 'require_field_names' => true ]
);
if ( is_wp_error( $schema ) ) {
return $schema;
}
if ( is_array( $schema ) && empty( $schema ) ) {
return new WP_Error(
'gb_form_no_fields',
__( 'Imported form has no fields.', 'generateblocks-pro' )
);
}
$disallowed_block = $this->find_disallowed_imported_form_block( parse_blocks( $content ) );
if ( is_wp_error( $disallowed_block ) ) {
return $disallowed_block;
}
return true;
}
/**
* Find block types that should not be accepted from remote form imports.
*
* Normal save filters still handle ordinary block content based on the
* current user's capabilities. These blocks are different: their purpose
* is arbitrary markup or shortcode execution, so remote form dependencies
* should not import them implicitly.
*
* @param array $blocks Parsed blocks.
*
* @return WP_Error|null
*/
private function find_disallowed_imported_form_block( $blocks ) {
if ( ! is_array( $blocks ) ) {
return null;
}
$disallowed_blocks = [
'core/html',
'core/shortcode',
];
foreach ( $blocks as $block ) {
if ( ! is_array( $block ) ) {
continue;
}
if ( in_array( $block['blockName'] ?? '', $disallowed_blocks, true ) ) {
return new WP_Error(
'gb_form_import_disallowed_block',
__( 'Imported forms cannot include Custom HTML or Shortcode blocks.', 'generateblocks-pro' )
);
}
if ( ! empty( $block['innerBlocks'] ) ) {
$disallowed_block = $this->find_disallowed_imported_form_block( $block['innerBlocks'] );
if ( is_wp_error( $disallowed_block ) ) {
return $disallowed_block;
}
}
}
return null;
}
/**
* Get global style selectors referenced by allowed pattern collections.
*
* @param array $collections The allowed collection term IDs.
*
* @return array
*/
private function get_global_style_selectors_by_collections( array $collections ): array {
if ( empty( $collections ) ) {
return [];
}
// Defensive cast — see list_patterns() for rationale. No upper
// clamp so sites that raise the filter intentionally keep their
// exact value.
$pattern_count = apply_filters( 'generateblocks_pattern_library_count', 250 );
$pattern_count = is_numeric( $pattern_count ) ? (int) $pattern_count : 250;
$pattern_count = max( 1, $pattern_count );
$posts = get_posts(
[
'post_type' => 'wp_block',
'post_status' => 'publish',
'posts_per_page' => $pattern_count, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page -- Defensive cast above; filter is opt-in.
'tax_query' => [
[
'taxonomy' => 'gblocks_pattern_collections',
'field' => 'term_id',
'terms' => $collections,
],
],
]
);
if ( ! is_array( $posts ) ) {
return [];
}
$selectors = [];
$form_selector_cache = [];
foreach ( $posts as $post ) {
if ( ! isset( $post->ID ) ) {
continue;
}
$patterns = get_post_meta( $post->ID, 'generateblocks_patterns_tree', true );
if ( ! is_array( $patterns ) ) {
continue;
}
foreach ( $patterns as $pattern ) {
if ( ! is_array( $pattern ) ) {
continue;
}
if ( is_array( $pattern['globalStyleSelectors'] ?? null ) ) {
foreach ( $pattern['globalStyleSelectors'] as $selector ) {
if ( is_string( $selector ) && '' !== $selector ) {
$selectors[] = $selector;
}
}
}
$form_refs = array_values(
array_filter(
array_map( 'absint', (array) ( $pattern['formRefs'] ?? [] ) )
)
);
if ( empty( $form_refs ) && is_string( $pattern['pattern'] ?? null ) ) {
$form_refs = $this->collect_form_refs_from_blocks(
parse_blocks( wp_unslash( $pattern['pattern'] ) )
);
}
if ( ! empty( $form_refs ) ) {
$selectors = array_merge(
$selectors,
$this->get_current_form_global_style_selectors( $form_refs, $form_selector_cache )
);
}
}
}
return array_values( array_unique( $selectors ) );
}
/**
* Get current global style selectors from referenced forms.
*
* @param array $form_refs Form post IDs.
* @param array $form_selector_cache Cached selectors by form ID.
*
* @return array
*/
private function get_current_form_global_style_selectors( array $form_refs, array &$form_selector_cache ): array {
if ( ! class_exists( 'GenerateBlocks_Pro_Form_Post_Type' ) ) {
return [];
}
$selectors = [];
foreach ( $form_refs as $form_ref ) {
$form_id = absint( $form_ref );
if ( $form_id <= 0 ) {
continue;
}
if ( ! array_key_exists( $form_id, $form_selector_cache ) ) {
$form_selector_cache[ $form_id ] = [];
$form = get_post( $form_id );
if (
$form &&
GenerateBlocks_Pro_Form_Post_Type::POST_TYPE === $form->post_type &&
'publish' === $form->post_status
) {
$form_selector_cache[ $form_id ] = $this->collect_global_style_selectors_from_blocks(
parse_blocks( $form->post_content )
);
}
}
$selectors = array_merge( $selectors, $form_selector_cache[ $form_id ] );
}
return array_values( array_unique( $selectors ) );
}
/**
* Collect global style selectors from parsed blocks.
*
* @param array $blocks Parsed blocks.
* @param array $data Accumulator.
* @param array $reusable_block_ids Reusable block IDs already followed.
*
* @return array
*/
private function collect_global_style_selectors_from_blocks( $blocks, array $data = [], array $reusable_block_ids = [] ): array {
if ( ! is_array( $blocks ) ) {
return $data;
}
foreach ( $blocks as $block ) {
if ( ! is_array( $block ) ) {
continue;
}
$global_classes = is_array( $block['attrs']['globalClasses'] ?? null )
? $block['attrs']['globalClasses']
: [];
foreach ( $global_classes as $global_class ) {
if ( is_string( $global_class ) && '' !== $global_class ) {
$data[] = '.' === $global_class[0] ? $global_class : '.' . $global_class;
}
}
if ( 'core/block' === ( $block['blockName'] ?? '' ) ) {
$ref = absint( $block['attrs']['ref'] ?? 0 );
if ( $ref > 0 && ! in_array( $ref, $reusable_block_ids, true ) ) {
$reusable_block = get_post( $ref );
if ( $reusable_block && 'wp_block' === $reusable_block->post_type && 'publish' === $reusable_block->post_status ) {
$reusable_block_ids[] = $ref;
$data = $this->collect_global_style_selectors_from_blocks(
parse_blocks( $reusable_block->post_content ),
$data,
$reusable_block_ids
);
}
}
}
if ( ! empty( $block['innerBlocks'] ) ) {
$data = $this->collect_global_style_selectors_from_blocks( $block['innerBlocks'], $data, $reusable_block_ids );
}
}
return array_values( array_unique( $data ) );
}
/**
* Get the next menu order for a new style
*
* @return int The new menu_order to use.
*/
public function get_next_style_menu_order() {
$styles = GenerateBlocks_Pro_Styles::get_styles();
$last_style = end( $styles );
if ( $last_style ) {
return ( $last_style['menu_order'] ?? 0 ) + 1;
}
return 0;
}
/**
* Import a set of global classes.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function import_styles( WP_REST_Request $request ): WP_REST_Response {
$styles = $this->validate_import_styles_payload( $request->get_param( 'styles' ) );
if ( is_wp_error( $styles ) ) {
return $this->error( 400, $styles->get_error_message() );
}
$response = [
'imported' => [],
'existing' => [],
];
foreach ( $styles as $style ) {
$existing_class = new WP_Query(
[
'post_type' => 'gblocks_styles',
'posts_per_page' => 1,
'post_status' => 'any',
'meta_query' => [
[
'key' => 'gb_style_selector',
'value' => $style['className'],
'compare' => '=',
],
],
]
);
$existing_post_id = false;
if ( ! empty( $existing_class->found_posts ) ) {
$existing_class = $existing_class->posts[0];
$response['existing'][] = [
'id' => $existing_class->ID,
'selector' => get_post_meta( $existing_class->ID, 'gb_style_selector', true ),
'styles' => get_post_meta( $existing_class->ID, 'gb_style_data', true ),
];
// Bail out if we already have this class published.
if ( 'publish' === $existing_class->post_status ) {
continue;
}
$existing_post_id = $existing_class->ID;
}
if ( $existing_post_id ) {
$post_id = wp_update_post(
[
'ID' => $existing_post_id,
'post_status' => 'publish',
'menu_order' => $this->get_next_style_menu_order(),
]
);
} else {
$post_id = wp_insert_post(
[
'post_type' => 'gblocks_styles',
'post_status' => 'publish',
'post_title' => $style['title'],
'menu_order' => $this->get_next_style_menu_order(),
'meta_input' => [
'gb_style_selector' => $style['className'],
'gb_style_data' => $style['styles'],
'gb_style_css' => $style['css'],
],
]
);
}
if ( is_wp_error( $post_id ) ) {
return $this->error(
500,
sprintf(
/* translators: %1$s: name of the component */
__( 'Could not import class %1$s', 'generateblocks-pro' ),
$style['title']
)
);
}
$response['imported'][] = get_post_meta( $post_id, 'gb_style_selector', true );
}
return $this->success( $response );
}
/**
* Validate and normalize incoming global style import data.
*
* @param mixed $styles Raw request value.
*
* @return array|WP_Error
*/
private function validate_import_styles_payload( $styles ) {
if ( ! is_array( $styles ) ) {
return new WP_Error(
'invalid_styles_payload',
__( 'Invalid global styles payload.', 'generateblocks-pro' )
);
}
$validated = [];
foreach ( $styles as $style ) {
if (
! is_array( $style ) ||
! isset( $style['className'], $style['title'], $style['styles'], $style['css'] ) ||
! is_string( $style['className'] ) ||
! is_string( $style['title'] ) ||
! is_string( $style['styles'] ) ||
! is_string( $style['css'] )
) {
return new WP_Error(
'invalid_style_payload',
__( 'Invalid global style item.', 'generateblocks-pro' )
);
}
$class_name = trim( $style['className'] );
$title = trim( $style['title'] );
$style_data = json_decode( $style['styles'], true );
if ( '' === $class_name || '' === $title || ! is_array( $style_data ) ) {
return new WP_Error(
'invalid_style_payload',
__( 'Invalid global style item.', 'generateblocks-pro' )
);
}
$validated[] = [
'className' => $class_name,
'title' => $title,
'styles' => $style_data,
'css' => $style['css'],
];
}
return $validated;
}
/**
* Returns some library data when given a public key.
* This function is called on the providers server.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function get_library_by_public_key( WP_REST_Request $request ): WP_REST_Response {
$public_key = $this->sanitize_request_string( $request->get_param( 'publicKey' ) );
$header_key = $this->sanitize_request_string( $request->get_header( 'X-GB-Public-Key' ) );
if ( '' === $public_key ) {
$public_key = $header_key;
}
if ( '' !== $header_key && $public_key !== $header_key ) {
return $this->failed( __( 'No library found.', 'generateblocks-pro' ) );
}
$permissions = GenerateBlocks_Pro_Pattern_Library::get_instance()
->get_pattern_permissions_by_public_key( $public_key );
if ( is_null( $permissions ) ) {
return $this->failed( __( 'No library found.', 'generateblocks-pro' ) );
}
$library_name = sanitize_text_field( $permissions['name'] );
$data = [
'name' => $library_name,
];
return $this->success( $data );
}
/**
* Adds a new library when given data.
* Gets the name of the library from the provider.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function add_library( WP_REST_Request $request ): WP_REST_Response {
$data = $request->get_param( 'data' );
$public_key = $this->sanitize_request_string( $request->get_param( 'publicKey' ) );
$domain = esc_url_raw( $this->sanitize_request_string( $request->get_param( 'domain' ) ) );
if ( ! is_array( $data ) ) {
return $this->failed( __( 'Invalid library data.', 'generateblocks-pro' ) );
}
if ( ! $public_key ) {
return $this->failed( __( 'No public key provided.', 'generateblocks-pro' ) );
}
if ( ! $domain ) {
return $this->failed( __( 'No domain provided.', 'generateblocks-pro' ) );
}
$endpoint = $this->build_remote_endpoint_url(
$domain,
self::ENDPOINT_LIBRARY_BY_KEY,
[
'publicKey' => $public_key,
]
);
if ( '' === $endpoint ) {
return $this->failed( __( 'Invalid domain provided.', 'generateblocks-pro' ) );
}
$response = $this->fetch_remote_endpoint(
$domain,
self::ENDPOINT_LIBRARY_BY_KEY,
[
'headers' => array(
'X-GB-Public-Key' => $public_key,
),
'timeout' => 15,
],
[
'publicKey' => $public_key,
]
);
if ( is_wp_error( $response ) ) {
return $this->failed( $response->get_error_message() );
}
if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
return $this->failed( wp_remote_retrieve_response_message( $response ) );
}
$body = wp_remote_retrieve_body( $response );
$body = json_decode( $body, true );
$response_data = is_array( $body ) ? $body['response']['data'] ?? null : null;
$name = is_array( $response_data ) && is_string( $response_data['name'] ?? null ) ? $response_data['name'] : '';
if ( true !== ( $body['success'] ?? false ) || empty( $name ) ) {
return $this->failed( __( 'Unable to get library name.', 'generateblocks-pro' ) );
}
$id = $this->sanitize_request_string( $data['id'] ?? '' );
// Bound id to a slug/uuid character set so a caller can't inject
// option-key-corrupting characters or ids that collide after
// sanitization. Format is permissive enough to accept uuid-v4
// ('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx') and short slugs alike.
if ( '' === $id || ! preg_match( '/^[A-Za-z0-9_-]{1,64}$/', $id ) ) {
return $this->failed( __( 'Invalid library data.', 'generateblocks-pro' ) );
}
$libraries = get_option( 'generateblocks_pattern_libraries', [] );
if ( ! is_array( $libraries ) ) {
$libraries = [];
}
/**
* Filters the maximum number of pattern libraries that can be stored.
*
* Caps the autoloaded `generateblocks_pattern_libraries` option so a
* runaway client loop or buggy UI cannot balloon it. Manage_options is
* still the primary gate; this is a belt-and-braces ceiling.
*
* @param int $max_libraries Default 50.
*/
$max_libraries = (int) apply_filters( 'generateblocks_pattern_library_max_libraries', 50 );
if ( count( $libraries ) >= $max_libraries ) {
return $this->failed(
sprintf(
/* translators: %d: maximum number of pattern libraries. */
__( 'You have reached the maximum of %d pattern libraries.', 'generateblocks-pro' ),
$max_libraries
)
);
}
foreach ( $libraries as $existing ) {
if ( is_array( $existing ) && isset( $existing['id'] ) && $existing['id'] === $id ) {
return $this->failed( __( 'A library with this id already exists.', 'generateblocks-pro' ) );
}
}
$sanitized_data = [
'id' => $id,
'name' => sanitize_text_field( $name ),
'domain' => $domain,
'publicKey' => $public_key,
'isEnabled' => isset( $data['isEnabled'] ) ? (bool) $data['isEnabled'] : true,
'isDefault' => false,
'isLocal' => false,
];
$libraries[] = $sanitized_data;
update_option( 'generateblocks_pattern_libraries', $libraries );
return $this->success( $sanitized_data );
}
/**
* Returns a success response.
*
* @param array $data The data.
*
* @return WP_REST_Response
*/
private function success( array $data ): WP_REST_Response {
return new WP_REST_Response(
array(
'success' => true,
'response' => array(
'data' => $data,
),
),
200
);
}
/**
* Returns a success response.
*
* @param string $message The error message.
*
* @return WP_REST_Response
*/
private function failed( string $message ): WP_REST_Response {
return new WP_REST_Response(
array(
'success' => false,
'response' => $message,
),
200
);
}
/**
* Returns a error response.
*
* @param int $code Error code.
* @param string $message Error message.
*
* @return WP_REST_Response
*/
private function error( int $code, string $message ): WP_REST_Response {
return new WP_REST_Response(
array(
'error' => true,
'success' => false,
'error_code' => $code,
'response' => $message,
),
$code
);
}
}
GenerateBlocks_Pro_Pattern_Library_Rest::get_instance()->init();
endif;