__('Event', 'acs-agenda-manager'), 'plural' => __('Events', 'acs-agenda-manager'), 'ajax' => false, ]); } public function get_columns(): array { return [ 'cb' => '', 'categorie' => __('Category', 'acs-agenda-manager'), 'title' => __('Title', 'acs-agenda-manager'), 'emplacement' => __('Location', 'acs-agenda-manager'), 'image' => __('Image', 'acs-agenda-manager'), 'intro' => __('Description', 'acs-agenda-manager'), 'link' => __('Page Link', 'acs-agenda-manager'), 'date' => __('Schedule', 'acs-agenda-manager'), 'price' => __('Price', 'acs-agenda-manager'), 'account' => __('Advance Payment', 'acs-agenda-manager'), 'candopartial' => __('Partial Attendance', 'acs-agenda-manager'), 'redirect' => __('External URL', 'acs-agenda-manager'), 'created_at' => __('Created', 'acs-agenda-manager'), ]; } public function get_sortable_columns(): array { return [ 'categorie' => ['categorie', true], 'title' => ['title', false], 'date' => ['date', false], 'price' => ['price', false], 'created_at' => ['created_at', false], ]; } public function get_bulk_actions(): array { return ['bulk-delete' => __('Delete', 'acs-agenda-manager')]; } public function column_cb($item): string { return sprintf('', $item['id']); } public function column_default($item, $column_name): string { $partial_options = [ __('No', 'acs-agenda-manager'), __('Yes', 'acs-agenda-manager'), __('Keep until end', 'acs-agenda-manager'), ]; $yes_no = [ __('No', 'acs-agenda-manager'), __('Yes', 'acs-agenda-manager'), ]; $name_attr = sprintf('data-name="%s"', esc_attr($column_name)); $item_class = sprintf('origItem_%d', $item['id']); switch ($column_name) { case 'candopartial': $value = isset($partial_options[$item[$column_name]]) ? $partial_options[$item[$column_name]] : ''; return sprintf('%s', esc_attr($item_class), $name_attr, esc_html($value)); case 'account': $value = isset($yes_no[$item[$column_name]]) ? $yes_no[$item[$column_name]] : ''; return sprintf('%s', esc_attr($item_class), $name_attr, esc_html($value)); case 'intro': return sprintf( '%s
', esc_html__('Read more', 'acs-agenda-manager'), esc_html__('Hide', 'acs-agenda-manager'), esc_attr($item_class), $name_attr, esc_html($item[$column_name]) ); case 'image': return sprintf( ' ', esc_attr($item_class), $name_attr, esc_attr($item[$column_name]), esc_url($item[$column_name]) ); case 'link': $page_link = trim((string) ($item[$column_name] ?? '')); $valid_page_link = esc_url_raw($page_link); if ($valid_page_link === '') { return sprintf( '%s%s', esc_attr($item_class), $name_attr, esc_html($page_link), esc_html__('No page link', 'acs-agenda-manager') ); } return sprintf( '%s %s', esc_attr($item_class), $name_attr, esc_html($page_link), esc_url($valid_page_link), esc_html__('Open page', 'acs-agenda-manager') ); default: return sprintf( '%s', esc_attr($item_class), $name_attr, esc_html($item[$column_name] ?? '') ); } } public function column_categorie($item): string { $nonce = wp_create_nonce('acsagma_delete_event'); $item_class = sprintf('origItem_%d', $item['id']); $name = sprintf( '%s', $item['id'], esc_attr($item_class), esc_html($item['categorie']) ); $delete_url = wp_nonce_url( add_query_arg( [ 'page' => 'acsagma-agenda', 'action' => 'delete', 'id' => $item['id'], ], admin_url('admin.php') ), 'acsagma_delete_event' ); $actions = [ 'edit' => sprintf( '%s', $item['id'], esc_html__('Edit', 'acs-agenda-manager') ), 'delete' => sprintf( '%s', esc_url($delete_url), esc_html__('Delete', 'acs-agenda-manager') ), ]; return $name . str_replace('|', '', $this->row_actions($actions)); } public function prepare_items(): void { $this->_column_headers = $this->get_column_info(); $this->process_bulk_action(); $per_page = $this->get_items_per_page('events_per_page', 10); $current_page = $this->get_pagenum(); // List table filtering/sorting/pagination uses WP's built-in list table nonce handling. // phpcs:disable WordPress.Security.NonceVerification.Recommended $args = [ 'per_page' => $per_page, 'page' => $current_page, 'orderby' => sanitize_text_field(wp_unslash($_REQUEST['orderby'] ?? 'id')), 'order' => sanitize_text_field(wp_unslash($_REQUEST['order'] ?? 'DESC')), 'search' => sanitize_text_field(wp_unslash($_REQUEST['s'] ?? '')), 'filter' => sanitize_text_field(wp_unslash($_REQUEST['event-filter'] ?? '')), ]; // phpcs:enable WordPress.Security.NonceVerification.Recommended $this->items = ACSAGMA_Database::get_events($args); $total_items = ACSAGMA_Database::count_events($args); $this->set_pagination_args([ 'total_items' => $total_items, 'per_page' => $per_page, ]); } public function process_bulk_action(): void { // Delete and bulk actions are now handled in handle_actions() // which runs on admin_init before any output is sent. } public function no_items(): void { esc_html_e('No events found.', 'acs-agenda-manager'); } public function extra_tablenav($which): void { if ($which !== 'top') { return; } $filters = ACSAGMA_Database::get_event_filters(); if (empty($filters)) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter dropdown uses WP's built-in list table form with nonce. $current_filter = sanitize_text_field(wp_unslash($_REQUEST['event-filter'] ?? '')); echo '
'; echo ''; echo ''; echo '
'; } } /** * Admin page controller */ class ACSAGMA_Admin { /** @var self|null */ private static $instance = null; /** @var ACSAGMA_Agenda_List_Table */ private $list_table; public static function get_instance(): self { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_action('admin_menu', [$this, 'add_menu_page']); add_filter('set-screen-option', [$this, 'set_screen_option'], 10, 3); add_action('admin_init', [$this, 'handle_actions']); // AJAX handlers add_action('wp_ajax_acsagma_update_agenda', [$this, 'ajax_update_event']); add_action('wp_ajax_acsagma_add_item_agenda', [$this, 'ajax_add_event']); } /** * Handle delete and bulk actions early, before any output is sent. */ public function handle_actions(): void { // Only process on our admin page $current_page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; if ($current_page !== 'acsagma-agenda') { return; } $redirect_url = admin_url('admin.php?page=acsagma-agenda'); if ((isset($_POST['action']) && $_POST['action'] !== '') || (isset($_POST['action2']) && $_POST['action2'] !== '')) { check_admin_referer('acsagma_agenda_admin_form', 'acsagma_agenda_admin_form_nonce'); } // Handle single delete action if (isset($_GET['action']) && $_GET['action'] === 'delete') { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied', 'acs-agenda-manager')); } $nonce = isset($_REQUEST['_wpnonce']) ? sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])) : ''; if (!wp_verify_nonce($nonce, 'acsagma_delete_event')) { wp_die(esc_html__('Security check failed', 'acs-agenda-manager')); } $id = absint(isset($_GET['id']) ? wp_unslash($_GET['id']) : 0); if ($id) { ACSAGMA_Database::delete_event($id); wp_safe_redirect($redirect_url . '&deleted=1'); exit; } } // Handle bulk delete action if ((isset($_POST['action']) && $_POST['action'] === 'bulk-delete') || (isset($_POST['action2']) && $_POST['action2'] === 'bulk-delete')) { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied', 'acs-agenda-manager')); } if (!check_admin_referer('acsagma_agenda_admin_form', 'acsagma_agenda_admin_form_nonce')) { wp_die(esc_html__('Security check failed', 'acs-agenda-manager')); } $ids = array_map('absint', wp_unslash($_POST['bulk-delete'] ?? [])); foreach ($ids as $id) { ACSAGMA_Database::delete_event($id); } wp_safe_redirect($redirect_url . '&deleted=' . count($ids)); exit; } } public function add_menu_page(): void { $hook = add_menu_page( __('ACS Agenda', 'acs-agenda-manager'), __('ACS Agenda', 'acs-agenda-manager'), 'manage_options', 'acsagma-agenda', [$this, 'render_admin_page'], 'dashicons-calendar-alt' ); add_action("load-$hook", [$this, 'screen_options']); } public function screen_options(): void { add_screen_option('per_page', [ 'label' => __('Events per page', 'acs-agenda-manager'), 'default' => 10, 'option' => 'events_per_page', ]); $this->list_table = new ACSAGMA_Agenda_List_Table(); } public function set_screen_option($status, $option, $value) { return $value; } public function render_admin_page(): void { $agenda_page = ACSAGMA_Agenda_Manager::get_page_by_title(get_option('acsagma_page', 'Agenda')); $agenda_url = $agenda_page ? get_permalink($agenda_page) : '#'; include ACSAGMA_AGENDA_PLUGIN_DIR . 'templates/admin-page.php'; } public function ajax_update_event(): void { $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!wp_verify_nonce($nonce, 'acsagma_agenda_admin_nonce')) { wp_send_json_error(esc_html__('Security check failed', 'acs-agenda-manager')); } if (!current_user_can('manage_options')) { wp_send_json_error(esc_html__('Permission denied', 'acs-agenda-manager')); } $id = absint(wp_unslash($_POST['id'] ?? 0)); if (!$id) { wp_send_json_error(esc_html__('Invalid event ID', 'acs-agenda-manager')); } $data = $this->get_event_data_from_post(); $validation_error = $this->validate_event_data($data); if ($validation_error) { wp_send_json_error($validation_error); } $success = ACSAGMA_Database::update_event($id, $data); if ($success) { wp_send_json_success(esc_html__('Event updated successfully', 'acs-agenda-manager')); } else { wp_send_json_error(esc_html__('Failed to update event', 'acs-agenda-manager')); } } public function ajax_add_event(): void { $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!wp_verify_nonce($nonce, 'acsagma_agenda_admin_nonce')) { wp_send_json_error(esc_html__('Security check failed', 'acs-agenda-manager')); } if (!current_user_can('manage_options')) { wp_send_json_error(esc_html__('Permission denied', 'acs-agenda-manager')); } $data = $this->get_event_data_from_post(); $validation_error = $this->validate_event_data($data); if ($validation_error) { wp_send_json_error($validation_error); } $id = ACSAGMA_Database::insert_event($data); if ($id) { wp_send_json_success(esc_html__('Event added successfully', 'acs-agenda-manager')); } else { wp_send_json_error(esc_html__('Failed to add event', 'acs-agenda-manager')); } } /** * Validate event data before saving * * @param array $data Event data to validate. * @return string|null Error message if validation fails, null if valid. */ private function validate_event_data(array &$data): ?string { // Validate required fields. if (empty($data['title'])) { return esc_html__('Title is required', 'acs-agenda-manager'); } // Validate and sanitize date field. if (!empty($data['date'])) { $dates = array_map('trim', explode(',', $data['date'])); $valid_dates = []; foreach ($dates as $date_str) { if (empty($date_str)) { continue; } // Check format: dd/mm/yy or dd/mm/yyyy. if (!preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/', $date_str, $matches)) { continue; // Skip invalid format. } $day = (int) $matches[1]; $month = (int) $matches[2]; $year = (int) $matches[3]; // Validate ranges. if ($month < 1 || $month > 12 || $day < 1 || $day > 31) { continue; // Skip invalid date. } // Normalize year to 2-digit. if ($year >= 2000) { $year -= 2000; } // Format consistently. $valid_dates[] = sprintf('%02d/%02d/%02d', $day, $month, $year); } // Update data with only valid dates. $data['date'] = implode(', ', $valid_dates); } return null; } private function get_event_data_from_post(): array { // Nonce is verified in calling methods (handle_ajax_add_event, handle_ajax_edit_event). // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified before this method is called. $post_data = wp_unslash($_POST); return [ 'categorie' => sanitize_text_field($post_data['categorie'] ?? ''), 'title' => sanitize_text_field($post_data['title'] ?? ''), 'emplacement' => sanitize_text_field($post_data['emplacement'] ?? ''), 'image' => sanitize_text_field($post_data['image'] ?? ''), 'intro' => sanitize_textarea_field($post_data['intro'] ?? ''), 'link' => esc_url_raw($post_data['link'] ?? ''), 'date' => sanitize_text_field($post_data['date'] ?? ''), 'price' => sanitize_text_field($post_data['price'] ?? ''), 'account' => absint($post_data['account'] ?? 0), 'candopartial' => absint($post_data['candopartial'] ?? 0), 'redirect' => esc_url_raw($post_data['redirect'] ?? ''), ]; } }