This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,301 @@
<?php
/**
* Handles the Global CSS Output.
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Build and enqueue our global CSS.
*/
class GenerateBlocks_Pro_Enqueue_Styles extends GenerateBlocks_Pro_Singleton {
/**
* Initialize.
*
* @return void
*/
public function init() {
add_action( 'init', array( $this, 'enqueue_assets' ) );
add_action( 'wp_after_insert_post', [ $this, 'build_css_file_on_save' ], 100, 2 );
add_action( 'after_delete_post', [ $this, 'build_css_on_delete' ], 100, 2 );
add_action( 'customize_after_save', [ $this, 'build_css' ] );
}
/**
* Enqueue our assets.
*/
public function enqueue_assets() {
add_action(
'wp_enqueue_scripts',
[ $this, 'enqueue_css' ],
apply_filters( 'generateblocks_global_css_priority', 20 ) // Less than block-specific stylesheets.
);
}
/**
* Check whether we're using file or inline mode.
*/
public function mode() {
$mode = apply_filters( 'generateblocks_global_css_print_method', 'file' );
if (
( function_exists( 'is_customize_preview' ) && is_customize_preview() ) ||
is_preview() ||
// AMP inlines all CSS, so inlining from the start improves CSS processing performance.
( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() )
) {
return 'inline';
}
return $mode;
}
/**
* This builds the CSS file.
* It will also delete an existing file if creating fails for any reason.
*
* @return boolean Whether the file has been built or not.
*/
public function build_css_file() {
$built = $this->create_css_file();
// Delete the file if `create_css_file()` fails for any reason.
if ( ! $built && file_exists( $this->file( 'path' ) ) ) {
wp_delete_file( $this->file( 'path' ) );
}
return $built;
}
/**
* Build our cache of utility classes.
*/
public function build_css_cache() {
update_option(
'generateblocks_style_css',
GenerateBlocks_Pro_Styles::get_styles_css( false )
);
}
/**
* Check to see if we have a global css file.
*/
public function has_css_file() {
return file_exists( $this->file( 'path' ) );
}
/**
* Enqueue the CSS.
*/
public function enqueue_css() {
if ( 'inline' === $this->mode() || ! $this->has_css_file() ) {
$css = apply_filters(
'generateblocks_global_css',
GenerateBlocks_Pro_Styles::get_styles_css()
);
// Add a "dummy" handle we can add inline styles to.
wp_register_style( 'generateblocks-global', false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_enqueue_style( 'generateblocks-global' );
wp_add_inline_style(
'generateblocks-global',
wp_strip_all_tags( $css ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
if ( 'file' === $this->mode() && $this->has_css_file() ) {
wp_enqueue_style( 'generateblocks-global', esc_url( $this->file( 'uri' ) ), array(), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
}
}
/**
* Creates our CSS file and puts it on the server.
*/
public function create_css_file() {
$css = apply_filters(
'generateblocks_global_css',
GenerateBlocks_Pro_Styles::get_styles_css()
);
if ( ! $css || ! $this->can_write() || ! function_exists( 'generateblocks_get_wp_filesystem' ) ) {
return false;
}
$filesystem = generateblocks_get_wp_filesystem();
if ( ! $filesystem ) {
return false;
}
// Take care of domain mapping.
if ( defined( 'DOMAIN_MAPPING' ) && DOMAIN_MAPPING ) {
if ( function_exists( 'domain_mapping_siteurl' ) && function_exists( 'get_original_url' ) ) {
$mapped_domain = domain_mapping_siteurl( false );
$original_domain = get_original_url( 'siteurl' );
$css = str_replace( $original_domain, $mapped_domain, $css );
}
}
if ( is_writable( $this->file( 'path' ) ) || ( ! file_exists( $this->file( 'path' ) ) && is_writable( dirname( $this->file( 'path' ) ) ) ) ) {
$chmod_file = 0644;
if ( defined( 'FS_CHMOD_FILE' ) ) {
$chmod_file = FS_CHMOD_FILE;
}
if ( ! $filesystem->put_contents( $this->file( 'path' ), wp_strip_all_tags( $css ), $chmod_file ) ) {
return false;
} else {
// Do a quick double-check to make sure the file was created.
if ( ! file_exists( $this->file( 'path' ) ) ) {
return false;
}
return true;
}
}
}
/**
* Determines if the CSS file is writable.
*/
public function can_write() {
global $blog_id;
// Get the upload directory for this site.
$upload_dir = wp_get_upload_dir();
// If this is a multisite installation, append the blogid to the filename.
$css_blog_id = ( is_multisite() && $blog_id > 1 ) ? '_blog-' . $blog_id : null;
$file_name = '/style' . $css_blog_id . '-global.css';
$folder_path = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'generateblocks';
if ( ! file_exists( $folder_path ) ) {
// returns true if yes and false if not.
return wp_mkdir_p( $folder_path );
}
$file_path = $folder_path . $file_name;
if (
! is_writable( $folder_path ) &&
( ! file_exists( $file_path ) || ! is_writable( $file_path ) )
) {
// Folder not writable & file does not exist or it's not writable.
return false;
}
if (
is_writable( $folder_path ) &&
( file_exists( $file_path ) && ! is_writable( $file_path ) )
) {
// Folder is writable but the file is not writable.
return false;
}
// All is well!
return true;
}
/**
* Gets the css path or url to the stylesheet
*
* @param string $target path/url.
*/
public function file( $target = 'path' ) {
global $blog_id;
// Get the upload directory for this site.
$upload_dir = wp_get_upload_dir();
// If this is a multisite installation, append the blogid to the filename.
$css_blog_id = ( is_multisite() && $blog_id > 1 ) ? '_blog-' . $blog_id : null;
$file_name = 'style' . $css_blog_id . '-global.css';
$folder_path = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'generateblocks';
// The complete path to the file.
$file_path = $folder_path . DIRECTORY_SEPARATOR . $file_name;
// Get the URL directory of the stylesheet.
$css_uri_folder = $upload_dir['baseurl'];
$css_uri = trailingslashit( $css_uri_folder ) . 'generateblocks/' . $file_name;
// Take care of domain mapping.
if ( defined( 'DOMAIN_MAPPING' ) && DOMAIN_MAPPING ) {
if ( function_exists( 'domain_mapping_siteurl' ) && function_exists( 'get_original_url' ) ) {
$mapped_domain = domain_mapping_siteurl( false );
$original_domain = get_original_url( 'siteurl' );
$css_uri = str_replace( $original_domain, $mapped_domain, $css_uri );
}
}
$css_uri = set_url_scheme( $css_uri );
if ( 'path' === $target ) {
return $file_path;
} elseif ( 'url' === $target || 'uri' === $target ) {
$timestamp = ( file_exists( $file_path ) ) ? '?ver=' . filemtime( $file_path ) : '';
return $css_uri . $timestamp;
}
}
/**
* Rebuild our CSS cache and file after we save a class.
*
* @param int $post_id The post ID being saved.
* @param object $post The post object.
*/
public function build_css_file_on_save( $post_id, $post ) {
if ( 'gblocks_styles' !== $post->post_type ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Check if it is an autosave or a revision.
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$this->build_css();
}
/**
* Rebuild our CSS cache and file after we delete a style.
*
* @param int $post_id The post ID being deleted.
* @param object $post The post object.
*/
public function build_css_on_delete( $post_id, $post ) {
if ( 'gblocks_styles' !== $post->post_type ) {
return;
}
$this->build_css();
}
/**
* Rebuild our CSS cache and file.
*/
public function build_css() {
$this->build_css_cache();
$this->build_css_file();
}
}
GenerateBlocks_Pro_Enqueue_Styles::get_instance()->init();
@@ -0,0 +1,164 @@
<?php
/**
* The Utility Class post type file.
*
* @package GenerateBlocksPro\Post_Types
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class to register post type Utility Classes.
*
* @since 1.7.0
*/
class GenerateBlocks_Pro_Styles_Post_Type extends GenerateBlocks_Pro_Singleton {
/**
* Initialize the class filters.
*
* @return void
*/
public function init() {
add_action( 'init', array( $this, 'register_post_type' ) );
}
/**
* Register the post type.
*
* @return void
*/
public function register_post_type() {
register_post_type(
'gblocks_styles',
array(
'labels' => array(
'name' => _x( 'Global Styles', 'Post Type General Name', 'generateblocks-pro' ),
'singular_name' => _x( 'Global Style', 'Post Type Singular Name', 'generateblocks-pro' ),
'menu_name' => __( 'Global Styles', 'generateblocks-pro' ),
'parent_item_colon' => __( 'Parent Global Style', 'generateblocks-pro' ),
'all_items' => __( 'Global Styles', 'generateblocks-pro' ),
'view_item' => __( 'View Global Style', 'generateblocks-pro' ),
'add_new_item' => __( 'Add New Global Style', 'generateblocks-pro' ),
'add_new' => __( 'Add New Global Style', 'generateblocks-pro' ),
'edit_item' => __( 'Edit Global Style', 'generateblocks-pro' ),
'update_item' => __( 'Update Global Style', 'generateblocks-pro' ),
'search_items' => __( 'Search Global Styles', 'generateblocks-pro' ),
'not_found' => __( 'Not Found', 'generateblocks-pro' ),
'not_found_in_trash' => __( 'Not found in Trash', 'generateblocks-pro' ),
),
'public' => false,
'publicly_queryable' => false,
'has_archive' => false,
'show_ui' => false,
'show_in_menu' => false,
'exclude_from_search' => true,
'show_in_nav_menus' => false,
'rewrite' => false,
'hierarchical' => false,
'show_in_admin_bar' => false,
'show_in_rest' => true,
'supports' => array( 'title', 'page-attributes' ),
'capabilities' => array(
'publish_posts' => GenerateBlocks_Pro_Styles::get_manage_styles_capability(),
'edit_posts' => GenerateBlocks_Pro_Styles::get_manage_styles_capability(),
'edit_others_posts' => GenerateBlocks_Pro_Styles::get_manage_styles_capability(),
'delete_posts' => GenerateBlocks_Pro_Styles::get_manage_styles_capability(),
'delete_others_posts' => GenerateBlocks_Pro_Styles::get_manage_styles_capability(),
'read_private_posts' => GenerateBlocks_Pro_Styles::get_manage_styles_capability(),
'edit_post' => GenerateBlocks_Pro_Styles::get_manage_styles_capability(),
'delete_post' => GenerateBlocks_Pro_Styles::get_manage_styles_capability(),
'read_post' => GenerateBlocks_Pro_Styles::get_manage_styles_capability(),
),
)
);
register_post_meta(
'gblocks_styles',
'gb_style_selector',
array(
'type' => 'string',
'default' => '',
'show_in_rest' => false,
)
);
register_post_meta(
'gblocks_styles',
'gb_style_css',
array(
'type' => 'string',
'default' => '',
'show_in_rest' => false,
)
);
register_post_meta(
'gblocks_styles',
'gb_style_data',
array(
'type' => 'array',
'show_in_rest' => false,
)
);
register_rest_field(
'gblocks_styles',
'gb_style_selector',
array(
'get_callback' => function( $data ) {
return get_post_meta( $data['id'], 'gb_style_selector', true );
},
'update_callback' => function( $value, $post ) {
update_post_meta( $post->ID, 'gb_style_selector', $value );
},
'schema' => array(
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => function ( $value ) {
return wp_strip_all_tags( $value );
},
),
),
)
);
register_rest_field(
'gblocks_styles',
'gb_style_css',
array(
'get_callback' => function( $data ) {
return get_post_meta( $data['id'], 'gb_style_css', true );
},
'update_callback' => function( $value, $post ) {
update_post_meta( $post->ID, 'gb_style_css', $value );
},
'schema' => array(
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => function ( $value ) {
return wp_strip_all_tags( $value );
},
),
),
)
);
register_rest_field(
'gblocks_styles',
'gb_style_data',
array(
'get_callback' => function( $data ) {
return get_post_meta( $data['id'], 'gb_style_data', true );
},
'update_callback' => function( $value, $post ) {
update_post_meta( $post->ID, 'gb_style_data', $value );
},
)
);
}
}
GenerateBlocks_Pro_Styles_Post_Type::get_instance()->init();
@@ -0,0 +1,683 @@
<?php
/**
* The Global Classes rest class file.
*
* @package GenerateBlocksPro\Global_Classes_Rest
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
require_once GENERATEBLOCKS_PRO_DIR . 'includes/utils/trait-usage-search.php';
/**
* Main class for the Global Classes Rest functions.
*
* @since 1.9
*/
class GenerateBlocks_Pro_Styles_Rest extends GenerateBlocks_Pro_Singleton {
use GenerateBlocks_Pro_Usage_Search;
/**
* 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,
'/global-classes/check_class_name',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'check_class_name' ),
'permission_callback' => array( $this, 'can_create' ),
)
);
register_rest_route(
$namespace,
'/global-classes/get',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_global_styles' ),
'permission_callback' => array( $this, 'can_create' ),
)
);
register_rest_route(
$namespace,
'/global-classes/get_css',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_global_class_css' ),
'permission_callback' => array( $this, 'can_create' ),
)
);
register_rest_route(
$namespace,
'/global-classes/get_styles',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_global_class_styles' ),
'permission_callback' => array( $this, 'can_create' ),
)
);
register_rest_route(
$namespace,
'/global-styles/update_menu_order',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_menu_order' ),
'permission_callback' => array( $this, 'can_manage' ),
)
);
register_rest_route(
$namespace,
'/global-classes/(?P<id>\d+)/usage',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_global_style_usage' ),
'permission_callback' => array( $this, 'can_manage' ),
'args' => array(
'id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
),
'limit' => array(
'required' => false,
'type' => 'integer',
'default' => 50,
'sanitize_callback' => 'absint',
),
'refresh' => array(
'required' => false,
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
),
),
)
);
}
/**
* Manage options permission callback.
*
* @return bool
*/
public function can_create(): bool {
return current_user_can( 'edit_posts' );
}
/**
* Permission callback for endpoints that require elevated management access.
*
* @return bool
*/
public function can_manage(): bool {
return GenerateBlocks_Pro_Styles::can_manage_styles();
}
/**
* Check if a class name already exists.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function check_class_name( WP_REST_Request $request ): WP_REST_RESPONSE {
$class_name = $request->get_param( 'className' );
if ( empty( $class_name ) ) {
return $this->failed( __( 'Style name cannot be empty', 'generateblocks-pro' ) );
}
$existing_class = new WP_Query(
[
'post_type' => 'gblocks_styles',
'posts_per_page' => -1,
'post_status' => 'any',
'meta_query' => [
[
'key' => 'gb_style_selector',
'value' => $class_name,
'compare' => '=',
],
],
]
);
if ( ! empty( $existing_class->found_posts ) ) {
return $this->failed( __( 'Style name already exists', 'generateblocks-pro' ) );
}
return $this->success( [ 'success' ] );
}
/**
* Returns a list of active or inactive classes.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function get_global_styles( WP_REST_Request $request ): WP_REST_Response {
$status = $request->get_param( 'status' ) ?? 'publish';
$custom_args = [
'post_status' => $status,
];
$styles = GenerateBlocks_Pro_Styles::get_styles( $custom_args );
$class_data = [
'styles' => $styles,
];
return $this->success( $class_data );
}
/**
* Returns the CSS for the complete set of global classes or a specific class if specified.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function get_global_class_css( WP_REST_Request $request ): WP_REST_Response {
$class_name = $request->get_param( 'className' );
if ( $class_name ) {
$class = GenerateBlocks_Pro_Styles::get_class_by_name( $class_name );
if ( ! isset( $class->ID ) ) {
return $this->failed( 'No CSS found.' );
}
$css = get_post_meta( $class->ID, 'gb_style_css', true );
} else {
$css = GenerateBlocks_Pro_Styles::get_styles_css();
}
return $this->success( [ $css ] );
}
/**
* Returns the styles for a specific class name.
*
* @param WP_REST_Request $request The request.
*
* @return WP_REST_Response
*/
public function get_global_class_styles( WP_REST_Request $request ): WP_REST_Response {
$class_name = $request->get_param( 'globalClass' );
$class = GenerateBlocks_Pro_Styles::get_class_by_name( $class_name );
if ( ! isset( $class ) ) {
return $this->failed( __( 'Style does not exist', 'generateblocks-pro' ) );
}
$post_id = $class->ID;
$styles = get_post_meta( $post_id, 'gb_style_data', true );
return $this->success(
[
'postId' => $post_id,
'styles' => $styles ?? [],
]
);
}
/**
* Handles updating global style menu order
*
* @param WP_REST_Request $request WP Request object.
* @return WP_REST_RESPONSE;
*/
public function update_menu_order( WP_REST_Request $request ): WP_REST_Response {
$order = $request->get_param( 'order' );
if ( empty( $order ) || ! is_array( $order ) ) {
return $this->failed( __( 'Order parameter is invalid.', 'generateblocks-pro' ) );
}
global $wpdb;
$sql = '';
$rows_affected = 0;
foreach ( $order as $index => $post_id ) {
$post_id = absint( $post_id );
if ( ! $post_id ) {
continue;
}
$query = $wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->posts} SET menu_order = %d WHERE ID = %d AND post_type = %s;",
[
$index,
$post_id,
'gblocks_styles',
]
)
);
if ( false === $query ) {
return $this->failed( __( 'Failed to update menu order.', 'generateblocks-pro' ) );
} else {
$rows_affected += $query;
}
}
$wpdb->flush();
// Flush the cache after the operation completes.
GenerateBlocks_Pro_Enqueue_Styles::get_instance()->build_css();
return $this->success(
[
'message' => __( 'Menu order updated', 'generateblocks-pro' ),
'rows_affected' => $rows_affected,
]
);
}
/**
* Get global style usage across the site.
*
* @param WP_REST_Request $request The request object.
* @return WP_REST_Response
*/
public function get_global_style_usage( WP_REST_Request $request ): WP_REST_Response {
$style_id = $request->get_param( 'id' );
$limit = $request->get_param( 'limit' );
$refresh = $request->get_param( 'refresh' );
if ( empty( $style_id ) ) {
return $this->failed( __( 'Style ID is required.', 'generateblocks-pro' ) );
}
// Verify the style exists and get the class name.
$style_post = get_post( $style_id );
if ( ! $style_post || 'gblocks_styles' !== $style_post->post_type ) {
return $this->failed( __( 'Style not found.', 'generateblocks-pro' ) );
}
$class_name = get_post_meta( $style_id, 'gb_style_selector', true );
if ( empty( $class_name ) ) {
return $this->failed( __( 'Style class name not found.', 'generateblocks-pro' ) );
}
// Check cache unless refresh is requested.
$cache_key = sprintf( 'gb_style_usage_%d_limit%d', $style_id, $limit );
if ( ! $refresh ) {
$cached_data = $this->get_usage_cache( $cache_key );
if ( false !== $cached_data ) {
$cached_data['cached'] = true;
$cached_data['cached_at'] = $cached_data['cached_at'] ?? time();
return $this->success( $cached_data );
}
}
$usage_data = $this->search_global_style_usage( $class_name, $limit );
// Cache for 15 minutes (900 seconds).
// Note: Cache invalidation is intentionally manual (via refresh button in UI).
// Automatic invalidation on post save/delete was deemed too complex and risky
// due to the need to clear caches across all styles when any post changes.
// Users can click refresh to get updated results when needed.
$usage_data['cached_at'] = time();
$this->set_usage_cache( $cache_key, $usage_data, 900 );
$usage_data['cached'] = false;
return $this->success( $usage_data );
}
/**
* Search for global style usage across different data sources.
*
* @param string $class_name The class name to search for.
* @param int $limit Maximum total results to return (default: 50).
* @return array
*/
private function search_global_style_usage( string $class_name, int $limit = 50 ): array {
$all_usage = array();
$total_found = 0;
$has_more = false;
$limitations = array();
$has_limitations = false;
// Define search handlers - easily extensible for future use cases.
$search_handlers = array(
'style_usage' => array(
'method' => 'search_global_style_attribute_usage',
'label' => __( 'Style Usage', 'generateblocks-pro' ),
),
);
// Apply filters to allow other plugins/themes to add search handlers.
$search_handlers = apply_filters( 'generateblocks_global_style_usage_handlers', $search_handlers, $class_name );
foreach ( $search_handlers as $handler_key => $handler_config ) {
// Stop processing if we've found enough results.
if ( $total_found >= $limit ) {
$has_more = true;
break;
}
if ( ! method_exists( $this, $handler_config['method'] ) ) {
continue;
}
// Calculate how many more results we need.
$remaining = $limit - $total_found;
$handler_results = $this->{$handler_config['method']}( $class_name, $remaining );
if (
! empty( $handler_results['items'] )
|| ! empty( $handler_results['has_more'] )
|| ! empty( $handler_results['limited'] )
) {
// Take only what we need.
$items = array_slice( $handler_results['items'], 0, $remaining );
$all_usage[ $handler_key ] = array(
'label' => $handler_config['label'],
'items' => $items,
'has_more' => ! empty( $handler_results['has_more'] ) || ( count( $handler_results['items'] ) > $remaining ),
);
$total_found += count( $items );
// If any handler has more results, set the overall has_more flag.
if ( ! empty( $handler_results['has_more'] ) ) {
$has_more = true;
}
if ( ! empty( $handler_results['limited'] ) ) {
$has_limitations = true;
}
if ( ! empty( $handler_results['limits'] ) && is_array( $handler_results['limits'] ) ) {
foreach ( $handler_results['limits'] as $limit_key => $limit_data ) {
if ( ! isset( $limitations[ $limit_key ] ) ) {
$limitations[ $limit_key ] = $limit_data;
continue;
}
$current = $limitations[ $limit_key ];
if ( isset( $limit_data['max_posts'] ) ) {
$current['max_posts'] = isset( $current['max_posts'] )
? max( $current['max_posts'], $limit_data['max_posts'] )
: $limit_data['max_posts'];
}
if ( isset( $limit_data['scanned_posts'] ) ) {
$current['scanned_posts'] = isset( $current['scanned_posts'] )
? max( $current['scanned_posts'], $limit_data['scanned_posts'] )
: $limit_data['scanned_posts'];
}
if ( isset( $limit_data['total_posts'] ) ) {
$current['total_posts'] = isset( $current['total_posts'] )
? max( $current['total_posts'], $limit_data['total_posts'] )
: $limit_data['total_posts'];
}
$limitations[ $limit_key ] = $current;
}
}
}
}
return array(
'usage' => $all_usage,
'total' => $total_found,
'has_more' => $has_more,
'limited' => $has_limitations,
'limitations' => $limitations,
);
}
/**
* Search for blocks using this style via the globalClasses attribute.
*
* @param string $class_name The class name to search for.
* @param int $limit Maximum results to return.
* @return array
*/
private function search_global_style_attribute_usage( string $class_name, int $limit ): array {
global $wpdb;
// Validate and sanitize inputs.
$limit = absint( $limit );
if ( empty( $class_name ) ) {
return array(
'items' => array(),
'has_more' => false,
);
}
// Get post types that support the block editor.
$post_types = get_post_types_by_support( 'editor' );
$excluded_types = array( 'revision', 'attachment', 'nav_menu_item' );
$post_types = array_diff( $post_types, $excluded_types );
$post_types = apply_filters( 'generateblocks_usage_search_post_types', $post_types, 'styles' );
if ( empty( $post_types ) ) {
return array(
'items' => array(),
'has_more' => false,
);
}
$post_statuses = array( 'publish', 'private', 'draft' );
// Strip leading dot if present - HTML class attributes don't have dots.
$class_name = ltrim( $class_name, '.' );
// Search in the rendered HTML (class="...") instead of the JSON.
// This avoids unicode encoding issues and is much simpler and more reliable.
// Pattern: class="...class-name..." (with word boundaries for accuracy).
$search_pattern = '%class="%' . $wpdb->esc_like( $class_name ) . '%"%';
// Query one more than requested to determine if there are more results.
$query_limit = $limit + 1;
// Cap the number of posts scanned (prevents slow queries on large sites).
// Users can adjust this via filter if needed.
$max_posts_to_scan = apply_filters( 'generateblocks_usage_search_max_posts', 5000 );
$post_types_placeholders = implode( ', ', array_fill( 0, count( $post_types ), '%s' ) );
$status_placeholders = implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) );
// Determine dataset size so we can surface limitations to the UI.
$total_posts_sql = sprintf(
"SELECT COUNT(ID)
FROM {$wpdb->posts}
WHERE post_status IN (%s)
AND post_type IN (%s)",
$status_placeholders,
$post_types_placeholders
);
$total_posts_query = $wpdb->prepare(
$total_posts_sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
array_merge( $post_statuses, $post_types )
);
$total_posts = absint( $wpdb->get_var( $total_posts_query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$scanned_posts = min( $total_posts, $max_posts_to_scan );
$limited_scan = $total_posts > $max_posts_to_scan;
// Two-phase query: First get recent posts, then search within them.
// This prevents full table scans on large sites.
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$sql = sprintf(
"SELECT ID, post_title, post_type, post_status, post_content
FROM (
SELECT ID, post_title, post_type, post_status, post_content, post_modified
FROM {$wpdb->posts}
WHERE post_status IN ('publish', 'private', 'draft')
AND post_type IN (%s)
ORDER BY post_modified DESC
LIMIT %%d
) AS recent_posts
WHERE post_content LIKE %%s
LIMIT %%d",
$post_types_placeholders
);
$results_query = $wpdb->prepare(
$sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
array_merge( $post_types, array( $max_posts_to_scan, $search_pattern, $query_limit ) )
);
// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$results = $wpdb->get_results( $results_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
// Check if there are more results.
$has_more = count( $results ) > $limit;
// Remove the extra result if we got it.
if ( $has_more ) {
array_pop( $results );
}
$formatted_results = array();
foreach ( $results as $result ) {
// Extract class attribute values so we can inspect individual tokens.
preg_match_all( '/class=(["\'])([^"\']+)\1/i', $result->post_content, $class_attr_matches );
$match_count = 0;
if ( ! empty( $class_attr_matches[2] ) ) {
foreach ( $class_attr_matches[2] as $class_attr ) {
$tokens = preg_split( '/\s+/', trim( $class_attr ) );
if ( empty( $tokens ) ) {
continue;
}
foreach ( $tokens as $token ) {
if ( $class_name === $token ) {
$match_count++;
}
}
}
}
if ( 0 === $match_count ) {
continue;
}
$post_type_object = get_post_type_object( $result->post_type );
$post_type_label = $post_type_object ? $post_type_object->labels->singular_name : $result->post_type;
// Check if post type is publicly viewable.
$is_public = $post_type_object && ( $post_type_object->public || $post_type_object->publicly_queryable );
// translators: %1$s: Post type label, %2$d: Post ID.
$fallback_title = $result->post_title ? $result->post_title : sprintf( __( '%1$s #%2$d', 'generateblocks-pro' ), $post_type_label, $result->ID );
$formatted_results[] = array(
'id' => absint( $result->ID ),
'title' => $fallback_title,
'type' => $result->post_type,
'type_label' => $post_type_label,
'status' => $result->post_status,
'edit_url' => get_edit_post_link( $result->ID, 'raw' ),
'view_url' => ( 'publish' === $result->post_status && $is_public ) ? get_permalink( $result->ID ) : null,
'usage_type' => 'global_style_usage',
'block_count' => $match_count,
);
}
return array(
'items' => $formatted_results,
'has_more' => $has_more || $limited_scan,
'limited' => $limited_scan,
'limits' => $limited_scan
? array(
'recent_post_scan' => array(
'max_posts' => $max_posts_to_scan,
'scanned_posts' => $scanned_posts,
'total_posts' => $total_posts,
),
)
: array(),
);
}
/**
* 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_Styles_Rest::get_instance()->init();
@@ -0,0 +1,387 @@
<?php
/**
* The Global Class file.
*
* @package GenerateBlocks\Global_Classes
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class for handling global classes.
*
* @since 1.9.0
*/
class GenerateBlocks_Pro_Styles extends GenerateBlocks_Pro_Singleton {
/**
* Initialize the class filters.
*
* @return void
*/
public function init() {
add_action( 'admin_menu', [ $this, 'add_menu' ] );
add_action( 'generateblocks_dashboard_tabs', [ $this, 'add_tab' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_filter( 'generateblocks_dashboard_screens', [ $this, 'add_to_dashboard_pages' ] );
add_filter( 'block_editor_settings_all', [ $this, 'add_editor_styles_css' ], 21 );
// Add global classes to dynamic elements.
add_filter( 'generateblocks_attr_container', [ $this, 'add_global_class_names' ], 10, 2 );
add_filter( 'generateblocks_attr_grid-wrapper', [ $this, 'add_global_class_names' ], 10, 2 );
add_filter( 'generateblocks_attr_image', [ $this, 'add_global_class_names' ], 10, 2 );
add_filter( 'generateblocks_attr_dynamic-headline', [ $this, 'add_global_class_names' ], 10, 2 );
add_filter( 'generateblocks_attr_dynamic-button', [ $this, 'add_global_class_names' ], 10, 2 );
}
/**
* Add our Dashboard menu item.
*/
public function add_menu() {
$settings = add_submenu_page(
'generateblocks',
__( 'Global Styles', 'generateblocks-pro' ),
__( 'Global Styles', 'generateblocks-pro' ),
'manage_options',
'generateblocks-styles',
array( $this, 'styles_dashboard' ),
3
);
}
/**
* Add a Local Templates tab to the GB Dashboard tabs.
*
* @param array $tabs The existing tabs.
*/
public function add_tab( $tabs ) {
$screen = get_current_screen();
$tabs['styles'] = array(
'name' => __( 'Global Styles', 'generateblocks-pro' ),
'url' => admin_url( 'admin.php?page=generateblocks-styles' ),
'class' => 'generateblocks_page_generateblocks-styles' === $screen->id ? 'active' : '',
);
return $tabs;
}
/**
* Enqueue our scripts.
*/
public function enqueue_scripts() {
$screen = get_current_screen();
if ( 'generateblocks_page_generateblocks-styles' !== $screen->id ) {
return;
}
$assets = generateblocks_pro_get_enqueue_assets( 'global-class-dashboard' );
wp_enqueue_script(
'generateblocks-pro-global-class-dashboard',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/global-class-dashboard.js',
$assets['dependencies'],
$assets['version'],
true
);
wp_set_script_translations( 'generateblocks-pro-global-class-dashboard', 'generateblocks-pro', GENERATEBLOCKS_PRO_DIR . 'languages' );
wp_localize_script(
'generateblocks-pro-global-class-dashboard',
'generateBlocksPro',
array(
'canManageStyles' => self::can_manage_styles(),
'legacyGlobalStylesUrl' => admin_url( 'edit.php?post_type=gblocks_global_style' ),
'legacyGlobalStylesCount' => wp_count_posts( 'gblocks_global_style' ),
)
);
wp_enqueue_style(
'generateblocks-pro-global-class-dashboard',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/global-class-dashboard.css',
array( 'wp-components' ),
GENERATEBLOCKS_PRO_VERSION
);
}
/**
* Add to our Dashboard pages.
*
* @since 1.0.0
* @param array $pages The existing pages.
*/
public function add_to_dashboard_pages( $pages ) {
$pages[] = 'generateblocks_page_generateblocks-styles';
return $pages;
}
/**
* Output our Dashboard HTML.
*
* @since 1.0.0
*/
public function styles_dashboard() {
?>
<div class="wrap gblocks-dashboard-wrap">
<div class="generateblocks-settings-area generateblocks-global-classes-area">
<div id="gblocks-global-classes" class="generateblocks-settings-area__inner"></div>
</div>
</div>
<?php
}
/**
* Get an array of our global styles.
* These styles are sorted so child styles show up immediately after their parents.
*
* @param array $custom_args Any custom args to be passed to the query.
*/
public static function get_styles( array $custom_args = [] ) {
$styles = [];
self::loop_styles(
function ( $post, $meta ) use ( &$styles ) {
$styles[] = [
'ID' => $post->ID,
'style' => $post->post_title,
'status' => $post->post_status,
'menu_order' => $post->menu_order,
];
},
$custom_args
);
return $styles;
}
/**
* Build our global class CSS.
*
* @param boolean $cached Whether we should the cached data or not.
*/
public static function get_styles_css( $cached = true ) {
$cached_css = get_option( 'generateblocks_style_css', '' );
if ( $cached && ! empty( $cached_css ) ) {
return $cached_css;
}
$css = '';
self::loop_styles(
function ( $post, $meta ) use ( &$css ) {
if ( isset( $meta['gb_style_css'] ) ) {
$css .= $meta['gb_style_css'];
}
},
[],
[ 'gb_style_css' ]
);
// Cache our results.
update_option( 'generateblocks_style_css', $css );
return $css;
}
/**
* Add our global CSS to the editor.
*
* This adds a single instance per class, which allows us to live edit each
* class in the editor without affecting the others.
*
* @param array $editor_settings Existing editor settings.
*/
public function add_editor_styles_css( $editor_settings ) {
self::loop_styles(
function ( $post, $meta ) use ( &$editor_settings ) {
$class_css = $meta['gb_style_css'] ?? '';
$class_name = $meta['gb_style_selector'] ?? '';
if ( '' === $class_css || '' === $class_name ) {
return;
}
$editor_settings['styles'][] = [
'css' => $class_css,
'source' => 'gb_class:' . $class_name,
];
},
[],
[ 'gb_style_css', 'gb_style_selector' ]
);
return $editor_settings;
}
/**
* Add global classes to dynamic HTML blocks.
*
* @param array $htmlAttributes Current HTML attributes.
* @param array $blockAttributes Attributes for the current block.
*/
public function add_global_class_names( $htmlAttributes, $blockAttributes ) {
if ( ! empty( $blockAttributes['globalClasses'] ) ) {
$classes = implode( ' ', $blockAttributes['globalClasses'] );
if ( ! empty( $classes ) ) {
$htmlAttributes['class'] .= ' ' . esc_attr( $classes );
}
}
return $htmlAttributes;
}
/**
* The default capability for users who can manage classes.
*/
public static function get_manage_styles_capability() {
return apply_filters(
'generateblocks_manage_classes_capability',
'manage_options'
);
}
/**
* Check if we can manage classes.
*/
public static function can_manage_styles() {
$can_manage = current_user_can( self::get_manage_styles_capability() );
$additional_checks = apply_filters(
'generateblocks_can_manage_styles',
true
);
return $can_manage && $additional_checks;
}
/**
* Get our class post object using the class name.
*
* @param string $class_name The name of the class to query.
*/
public static function get_class_by_name( $class_name ) {
$query = new WP_Query(
[
'post_type' => 'gblocks_styles',
'posts_per_page' => 1,
'post_status' => 'any',
'meta_query' => [
[
'key' => 'gb_style_selector',
'value' => $class_name,
'compare' => '=',
],
],
]
);
if ( empty( $query->found_posts ) ) {
return false;
}
return $query->posts[0];
}
/**
* Iterate over styles in batches to avoid memory issues with unlimited queries.
*
* @param callable $callback Callback executed for every post. Receives the post object and an array of meta values.
* @param array $custom_args Optional WP_Query args (supports all standard args).
* @param array $meta_keys Meta keys to retrieve for each post.
*
* @return void
*/
private static function loop_styles( callable $callback, array $custom_args = [], array $meta_keys = [] ) {
$defaults = [
'post_type' => 'gblocks_styles',
'post_status' => 'publish',
'order' => 'ASC',
'orderby' => 'menu_order',
'posts_per_page' => apply_filters( 'generateblocks_styles_posts_per_page', 5000 ), // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
];
$args = wp_parse_args( $custom_args, $defaults );
$batch_size = apply_filters( 'generateblocks_styles_batch_size', 200 );
$posts_per_page = isset( $args['posts_per_page'] ) ? (int) $args['posts_per_page'] : 0;
$args['posts_per_page'] = $posts_per_page;
// Batch if unlimited requested OR if limit is larger than batch size.
if ( 0 === $posts_per_page || -1 === $posts_per_page || $posts_per_page > $batch_size ) {
$original_posts_per_page = $posts_per_page;
$offset = isset( $args['offset'] ) ? (int) $args['offset'] : 0;
if ( ! empty( $args['paged'] ) && $original_posts_per_page > 0 ) {
$offset += ( (int) $args['paged'] - 1 ) * $original_posts_per_page;
}
unset( $args['offset'], $args['paged'] );
$remaining = $original_posts_per_page > 0 ? $original_posts_per_page : null;
$current_offset = $offset;
do {
$limit = null === $remaining ? $batch_size : min( $batch_size, $remaining );
$query_args = $args;
$query_args['posts_per_page'] = $limit;
$query_args['offset'] = $current_offset;
$query = new WP_Query( $query_args );
if ( empty( $query->posts ) ) {
break;
}
// Prime meta cache to avoid N+1 queries.
if ( ! empty( $meta_keys ) ) {
update_meta_cache( 'post', wp_list_pluck( $query->posts, 'ID' ) );
}
foreach ( $query->posts as $post ) {
$meta = [];
foreach ( $meta_keys as $key ) {
$meta[ $key ] = get_post_meta( $post->ID, $key, true );
}
$callback( $post, $meta );
}
$posts_count = count( $query->posts );
$current_offset += $posts_count;
if ( null !== $remaining ) {
$remaining -= $posts_count;
if ( $remaining <= 0 ) {
break;
}
}
} while ( $posts_count === $limit );
} else {
// If specific limit requested, just do single query.
$query = new WP_Query( $args );
if ( ! empty( $meta_keys ) ) {
update_meta_cache( 'post', wp_list_pluck( $query->posts, 'ID' ) );
}
foreach ( $query->posts as $post ) {
$meta = [];
foreach ( $meta_keys as $key ) {
$meta[ $key ] = get_post_meta( $post->ID, $key, true );
}
$callback( $post, $meta );
}
}
}
}
GenerateBlocks_Pro_Styles::get_instance()->init();