PluginProbe
PatternsWP – Gutenberg Block Patterns & Page Templates Library / 1.1.0
PatternsWP – Gutenberg Block Patterns & Page Templates Library v1.1.0
1.1.0 trunk 1.0.0 1.0.1 1.0.10 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9
← All changes | includes/class-patternswp-admin.php +1061 -169 1.0.61.1.0 View file →
@@ -2,8 +2,13 @@
2 2 /**
3 3 * Admin Page
4 4 */
5 5
6 +// Exit if accessed directly
7 +if (!defined('ABSPATH')) {
8 + exit;
9 +}
10 +
6 11 class PatternsWP_Admin {
7 12
8 13 /**
9 14 * Constructor
@@ -10,16 +15,296 @@
10 15 */
11 16 public function __construct() {
12 17 // Add menu and pages
13 18 add_action('admin_menu', array($this, 'add_admin_menu'));
19 + add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
14 20 add_action('admin_init', array($this, 'patternswp_clear_cache'));
15 - add_action( 'admin_init', array( $this, 'patternswp_ensure_hourly_cron' ) );
16 - add_action( 'wp_ajax_patternswp_background_transient_load_ajax', array( $this, 'patternswp_background_transient_load_ajaxcc' ) );
17 - add_action( 'wp_ajax_nopriv_patternswp_background_transient_load_ajax', array( $this, 'patternswp_background_transient_load_ajaxcc' ) );
18 - add_action( 'patternswp_load_patterns_by_ra', array( $this, 'patternswp_load_patterns_by_remote_ajax' ) );
21 + add_action('admin_init', array($this, 'patternswp_ensure_daily_cron'));
22 + add_action('wp_ajax_patternswp_background_transient_load_ajax', array($this, 'patternswp_background_transient_load_ajaxcc'));
23 + add_action('wp_ajax_patternswp_save_settings', array($this, 'ajax_save_settings'));
24 + add_action('wp_ajax_patternswp_clear_cache_ajax', array($this, 'ajax_clear_cache'));
25 + add_action('patternswp_load_patterns_by_ra', array($this, 'patternswp_load_patterns_by_remote_ajax'));
26 + register_activation_hook(plugin_dir_path(__DIR__) . 'patternswp.php', array($this, 'patternswp_on_activation'));
27 + add_action('admin_init', array($this, 'patternswp_redirect_on_activation'));
28 +
29 + // Initialize settings
30 + add_action('admin_init', array($this, 'register_pattern_visibility_settings'));
31 +
32 + // Add pattern deregistration hooks
33 + add_action('init', array($this, 'maybe_deregister_core_patterns'), 999);
34 + add_action('init', array($this, 'maybe_deregister_theme_patterns'), 1000);
35 + add_action('init', array($this, 'maybe_deregister_uncategorized_patterns'), 1001);
36 +
37 + // Add a second check later to ensure patterns are deregistered
38 + add_action('init', array($this, 'maybe_deregister_theme_patterns'), 9999);
39 + add_action('init', array($this, 'maybe_deregister_uncategorized_patterns'), 10000);
40 + add_action('init', array($this, 'maybe_deregister_core_patterns'), 10001);
41 +
42 + // Apply filters for different pattern sources
43 + add_filter('patternswp_patterns', array($this, 'filter_patterns_by_visibility'), 20);
44 + add_filter('block_editor_settings_all', array($this, 'filter_block_editor_settings'), 10, 2);
45 +
46 + // Filter REST API patterns
47 + add_filter('rest_wp_block_query', array($this, 'filter_rest_patterns'), 10, 2);
48 +
49 + // Handle core and theme patterns
50 + add_filter('should_load_remote_block_patterns', array($this, 'maybe_disable_remote_patterns'), 10, 1);
51 + add_filter('should_load_remote_patterns_default_override', array($this, 'maybe_disable_remote_patterns'), 10, 1);
52 +
53 + // Add action to clear pattern cache when theme changes
54 + add_action('switch_theme', array($this, 'clear_pattern_cache_on_theme_switch'));
55 +
56 + // Also filter the REST API response directly
57 + add_filter('rest_prepare_wp_block', array($this, 'filter_rest_block_response'), 10, 3);
58 +
59 + // Filter the block patterns list in the editor
60 + add_filter('wp_block_patterns', array($this, 'filter_block_patterns_list'), 10, 1);
61 +
62 + // Add a test hook to verify settings are loaded
63 + add_action('wp_loaded', array($this, 'test_settings'));
19 64 }
20 65
21 66 /**
67 + * Whether the current screen is a PatternsWP admin page.
68 + *
69 + * @param string $hook_suffix Current admin page hook.
70 + * @return bool
71 + */
72 + private function is_patternswp_admin_page( $hook_suffix ) {
73 + $pages = array(
74 + 'toplevel_page_patternswp-plugin-menu',
75 + 'patternswp_page_patternswp-settings',
76 + 'patternswp_page_patternswp-plugin-page-1',
77 + 'patternswp_page_patternswp-license_section',
78 + );
79 + return in_array( $hook_suffix, $pages, true );
80 + }
81 +
82 + /**
83 + * Map menu slug to React page id.
84 + *
85 + * @return string
86 + */
87 + private function get_current_admin_page_id() {
88 + $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : 'patternswp-plugin-menu'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
89 + $map = array(
90 + 'patternswp-plugin-menu' => 'dashboard',
91 + 'patternswp-settings' => 'settings',
92 + 'patternswp-plugin-page-1' => 'support',
93 + 'patternswp-license_section' => 'license',
94 + );
95 + return isset( $map[ $page ] ) ? $map[ $page ] : 'dashboard';
96 + }
97 +
98 + /**
99 + * Enqueue React admin assets on PatternsWP screens.
100 + *
101 + * @param string $hook_suffix Current admin page hook.
102 + */
103 + public function enqueue_admin_assets( $hook_suffix ) {
104 + if ( ! $this->is_patternswp_admin_page( $hook_suffix ) ) {
105 + return;
106 + }
107 +
108 + $script_path = 'assets/js/patternswp-admin.js';
109 + $style_path = 'assets/css/patternswp-admin.css';
110 +
111 + wp_enqueue_style( 'wp-components' );
112 + wp_enqueue_style(
113 + 'patternswp-admin-styles',
114 + PWP_PLUGIN_URL . $style_path,
115 + array( 'wp-components' ),
116 + patternswp_asset_version( $style_path )
117 + );
118 +
119 + wp_enqueue_script(
120 + 'patternswp-admin-scripts',
121 + PWP_PLUGIN_URL . $script_path,
122 + array(
123 + 'wp-element',
124 + 'wp-components',
125 + 'wp-i18n',
126 + 'wp-api-fetch',
127 + 'wp-dom-ready',
128 + 'wp-primitives',
129 + ),
130 + patternswp_asset_version( $script_path ),
131 + true
132 + );
133 +
134 + $license_option = get_option( 'patternswp_license_key', array() );
135 + $license_key = isset( $license_option['patternswp_pro_license_key'] ) ? $license_option['patternswp_pro_license_key'] : '';
136 + $is_active = PatternsWP_API_Section::get_instance()->is_license_active();
137 +
138 + if ( ! empty( $license_key ) && strlen( $license_key ) > 8 ) {
139 + $masked_key = substr( $license_key, 0, 8 ) . str_repeat( 'X', strlen( $license_key ) - 8 );
140 + } else {
141 + $masked_key = $license_key;
142 + }
143 +
144 + $initial_notice = null;
145 + if ( isset( $_GET['pt_msg'] ) && ! empty( $_GET['pt_msg'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
146 + $status = isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : 'info'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
147 + $notice_status = 'info';
148 + if ( 'success' === $status ) {
149 + $notice_status = 'success';
150 + } elseif ( 'error' === $status ) {
151 + $notice_status = 'error';
152 + }
153 + $initial_notice = array(
154 + 'status' => $notice_status,
155 + 'message' => sanitize_text_field( urldecode( wp_unslash( $_GET['pt_msg'] ) ) ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
156 + );
157 + }
158 +
159 + $images_base = plugin_dir_url( __FILE__ );
160 +
161 + wp_localize_script(
162 + 'patternswp-admin-scripts',
163 + 'patternswpAdmin',
164 + array(
165 + 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
166 + 'adminUrl' => admin_url( 'admin.php' ),
167 + 'newPageUrl' => admin_url( 'post-new.php?post_type=page' ),
168 + 'nonce' => wp_create_nonce( 'patternswp_admin_nonce' ),
169 + 'currentPage' => $this->get_current_admin_page_id(),
170 + 'userName' => wp_get_current_user()->display_name,
171 + 'pluginVersion' => PWP_P_VERSION,
172 + 'isLicenseActive' => (bool) $is_active,
173 + 'settings' => array(
174 + 'hideThemePatterns' => (bool) rest_sanitize_boolean( get_option( 'patternswp_hide_theme_patterns', false ) ),
175 + 'hideUncategorizedPatterns' => (bool) rest_sanitize_boolean( get_option( 'patternswp_hide_uncategorized_patterns', false ) ),
176 + 'hideCorePatterns' => (bool) rest_sanitize_boolean( get_option( 'patternswp_hide_core_patterns', false ) ),
177 + ),
178 + 'license' => array(
179 + 'maskedKey' => $masked_key,
180 + 'activated' => (bool) $is_active,
181 + ),
182 + 'images' => array(
183 + 'step1' => $images_base . 'pwp-welcome-01.png',
184 + 'step2' => $images_base . 'pwp-welcome-02.png',
185 + 'step3' => $images_base . 'pwp-welcome-03.png',
186 + ),
187 + 'initialNotice' => $initial_notice,
188 + )
189 + );
190 + }
191 +
192 + /**
193 + * Shared React mount markup for all admin pages.
194 + *
195 + * @param string $page_title Screen reader / fallback title.
196 + */
197 + private function render_admin_app( $page_title ) {
198 + if ( ! current_user_can( 'manage_options' ) ) {
199 + return;
200 + }
201 + ?>
202 + <div class="wrap patternswp-admin-wrap">
203 + <h1 class="screen-reader-text"><?php echo esc_html( $page_title ); ?></h1>
204 + <div id="patternswp-admin-root">
205 + <div class="patternswp-admin__loading">
206 + <span class="spinner is-active" style="float:none;margin:0;"></span>
207 + </div>
208 + </div>
209 + </div>
210 + <?php
211 + }
212 +
213 + /**
214 + * AJAX: save pattern visibility settings.
215 + */
216 + public function ajax_save_settings() {
217 + if ( ! current_user_can( 'manage_options' ) ) {
218 + wp_send_json_error( array( 'message' => __( 'You do not have permission to manage these settings.', 'patternswp' ) ), 403 );
219 + }
220 +
221 + check_ajax_referer( 'patternswp_admin_nonce', 'nonce' );
222 +
223 + $hide_theme = ! empty( $_POST['hide_theme_patterns'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['hide_theme_patterns'] ) );
224 + $hide_uncat = ! empty( $_POST['hide_uncategorized_patterns'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['hide_uncategorized_patterns'] ) );
225 + $hide_core = ! empty( $_POST['hide_core_patterns'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['hide_core_patterns'] ) );
226 +
227 + update_option( 'patternswp_hide_theme_patterns', $hide_theme ? 1 : 0, false );
228 + update_option( 'patternswp_hide_uncategorized_patterns', $hide_uncat ? 1 : 0, false );
229 + update_option( 'patternswp_hide_core_patterns', $hide_core ? 1 : 0, false );
230 +
231 + wp_send_json_success(
232 + array(
233 + 'message' => __( 'Settings saved.', 'patternswp' ),
234 + 'settings' => array(
235 + 'hideThemePatterns' => $hide_theme,
236 + 'hideUncategorizedPatterns' => $hide_uncat,
237 + 'hideCorePatterns' => $hide_core,
238 + ),
239 + )
240 + );
241 + }
242 +
243 + /**
244 + * AJAX: clear pattern cache.
245 + */
246 + public function ajax_clear_cache() {
247 + global $wpdb;
248 +
249 + if ( ! current_user_can( 'manage_options' ) ) {
250 + wp_send_json_error( array( 'message' => __( 'You do not have permission to clear the cache.', 'patternswp' ) ), 403 );
251 + }
252 +
253 + check_ajax_referer( 'patternswp_admin_nonce', 'nonce' );
254 +
255 + try {
256 + delete_transient( 'patternswp_category_type' );
257 +
258 + $pattern = '_transient_patternswp_';
259 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
260 + $results = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", $wpdb->esc_like( $pattern ) . '%' ) );
261 +
262 + $patterns = 'patterns_cache_';
263 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
264 + $wpdb->query(
265 + $wpdb->prepare(
266 + "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
267 + $wpdb->esc_like( $patterns ) . '%'
268 + )
269 + );
270 +
271 + foreach ( $results as $transient ) {
272 + delete_option( $transient );
273 + delete_option( str_replace( '_transient_', '_transient_timeout_', $transient ) );
274 + }
275 +
276 + $this->clear_pattern_cache( false, false );
277 + $api = PatternsWP_API_Section::get_instance();
278 + $api->purge_library_caches();
279 + $api->schedule_library_refresh( 1 );
280 + } catch ( \Throwable $e ) {
281 + wp_send_json_error(
282 + array(
283 + 'message' => __( 'Could not clear the cache. Please try again.', 'patternswp' ),
284 + )
285 + );
286 + }
287 +
288 + wp_send_json_success(
289 + array(
290 + 'message' => __( 'Cache cleared. Patterns will reload in the background.', 'patternswp' ),
291 + )
292 + );
293 + }
294 +
295 + /**
296 + * Test method to verify settings are loaded
297 + */
298 + public function test_settings() {
299 + $hide_theme = get_option('patternswp_hide_theme_patterns', false);
300 + $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
301 + $hide_core = get_option('patternswp_hide_core_patterns', false);
302 +
303 + // error_log('[PatternsWP] Settings Check - Theme: ' . ($hide_theme ? 'true' : 'false') . ', Uncategorized: ' . ($hide_uncategorized ? 'true' : 'false') . ', Core: ' . ($hide_core ? 'true' : 'false'));
304 + }
305 +
306 + /**
22 307 * Add admin menu
23 308 */
24 309 public function add_admin_menu() {
25 310 // Custom SVG icon (base64 encoded)
@@ -45,10 +330,10 @@
45 330 );
46 331
47 332 // Add subpages
48 333 $this->add_subpage($menu_slug, 'Dashboard', 'Dashboard', 'patternswp-plugin-menu', array($this, 'render_main_page'));
334 + $this->add_subpage($menu_slug, 'Settings', 'Settings', 'patternswp-settings', array($this, 'render_pattern_visibility_page'));
49 335 $this->add_subpage($menu_slug, 'Support', 'Support', 'patternswp-plugin-page-1', array($this, 'patternswp_support'));
50 - $this->add_subpage($menu_slug, 'Clear Cache', 'Clear Cache', 'patternswp-clear-cache', array($this, 'patternswp_clear_cache_form'));
51 336 }
52 337
53 338 /**
54 339 * Add subpage
@@ -63,15 +348,29 @@
63 348 */
64 349 public function patterswp_tab( $tab ) {
65 350 $tabs = array(
66 351 'Dashboard' => 'patternswp-plugin-menu',
352 + 'Settings' => 'patternswp-settings',
67 353 'Support' => 'patternswp-plugin-page-1',
68 - 'Clear Cache' => 'patternswp-clear-cache',
69 354 'License' => 'patternswp-license_section'
70 355 );
71 356
357 + // Get the current page from URL parameter (read-only tab highlight; no state change).
358 + $current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : 'patternswp-plugin-menu'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
359 +
360 + // Map page slugs to tab names
361 + $page_to_tab = array(
362 + 'patternswp-plugin-menu' => 'Dashboard',
363 + 'patternswp-settings' => 'Settings',
364 + 'patternswp-plugin-page-1' => 'Support',
365 + 'patternswp-license_section' => 'License'
366 + );
367 +
368 + // Determine current tab
369 + $current_tab = isset($page_to_tab[$current_page]) ? $page_to_tab[$current_page] : 'Dashboard';
370 +
72 371 foreach ($tabs as $name => $slug) {
73 - $class = ( $tab === $name ) ? 'nav-tab-active' : '';
372 + $class = ( $current_tab === $name ) ? 'nav-tab-active' : '';
74 373 echo '<a href="?page=' . esc_attr($slug) . '" class="nav-tab ' . esc_attr($class) . '">' . esc_html($name) . '</a>';
75 374 }
76 375 }
77 376
@@ -77,160 +376,56 @@
77 376
78 377 /**
79 378 * Render main page
80 379 */
81 - public function render_main_page() { ?>
82 - <div class="wrap">
83 - <h1><?php esc_html_e('PatternsWP', 'patternswp'); ?></h1>
84 - <h2 class="nav-tab-wrapper">
85 - <?php $this->patterswp_tab( 'Dashboard' ); ?>
86 - </h2>
87 - <div class="patterns-wp-tabs-content">
88 - <div id="tab-1" class="patterns-wp-tab-content patterns-wp-tab-active">
89 - <div class="wrap">
90 - <div class="pw-feature-box big-box" style="max-width: 1280px; padding: 40px; background-color: white; border-radius: 10px; overflow: hidden;">
91 - <div class="about__section has-1-columns">
92 - <div class="column" style="text-align:center;">
93 - <h4><?php echo esc_html('Hello, ' . wp_get_current_user()->display_name . ' 👋'); ?></h4>
94 - <h1 class="feature-title"><?php esc_html_e('Welcome to PatternsWP', 'patternswp'); ?></h1>
95 - <p><?php esc_html_e('Thanks for choosing PatternsWP! Follow these three simple steps to get started!', 'patternswp'); ?></p>
96 - </div>
97 - </div>
98 - <div class="about__section has-3-columns">
99 - <div class="column" style="padding: 40px;">
100 - <div class="about__image">
101 - <?php
102 - // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
103 - echo '<img src="' . esc_url(plugin_dir_url(__FILE__) . 'pwp-welcome-01.png') . '" alt="" height="auto" width="100%">';
104 - ?>
105 - </div>
106 - <h4><?php esc_html_e('01. Open the PatternsWP Library', 'patternswp'); ?></h4>
107 - <p><?php esc_html_e('When editing a page or post in the block editor, locate the PatternsWP Library button in the editor’s header. Click it to access a collection of pre-designed patterns and full-page templates.', 'patternswp'); ?></p>
108 - </div>
109 - <div class="column" style="padding: 40px;">
110 - <div class="about__image">
111 - <?php
112 - // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
113 - echo '<img src="' . esc_url(plugin_dir_url(__FILE__) . 'pwp-welcome-02.png') . '" alt="" height="auto" width="100%">';
114 - ?>
115 - </div>
116 - <h4><?php esc_html_e('02. Browse Patterns & Templates', 'patternswp'); ?></h4>
117 - <p><?php esc_html_e('Explore a diverse range of block patterns and full-page layouts. Use the search box or filter by category to quickly find the perfect design for your website.', 'patternswp'); ?></p>
118 - </div>
119 - <div class="column" style="padding: 40px;">
120 - <div class="about__image">
121 - <?php
122 - // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
123 - echo '<img src="' . esc_url(plugin_dir_url(__FILE__) . 'pwp-welcome-03.png') . '" alt="" height="auto" width="100%">';
124 - ?>
125 - </div>
126 - <h4><?php esc_html_e('03. Add Patterns & Customize', 'patternswp'); ?></h4>
127 - <p><?php esc_html_e('Once you’ve found the right pattern, add it to your page with a single click. Every pattern is fully customizable, allowing you to tweak colors, typography, and content effortlessly.', 'patternswp'); ?></p>
128 - </div>
129 - </div>
130 - </div>
131 - </div>
132 - </div>
133 - </div>
134 - </div>
135 - <?php }
380 + public function render_main_page() {
381 + $this->render_admin_app( __( 'PatternsWP', 'patternswp' ) );
382 + }
136 383
137 384 /**
138 385 * Support page
139 386 */
140 - public function patternswp_support() { ?>
141 - <div class="wrap">
142 - <h1><?php esc_html_e('Support', 'patternswp'); ?></h1>
143 - <h2 class="nav-tab-wrapper">
144 - <?php $this->patterswp_tab( 'Support' ); ?>
145 - </h2>
146 - <div class="patterns-wp-tabs-content">
147 - <div id="tab-1" class="patterns-wp-tab-content patterns-wp-tab-active">
148 - </div>
149 - <div id="tab-2" class="patterns-wp-tab-content">
150 - <div class="wrap">
151 - <div class="pw-feature-box big-box" style="max-width: 1280px; padding: 40px; background-color: white; border-radius: 10px; overflow: hidden;">
152 - <div class="about__section has-1-columns">
153 - <div class="column" style="">
154 - <h1 class="feature-title"><?php esc_html_e('Support', 'patternswp'); ?></h1>
155 - <p><?php esc_html_e('Need help with PatternsWP? Whether you have a question, need technical assistance, or just want to reach out, we’re here for you. Contact us, and we’ll be happy to assist!', 'patternswp'); ?></p>
156 - </div>
157 - <div style="height: 1px; background-color: #ccc; width: 100%; margin: 20px 0;"></div>
158 - </div>
159 - <div class="about__section has-2-columns">
160 - <div class="column" style="padding-right: 100px;">
161 -
162 - <h4><?php esc_html_e('Frequently asked questions', 'patternswp'); ?></h4>
163 - <p>
164 - <?php
165 - printf(
166 - esc_attr('Here, you will find answers to commonly asked questions about using PatternsWP. If you need further assistance, feel free to %s.', 'patternswp'),
167 - '<a href="https://thepatternswp.com/contact/" target="_blank">'.esc_attr('contact us via the support form →', 'patternswp').'</a>'
168 - );
169 - ?>
170 - </p>
171 - </div>
172 - <div class="column" style="padding: 0px;">
173 - <h5>
174 - <a href="https://thepatternswp.com/docs/getting-started-with-patternswp/" target="_blank" style="color: #3858e9; text-decoration: none;">
175 - <?php esc_html_e('Getting Started with PatternsWP →', 'patternswp'); ?>
176 - </a>
177 - </h5>
178 - <div style="height: 1px; background-color: #ccc; width: 100%; margin: 10px 0;"></div>
179 - <h5>
180 - <a href="https://thepatternswp.com/docs/how-to-install-patternswp/" target="_blank" style="color: #3858e9; text-decoration: none;">
181 - <?php esc_html_e('How to install PatternsWP →', 'patternswp'); ?>
182 - </a>
183 - </h5> <div style="height: 1px; background-color: #ccc; width: 100%; margin: 10px 0;"></div>
184 - <h5>
185 - <a href="https://thepatternswp.com/docs/how-to-upgrade-patternswp-to-pro/" target="_blank" style="color: #3858e9; text-decoration: none;">
186 - <?php esc_html_e('How to Upgrade PatternsWP to Pro →', 'patternswp'); ?>
187 - </a>
188 - </h5> <div style="height: 1px; background-color: #ccc; width: 100%; margin: 10px 0;"></div>
189 - <h5>
190 - <a href="https://thepatternswp.com/docs/patternswp-support/" target="_blank" style="color: #3858e9; text-decoration: none;">
191 - <?php esc_html_e('PatternsWP Support →', 'patternswp'); ?>
192 - </a>
193 - </h5> <div style="height: 1px; background-color: #ccc; width: 100%; margin: 10px 0;"></div>
194 - </div>
195 -
196 - </div>
197 - </div>
198 - </div>
199 - </div>
200 - </div>
201 - </div>
202 - <?php }
387 + public function patternswp_support() {
388 + $this->render_admin_app( __( 'Support', 'patternswp' ) );
389 + }
203 390
204 391 /**
205 392 * Clear Cache page
206 393 */
207 - public function patternswp_clear_cache_form() { ?>
208 - <div class="wrap">
209 - <h1><?php esc_html_e('Clear Cache', 'patternswp'); ?></h1>
210 - <h2 class="nav-tab-wrapper">
211 - <?php $this->patterswp_tab( 'Clear Cache' ); ?>
212 - </h2>
213 - <div class="patterns-wp-tabs-content">
214 - <div id="tab-1" class="patterns-wp-tab-content patterns-wp-tab-active">
215 - <div class="wrap">
216 - <div class="container" style="padding: 20px; background-color: white; border-radius: 5px; overflow: hidden; width: 70%;">
217 - <div class="">
218 - <form id="patternswp_clearcache_form" method="post">
219 - <?php wp_nonce_field('patternswp_clear_cache_action', 'patternswp_clear_cache_nonce'); ?>
220 - <input name="patternswp_clear_cache" type="submit" class="button button-primary" value="Clear Cache">
221 - <div>
222 - </div>
223 - </form>
224 - </div>
225 - </div>
226 - </div>
227 - </div>
228 - </div>
229 - </div>
230 - <?php }
394 + public function patternswp_clear_cache_form() {
395 + $this->render_admin_app( __( 'Clear Cache', 'patternswp' ) );
396 + }
231 397
232 398 /**
399 + * Clear pattern cache when theme is switched
400 + */
401 + public function clear_pattern_cache_on_theme_switch() {
402 + $this->clear_pattern_cache(false, false);
403 + }
404 +
405 + /**
406 + * Handle remote pattern loading based on settings
407 + */
408 + public function maybe_disable_remote_patterns($should_load) {
409 + if (get_option('patternswp_hide_core_patterns', false)) {
410 + return false;
411 + }
412 + return $should_load;
413 + }
414 +
415 + /**
416 + * Filter block editor settings to handle pattern visibility
417 + */
418 + public function filter_block_editor_settings($settings, $context) {
419 + if (get_option('patternswp_hide_core_patterns', false)) {
420 + $settings['__experimentalBlockPatterns'] = array();
421 + $settings['__experimentalBlockPatternCategories'] = array();
422 + $settings['__experimentalBlockPatterns'] = array();
423 + }
424 + return $settings;
425 + }
426 +
427 + /**
233 428 * Clear Cache
234 429 */
235 430 public function patternswp_clear_cache() {
236 431 global $wpdb;
@@ -246,9 +441,9 @@
246 441 wp_die(esc_attr__('Security check failed. Please try again.', 'patternswp'));
247 442 }
248 443
249 444 if (!current_user_can('manage_options')) {
250 - wp_die(esc_attr('You do not have sufficient permissions to perform this action.', 'patternswp'));
445 + wp_die(esc_html__('You do not have sufficient permissions to perform this action.', 'patternswp'));
251 446 }
252 447
253 448 // Delete category type transient
254 449 delete_transient('patternswp_category_type');
@@ -272,27 +467,34 @@
272 467 delete_option($transient);
273 468 delete_option(str_replace('_transient_', '_transient_timeout_', $transient));
274 469 }
275 470
276 - //load patterns in background
277 - $this->patternswp_load_patterns_by_remote_ajax();
471 + // Load patterns in background
472 + $api = PatternsWP_API_Section::get_instance();
473 + $api->purge_library_caches();
474 + $api->schedule_library_refresh( 1 );
278 475
279 - wp_redirect(add_query_arg(array(
280 - 'page' => 'patternswp-clear-cache',
281 - 'pt_msg' => urlencode('Clear Cache Successfully'),
282 - 'status' => 'success'
283 - ), admin_url('admin.php')));
476 + // Set a transient for the success message
477 + set_transient('patternswp_cache_cleared', 'Cache cleared successfully', 5);
478 +
479 + // Redirect to Settings page
480 + wp_safe_redirect(admin_url('admin.php?page=patternswp-settings'));
284 481 exit;
285 482 }
286 483 }
287 484
288 485 /**
289 - * Ensure hourly cron job is scheduled
486 + * Ensure daily cron job is scheduled
290 487 */
291 - public function patternswp_ensure_hourly_cron() {
292 - if ( ! wp_next_scheduled( 'patternswp_hourly_transient_load' ) ) {
293 - wp_schedule_event( time(), 'hourly', 'patternswp_hourly_transient_load' );
488 + public function patternswp_ensure_daily_cron() {
489 + if ( wp_next_scheduled( 'patternswp_hourly_transient_load' ) ) {
490 + wp_clear_scheduled_hook( 'patternswp_hourly_transient_load' );
294 491 }
492 + if ( ! wp_next_scheduled( 'patternswp_daily_transient_load' ) ) {
493 + wp_schedule_event( time(), 'daily', 'patternswp_daily_transient_load' );
494 + }
495 +
496 + PatternsWP_API_Section::get_instance()->maybe_schedule_warm();
295 497 }
296 498
297 499 /**
298 500 * AJAX handler to load transient data
@@ -297,23 +499,713 @@
297 499 /**
298 500 * AJAX handler to load transient data
299 501 */
300 502 public function patternswp_background_transient_load_ajaxcc() {
301 - do_action( 'patternswp_hourly_transient_load' );
503 + if ( ! current_user_can( 'manage_options' ) ) {
504 + wp_die( -1, 403 );
505 + }
506 +
507 + check_ajax_referer( 'patternswp_admin_nonce', 'nonce' );
508 +
509 + do_action( 'patternswp_daily_transient_load' );
302 510 wp_die();
303 511 }
304 512
305 513 /**
306 - * Load patterns via AJAX
514 + * Warm pattern transients without an unauthenticated HTTP loopback.
307 515 */
308 516 public function patternswp_load_patterns_by_remote_ajax() {
309 - $ajax_url = admin_url('admin-ajax.php');
310 - wp_remote_post( $ajax_url , array(
311 - 'body' => array( 'action' => 'patternswp_background_transient_load_ajax' ),
312 - 'timeout' => 1,
313 - 'blocking' => false,
314 - 'sslverify' => false,
517 + do_action( 'patternswp_daily_transient_load' );
518 + }
519 +
520 + /**
521 + * Set transient on plugin activation
522 + */
523 + public function patternswp_on_activation() {
524 + set_transient('patternswp_activation_redirect', true, 30);
525 + PatternsWP_API_Section::get_instance()->schedule_library_refresh( 3 );
526 + }
527 +
528 + /**
529 + * Clear pattern cache when visibility settings change
530 + */
531 + public function clear_pattern_cache($old_value, $new_value) {
532 + // Clear known transients
533 + $transients = [
534 + 'patternswp_all_patterns',
535 + 'patternswp_categorized_patterns',
536 + 'patternswp_patterns_data',
537 + 'patternswp_categories',
538 + 'patternswp_api_token',
539 + 'patternswp_library_lock',
540 + 'patternswp_lib_free_count',
541 + 'patternswp_lib_pro_count',
542 + ];
543 +
544 + foreach ($transients as $transient) {
545 + delete_transient($transient);
546 + }
547 +
548 + // Clear any remaining transients with our prefix
549 + if (!function_exists('delete_expired_transients')) {
550 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
551 + }
552 +
553 + // This is a core function that will clean up expired transients
554 + delete_expired_transients(true);
555 +
556 + // Clear specific pattern caches
557 + if (function_exists('wp_cache_delete')) {
558 + wp_cache_delete('all_patterns', 'patternswp');
559 + wp_cache_delete('categorized_patterns', 'patternswp');
560 + }
561 + }
562 +
563 + /**
564 + * Attempt to deregister any patterns from the active or child theme.
565 + */
566 + public function maybe_deregister_theme_patterns() {
567 + $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
568 + // error_log('[PatternsWP] maybe_deregister_theme_patterns called - hide_theme_patterns: ' . ($hide_theme_patterns ? 'true' : 'false'));
569 +
570 + if (!$hide_theme_patterns) {
571 + // error_log('[PatternsWP] Theme pattern hiding is disabled');
572 + return;
573 + }
574 +
575 + // Get the active theme.
576 + $theme = wp_get_theme();
577 +
578 + // Let's get some theme data.
579 + $paths_to_search = array(
580 + $theme->stylesheet,
581 + );
582 +
583 + // Check if this is a child theme.
584 + if ($theme->parent()) {
585 + $parent_theme = wp_get_theme($theme->parent()->template);
586 + $paths_to_search[] = $parent_theme->template;
587 + }
588 + $paths_to_search = array_unique($paths_to_search);
589 +
590 + // error_log('[PatternsWP] Theme paths to search: ' . implode(', ', $paths_to_search));
591 +
592 + // Get all registered patterns.
593 + $patterns = \WP_Block_Patterns_Registry::get_instance();
594 + $all_patterns = $patterns->get_all_registered();
595 +
596 + // error_log('[PatternsWP] Total registered patterns: ' . count($all_patterns));
597 +
598 + // Loop through all patterns and deregister any that are from the active or child theme.
599 + foreach ($all_patterns as $index => $pattern) {
600 + $file_path = $pattern['filePath'] ?? '';
601 + if (empty($file_path)) {
602 + continue;
603 + }
604 +
605 + // Check if the pattern is from the active or child theme.
606 + foreach ($paths_to_search as $path) {
607 + if (false !== strpos($file_path, 'themes/' . $path)) {
608 + unregister_block_pattern($pattern['name']);
609 + // error_log('[PatternsWP] Deregistered theme pattern: ' . $pattern['name'] . ' from path: ' . $file_path);
610 + }
611 + }
612 + }
613 + }
614 +
615 + /**
616 + * Deregister any uncategorized patterns.
617 + */
618 + public function maybe_deregister_uncategorized_patterns() {
619 + $hide_uncategorized_enabled = get_option('patternswp_hide_uncategorized_patterns', false);
620 + // error_log('[PatternsWP] maybe_deregister_uncategorized_patterns called - hide_uncategorized: ' . ($hide_uncategorized_enabled ? 'true' : 'false'));
621 +
622 + // Exit early if not enabled.
623 + if (!$hide_uncategorized_enabled) {
624 + // error_log('[PatternsWP] Uncategorized pattern hiding is disabled');
625 + return;
626 + }
627 +
628 + // Retrieve all patterns.
629 + $patterns = \WP_Block_Patterns_Registry::get_instance();
630 + $all_patterns = $patterns->get_all_registered();
631 +
632 + // error_log('[PatternsWP] Checking ' . count($all_patterns) . ' patterns for uncategorized ones');
633 +
634 + // Loop through all patterns and deregister any that are uncategorized.
635 + foreach ($all_patterns as $index => $pattern) {
636 + if (!isset($pattern['categories']) || empty($pattern['categories'])) {
637 + unregister_block_pattern($pattern['name']);
638 + // error_log('[PatternsWP] Deregistered uncategorized pattern: ' . $pattern['name']);
639 + } else {
640 + $found = false;
641 + $block_categories = $pattern['categories'] ?? array();
642 + foreach ($block_categories as $block_category) {
643 + $categories = \WP_Block_Pattern_Categories_Registry::get_instance();
644 + if ($categories->is_registered($block_category)) {
645 + $found = true;
646 + }
647 + }
648 + if (!$found) {
649 + unregister_block_pattern($pattern['name']);
650 + // error_log('[PatternsWP] Deregistered uncategorized pattern (invalid category): ' . $pattern['name']);
651 + }
652 + }
653 + }
654 + }
655 +
656 + /**
657 + * Check if a registered block pattern belongs to PatternsWP.
658 + */
659 + private function is_patternswp_registry_pattern( $pattern_name ) {
660 + return strpos( $pattern_name, 'patternswp-gutenberg-block-patterns/' ) === 0
661 + || strpos( $pattern_name, 'patternswp/' ) === 0;
662 + }
663 +
664 + /**
665 + * Deregister core patterns.
666 + */
667 + public function maybe_deregister_core_patterns() {
668 + $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
669 + // error_log('[PatternsWP] maybe_deregister_core_patterns called - hide_core_patterns: ' . ($hide_core_patterns ? 'true' : 'false'));
670 +
671 + // Exit early if not enabled.
672 + if (!$hide_core_patterns) {
673 + // error_log('[PatternsWP] Core pattern hiding is disabled');
674 + return;
675 + }
676 +
677 + // Retrieve all patterns.
678 + $patterns = \WP_Block_Patterns_Registry::get_instance();
679 + $all_patterns = $patterns->get_all_registered();
680 +
681 + // error_log('[PatternsWP] Checking ' . count($all_patterns) . ' patterns for core ones');
682 +
683 + // Loop through all patterns and deregister any that are core patterns.
684 + foreach ($all_patterns as $index => $pattern) {
685 + // Core patterns typically have names starting with 'core/' or no file path
686 + $pattern_name = $pattern['name'] ?? '';
687 + $file_path = $pattern['filePath'] ?? '';
688 +
689 + if ( $this->is_patternswp_registry_pattern( $pattern_name ) ) {
690 + continue;
691 + }
692 +
693 + // Check if it's a core pattern
694 + $is_core_pattern = false;
695 + $reason = '';
696 +
697 + // Method 1: Check if pattern name starts with 'core/'
698 + if (strpos($pattern_name, 'core/') === 0) {
699 + $is_core_pattern = true;
700 + $reason = 'name starts with core/';
701 + }
702 +
703 + // Method 2: Check if file path contains wp-includes
704 + if (!$is_core_pattern && !empty($file_path) && strpos($file_path, 'wp-includes') !== false) {
705 + $is_core_pattern = true;
706 + $reason = 'file path contains wp-includes';
707 + }
708 +
709 + // Method 3: Check if no file path (likely core pattern, but not plugin-registered)
710 + if (!$is_core_pattern && empty($file_path)) {
711 + $is_core_pattern = true;
712 + $reason = 'no file path';
713 + }
714 +
715 + if ($is_core_pattern) {
716 + unregister_block_pattern($pattern['name']);
717 + // error_log('[PatternsWP] Deregistered core pattern: ' . $pattern['name'] . ' (' . $reason . ')');
718 + }
719 + }
720 + }
721 +
722 + /**
723 + * Filter the block patterns list in the editor
724 + */
725 + public function filter_block_patterns_list($patterns) {
726 + if ( ! is_array( $patterns ) ) {
727 + return $patterns;
728 + }
729 +
730 + $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
731 + $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
732 + $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
733 +
734 + // error_log('[PatternsWP] Block patterns filter - Theme: ' . ($hide_theme_patterns ? 'true' : 'false') . ', Uncategorized: ' . ($hide_uncategorized ? 'true' : 'false') . ', Core: ' . ($hide_core_patterns ? 'true' : 'false'));
735 +
736 + // If no filters are enabled, return original patterns
737 + if (!$hide_theme_patterns && !$hide_uncategorized && !$hide_core_patterns) {
738 + return $patterns;
739 + }
740 +
741 + $filtered_patterns = array();
742 +
743 + foreach ($patterns as $pattern) {
744 + $should_include = true;
745 +
746 + // Check theme patterns
747 + if ($hide_theme_patterns && $should_include) {
748 + // Check if pattern name starts with theme prefix
749 + if (isset($pattern['name']) && strpos($pattern['name'], 'theme-') === 0) {
750 + $should_include = false;
751 + // error_log('[PatternsWP] Excluding theme pattern by name: ' . $pattern['name']);
752 + }
753 + // Check if pattern has theme file path
754 + if (isset($pattern['filePath']) && strpos($pattern['filePath'], 'themes/') !== false) {
755 + $should_include = false;
756 + // error_log('[PatternsWP] Excluding theme pattern by path: ' . $pattern['name']);
757 + }
758 + }
759 +
760 + // Check uncategorized patterns
761 + if ($hide_uncategorized && $should_include) {
762 + if (!isset($pattern['categories']) || empty($pattern['categories'])) {
763 + $should_include = false;
764 + // error_log('[PatternsWP] Excluding uncategorized pattern: ' . $pattern['name']);
765 + }
766 + }
767 +
768 + // Check core patterns
769 + if ($hide_core_patterns && $should_include) {
770 + if (isset($pattern['name']) && strpos($pattern['name'], 'core/') === 0) {
771 + $should_include = false;
772 + // error_log('[PatternsWP] Excluding core pattern: ' . $pattern['name']);
773 + }
774 + }
775 +
776 + if ($should_include) {
777 + $filtered_patterns[] = $pattern;
778 + }
779 + }
780 +
781 + // error_log('[PatternsWP] Filtered from ' . count($patterns) . ' to ' . count($filtered_patterns) . ' patterns');
782 +
783 + return $filtered_patterns;
784 + }
785 +
786 + /**
787 + * Filter REST API response for individual blocks
788 + */
789 + public function filter_rest_block_response($response, $post, $request) {
790 + // Only filter in the block editor context
791 + if (!isset($request['context']) || $request['context'] !== 'edit') {
792 + return $response;
793 + }
794 +
795 + $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
796 + $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
797 + $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
798 +
799 + // If no filters are enabled, return original response
800 + if (!$hide_theme_patterns && !$hide_uncategorized && !$hide_core_patterns) {
801 + return $response;
802 + }
803 +
804 + // Get the pattern data
805 + $pattern_data = $response->data;
806 +
807 + // Check if this should be hidden
808 + $should_hide = false;
809 +
810 + // Check theme patterns
811 + if ($hide_theme_patterns) {
812 + // Check if pattern has theme-specific metadata
813 + if (isset($pattern_data['patternswp_source']) && $pattern_data['patternswp_source'] === 'theme') {
814 + $should_hide = true;
815 + }
816 + }
817 +
818 + // Check uncategorized patterns
819 + if (!$should_hide && $hide_uncategorized) {
820 + if (empty($pattern_data['pattern_categories'])) {
821 + $should_hide = true;
822 + }
823 + }
824 +
825 + // Check core patterns
826 + if (!$should_hide && $hide_core_patterns) {
827 + if (isset($pattern_data['name']) && strpos($pattern_data['name'], 'core/') === 0) {
828 + $should_hide = true;
829 + }
830 + }
831 +
832 + if ($should_hide) {
833 + // error_log('[PatternsWP] Filtering REST response for pattern: ' . ($pattern_data['title'] ?? 'Unknown'));
834 + // Return an error response to hide this pattern
835 + return new WP_Error(
836 + 'rest_pattern_hidden',
837 + 'Pattern is hidden by visibility settings',
838 + array('status' => 404)
839 + );
840 + }
841 +
842 + return $response;
843 + }
844 +
845 + /**
846 + * Filter REST API patterns
847 + */
848 + public function filter_rest_patterns($args, $request) {
849 + // Only filter if we're in the block editor context
850 + if (!isset($request['context']) || $request['context'] !== 'edit') {
851 + return $args;
852 + }
853 +
854 + // Get the current registered patterns
855 + $patterns = \WP_Block_Patterns_Registry::get_instance();
856 + $all_patterns = $patterns->get_all_registered();
857 +
858 + $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
859 + $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
860 + $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
861 +
862 + // If no filters are enabled, return original args
863 + if (!$hide_theme_patterns && !$hide_uncategorized && !$hide_core_patterns) {
864 + return $args;
865 + }
866 +
867 + // Get theme info for theme pattern detection
868 + $theme = wp_get_theme();
869 + $paths_to_search = array($theme->stylesheet);
870 + if ($theme->parent()) {
871 + $parent_theme = wp_get_theme($theme->parent()->template);
872 + $paths_to_search[] = $parent_theme->template;
873 + }
874 + $paths_to_search = array_unique($paths_to_search);
875 +
876 + // Build list of pattern names to exclude
877 + $exclude_patterns = array();
878 +
879 + foreach ($all_patterns as $pattern) {
880 + $should_exclude = false;
881 +
882 + // Check if it's a theme pattern
883 + if ($hide_theme_patterns) {
884 + $file_path = $pattern['filePath'] ?? '';
885 + if (!empty($file_path)) {
886 + foreach ($paths_to_search as $path) {
887 + if (false !== strpos($file_path, 'themes/' . $path)) {
888 + $should_exclude = true;
889 + break;
890 + }
891 + }
892 + }
893 + }
894 +
895 + // Check if it's a core pattern
896 + if (!$should_exclude && $hide_core_patterns) {
897 + $pattern_name = $pattern['name'] ?? '';
898 + $file_path = $pattern['filePath'] ?? '';
899 +
900 + if ( $this->is_patternswp_registry_pattern( $pattern_name ) ) {
901 + // Keep PatternsWP patterns visible.
902 + } elseif (strpos($pattern_name, 'core/') === 0) {
903 + $should_exclude = true;
904 + } elseif (!empty($file_path) && strpos($file_path, 'wp-includes') !== false) {
905 + $should_exclude = true;
906 + } elseif (empty($file_path)) {
907 + $should_exclude = true;
908 + }
909 + }
910 +
911 + // Check if it's uncategorized
912 + if (!$should_exclude && $hide_uncategorized) {
913 + if (!isset($pattern['categories']) || empty($pattern['categories'])) {
914 + $should_exclude = true;
915 + } else {
916 + $found = false;
917 + $block_categories = $pattern['categories'] ?? array();
918 + foreach ($block_categories as $block_category) {
919 + $categories = \WP_Block_Pattern_Categories_Registry::get_instance();
920 + if ($categories->is_registered($block_category)) {
921 + $found = true;
922 + break;
923 + }
924 + }
925 + if (!$found) {
926 + $should_exclude = true;
927 + }
928 + }
929 + }
930 +
931 + if ($should_exclude) {
932 + $exclude_patterns[] = $pattern['name'];
933 + }
934 + }
935 +
936 + // Add exclude filter using direct SQL for better performance
937 + if (!empty($exclude_patterns)) {
938 + $pwp_posts_where_filter = function($where) use ($exclude_patterns, &$pwp_posts_where_filter) {
939 + global $wpdb;
940 +
941 + // Self-remove so this filter does not leak into other queries.
942 + remove_filter( 'posts_where', $pwp_posts_where_filter );
943 +
944 + $placeholders = implode( ',', array_fill( 0, count( $exclude_patterns ), '%s' ) );
945 + $query = 'SELECT post_id FROM ' . $wpdb->postmeta . ' WHERE meta_key = %s AND meta_value IN (' . $placeholders . ')';
946 +
947 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders is a list of %s tokens; values are bound below.
948 + $exclude_sql = $wpdb->prepare( $query, array_merge( array( 'pattern_id' ), $exclude_patterns ) );
949 +
950 + // Add the exclusion to the WHERE clause
951 + $where .= " AND {$wpdb->posts}.ID NOT IN ($exclude_sql)";
952 +
953 + return $where;
954 + };
955 + add_filter( 'posts_where', $pwp_posts_where_filter );
956 +
957 + // Ensure we don't use post__not_in or meta_query
958 + unset($args['post__not_in']);
959 + unset($args['meta_query']);
960 + }
961 +
962 + return $args;
963 + }
964 +
965 + /**
966 + * Filter patterns based on visibility settings
967 + */
968 + /**
969 + * Check if a pattern is a theme pattern
970 + */
971 + private function is_theme_pattern($pattern) {
972 + // Check source
973 + if (!empty($pattern['source']) && $pattern['source'] === 'theme') {
974 + return true;
975 + }
976 +
977 + // Check name prefix
978 + if (!empty($pattern['name']) && (strpos($pattern['name'], 'theme-') === 0 ||
979 + strpos($pattern['name'], 'tw/') === 0)) {
980 + return true;
981 + }
982 +
983 + // Check categories
984 + if (!empty($pattern['categories'])) {
985 + $categories = is_array($pattern['categories']) ?
986 + $pattern['categories'] :
987 + array_map('trim', explode(',', $pattern['categories']));
988 +
989 + foreach ( $categories as $category ) {
990 + if ( ! is_string( $category ) ) {
991 + continue;
992 + }
993 +
994 + // PatternsWP library slugs such as patternswp-page-templates.
995 + if ( 0 === strpos( $category, 'patternswp-' ) || 0 === strpos( $category, 'patternswp/' ) ) {
996 + continue;
997 + }
998 +
999 + if ( false !== stripos( $category, 'theme' ) || false !== stripos( $category, 'template' ) ) {
1000 + return true;
1001 + }
1002 + }
1003 + }
1004 +
1005 + return false;
1006 + }
1007 +
1008 + /**
1009 + * Check if a pattern is a core pattern
1010 + */
1011 + private function is_core_pattern($pattern) {
1012 + // Check source
1013 + if (!empty($pattern['source']) && $pattern['source'] === 'core') {
1014 + return true;
1015 + }
1016 +
1017 + // Check name prefix
1018 + if (!empty($pattern['name']) && strpos($pattern['name'], 'core/') === 0) {
1019 + return true;
1020 + }
1021 +
1022 + return false;
1023 + }
1024 +
1025 + /**
1026 + * Check if a pattern is uncategorized
1027 + */
1028 + private function is_uncategorized_pattern($pattern) {
1029 + if (empty($pattern['categories'])) {
1030 + return true;
1031 + }
1032 +
1033 + $categories = is_array($pattern['categories']) ?
1034 + $pattern['categories'] :
1035 + array_filter(array_map('trim', explode(',', $pattern['categories'])));
1036 +
1037 + return empty($categories);
1038 + }
1039 +
1040 + /**
1041 + * Filter patterns based on visibility settings
1042 + */
1043 + public function filter_patterns_by_visibility($patterns) {
1044 + // Debug logging
1045 + // error_log('[PatternsWP] Filter applied with ' . count((array)$patterns) . ' patterns');
1046 +
1047 + // If no patterns or not an array, return early
1048 + if (empty($patterns) || !is_array($patterns)) {
1049 + // error_log('[PatternsWP] No patterns to filter');
1050 + return $patterns;
1051 + }
1052 +
1053 + $filtered_patterns = array();
1054 + $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
1055 + $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
1056 + $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
1057 +
1058 + // error_log('[PatternsWP] Settings - Hide Theme: ' . ($hide_theme_patterns ? 'true' : 'false') .
1059 + // ', Hide Uncategorized: ' . ($hide_uncategorized ? 'true' : 'false') .
1060 + // ', Hide Core: ' . ($hide_core_patterns ? 'true' : 'false'));
1061 +
1062 + foreach ($patterns as $pattern) {
1063 + // Skip if pattern is not an array
1064 + if (!is_array($pattern)) {
1065 + continue;
1066 + }
1067 +
1068 + // Skip theme patterns if enabled
1069 + if ($hide_theme_patterns && $this->is_theme_pattern($pattern)) {
1070 + // error_log('[PatternsWP] Skipping theme pattern: ' . ($pattern['title'] ?? 'Unknown'));
1071 + continue;
1072 + }
1073 +
1074 + // Skip core patterns if enabled
1075 + if ($hide_core_patterns && $this->is_core_pattern($pattern)) {
1076 + // error_log('[PatternsWP] Skipping core pattern: ' . ($pattern['title'] ?? 'Unknown'));
1077 + continue;
1078 + }
1079 +
1080 + // Skip uncategorized patterns if enabled
1081 + if ($hide_uncategorized && $this->is_uncategorized_pattern($pattern)) {
1082 + // error_log('[PatternsWP] Skipping uncategorized pattern: ' . ($pattern['title'] ?? 'Unknown'));
1083 + continue;
1084 + }
1085 +
1086 + $filtered_patterns[] = $pattern;
1087 + }
1088 +
1089 + // error_log('[PatternsWP] Filtered to ' . count($filtered_patterns) . ' patterns');
1090 + return $filtered_patterns;
1091 + }
1092 +
1093 + /**
1094 + * Register Pattern Visibility settings
1095 + */
1096 + public function register_pattern_visibility_settings() {
1097 + // Register settings
1098 + register_setting('patternswp_visibility_settings', 'patternswp_hide_theme_patterns', array(
1099 + 'type' => 'boolean',
1100 + 'default' => false,
1101 + 'sanitize_callback' => 'rest_sanitize_boolean',
315 1102 ));
1103 +
1104 + register_setting('patternswp_visibility_settings', 'patternswp_hide_uncategorized_patterns', array(
1105 + 'type' => 'boolean',
1106 + 'default' => false,
1107 + 'sanitize_callback' => 'rest_sanitize_boolean',
1108 + ));
1109 +
1110 + register_setting('patternswp_visibility_settings', 'patternswp_hide_core_patterns', array(
1111 + 'type' => 'boolean',
1112 + 'default' => false,
1113 + 'sanitize_callback' => 'rest_sanitize_boolean',
1114 + ));
1115 +
1116 + // Clear cache when settings are updated
1117 + add_action('update_option_patternswp_hide_theme_patterns', array($this, 'clear_pattern_cache'), 10, 2);
1118 + add_action('update_option_patternswp_hide_uncategorized_patterns', array($this, 'clear_pattern_cache'), 10, 2);
1119 + add_action('update_option_patternswp_hide_core_patterns', array($this, 'clear_pattern_cache'), 10, 2);
1120 +
1121 + // Add settings section
1122 + add_settings_section(
1123 + 'patternswp_visibility_section',
1124 + __('Pattern Visibility', 'patternswp'),
1125 + array($this, 'pattern_visibility_section_callback'),
1126 + 'patternswp-settings'
1127 + );
1128 +
1129 + // Add settings fields
1130 + add_settings_field(
1131 + 'patternswp_hide_theme_patterns',
1132 + __('Hide Theme Patterns', 'patternswp'),
1133 + array($this, 'render_checkbox_field'),
1134 + 'patternswp-settings',
1135 + 'patternswp_visibility_section',
1136 + array(
1137 + 'label_for' => 'patternswp_hide_theme_patterns',
1138 + 'description' => __('Prevent patterns registered by the active theme from displaying in the patterns list.', 'patternswp')
1139 + )
1140 + );
1141 +
1142 + add_settings_field(
1143 + 'patternswp_hide_uncategorized_patterns',
1144 + __('Hide Uncategorized Patterns', 'patternswp'),
1145 + array($this, 'render_checkbox_field'),
1146 + 'patternswp-settings',
1147 + 'patternswp_visibility_section',
1148 + array(
1149 + 'label_for' => 'patternswp_hide_uncategorized_patterns',
1150 + 'description' => __('Prevent any patterns not in any registered categories from displaying.', 'patternswp')
1151 + )
1152 + );
1153 +
1154 + add_settings_field(
1155 + 'patternswp_hide_core_patterns',
1156 + __('Hide Core Patterns', 'patternswp'),
1157 + array($this, 'render_checkbox_field'),
1158 + 'patternswp-settings',
1159 + 'patternswp_visibility_section',
1160 + array(
1161 + 'label_for' => 'patternswp_hide_core_patterns',
1162 + 'description' => __('Remove all core patterns from the pattern selector by disabling core patterns.', 'patternswp')
1163 + )
1164 + );
316 1165 }
1166 +
1167 + /**
1168 + * Pattern Visibility section callback
1169 + */
1170 + public function pattern_visibility_section_callback() {
1171 + echo '<p>' . esc_html__('Control which patterns are visible in the block editor.', 'patternswp') . '</p>';
1172 + }
1173 +
1174 + /**
1175 + * Render checkbox field
1176 + */
1177 + public function render_checkbox_field($args) {
1178 + $option = get_option($args['label_for']);
1179 + ?>
1180 + <label>
1181 + <input type="checkbox"
1182 + name="<?php echo esc_attr($args['label_for']); ?>"
1183 + value="1"
1184 + <?php checked(1, $option, true); ?>>
1185 + <span class="description"><?php echo esc_html($args['description']); ?></span>
1186 + </label>
1187 + <?php
1188 + }
1189 +
1190 + /**
1191 + * Render Pattern Visibility page
1192 + */
1193 + public function render_pattern_visibility_page() {
1194 + $this->render_admin_app( __( 'Settings', 'patternswp' ) );
1195 + }
1196 +
1197 + /**
1198 + * Redirect to welcome page after activation
1199 + */
1200 +public function patternswp_redirect_on_activation() {
1201 + if (get_transient('patternswp_activation_redirect')) {
1202 + delete_transient('patternswp_activation_redirect');
1203 + if (!is_network_admin() && !isset($_GET['activate-multi'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Safe usage: only checking if 'activate-multi' is set
1204 + wp_safe_redirect(admin_url('admin.php?page=patternswp-plugin-menu'));
1205 + exit;
1206 + }
1207 + }
1208 +}
317 1209 }
318 1210
319 1211 new PatternsWP_Admin();