Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions includes/API/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -756,8 +756,9 @@ public function get_helpful_docs() {
continue;
}

$positive = (int) get_post_meta( $doc->ID, 'positive', true );
$negative = (int) get_post_meta( $doc->ID, 'negative', true );
// Get feedback counts
$positive = (int) get_post_meta($doc->ID, 'positive', true);
$negative = (int) get_post_meta($doc->ID, 'negative', true);
if ( empty( $positive ) && empty( $negative ) ) {
continue;
}
Expand Down Expand Up @@ -1231,7 +1232,7 @@ public function get_promotional_notice() {
return false;
}

/**
/**
* Handle promotional notice hidden action
*
* @since 2.1.11
Expand Down
72 changes: 41 additions & 31 deletions includes/Admin/Docs_List_Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,59 @@ class Docs_List_Table {
* Constructor
*/
public function __construct() {
add_filter( 'manage_docs_posts_columns', [ $this, 'docs_list_columns' ] );
add_action( 'manage_docs_posts_custom_column', [ $this, 'docs_list_columns_row' ], 10, 2 );
add_filter( 'manage_edit-docs_sortable_columns', [ $this, 'docs_sortable_columns' ] );
add_filter('manage_docs_posts_columns', [$this, 'docs_list_columns']);
add_action('manage_docs_posts_custom_column', [$this, 'docs_list_columns_row'], 10, 2);
add_filter('manage_edit-docs_sortable_columns', [$this, 'docs_sortable_columns']);

add_action( 'load-edit.php', [ $this, 'edit_docs_load' ] );
add_action( 'load-post.php', [ $this, 'add_meta_box' ] );
add_action('load-edit.php', [$this, 'edit_docs_load']);
add_action('load-post.php', [$this, 'add_meta_box']);

// load css
add_action( 'admin_print_styles-post.php', [ $this, 'helpfulness_css' ] );
add_action( 'admin_print_styles-edit.php', [ $this, 'helpfulness_css' ] );
add_action('admin_print_styles-post.php', [$this, 'helpfulness_css']);
add_action('admin_print_styles-edit.php', [$this, 'helpfulness_css']);
}

public function add_meta_box() {
add_meta_box( 'op-menu-meta-box-id', __( 'Helpfulness', 'wedocs' ), [ $this, 'helpfulness_metabox' ], 'docs', 'side', 'core' );
add_meta_box('op-menu-meta-box-id', __('Helpfulness', 'wedocs'), [$this, 'helpfulness_metabox'], 'docs', 'side', 'core');
}

