'); // Add top-level menu with custom SVG icon $menu_slug = 'patternswp-plugin-menu'; add_menu_page( 'PatternsWP', 'PatternsWP', 'manage_options', $menu_slug, array($this, 'render_main_page'), $custom_icon, 60 ); // Add subpages $this->add_subpage($menu_slug, 'Dashboard', 'Dashboard', 'patternswp-plugin-menu', array($this, 'render_main_page')); $this->add_subpage($menu_slug, 'Settings', 'Settings', 'patternswp-settings', array($this, 'render_pattern_visibility_page')); $this->add_subpage($menu_slug, 'Support', 'Support', 'patternswp-plugin-page-1', array($this, 'patternswp_support')); } /** * Add subpage */ private function add_subpage($parent_slug, $page_title, $menu_title, $menu_slug, $callback) { // Add subpage add_submenu_page($parent_slug, $page_title, $menu_title, 'manage_options', $menu_slug, $callback); } /** * Render tabs */ public function patterswp_tab( $tab ) { $tabs = array( 'Dashboard' => 'patternswp-plugin-menu', 'Settings' => 'patternswp-settings', 'Support' => 'patternswp-plugin-page-1', 'License' => 'patternswp-license_section' ); // Get the current page from URL parameter $current_page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : 'patternswp-plugin-menu'; // Map page slugs to tab names $page_to_tab = array( 'patternswp-plugin-menu' => 'Dashboard', 'patternswp-settings' => 'Settings', 'patternswp-plugin-page-1' => 'Support', 'patternswp-license_section' => 'License' ); // Determine current tab $current_tab = isset($page_to_tab[$current_page]) ? $page_to_tab[$current_page] : 'Dashboard'; foreach ($tabs as $name => $slug) { $class = ( $current_tab === $name ) ? 'nav-tab-active' : ''; echo '' . esc_html($name) . ''; } } /** * Render main page */ public function render_main_page() { ?>

display_name . ' 👋'); ?>

'; ?>

'; ?>

'; ?>

'.esc_attr('contact us via the support form →', 'patternswp').'' ); ?>

