PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.6
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.6
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 3.5.2 All 199 releases
betterdocs / includes / Core / ArticleSummary.php

ArticleSummary.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.5.6, at includes/Core/ArticleSummary.php

275 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPDeveloper\BetterDocs\Core;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8
9 use WPDeveloper\BetterDocs\Utils\Base;
10 use WPDeveloper\BetterDocs\Core\Settings;
11 use WPDeveloper\BetterDocs\Utils\AIHelper;
12 use WPDeveloper\BetterDocs\Utils\AIUsage;
13
14 class ArticleSummary extends Base {
15
16 public $settings;
17 public $ai_helper;
18
19 public function __construct( Settings $settings ) {
20 $this->settings = $settings;
21 $this->ai_helper = new AIHelper( $settings );
22
23 // Register AJAX handlers
24 add_action( 'wp_ajax_betterdocs_generate_article_summary', [ $this, 'generate_article_summary_callback' ] );
25 add_action( 'wp_ajax_nopriv_betterdocs_generate_article_summary', [ $this, 'generate_article_summary_callback' ] );
26
27 // Clear summary when post is updated
28 add_action( 'post_updated', [ $this, 'clear_article_summary_on_update' ], 10, 3 );
29 }
30
31 /**
32 * Check if article summary feature is enabled
33 *
34 * @return bool
35 */
36 public function is_enabled() {
37 return $this->settings->get( 'enable_article_summary', false );
38 }
39
40 /**
41 * Get OpenAI API key from settings
42 *
43 * @return string
44 */
45 public function get_api_key() {
46 return $this->ai_helper->get_api_key();
47 }
48
49 /**
50 * AJAX callback for generating article summary
51 */
52 public function generate_article_summary_callback() {
53 // Check if Article Summary feature is enabled
54 if ( ! $this->is_enabled() ) {
55 wp_send_json_error( 'AI Doc Summarizer feature is not enabled.' );
56 wp_die();
57 }
58
59 // Verify the nonce
60 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
61 if ( ! wp_verify_nonce( $nonce, 'betterdocs_article_summary_nonce' ) ) {
62 wp_send_json_error( 'Invalid nonce' );
63 wp_die();
64 }
65
66 $post_id = isset( $_POST['post_id'] ) ? intval( wp_unslash( $_POST['post_id'] ) ) : 0; //phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
67
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 }
74
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();
81 }
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
102 if ( empty( $post_content ) ) {
103 wp_send_json_error( 'No content provided for summary generation.' );
104 wp_die();
105 }
106
107 // Check if summary already exists in post meta
108 if ( $post_id > 0 ) {
109 $existing_summary = get_post_meta( $post_id, '_betterdocs_article_summary', true );
110 $content_hash = md5( $post_content );
111 $stored_hash = get_post_meta( $post_id, '_betterdocs_article_summary_hash', true );
112
113 // Return existing summary if content hasn't changed
114 if ( ! empty( $existing_summary ) && $content_hash === $stored_hash ) {
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 ) );
119 wp_send_json_success( $cleaned_existing );
120 wp_die();
121 }
122 }
123
124 // Generate new summary using OpenAI
125 $summary = $this->generate_summary( $post_title, $post_content );
126
127 if ( is_wp_error( $summary ) ) {
128 wp_send_json_error( $summary->get_error_message() );
129 wp_die();
130 }
131
132 // Count only fresh generations (cache hits returned earlier).
133 AIUsage::record( 'article_summary', $post_id );
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
141 // Store summary in post meta if post ID is provided
142 if ( $post_id > 0 && ! empty( $cleaned_summary ) ) {
143 update_post_meta( $post_id, '_betterdocs_article_summary', $cleaned_summary );
144 update_post_meta( $post_id, '_betterdocs_article_summary_hash', md5( $post_content ) );
145 }
146
147 wp_send_json_success( $cleaned_summary );
148 wp_die();
149 }
150
151 /**
152 * Generate article summary using OpenAI
153 *
154 * @param string $title Article title
155 * @param string $content Article content
156 * @return string|\WP_Error Generated summary or error
157 */
158 public function generate_summary( $title, $content ) {
159 // Check if Article Summary feature is enabled
160 if ( ! $this->is_enabled() ) {
161 return new \WP_Error( 'feature_disabled', 'AI Doc Summarizer feature is not enabled.' );
162 }
163
164 try {
165 if ( ! $this->ai_helper->has_api_key() ) {
166 return new \WP_Error( 'no_api_key', 'OpenAI API key is not configured. Please add your API key in BetterDocs settings.' );
167 }
168
169 // Prepare content for AI processing
170 $prepared_content = $this->ai_helper->prepare_content_for_ai( $content, 4000 );
171
172 // Create messages for summary generation
173 $messages = $this->ai_helper->create_summary_messages( $title, $prepared_content );
174
175 // Set options for summary generation
176 $options = [
177 'max_tokens' => 500,
178 'temperature' => 0.3,
179 'timeout' => 30
180 ];
181
182 $response = $this->ai_helper->make_openai_request( $messages, $options );
183
184 return $response;
185
186 } catch ( \Exception $error ) {
187 return new \WP_Error( 'exception', 'Error generating summary: ' . $error->getMessage() );
188 }
189 }
190
191
192
193 /**
194 * Clear article summary when post is updated
195 *
196 * @param int $post_id Post ID
197 * @param \WP_Post $post_after Post object after update
198 * @param \WP_Post $post_before Post object before update
199 */
200 public function clear_article_summary_on_update( $post_id, $post_after, $post_before ) {
201 // Only clear summary for docs post type
202 if ( get_post_type( $post_id ) !== 'docs' ) {
203 return;
204 }
205
206 // Only clear if content has actually changed
207 if ( $post_after->post_content !== $post_before->post_content ) {
208 delete_post_meta( $post_id, '_betterdocs_article_summary' );
209 delete_post_meta( $post_id, '_betterdocs_article_summary_hash' );
210 }
211 }
212
213 /**
214 * Get cached summary for a post
215 *
216 * @param int $post_id Post ID
217 * @return string|false Cached summary or false if not found
218 */
219 public function get_cached_summary( $post_id ) {
220 return get_post_meta( $post_id, '_betterdocs_article_summary', true );
221 }
222
223 /**
224 * Check if summary exists for a post
225 *
226 * @param int $post_id Post ID
227 * @return bool True if summary exists
228 */
229 public function has_cached_summary( $post_id ) {
230 $summary = $this->get_cached_summary( $post_id );
231 return ! empty( $summary );
232 }
233
234 /**
235 * Manually clear summary cache for a post
236 *
237 * @param int $post_id Post ID
238 * @return bool True on success
239 */
240 public function clear_summary_cache( $post_id ) {
241 $deleted_summary = delete_post_meta( $post_id, '_betterdocs_article_summary' );
242 $deleted_hash = delete_post_meta( $post_id, '_betterdocs_article_summary_hash' );
243
244 return $deleted_summary || $deleted_hash;
245 }
246
247 /**
248 * Clean summary content by removing markdown code blocks and unwanted formatting
249 *
250 * @param string $content Raw summary content from OpenAI
251 * @return string Cleaned summary content
252 */
253 public function clean_summary_content( $content ) {
254 if ( empty( $content ) ) {
255 return $content;
256 }
257
258 // Remove markdown code blocks (```html, ```, etc.)
259 $content = preg_replace( '/^```[a-zA-Z]*\s*/m', '', $content );
260 $content = preg_replace( '/\s*```\s*$/m', '', $content );
261
262 // Remove any remaining triple backticks
263 $content = str_replace( '```', '', $content );
264
265 // Clean up extra whitespace
266 $content = trim( $content );
267
268 // Remove any leading/trailing newlines
269 $content = preg_replace( '/^\s*\n+/', '', $content );
270 $content = preg_replace( '/\n+\s*$/', '', $content );
271
272 return $content;
273 }
274 }
275