class-field-manager.php (6388B)
1 <?php 2 /** 3 * Field Manager 4 * 5 * Manages the price category meta fields on post types. 6 */ 7 8 if (!defined('ABSPATH')) { 9 exit; 10 } 11 12 class Taler_Field_Manager { 13 14 /** 15 * Add price category fields to specified post types 16 * 17 * @param array $post_types Array of post type names 18 */ 19 public static function add_fields_to_post_types($post_types) { 20 foreach ($post_types as $post_type) { 21 // Verify post type exists 22 if (!post_type_exists($post_type)) { 23 continue; 24 } 25 26 self::add_price_category_field($post_type); 27 } 28 } 29 30 /** 31 * Add price category field to a specific post type 32 * 33 * @param string $post_type The post type name 34 */ 35 protected static function add_price_category_field($post_type) { 36 // Register the meta box for this post type 37 add_action('add_meta_boxes', function() use ($post_type) { 38 add_meta_box( 39 'taler_turnstile_price_category', 40 __('Taler Turnstile Price Category', 'taler-turnstile'), 41 array('Taler_Field_Manager', 'render_price_category_meta_box'), 42 $post_type, 43 'side', 44 'default' 45 ); 46 }); 47 48 // Register the meta field 49 register_post_meta($post_type, '_taler_price_category', array( 50 'type' => 'string', 51 'description' => __('Selected price category for this content', 'taler-turnstile'), 52 'single' => true, 53 'show_in_rest' => true, 54 'sanitize_callback' => 'sanitize_key', 55 'auth_callback' => function() { 56 return current_user_can('edit_posts'); 57 } 58 )); 59 60 // Save the meta field 61 add_action('save_post_' . $post_type, array('Taler_Field_Manager', 'save_price_category_meta'), 10, 2); 62 } 63 64 /** 65 * Render the price category meta box 66 * 67 * @param WP_Post $post The post object 68 */ 69 public static function render_price_category_meta_box($post) { 70 wp_nonce_field('taler_price_category_meta', 'taler_price_category_nonce'); 71 72 $selected_category = get_post_meta($post->ID, '_taler_price_category', true); 73 $categories = Taler_Price_Category::get_all(); 74 ?> 75 <p> 76 <label for="taler_price_category"> 77 <?php esc_html_e('Price Category:', 'taler-turnstile'); ?> 78 </label> 79 <select name="taler_price_category" id="taler_price_category" style="width: 100%;"> 80 <option value=""><?php esc_html_e('-- None --', 'taler-turnstile'); ?></option> 81 <?php foreach ($categories as $id => $category): ?> 82 <option value="<?php echo esc_attr($id); ?>" <?php selected($selected_category, $id); ?>> 83 <?php echo esc_html($category['label']); ?> 84 </option> 85 <?php endforeach; ?> 86 </select> 87 </p> 88 <p class="description"> 89 <?php esc_html_e('Select a price category for this content. Leave empty for free access.', 'taler-turnstile'); ?> 90 </p> 91 <?php 92 } 93 94 /** 95 * Save the price category meta field 96 * 97 * @param int $post_id The post ID 98 * @param WP_Post $post The post object 99 */ 100 public static function save_price_category_meta($post_id, $post) { 101 // Check nonce 102 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 103 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash 104 // FIXME: reviewers say sanitization is needed here for wp_verify_nonce() 105 // But https://wordpress.stackexchange.com/questions/256513/should-nonce-be-sanitized 106 // says this is not needed and points to 107 // https://github.com/WordPress/wordpress-develop/blob/6.2/src/wp-includes/pluggable.php#L1271 108 // for an example where the wordpress core also does not do it. 109 // I truly do not see why the reviewers believe sanitization is useful here. 110 if (!isset($_POST['taler_price_category_nonce']) || 111 !wp_verify_nonce($_POST['taler_price_category_nonce'], 'taler_price_category_meta')) { 112 return; 113 } 114 115 // Check autosave 116 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { 117 return; 118 } 119 120 // Check permissions 121 if (!current_user_can('edit_post', $post_id)) { 122 return; 123 } 124 125 // Save or delete the meta field 126 if (isset($_POST['taler_price_category'])) { 127 $category = sanitize_key($_POST['taler_price_category']); 128 129 if (empty($category)) { 130 delete_post_meta($post_id, '_taler_price_category'); 131 } else { 132 update_post_meta($post_id, '_taler_price_category', $category); 133 } 134 } 135 } 136 137 /** 138 * Remove price category fields from specified post types 139 * 140 * @param array $post_types Array of post type names 141 */ 142 public static function remove_fields_from_post_types($post_types) { 143 foreach ($post_types as $post_type) { 144 self::remove_price_category_field($post_type); 145 } 146 } 147 148 /** 149 * Remove price category field from a specific post type 150 * 151 * @param string $post_type The post type name 152 */ 153 protected static function remove_price_category_field($post_type) { 154 // Unregister the meta field 155 unregister_post_meta($post_type, '_taler_price_category'); 156 157 // Note: Meta boxes are removed automatically when not registered 158 // We don't need to explicitly remove them since they're added via hooks 159 } 160 161 /** 162 * Cleanup all price category meta when no post types are using it 163 */ 164 public static function cleanup_field_storage() { 165 global $wpdb; 166 167 // Delete all price category meta from all posts 168 $wpdb->delete( 169 $wpdb->postmeta, 170 array('meta_key' => '_taler_price_category'), 171 array('%s') 172 ); 173 } 174 175 /** 176 * Initialize field manager hooks 177 */ 178 public static function init() { 179 $enabled_types = get_option('taler_turnstile_enabled_post_types', array('post')); 180 181 if (!empty($enabled_types)) { 182 self::add_fields_to_post_types($enabled_types); 183 } 184 } 185 }