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-admin.php
widget-options / includes / snippets Last commit date
class-snippets-admin.php 3 months ago class-snippets-api.php 3 months ago class-snippets-cpt.php 3 months ago class-snippets-migration.php 3 months ago
class-snippets-admin.php
741 lines
1 <?php
2 /**
3 * Display Logic Snippets - Admin Interface
4 *
5 * Provides admin pages for managing snippets and handles admin notices
6 * for migration status.
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_Admin
17 *
18 * Handles admin interface for snippets management
19 */
20 class WidgetOpts_Snippets_Admin {
21
22 /**
23 * Hook suffix for the migration page
24 */
25 private static $migration_hook = '';
26
27 /**
28 * Initialize the admin interface
29 */
30 public static function init() {
31 add_action('admin_menu', array(__CLASS__, 'add_admin_menu'), 11);
32 add_action('admin_notices', array(__CLASS__, 'migration_notice'));
33 add_action('admin_notices', array(__CLASS__, 'legacy_migration_required_notice'));
34 add_action('admin_notices', array(__CLASS__, 'snippet_validation_notice'));
35 add_action('add_meta_boxes', array(__CLASS__, 'add_meta_boxes'));
36 add_action('save_post_' . WidgetOpts_Snippets_CPT::POST_TYPE, array(__CLASS__, 'save_snippet_meta'), 10, 2);
37 add_filter('manage_' . WidgetOpts_Snippets_CPT::POST_TYPE . '_posts_columns', array(__CLASS__, 'add_columns'));
38 add_action('manage_' . WidgetOpts_Snippets_CPT::POST_TYPE . '_posts_custom_column', array(__CLASS__, 'column_content'), 10, 2);
39
40 // AJAX for manual migration
41 add_action('wp_ajax_widgetopts_run_migration', array(__CLASS__, 'ajax_run_migration'));
42 add_action('wp_ajax_widgetopts_dismiss_migration_notice', array(__CLASS__, 'ajax_dismiss_migration_notice'));
43
44 // AJAX for migration page
45 add_action('wp_ajax_widgetopts_migration_scan', array(__CLASS__, 'ajax_migration_scan'));
46 add_action('wp_ajax_widgetopts_migration_migrate', array(__CLASS__, 'ajax_migration_migrate'));
47 add_action('wp_ajax_widgetopts_migration_delete', array(__CLASS__, 'ajax_migration_delete'));
48
49 // Enqueue migration page assets
50 add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_migration_assets'));
51
52 // Redirect to migration page after plugin update if legacy code exists
53 add_action('admin_init', array(__CLASS__, 'maybe_redirect_to_migration'));
54 }
55
56 /**
57 * Add admin menu - only if Display Logic is enabled
58 */
59 public static function add_admin_menu() {
60 global $widget_options;
61
62 // Only show menu if Display Logic is enabled
63 if (!isset($widget_options['logic']) || $widget_options['logic'] !== 'activate') {
64 return;
65 }
66
67 // Add as submenu right after Widget Options (priority 11, Widget Options uses 10)
68 add_submenu_page(
69 'options-general.php',
70 __('Widget Options Snippets', 'widget-options'),
71 __('Widget Options Snippets', 'widget-options'),
72 'manage_options',
73 'edit.php?post_type=' . WidgetOpts_Snippets_CPT::POST_TYPE
74 );
75
76 // Migration page (hidden from menu, accessible via direct link)
77 self::$migration_hook = add_submenu_page(
78 null,
79 __('Display Logic Migration', 'widget-options'),
80 __('Display Logic Migration', 'widget-options'),
81 WIDGETOPTS_MIGRATION_PERMISSIONS,
82 'widgetopts_migration',
83 array(__CLASS__, 'render_migration_page')
84 );
85 }
86
87 /**
88 * Render the migration page
89 */
90 public static function render_migration_page() {
91 if (!current_user_can(WIDGETOPTS_MIGRATION_PERMISSIONS)) {
92 wp_die(__('You do not have sufficient permissions to access this page.', 'widget-options'));
93 }
94 require_once WIDGETOPTS_PLUGIN_DIR . 'includes/admin/settings/migration-page.php';
95 }
96
97 /**
98 * Enqueue migration page scripts/styles
99 */
100 public static function enqueue_migration_assets($hook) {
101 if (!self::$migration_hook || $hook !== self::$migration_hook) {
102 return;
103 }
104 wp_enqueue_script(
105 'widgetopts-migration',
106 WIDGETOPTS_PLUGIN_URL . 'assets/js/widgetopts.migration.js',
107 array('jquery'),
108 WIDGETOPTS_VERSION,
109 true
110 );
111 wp_localize_script('widgetopts-migration', 'widgetoptsMigration', array(
112 'ajaxurl' => admin_url('admin-ajax.php'),
113 'nonce' => wp_create_nonce('widgetopts_migration_page'),
114 'i18n' => array(
115 'scanning' => __('Scanning...', 'widget-options'),
116 'migrating' => __('Migrating...', 'widget-options'),
117 'deleting' => __('Deleting...', 'widget-options'),
118 'noItems' => __('No legacy display logic found. All widgets are using the new snippet-based system.', 'widget-options'),
119 'confirmDelete' => __('Are you sure you want to delete this legacy code? Display logic will no longer execute for the affected widgets.', 'widget-options'),
120 'confirmMigrate' => __('Migrate the selected snippets?', 'widget-options'),
121 'selectAtLeast' => __('Please select at least one snippet to migrate.', 'widget-options'),
122 'migrationDone' => __('Migration completed!', 'widget-options'),
123 'error' => __('An error occurred. Please try again.', 'widget-options'),
124 ),
125 ));
126 }
127
128 /**
129 * AJAX: Scan all editors for legacy logic codes with locations
130 */
131 public static function ajax_migration_scan() {
132 check_ajax_referer('widgetopts_migration_page', 'nonce');
133 if (!current_user_can(WIDGETOPTS_MIGRATION_PERMISSIONS)) {
134 wp_send_json_error(array('message' => __('Permission denied.', 'widget-options')));
135 }
136
137 require_once WIDGETOPTS_PLUGIN_DIR . 'includes/snippets/class-snippets-migration.php';
138 $items = WidgetOpts_Snippets_Migration::collect_all_logic_codes_with_locations();
139
140 // Add hash and suggested title
141 $index = 1;
142 foreach ($items as &$item) {
143 $item['hash'] = md5($item['code']);
144 $item['title'] = sprintf(__('Migrated Snippet #%d', 'widget-options'), $index);
145 $index++;
146 }
147
148 wp_send_json_success(array('items' => $items));
149 }
150
151 /**
152 * AJAX: Migrate selected snippets
153 */
154 public static function ajax_migration_migrate() {
155 check_ajax_referer('widgetopts_migration_page', 'nonce');
156 if (!current_user_can(WIDGETOPTS_MIGRATION_PERMISSIONS)) {
157 wp_send_json_error(array('message' => __('Permission denied.', 'widget-options')));
158 }
159
160 $raw = isset($_POST['items']) ? $_POST['items'] : array();
161 if (empty($raw) || !is_array($raw)) {
162 wp_send_json_error(array('message' => __('No items provided.', 'widget-options')));
163 }
164
165 // Sanitize
166 $items = array();
167 foreach ($raw as $entry) {
168 $items[] = array(
169 'hash' => sanitize_text_field($entry['hash']),
170 'title' => sanitize_text_field($entry['title']),
171 );
172 }
173
174 require_once WIDGETOPTS_PLUGIN_DIR . 'includes/snippets/class-snippets-migration.php';
175 $results = WidgetOpts_Snippets_Migration::migrate_selected($items);
176
177 wp_send_json_success(array('results' => $results));
178 }
179
180 /**
181 * AJAX: Delete a legacy code entry by hash
182 */
183 public static function ajax_migration_delete() {
184 check_ajax_referer('widgetopts_migration_page', 'nonce');
185 if (!current_user_can(WIDGETOPTS_MIGRATION_PERMISSIONS)) {
186 wp_send_json_error(array('message' => __('Permission denied.', 'widget-options')));
187 }
188
189 $hash = isset($_POST['hash']) ? sanitize_text_field($_POST['hash']) : '';
190 if (empty($hash)) {
191 wp_send_json_error(array('message' => __('No hash provided.', 'widget-options')));
192 }
193
194 require_once WIDGETOPTS_PLUGIN_DIR . 'includes/snippets/class-snippets-migration.php';
195 $results = WidgetOpts_Snippets_Migration::delete_legacy_code($hash);
196
197 wp_send_json_success(array('results' => $results));
198 }
199
200 /**
201 * Display notice when legacy display logic migration is required
202 */
203 public static function legacy_migration_required_notice() {
204 $screen = get_current_screen();
205 if ($screen && $screen->id === 'settings_page_widgetopts_migration') {
206 return;
207 }
208
209 if (!get_option('wopts_display_logic_migration_required', false)) {
210 return;
211 }
212
213 if (!current_user_can(WIDGETOPTS_MIGRATION_PERMISSIONS)) {
214 return;
215 }
216
217 $migration_url = admin_url('options-general.php?page=widgetopts_migration');
218 ?>
219 <div class="notice notice-warning">
220 <p>
221 <strong><?php _e('Widget Options - Display Logic Migration Required', 'widget-options'); ?></strong>
222 </p>
223 <p>
224 <?php _e('Some widgets still use legacy inline display logic code that needs to be migrated to the new snippet-based system.', 'widget-options'); ?>
225 </p>
226 <p>
227 <a href="<?php echo esc_url($migration_url); ?>" class="button button-primary">
228 <?php _e('Go to Migration Page', 'widget-options'); ?>
229 </a>
230 </p>
231 </div>
232 <?php
233 }
234
235 /**
236 * Redirect to migration page on first admin load after plugin update,
237 * if legacy display logic code exists and needs migration.
238 */
239 public static function maybe_redirect_to_migration() {
240 if (!defined('WIDGETOPTS_VERSION')) return;
241
242 $stored_version = get_option('widgetopts_plugin_version', '');
243 if ($stored_version === WIDGETOPTS_VERSION) return;
244
245 // Version changed — update stored version
246 update_option('widgetopts_plugin_version', WIDGETOPTS_VERSION);
247
248 // Only redirect on UPDATE (not first install)
249 if ($stored_version === '') return;
250
251 // Trigger a scan to set the migration flag if needed
252 if (class_exists('WidgetOpts_Snippets_CPT')) {
253 WidgetOpts_Snippets_CPT::scan_for_legacy_logic();
254 }
255
256 if (!get_option('wopts_display_logic_migration_required', false)) return;
257 if (!current_user_can(WIDGETOPTS_MIGRATION_PERMISSIONS)) return;
258
259 // Don't redirect during AJAX, CLI, or if already on migration page
260 if (wp_doing_ajax() || (defined('WP_CLI') && WP_CLI)) return;
261 if (isset($_GET['page']) && $_GET['page'] === 'widgetopts_migration') return;
262
263 // Set transient and redirect
264 set_transient('widgetopts_migration_redirect', true, 60);
265 wp_safe_redirect(admin_url('options-general.php?page=widgetopts_migration'));
266 exit;
267 }
268
269 /**
270 * Display migration notice if needed
271 */
272 public static function migration_notice() {
273 $migration_status = get_option(WidgetOpts_Snippets_Migration::MIGRATION_OPTION, array());
274
275 // Show notice for failed migration
276 if (isset($migration_status['status']) && $migration_status['status'] === 'failed') {
277 $dismissed = get_option('widgetopts_migration_notice_dismissed', false);
278 if ($dismissed) {
279 return;
280 }
281 ?>
282 <div class="notice notice-error is-dismissible" id="widgetopts-migration-failed-notice">
283 <p>
284 <strong><?php _e('Widget Options - Migration Failed', 'widget-options'); ?></strong>
285 </p>
286 <p>
287 <?php _e('The automatic migration of display logic to snippets has failed. Please try running the migration manually.', 'widget-options'); ?>
288 </p>
289 <?php if (isset($migration_status['error'])): ?>
290 <p><code><?php echo esc_html($migration_status['error']); ?></code></p>
291 <?php endif; ?>
292 <p>
293 <button type="button" class="button button-primary" id="widgetopts-run-migration">
294 <?php _e('Run Migration Manually', 'widget-options'); ?>
295 </button>
296 <button type="button" class="button" id="widgetopts-dismiss-migration">
297 <?php _e('Dismiss', 'widget-options'); ?>
298 </button>
299 </p>
300 </div>
301 <script>
302 jQuery(document).ready(function($) {
303 $('#widgetopts-run-migration').on('click', function() {
304 var $btn = $(this);
305 $btn.prop('disabled', true).text('<?php _e('Running...', 'widget-options'); ?>');
306
307 $.post(ajaxurl, {
308 action: 'widgetopts_run_migration',
309 nonce: '<?php echo wp_create_nonce('widgetopts_migration'); ?>'
310 }, function(response) {
311 if (response.success) {
312 location.reload();
313 } else {
314 alert(response.data.message || '<?php _e('Migration failed. Please try again.', 'widget-options'); ?>');
315 $btn.prop('disabled', false).text('<?php _e('Run Migration Manually', 'widget-options'); ?>');
316 }
317 });
318 });
319
320 $('#widgetopts-dismiss-migration').on('click', function() {
321 $.post(ajaxurl, {
322 action: 'widgetopts_dismiss_migration_notice',
323 nonce: '<?php echo wp_create_nonce('widgetopts_migration'); ?>'
324 });
325 $('#widgetopts-migration-failed-notice').fadeOut();
326 });
327 });
328 </script>
329 <?php
330 }
331
332 // Show success notice after migration
333 if (isset($migration_status['status']) && $migration_status['status'] === 'completed' && isset($migration_status['results'])) {
334 $show_success = get_transient('widgetopts_migration_success_notice');
335 if ($show_success) {
336 delete_transient('widgetopts_migration_success_notice');
337 $results = $migration_status['results'];
338 ?>
339 <div class="notice notice-success is-dismissible">
340 <p>
341 <strong><?php _e('Widget Options - Migration Completed', 'widget-options'); ?></strong>
342 </p>
343 <p>
344 <?php
345 printf(
346 __('Successfully migrated display logic to snippets. Created %d snippets and updated %d widgets.', 'widget-options'),
347 $results['snippets_created'],
348 $results['widgets_updated']
349 );
350 ?>
351 </p>
352 <?php if (!empty($results['errors'])): ?>
353 <p><?php _e('Some errors occurred:', 'widget-options'); ?></p>
354 <ul>
355 <?php foreach ($results['errors'] as $error): ?>
356 <li><?php echo esc_html($error); ?></li>
357 <?php endforeach; ?>
358 </ul>
359 <?php endif; ?>
360 </div>
361 <?php
362 }
363 }
364 }
365
366 /**
367 * AJAX handler for manual migration
368 */
369 public static function ajax_run_migration() {
370 check_ajax_referer('widgetopts_migration', 'nonce');
371
372 if (!current_user_can('manage_options')) {
373 wp_send_json_error(array('message' => __('Permission denied.', 'widget-options')));
374 }
375
376 // Reset migration status to allow re-run
377 WidgetOpts_Snippets_Migration::reset_migration();
378
379 // Run migration
380 $result = WidgetOpts_Snippets_Migration::migrate();
381
382 if ($result) {
383 set_transient('widgetopts_migration_success_notice', true, 60);
384 wp_send_json_success();
385 } else {
386 $status = WidgetOpts_Snippets_Migration::get_migration_status();
387 wp_send_json_error(array(
388 'message' => isset($status['error']) ? $status['error'] : __('Unknown error occurred.', 'widget-options')
389 ));
390 }
391 }
392
393 /**
394 * AJAX handler to dismiss migration notice
395 */
396 public static function ajax_dismiss_migration_notice() {
397 check_ajax_referer('widgetopts_migration', 'nonce');
398
399 if (!current_user_can('manage_options')) {
400 wp_send_json_error();
401 }
402
403 update_option('widgetopts_migration_notice_dismissed', true);
404 wp_send_json_success();
405 }
406
407 /**
408 * Add meta boxes for snippet editing
409 */
410 public static function add_meta_boxes() {
411 add_meta_box(
412 'widgetopts_snippet_code',
413 __('Snippet Code', 'widget-options'),
414 array(__CLASS__, 'render_code_meta_box'),
415 WidgetOpts_Snippets_CPT::POST_TYPE,
416 'normal',
417 'high'
418 );
419
420 add_meta_box(
421 'widgetopts_snippet_description',
422 __('Description', 'widget-options'),
423 array(__CLASS__, 'render_description_meta_box'),
424 WidgetOpts_Snippets_CPT::POST_TYPE,
425 'normal',
426 'default'
427 );
428
429 add_meta_box(
430 'widgetopts_snippet_help',
431 __('Help & Examples', 'widget-options'),
432 array(__CLASS__, 'render_help_meta_box'),
433 WidgetOpts_Snippets_CPT::POST_TYPE,
434 'side',
435 'default'
436 );
437
438 // Remove default editor
439 remove_post_type_support(WidgetOpts_Snippets_CPT::POST_TYPE, 'editor');
440 }
441
442 /**
443 * Render code meta box
444 */
445 public static function render_code_meta_box($post) {
446 wp_nonce_field('widgetopts_snippet_save', 'widgetopts_snippet_nonce');
447
448 $code = $post->post_content;
449 ?>
450
451 <div style="position: relative;">
452 <textarea name="widgetopts_snippet_code" id="widgetopts_snippet_code" class="large-text code" rows="20" style="font-family: 'Courier New', Consolas, Monaco, monospace; font-size: 14px; line-height: 1.6; padding: 15px; background: #f9f9f9; border: 1px solid #dcdcde; border-radius: 4px; color: #1d2327; tab-size: 4;"><?php echo esc_textarea($code); ?></textarea>
453 </div>
454 <div style="background: #f0f0f1; padding: 15px; border-radius: 4px; margin-bottom: 15px;">
455 <p class="description" style="margin: 0; color: #1d2327;">
456 <strong style="color: #1d2327;"><?php _e('Instructions:', 'widget-options'); ?></strong><br>
457 <?php _e('Enter PHP code that returns true (show widget) or false (hide widget). You can use WordPress Conditional Tags.', 'widget-options'); ?><br>
458 <?php _e('Do NOT use PHP tags', 'widget-options'); ?> <code style="background: #fff; padding: 2px 6px; border-radius: 3px; color: #d63638;">&lt;?php ?&gt;</code> <?php _e('just write the code directly.', 'widget-options'); ?><br>
459 <?php _e('Example:', 'widget-options'); ?> <code style="background: #f0f0f1; padding: 2px 6px; border-radius: 3px;">return is_user_logged_in();</code>
460 </p>
461 <p class="description" style="margin: 10px 0 0; padding: 10px; background: #fbfcf0; border-left: 4px solid #af991d; border-radius: 2px; color: #1d2327;">
462 <strong style="color: #af991d;"><?php _e('Security Warning:', 'widget-options'); ?></strong><br>
463 <?php _e('Code is executed via eval(). Only administrators can create/edit snippets. The code is validated against a whitelist of allowed functions.', 'widget-options'); ?>
464 </p>
465 </div>
466
467 <style>
468 #widgetopts_snippet_code:focus {
469 border-color: #2271b1;
470 box-shadow: 0 0 0 1px #2271b1;
471 outline: 2px solid transparent;
472 }
473 #widgetopts_snippet_code::selection {
474 background: #b4d7ff;
475 }
476 </style>
477 <?php
478 }
479
480 /**
481 * Render description meta box
482 */
483 public static function render_description_meta_box($post) {
484 $description = get_post_meta($post->ID, '_widgetopts_snippet_description', true);
485 ?>
486 <p class="description">
487 <?php _e('Brief description of what this snippet does. This will be shown to users when selecting snippets.', 'widget-options'); ?>
488 </p>
489 <textarea name="widgetopts_snippet_description" id="widgetopts_snippet_description" class="large-text" rows="3"><?php echo esc_textarea($description); ?></textarea>
490 <?php
491 }
492
493 /**
494 * Render help meta box
495 */
496 public static function render_help_meta_box($post) {
497 ?>
498 <p><strong><?php _e('Common Conditional Tags:', 'widget-options'); ?></strong></p>
499 <ul style="font-size: 12px;">
500 <li><code>is_user_logged_in()</code> - <?php _e('User is logged in', 'widget-options'); ?></li>
501 <li><code>is_front_page()</code> - <?php _e('Front page', 'widget-options'); ?></li>
502 <li><code>is_home()</code> - <?php _e('Blog page', 'widget-options'); ?></li>
503 <li><code>is_single()</code> - <?php _e('Single post', 'widget-options'); ?></li>
504 <li><code>is_page()</code> - <?php _e('Static page', 'widget-options'); ?></li>
505 <li><code>is_archive()</code> - <?php _e('Archive page', 'widget-options'); ?></li>
506 <li><code>is_search()</code> - <?php _e('Search results', 'widget-options'); ?></li>
507 <li><code>is_404()</code> - <?php _e('404 page', 'widget-options'); ?></li>
508 </ul>
509
510 <p><strong><?php _e('Examples:', 'widget-options'); ?></strong></p>
511 <p><code>return is_user_logged_in();</code></p>
512 <p><code>return is_page(array(5, 10));</code></p>
513 <p><code>return is_single() && has_tag('featured');</code></p>
514
515 <p>
516 <a href="https://developer.wordpress.org/themes/basics/conditional-tags/" target="_blank">
517 <?php _e('View all Conditional Tags →', 'widget-options'); ?>
518 </a>
519 </p>
520 <?php
521 }
522
523 /**
524 * Validate snippet code for syntax and security errors
525 *
526 * @param string $code The PHP code to validate
527 * @return array Array with 'valid' (bool) and 'errors' (array of error messages)
528 */
529 public static function validate_snippet_code($code) {
530 $errors = array();
531
532 // Skip validation for empty code
533 if (empty(trim($code))) {
534 return array('valid' => true, 'errors' => array());
535 }
536
537 // 1. Check for PHP syntax errors using php -l
538 $syntax_error = self::check_php_syntax($code);
539 if ($syntax_error) {
540 $errors[] = array(
541 'type' => 'syntax',
542 'message' => $syntax_error
543 );
544 }
545
546 // 2. Security validation using existing system
547 if (function_exists('widgetopts_validate_expression')) {
548 $validation = widgetopts_validate_expression($code);
549 if ($validation['valid'] === false) {
550 $errors[] = array(
551 'type' => 'security',
552 'message' => $validation['message']
553 );
554 }
555 }
556
557 return array(
558 'valid' => empty($errors),
559 'errors' => $errors
560 );
561 }
562
563 /**
564 * Check PHP syntax without executing the code
565 *
566 * Uses token_get_all() to parse the code and catch syntax errors.
567 *
568 * @param string $code The PHP code to check
569 * @return string|false Error message or false if valid
570 */
571 public static function check_php_syntax($code) {
572 // Mirror the same transformation that widgetopts_safe_eval() applies at runtime:
573 // if code has no "return", it gets wrapped as "return (code);"
574 if (stristr($code, "return") === false) {
575 $code = "return (" . $code . ");";
576 }
577
578 // Wrap code in PHP tags for tokenization
579 $wrapped_code = '<?php ' . $code;
580
581 // Use token_get_all to check syntax - it throws ParseError on invalid syntax
582 try {
583 $previous_error_reporting = error_reporting(0);
584 @token_get_all($wrapped_code, TOKEN_PARSE);
585 error_reporting($previous_error_reporting);
586 return false; // No syntax errors
587 } catch (\ParseError $e) {
588 error_reporting($previous_error_reporting);
589 // Line numbers are correct because <?php is added on the same line as the first line of code
590 $line = $e->getLine();
591 $message = $e->getMessage();
592
593 // Make error message more user-friendly
594 return sprintf(
595 __('Syntax error on line %d: %s', 'widget-options'),
596 $line,
597 $message
598 );
599 } catch (\Throwable $e) {
600 error_reporting($previous_error_reporting);
601 return sprintf(
602 __('Code error: %s', 'widget-options'),
603 $e->getMessage()
604 );
605 }
606 }
607
608 /**
609 * Save snippet meta
610 */
611 public static function save_snippet_meta($post_id, $post) {
612 // Verify nonce
613 if (!isset($_POST['widgetopts_snippet_nonce']) || !wp_verify_nonce($_POST['widgetopts_snippet_nonce'], 'widgetopts_snippet_save')) {
614 return;
615 }
616
617 // Check permissions
618 if (!current_user_can('manage_options')) {
619 return;
620 }
621
622 // Check autosave
623 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
624 return;
625 }
626
627 // Save code to post_content
628 if (isset($_POST['widgetopts_snippet_code'])) {
629 $code = wp_unslash($_POST['widgetopts_snippet_code']);
630
631 // Validate code
632 $validation = self::validate_snippet_code($code);
633
634 $update_data = array(
635 'ID' => $post_id,
636 'post_content' => $code,
637 );
638
639 // If validation failed, set status to draft
640 if (!$validation['valid']) {
641 $update_data['post_status'] = 'draft';
642
643 // Store validation errors for display
644 set_transient('widgetopts_snippet_errors_' . $post_id, $validation['errors'], 120);
645 } else {
646 // Clear any previous errors
647 delete_transient('widgetopts_snippet_errors_' . $post_id);
648 }
649
650 // Update post content directly
651 remove_action('save_post_' . WidgetOpts_Snippets_CPT::POST_TYPE, array(__CLASS__, 'save_snippet_meta'), 10);
652 wp_update_post($update_data);
653 add_action('save_post_' . WidgetOpts_Snippets_CPT::POST_TYPE, array(__CLASS__, 'save_snippet_meta'), 10, 2);
654 }
655
656 // Save description
657 if (isset($_POST['widgetopts_snippet_description'])) {
658 update_post_meta($post_id, '_widgetopts_snippet_description', sanitize_textarea_field($_POST['widgetopts_snippet_description']));
659 }
660 }
661
662 /**
663 * Display validation error notice on snippet edit page
664 */
665 public static function snippet_validation_notice() {
666 global $post;
667
668 if (!$post || $post->post_type !== WidgetOpts_Snippets_CPT::POST_TYPE) {
669 return;
670 }
671
672 $errors = get_transient('widgetopts_snippet_errors_' . $post->ID);
673
674 if (empty($errors)) {
675 return;
676 }
677
678 // Don't delete transient here - keep showing until code is fixed
679 ?>
680 <div class="notice notice-error">
681 <p><strong><?php _e('Snippet Validation Failed', 'widget-options'); ?></strong></p>
682 <p><?php _e('The snippet contains errors and has been saved as Draft. Please fix the following issues:', 'widget-options'); ?></p>
683 <ul style="list-style: disc; margin-left: 20px;">
684 <?php foreach ($errors as $error): ?>
685 <li>
686 <strong>
687 <?php
688 if ($error['type'] === 'syntax') {
689 _e('Syntax Error:', 'widget-options');
690 } else {
691 _e('Security Error:', 'widget-options');
692 }
693 ?>
694 </strong>
695 <code style="display: block; margin-top: 5px; padding: 10px; background: #f6f6f6; white-space: pre-wrap; word-break: break-all;"><?php echo esc_html($error['message']); ?></code>
696 </li>
697 <?php endforeach; ?>
698 </ul>
699 <p><?php _e('Fix the errors above and save again to publish the snippet.', 'widget-options'); ?></p>
700 </div>
701 <?php
702 }
703
704 /**
705 * Add custom columns to snippets list
706 */
707 public static function add_columns($columns) {
708 $new_columns = array();
709 foreach ($columns as $key => $value) {
710 $new_columns[$key] = $value;
711 if ($key === 'title') {
712 $new_columns['description'] = __('Description', 'widget-options');
713 $new_columns['code_preview'] = __('Code Preview', 'widget-options');
714 }
715 }
716 return $new_columns;
717 }
718
719 /**
720 * Render custom column content
721 */
722 public static function column_content($column, $post_id) {
723 switch ($column) {
724 case 'description':
725 $description = get_post_meta($post_id, '_widgetopts_snippet_description', true);
726 echo esc_html($description ?: '');
727 break;
728
729 case 'code_preview':
730 $post = get_post($post_id);
731 $code = $post->post_content;
732 $preview = strlen($code) > 80 ? substr($code, 0, 80) . '...' : $code;
733 echo '<code style="font-size: 11px;">' . esc_html($preview) . '</code>';
734 break;
735 }
736 }
737 }
738
739 // Initialize
740 WidgetOpts_Snippets_Admin::init();
741