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
patternswp / includes / class-patternswp-admin.php

class-patternswp-admin.php in PatternsWP – Gutenberg Block Patterns & Page Templates Library 1.1.0, at includes/class-patternswp-admin.php

1,211 lines 48.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin Page
4 */
5
6 // Exit if accessed directly
7 if (!defined('ABSPATH')) {
8 exit;
9 }
10
11 class PatternsWP_Admin {
12
13 /**
14 * Constructor
15 */
16 public function __construct() {
17 // Add menu and pages
18 add_action('admin_menu', array($this, 'add_admin_menu'));
19 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
20 add_action('admin_init', array($this, 'patternswp_clear_cache'));
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'));
64 }
65
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 /**
307 * Add admin menu
308 */
309 public function add_admin_menu() {
310 // Custom SVG icon (base64 encoded)
311 $custom_icon = 'data:image/svg+xml;base64,' . base64_encode('
312 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
313 <path d="M9.34 24L4.45 21.16L4.03 20.92V15.3L9.34 12V24Z"/>
314 <path fill-rule="evenodd" clip-rule="evenodd" d="M14.66 9.2L20.97 6L14.66 2.8V9.2Z"/>
315 <path d="M14.66 2.8L9.34 6L4.03 9.2V2.8L9.34 0L14.66 2.8Z"/>
316 <path fill-rule="evenodd" clip-rule="evenodd" d="M10.79 11.44V17.84L14.66 15.3L20.97 12V6L14.66 9.2L10.79 11.44Z"/>
317 </svg>
318 ');
319
320 // Add top-level menu with custom SVG icon
321 $menu_slug = 'patternswp-plugin-menu';
322 add_menu_page(
323 'PatternsWP',
324 'PatternsWP',
325 'manage_options',
326 $menu_slug,
327 array($this, 'render_main_page'),
328 $custom_icon,
329 60
330 );
331
332 // Add subpages
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'));
335 $this->add_subpage($menu_slug, 'Support', 'Support', 'patternswp-plugin-page-1', array($this, 'patternswp_support'));
336 }
337
338 /**
339 * Add subpage
340 */
341 private function add_subpage($parent_slug, $page_title, $menu_title, $menu_slug, $callback) {
342 // Add subpage
343 add_submenu_page($parent_slug, $page_title, $menu_title, 'manage_options', $menu_slug, $callback);
344 }
345
346 /**
347 * Render tabs
348 */
349 public function patterswp_tab( $tab ) {
350 $tabs = array(
351 'Dashboard' => 'patternswp-plugin-menu',
352 'Settings' => 'patternswp-settings',
353 'Support' => 'patternswp-plugin-page-1',
354 'License' => 'patternswp-license_section'
355 );
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
371 foreach ($tabs as $name => $slug) {
372 $class = ( $current_tab === $name ) ? 'nav-tab-active' : '';
373 echo '<a href="?page=' . esc_attr($slug) . '" class="nav-tab ' . esc_attr($class) . '">' . esc_html($name) . '</a>';
374 }
375 }
376
377 /**
378 * Render main page
379 */
380 public function render_main_page() {
381 $this->render_admin_app( __( 'PatternsWP', 'patternswp' ) );
382 }
383
384 /**
385 * Support page
386 */
387 public function patternswp_support() {
388 $this->render_admin_app( __( 'Support', 'patternswp' ) );
389 }
390
391 /**
392 * Clear Cache page
393 */
394 public function patternswp_clear_cache_form() {
395 $this->render_admin_app( __( 'Clear Cache', 'patternswp' ) );
396 }
397
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 /**
428 * Clear Cache
429 */
430 public function patternswp_clear_cache() {
431 global $wpdb;
432
433 if (isset($_POST['patternswp_clear_cache'])) {
434 if (!isset($_POST['patternswp_clear_cache_nonce'])) {
435 wp_die(esc_attr__('Security check failed. Please try again.', 'patternswp'));
436 }
437
438 $nonce = sanitize_text_field(wp_unslash($_POST['patternswp_clear_cache_nonce']));
439
440 if (!wp_verify_nonce($nonce, 'patternswp_clear_cache_action')) {
441 wp_die(esc_attr__('Security check failed. Please try again.', 'patternswp'));
442 }
443
444 if (!current_user_can('manage_options')) {
445 wp_die(esc_html__('You do not have sufficient permissions to perform this action.', 'patternswp'));
446 }
447
448 // Delete category type transient
449 delete_transient('patternswp_category_type');
450
451 // Delete all API-related transients
452 $pattern = '_transient_patternswp_';
453 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
454 $results = $wpdb->get_col($wpdb->prepare("SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", $wpdb->esc_like($pattern) . '%'));
455
456 // Delete all patterns cache
457 $patterns = 'patterns_cache_';
458 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
459 $wpdb->query(
460 $wpdb->prepare(
461 "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
462 $wpdb->esc_like($patterns) . '%'
463 )
464 );
465
466 foreach ($results as $transient) {
467 delete_option($transient);
468 delete_option(str_replace('_transient_', '_transient_timeout_', $transient));
469 }
470
471 // Load patterns in background
472 $api = PatternsWP_API_Section::get_instance();
473 $api->purge_library_caches();
474 $api->schedule_library_refresh( 1 );
475
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'));
481 exit;
482 }
483 }
484
485 /**
486 * Ensure daily cron job is scheduled
487 */
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' );
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();
497 }
498
499 /**
500 * AJAX handler to load transient data
501 */
502 public function patternswp_background_transient_load_ajaxcc() {
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' );
510 wp_die();
511 }
512
513 /**
514 * Warm pattern transients without an unauthenticated HTTP loopback.
515 */
516 public function patternswp_load_patterns_by_remote_ajax() {
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',
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 );
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 }
1209 }
1210
1211 new PatternsWP_Admin();