PluginProbe ʕ •ᴥ•ʔ
Widget Options – Advanced Conditional Visibility for Gutenberg Blocks & Classic Widgets / 4.2.3
Widget Options – Advanced Conditional Visibility for Gutenberg Blocks & Classic Widgets v4.2.3
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 2 months ago class-snippets-api.php 2 months ago class-snippets-cpt.php 2 months ago class-snippets-migration.php 2 months ago
class-snippets-cpt.php
437 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
116 /**
117 * Scan all widget sources for legacy display logic code
118 * Sets wopts_display_logic_migration_required flag if found
119 */
120 public static function scan_for_legacy_logic() {
121 // 1. Classic Widgets
122 $sidebars_widgets = get_option('sidebars_widgets', array());
123 foreach ($sidebars_widgets as $sidebar_id => $widgets) {
124 if (!is_array($widgets) || $sidebar_id === 'wp_inactive_widgets') {
125 continue;
126 }
127 foreach ($widgets as $widget_id) {
128 preg_match('/^(.+)-(\d+)$/', $widget_id, $matches);
129 if (count($matches) !== 3) {
130 continue;
131 }
132 $widget_base = $matches[1];
133 $widget_num = (int) $matches[2];
134 $widget_options_data = get_option('widget_' . $widget_base);
135 if (!is_array($widget_options_data) || !isset($widget_options_data[$widget_num])) {
136 continue;
137 }
138 $instance = $widget_options_data[$widget_num];
139
140 if (self::has_legacy_logic_in_widget_instance($instance)) {
141 update_option('wopts_display_logic_migration_required', true);
142 return;
143 }
144
145 if ($widget_base === 'block' && !empty($instance['content']) && is_string($instance['content'])) {
146 $blocks = parse_blocks($instance['content']);
147 if (self::has_legacy_logic_in_blocks($blocks)) {
148 update_option('wopts_display_logic_migration_required', true);
149 return;
150 }
151 }
152 }
153 }
154
155 // 2. Gutenberg blocks in posts
156 global $wpdb;
157 $has_legacy = $wpdb->get_var(
158 "SELECT COUNT(*) FROM {$wpdb->posts}
159 WHERE post_content LIKE '%extended_widget_opts%'
160 AND post_content LIKE '%\"logic\"%'
161 AND post_type != 'revision'
162 AND post_status IN ('publish', 'draft', 'private')
163 LIMIT 1"
164 );
165 if ($has_legacy > 0) {
166 update_option('wopts_display_logic_migration_required', true);
167 return;
168 }
169
170 // 3. Elementor
171 $has_elementor_legacy = $wpdb->get_var(
172 "SELECT COUNT(*) FROM {$wpdb->postmeta} pm
173 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
174 WHERE pm.meta_key = '_elementor_data'
175 AND pm.meta_value LIKE '%widgetopts_logic%'
176 LIMIT 1"
177 );
178 if ($has_elementor_legacy > 0) {
179 update_option('wopts_display_logic_migration_required', true);
180 return;
181 }
182
183 // 4. Beaver Builder
184 $has_beaver_legacy = $wpdb->get_var(
185 "SELECT COUNT(*) FROM {$wpdb->postmeta} pm
186 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
187 WHERE pm.meta_key LIKE '_fl_builder_%'
188 AND pm.meta_value LIKE '%widgetopts_logic%'
189 LIMIT 1"
190 );
191 if ($has_beaver_legacy > 0) {
192 update_option('wopts_display_logic_migration_required', true);
193 return;
194 }
195
196 // 5. SiteOrigin
197 $has_so_legacy = $wpdb->get_var(
198 "SELECT COUNT(*) FROM {$wpdb->postmeta} pm
199 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
200 WHERE pm.meta_key = 'panels_data'
201 AND pm.meta_value LIKE '%\"logic\"%'
202 LIMIT 1"
203 );
204 if ($has_so_legacy > 0) {
205 update_option('wopts_display_logic_migration_required', true);
206 return;
207 }
208 }
209
210 /**
211 * Check recursively whether parsed blocks contain legacy logic.
212 *
213 * @param array $blocks Parsed blocks.
214 * @return bool
215 */
216 private static function has_legacy_logic_in_blocks($blocks) {
217 foreach ($blocks as $block) {
218 if (!empty($block['attrs']['extended_widget_opts']['class']['logic'])) {
219 return true;
220 }
221
222 if (!empty($block['innerBlocks']) && self::has_legacy_logic_in_blocks($block['innerBlocks'])) {
223 return true;
224 }
225 }
226
227 return false;
228 }
229
230 /**
231 * Check whether a widget instance contains legacy logic in instance meta.
232 *
233 * @param array $instance Widget instance data.
234 * @return bool
235 */
236 private static function has_legacy_logic_in_widget_instance($instance) {
237 if (!is_array($instance)) {
238 return false;
239 }
240
241 foreach ($instance as $key => $value) {
242 if (strpos($key, 'extended_widget_opts') === 0 && is_array($value) && !empty($value['class']['logic'])) {
243 return true;
244 }
245 }
246
247 return false;
248 }
249
250 /**
251 * Check migration status and run if needed
252 */
253 public static function check_and_run_migration() {
254 $migration_status = get_option(self::MIGRATION_OPTION, array());
255
256 // If migration completed or failed (manual intervention required), do nothing
257 if (isset($migration_status['status']) && in_array($migration_status['status'], array('completed', 'failed'))) {
258 return;
259 }
260
261 // If never migrated, run migration
262 if (empty($migration_status)) {
263 self::run_migration();
264 }
265 }
266
267 /**
268 * Run the migration process
269 */
270 public static function run_migration() {
271 require_once WIDGETOPTS_PLUGIN_DIR . 'includes/snippets/class-snippets-migration.php';
272 WidgetOpts_Snippets_Migration::migrate();
273 }
274
275 /**
276 * Get all published snippets
277 *
278 * @param string $search Optional search term to filter snippets by title
279 * @return array Array of snippet objects with id, title, description
280 */
281 public static function get_all_snippets($search = '') {
282 $args = array(
283 'post_type' => self::POST_TYPE,
284 'post_status' => 'publish',
285 'posts_per_page' => -1,
286 'orderby' => 'title',
287 'order' => 'ASC',
288 );
289
290 if (!empty($search)) {
291 $args['s'] = $search;
292 }
293
294 $snippets = get_posts($args);
295
296 $result = array();
297 foreach ($snippets as $snippet) {
298 $result[] = array(
299 'id' => $snippet->ID,
300 'title' => $snippet->post_title,
301 'description' => get_post_meta($snippet->ID, '_widgetopts_snippet_description', true),
302 'code' => $snippet->post_content,
303 );
304 }
305
306 return $result;
307 }
308
309 /**
310 * Get snippet by ID
311 *
312 * @param int $snippet_id Snippet post ID
313 * @return array|false Snippet data or false if not found
314 */
315 public static function get_snippet($snippet_id) {
316 $snippet = get_post($snippet_id);
317
318 if (!$snippet || $snippet->post_type !== self::POST_TYPE || $snippet->post_status !== 'publish') {
319 return false;
320 }
321
322 return array(
323 'id' => $snippet->ID,
324 'title' => $snippet->post_title,
325 'description' => get_post_meta($snippet->ID, '_widgetopts_snippet_description', true),
326 'code' => $snippet->post_content,
327 );
328 }
329
330 /**
331 * Get snippet code by ID
332 *
333 * @param int $snippet_id Snippet post ID
334 * @return string|false Snippet code or false if not found
335 */
336 public static function get_snippet_code($snippet_id) {
337 $snippet = self::get_snippet($snippet_id);
338 return $snippet ? $snippet['code'] : false;
339 }
340
341 /**
342 * Create a new snippet
343 *
344 * @param string $title Snippet title
345 * @param string $code PHP code
346 * @param string $description Optional description
347 * @return int|WP_Error Post ID on success, WP_Error on failure
348 */
349 public static function create_snippet($title, $code, $description = '') {
350 $post_data = array(
351 'post_title' => sanitize_text_field($title),
352 'post_content' => $code, // Will be validated on execution
353 'post_type' => self::POST_TYPE,
354 'post_status' => 'publish',
355 );
356
357 $post_id = wp_insert_post($post_data, true);
358
359 if (!is_wp_error($post_id) && $description) {
360 update_post_meta($post_id, '_widgetopts_snippet_description', sanitize_textarea_field($description));
361 }
362
363 return $post_id;
364 }
365
366 /**
367 * Find snippet by code (for migration - to avoid duplicates)
368 *
369 * @param string $code PHP code to search
370 * @return int|false Snippet ID or false if not found
371 */
372 public static function find_snippet_by_code($code) {
373 global $wpdb;
374
375 $normalized_code = trim($code);
376
377 $snippet_id = $wpdb->get_var($wpdb->prepare(
378 "SELECT ID FROM {$wpdb->posts}
379 WHERE post_type = %s
380 AND post_status = 'publish'
381 AND TRIM(post_content) = %s
382 LIMIT 1",
383 self::POST_TYPE,
384 $normalized_code
385 ));
386
387 return $snippet_id ? (int) $snippet_id : false;
388 }
389
390 /**
391 * Generate a unique title for migrated snippet
392 *
393 * @param string $code PHP code
394 * @param int $index Migration index
395 * @return string Generated title
396 */
397 public static function generate_snippet_title($code, $index = 0) {
398 // Default: Migrated Snippet with index
399 return sprintf(__('Migrated Snippet #%d', 'widget-options'), $index + 1);
400 }
401
402 /**
403 * Remove Quick Edit from row actions
404 *
405 * @param array $actions Row actions
406 * @param WP_Post $post Current post
407 * @return array Modified actions
408 */
409 public static function remove_quick_edit($actions, $post) {
410 if ($post->post_type === self::POST_TYPE) {
411 unset($actions['inline hide-if-no-js']);
412 }
413 return $actions;
414 }
415
416 /**
417 * Hide publishing actions (status, visibility) in post editor
418 */
419 public static function hide_publishing_actions() {
420 global $post;
421
422 if (isset($post) && $post->post_type === self::POST_TYPE) {
423 echo '<style>
424 #minor-publishing-actions,
425 #visibility,
426 .misc-pub-visibility,
427 .misc-pub-curtime {
428 display: none !important;
429 }
430 </style>';
431 }
432 }
433 }
434
435 // Initialize
436 WidgetOpts_Snippets_CPT::init();
437