current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'dashboard'; // AJAX handlers add_action('wp_ajax_tpoll_save_poll', array($this, 'ajax_save_poll')); add_action('wp_ajax_tpoll_delete_poll', array($this, 'ajax_delete_poll')); add_action('wp_ajax_tpoll_duplicate_poll', array($this, 'ajax_duplicate_poll')); add_action('wp_ajax_tpoll_reset_votes', array($this, 'ajax_reset_votes')); } /** * Add admin menu */ public function add_menu() { // Main menu add_menu_page( __('3task Polls', '3task-polls'), __('3task Polls', '3task-polls'), 'create_tpoll', 'tpoll', array($this, 'render_admin_page'), 'dashicons-chart-bar', 30 ); } /** * Get available tabs */ private function get_tabs() { $tabs = array( 'dashboard' => array( 'title' => __('Dashboard', '3task-polls'), 'icon' => 'dashicons-dashboard' ), 'polls' => array( 'title' => __('Polls', '3task-polls'), 'icon' => 'dashicons-chart-bar' ), 'results' => array( 'title' => __('Results', '3task-polls'), 'icon' => 'dashicons-analytics' ), 'settings' => array( 'title' => __('Settings', '3task-polls'), 'icon' => 'dashicons-admin-settings' ), ); /** * Filters the admin tabs. Add-ons render their tab on the action * "tpoll_admin_tab_{key}". * * @param array $tabs Key => array('title' => ..., 'icon' => ...). */ $tabs = (array) apply_filters('tpoll_admin_tabs', $tabs); // Settings are saved through options.php, which requires manage_options. if (!current_user_can('manage_options')) { unset($tabs['settings']); } return $tabs; } /** * Render main admin page with tabs */ public function render_admin_page() { $tabs = $this->get_tabs(); $tab_icons = array( 'dashboard' => '🏠', 'polls' => '📊', 'results' => '📈', 'settings' => '⚙️', ); $poll_count = TPoll_Poll::get_count(); ?>
📊

render_tab_content(); ?>
current_tab) { case 'dashboard': $this->render_dashboard(); break; case 'polls': $this->render_polls(); break; case 'results': $this->render_results(); break; case 'settings': if (current_user_can('manage_options')) { $this->render_settings(); } else { $this->render_dashboard(); } break; default: if (array_key_exists($this->current_tab, $this->get_tabs()) && has_action('tpoll_admin_tab_' . $this->current_tab)) { /** * Renders an add-on tab. */ do_action('tpoll_admin_tab_' . $this->current_tab); } else { $this->render_dashboard(); } } } /** * Render dashboard tab */ private function render_dashboard() { $stats = $this->get_poll_stats(); ?>
📊
✅
📝
🗳
0 ) : ?>

📊

    5, 'orderby' => 'id' ) ) as $tpoll_item ) : $tpoll_voters = $tpoll_item->get_total_voters(); ?>
  • title ); ?> status ] ?? $tpoll_item->status ); ?>
➕

👋

➕
render_edit_poll(); return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- View routing, read-only. if (isset($_GET['action']) && $_GET['action'] === 'new') { $this->render_new_poll(); return; } // List all polls $polls = $this->get_polls(); ?>

📊

➕
📊

➕

title ); ?>

status ] ?? $poll->status ); ?>
type ] ?? $poll->type ); ?> [tpoll id="id ); ?>"]

question, 15 ) ); ?>

' . esc_html__('Poll not found.', '3task-polls') . '

'; return; } require_once TPOLL_PLUGIN_DIR . 'admin/views/poll-edit.php'; } /** * Render new poll form */ private function render_new_poll() { $poll = new TPoll_Poll(); $global = get_option('tpoll_settings', array()); $poll->settings = array( 'vote_restriction' => $global['vote_restriction'] ?? 'ip', 'show_results' => $global['show_results'] ?? 'after_vote', 'allow_revote' => !empty($global['allow_revote']), 'show_voters_count' => $global['show_voters_count'] ?? true, 'randomize_answers' => !empty($global['randomize_answers']), 'theme' => $global['theme'] ?? 'default', 'dark_mode' => $global['dark_mode'] ?? 'no', ); require_once TPOLL_PLUGIN_DIR . 'admin/views/poll-edit.php'; } /** * Render results tab */ private function render_results() { $polls = $this->get_polls(); ?>

📈

📈

id ); $results = $poll_obj ? TPoll_Vote::get_results( $poll_obj->id, $poll_obj ) : null; if ( ! $results || empty( $results['total_voters'] ) ) { continue; } ?>

title ); ?>

status ] ?? $poll_obj->status ); ?>

