PluginProbe
PatternsWP – Gutenberg Block Patterns & Page Templates Library / 1.0.10
PatternsWP – Gutenberg Block Patterns & Page Templates Library v1.0.10
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.0.10, at includes/class-patternswp-admin.php

1,188 lines 51.2 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_init', array($this, 'patternswp_clear_cache'));
20 add_action('admin_init', array($this, 'patternswp_ensure_hourly_cron'));
21 add_action('wp_ajax_patternswp_background_transient_load_ajax', array($this, 'patternswp_background_transient_load_ajaxcc'));
22 add_action('wp_ajax_nopriv_patternswp_background_transient_load_ajax', array($this, 'patternswp_background_transient_load_ajaxcc'));
23 add_action('patternswp_load_patterns_by_ra', array($this, 'patternswp_load_patterns_by_remote_ajax'));
24 register_activation_hook(plugin_dir_path(__DIR__) . 'patternswp.php', array($this, 'patternswp_on_activation'));
25 add_action('admin_init', array($this, 'patternswp_redirect_on_activation'));
26
27 // Initialize settings
28 add_action('admin_init', array($this, 'register_pattern_visibility_settings'));
29
30 // Add pattern deregistration hooks
31 add_action('init', array($this, 'maybe_deregister_core_patterns'), 999);
32 add_action('init', array($this, 'maybe_deregister_theme_patterns'), 1000);
33 add_action('init', array($this, 'maybe_deregister_uncategorized_patterns'), 1001);
34
35 // Add a second check later to ensure patterns are deregistered
36 add_action('init', array($this, 'maybe_deregister_theme_patterns'), 9999);
37 add_action('init', array($this, 'maybe_deregister_uncategorized_patterns'), 10000);
38 add_action('init', array($this, 'maybe_deregister_core_patterns'), 10001);
39
40 // error_log('[PatternsWP] Constructor - All pattern deregistration hooks registered');
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 * Test method to verify settings are loaded
68 */
69 public function test_settings() {
70 $hide_theme = get_option('patternswp_hide_theme_patterns', false);
71 $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
72 $hide_core = get_option('patternswp_hide_core_patterns', false);
73
74 // error_log('[PatternsWP] Settings Check - Theme: ' . ($hide_theme ? 'true' : 'false') . ', Uncategorized: ' . ($hide_uncategorized ? 'true' : 'false') . ', Core: ' . ($hide_core ? 'true' : 'false'));
75 }
76
77 /**
78 * Add admin menu
79 */
80 public function add_admin_menu() {
81 // Custom SVG icon (base64 encoded)
82 $custom_icon = 'data:image/svg+xml;base64,' . base64_encode('
83 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
84 <path d="M9.34 24L4.45 21.16L4.03 20.92V15.3L9.34 12V24Z"/>
85 <path fill-rule="evenodd" clip-rule="evenodd" d="M14.66 9.2L20.97 6L14.66 2.8V9.2Z"/>
86 <path d="M14.66 2.8L9.34 6L4.03 9.2V2.8L9.34 0L14.66 2.8Z"/>
87 <path fill-rule="evenodd" clip-rule="evenodd" d="M10.79 11.44V17.84L14.66 15.3L20.97 12V6L14.66 9.2L10.79 11.44Z"/>
88 </svg>
89 ');
90
91 // Add top-level menu with custom SVG icon
92 $menu_slug = 'patternswp-plugin-menu';
93 add_menu_page(
94 'PatternsWP',
95 'PatternsWP',
96 'manage_options',
97 $menu_slug,
98 array($this, 'render_main_page'),
99 $custom_icon,
100 60
101 );
102
103 // Add subpages
104 $this->add_subpage($menu_slug, 'Dashboard', 'Dashboard', 'patternswp-plugin-menu', array($this, 'render_main_page'));
105 $this->add_subpage($menu_slug, 'Settings', 'Settings', 'patternswp-settings', array($this, 'render_pattern_visibility_page'));
106 $this->add_subpage($menu_slug, 'Support', 'Support', 'patternswp-plugin-page-1', array($this, 'patternswp_support'));
107 }
108
109 /**
110 * Add subpage
111 */
112 private function add_subpage($parent_slug, $page_title, $menu_title, $menu_slug, $callback) {
113 // Add subpage
114 add_submenu_page($parent_slug, $page_title, $menu_title, 'manage_options', $menu_slug, $callback);
115 }
116
117 /**
118 * Render tabs
119 */
120 public function patterswp_tab( $tab ) {
121 $tabs = array(
122 'Dashboard' => 'patternswp-plugin-menu',
123 'Settings' => 'patternswp-settings',
124 'Support' => 'patternswp-plugin-page-1',
125 'License' => 'patternswp-license_section'
126 );
127
128 // Get the current page from URL parameter
129 $current_page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : 'patternswp-plugin-menu';
130
131 // Map page slugs to tab names
132 $page_to_tab = array(
133 'patternswp-plugin-menu' => 'Dashboard',
134 'patternswp-settings' => 'Settings',
135 'patternswp-plugin-page-1' => 'Support',
136 'patternswp-license_section' => 'License'
137 );
138
139 // Determine current tab
140 $current_tab = isset($page_to_tab[$current_page]) ? $page_to_tab[$current_page] : 'Dashboard';
141
142 foreach ($tabs as $name => $slug) {
143 $class = ( $current_tab === $name ) ? 'nav-tab-active' : '';
144 echo '<a href="?page=' . esc_attr($slug) . '" class="nav-tab ' . esc_attr($class) . '">' . esc_html($name) . '</a>';
145 }
146 }
147
148 /**
149 * Render main page
150 */
151 public function render_main_page() { ?>
152 <div class="wrap">
153 <h1><?php esc_html_e('PatternsWP', 'patternswp'); ?></h1>
154 <h2 class="nav-tab-wrapper">
155 <?php $this->patterswp_tab( 'Dashboard' ); ?>
156 </h2>
157 <div class="patterns-wp-tabs-content">
158 <div id="tab-1" class="patterns-wp-tab-content patterns-wp-tab-active">
159 <div class="wrap">
160 <div class="pw-feature-box big-box" style="max-width: 1280px; padding: 40px; background-color: white; border-radius: 10px; overflow: hidden;">
161 <div class="about__section has-1-columns">
162 <div class="column" style="text-align:center;">
163 <h4><?php echo esc_html('Hello, ' . wp_get_current_user()->display_name . ' 👋'); ?></h4>
164 <h1 class="feature-title"><?php esc_html_e('Welcome to PatternsWP', 'patternswp'); ?></h1>
165 <p><?php esc_html_e('Thanks for choosing PatternsWP! Follow these three simple steps to get started!', 'patternswp'); ?></p>
166 </div>
167 <div class="column" style="padding: 10px; text-align: center;">
168 <a href="<?php echo esc_url(admin_url('post-new.php?post_type=page')); ?>" class="button button-primary">
169 <?php esc_html_e('Start building with PatternsWP', 'patternswp'); ?>
170 </a>
171 </div>
172 </div>
173 <div class="about__section has-3-columns">
174 <div class="column" style="padding: 40px;">
175 <div class="about__image">
176 <?php
177 // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
178 echo '<img src="' . esc_url(plugin_dir_url(__FILE__) . 'pwp-welcome-01.png') . '" alt="" height="auto" width="100%">';
179 ?>
180 </div>
181 <h4><?php esc_html_e('01. Open the PatternsWP Library', 'patternswp'); ?></h4>
182 <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>
183 </div>
184 <div class="column" style="padding: 40px;">
185 <div class="about__image">
186 <?php
187 // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
188 echo '<img src="' . esc_url(plugin_dir_url(__FILE__) . 'pwp-welcome-02.png') . '" alt="" height="auto" width="100%">';
189 ?>
190 </div>
191 <h4><?php esc_html_e('02. Browse Patterns & Templates', 'patternswp'); ?></h4>
192 <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>
193 </div>
194 <div class="column" style="padding: 40px;">
195 <div class="about__image">
196 <?php
197 // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
198 echo '<img src="' . esc_url(plugin_dir_url(__FILE__) . 'pwp-welcome-03.png') . '" alt="" height="auto" width="100%">';
199 ?>
200 </div>
201 <h4><?php esc_html_e('03. Add Patterns & Customize', 'patternswp'); ?></h4>
202 <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>
203 </div>
204 </div>
205 </div>
206 </div>
207 </div>
208 </div>
209 </div>
210 <?php }
211
212 /**
213 * Support page
214 */
215 public function patternswp_support() { ?>
216 <div class="wrap">
217 <h1><?php esc_html_e('Support', 'patternswp'); ?></h1>
218 <h2 class="nav-tab-wrapper">
219 <?php $this->patterswp_tab( 'Support' ); ?>
220 </h2>
221 <div class="patterns-wp-tabs-content">
222 <div id="tab-1" class="patterns-wp-tab-content patterns-wp-tab-active">
223 </div>
224 <div id="tab-2" class="patterns-wp-tab-content">
225 <div class="wrap">
226 <div class="pw-feature-box big-box" style="max-width: 1280px; padding: 40px; background-color: white; border-radius: 10px; overflow: hidden;">
227 <div class="about__section has-1-columns">
228 <div class="column" style="">
229 <h1 class="feature-title"><?php esc_html_e('Support', 'patternswp'); ?></h1>
230 <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>
231 </div>
232 <div style="height: 1px; background-color: #ccc; width: 100%; margin: 20px 0;"></div>
233 </div>
234 <div class="about__section has-2-columns">
235 <div class="column" style="padding-right: 100px;">
236
237 <h4><?php esc_html_e('Frequently asked questions', 'patternswp'); ?></h4>
238 <p>
239 <?php
240 printf(
241 esc_attr('Here, you will find answers to commonly asked questions about using PatternsWP. If you need further assistance, feel free to %s.', 'patternswp'),
242 '<a href="https://thepatternswp.com/contact/" target="_blank">'.esc_attr('contact us via the support form →', 'patternswp').'</a>'
243 );
244 ?>
245 </p>
246 </div>
247 <div class="column" style="padding: 0px;">
248 <h5>
249 <a href="https://thepatternswp.com/docs/getting-started-with-patternswp/" target="_blank" style="color: #3858e9; text-decoration: none;">
250 <?php esc_html_e('Getting Started with PatternsWP →', 'patternswp'); ?>
251 </a>
252 </h5>
253 <div style="height: 1px; background-color: #ccc; width: 100%; margin: 10px 0;"></div>
254 <h5>
255 <a href="https://thepatternswp.com/docs/how-to-install-patternswp/" target="_blank" style="color: #3858e9; text-decoration: none;">
256 <?php esc_html_e('How to install PatternsWP →', 'patternswp'); ?>
257 </a>
258 </h5> <div style="height: 1px; background-color: #ccc; width: 100%; margin: 10px 0;"></div>
259 <h5>
260 <a href="https://thepatternswp.com/docs/how-to-upgrade-patternswp-to-pro/" target="_blank" style="color: #3858e9; text-decoration: none;">
261 <?php esc_html_e('How to Upgrade PatternsWP to Pro →', 'patternswp'); ?>
262 </a>
263 </h5> <div style="height: 1px; background-color: #ccc; width: 100%; margin: 10px 0;"></div>
264 <h5>
265 <a href="https://thepatternswp.com/docs/patternswp-support/" target="_blank" style="color: #3858e9; text-decoration: none;">
266 <?php esc_html_e('PatternsWP Support →', 'patternswp'); ?>
267 </a>
268 </h5> <div style="height: 1px; background-color: #ccc; width: 100%; margin: 10px 0;"></div>
269 </div>
270
271 </div>
272 </div>
273 </div>
274 </div>
275 </div>
276 </div>
277 <?php }
278
279 /**
280 * Clear Cache page
281 */
282 public function patternswp_clear_cache_form() { ?>
283 <div class="wrap">
284 <h1><?php esc_html_e('Clear Cache', 'patternswp'); ?></h1>
285 <h2 class="nav-tab-wrapper">
286 <?php $this->patterswp_tab( 'Clear Cache' ); ?>
287 </h2>
288 <div class="patterns-wp-tabs-content">
289 <div id="tab-1" class="patterns-wp-tab-content patterns-wp-tab-active">
290 <div class="wrap">
291 <div class="pw-feature-box big-box" style="max-width: 1280px; padding: 40px; background-color: white; border-radius: 10px; overflow: hidden; margin: 0 auto;">
292 <div class="">
293 <form id="patternswp_clearcache_form" method="post">
294 <?php wp_nonce_field('patternswp_clear_cache_action', 'patternswp_clear_cache_nonce'); ?>
295 <input name="patternswp_clear_cache" type="submit" class="button button-primary" value="Clear Cache">
296 <div>
297 </div>
298 </form>
299 </div>
300 </div>
301 </div>
302 </div>
303 </div>
304 </div>
305 <?php }
306
307 /**
308 * Clear pattern cache when theme is switched
309 */
310 public function clear_pattern_cache_on_theme_switch() {
311 $this->clear_pattern_cache(false, false);
312 }
313
314 /**
315 * Handle remote pattern loading based on settings
316 */
317 public function maybe_disable_remote_patterns($should_load) {
318 if (get_option('patternswp_hide_core_patterns', false)) {
319 return false;
320 }
321 return $should_load;
322 }
323
324 /**
325 * Filter block editor settings to handle pattern visibility
326 */
327 public function filter_block_editor_settings($settings, $context) {
328 if (get_option('patternswp_hide_core_patterns', false)) {
329 $settings['__experimentalBlockPatterns'] = array();
330 $settings['__experimentalBlockPatternCategories'] = array();
331 $settings['__experimentalBlockPatterns'] = array();
332 }
333 return $settings;
334 }
335
336 /**
337 * Clear Cache
338 */
339 public function patternswp_clear_cache() {
340 global $wpdb;
341
342 if (isset($_POST['patternswp_clear_cache'])) {
343 if (!isset($_POST['patternswp_clear_cache_nonce'])) {
344 wp_die(esc_attr__('Security check failed. Please try again.', 'patternswp'));
345 }
346
347 $nonce = sanitize_text_field(wp_unslash($_POST['patternswp_clear_cache_nonce']));
348
349 if (!wp_verify_nonce($nonce, 'patternswp_clear_cache_action')) {
350 wp_die(esc_attr__('Security check failed. Please try again.', 'patternswp'));
351 }
352
353 if (!current_user_can('manage_options')) {
354 wp_die(esc_attr('You do not have sufficient permissions to perform this action.', 'patternswp'));
355 }
356
357 // Delete category type transient
358 delete_transient('patternswp_category_type');
359
360 // Delete all API-related transients
361 $pattern = '_transient_patternswp_';
362 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
363 $results = $wpdb->get_col($wpdb->prepare("SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", $wpdb->esc_like($pattern) . '%'));
364
365 // Delete all patterns cache
366 $patterns = 'patterns_cache_';
367 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
368 $wpdb->query(
369 $wpdb->prepare(
370 "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
371 $wpdb->esc_like($patterns) . '%'
372 )
373 );
374
375 foreach ($results as $transient) {
376 delete_option($transient);
377 delete_option(str_replace('_transient_', '_transient_timeout_', $transient));
378 }
379
380 // Load patterns in background
381 $this->patternswp_load_patterns_by_remote_ajax();
382
383 // Set a transient for the success message
384 set_transient('patternswp_cache_cleared', 'Cache cleared successfully', 5);
385
386 // Redirect to Settings page
387 wp_safe_redirect(admin_url('admin.php?page=patternswp-settings'));
388 exit;
389 }
390 }
391
392 /**
393 * Ensure hourly cron job is scheduled
394 */
395 public function patternswp_ensure_hourly_cron() {
396 if ( ! wp_next_scheduled( 'patternswp_hourly_transient_load' ) ) {
397 wp_schedule_event( time(), 'hourly', 'patternswp_hourly_transient_load' );
398 }
399 }
400
401 /**
402 * AJAX handler to load transient data
403 */
404 public function patternswp_background_transient_load_ajaxcc() {
405 do_action( 'patternswp_hourly_transient_load' );
406 wp_die();
407 }
408
409 /**
410 * Load patterns via AJAX
411 */
412 public function patternswp_load_patterns_by_remote_ajax() {
413 $ajax_url = admin_url('admin-ajax.php');
414 wp_remote_post( $ajax_url , array(
415 'body' => array( 'action' => 'patternswp_background_transient_load_ajax' ),
416 'timeout' => 1,
417 'blocking' => false,
418 'sslverify' => false,
419 ));
420 }
421
422 /**
423 * Set transient on plugin activation
424 */
425 public function patternswp_on_activation() {
426 set_transient('patternswp_activation_redirect', true, 30);
427 }
428
429 /**
430 * Clear pattern cache when visibility settings change
431 */
432 public function clear_pattern_cache($old_value, $new_value) {
433 // Clear object cache first
434 if (function_exists('wp_cache_flush')) {
435 wp_cache_flush();
436 }
437
438 // Clear known transients
439 $transients = [
440 'patternswp_all_patterns',
441 'patternswp_categorized_patterns',
442 'patternswp_patterns_data',
443 'patternswp_categories'
444 ];
445
446 foreach ($transients as $transient) {
447 delete_transient($transient);
448 }
449
450 // Clear any remaining transients with our prefix
451 if (!function_exists('delete_expired_transients')) {
452 require_once ABSPATH . 'wp-admin/includes/plugin.php';
453 }
454
455 // This is a core function that will clean up expired transients
456 delete_expired_transients(true);
457
458 // Clear specific pattern caches
459 if (function_exists('wp_cache_delete')) {
460 wp_cache_delete('all_patterns', 'patternswp');
461 wp_cache_delete('categorized_patterns', 'patternswp');
462 }
463 }
464
465 /**
466 * Attempt to deregister any patterns from the active or child theme.
467 */
468 public function maybe_deregister_theme_patterns() {
469 $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
470 // error_log('[PatternsWP] maybe_deregister_theme_patterns called - hide_theme_patterns: ' . ($hide_theme_patterns ? 'true' : 'false'));
471
472 if (!$hide_theme_patterns) {
473 // error_log('[PatternsWP] Theme pattern hiding is disabled');
474 return;
475 }
476
477 // Get the active theme.
478 $theme = wp_get_theme();
479
480 // Let's get some theme data.
481 $paths_to_search = array(
482 $theme->stylesheet,
483 );
484
485 // Check if this is a child theme.
486 if ($theme->parent()) {
487 $parent_theme = wp_get_theme($theme->parent()->template);
488 $paths_to_search[] = $parent_theme->template;
489 }
490 $paths_to_search = array_unique($paths_to_search);
491
492 // error_log('[PatternsWP] Theme paths to search: ' . implode(', ', $paths_to_search));
493
494 // Get all registered patterns.
495 $patterns = \WP_Block_Patterns_Registry::get_instance();
496 $all_patterns = $patterns->get_all_registered();
497
498 // error_log('[PatternsWP] Total registered patterns: ' . count($all_patterns));
499
500 // Loop through all patterns and deregister any that are from the active or child theme.
501 foreach ($all_patterns as $index => $pattern) {
502 $file_path = $pattern['filePath'] ?? '';
503 if (empty($file_path)) {
504 continue;
505 }
506
507 // Check if the pattern is from the active or child theme.
508 foreach ($paths_to_search as $path) {
509 if (false !== strpos($file_path, 'themes/' . $path)) {
510 unregister_block_pattern($pattern['name']);
511 // error_log('[PatternsWP] Deregistered theme pattern: ' . $pattern['name'] . ' from path: ' . $file_path);
512 }
513 }
514 }
515 }
516
517 /**
518 * Deregister any uncategorized patterns.
519 */
520 public function maybe_deregister_uncategorized_patterns() {
521 $hide_uncategorized_enabled = get_option('patternswp_hide_uncategorized_patterns', false);
522 // error_log('[PatternsWP] maybe_deregister_uncategorized_patterns called - hide_uncategorized: ' . ($hide_uncategorized_enabled ? 'true' : 'false'));
523
524 // Exit early if not enabled.
525 if (!$hide_uncategorized_enabled) {
526 // error_log('[PatternsWP] Uncategorized pattern hiding is disabled');
527 return;
528 }
529
530 // Retrieve all patterns.
531 $patterns = \WP_Block_Patterns_Registry::get_instance();
532 $all_patterns = $patterns->get_all_registered();
533
534 // error_log('[PatternsWP] Checking ' . count($all_patterns) . ' patterns for uncategorized ones');
535
536 // Loop through all patterns and deregister any that are uncategorized.
537 foreach ($all_patterns as $index => $pattern) {
538 if (!isset($pattern['categories']) || empty($pattern['categories'])) {
539 unregister_block_pattern($pattern['name']);
540 // error_log('[PatternsWP] Deregistered uncategorized pattern: ' . $pattern['name']);
541 } else {
542 $found = false;
543 $block_categories = $pattern['categories'] ?? array();
544 foreach ($block_categories as $block_category) {
545 $categories = \WP_Block_Pattern_Categories_Registry::get_instance();
546 if ($categories->is_registered($block_category)) {
547 $found = true;
548 }
549 }
550 if (!$found) {
551 unregister_block_pattern($pattern['name']);
552 // error_log('[PatternsWP] Deregistered uncategorized pattern (invalid category): ' . $pattern['name']);
553 }
554 }
555 }
556 }
557
558 /**
559 * Deregister core patterns.
560 */
561 public function maybe_deregister_core_patterns() {
562 $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
563 // error_log('[PatternsWP] maybe_deregister_core_patterns called - hide_core_patterns: ' . ($hide_core_patterns ? 'true' : 'false'));
564
565 // Exit early if not enabled.
566 if (!$hide_core_patterns) {
567 // error_log('[PatternsWP] Core pattern hiding is disabled');
568 return;
569 }
570
571 // Retrieve all patterns.
572 $patterns = \WP_Block_Patterns_Registry::get_instance();
573 $all_patterns = $patterns->get_all_registered();
574
575 // error_log('[PatternsWP] Checking ' . count($all_patterns) . ' patterns for core ones');
576
577 // Loop through all patterns and deregister any that are core patterns.
578 foreach ($all_patterns as $index => $pattern) {
579 // Core patterns typically have names starting with 'core/' or no file path
580 $pattern_name = $pattern['name'] ?? '';
581 $file_path = $pattern['filePath'] ?? '';
582
583 // Check if it's a core pattern
584 $is_core_pattern = false;
585 $reason = '';
586
587 // Method 1: Check if pattern name starts with 'core/'
588 if (strpos($pattern_name, 'core/') === 0) {
589 $is_core_pattern = true;
590 $reason = 'name starts with core/';
591 }
592
593 // Method 2: Check if file path contains wp-includes
594 if (!$is_core_pattern && !empty($file_path) && strpos($file_path, 'wp-includes') !== false) {
595 $is_core_pattern = true;
596 $reason = 'file path contains wp-includes';
597 }
598
599 // Method 3: Check if no file path (likely core pattern)
600 if (!$is_core_pattern && empty($file_path)) {
601 $is_core_pattern = true;
602 $reason = 'no file path';
603 }
604
605 if ($is_core_pattern) {
606 unregister_block_pattern($pattern['name']);
607 // error_log('[PatternsWP] Deregistered core pattern: ' . $pattern['name'] . ' (' . $reason . ')');
608 }
609 }
610 }
611
612 /**
613 * Filter the block patterns list in the editor
614 */
615 public function filter_block_patterns_list($patterns) {
616 // error_log('[PatternsWP] filter_block_patterns_list called with ' . count($patterns) . ' patterns');
617
618 $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
619 $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
620 $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
621
622 // error_log('[PatternsWP] Block patterns filter - Theme: ' . ($hide_theme_patterns ? 'true' : 'false') . ', Uncategorized: ' . ($hide_uncategorized ? 'true' : 'false') . ', Core: ' . ($hide_core_patterns ? 'true' : 'false'));
623
624 // If no filters are enabled, return original patterns
625 if (!$hide_theme_patterns && !$hide_uncategorized && !$hide_core_patterns) {
626 return $patterns;
627 }
628
629 $filtered_patterns = array();
630
631 foreach ($patterns as $pattern) {
632 $should_include = true;
633
634 // Check theme patterns
635 if ($hide_theme_patterns && $should_include) {
636 // Check if pattern name starts with theme prefix
637 if (isset($pattern['name']) && strpos($pattern['name'], 'theme-') === 0) {
638 $should_include = false;
639 // error_log('[PatternsWP] Excluding theme pattern by name: ' . $pattern['name']);
640 }
641 // Check if pattern has theme file path
642 if (isset($pattern['filePath']) && strpos($pattern['filePath'], 'themes/') !== false) {
643 $should_include = false;
644 // error_log('[PatternsWP] Excluding theme pattern by path: ' . $pattern['name']);
645 }
646 }
647
648 // Check uncategorized patterns
649 if ($hide_uncategorized && $should_include) {
650 if (!isset($pattern['categories']) || empty($pattern['categories'])) {
651 $should_include = false;
652 // error_log('[PatternsWP] Excluding uncategorized pattern: ' . $pattern['name']);
653 }
654 }
655
656 // Check core patterns
657 if ($hide_core_patterns && $should_include) {
658 if (isset($pattern['name']) && strpos($pattern['name'], 'core/') === 0) {
659 $should_include = false;
660 // error_log('[PatternsWP] Excluding core pattern: ' . $pattern['name']);
661 }
662 }
663
664 if ($should_include) {
665 $filtered_patterns[] = $pattern;
666 }
667 }
668
669 // error_log('[PatternsWP] Filtered from ' . count($patterns) . ' to ' . count($filtered_patterns) . ' patterns');
670
671 return $filtered_patterns;
672 }
673
674 /**
675 * Filter REST API response for individual blocks
676 */
677 public function filter_rest_block_response($response, $post, $request) {
678 // Only filter in the block editor context
679 if (!isset($request['context']) || $request['context'] !== 'edit') {
680 return $response;
681 }
682
683 $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
684 $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
685 $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
686
687 // If no filters are enabled, return original response
688 if (!$hide_theme_patterns && !$hide_uncategorized && !$hide_core_patterns) {
689 return $response;
690 }
691
692 // Get the pattern data
693 $pattern_data = $response->data;
694
695 // Check if this should be hidden
696 $should_hide = false;
697
698 // Check theme patterns
699 if ($hide_theme_patterns) {
700 // Check if pattern has theme-specific metadata
701 if (isset($pattern_data['patternswp_source']) && $pattern_data['patternswp_source'] === 'theme') {
702 $should_hide = true;
703 }
704 }
705
706 // Check uncategorized patterns
707 if (!$should_hide && $hide_uncategorized) {
708 if (empty($pattern_data['pattern_categories'])) {
709 $should_hide = true;
710 }
711 }
712
713 // Check core patterns
714 if (!$should_hide && $hide_core_patterns) {
715 if (isset($pattern_data['name']) && strpos($pattern_data['name'], 'core/') === 0) {
716 $should_hide = true;
717 }
718 }
719
720 if ($should_hide) {
721 // error_log('[PatternsWP] Filtering REST response for pattern: ' . ($pattern_data['title'] ?? 'Unknown'));
722 // Return an error response to hide this pattern
723 return new WP_Error(
724 'rest_pattern_hidden',
725 'Pattern is hidden by visibility settings',
726 array('status' => 404)
727 );
728 }
729
730 return $response;
731 }
732
733 /**
734 * Filter REST API patterns
735 */
736 public function filter_rest_patterns($args, $request) {
737 // Only filter if we're in the block editor context
738 if (!isset($request['context']) || $request['context'] !== 'edit') {
739 return $args;
740 }
741
742 // Get the current registered patterns
743 $patterns = \WP_Block_Patterns_Registry::get_instance();
744 $all_patterns = $patterns->get_all_registered();
745
746 $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
747 $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
748 $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
749
750 // If no filters are enabled, return original args
751 if (!$hide_theme_patterns && !$hide_uncategorized && !$hide_core_patterns) {
752 return $args;
753 }
754
755 // Get theme info for theme pattern detection
756 $theme = wp_get_theme();
757 $paths_to_search = array($theme->stylesheet);
758 if ($theme->parent()) {
759 $parent_theme = wp_get_theme($theme->parent()->template);
760 $paths_to_search[] = $parent_theme->template;
761 }
762 $paths_to_search = array_unique($paths_to_search);
763
764 // Build list of pattern names to exclude
765 $exclude_patterns = array();
766
767 foreach ($all_patterns as $pattern) {
768 $should_exclude = false;
769
770 // Check if it's a theme pattern
771 if ($hide_theme_patterns) {
772 $file_path = $pattern['filePath'] ?? '';
773 if (!empty($file_path)) {
774 foreach ($paths_to_search as $path) {
775 if (false !== strpos($file_path, 'themes/' . $path)) {
776 $should_exclude = true;
777 break;
778 }
779 }
780 }
781 }
782
783 // Check if it's a core pattern
784 if (!$should_exclude && $hide_core_patterns) {
785 $pattern_name = $pattern['name'] ?? '';
786 $file_path = $pattern['filePath'] ?? '';
787
788 // Method 1: Check if pattern name starts with 'core/'
789 if (strpos($pattern_name, 'core/') === 0) {
790 $should_exclude = true;
791 }
792
793 // Method 2: Check if file path contains wp-includes
794 if (!$should_exclude && !empty($file_path) && strpos($file_path, 'wp-includes') !== false) {
795 $should_exclude = true;
796 }
797
798 // Method 3: Check if no file path (likely core pattern)
799 if (!$should_exclude && empty($file_path)) {
800 $should_exclude = true;
801 }
802 }
803
804 // Check if it's uncategorized
805 if (!$should_exclude && $hide_uncategorized) {
806 if (!isset($pattern['categories']) || empty($pattern['categories'])) {
807 $should_exclude = true;
808 } else {
809 $found = false;
810 $block_categories = $pattern['categories'] ?? array();
811 foreach ($block_categories as $block_category) {
812 $categories = \WP_Block_Pattern_Categories_Registry::get_instance();
813 if ($categories->is_registered($block_category)) {
814 $found = true;
815 break;
816 }
817 }
818 if (!$found) {
819 $should_exclude = true;
820 }
821 }
822 }
823
824 if ($should_exclude) {
825 $exclude_patterns[] = $pattern['name'];
826 }
827 }
828
829 // Add exclude filter using direct SQL for better performance
830 if (!empty($exclude_patterns)) {
831 add_filter('posts_where', function($where) use ($exclude_patterns) {
832 global $wpdb;
833
834 // Prepare placeholders for the IN clause
835 $placeholders = array_fill(0, count($exclude_patterns), '%s');
836 $placeholders = implode(',', $placeholders);
837
838 // Prepare the exclusion subquery
839 $exclude_sql = $wpdb->prepare(
840 "SELECT post_id
841 FROM {$wpdb->postmeta}
842 WHERE meta_key = 'pattern_id'
843 AND meta_value IN ($placeholders)",
844 $exclude_patterns
845 );
846
847 // Add the exclusion to the WHERE clause
848 $where .= " AND {$wpdb->posts}.ID NOT IN ($exclude_sql)";
849
850 return $where;
851 });
852
853 // Ensure we don't use post__not_in or meta_query
854 unset($args['post__not_in']);
855 unset($args['meta_query']);
856 }
857
858 return $args;
859 }
860
861 /**
862 * Filter patterns based on visibility settings
863 */
864 /**
865 * Check if a pattern is a theme pattern
866 */
867 private function is_theme_pattern($pattern) {
868 // Check source
869 if (!empty($pattern['source']) && $pattern['source'] === 'theme') {
870 return true;
871 }
872
873 // Check name prefix
874 if (!empty($pattern['name']) && (strpos($pattern['name'], 'theme-') === 0 ||
875 strpos($pattern['name'], 'tw/') === 0)) {
876 return true;
877 }
878
879 // Check categories
880 if (!empty($pattern['categories'])) {
881 $categories = is_array($pattern['categories']) ?
882 $pattern['categories'] :
883 array_map('trim', explode(',', $pattern['categories']));
884
885 foreach ($categories as $category) {
886 if (is_string($category) &&
887 (stripos($category, 'theme') !== false ||
888 stripos($category, 'template') !== false)) {
889 return true;
890 }
891 }
892 }
893
894 return false;
895 }
896
897 /**
898 * Check if a pattern is a core pattern
899 */
900 private function is_core_pattern($pattern) {
901 // Check source
902 if (!empty($pattern['source']) && $pattern['source'] === 'core') {
903 return true;
904 }
905
906 // Check name prefix
907 if (!empty($pattern['name']) && strpos($pattern['name'], 'core/') === 0) {
908 return true;
909 }
910
911 return false;
912 }
913
914 /**
915 * Check if a pattern is uncategorized
916 */
917 private function is_uncategorized_pattern($pattern) {
918 if (empty($pattern['categories'])) {
919 return true;
920 }
921
922 $categories = is_array($pattern['categories']) ?
923 $pattern['categories'] :
924 array_filter(array_map('trim', explode(',', $pattern['categories'])));
925
926 return empty($categories);
927 }
928
929 /**
930 * Filter patterns based on visibility settings
931 */
932 public function filter_patterns_by_visibility($patterns) {
933 // Debug logging
934 // error_log('[PatternsWP] Filter applied with ' . count((array)$patterns) . ' patterns');
935
936 // If no patterns or not an array, return early
937 if (empty($patterns) || !is_array($patterns)) {
938 // error_log('[PatternsWP] No patterns to filter');
939 return $patterns;
940 }
941
942 $filtered_patterns = array();
943 $hide_theme_patterns = get_option('patternswp_hide_theme_patterns', false);
944 $hide_uncategorized = get_option('patternswp_hide_uncategorized_patterns', false);
945 $hide_core_patterns = get_option('patternswp_hide_core_patterns', false);
946
947 // error_log('[PatternsWP] Settings - Hide Theme: ' . ($hide_theme_patterns ? 'true' : 'false') .
948 // ', Hide Uncategorized: ' . ($hide_uncategorized ? 'true' : 'false') .
949 // ', Hide Core: ' . ($hide_core_patterns ? 'true' : 'false'));
950
951 foreach ($patterns as $pattern) {
952 // Skip if pattern is not an array
953 if (!is_array($pattern)) {
954 $filtered_patterns[] = $pattern;
955 continue;
956 }
957
958 // Skip theme patterns if enabled
959 if ($hide_theme_patterns && $this->is_theme_pattern($pattern)) {
960 // error_log('[PatternsWP] Skipping theme pattern: ' . ($pattern['title'] ?? 'Unknown'));
961 continue;
962 }
963
964 // Skip core patterns if enabled
965 if ($hide_core_patterns && $this->is_core_pattern($pattern)) {
966 // error_log('[PatternsWP] Skipping core pattern: ' . ($pattern['title'] ?? 'Unknown'));
967 continue;
968 }
969
970 // Skip uncategorized patterns if enabled
971 if ($hide_uncategorized && $this->is_uncategorized_pattern($pattern)) {
972 // error_log('[PatternsWP] Skipping uncategorized pattern: ' . ($pattern['title'] ?? 'Unknown'));
973 continue;
974 }
975
976 $filtered_patterns[] = $pattern;
977 }
978
979 // error_log('[PatternsWP] Filtered to ' . count($filtered_patterns) . ' patterns');
980 return $filtered_patterns;
981 }
982
983 /**
984 * Register Pattern Visibility settings
985 */
986 public function register_pattern_visibility_settings() {
987 // Register settings
988 register_setting('patternswp_visibility_settings', 'patternswp_hide_theme_patterns', array(
989 'type' => 'boolean',
990 'default' => false,
991 'sanitize_callback' => 'rest_sanitize_boolean',
992 ));
993
994 register_setting('patternswp_visibility_settings', 'patternswp_hide_uncategorized_patterns', array(
995 'type' => 'boolean',
996 'default' => false,
997 'sanitize_callback' => 'rest_sanitize_boolean',
998 ));
999
1000 register_setting('patternswp_visibility_settings', 'patternswp_hide_core_patterns', array(
1001 'type' => 'boolean',
1002 'default' => false,
1003 'sanitize_callback' => 'rest_sanitize_boolean',
1004 ));
1005
1006 // Clear cache when settings are updated
1007 add_action('update_option_patternswp_hide_theme_patterns', array($this, 'clear_pattern_cache'), 10, 2);
1008 add_action('update_option_patternswp_hide_uncategorized_patterns', array($this, 'clear_pattern_cache'), 10, 2);
1009 add_action('update_option_patternswp_hide_core_patterns', array($this, 'clear_pattern_cache'), 10, 2);
1010
1011 // Add settings section
1012 add_settings_section(
1013 'patternswp_visibility_section',
1014 __('Pattern Visibility', 'patternswp'),
1015 array($this, 'pattern_visibility_section_callback'),
1016 'patternswp-settings'
1017 );
1018
1019 // Add settings fields
1020 add_settings_field(
1021 'patternswp_hide_theme_patterns',
1022 __('Hide Theme Patterns', 'patternswp'),
1023 array($this, 'render_checkbox_field'),
1024 'patternswp-settings',
1025 'patternswp_visibility_section',
1026 array(
1027 'label_for' => 'patternswp_hide_theme_patterns',
1028 'description' => __('Prevent patterns registered by the active theme from displaying in the patterns list.', 'patternswp')
1029 )
1030 );
1031
1032 add_settings_field(
1033 'patternswp_hide_uncategorized_patterns',
1034 __('Hide Uncategorized Patterns', 'patternswp'),
1035 array($this, 'render_checkbox_field'),
1036 'patternswp-settings',
1037 'patternswp_visibility_section',
1038 array(
1039 'label_for' => 'patternswp_hide_uncategorized_patterns',
1040 'description' => __('Prevent any patterns not in any registered categories from displaying.', 'patternswp')
1041 )
1042 );
1043
1044 add_settings_field(
1045 'patternswp_hide_core_patterns',
1046 __('Hide Core Patterns', 'patternswp'),
1047 array($this, 'render_checkbox_field'),
1048 'patternswp-settings',
1049 'patternswp_visibility_section',
1050 array(
1051 'label_for' => 'patternswp_hide_core_patterns',
1052 'description' => __('Remove all core patterns from the pattern selector by disabling core patterns.', 'patternswp')
1053 )
1054 );
1055 }
1056
1057 /**
1058 * Pattern Visibility section callback
1059 */
1060 public function pattern_visibility_section_callback() {
1061 echo '<p>' . esc_html__('Control which patterns are visible in the block editor.', 'patternswp') . '</p>';
1062 }
1063
1064 /**
1065 * Render checkbox field
1066 */
1067 public function render_checkbox_field($args) {
1068 $option = get_option($args['label_for']);
1069 ?>
1070 <label>
1071 <input type="checkbox"
1072 name="<?php echo esc_attr($args['label_for']); ?>"
1073 value="1"
1074 <?php checked(1, $option, true); ?>>
1075 <span class="description"><?php echo esc_html($args['description']); ?></span>
1076 </label>
1077 <?php
1078 }
1079
1080 /**
1081 * Render Pattern Visibility page
1082 */
1083 public function render_pattern_visibility_page() {
1084 // Check user capabilities
1085 if (!current_user_can('manage_options')) {
1086 return;
1087 }
1088
1089 // Show success/error messages
1090 if (isset($_GET['settings-updated'])) {
1091 add_settings_error(
1092 'patternswp_messages',
1093 'patternswp_message',
1094 __('Settings Saved', 'patternswp'),
1095 'updated'
1096 );
1097 }
1098
1099 // Show cache clear messages from transient
1100 $cache_message = get_transient('patternswp_cache_cleared');
1101 if ($cache_message) {
1102 add_settings_error(
1103 'patternswp_messages',
1104 'patternswp_cache_message',
1105 $cache_message,
1106 'updated'
1107 );
1108 // Delete the transient so it only shows once
1109 delete_transient('patternswp_cache_cleared');
1110 } elseif (isset($_GET['pt_msg']) && isset($_GET['status'])) {
1111 // Keep backward compatibility with URL parameters
1112 $message = sanitize_text_field(wp_unslash($_GET['pt_msg']));
1113 $status = sanitize_text_field(wp_unslash($_GET['status']));
1114 $type = $status === 'success' ? 'updated' : 'error';
1115
1116 add_settings_error(
1117 'patternswp_messages',
1118 'patternswp_cache_message',
1119 $message,
1120 $type
1121 );
1122 }
1123
1124 settings_errors('patternswp_messages');
1125 ?>
1126 <div class="wrap">
1127 <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
1128
1129 <h2 class="nav-tab-wrapper">
1130 <?php
1131 $this->patterswp_tab('');
1132 ?>
1133 </h2>
1134 <div class="patterns-wp-tabs-content">
1135 <div id="tab-1" class="patterns-wp-tab-content patterns-wp-tab-active">
1136 <div class="wrap">
1137 <div class="pw-feature-box big-box" style="max-width: 1280px; padding: 40px; background-color: white; border-radius: 10px; overflow: hidden; margin: 0 auto;">
1138 <form action="options.php" method="post">
1139 <?php
1140 // Output security fields
1141 settings_fields('patternswp_visibility_settings');
1142 // Output settings sections and fields
1143 do_settings_sections('patternswp-settings');
1144 // Output save settings button
1145 submit_button(__('Save Settings', 'patternswp'));
1146 ?>
1147 </form>
1148
1149 <hr style="margin: 30px 0;">
1150
1151 <h3><?php esc_html_e('Cache Management', 'patternswp'); ?></h3>
1152 <p><?php esc_html_e('Clear all cached patterns and data. This may be necessary if patterns are not updating correctly or after changing visibility settings.', 'patternswp'); ?></p>
1153
1154 <form method="post" action="">
1155 <?php wp_nonce_field('patternswp_clear_cache_action', 'patternswp_clear_cache_nonce'); ?>
1156 <input type="hidden" name="patternswp_clear_cache" value="1">
1157 <?php
1158 submit_button(
1159 __('Clear All Cache', 'patternswp'),
1160 'primary',
1161 'patternswp_clear_cache',
1162 false
1163 );
1164 ?>
1165 </form>
1166 </div>
1167 </div>
1168 </div>
1169 </div>
1170 </div>
1171 <?php
1172 }
1173
1174 /**
1175 * Redirect to welcome page after activation
1176 */
1177 public function patternswp_redirect_on_activation() {
1178 if (get_transient('patternswp_activation_redirect')) {
1179 delete_transient('patternswp_activation_redirect');
1180 if (!is_network_admin() && !isset($_GET['activate-multi'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Safe usage: only checking if 'activate-multi' is set
1181 wp_safe_redirect(admin_url('admin.php?page=patternswp-plugin-menu'));
1182 exit;
1183 }
1184 }
1185 }
1186 }
1187
1188 new PatternsWP_Admin();