type === self::TYPE; } /** * Add the type to the type list. * * @param array $types Type key => label. */ public static function register_type($types) { $types[self::TYPE] = __('Questionnaire', '3task-polls'); return $types; } /** * Add the type to a list of type keys. * * @param string[] $types Type keys. */ public static function add_type($types) { $types[] = self::TYPE; return $types; } /** * The questionnaire is sent once at the end. * * @param string $text Button text. * @param TPoll_Poll $poll Poll. */ public static function button_text($text, $poll) { return self::is_survey($poll) ? _x('Send', 'questionnaire button', '3task-polls') : $text; } /** * Steps as loaded polls, in their order. Steps that no longer exist are skipped. * * @param TPoll_Poll $poll Questionnaire. * @return TPoll_Poll[] */ public static function steps($poll) { $steps = array(); foreach (self::step_ids($poll) as $id) { $step = TPoll_Poll::get($id); if ($step && in_array($step->type, self::step_types(), true)) { $steps[] = $step; } } return $steps; } /** * Editor box: the questions of the questionnaire in their order. * * @param TPoll_Poll $poll Poll being edited. */ public static function render_edit_box($poll) { $types = TPoll_Poll::get_types(); $label = function ($step) use ($types) { return $step->title . ' (' . (isset($types[$step->type]) ? $types[$step->type] : $step->type) . ')'; }; $chosen = self::is_survey($poll) ? self::steps($poll) : array(); $chosen_ids = wp_list_pluck($chosen, 'id'); $available = array(); foreach (TPoll_Poll::get_all(array('status' => 'published', 'per_page' => 200, 'orderby' => 'title', 'order' => 'ASC')) as $candidate) { if (in_array($candidate->type, self::step_types(), true) && (int) $candidate->id !== (int) $poll->id) { $available[] = $candidate; } } ?>

    id, $label($step)); ?>