question ); ?>

type ) : ?>

type ) : ?>

type, array( 'rating', 'voting' ), true ) && ! TPoll_Poll::is_choice_type( $poll_obj->type ) ) : ?>

type ) ) : ?>
type ) : ?>

get_var("SELECT COUNT(*) FROM {$wpdb->prefix}tpoll_polls"); $published_polls = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}tpoll_polls WHERE status = 'published'"); $draft_polls = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}tpoll_polls WHERE status = 'draft'"); $total_votes = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}tpoll_votes"); // phpcs:enable return array( 'total_polls' => $total_polls ?: 0, 'published_polls' => $published_polls ?: 0, 'draft_polls' => $draft_polls ?: 0, 'total_votes' => $total_votes ?: 0 ); } /** * Get polls */ private function get_polls() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. $results = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}tpoll_polls ORDER BY created_at DESC"); return $results ?: array(); } /** * Enqueue admin assets */ public function enqueue_assets($hook) { // Only on our pages if (strpos($hook, 'tpoll') === false) { return; } wp_enqueue_style( 'tpoll-admin', TPOLL_PLUGIN_URL . 'assets/css/admin.css', array(), TPOLL_VERSION ); wp_enqueue_script( 'tpoll-admin', TPOLL_PLUGIN_URL . 'assets/js/admin.js', // wp-util provides wp.template() for new answer rows. array('jquery', 'wp-util'), TPOLL_VERSION, true ); wp_localize_script('tpoll-admin', 'tpollAdmin', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('tpoll_admin'), 'choiceTypes' => TPoll_Poll::choice_types(), 'strings' => array( 'confirmDelete' => __('Are you sure you want to delete this poll? This cannot be undone.', '3task-polls'), 'confirmReset' => __('Are you sure you want to reset all votes for this poll?', '3task-polls'), 'saved' => __('Poll saved successfully!', '3task-polls'), 'error' => __('An error occurred. Please try again.', '3task-polls'), 'addAnswer' => __('Add Answer', '3task-polls'), 'removeAnswer' => __('Remove', '3task-polls'), 'minAnswers' => __('A poll needs at least two answers.', '3task-polls'), 'reactionsPreset' => array('👍', '❤️', '😂', '😮', '😢'), /* translators: %d: number of votes */ 'confirmRemoveVoted' => __('This answer has %d votes. If you remove it and save, these votes are deleted. Continue?', '3task-polls') ) )); // Color picker wp_enqueue_style('wp-color-picker'); wp_enqueue_script('wp-color-picker'); } /** * Register settings */ public function register_settings() { register_setting('tpoll_settings_group', 'tpoll_settings', array( 'sanitize_callback' => array($this, 'sanitize_settings') )); } /** * Sanitize settings */ public function sanitize_settings($input) { $input = is_array($input) ? $input : array(); $sanitized = array(); $sanitized['vote_restriction'] = self::pick($input['vote_restriction'] ?? '', array('ip', 'cookie', 'user'), 'ip'); $sanitized['show_results'] = self::pick($input['show_results'] ?? '', array('after_vote', 'always', 'never'), 'after_vote'); $sanitized['allow_revote'] = !empty($input['allow_revote']); // The defaults are stored empty, so designs keep their own colors unless the site owner picks one. $bar_color = (string) sanitize_hex_color($input['results_bar_color'] ?? ''); $bar_bg = (string) sanitize_hex_color($input['results_bar_bg'] ?? ''); $sanitized['results_bar_color'] = strtoupper($bar_color) === '#4F46E5' ? '' : $bar_color; $sanitized['results_bar_bg'] = strtoupper($bar_bg) === '#E5E7EB' ? '' : $bar_bg; $sanitized['button_text'] = sanitize_text_field($input['button_text'] ?? ''); $sanitized['voted_text'] = sanitize_text_field($input['voted_text'] ?? ''); $sanitized['closed_text'] = sanitize_text_field($input['closed_text'] ?? ''); $sanitized['theme'] = self::pick($input['theme'] ?? '', array_keys(TPoll_Shortcode::themes()), 'default'); $sanitized['dark_mode'] = self::pick($input['dark_mode'] ?? '', array('auto', 'yes', 'no'), 'no'); $sanitized['animation'] = self::pick($input['animation'] ?? '', array('grow', 'none'), 'grow'); $sanitized['show_voters_count'] = !empty($input['show_voters_count']); $sanitized['randomize_answers'] = !empty($input['randomize_answers']); $sanitized['retention_days'] = max(1, min(3650, absint($input['retention_days'] ?? 30))); $sanitized['delete_on_uninstall'] = !empty($input['delete_on_uninstall']); $sanitized['feedback_enabled'] = !empty($input['feedback_enabled']); $sanitized['feedback_style'] = self::pick($input['feedback_style'] ?? '', array('helpful', 'reactions'), 'helpful'); $sanitized['feedback_question'] = sanitize_text_field($input['feedback_question'] ?? ''); $public_types = get_post_types(array('public' => true)); $sanitized['feedback_post_types'] = array_values(array_intersect( array_map('sanitize_key', (array) ($input['feedback_post_types'] ?? array('post'))), array_keys($public_types) )); if (empty($sanitized['feedback_post_types'])) { $sanitized['feedback_post_types'] = array('post'); } return $sanitized; } /** * Return $value if it is allowed, otherwise $default. */ private static function pick($value, $allowed, $default) { $value = sanitize_key((string) $value); return in_array($value, $allowed, true) ? $value : $default; } /** * AJAX: Save poll */ public function ajax_save_poll() { check_ajax_referer('tpoll_admin', 'nonce'); if (!current_user_can('create_tpoll')) { wp_send_json_error(array('message' => __('Permission denied.', '3task-polls'))); } $poll_id = isset($_POST['poll_id']) ? absint($_POST['poll_id']) : 0; $poll = $poll_id ? TPoll_Poll::get($poll_id) : new TPoll_Poll(); if (!$poll) { $poll = new TPoll_Poll(); } // Basic fields $poll->title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : ''; $poll->question = isset($_POST['question']) ? sanitize_textarea_field(wp_unslash($_POST['question'])) : ''; // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Whitelisted in pick(). $poll->type = self::pick(isset($_POST['type']) ? wp_unslash($_POST['type']) : '', array_keys(TPoll_Poll::get_types()), 'single'); $poll->status = self::pick(isset($_POST['status']) ? wp_unslash($_POST['status']) : '', array_keys(TPoll_Poll::get_statuses()), 'draft'); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // Dates $poll->start_date = !empty($_POST['start_date']) ? self::to_mysql_date(sanitize_text_field(wp_unslash($_POST['start_date']))) : null; $poll->end_date = !empty($_POST['end_date']) ? self::to_mysql_date(sanitize_text_field(wp_unslash($_POST['end_date']))) : null; if ($poll->start_date && $poll->end_date && $poll->end_date <= $poll->start_date) { wp_send_json_error(array('message' => __('The end date must be after the start date.', '3task-polls'))); } // Settings // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Whitelisted in pick(). $poll->settings = array( 'vote_restriction' => self::pick(isset($_POST['vote_restriction']) ? wp_unslash($_POST['vote_restriction']) : '', array('ip', 'cookie', 'user'), 'ip'), 'show_results' => self::pick(isset($_POST['show_results']) ? wp_unslash($_POST['show_results']) : '', array('after_vote', 'always', 'never'), 'after_vote'), 'allow_revote' => !empty($_POST['allow_revote']), 'max_choices' => isset($_POST['max_choices']) ? absint($_POST['max_choices']) : 0, 'result_chart' => self::pick(isset($_POST['result_chart']) ? wp_unslash($_POST['result_chart']) : '', array('bars', 'donut'), 'bars'), 'per_post' => (bool) $poll->get_setting('per_post', false), 'randomize_answers' => !empty($_POST['randomize_answers']), 'show_voters_count' => !empty($_POST['show_voters_count']), 'theme' => self::pick(isset($_POST['theme']) ? wp_unslash($_POST['theme']) : '', array_keys(TPoll_Shortcode::themes()), 'default'), 'dark_mode' => self::pick(isset($_POST['dark_mode']) ? wp_unslash($_POST['dark_mode']) : '', array('auto', 'yes', 'no'), 'no') ); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized /** * Filters the poll settings before saving. Add-ons sanitize and add their own fields here. * * @param array $settings Sanitized settings. * @param array $raw Unslashed POST data (not sanitized). * @param TPoll_Poll $poll Poll being saved. */ $poll->settings = (array) apply_filters('tpoll_save_poll_settings', $poll->settings, wp_unslash($_POST), $poll); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Passed to sanitizing filters, nonce checked above. // Styles $poll->styles = array( 'bar_color' => isset($_POST['bar_color']) ? (string) sanitize_hex_color(wp_unslash($_POST['bar_color'])) : '', ); // Answers $answers = array(); if (isset($_POST['answers']) && is_array($_POST['answers'])) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized in loop below. $raw_answers = wp_unslash($_POST['answers']); foreach ($raw_answers as $answer) { if (is_array($answer) && isset($answer['text']) && trim((string) $answer['text']) !== '') { $answers[] = array( 'id' => isset($answer['id']) ? absint($answer['id']) : 0, 'text' => sanitize_text_field($answer['text']), 'image' => isset($answer['image']) ? esc_url_raw($answer['image']) : '' ); } } } if (TPoll_Poll::is_choice_type($poll->type) && count($answers) < 2) { wp_send_json_error(array('message' => __('A poll needs at least two answers.', '3task-polls'))); } /** * Lets add-ons validate a poll before it is saved. Return a WP_Error to stop. * * @param true $valid True. * @param TPoll_Poll $poll Poll with the new values. * @param array $answers Sanitized answers. */ $valid = apply_filters('tpoll_validate_poll', true, $poll, $answers); if (is_wp_error($valid)) { wp_send_json_error(array('message' => $valid->get_error_message())); } // Types without an answer list use one internal answer: the question itself. if (!TPoll_Poll::is_choice_type($poll->type)) { $existing = $poll->id ? $poll->get_answers() : array(); $answers = array(array( 'id' => !empty($existing) ? (int) $existing[0]->id : 0, 'text' => $poll->question, 'image' => '' )); } // Save poll $poll_id = $poll->save(); // Check for WP_Error (poll limit reached) if (is_wp_error($poll_id)) { wp_send_json_error(array('message' => $poll_id->get_error_message())); } if (!$poll_id) { wp_send_json_error(array('message' => __('Error saving poll.', '3task-polls'))); } $poll->save_answers($answers); /** * Fires after a poll and its answers were saved, when the answers have their IDs. * * @param TPoll_Poll $poll Saved poll. * @param array $raw Unslashed POST data (not sanitized). */ do_action('tpoll_poll_saved', $poll, wp_unslash($_POST)); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Passed to sanitizing callbacks, nonce checked above. wp_send_json_success(array( 'message' => __('Poll saved successfully!', '3task-polls'), 'poll_id' => $poll_id, 'redirect' => admin_url('admin.php?page=tpoll&tab=polls&action=edit&poll=' . $poll_id . '&saved=1') )); } /** * Convert the datetime-local value of the editor to MySQL format (site time). */ private static function to_mysql_date($value) { $ts = strtotime($value); return $ts ? gmdate('Y-m-d H:i:s', $ts) : null; } /** * AJAX: Delete poll */ public function ajax_delete_poll() { check_ajax_referer('tpoll_admin', 'nonce'); if (!current_user_can('delete_tpoll')) { wp_send_json_error(array('message' => __('Permission denied.', '3task-polls'))); } $poll_id = isset($_POST['poll_id']) ? absint($_POST['poll_id']) : 0; $poll = TPoll_Poll::get($poll_id); if (!$poll) { wp_send_json_error(array('message' => __('Poll not found.', '3task-polls'))); } $poll->delete(); wp_send_json_success(array( 'message' => __('Poll deleted successfully!', '3task-polls') )); } /** * AJAX: Duplicate poll */ public function ajax_duplicate_poll() { check_ajax_referer('tpoll_admin', 'nonce'); if (!current_user_can('create_tpoll')) { wp_send_json_error(array('message' => __('Permission denied.', '3task-polls'))); } $poll_id = isset($_POST['poll_id']) ? absint($_POST['poll_id']) : 0; $poll = TPoll_Poll::get($poll_id); if (!$poll) { wp_send_json_error(array('message' => __('Poll not found.', '3task-polls'))); } $new_poll = $poll->duplicate(); wp_send_json_success(array( 'message' => __('Poll duplicated successfully!', '3task-polls'), 'poll_id' => $new_poll->id, 'redirect' => admin_url('admin.php?page=tpoll&tab=polls&action=edit&poll=' . $new_poll->id) )); } /** * AJAX: Reset votes */ public function ajax_reset_votes() { check_ajax_referer('tpoll_admin', 'nonce'); if (!current_user_can('manage_tpoll')) { wp_send_json_error(array('message' => __('Permission denied.', '3task-polls'))); } $poll_id = isset($_POST['poll_id']) ? absint($_POST['poll_id']) : 0; $poll = TPoll_Poll::get($poll_id); if (!$poll) { wp_send_json_error(array('message' => __('Poll not found.', '3task-polls'))); } // Delete votes TPoll_Vote::delete_votes($poll_id); // Reset answer vote counts global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. $wpdb->update( $wpdb->prefix . 'tpoll_answers', array('votes' => 0), array('poll_id' => $poll_id), array('%d'), array('%d') ); wp_send_json_success(array( 'message' => __('Votes reset successfully!', '3task-polls') )); } }