PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
← All changes | includes/Core/ArticleSummary.php +53 -22 4.5.34.9.2 View file →
@@ -1,11 +1,16 @@
1 1 <?php
2 +namespace WPDeveloper\BetterDocs\Core;
2 3
3 -namespace WPDeveloper\BetterDocs\Core;
4 +if ( ! defined( 'ABSPATH' ) ) {
5 + exit;
6 +}
4 7
8 +
5 9 use WPDeveloper\BetterDocs\Utils\Base;
6 10 use WPDeveloper\BetterDocs\Core\Settings;
7 11 use WPDeveloper\BetterDocs\Utils\AIHelper;
12 +use WPDeveloper\BetterDocs\Utils\AIUsage;
8 13
9 14 class ArticleSummary extends Base {
10 15
11 16 public $settings;
@@ -51,32 +56,50 @@
51 56 wp_die();
52 57 }
53 58
54 59 // Verify the nonce
55 - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'betterdocs_article_summary_nonce' ) ) { //phpcs:ignore
60 + $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
61 + if ( ! wp_verify_nonce( $nonce, 'betterdocs_article_summary_nonce' ) ) {
56 62 wp_send_json_error( 'Invalid nonce' );
57 63 wp_die();
58 64 }
59 65
60 - $post_id = intval( $_POST['post_id'] ); //phpcs:ignore
61 - $post_title = sanitize_text_field( $_POST['post_title'] ); //phpcs:ignore
62 - $post_content = wp_kses_post( $_POST['post_content'] ); //phpcs:ignore
66 + $post_id = isset( $_POST['post_id'] ) ? intval( wp_unslash( $_POST['post_id'] ) ) : 0; //phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
63 67
64 - // Validate that this is a docs post type
65 - if ( $post_id > 0 ) {
66 - $post_type = get_post_type( $post_id );
67 - if ( $post_type !== 'docs' ) {
68 - wp_send_json_error( 'Doc Summarizer is only available for documentation posts.' );
69 - wp_die();
70 - }
68 + // A valid documentation post is required. The summary is always derived from
69 + // the canonical post record below, never from anything in the request body.
70 + if ( $post_id <= 0 ) {
71 + wp_send_json_error( 'Invalid document.' );
72 + wp_die();
73 + }
71 74
72 - // Check if post is password protected and user hasn't provided correct password
73 - if ( post_password_required( $post_id ) ) {
74 - wp_send_json_error( 'This document is password protected. Please enter the correct password to access the summary.' );
75 - wp_die();
76 - }
75 + $post = get_post( $post_id );
76 +
77 + // Validate that this is a docs post type.
78 + if ( ! $post || $post->post_type !== 'docs' ) {
79 + wp_send_json_error( 'Doc Summarizer is only available for documentation posts.' );
80 + wp_die();
77 81 }
78 82
83 + // Only summarize content the requester is actually allowed to view, so an
84 + // unauthenticated caller can't trigger summaries of drafts/private docs.
85 + if ( 'publish' !== $post->post_status && ! current_user_can( 'read_post', $post_id ) ) {
86 + wp_send_json_error( 'Doc Summarizer is only available for published documentation posts.' );
87 + wp_die();
88 + }
89 +
90 + // Check if post is password protected and user hasn't provided correct password.
91 + if ( post_password_required( $post_id ) ) {
92 + wp_send_json_error( 'This document is password protected. Please enter the correct password to access the summary.' );
93 + wp_die();
94 + }
95 +
96 + // SECURITY (fbs-82814): never trust client-supplied title/content. Load the
97 + // canonical values server-side so prompt-injection payloads in the request
98 + // body cannot reach the AI model or influence the cache key.
99 + $post_title = $post->post_title;
100 + $post_content = wp_strip_all_tags( $post->post_content );
101 +
79 102 if ( empty( $post_content ) ) {
80 103 wp_send_json_error( 'No content provided for summary generation.' );
81 104 wp_die();
82 105 }
@@ -88,10 +111,12 @@
88 111 $stored_hash = get_post_meta( $post_id, '_betterdocs_article_summary_hash', true );
89 112
90 113 // Return existing summary if content hasn't changed
91 114 if ( ! empty( $existing_summary ) && $content_hash === $stored_hash ) {
92 - // Clean existing summary in case it has old formatting
93 - $cleaned_existing = $this->clean_summary_content( $existing_summary );
115 + // Clean existing summary in case it has old formatting, and run it
116 + // through wp_kses_post() so any previously-stored poisoned markup is
117 + // neutralized before it is returned to the browser. (fbs-82814)
118 + $cleaned_existing = wp_kses_post( $this->clean_summary_content( $existing_summary ) );
94 119 wp_send_json_success( $cleaned_existing );
95 120 wp_die();
96 121 }
97 122 }
@@ -103,11 +128,17 @@
103 128 wp_send_json_error( $summary->get_error_message() );
104 129 wp_die();
105 130 }
106 131
107 - // Clean up the summary content
108 - $cleaned_summary = $this->clean_summary_content( $summary );
132 + // Count only fresh generations (cache hits returned earlier).
133 + AIUsage::record( 'article_summary', $post_id );
109 134
135 + // Clean up the summary content and sanitize the AI output with wp_kses_post()
136 + // before it is stored or returned. This strips dangerous attributes/tags
137 + // (onerror, <script>, etc.) even if a prompt injection ever succeeds, while
138 + // preserving safe formatting like <p>/<strong>/<ul>. (fbs-82814)
139 + $cleaned_summary = wp_kses_post( $this->clean_summary_content( $summary ) );
140 +
110 141 // Store summary in post meta if post ID is provided
111 142 if ( $post_id > 0 && ! empty( $cleaned_summary ) ) {
112 143 update_post_meta( $post_id, '_betterdocs_article_summary', $cleaned_summary );
113 144 update_post_meta( $post_id, '_betterdocs_article_summary_hash', md5( $post_content ) );
@@ -131,9 +162,9 @@
131 162 }
132 163
133 164 try {
134 165 if ( ! $this->ai_helper->has_api_key() ) {
135 - return new \WP_Error( 'no_api_key', 'OpenAI API key is not configured. Please add your API key in BetterDocs settings.' );
166 + return new \WP_Error( 'no_api_key', 'AI API key is not configured. Please add your API key in BetterDocs settings.' );
136 167 }
137 168
138 169 // Prepare content for AI processing
139 170 $prepared_content = $this->ai_helper->prepare_content_for_ai( $content, 4000 );