id) : ?>

  • $step) : ?>
    question); ?>
    ][]. * * @param TPoll_Poll $poll Questionnaire. * @param TPoll_Poll $step Step. */ private static function render_step($poll, $step) { $name = 'steps[' . (int) $step->id . '][]'; if ($step->type === 'rating') { ?>
    = 1; $i--) : ?> id . '-' . (int) $step->id . '-' . $i; ?>
    get_answers(); if (apply_filters('tpoll_shuffle_answers', (bool) $step->get_setting('randomize_answers', false), $step)) { shuffle($answers); } $multiple = $step->type === 'multiple'; $max = $multiple ? (int) $step->get_setting('max_choices', 0) : 0; ?>
    >

    get_setting('survey_steps', array()))); } /** * Store the selected steps (in order, each once). * * @param array $settings Sanitized settings. * @param array $raw Unslashed POST data. * @param TPoll_Poll $poll Poll being saved. */ public static function save_settings($settings, $raw, $poll) { if (!self::is_survey($poll)) { return $settings; } $ids = isset($raw['survey_steps']) ? array_map('absint', (array) $raw['survey_steps']) : array(); $settings['survey_steps'] = array_values(array_unique(array_filter($ids))); return $settings; } /** * Steps must exist, be published and have a type that fits into a form. * * @param true|WP_Error $valid Result so far. * @param TPoll_Poll $poll Poll with the new values. */ public static function validate($valid, $poll) { if (is_wp_error($valid) || !self::is_survey($poll)) { return $valid; } $ids = self::step_ids($poll); if (count($ids) < 2) { return new WP_Error('survey_steps_missing', __('A questionnaire needs at least two questions.', '3task-polls')); } foreach ($ids as $id) { $step = TPoll_Poll::get($id); if (!$step) { return new WP_Error('survey_step_missing', __('A selected question no longer exists.', '3task-polls')); } if ((int) $step->id === (int) $poll->id || !in_array($step->type, self::step_types(), true)) { /* translators: %s: title of the poll */ return new WP_Error('survey_step_type', sprintf(__('The question "%s" cannot be part of a questionnaire.', '3task-polls'), $step->title)); } if ($step->status !== 'published') { /* translators: %s: title of the poll */ return new WP_Error('survey_step_draft', sprintf(__('The question "%s" is not published yet.', '3task-polls'), $step->title)); } } return $valid; } /** * A visitor has taken part when one of the steps has a vote from them, * also a vote given outside the questionnaire. * * @param bool $voted Result for the poll itself. * @param TPoll_Poll $poll Poll. */ public static function has_voted($voted, $poll) { if (!self::is_survey($poll)) { return $voted; } foreach (self::steps($poll) as $step) { if (TPoll_Vote::has_voted($step->id, $step)) { return true; } } return false; } /** * Results only for participants (the steps may contain quiz questions), or when closed. * * @param bool $visible Result of the poll setting. * @param TPoll_Poll $poll Poll. * @param string $show_results Poll setting. * @param bool $has_voted Visitor has taken part. */ public static function results_visible($visible, $poll, $show_results, $has_voted) { if (!self::is_survey($poll)) { return $visible; } return $has_voted || $poll->is_closed(); } /** * Results of a questionnaire: the results of each step. * * @param string $html Empty string. * @param TPoll_Poll $poll Poll. */ public static function results_html($html, $poll) { return self::is_survey($poll) ? self::summary_html($poll) : $html; } /** * A questionnaire has no answers of its own to vote on. * * @param null|WP_Error $check Result so far. * @param TPoll_Poll $poll Poll. */ public static function block_direct_vote($check, $poll) { if (self::is_survey($poll)) { return new WP_Error('invalid_poll', __('Invalid poll.', '3task-polls')); } return $check; } /** * Ballot prefix of one questionnaire. The rest of the 32 characters is random. * * @param TPoll_Poll $poll Questionnaire. */ private static function ballot_prefix($poll) { return 's' . (int) $poll->id . '-'; } /** * All submissions of a questionnaire, each with its answers per step. * * @param TPoll_Poll $poll Questionnaire. * @return array[] List of array('ballot' => string, 'steps' => array(poll ID => array('answers' => int[], 'value' => int))). */ public static function participations($poll) { global $wpdb; if (!self::is_survey($poll)) { return array(); } $table = $wpdb->prefix . 'tpoll_votes'; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table, table name is safe. $rows = $wpdb->get_results($wpdb->prepare( "SELECT ballot, poll_id, answer_id, vote_value FROM $table WHERE ballot LIKE %s ORDER BY id ASC", $wpdb->esc_like(self::ballot_prefix($poll)) . '%' )); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $list = array(); foreach ((array) $rows as $row) { $ballot = (string) $row->ballot; $step = (int) $row->poll_id; if (!isset($list[$ballot])) { $list[$ballot] = array('ballot' => $ballot, 'steps' => array()); } if (!isset($list[$ballot]['steps'][$step])) { $list[$ballot]['steps'][$step] = array('answers' => array(), 'value' => (int) $row->vote_value); } $list[$ballot]['steps'][$step]['answers'][] = (int) $row->answer_id; } return array_values($list); } /** * Results of all steps, with the number of correct quiz answers when it is known. * * @param TPoll_Poll $poll Questionnaire. * @param array|null $score array('correct' => int, 'total' => int) right after sending, else null. * @return string */ public static function summary_html($poll, $score = null) { ob_start(); ?>
    0) : ?>

    question); ?>

    id, $step), false); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped inside results_html(). ?>
    __('Security check failed. Please reload the page and try again.', '3task-polls'))); } if (!empty($_POST['tpoll_hp'])) { wp_send_json_error(array('message' => __('Your vote could not be counted.', '3task-polls'))); } $poll = TPoll_Poll::get(isset($_POST['poll_id']) ? absint($_POST['poll_id']) : 0); if (!self::is_survey($poll)) { wp_send_json_error(array('message' => __('Invalid poll.', '3task-polls'))); } if (!$poll->is_active()) { wp_send_json_error(array('message' => __('This poll is not active.', '3task-polls'))); } if ($poll->get_setting('vote_restriction', 'ip') === 'user' && !is_user_logged_in()) { wp_send_json_error(array('message' => __('Please log in to vote.', '3task-polls'))); } if (TPoll_Vote::has_voted($poll->id, $poll)) { wp_send_json_error(array('message' => __('You have already taken part.', '3task-polls'))); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each value goes through absint() below. $raw = isset($_POST['steps']) && is_array($_POST['steps']) ? wp_unslash($_POST['steps']) : array(); $plan = array(); foreach (self::steps($poll) as $step) { $values = isset($raw[$step->id]) ? array_values(array_filter(array_map('absint', (array) $raw[$step->id]))) : array(); if ($step->type === 'rating') { $answers = $step->get_answers(); if (count($values) !== 1 || $values[0] > 5 || empty($answers)) { wp_send_json_error(array('message' => __('Please answer all questions.', '3task-polls'))); } $plan[] = array($step, array((int) $answers[0]->id), $values[0]); continue; } if (empty($values)) { wp_send_json_error(array('message' => __('Please answer all questions.', '3task-polls'))); } $plan[] = array($step, $values, 1); } $ballot = self::ballot_prefix($poll); $ballot .= wp_generate_password(32 - strlen($ballot), false, false); $score = array('correct' => 0, 'total' => 0); // The rate limit counts one request, not one per step. $no_limit = function () { return 0; }; foreach ($plan as $index => $item) { list($step, $answers, $value) = $item; if ($index === 1) { add_filter('tpoll_rate_limit', $no_limit); } $result = TPoll_Vote::cast($step->id, $answers, $value, 0, $ballot); if (is_wp_error($result)) { remove_filter('tpoll_rate_limit', $no_limit); TPoll_Vote::delete_ballot($ballot); wp_send_json_error(array('message' => $result->get_error_message())); } if (TPoll_Quiz::is_quiz($step)) { $score['total']++; if (in_array((int) $step->get_setting('quiz_correct', 0), $answers, true)) { $score['correct']++; } } } remove_filter('tpoll_rate_limit', $no_limit); wp_send_json_success(array( 'message' => __('Thank you for taking part!', '3task-polls'), 'score' => $score, 'html' => self::summary_html($poll, $score), )); } }