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