clear_pattern_cache(false, false); } /** * Handle remote pattern loading based on settings */ public function maybe_disable_remote_patterns($should_load) { if (get_option('patternswp_hide_core_patterns', false)) { return false; } return $should_load; } /** * Filter block editor settings to handle pattern visibility */ public function filter_block_editor_settings($settings, $context) { if (get_option('patternswp_hide_core_patterns', false)) { $settings['__experimentalBlockPatterns'] = array(); $settings['__experimentalBlockPatternCategories'] = array(); $settings['__experimentalBlockPatterns'] = array(); } return $settings; } /** * Clear Cache */ public function patternswp_clear_cache() { global $wpdb; if (isset($_POST['patternswp_clear_cache'])) { if (!isset($_POST['patternswp_clear_cache_nonce'])) { wp_die(esc_attr__('Security check failed. Please try again.', 'patternswp')); } $nonce = sanitize_text_field(wp_unslash($_POST['patternswp_clear_cache_nonce'])); if (!wp_verify_nonce($nonce, 'patternswp_clear_cache_action')) { wp_die(esc_attr__('Security check failed. Please try again.', 'patternswp')); } if (!current_user_can('manage_options')) { wp_die(esc_attr('You do not have sufficient permissions to perform this action.', 'patternswp')); } // Delete category type transient delete_transient('patternswp_category_type'); // Delete all API-related transients $pattern = '_transient_patternswp_'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $results = $wpdb->get_col($wpdb->prepare("SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", $wpdb->esc_like($pattern) . '%')); // Delete all patterns cache $patterns = 'patterns_cache_'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name LIKE %s", $wpdb->esc_like($patterns) . '%' ) ); foreach ($results as $transient) { delete_option($transient); delete_option(str_replace('_transient_', '_transient_timeout_', $transient)); } // Load patterns in background $this->patternswp_load_patterns_by_remote_ajax(); // Set a transient for the success message set_transient('patternswp_cache_cleared', 'Cache cleared successfully', 5); // Redirect to Settings page wp_safe_redirect(admin_url('admin.php?page=patternswp-settings')); exit; } } /** * Ensure hourly cron job is scheduled */ public function patternswp_ensure_hourly_cron() { if ( ! wp_next_scheduled( 'patternswp_hourly_transient_load' ) ) { wp_schedule_event( time(), 'hourly', 'patternswp_hourly_transient_load' ); } } /** * AJAX handler to load transient data */ public function patternswp_background_transient_load_ajaxcc() { do_action( 'patternswp_hourly_transient_load' ); wp_die(); } /** * Load patterns via AJAX */ public function patternswp_load_patterns_by_remote_ajax() { $ajax_url = admin_url('admin-ajax.php'); wp_remote_post( $ajax_url , array( 'body' => array( 'action' => 'patternswp_background_transient_load_ajax' ), 'timeout' => 1, 'blocking' => false, 'sslverify' => false, )); } /** * Set transient on plugin activation */ public function patternswp_on_activation() { set_transient('patternswp_activation_redirect', true, 30); } /** * Clear pattern cache when visibility settings change */ public function clear_pattern_cache($old_value, $new_value) { // Clear object cache first if (function_exists('wp_cache_flush')) { wp_cache_flush(); } // Clear known transients $transients = [ 'patternswp_all_patterns', 'patternswp_categorized_patterns', 'patternswp_patterns_data', 'patternswp_categories' ]; foreach ($transients as $transient) { delete_transient($transient); } // Clear any remaining transients with our prefix if (!function_exists('delete_expired_transients')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // This is a core function that will clean up expired transients delete_expired_transients(true); // Clear specific pattern caches if (function_exists('wp_cache_delete')) { wp_cache_delete('all_patterns', 'patternswp'); wp_cache_delete('categorized_patterns', 'patternswp'); } } /** * Attempt to deregister any patterns from the active or child theme. */ public function maybe_deregister_theme_patterns() { $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false); error_log('[PatternsWP] maybe_deregister_theme_patterns called - hide_theme_patterns: ' . ($hide_theme_patterns ? 'true' : 'false')); if (!$hide_theme_patterns) { error_log('[PatternsWP] Theme pattern hiding is disabled'); return; } // Get the active theme. $theme = wp_get_theme(); // Let's get some theme data. $paths_to_search = array( $theme->stylesheet, ); // Check if this is a child theme. if ($theme->parent()) { $parent_theme = wp_get_theme($theme->parent()->template); $paths_to_search[] = $parent_theme->template; } $paths_to_search = array_unique($paths_to_search); error_log('[PatternsWP] Theme paths to search: ' . implode(', ', $paths_to_search)); // Get all registered patterns. $patterns = \WP_Block_Patterns_Registry::get_instance(); $all_patterns = $patterns->get_all_registered(); error_log('[PatternsWP] Total registered patterns: ' . count($all_patterns)); // Loop through all patterns and deregister any that are from the active or child theme. foreach ($all_patterns as $index => $pattern) { $file_path = $pattern['filePath'] ?? ''; if (empty($file_path)) { continue; } // Check if the pattern is from the active or child theme. foreach ($paths_to_search as $path) { if (false !== strpos($file_path, 'themes/' . $path)) { unregister_block_pattern($pattern['name']); error_log('[PatternsWP] Deregistered theme pattern: ' . $pattern['name'] . ' from path: ' . $file_path); } } } } /** * Deregister any uncategorized patterns. */ public function maybe_deregister_uncategorized_patterns() { $hide_uncategorized_enabled = get_option('patternswp_hide_uncategorized_patterns', false); error_log('[PatternsWP] maybe_deregister_uncategorized_patterns called - hide_uncategorized: ' . ($hide_uncategorized_enabled ? 'true' : 'false')); // Exit early if not enabled. if (!$hide_uncategorized_enabled) { error_log('[PatternsWP] Uncategorized pattern hiding is disabled'); return; } // Retrieve all patterns. $patterns = \WP_Block_Patterns_Registry::get_instance(); $all_patterns = $patterns->get_all_registered(); error_log('[PatternsWP] Checking ' . count($all_patterns) . ' patterns for uncategorized ones'); // Loop through all patterns and deregister any that are uncategorized. foreach ($all_patterns as $index => $pattern) { if (!isset($pattern['categories']) || empty($pattern['categories'])) { unregister_block_pattern($pattern['name']); error_log('[PatternsWP] Deregistered uncategorized pattern: ' . $pattern['name']); } else { $found = false; $block_categories = $pattern['categories'] ?? array(); foreach ($block_categories as $block_category) { $categories = \WP_Block_Pattern_Categories_Registry::get_instance(); if ($categories->is_registered($block_category)) { $found = true; } } if (!$found) { unregister_block_pattern($pattern['name']); error_log('[PatternsWP] Deregistered uncategorized pattern (invalid category): ' . $pattern['name']); } } } } /** * Deregister core patterns. */ public function maybe_deregister_core_patterns() { $hide_core_patterns = get_option('patternswp_hide_core_patterns', false); error_log('[PatternsWP] maybe_deregister_core_patterns called - hide_core_patterns: ' . ($hide_core_patterns ? 'true' : 'false')); // Exit early if not enabled. if (!$hide_core_patterns) { error_log('[PatternsWP] Core pattern hiding is disabled'); return; } // Retrieve all patterns. $patterns = \WP_Block_Patterns_Registry::get_instance(); $all_patterns = $patterns->get_all_registered(); error_log('[PatternsWP] Checking ' . count($all_patterns) . ' patterns for core ones'); // Loop through all patterns and deregister any that are core patterns. foreach ($all_patterns as $index => $pattern) { // Core patterns typically have names starting with 'core/' or no file path $pattern_name = $pattern['name'] ?? ''; $file_path = $pattern['filePath'] ?? ''; // Check if it's a core pattern $is_core_pattern = false; $reason = ''; // Method 1: Check if pattern name starts with 'core/' if (strpos($pattern_name, 'core/') === 0) { $is_core_pattern = true; $reason = 'name starts with core/'; } // Method 2: Check if file path contains wp-includes if (!$is_core_pattern && !empty($file_path) && strpos($file_path, 'wp-includes') !== false) { $is_core_pattern = true; $reason = 'file path contains wp-includes'; } // Method 3: Check if no file path (likely core pattern) if (!$is_core_pattern && empty($file_path)) { $is_core_pattern = true; $reason = 'no file path'; } if ($is_core_pattern) { unregister_block_pattern($pattern['name']); error_log('[PatternsWP] Deregistered core pattern: ' . $pattern['name'] . ' (' . $reason . ')'); } } } /** * Filter the block patterns list in the editor */ public function filter_block_patterns_list($patterns) { error_log('[PatternsWP] filter_block_patterns_list called with ' . count($patterns) . ' patterns'); $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false); $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false); $hide_core_patterns = get_option('patternswp_hide_core_patterns', false); error_log('[PatternsWP] Block patterns filter - Theme: ' . ($hide_theme_patterns ? 'true' : 'false') . ', Uncategorized: ' . ($hide_uncategorized ? 'true' : 'false') . ', Core: ' . ($hide_core_patterns ? 'true' : 'false')); // If no filters are enabled, return original patterns if (!$hide_theme_patterns && !$hide_uncategorized && !$hide_core_patterns) { return $patterns; } $filtered_patterns = array(); foreach ($patterns as $pattern) { $should_include = true; // Check theme patterns if ($hide_theme_patterns && $should_include) { // Check if pattern name starts with theme prefix if (isset($pattern['name']) && strpos($pattern['name'], 'theme-') === 0) { $should_include = false; error_log('[PatternsWP] Excluding theme pattern by name: ' . $pattern['name']); } // Check if pattern has theme file path if (isset($pattern['filePath']) && strpos($pattern['filePath'], 'themes/') !== false) { $should_include = false; error_log('[PatternsWP] Excluding theme pattern by path: ' . $pattern['name']); } } // Check uncategorized patterns if ($hide_uncategorized && $should_include) { if (!isset($pattern['categories']) || empty($pattern['categories'])) { $should_include = false; error_log('[PatternsWP] Excluding uncategorized pattern: ' . $pattern['name']); } } // Check core patterns if ($hide_core_patterns && $should_include) { if (isset($pattern['name']) && strpos($pattern['name'], 'core/') === 0) { $should_include = false; error_log('[PatternsWP] Excluding core pattern: ' . $pattern['name']); } } if ($should_include) { $filtered_patterns[] = $pattern; } } error_log('[PatternsWP] Filtered from ' . count($patterns) . ' to ' . count($filtered_patterns) . ' patterns'); return $filtered_patterns; } /** * Filter REST API response for individual blocks */ public function filter_rest_block_response($response, $post, $request) { // Only filter in the block editor context if (!isset($request['context']) || $request['context'] !== 'edit') { return $response; } $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false); $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false); $hide_core_patterns = get_option('patternswp_hide_core_patterns', false); // If no filters are enabled, return original response if (!$hide_theme_patterns && !$hide_uncategorized && !$hide_core_patterns) { return $response; } // Get the pattern data $pattern_data = $response->data; // Check if this should be hidden $should_hide = false; // Check theme patterns if ($hide_theme_patterns) { // Check if pattern has theme-specific metadata if (isset($pattern_data['patternswp_source']) && $pattern_data['patternswp_source'] === 'theme') { $should_hide = true; } } // Check uncategorized patterns if (!$should_hide && $hide_uncategorized) { if (empty($pattern_data['pattern_categories'])) { $should_hide = true; } } // Check core patterns if (!$should_hide && $hide_core_patterns) { if (isset($pattern_data['name']) && strpos($pattern_data['name'], 'core/') === 0) { $should_hide = true; } } if ($should_hide) { error_log('[PatternsWP] Filtering REST response for pattern: ' . ($pattern_data['title'] ?? 'Unknown')); // Return an error response to hide this pattern return new WP_Error( 'rest_pattern_hidden', 'Pattern is hidden by visibility settings', array('status' => 404) ); } return $response; } /** * Filter REST API patterns */ public function filter_rest_patterns($args, $request) { // Only filter if we're in the block editor context if (!isset($request['context']) || $request['context'] !== 'edit') { return $args; } // Get the current registered patterns $patterns = \WP_Block_Patterns_Registry::get_instance(); $all_patterns = $patterns->get_all_registered(); $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false); $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false); $hide_core_patterns = get_option('patternswp_hide_core_patterns', false); // If no filters are enabled, return original args if (!$hide_theme_patterns && !$hide_uncategorized && !$hide_core_patterns) { return $args; } // Get theme info for theme pattern detection $theme = wp_get_theme(); $paths_to_search = array($theme->stylesheet); if ($theme->parent()) { $parent_theme = wp_get_theme($theme->parent()->template); $paths_to_search[] = $parent_theme->template; } $paths_to_search = array_unique($paths_to_search); // Build list of pattern names to exclude $exclude_patterns = array(); foreach ($all_patterns as $pattern) { $should_exclude = false; // Check if it's a theme pattern if ($hide_theme_patterns) { $file_path = $pattern['filePath'] ?? ''; if (!empty($file_path)) { foreach ($paths_to_search as $path) { if (false !== strpos($file_path, 'themes/' . $path)) { $should_exclude = true; break; } } } } // Check if it's a core pattern if (!$should_exclude && $hide_core_patterns) { $pattern_name = $pattern['name'] ?? ''; $file_path = $pattern['filePath'] ?? ''; // Method 1: Check if pattern name starts with 'core/' if (strpos($pattern_name, 'core/') === 0) { $should_exclude = true; } // Method 2: Check if file path contains wp-includes if (!$should_exclude && !empty($file_path) && strpos($file_path, 'wp-includes') !== false) { $should_exclude = true; } // Method 3: Check if no file path (likely core pattern) if (!$should_exclude && empty($file_path)) { $should_exclude = true; } } // Check if it's uncategorized if (!$should_exclude && $hide_uncategorized) { if (!isset($pattern['categories']) || empty($pattern['categories'])) { $should_exclude = true; } else { $found = false; $block_categories = $pattern['categories'] ?? array(); foreach ($block_categories as $block_category) { $categories = \WP_Block_Pattern_Categories_Registry::get_instance(); if ($categories->is_registered($block_category)) { $found = true; break; } } if (!$found) { $should_exclude = true; } } } if ($should_exclude) { $exclude_patterns[] = $pattern['name']; } } // Add exclude filter using direct SQL for better performance if (!empty($exclude_patterns)) { add_filter('posts_where', function($where) use ($exclude_patterns) { global $wpdb; // Prepare placeholders for the IN clause $placeholders = array_fill(0, count($exclude_patterns), '%s'); $placeholders = implode(',', $placeholders); // Prepare the exclusion subquery $exclude_sql = $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = 'pattern_id' AND meta_value IN ($placeholders)", $exclude_patterns ); // Add the exclusion to the WHERE clause $where .= " AND {$wpdb->posts}.ID NOT IN ($exclude_sql)"; return $where; }); // Ensure we don't use post__not_in or meta_query unset($args['post__not_in']); unset($args['meta_query']); } return $args; } /** * Filter patterns based on visibility settings */ /** * Check if a pattern is a theme pattern */ private function is_theme_pattern($pattern) { // Check source if (!empty($pattern['source']) && $pattern['source'] === 'theme') { return true; } // Check name prefix if (!empty($pattern['name']) && (strpos($pattern['name'], 'theme-') === 0 || strpos($pattern['name'], 'tw/') === 0)) { return true; } // Check categories if (!empty($pattern['categories'])) { $categories = is_array($pattern['categories']) ? $pattern['categories'] : array_map('trim', explode(',', $pattern['categories'])); foreach ($categories as $category) { if (is_string($category) && (stripos($category, 'theme') !== false || stripos($category, 'template') !== false)) { return true; } } } return false; } /** * Check if a pattern is a core pattern */ private function is_core_pattern($pattern) { // Check source if (!empty($pattern['source']) && $pattern['source'] === 'core') { return true; } // Check name prefix if (!empty($pattern['name']) && strpos($pattern['name'], 'core/') === 0) { return true; } return false; } /** * Check if a pattern is uncategorized */ private function is_uncategorized_pattern($pattern) { if (empty($pattern['categories'])) { return true; } $categories = is_array($pattern['categories']) ? $pattern['categories'] : array_filter(array_map('trim', explode(',', $pattern['categories']))); return empty($categories); } /** * Filter patterns based on visibility settings */ public function filter_patterns_by_visibility($patterns) { // Debug logging error_log('[PatternsWP] Filter applied with ' . count((array)$patterns) . ' patterns'); // If no patterns or not an array, return early if (empty($patterns) || !is_array($patterns)) { error_log('[PatternsWP] No patterns to filter'); return $patterns; } $filtered_patterns = array(); $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false); $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false); $hide_core_patterns = get_option('patternswp_hide_core_patterns', false); error_log('[PatternsWP] Settings - Hide Theme: ' . ($hide_theme_patterns ? 'true' : 'false') . ', Hide Uncategorized: ' . ($hide_uncategorized ? 'true' : 'false') . ', Hide Core: ' . ($hide_core_patterns ? 'true' : 'false')); foreach ($patterns as $pattern) { // Skip if pattern is not an array if (!is_array($pattern)) { $filtered_patterns[] = $pattern; continue; } // Skip theme patterns if enabled if ($hide_theme_patterns && $this->is_theme_pattern($pattern)) { error_log('[PatternsWP] Skipping theme pattern: ' . ($pattern['title'] ?? 'Unknown')); continue; } // Skip core patterns if enabled if ($hide_core_patterns && $this->is_core_pattern($pattern)) { error_log('[PatternsWP] Skipping core pattern: ' . ($pattern['title'] ?? 'Unknown')); continue; } // Skip uncategorized patterns if enabled if ($hide_uncategorized && $this->is_uncategorized_pattern($pattern)) { error_log('[PatternsWP] Skipping uncategorized pattern: ' . ($pattern['title'] ?? 'Unknown')); continue; } $filtered_patterns[] = $pattern; } error_log('[PatternsWP] Filtered to ' . count($filtered_patterns) . ' patterns'); return $filtered_patterns; } /** * Register Pattern Visibility settings */ public function register_pattern_visibility_settings() { // Register settings register_setting('patternswp_visibility_settings', 'patternswp_hide_theme_patterns', array( 'type' => 'boolean', 'default' => false, 'sanitize_callback' => 'rest_sanitize_boolean', )); register_setting('patternswp_visibility_settings', 'patternswp_hide_uncategorized_patterns', array( 'type' => 'boolean', 'default' => false, 'sanitize_callback' => 'rest_sanitize_boolean', )); register_setting('patternswp_visibility_settings', 'patternswp_hide_core_patterns', array( 'type' => 'boolean', 'default' => false, 'sanitize_callback' => 'rest_sanitize_boolean', )); // Clear cache when settings are updated add_action('update_option_patternswp_hide_theme_patterns', array($this, 'clear_pattern_cache'), 10, 2); add_action('update_option_patternswp_hide_uncategorized_patterns', array($this, 'clear_pattern_cache'), 10, 2); add_action('update_option_patternswp_hide_core_patterns', array($this, 'clear_pattern_cache'), 10, 2); // Add settings section add_settings_section( 'patternswp_visibility_section', __('Pattern Visibility', 'patternswp'), array($this, 'pattern_visibility_section_callback'), 'patternswp-settings' ); // Add settings fields add_settings_field( 'patternswp_hide_theme_patterns', __('Hide Theme Patterns', 'patternswp'), array($this, 'render_checkbox_field'), 'patternswp-settings', 'patternswp_visibility_section', array( 'label_for' => 'patternswp_hide_theme_patterns', 'description' => __('Prevent patterns registered by the active theme from displaying in the patterns list.', 'patternswp') ) ); add_settings_field( 'patternswp_hide_uncategorized_patterns', __('Hide Uncategorized Patterns', 'patternswp'), array($this, 'render_checkbox_field'), 'patternswp-settings', 'patternswp_visibility_section', array( 'label_for' => 'patternswp_hide_uncategorized_patterns', 'description' => __('Prevent any patterns not in any registered categories from displaying.', 'patternswp') ) ); add_settings_field( 'patternswp_hide_core_patterns', __('Hide Core Patterns', 'patternswp'), array($this, 'render_checkbox_field'), 'patternswp-settings', 'patternswp_visibility_section', array( 'label_for' => 'patternswp_hide_core_patterns', 'description' => __('Remove all core patterns from the pattern selector by disabling core patterns.', 'patternswp') ) ); } /** * Pattern Visibility section callback */ public function pattern_visibility_section_callback() { echo '

' . esc_html__('Control which patterns are visible in the block editor.', 'patternswp') . '

'; } /** * Render checkbox field */ public function render_checkbox_field($args) { $option = get_option($args['label_for']); ?>