public function helpfulness_css() {
if ( 'docs' != get_current_screen()->post_type ) {
if ('docs' != get_current_screen()->post_type) {
return;
} ?>
<style type="text/css">
.wedocs-positive { color: green; }
.wedocs-negative { color: red; text-align: right; }
.wedocs-positive {
color: green;
}

.wedocs-negative {
color: red;
text-align: right;
}
</style>
<?php
<?php
}

public function helpfulness_metabox() {
global $post; ?>
global $post;

$positive = (int) get_post_meta($post->ID, 'positive', true);
$negative = (int) get_post_meta($post->ID, 'negative', true);
?>
<table style="width: 100%;">
<tr>
<td class="wedocs-positive">
<span class="dashicons dashicons-thumbs-up"></span>
<?php printf( '%d', get_post_meta( $post->ID, 'positive', true ) ); ?>
<?php printf('%d', $positive); ?>
</td>

<td class="wedocs-negative">
<span class="dashicons dashicons-thumbs-down"></span>
<?php printf( '%d', get_post_meta( $post->ID, 'negative', true ) ); ?>
<?php printf('%d', $negative); ?>
</td>
</tr>
</table>
<?php
<?php
}

/**
Expand All @@ -63,41 +73,41 @@ public function helpfulness_metabox() {
*
* @return array
*/
public function docs_list_columns( $columns ) {
$vote = [ 'votes' => __( 'Votes', 'wedocs' ) ];
public function docs_list_columns($columns) {
$vote = ['votes' => __('Votes', 'wedocs')];

// insert before last element, date
$first_items = array_splice( $columns, 0, 3 ); // remove first 3 items and store to $first_items, date remains to $columns
$new_columns = array_merge( $first_items, $vote, $columns ); // merge all those
$first_items = array_splice($columns, 0, 3); // remove first 3 items and store to $first_items, date remains to $columns
$new_columns = array_merge($first_items, $vote, $columns); // merge all those

return $new_columns;
}

public function docs_sortable_columns( $columns ) {
$columns['votes'] = [ 'votes', true ];
public function docs_sortable_columns($columns) {
$columns['votes'] = ['votes', true];

return $columns;
}

public function docs_list_columns_row( $column_name, $post_id ) {
if ( 'votes' == $column_name ) {
$positive = get_post_meta( $post_id, 'positive', true );
$negative = get_post_meta( $post_id, 'negative', true );
public function docs_list_columns_row($column_name, $post_id) {
if ('votes' == $column_name) {
$positive = get_post_meta($post_id, 'positive', true);
$negative = get_post_meta($post_id, 'negative', true);

printf( '<span class="wedocs-positive">%d</span>/<span class="wedocs-negative">%d</span>', $positive, $negative );
printf('<span class="wedocs-positive">%d</span>/<span class="wedocs-negative">%d</span>', $positive, $negative);
}
}

public function edit_docs_load() {
add_filter( 'request', [ $this, 'sort_docs' ] );
add_filter('request', [$this, 'sort_docs']);
}

// Sorts the movies.
public function sort_docs( $vars ) {
public function sort_docs($vars) {
// Check if we're viewing the 'movie' post type.
if ( isset( $vars['post_type'] ) && 'docs' == $vars['post_type'] ) {
if (isset($vars['post_type']) && 'docs' == $vars['post_type']) {
// Check if 'orderby' is set to 'duration'.
if ( isset( $vars['orderby'] ) && 'votes' == $vars['orderby'] ) {
if (isset($vars['orderby']) && 'votes' == $vars['orderby']) {
$vars = array_merge(
$vars,
[
Expand Down
184 changes: 184 additions & 0 deletions includes/Ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ public function __construct() {
// Handle load more for DocsGrid widget
add_action('wp_ajax_wedocs_load_more_docs', [$this, 'load_more_docs']);
add_action('wp_ajax_nopriv_wedocs_load_more_docs', [$this, 'load_more_docs']);

// Handle "Was This Helpful" votes
add_action('wp_ajax_wedocs_helpful_vote', [$this, 'handle_helpful_vote']);
add_action('wp_ajax_nopriv_wedocs_helpful_vote', [$this, 'handle_helpful_vote']);

// Handle "Was This Helpful" feedback
add_action('wp_ajax_wedocs_helpful_feedback', [$this, 'handle_helpful_feedback']);
add_action('wp_ajax_nopriv_wedocs_helpful_feedback', [$this, 'handle_helpful_feedback']);

// Handle "Need More Help" form submission
add_action('wp_ajax_wedocs_need_help_submit', [$this, 'handle_need_help_submit']);
add_action('wp_ajax_nopriv_wedocs_need_help_submit', [$this, 'handle_need_help_submit']);
}

/**
Expand Down Expand Up @@ -574,4 +586,176 @@ public function quick_search() {
wp_send_json_success( $results );
}
}

/**
* Handle "Was This Helpful" vote.
*/
public function handle_helpful_vote() {
check_ajax_referer('wedocs_helpful_vote', 'nonce');

$post_id = intval($_POST['post_id'] ?? 0);
$vote = sanitize_text_field($_POST['vote'] ?? '');

if (!$post_id || !in_array($vote, ['yes', 'no'], true)) {
wp_send_json_error(['message' => __('Invalid vote.', 'wedocs')]);
}

$meta_key = $vote === 'yes' ? 'positive' : 'negative';
$current = (int) get_post_meta($post_id, $meta_key, true);
update_post_meta($post_id, $meta_key, $current + 1);

wp_send_json_success([
'yes' => (int) get_post_meta($post_id, 'positive', true),
'no' => (int) get_post_meta($post_id, 'negative', true),
]);
}
Comment on lines +593 to +611

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing duplicate vote prevention allows unlimited vote manipulation.

Unlike the existing handle_helpful_feedback_vote() method (lines 434-481) which implements cookie-based tracking, user meta, and IP meta checks to prevent duplicate votes, this handler has no such protection. Any user can call this endpoint repeatedly to artificially inflate positive or negative counts.

Additionally, there's no validation that $post_id refers to a docs post type (compare to line 417 in the existing handler).

🔒 Proposed fix: Add duplicate vote prevention
 public function handle_helpful_vote() {
     check_ajax_referer('wedocs_helpful_vote', 'nonce');

     $post_id = intval($_POST['post_id'] ?? 0);
     $vote = sanitize_text_field($_POST['vote'] ?? '');

     if (!$post_id || !in_array($vote, ['yes', 'no'], true)) {
         wp_send_json_error(['message' => __('Invalid vote.', 'wedocs')]);
     }

+    // Verify this is a docs post
+    if (get_post_type($post_id) !== 'docs') {
+        wp_send_json_error(['message' => __('Invalid post type.', 'wedocs')]);
+    }
+
+    // Check for duplicate vote via cookie
+    $previous = isset($_COOKIE['wedocs_response']) ? explode(',', $_COOKIE['wedocs_response']) : [];
+    if (in_array($post_id, $previous, false)) {
+        wp_send_json_error([
+            'already_voted' => true,
+            'message' => __('You have already voted on this article.', 'wedocs'),
+        ]);
+    }
+
     $meta_key = $vote === 'yes' ? 'positive' : 'negative';
     $current = (int) get_post_meta($post_id, $meta_key, true);
     update_post_meta($post_id, $meta_key, $current + 1);

+    // Set cookie to prevent duplicate votes
+    $previous[] = $post_id;
+    setcookie('wedocs_response', implode(',', $previous), time() + WEEK_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
+
     wp_send_json_success([
         'yes' => (int) get_post_meta($post_id, 'positive', true),
         'no' => (int) get_post_meta($post_id, 'negative', true),
     ]);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/Ajax.php` around lines 593 - 611, The handle_helpful_vote() method
lacks duplicate vote prevention mechanisms that exist in the
handle_helpful_feedback_vote() method. Add validation to check if the post is of
type 'docs' using get_post_type() similar to line 417. Then implement duplicate
vote prevention by checking for existing votes using cookie-based tracking (via
isset($_COOKIE)), user meta checks (if user is logged in), and IP-based meta
checks to prevent the same user or IP from voting multiple times on the same
post. Store the vote tracking information using update_post_meta() or
setcookie() as appropriate after a successful vote, and return an error response
if a duplicate vote is detected before incrementing the vote counts.


/**
* Handle "Was This Helpful" negative feedback text.
*/
public function handle_helpful_feedback() {
check_ajax_referer('wedocs_helpful_vote', 'nonce');

$post_id = intval($_POST['post_id'] ?? 0);
$feedback = sanitize_textarea_field($_POST['feedback'] ?? '');

if (!$post_id || empty($feedback)) {
wp_send_json_error(['message' => __('Invalid feedback.', 'wedocs')]);
}

$existing = get_post_meta($post_id, '_wedocs_helpful_feedback', true);
if (!is_array($existing)) {
$existing = [];
}

$existing[] = [
'feedback' => $feedback,
'date' => current_time('mysql'),
'ip' => sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? ''),
];

update_post_meta($post_id, '_wedocs_helpful_feedback', $existing);
wp_send_json_success();
}
Comment on lines +616 to +639

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add post type validation to prevent feedback on non-docs posts.

Similar to handle_helpful_vote(), this handler accepts any valid post ID without verifying it's a docs post type.

🛡️ Proposed fix
     $post_id = intval($_POST['post_id'] ?? 0);
     $feedback = sanitize_textarea_field($_POST['feedback'] ?? '');

     if (!$post_id || empty($feedback)) {
         wp_send_json_error(['message' => __('Invalid feedback.', 'wedocs')]);
     }

+    if (get_post_type($post_id) !== 'docs') {
+        wp_send_json_error(['message' => __('Invalid post type.', 'wedocs')]);
+    }
+
     $existing = get_post_meta($post_id, '_wedocs_helpful_feedback', true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/Ajax.php` around lines 616 - 639, The handle_helpful_feedback method
lacks post type validation, allowing feedback to be submitted for any post ID
rather than only docs post type. After retrieving the post_id and before
processing the feedback, add validation to check that the post exists and is of
type docs using get_post and verify its post_type property equals docs,
returning an appropriate error if validation fails, similar to the pattern used
in handle_helpful_vote.


/**
* Handle "Need More Help" contact form submission.
*/
public function handle_need_help_submit() {
$widget_id = sanitize_text_field($_POST['widget_id'] ?? '');

if (!wp_verify_nonce($_POST['nonce'] ?? '', 'wedocs_need_help_' . $widget_id)) {
wp_send_json_error(['message' => __('Security check failed.', 'wedocs')]);
}

$name = sanitize_text_field($_POST['name'] ?? '');
$email = sanitize_email($_POST['email'] ?? '');
$subject = sanitize_text_field($_POST['subject'] ?? '');
$message = sanitize_textarea_field($_POST['message'] ?? '');
$page_url = esc_url_raw($_POST['page_url'] ?? '');
$page_title = sanitize_text_field($_POST['page_title'] ?? '');
$recipient = sanitize_email($_POST['recipient'] ?? get_option('admin_email'));
$save_to_elementor = sanitize_text_field($_POST['save_to_elementor'] ?? '');
$post_id = intval($_POST['post_id'] ?? 0);

if (empty($message)) {
wp_send_json_error(['message' => __('Message is required.', 'wedocs')]);
}

if (empty($recipient) || !is_email($recipient)) {
$recipient = get_option('admin_email');
}

// Send email
$email_subject = !empty($subject) ? $subject : sprintf(__('[weDocs] Support request from %s', 'wedocs'), $page_title);

$body = sprintf(__("Name: %s\n", 'wedocs'), $name ?: __('Not provided', 'wedocs'));
$body .= sprintf(__("Email: %s\n", 'wedocs'), $email ?: __('Not provided', 'wedocs'));
$body .= sprintf(__("Page: %s (%s)\n\n", 'wedocs'), $page_title, $page_url);
$body .= sprintf(__("Message:\n%s", 'wedocs'), $message);

$headers = ['Content-Type: text/plain; charset=UTF-8'];
if (!empty($email) && is_email($email)) {
$headers[] = 'Reply-To: ' . ($name ? "$name <$email>" : $email);
}

$sent = wp_mail($recipient, $email_subject, $body, $headers);

// Save to Elementor Pro submissions if enabled
if ($save_to_elementor === 'yes') {
$this->save_to_elementor_submissions($widget_id, $post_id, $page_url, $page_title, [
'name' => $name,
'email' => $email,
'subject' => $subject,
'message' => $message,
]);
}

if ($sent) {
wp_send_json_success();
} else {
wp_send_json_error(['message' => __('Failed to send email. Please try again.', 'wedocs')]);
}
}

/**
* Save form data to Elementor Pro submissions table.
*
* @param string $widget_id The Elementor widget ID.
* @param int $post_id The post/page ID where the form was submitted.
* @param string $page_url The page URL (referer).
* @param string $page_title The page title.
* @param array $fields Associative array of field id => value.
*/
private function save_to_elementor_submissions($widget_id, $post_id, $page_url, $page_title, $fields) {
// Check if Elementor Pro submissions are available
if (!class_exists('\ElementorPro\Modules\Forms\Submissions\Database\Query')) {
return;
}

$query = \ElementorPro\Modules\Forms\Submissions\Database\Query::get_instance();

$fields_data = [];
foreach ($fields as $id => $value) {
if (empty($value)) {
continue;
}

$type = 'text';
if ($id === 'email') {
$type = 'email';
} elseif ($id === 'message') {
$type = 'textarea';
}

$fields_data[] = [
'id' => $id,
'value' => $value,
'type' => $type,
];
}

if (empty($fields_data)) {
return;
}

$submission_data = [
'post_id' => $post_id ?: 0,
'referer' => $page_url,
'referer_title' => $page_title,
'element_id' => $widget_id,
'form_name' => __('weDocs - Need More Help', 'wedocs'),
'campaign_id' => 0,
'user_id' => get_current_user_id() ?: null,
'user_ip' => sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? ''),
'user_agent' => sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? ''),
'actions_count' => 1,
'actions_succeeded_count' => 1,
'meta' => wp_json_encode([
'edit_post_id' => $post_id ?: 0,
]),
];

$query->add_submission($submission_data, $fields_data);
}
}
Loading
Loading