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-migration.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-migration.php
1167 lines
1 <?php
2 /**
3 * Display Logic Snippets - Migration System
4 *
5 * Handles automatic migration from legacy inline logic code to the new
6 * snippets-based system. Scans all widgets across all editors and creates
7 * snippets from unique logic code patterns.
8 *
9 * @copyright Copyright (c) 2024, Widget Options Team
10 * @since 5.1
11 */
12
13 // Exit if accessed directly
14 if (!defined('ABSPATH')) exit;
15
16 /**
17 * Class WidgetOpts_Snippets_Migration
18 *
19 * Handles migration of legacy display logic to snippets
20 */
21 class WidgetOpts_Snippets_Migration {
22
23 /**
24 * Option name for migration status
25 */
26 const MIGRATION_OPTION = 'widgetopts_snippets_migration';
27
28 /**
29 * Run the complete migration process
30 *
31 * @return bool True on success, false on failure
32 */
33 public static function migrate() {
34 // Start migration
35 $migration_data = array(
36 'status' => 'in_progress',
37 'started_at' => current_time('mysql'),
38 'version' => WIDGETOPTS_VERSION,
39 );
40 update_option(self::MIGRATION_OPTION, $migration_data);
41
42 try {
43 $results = array(
44 'snippets_created' => 0,
45 'widgets_updated' => 0,
46 'errors' => array(),
47 );
48
49 // Collect all unique logic codes from all sources
50 $logic_codes = self::collect_all_logic_codes();
51
52 if (empty($logic_codes)) {
53 // No logic codes found - mark as completed
54 $migration_data['status'] = 'completed';
55 $migration_data['completed_at'] = current_time('mysql');
56 $migration_data['message'] = __('No legacy logic codes found to migrate.', 'widget-options');
57 update_option(self::MIGRATION_OPTION, $migration_data);
58 return true;
59 }
60
61 // Create snippets for unique codes
62 $code_to_snippet_map = self::create_snippets_from_codes($logic_codes, $results);
63
64 // Update all widgets with new snippet IDs
65 self::update_classic_widgets($code_to_snippet_map, $results);
66 self::update_gutenberg_blocks($code_to_snippet_map, $results);
67 self::update_elementor_widgets($code_to_snippet_map, $results);
68 self::update_beaver_builder_nodes($code_to_snippet_map, $results);
69 self::update_siteorigin_widgets($code_to_snippet_map, $results);
70
71 // Mark migration as completed
72 $migration_data['status'] = 'completed';
73 $migration_data['completed_at'] = current_time('mysql');
74 $migration_data['results'] = $results;
75 update_option(self::MIGRATION_OPTION, $migration_data);
76
77 return true;
78
79 } catch (Exception $e) {
80 // Mark migration as failed
81 $migration_data['status'] = 'failed';
82 $migration_data['failed_at'] = current_time('mysql');
83 $migration_data['error'] = $e->getMessage();
84 update_option(self::MIGRATION_OPTION, $migration_data);
85
86 return false;
87 }
88 }
89
90 /**
91 * Collect all unique logic codes from all widget sources
92 *
93 * @return array Array of unique logic codes
94 */
95 private static function collect_all_logic_codes() {
96 $codes = array();
97
98 // 1. Classic Widgets
99 $sidebars_widgets = get_option('sidebars_widgets', array());
100 foreach ($sidebars_widgets as $sidebar_id => $widgets) {
101 if (!is_array($widgets) || $sidebar_id === 'wp_inactive_widgets') {
102 continue;
103 }
104 foreach ($widgets as $widget_id) {
105 $codes = array_merge($codes, self::get_classic_widget_logic_codes($widget_id));
106 }
107 }
108
109 // 2. Gutenberg Widget Blocks (in widget areas)
110 $codes = array_merge($codes, self::collect_gutenberg_logic_codes());
111
112 // 3. Elementor
113 $codes = array_merge($codes, self::collect_elementor_logic_codes());
114
115 // 4. Beaver Builder
116 $codes = array_merge($codes, self::collect_beaver_logic_codes());
117
118 // 5. SiteOrigin
119 $codes = array_merge($codes, self::collect_siteorigin_logic_codes());
120
121 // Return unique codes only
122 return array_unique(array_filter($codes));
123 }
124
125 /**
126 * Get logic code from classic widget
127 *
128 * @param string $widget_id Widget ID
129 * @return array Array of logic codes
130 */
131 private static function get_classic_widget_logic_codes($widget_id) {
132 // Parse widget ID to get base and number
133 preg_match('/^(.+)-(\d+)$/', $widget_id, $matches);
134 if (count($matches) !== 3) {
135 return array();
136 }
137
138 $widget_base = $matches[1];
139 $widget_num = (int) $matches[2];
140
141 $widget_options = get_option('widget_' . $widget_base);
142 if (!is_array($widget_options) || !isset($widget_options[$widget_num])) {
143 return array();
144 }
145
146 $instance = $widget_options[$widget_num];
147
148 $codes = self::extract_logic_from_widget_instance($instance);
149
150 if ($widget_base === 'block' && !empty($instance['content']) && is_string($instance['content'])) {
151 $codes = array_merge($codes, self::extract_logic_from_blocks(parse_blocks($instance['content'])));
152 }
153
154 return array_unique(array_filter($codes));
155 }
156
157 /**
158 * Collect logic codes from Gutenberg blocks
159 *
160 * @return array Array of logic codes
161 */
162 private static function collect_gutenberg_logic_codes() {
163 $codes = array();
164
165 // Get all posts that might contain blocks
166 global $wpdb;
167 $posts = $wpdb->get_results(
168 "SELECT ID, post_content FROM {$wpdb->posts}
169 WHERE post_content LIKE '%extended_widget_opts%'
170 AND post_type != 'revision'
171 AND post_status IN ('publish', 'draft', 'private')"
172 );
173
174 foreach ($posts as $post) {
175 $blocks = parse_blocks($post->post_content);
176 $codes = array_merge($codes, self::extract_logic_from_blocks($blocks));
177 }
178
179 return $codes;
180 }
181
182 /**
183 * Extract logic codes from blocks recursively
184 *
185 * @param array $blocks Array of blocks
186 * @return array Array of logic codes
187 */
188 private static function extract_logic_from_blocks($blocks) {
189 $codes = array();
190
191 foreach ($blocks as $block) {
192 if (isset($block['attrs']['extended_widget_opts']['class']['logic'])) {
193 $logic = $block['attrs']['extended_widget_opts']['class']['logic'];
194 if (!empty($logic)) {
195 $codes[] = trim($logic);
196 }
197 }
198
199 // Check inner blocks
200 if (!empty($block['innerBlocks'])) {
201 $codes = array_merge($codes, self::extract_logic_from_blocks($block['innerBlocks']));
202 }
203 }
204
205 return $codes;
206 }
207
208 private static function extract_logic_from_widget_instance($instance) {
209 $codes = array();
210
211 if (!is_array($instance)) {
212 return $codes;
213 }
214
215 foreach ($instance as $key => $value) {
216 if (strpos($key, 'extended_widget_opts') === 0 && is_array($value)) {
217 if (isset($value['class']['logic']) && !empty($value['class']['logic'])) {
218 $codes[] = trim($value['class']['logic']);
219 }
220 }
221 }
222
223 return $codes;
224 }
225
226 /**
227 * Collect logic codes from Elementor
228 *
229 * @return array Array of logic codes
230 */
231 private static function collect_elementor_logic_codes() {
232 $codes = array();
233
234 global $wpdb;
235 $elementor_data = $wpdb->get_results(
236 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
237 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
238 WHERE pm.meta_key = '_elementor_data'
239 AND pm.meta_value LIKE '%widgetopts_logic%'"
240 );
241
242 foreach ($elementor_data as $data) {
243 $elements = json_decode($data->meta_value, true);
244 if (is_array($elements)) {
245 $codes = array_merge($codes, self::extract_elementor_logic($elements));
246 }
247 }
248
249 return $codes;
250 }
251
252 /**
253 * Extract logic codes from Elementor elements recursively
254 *
255 * @param array $elements Array of elements
256 * @return array Array of logic codes
257 */
258 private static function extract_elementor_logic($elements) {
259 $codes = array();
260
261 foreach ($elements as $element) {
262 if (isset($element['settings']['widgetopts_logic']) && !empty($element['settings']['widgetopts_logic'])) {
263 $codes[] = trim($element['settings']['widgetopts_logic']);
264 }
265
266 if (!empty($element['elements'])) {
267 $codes = array_merge($codes, self::extract_elementor_logic($element['elements']));
268 }
269 }
270
271 return $codes;
272 }
273
274 /**
275 * Collect logic codes from Beaver Builder
276 *
277 * @return array Array of logic codes
278 */
279 private static function collect_beaver_logic_codes() {
280 $codes = array();
281
282 global $wpdb;
283 $beaver_data = $wpdb->get_results(
284 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
285 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
286 WHERE pm.meta_key = '_fl_builder_data'
287 AND pm.meta_value LIKE '%widgetopts_settings_logic%'"
288 );
289
290 foreach ($beaver_data as $data) {
291 $nodes = maybe_unserialize($data->meta_value);
292 if (is_array($nodes)) {
293 foreach ($nodes as $node) {
294 if (isset($node->settings->widgetopts_settings_logic) && !empty($node->settings->widgetopts_settings_logic)) {
295 $codes[] = trim($node->settings->widgetopts_settings_logic);
296 }
297 }
298 }
299 }
300
301 return $codes;
302 }
303
304 /**
305 * Collect logic codes from SiteOrigin Page Builder
306 *
307 * @return array Array of logic codes
308 */
309 private static function collect_siteorigin_logic_codes() {
310 $codes = array();
311
312 global $wpdb;
313 $siteorigin_data = $wpdb->get_results(
314 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
315 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
316 WHERE pm.meta_key = 'panels_data'
317 AND pm.meta_value LIKE '%extended_widget_opts%'"
318 );
319
320 foreach ($siteorigin_data as $data) {
321 $panels = maybe_unserialize($data->meta_value);
322 if (is_array($panels) && isset($panels['widgets'])) {
323 foreach ($panels['widgets'] as $widget) {
324 if (isset($widget['extended_widget_opts']['class']['logic']) && !empty($widget['extended_widget_opts']['class']['logic'])) {
325 $codes[] = trim($widget['extended_widget_opts']['class']['logic']);
326 }
327 }
328 }
329 }
330
331 return $codes;
332 }
333
334 /**
335 * Create snippets from collected codes
336 *
337 * @param array $codes Array of logic codes
338 * @param array &$results Results array to update
339 * @return array Map of code => snippet_id
340 */
341 private static function create_snippets_from_codes($codes, &$results) {
342 $code_to_snippet_map = array();
343 $index = 0;
344
345 foreach ($codes as $code) {
346 $normalized_code = trim($code);
347
348 // Check if snippet with this code already exists
349 $existing_id = WidgetOpts_Snippets_CPT::find_snippet_by_code($normalized_code);
350
351 if ($existing_id) {
352 $code_to_snippet_map[$normalized_code] = $existing_id;
353 continue;
354 }
355
356 // Generate title based on code content
357 $title = WidgetOpts_Snippets_CPT::generate_snippet_title($normalized_code, $index);
358
359 // Ensure unique title
360 $title = self::ensure_unique_title($title);
361
362 // Create snippet
363 $snippet_id = WidgetOpts_Snippets_CPT::create_snippet(
364 $title,
365 $normalized_code,
366 __('Automatically migrated from legacy display logic.', 'widget-options')
367 );
368
369 if (!is_wp_error($snippet_id)) {
370 $code_to_snippet_map[$normalized_code] = $snippet_id;
371 $results['snippets_created']++;
372 $index++;
373 } else {
374 $results['errors'][] = sprintf(
375 __('Failed to create snippet for code: %s', 'widget-options'),
376 substr($normalized_code, 0, 50) . '...'
377 );
378 }
379 }
380
381 return $code_to_snippet_map;
382 }
383
384 /**
385 * Ensure snippet title is unique
386 *
387 * @param string $title Original title
388 * @return string Unique title
389 */
390 private static function ensure_unique_title($title) {
391 $original_title = $title;
392 $counter = 1;
393
394 while (get_page_by_title($title, OBJECT, WidgetOpts_Snippets_CPT::POST_TYPE)) {
395 $counter++;
396 $title = $original_title . ' (' . $counter . ')';
397 }
398
399 return $title;
400 }
401
402 /**
403 * Update classic widgets with snippet IDs
404 *
405 * @param array $code_to_snippet_map Map of code => snippet_id
406 * @param array &$results Results array to update
407 */
408 private static function update_classic_widgets($code_to_snippet_map, &$results) {
409 global $wpdb;
410
411 // Get all widget options
412 $widget_options = $wpdb->get_results(
413 "SELECT option_name, option_value FROM {$wpdb->options}
414 WHERE option_name LIKE 'widget_%'"
415 );
416
417 foreach ($widget_options as $option) {
418 $widgets = maybe_unserialize($option->option_value);
419
420 if (!is_array($widgets)) {
421 continue;
422 }
423
424 $updated = false;
425
426 foreach ($widgets as $widget_num => &$instance) {
427 if (!is_array($instance)) {
428 continue;
429 }
430
431 if ($option->option_name === 'widget_block' && !empty($instance['content']) && is_string($instance['content'])) {
432 $blocks = parse_blocks($instance['content']);
433 $updated_blocks = self::update_blocks_with_snippets($blocks, $code_to_snippet_map, $results);
434
435 if ($updated_blocks !== false) {
436 $instance['content'] = serialize_blocks($updated_blocks);
437 $updated = true;
438 }
439 }
440
441 foreach ($instance as $key => &$value) {
442 if (strpos($key, 'extended_widget_opts') === 0 && is_array($value)) {
443 if (isset($value['class']['logic']) && !empty($value['class']['logic'])) {
444 $code = trim($value['class']['logic']);
445
446 if (isset($code_to_snippet_map[$code])) {
447 // Backup old logic code
448 $value['class']['logic_backup'] = $code;
449 // Remove old logic field
450 unset($value['class']['logic']);
451 // Set new snippet ID
452 $value['class']['logic_snippet_id'] = $code_to_snippet_map[$code];
453 // Stamp version
454 $value['class']['wopt_version'] = WIDGETOPTS_VERSION;
455 $updated = true;
456 $results['widgets_updated']++;
457 }
458 }
459 }
460 }
461 }
462
463 if ($updated) {
464 update_option($option->option_name, $widgets);
465 }
466 }
467 }
468
469 /**
470 * Update Gutenberg blocks with snippet IDs
471 *
472 * @param array $code_to_snippet_map Map of code => snippet_id
473 * @param array &$results Results array to update
474 */
475 private static function update_gutenberg_blocks($code_to_snippet_map, &$results) {
476 global $wpdb;
477
478 $posts = $wpdb->get_results(
479 "SELECT ID, post_content FROM {$wpdb->posts}
480 WHERE post_content LIKE '%extended_widget_opts%'
481 AND post_type != 'revision'
482 AND post_status IN ('publish', 'draft', 'private')"
483 );
484
485 foreach ($posts as $post) {
486 $blocks = parse_blocks($post->post_content);
487 $updated_blocks = self::update_blocks_with_snippets($blocks, $code_to_snippet_map, $results);
488
489 if ($updated_blocks !== false) {
490 $new_content = serialize_blocks($updated_blocks);
491 $wpdb->update(
492 $wpdb->posts,
493 array('post_content' => $new_content),
494 array('ID' => $post->ID),
495 array('%s'),
496 array('%d')
497 );
498 }
499 }
500 }
501
502 /**
503 * Update blocks recursively with snippet IDs
504 *
505 * @param array $blocks Array of blocks
506 * @param array $code_to_snippet_map Map of code => snippet_id
507 * @param array &$results Results array
508 * @return array|false Updated blocks or false if no changes
509 */
510 private static function update_blocks_with_snippets($blocks, $code_to_snippet_map, &$results) {
511 $changed = false;
512
513 foreach ($blocks as &$block) {
514 if (isset($block['attrs']['extended_widget_opts']['class']['logic'])) {
515 $code = trim($block['attrs']['extended_widget_opts']['class']['logic']);
516
517 if (!empty($code) && isset($code_to_snippet_map[$code])) {
518 // Backup
519 $block['attrs']['extended_widget_opts']['class']['logic_backup'] = $code;
520 // Remove old
521 unset($block['attrs']['extended_widget_opts']['class']['logic']);
522 // Set new
523 $block['attrs']['extended_widget_opts']['class']['logic_snippet_id'] = $code_to_snippet_map[$code];
524 // Stamp version
525 $block['attrs']['extended_widget_opts']['class']['wopt_version'] = WIDGETOPTS_VERSION;
526 $changed = true;
527 $results['widgets_updated']++;
528 }
529 }
530
531 // Process inner blocks
532 if (!empty($block['innerBlocks'])) {
533 $updated_inner = self::update_blocks_with_snippets($block['innerBlocks'], $code_to_snippet_map, $results);
534 if ($updated_inner !== false) {
535 $block['innerBlocks'] = $updated_inner;
536 $changed = true;
537 }
538 }
539 }
540
541 return $changed ? $blocks : false;
542 }
543
544 /**
545 * Update Elementor widgets with snippet IDs
546 *
547 * @param array $code_to_snippet_map Map of code => snippet_id
548 * @param array &$results Results array to update
549 */
550 private static function update_elementor_widgets($code_to_snippet_map, &$results) {
551 global $wpdb;
552
553 $elementor_data = $wpdb->get_results(
554 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
555 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
556 WHERE pm.meta_key = '_elementor_data'
557 AND pm.meta_value LIKE '%widgetopts_logic%'"
558 );
559
560 foreach ($elementor_data as $data) {
561 $elements = json_decode($data->meta_value, true);
562
563 if (!is_array($elements)) {
564 continue;
565 }
566
567 $updated_elements = self::update_elementor_elements($elements, $code_to_snippet_map, $results);
568
569 if ($updated_elements !== false) {
570 update_post_meta($data->post_id, '_elementor_data', wp_slash(json_encode($updated_elements)));
571 }
572 }
573 }
574
575 /**
576 * Update Elementor elements recursively
577 *
578 * @param array $elements Array of elements
579 * @param array $code_to_snippet_map Map of code => snippet_id
580 * @param array &$results Results array
581 * @return array|false Updated elements or false if no changes
582 */
583 private static function update_elementor_elements($elements, $code_to_snippet_map, &$results) {
584 $changed = false;
585
586 foreach ($elements as &$element) {
587 if (isset($element['settings']['widgetopts_logic']) && !empty($element['settings']['widgetopts_logic'])) {
588 $code = trim($element['settings']['widgetopts_logic']);
589
590 if (isset($code_to_snippet_map[$code])) {
591 // Backup
592 $element['settings']['widgetopts_logic_backup'] = $code;
593 // Remove old
594 unset($element['settings']['widgetopts_logic']);
595 // Set new
596 $element['settings']['widgetopts_logic_snippet_id'] = $code_to_snippet_map[$code];
597 // Stamp version
598 $element['settings']['widgetopts_wopt_version'] = WIDGETOPTS_VERSION;
599 $changed = true;
600 $results['widgets_updated']++;
601 }
602 }
603
604 // Process child elements
605 if (!empty($element['elements'])) {
606 $updated_children = self::update_elementor_elements($element['elements'], $code_to_snippet_map, $results);
607 if ($updated_children !== false) {
608 $element['elements'] = $updated_children;
609 $changed = true;
610 }
611 }
612 }
613
614 return $changed ? $elements : false;
615 }
616
617 /**
618 * Update Beaver Builder nodes with snippet IDs
619 *
620 * @param array $code_to_snippet_map Map of code => snippet_id
621 * @param array &$results Results array to update
622 */
623 private static function update_beaver_builder_nodes($code_to_snippet_map, &$results) {
624 global $wpdb;
625
626 // Update both published and draft BB data
627 $meta_keys = array('_fl_builder_data', '_fl_builder_draft');
628
629 foreach ($meta_keys as $meta_key) {
630 $beaver_data = $wpdb->get_results($wpdb->prepare(
631 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
632 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
633 WHERE pm.meta_key = %s
634 AND pm.meta_value LIKE %s",
635 $meta_key,
636 '%widgetopts_settings_logic%'
637 ));
638
639 foreach ($beaver_data as $data) {
640 $nodes = maybe_unserialize($data->meta_value);
641
642 if (!is_array($nodes)) {
643 continue;
644 }
645
646 $changed = false;
647
648 foreach ($nodes as &$node) {
649 if (isset($node->settings->widgetopts_settings_logic) && !empty($node->settings->widgetopts_settings_logic)) {
650 $code = trim($node->settings->widgetopts_settings_logic);
651
652 if (isset($code_to_snippet_map[$code])) {
653 // Backup
654 $node->settings->widgetopts_settings_logic_backup = $code;
655 // Remove old
656 unset($node->settings->widgetopts_settings_logic);
657 // Set new
658 $node->settings->widgetopts_logic_snippet_id = $code_to_snippet_map[$code];
659 // Stamp version
660 $node->settings->widgetopts_wopt_version = WIDGETOPTS_VERSION;
661 $changed = true;
662 if ($meta_key === '_fl_builder_data') {
663 $results['widgets_updated']++;
664 }
665 }
666 }
667 }
668
669 if ($changed) {
670 update_post_meta($data->post_id, $meta_key, $nodes);
671 }
672 }
673 }
674 }
675
676 /**
677 * Update SiteOrigin widgets with snippet IDs
678 *
679 * @param array $code_to_snippet_map Map of code => snippet_id
680 * @param array &$results Results array to update
681 */
682 private static function update_siteorigin_widgets($code_to_snippet_map, &$results) {
683 global $wpdb;
684
685 $siteorigin_data = $wpdb->get_results(
686 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
687 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
688 WHERE pm.meta_key = 'panels_data'
689 AND pm.meta_value LIKE '%extended_widget_opts%'"
690 );
691
692 foreach ($siteorigin_data as $data) {
693 $panels = maybe_unserialize($data->meta_value);
694
695 if (!is_array($panels) || !isset($panels['widgets'])) {
696 continue;
697 }
698
699 $changed = false;
700
701 foreach ($panels['widgets'] as &$widget) {
702 if (isset($widget['extended_widget_opts']['class']['logic']) && !empty($widget['extended_widget_opts']['class']['logic'])) {
703 $code = trim($widget['extended_widget_opts']['class']['logic']);
704
705 if (isset($code_to_snippet_map[$code])) {
706 // Backup
707 $widget['extended_widget_opts']['class']['logic_backup'] = $code;
708 // Remove old
709 unset($widget['extended_widget_opts']['class']['logic']);
710 // Set new
711 $widget['extended_widget_opts']['class']['logic_snippet_id'] = $code_to_snippet_map[$code];
712 // Stamp version
713 $widget['extended_widget_opts']['class']['wopt_version'] = WIDGETOPTS_VERSION;
714 $changed = true;
715 $results['widgets_updated']++;
716 }
717 }
718 }
719
720 if ($changed) {
721 update_post_meta($data->post_id, 'panels_data', $panels);
722 }
723 }
724 }
725
726 /**
727 * Get migration status
728 *
729 * @return array Migration status data
730 */
731 public static function get_migration_status() {
732 return get_option(self::MIGRATION_OPTION, array());
733 }
734
735 /**
736 * Reset migration status (for manual retry)
737 */
738 public static function reset_migration() {
739 delete_option(self::MIGRATION_OPTION);
740 }
741
742 /**
743 * Collect all legacy logic codes grouped by unique code with location details.
744 * Used by the migration page to display a table of snippets to migrate.
745 *
746 * @return array Array of items: [ 'code' => string, 'locations' => [ ['type','label','id'], ... ] ]
747 */
748 public static function collect_all_logic_codes_with_locations() {
749 $map = array(); // code => [ locations ]
750
751 // Helper to add a location entry
752 $add = function ($code, $type, $label, $id = '') use (&$map) {
753 $code = trim($code);
754 if (empty($code)) return;
755 $key = md5($code);
756 if (!isset($map[$key])) {
757 $map[$key] = array('code' => $code, 'locations' => array());
758 }
759 $map[$key]['locations'][] = array(
760 'type' => $type,
761 'label' => $label,
762 'id' => $id,
763 );
764 };
765
766 // 1. Classic Widgets
767 $sidebars_widgets = get_option('sidebars_widgets', array());
768 foreach ($sidebars_widgets as $sidebar_id => $widgets) {
769 if (!is_array($widgets) || $sidebar_id === 'wp_inactive_widgets') {
770 continue;
771 }
772 foreach ($widgets as $widget_id) {
773 $codes = self::get_classic_widget_logic_codes($widget_id);
774 foreach ($codes as $logic) {
775 $add($logic, 'classic_widget', $sidebar_id . ' / ' . $widget_id, $widget_id);
776 }
777 }
778 }
779
780 // 2. Gutenberg blocks
781 global $wpdb;
782 $posts = $wpdb->get_results(
783 "SELECT ID, post_title, post_content FROM {$wpdb->posts}
784 WHERE post_content LIKE '%extended_widget_opts%'
785 AND post_type != 'revision'
786 AND post_status IN ('publish', 'draft', 'private')"
787 );
788 foreach ($posts as $post) {
789 $blocks = parse_blocks($post->post_content);
790 self::extract_logic_from_blocks_with_locations($blocks, $post, $add);
791 }
792
793 // 3. Elementor
794 $elementor_rows = $wpdb->get_results(
795 "SELECT pm.post_id, pm.meta_value, p.post_title
796 FROM {$wpdb->postmeta} pm
797 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
798 WHERE pm.meta_key = '_elementor_data'
799 AND pm.meta_value LIKE '%widgetopts_logic%'"
800 );
801 foreach ($elementor_rows as $row) {
802 $elements = json_decode($row->meta_value, true);
803 if (is_array($elements)) {
804 self::extract_elementor_logic_with_locations($elements, $row, $add);
805 }
806 }
807
808 // 4. Beaver Builder
809 $beaver_rows = $wpdb->get_results(
810 "SELECT pm.post_id, pm.meta_value, p.post_title
811 FROM {$wpdb->postmeta} pm
812 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
813 WHERE pm.meta_key = '_fl_builder_data'
814 AND pm.meta_value LIKE '%widgetopts_settings_logic%'"
815 );
816 foreach ($beaver_rows as $row) {
817 $nodes = maybe_unserialize($row->meta_value);
818 if (is_array($nodes)) {
819 foreach ($nodes as $node) {
820 if (isset($node->settings->widgetopts_settings_logic) && !empty($node->settings->widgetopts_settings_logic)) {
821 $label = 'Beaver Builder — ' . ($row->post_title ?: '#' . $row->post_id);
822 $add($node->settings->widgetopts_settings_logic, 'beaver', $label, $row->post_id);
823 }
824 }
825 }
826 }
827
828 // 5. SiteOrigin
829 $so_rows = $wpdb->get_results(
830 "SELECT pm.post_id, pm.meta_value, p.post_title
831 FROM {$wpdb->postmeta} pm
832 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
833 WHERE pm.meta_key = 'panels_data'
834 AND pm.meta_value LIKE '%extended_widget_opts%'"
835 );
836 foreach ($so_rows as $row) {
837 $panels = maybe_unserialize($row->meta_value);
838 if (is_array($panels) && isset($panels['widgets'])) {
839 foreach ($panels['widgets'] as $widget) {
840 if (isset($widget['extended_widget_opts']['class']['logic']) && !empty($widget['extended_widget_opts']['class']['logic'])) {
841 $label = 'SiteOrigin — ' . ($row->post_title ?: '#' . $row->post_id);
842 $add($widget['extended_widget_opts']['class']['logic'], 'siteorigin', $label, $row->post_id);
843 }
844 }
845 }
846 }
847
848 // Re-index
849 return array_values($map);
850 }
851
852 /**
853 * Extract logic from Gutenberg blocks with location info (recursive)
854 */
855 private static function extract_logic_from_blocks_with_locations($blocks, $post, $add) {
856 foreach ($blocks as $block) {
857 if (isset($block['attrs']['extended_widget_opts']['class']['logic'])) {
858 $logic = $block['attrs']['extended_widget_opts']['class']['logic'];
859 if (!empty($logic)) {
860 $block_name = !empty($block['blockName']) ? $block['blockName'] : 'block';
861 $label = 'Block editor — ' . ($post->post_title ?: '#' . $post->ID) . ' (' . $block_name . ')';
862 $add($logic, 'gutenberg', $label, $post->ID);
863 }
864 }
865 if (!empty($block['innerBlocks'])) {
866 self::extract_logic_from_blocks_with_locations($block['innerBlocks'], $post, $add);
867 }
868 }
869 }
870
871 /**
872 * Extract logic from Elementor elements with location info (recursive)
873 */
874 private static function extract_elementor_logic_with_locations($elements, $row, $add) {
875 foreach ($elements as $element) {
876 if (isset($element['settings']['widgetopts_logic']) && !empty($element['settings']['widgetopts_logic'])) {
877 $widget_type = isset($element['widgetType']) ? $element['widgetType'] : (isset($element['elType']) ? $element['elType'] : 'element');
878 $label = 'Elementor — ' . ($row->post_title ?: '#' . $row->post_id) . ' (' . $widget_type . ')';
879 $add($element['settings']['widgetopts_logic'], 'elementor', $label, $row->post_id);
880 }
881 if (!empty($element['elements'])) {
882 self::extract_elementor_logic_with_locations($element['elements'], $row, $add);
883 }
884 }
885 }
886
887 /**
888 * Migrate selected snippets by their code hashes.
889 *
890 * @param array $items Array of [ 'hash' => md5, 'title' => string ]
891 * @return array Results with snippets_created, widgets_updated, errors
892 */
893 public static function migrate_selected($items) {
894 $results = array(
895 'snippets_created' => 0,
896 'widgets_updated' => 0,
897 'errors' => array(),
898 );
899
900 // Build full scan first to get codes for hashes
901 $all = self::collect_all_logic_codes_with_locations();
902 $hash_to_code = array();
903 foreach ($all as $entry) {
904 $hash_to_code[md5($entry['code'])] = $entry['code'];
905 }
906
907 $code_to_snippet_map = array();
908
909 foreach ($items as $item) {
910 $hash = $item['hash'];
911 $title = !empty($item['title']) ? $item['title'] : 'Migrated Snippet';
912
913 if (!isset($hash_to_code[$hash])) {
914 $results['errors'][] = sprintf(
915 __('Code not found for hash: %s', 'widget-options'),
916 $hash
917 );
918 continue;
919 }
920
921 $code = $hash_to_code[$hash];
922
923 // Check if snippet with this code already exists
924 $existing_id = WidgetOpts_Snippets_CPT::find_snippet_by_code($code);
925 if ($existing_id) {
926 $code_to_snippet_map[$code] = $existing_id;
927 continue;
928 }
929
930 $snippet_id = WidgetOpts_Snippets_CPT::create_snippet(
931 $title,
932 $code,
933 __('Automatically migrated from legacy display logic.', 'widget-options')
934 );
935
936 if (!is_wp_error($snippet_id)) {
937 $code_to_snippet_map[$code] = $snippet_id;
938 $results['snippets_created']++;
939 } else {
940 $results['errors'][] = sprintf(
941 __('Failed to create snippet "%s": %s', 'widget-options'),
942 $title,
943 $snippet_id->get_error_message()
944 );
945 }
946 }
947
948 if (!empty($code_to_snippet_map)) {
949 self::update_classic_widgets($code_to_snippet_map, $results);
950 self::update_gutenberg_blocks($code_to_snippet_map, $results);
951 self::update_elementor_widgets($code_to_snippet_map, $results);
952 self::update_beaver_builder_nodes($code_to_snippet_map, $results);
953 self::update_siteorigin_widgets($code_to_snippet_map, $results);
954 }
955
956 // Re-scan and update migration flag
957 self::rescan_and_update_flag();
958
959 return $results;
960 }
961
962 /**
963 * Delete legacy logic code by hash from ALL editors (remove the field entirely).
964 * This means display logic will no longer execute for those widgets.
965 *
966 * @param string $hash md5 hash of the code
967 * @return array Results
968 */
969 public static function delete_legacy_code($hash) {
970 $results = array('widgets_updated' => 0);
971
972 // Get code for this hash
973 $all = self::collect_all_logic_codes_with_locations();
974 $code = null;
975 foreach ($all as $entry) {
976 if (md5($entry['code']) === $hash) {
977 $code = $entry['code'];
978 break;
979 }
980 }
981
982 if ($code === null) {
983 return $results;
984 }
985
986 $normalized = trim($code);
987
988 // 1. Classic Widgets
989 global $wpdb;
990 $widget_opts = $wpdb->get_results(
991 "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE 'widget_%'"
992 );
993 foreach ($widget_opts as $option) {
994 $widgets = maybe_unserialize($option->option_value);
995 if (!is_array($widgets)) continue;
996 $updated = false;
997 foreach ($widgets as &$instance) {
998 if (!is_array($instance)) continue;
999
1000 if ($option->option_name === 'widget_block' && !empty($instance['content']) && is_string($instance['content'])) {
1001 $blocks = parse_blocks($instance['content']);
1002 $changed = self::delete_logic_from_blocks($blocks, $normalized, $results);
1003 if ($changed !== false) {
1004 $instance['content'] = serialize_blocks($changed);
1005 $updated = true;
1006 }
1007 }
1008
1009 foreach ($instance as $key => &$value) {
1010 if (strpos($key, 'extended_widget_opts') === 0 && is_array($value)) {
1011 if (isset($value['class']['logic']) && trim($value['class']['logic']) === $normalized) {
1012 unset($value['class']['logic']);
1013 $updated = true;
1014 $results['widgets_updated']++;
1015 }
1016 }
1017 }
1018 }
1019 if ($updated) {
1020 update_option($option->option_name, $widgets);
1021 }
1022 }
1023
1024 // 2. Gutenberg
1025 $posts = $wpdb->get_results(
1026 "SELECT ID, post_content FROM {$wpdb->posts}
1027 WHERE post_content LIKE '%extended_widget_opts%'
1028 AND post_type != 'revision'
1029 AND post_status IN ('publish', 'draft', 'private')"
1030 );
1031 foreach ($posts as $post) {
1032 $blocks = parse_blocks($post->post_content);
1033 $changed = self::delete_logic_from_blocks($blocks, $normalized, $results);
1034 if ($changed !== false) {
1035 $wpdb->update($wpdb->posts, array('post_content' => serialize_blocks($changed)), array('ID' => $post->ID), array('%s'), array('%d'));
1036 }
1037 }
1038
1039 // 3. Elementor
1040 $el_rows = $wpdb->get_results(
1041 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
1042 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
1043 WHERE pm.meta_key = '_elementor_data' AND pm.meta_value LIKE '%widgetopts_logic%'"
1044 );
1045 foreach ($el_rows as $row) {
1046 $elements = json_decode($row->meta_value, true);
1047 if (!is_array($elements)) continue;
1048 $changed = self::delete_logic_from_elementor($elements, $normalized, $results);
1049 if ($changed !== false) {
1050 update_post_meta($row->post_id, '_elementor_data', wp_slash(json_encode($changed)));
1051 }
1052 }
1053
1054 // 4. Beaver Builder (both published and draft)
1055 foreach (array('_fl_builder_data', '_fl_builder_draft') as $bb_meta_key) {
1056 $bb_rows = $wpdb->get_results($wpdb->prepare(
1057 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
1058 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
1059 WHERE pm.meta_key = %s AND pm.meta_value LIKE %s",
1060 $bb_meta_key,
1061 '%widgetopts_settings_logic%'
1062 ));
1063 foreach ($bb_rows as $row) {
1064 $nodes = maybe_unserialize($row->meta_value);
1065 if (!is_array($nodes)) continue;
1066 $changed = false;
1067 foreach ($nodes as &$node) {
1068 if (isset($node->settings->widgetopts_settings_logic) && trim($node->settings->widgetopts_settings_logic) === $normalized) {
1069 unset($node->settings->widgetopts_settings_logic);
1070 $changed = true;
1071 if ($bb_meta_key === '_fl_builder_data') {
1072 $results['widgets_updated']++;
1073 }
1074 }
1075 }
1076 if ($changed) {
1077 update_post_meta($row->post_id, $bb_meta_key, $nodes);
1078 }
1079 }
1080 }
1081
1082 // 5. SiteOrigin
1083 $so_rows = $wpdb->get_results(
1084 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
1085 JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type != 'revision'
1086 WHERE pm.meta_key = 'panels_data' AND pm.meta_value LIKE '%extended_widget_opts%'"
1087 );
1088 foreach ($so_rows as $row) {
1089 $panels = maybe_unserialize($row->meta_value);
1090 if (!is_array($panels) || !isset($panels['widgets'])) continue;
1091 $changed = false;
1092 foreach ($panels['widgets'] as &$widget) {
1093 if (isset($widget['extended_widget_opts']['class']['logic']) && trim($widget['extended_widget_opts']['class']['logic']) === $normalized) {
1094 unset($widget['extended_widget_opts']['class']['logic']);
1095 $changed = true;
1096 $results['widgets_updated']++;
1097 }
1098 }
1099 if ($changed) {
1100 update_post_meta($row->post_id, 'panels_data', $panels);
1101 }
1102 }
1103
1104 // Re-scan
1105 self::rescan_and_update_flag();
1106
1107 return $results;
1108 }
1109
1110 /**
1111 * Delete logic field from Gutenberg blocks recursively
1112 */
1113 private static function delete_logic_from_blocks(&$blocks, $code, &$results) {
1114 $changed = false;
1115 foreach ($blocks as &$block) {
1116 if (isset($block['attrs']['extended_widget_opts']['class']['logic'])) {
1117 if (trim($block['attrs']['extended_widget_opts']['class']['logic']) === $code) {
1118 unset($block['attrs']['extended_widget_opts']['class']['logic']);
1119 $changed = true;
1120 $results['widgets_updated']++;
1121 }
1122 }
1123 if (!empty($block['innerBlocks'])) {
1124 $inner = self::delete_logic_from_blocks($block['innerBlocks'], $code, $results);
1125 if ($inner !== false) {
1126 $block['innerBlocks'] = $inner;
1127 $changed = true;
1128 }
1129 }
1130 }
1131 return $changed ? $blocks : false;
1132 }
1133
1134 /**
1135 * Delete logic field from Elementor elements recursively
1136 */
1137 private static function delete_logic_from_elementor(&$elements, $code, &$results) {
1138 $changed = false;
1139 foreach ($elements as &$element) {
1140 if (isset($element['settings']['widgetopts_logic']) && trim($element['settings']['widgetopts_logic']) === $code) {
1141 unset($element['settings']['widgetopts_logic']);
1142 $changed = true;
1143 $results['widgets_updated']++;
1144 }
1145 if (!empty($element['elements'])) {
1146 $inner = self::delete_logic_from_elementor($element['elements'], $code, $results);
1147 if ($inner !== false) {
1148 $element['elements'] = $inner;
1149 $changed = true;
1150 }
1151 }
1152 }
1153 return $changed ? $elements : false;
1154 }
1155
1156 /**
1157 * Re-scan all editors and update the migration required flag.
1158 * If no legacy logic found, flag is removed.
1159 */
1160 public static function rescan_and_update_flag() {
1161 // Delete current flag first
1162 delete_option('wopts_display_logic_migration_required');
1163 // Re-scan (will set the flag if legacy logic still exists)
1164 WidgetOpts_Snippets_CPT::scan_for_legacy_logic();
1165 }
1166 }
1167