wordpress-turnstile

Wordpress paywall plugin
Log | Files | Refs | README | LICENSE

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 }