PluginProbe ʕ •ᴥ•ʔ
Widget Options – Advanced Conditional Visibility for Gutenberg Blocks & Classic Widgets / 4.2.0
Widget Options – Advanced Conditional Visibility for Gutenberg Blocks & Classic Widgets v4.2.0
4.2.5 4.2.4 trunk 3.7.10 3.7.11 3.7.12 3.7.13 3.7.14 3.7.2 3.7.5 3.7.6 3.7.7 3.7.8 3.7.9 3.8 3.8.1 3.8.10 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.8.8 3.8.9 3.8.9.1 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.5.1 4.0.6 4.0.6.1 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2.0 4.2.1 4.2.2 4.2.3
widget-options / includes / snippets / class-snippets-cpt.php
widget-options / includes / snippets Last commit date
class-snippets-admin.php 4 months ago class-snippets-api.php 4 months ago class-snippets-cpt.php 4 months ago class-snippets-migration.php 4 months ago
class-snippets-cpt.php
440 lines
1 <?php
2 /**
3 * Display Logic Snippets - Custom Post Type
4 *
5 * Registers and manages the widgetopts_snippet CPT for storing
6 * reusable display logic code snippets.
7 *
8 * @copyright Copyright (c) 2024, Widget Options Team
9 * @since 5.1
10 */
11
12 // Exit if accessed directly
13 if (!defined('ABSPATH')) exit;
14
15 /**
16 * Class WidgetOpts_Snippets_CPT
17 *
18 * Handles Custom Post Type registration and basic operations for snippets
19 */
20 class WidgetOpts_Snippets_CPT {
21
22 /**
23 * Post type name
24 */
25 const POST_TYPE = 'widgetopts_snippet';
26
27 /**
28 * Option name for migration status
29 */
30 const MIGRATION_OPTION = 'widgetopts_snippets_migration';
31
32 /**
33 * Display logic version that triggers scanning
34 */
35 const DISPLAY_LOGIC_VERSION = '2.0';
36
37 /**
38 * Initialize the class
39 */
40 public static function init() {
41 add_action('init', array(__CLASS__, 'register_post_type'));
42 add_action('init', array(__CLASS__, 'check_display_logic_version'), 20);
43 add_filter('post_row_actions', array(__CLASS__, 'remove_quick_edit'), 10, 2);
44 add_action('admin_head-post.php', array(__CLASS__, 'hide_publishing_actions'));
45 add_action('admin_head-post-new.php', array(__CLASS__, 'hide_publishing_actions'));
46 }
47
48 /**
49 * Register the Custom Post Type
50 */
51 public static function register_post_type() {
52 $labels = array(
53 'name' => _x('Logic Snippets', 'Post type general name', 'widget-options'),
54 'singular_name' => _x('Logic Snippet', 'Post type singular name', 'widget-options'),
55 'menu_name' => _x('Logic Snippets', 'Admin Menu text', 'widget-options'),
56 'name_admin_bar' => _x('Logic Snippet', 'Add New on Toolbar', 'widget-options'),
57 'add_new' => __('Add New', 'widget-options'),
58 'add_new_item' => __('Add New Snippet', 'widget-options'),
59 'new_item' => __('New Snippet', 'widget-options'),
60 'edit_item' => __('Edit Snippet', 'widget-options'),
61 'view_item' => __('View Snippet', 'widget-options'),
62 'all_items' => __('All Snippets', 'widget-options'),
63 'search_items' => __('Search Snippets', 'widget-options'),
64 'not_found' => __('No snippets found.', 'widget-options'),
65 'not_found_in_trash' => __('No snippets found in Trash.', 'widget-options'),
66 );
67
68 $args = array(
69 'labels' => $labels,
70 'public' => false,
71 'publicly_queryable' => false,
72 'show_ui' => true,
73 'show_in_menu' => false, // We'll add it as submenu
74 'query_var' => false,
75 'rewrite' => false,
76 'capability_type' => 'post',
77 'capabilities' => array(
78 'edit_post' => 'manage_options',
79 'read_post' => 'edit_posts',
80 'delete_post' => 'manage_options',
81 'edit_posts' => 'manage_options',
82 'edit_others_posts' => 'manage_options',
83 'publish_posts' => 'manage_options',
84 'read_private_posts' => 'manage_options',
85 'create_posts' => 'manage_options',
86 ),
87 'has_archive' => false,
88 'hierarchical' => false,
89 'menu_position' => null,
90 'supports' => array('title'),
91 'show_in_rest' => true,
92 );
93
94 register_post_type(self::POST_TYPE, $args);
95 }
96
97 /**
98 * Check display logic version and scan widgets if needed
99 * Runs once when version is below 2.0 or undefined
100 */
101 public static function check_display_logic_version() {
102 if (!is_admin()) {
103 return;
104 }
105
106 $version = get_option('wopts_display_logic_version', false);
107
108 if ($version !== false && version_compare($version, self::DISPLAY_LOGIC_VERSION, '>=')) {
109 return;
110 }
111
112 // Set version to prevent re-scanning
113 update_option('wopts_display_logic_version', self::DISPLAY_LOGIC_VERSION);
114
115 // Scan all widgets for legacy logic that needs migration
116 self::scan_for_legacy_logic();
117 }
118
119 /**
120 * Scan all widget sources for legacy display logic code
121 * Sets wopts_display_logic_migration_required flag if found
122 */
123 public static function scan_for_legacy_logic() {
124 // 1. Classic Widgets
125 $sidebars_widgets = get_option('sidebars_widgets', array());
126 foreach ($sidebars_widgets as $sidebar_id => $widgets) {
127 if (!is_array($widgets) || $sidebar_id === 'wp_inactive_widgets') {
128 continue;
129 }
130 foreach ($widgets as $widget_id) {
131 preg_match('/^(.+)-(\d+)$/', $widget_id, $matches);
132 if (count($matches) !== 3) {
133 continue;
134 }
135 $widget_base = $matches[1];
136 $widget_num = (int) $matches[2];
137 $widget_options_data = get_option('widget_' . $widget_base);
138 if (!is_array($widget_options_data) || !isset($widget_options_data[$widget_num])) {
139 continue;
140 }
141 $instance = $widget_options_data[$widget_num];
142
143 if (self::has_legacy_logic_in_widget_instance($instance)) {
144 update_option('wopts_display_logic_migration_required', true);
145 return;
146 }
147
148 if ($widget_base === 'block' && !empty($instance['content']) && is_string($instance['content'])) {
149 $blocks = parse_blocks($instance['content']);
150 if (self::has_legacy_logic_in_blocks($blocks)) {
151 update_option('wopts_display_logic_migration_required', true);
152 return;
153 }
154 }
155 }
156 }
157
158 // 2. Gutenberg blocks in posts
159 global $wpdb;
160 $has_legacy = $wpdb->get_var(
161 "SELECT COUNT(*) FROM {$wpdb->posts}
162 WHERE post_content LIKE '%extended_widget_opts%'
163 AND post_content LIKE '%\"logic\"%'
164 AND post_type != 'revision'
165 AND post_status IN ('publish', 'draft', 'private')
166 LIMIT 1"
167 );
168 if ($has_legacy > 0) {
169 update_option('wopts_display_logic_migration_required', true);
170 return;
171 }
172
173 // 3. Elementor
174 $has_elementor_legacy = $wpdb->get_var(
175 "SELECT COUNT(*) FROM {$wpdb->postmeta} pm
176 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
177 WHERE pm.meta_key = '_elementor_data'
178 AND pm.meta_value LIKE '%widgetopts_logic%'
179 LIMIT 1"
180 );
181 if ($has_elementor_legacy > 0) {
182 update_option('wopts_display_logic_migration_required', true);
183 return;
184 }
185
186 // 4. Beaver Builder
187 $has_beaver_legacy = $wpdb->get_var(
188 "SELECT COUNT(*) FROM {$wpdb->postmeta} pm
189 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
190 WHERE pm.meta_key LIKE '_fl_builder_%'
191 AND pm.meta_value LIKE '%widgetopts_logic%'
192 LIMIT 1"
193 );
194 if ($has_beaver_legacy > 0) {
195 update_option('wopts_display_logic_migration_required', true);
196 return;
197 }
198
199 // 5. SiteOrigin
200 $has_so_legacy = $wpdb->get_var(
201 "SELECT COUNT(*) FROM {$wpdb->postmeta} pm
202 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
203 WHERE pm.meta_key = 'panels_data'
204 AND pm.meta_value LIKE '%\"logic\"%'
205 LIMIT 1"
206 );
207 if ($has_so_legacy > 0) {
208 update_option('wopts_display_logic_migration_required', true);
209 return;
210 }
211 }
212
213 /**
214 * Check recursively whether parsed blocks contain legacy logic.
215 *
216 * @param array $blocks Parsed blocks.
217 * @return bool
218 */
219 private static function has_legacy_logic_in_blocks($blocks) {
220 foreach ($blocks as $block) {
221 if (!empty($block['attrs']['extended_widget_opts']['class']['logic'])) {
222 return true;
223 }
224
225 if (!empty($block['innerBlocks']) && self::has_legacy_logic_in_blocks($block['innerBlocks'])) {
226 return true;
227 }
228 }
229
230 return false;
231 }
232
233 /**
234 * Check whether a widget instance contains legacy logic in instance meta.
235 *
236 * @param array $instance Widget instance data.
237 * @return bool
238 */
239 private static function has_legacy_logic_in_widget_instance($instance) {
240 if (!is_array($instance)) {
241 return false;
242 }
243
244 foreach ($instance as $key => $value) {
245 if (strpos($key, 'extended_widget_opts') === 0 && is_array($value) && !empty($value['class']['logic'])) {
246 return true;
247 }
248 }
249
250 return false;
251 }
252
253 /**
254 * Check migration status and run if needed
255 */
256 public static function check_and_run_migration() {
257 $migration_status = get_option(self::MIGRATION_OPTION, array());
258
259 // If migration completed or failed (manual intervention required), do nothing
260 if (isset($migration_status['status']) && in_array($migration_status['status'], array('completed', 'failed'))) {
261 return;
262 }
263
264 // If never migrated, run migration
265 if (empty($migration_status)) {
266 self::run_migration();
267 }
268 }
269
270 /**
271 * Run the migration process
272 */
273 public static function run_migration() {
274 require_once WIDGETOPTS_PLUGIN_DIR . 'includes/snippets/class-snippets-migration.php';
275 WidgetOpts_Snippets_Migration::migrate();
276 }
277
278 /**
279 * Get all published snippets
280 *
281 * @param string $search Optional search term to filter snippets by title
282 * @return array Array of snippet objects with id, title, description
283 */
284 public static function get_all_snippets($search = '') {
285 $args = array(
286 'post_type' => self::POST_TYPE,
287 'post_status' => 'publish',
288 'posts_per_page' => -1,
289 'orderby' => 'title',
290 'order' => 'ASC',
291 );
292
293 if (!empty($search)) {
294 $args['s'] = $search;
295 }
296
297 $snippets = get_posts($args);
298
299 $result = array();
300 foreach ($snippets as $snippet) {
301 $result[] = array(
302 'id' => $snippet->ID,
303 'title' => $snippet->post_title,
304 'description' => get_post_meta($snippet->ID, '_widgetopts_snippet_description', true),
305 'code' => $snippet->post_content,
306 );
307 }
308
309 return $result;
310 }
311
312 /**
313 * Get snippet by ID
314 *
315 * @param int $snippet_id Snippet post ID
316 * @return array|false Snippet data or false if not found
317 */
318 public static function get_snippet($snippet_id) {
319 $snippet = get_post($snippet_id);
320
321 if (!$snippet || $snippet->post_type !== self::POST_TYPE || $snippet->post_status !== 'publish') {
322 return false;
323 }
324
325 return array(
326 'id' => $snippet->ID,
327 'title' => $snippet->post_title,
328 'description' => get_post_meta($snippet->ID, '_widgetopts_snippet_description', true),
329 'code' => $snippet->post_content,
330 );
331 }
332
333 /**
334 * Get snippet code by ID
335 *
336 * @param int $snippet_id Snippet post ID
337 * @return string|false Snippet code or false if not found
338 */
339 public static function get_snippet_code($snippet_id) {
340 $snippet = self::get_snippet($snippet_id);
341 return $snippet ? $snippet['code'] : false;
342 }
343
344 /**
345 * Create a new snippet
346 *
347 * @param string $title Snippet title
348 * @param string $code PHP code
349 * @param string $description Optional description
350 * @return int|WP_Error Post ID on success, WP_Error on failure
351 */
352 public static function create_snippet($title, $code, $description = '') {
353 $post_data = array(
354 'post_title' => sanitize_text_field($title),
355 'post_content' => $code, // Will be validated on execution
356 'post_type' => self::POST_TYPE,
357 'post_status' => 'publish',
358 );
359
360 $post_id = wp_insert_post($post_data, true);
361
362 if (!is_wp_error($post_id) && $description) {
363 update_post_meta($post_id, '_widgetopts_snippet_description', sanitize_textarea_field($description));
364 }
365
366 return $post_id;
367 }
368
369 /**
370 * Find snippet by code (for migration - to avoid duplicates)
371 *
372 * @param string $code PHP code to search
373 * @return int|false Snippet ID or false if not found
374 */
375 public static function find_snippet_by_code($code) {
376 global $wpdb;
377
378 $normalized_code = trim($code);
379
380 $snippet_id = $wpdb->get_var($wpdb->prepare(
381 "SELECT ID FROM {$wpdb->posts}
382 WHERE post_type = %s
383 AND post_status = 'publish'
384 AND TRIM(post_content) = %s
385 LIMIT 1",
386 self::POST_TYPE,
387 $normalized_code
388 ));
389
390 return $snippet_id ? (int) $snippet_id : false;
391 }
392
393 /**
394 * Generate a unique title for migrated snippet
395 *
396 * @param string $code PHP code
397 * @param int $index Migration index
398 * @return string Generated title
399 */
400 public static function generate_snippet_title($code, $index = 0) {
401 // Default: Migrated Snippet with index
402 return sprintf(__('Migrated Snippet #%d', 'widget-options'), $index + 1);
403 }
404
405 /**
406 * Remove Quick Edit from row actions
407 *
408 * @param array $actions Row actions
409 * @param WP_Post $post Current post
410 * @return array Modified actions
411 */
412 public static function remove_quick_edit($actions, $post) {
413 if ($post->post_type === self::POST_TYPE) {
414 unset($actions['inline hide-if-no-js']);
415 }
416 return $actions;
417 }
418
419 /**
420 * Hide publishing actions (status, visibility) in post editor
421 */
422 public static function hide_publishing_actions() {
423 global $post;
424
425 if (isset($post) && $post->post_type === self::POST_TYPE) {
426 echo '<style>
427 #minor-publishing-actions,
428 #visibility,
429 .misc-pub-visibility,
430 .misc-pub-curtime {
431 display: none !important;
432 }
433 </style>';
434 }
435 }
436 }
437
438 // Initialize
439 WidgetOpts_Snippets_CPT::init();
440