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();
?>
$tab ) :
$active_class = ( $this->current_tab === $tab_key ) ? 'nav-tab-active' : '';
$url = admin_url( 'admin.php?page=tpoll&tab=' . $tab_key );
$icon = isset( $tab_icons[ $tab_key ] ) ? $tab_icons[ $tab_key ] : '';
?>
0 ) : ?>
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();
?>
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;
}
?>
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')
));
}
}