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;