class-price-category-admin.php (13612B)
1 <?php 2 /** 3 * Price Category Admin 4 * 5 * Handles the price category management interface. 6 */ 7 8 if (!defined('ABSPATH')) { 9 exit; 10 } 11 12 class Taler_Turnstile_Price_Category_Admin { 13 14 public function __construct() { 15 add_action('admin_post_taler_save_price_category', array($this, 'save_price_category')); 16 add_action('admin_post_taler_delete_price_category', array($this, 'delete_price_category')); 17 } 18 19 public static function render_list_page() { 20 if (!current_user_can('manage_options')) { 21 return; 22 } 23 24 $categories = Taler_Price_Category::get_all(); 25 ?> 26 <div class="wrap"> 27 <h1> 28 <?php esc_html_e('Price Categories', 'taler-turnstile'); ?> 29 <a href="<?php echo esc_url(admin_url('admin.php?page=taler-price-category-add')); ?>" class="page-title-action"> 30 <?php esc_html_e('Add New', 'taler-turnstile'); ?> 31 </a> 32 </h1> 33 34 <?php if (empty($categories)): ?> 35 <p><?php esc_html_e('No price categories found. Create your first price category to get started.', 'taler-turnstile'); ?></p> 36 <?php else: ?> 37 <table class="wp-list-table widefat fixed striped"> 38 <thead> 39 <tr> 40 <th><?php esc_html_e('Name', 'taler-turnstile'); ?></th> 41 <th><?php esc_html_e('Description', 'taler-turnstile'); ?></th> 42 <th><?php esc_html_e('Actions', 'taler-turnstile'); ?></th> 43 </tr> 44 </thead> 45 <tbody> 46 <?php foreach ($categories as $id => $category): ?> 47 <tr> 48 <td><strong><?php echo esc_html($category['label']); ?></strong></td> 49 <td><?php echo esc_html($category['description'] ?? ''); ?></td> 50 <td> 51 <a href="<?php echo esc_url(admin_url('admin.php?page=taler-price-category-add&edit=' . urlencode($id))); ?>"> 52 <?php esc_html_e('Edit', 'taler-turnstile'); ?> 53 </a> 54 | 55 <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=taler_delete_price_category&id=' . urlencode($id)), 'delete_price_category_' . $id)); ?>" 56 onclick="return confirm('<?php esc_attr_e('Are you sure you want to delete this price category?', 'taler-turnstile'); ?>');"> 57 <?php esc_html_e('Delete', 'taler-turnstile'); ?> 58 </a> 59 </td> 60 </tr> 61 <?php endforeach; ?> 62 </tbody> 63 </table> 64 <?php endif; ?> 65 </div> 66 <?php 67 } 68 69 public static function render_edit_page() { 70 if (!current_user_can('manage_options')) { 71 return; 72 } 73 74 $edit_id = isset($_GET['edit']) ? sanitize_key($_GET['edit']) : ''; 75 $category = $edit_id ? Taler_Price_Category::get($edit_id) : null; 76 77 $is_new = empty($category); 78 $label = $category['label'] ?? ''; 79 $description = $category['description'] ?? ''; 80 $prices = $category['prices'] ?? array(); 81 82 $subscriptions = Taler_Merchant_API::get_subscriptions(); 83 $currencies = Taler_Merchant_API::get_currencies(); 84 85 if (empty($subscriptions) || empty($currencies)) { 86 echo '<div class="notice notice-warning"><p>'; 87 esc_html_e('Unable to load subscriptions or currencies from API. Please check your configuration.', 'taler-turnstile'); 88 echo '</p></div>'; 89 } 90 ?> 91 <div class="wrap"> 92 <h1><?php echo $is_new ? esc_html__('Add Price Category', 'taler-turnstile') : esc_html__('Edit Price Category', 'taler-turnstile'); ?></h1> 93 94 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 95 <input type="hidden" name="action" value="taler_save_price_category"> 96 <?php wp_nonce_field('taler_save_price_category'); ?> 97 98 <?php if (!$is_new): ?> 99 <input type="hidden" name="id" value="<?php echo esc_attr($edit_id); ?>"> 100 <?php endif; ?> 101 102 <table class="form-table"> 103 <tr> 104 <th scope="row"> 105 <label for="label"><?php esc_html_e('Name', 'taler-turnstile'); ?> <span class="required">*</span></label> 106 </th> 107 <td> 108 <input type="text" name="label" id="label" value="<?php echo esc_attr($label); ?>" class="regular-text" required> 109 <p class="description"><?php esc_html_e('The name of the price category.', 'taler-turnstile'); ?></p> 110 </td> 111 </tr> 112 113 <tr> 114 <th scope="row"> 115 <label for="description"><?php esc_html_e('Description', 'taler-turnstile'); ?></label> 116 </th> 117 <td> 118 <textarea name="description" id="description" rows="3" class="large-text"><?php echo esc_textarea($description); ?></textarea> 119 <p class="description"><?php esc_html_e('A description of this price category.', 'taler-turnstile'); ?></p> 120 </td> 121 </tr> 122 </table> 123 124 <h2><?php esc_html_e('Prices', 'taler-turnstile'); ?></h2> 125 126 <?php foreach ($subscriptions as $subscription_id => $subscription): ?> 127 <div class="postbox"> 128 <div class="postbox-header"> 129 <h2 class="hndle"><?php echo esc_html($subscription['label']); ?></h2> 130 </div> 131 <div class="inside"> 132 <table class="form-table"> 133 <?php foreach ($currencies as $currency): ?> 134 <?php 135 $currency_code = $currency['code']; 136 $field_name = 'prices[' . esc_attr($subscription_id) . '][' . esc_attr($currency_code) . ']'; 137 $field_value = $prices[$subscription_id][$currency_code] ?? ''; 138 ?> 139 <tr> 140 <th scope="row"> 141 <label for="price_<?php echo esc_attr($subscription_id . '_' . $currency_code); ?>"> 142 <?php echo esc_html($currency['label']); ?> 143 </label> 144 </th> 145 <td> 146 <input type="number" 147 name="<?php echo esc_attr($field_name); ?>" 148 id="price_<?php echo esc_attr($subscription_id . '_' . $currency_code); ?>" 149 value="<?php echo esc_attr($field_value); ?>" 150 min="0" 151 step="<?php echo esc_attr($currency['step']); ?>" 152 class="small-text"> 153 <p class="description"><?php esc_html_e('Leave empty for no price.', 'taler-turnstile'); ?></p> 154 </td> 155 </tr> 156 <?php endforeach; ?> 157 </table> 158 </div> 159 </div> 160 <?php endforeach; ?> 161 162 <p class="submit"> 163 <input type="submit" name="submit" id="submit" class="button button-primary" value="<?php echo $is_new ? esc_attr__('Create Price Category', 'taler-turnstile') : esc_attr__('Update Price Category', 'taler-turnstile'); ?>"> 164 <a href="<?php echo esc_url(admin_url('admin.php?page=taler-price-categories')); ?>" class="button"> 165 <?php esc_html_e('Cancel', 'taler-turnstile'); ?> 166 </a> 167 </p> 168 </form> 169 </div> 170 <?php 171 } 172 173 public function save_price_category() { 174 if (!current_user_can('manage_options')) { 175 wp_die(esc_html(__('You do not have sufficient permissions to access this page.', 'taler-turnstile'))); 176 } 177 178 check_admin_referer('taler_save_price_category'); 179 180 $subscriptions = Taler_Merchant_API::get_subscriptions(); 181 182 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash 183 $label = isset($_POST['label']) ? sanitize_text_field($_POST['label']) : ''; 184 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash 185 $description = isset($_POST['description']) ? sanitize_textarea_field($_POST['description']) : ''; 186 187 // Determine if this is an edit or new category 188 $id = isset($_POST['id']) ? sanitize_key($_POST['id']) : ''; 189 190 if (empty($id)) { 191 // Generate a new unique ID 192 $id = $this->generate_unique_id(); 193 } 194 195 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash 196 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 197 // Note: reviewers suggested sanitization is needed here; 198 // alas, the sanitization happens just below in the loops. 199 $prices = isset($_POST['prices']) ? $_POST['prices'] : array(); 200 201 // Validate and filter prices 202 $filtered_prices = array(); 203 if (is_array($prices)) { 204 foreach ($prices as $subscription_id => $currencies) { 205 $sub_active = FALSE; 206 if (! check_valid_subscription_id ($subscription_id)) 207 continue; // ignore 208 if (is_array($currencies)) { 209 foreach ($currencies as $currency_code => $price) { 210 if (! check_valid_currency_code ($currency_code)) 211 continue; // ignore 212 if ($price !== '' && is_numeric($price) && $price >= 0) { 213 $filtered_prices[$subscription_id][$currency_code] = floatval($price); 214 $sub_active = TRUE; 215 } 216 } 217 } 218 } 219 } 220 221 $category_data = array( 222 'label' => $label, 223 'description' => $description, 224 'prices' => $filtered_prices, 225 ); 226 227 Taler_Price_Category::save($id, $category_data); 228 229 $message = isset($_POST['id']) && !empty($_POST['id']) 230 ? __('Price category updated.', 'taler-turnstile') 231 : __('Price category created.', 'taler-turnstile'); 232 233 wp_redirect(add_query_arg( 234 array('page' => 'taler-price-categories', 'message' => urlencode($message)), 235 admin_url('admin.php') 236 )); 237 exit; 238 } 239 240 public function delete_price_category() { 241 if (!current_user_can('manage_options')) { 242 wp_die(__('You do not have sufficient permissions to access this page.', 'taler-turnstile')); 243 } 244 245 $id = isset($_GET['id']) ? sanitize_key($_GET['id']) : ''; 246 247 check_admin_referer('delete_price_category_' . $id); 248 249 if (Taler_Price_Category::delete($id)) { 250 $message = __('Price category deleted.', 'taler-turnstile'); 251 } else { 252 $message = __('Failed to delete price category.', 'taler-turnstile'); 253 } 254 255 wp_redirect(add_query_arg( 256 array('page' => 'taler-price-categories', 'message' => urlencode($message)), 257 admin_url('admin.php') 258 )); 259 exit; 260 } 261 262 /** 263 * Check if the given string is a valid Taler currency code. 264 * Accepted are [a-z][A-Z], 1 to 11 characters long. 265 * 266 * @return boolean true if $cc is valid. 267 */ 268 private function check_valid_currency_code ($cc) { 269 return preg_match('/^[a-zA-Z]{1,11}$/', $cc) === 1; 270 } 271 272 /** 273 * Check if the given string is a valid $id, consisting 274 * of only unreserved characters as per RFC 3986. 275 * 276 * @return boolean true if $id is valid. 277 */ 278 private function check_valid_subscription_id ($id) { 279 // RFC 3986 unreserved characters are A–Z a–z 0–9 - . _ ~ 280 return preg_match('/^[A-Za-z0-9\-._~]+$/', $id) === 1; 281 } 282 283 /** 284 * Generate a unique ID for a new price category 285 * 286 * @return string Unique ID 287 */ 288 private function generate_unique_id() { 289 $categories = Taler_Price_Category::get_all(); 290 291 // Use auto-incrementing numeric IDs starting from 1 292 $max_id = 0; 293 foreach (array_keys($categories) as $existing_id) { 294 if (is_numeric($existing_id)) { 295 $max_id = max($max_id, intval($existing_id)); 296 } 297 } 298 299 return strval($max_id + 1); 300 } 301 }