PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.8.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.8.0
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 / ArticleQualityScore.php

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

526 lines 23.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\Utils\AIHelper;
11 use WPDeveloper\BetterDocs\Utils\AIUsage;
12 use WPDeveloper\BetterDocs\Core\Settings;
13
14 class ArticleQualityScore extends Base {
15
16 /**
17 * Settings instance
18 *
19 * @var Settings
20 */
21 private $settings;
22
23 /**
24 * AIHelper instance
25 *
26 * @var AIHelper
27 */
28 private $ai_helper;
29
30 public function __construct( Settings $settings ) {
31 $this->settings = $settings;
32 $this->ai_helper = new AIHelper( $settings );
33
34 // Only initialize for docs post type in admin
35 if ( is_admin() ) {
36 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] );
37 add_action( 'wp_ajax_betterdocs_analyze_quality', [ $this, 'ajax_analyze_article_quality' ] );
38 add_action( 'wp_ajax_betterdocs_save_quality_analysis', [ $this, 'ajax_save_quality_analysis' ] );
39 add_action( 'wp_ajax_betterdocs_check_cached_analysis', [ $this, 'ajax_check_cached_analysis' ] );
40
41 // Hook into unified metabox
42 add_action( 'betterdocs_quality_analysis_tab_content', [ $this, 'render_quality_analysis_content' ] );
43 }
44 }
45
46 /**
47 * Enqueue admin scripts for quality score functionality
48 */
49 public function enqueue_admin_scripts( $hook ) {
50 global $post_type;
51
52 // Only load on docs edit screens
53 if ( ( $hook === 'post.php' || $hook === 'post-new.php' ) && $post_type === 'docs' ) {
54 betterdocs()->assets->enqueue( 'betterdocs-article-quality-score', 'admin/js/article-quality-score.js' );
55
56 // Localize script with AJAX data
57 wp_localize_script(
58 'betterdocs-article-quality-score',
59 'betterdocsQualityScore',
60 [
61 'ajax_url' => admin_url( 'admin-ajax.php' ),
62 'nonce' => wp_create_nonce( 'betterdocs_quality_score_nonce' ),
63 'post_id' => get_the_ID(),
64 'strings' => [
65 'analysing' => __( 'Analysing your docs with BetterDocs AI...', 'betterdocs' ),
66 'error' => __( 'Error', 'betterdocs' ),
67 'regenerate' => __( 'Regenerate', 'betterdocs' ),
68 'analyze' => __( 'Analyze with BetterDocs AI', 'betterdocs' ),
69 ]
70 ]
71 );
72 }
73 }
74
75 /**
76 * Render quality analysis content for the unified metabox tab
77 *
78 * @param \WP_Post $post Current post object
79 */
80 public function render_quality_analysis_content( $post ) {
81 $nonce = wp_create_nonce( 'betterdocs_quality_score_nonce' );
82 $cached_analysis = get_post_meta( $post->ID, '_betterdocs_article_quality_analysis', true );
83 $has_results = ! empty( $cached_analysis ) && isset( $cached_analysis['overall_score'] );
84
85 ?>
86 <div class="betterdocs-quality-analysis-content" data-post-id="<?php echo esc_attr( $post->ID ); ?>">
87 <!-- Loading State -->
88 <div class="quality-loading" style="display: none;">
89 <div class="quality-loading-spinner">
90 <div class="quality-spinner is-active">
91 <div class="quality-spinner-color"></div>
92 <div class="quality-spinner-mask"></div>
93 </div>
94 </div>
95 <p><?php esc_html_e( 'Analysing your docs with BetterDocs AI...', 'betterdocs' ); ?></p>
96 </div>
97
98 <!-- Results State (always rendered, hidden if no results) -->
99 <div class="quality-results" style="<?php echo $has_results ? '' : 'display: none;'; ?>">
100 <?php if ( $has_results ) : ?>
101 <?php $this->render_quality_results( $cached_analysis ); ?>
102 <?php endif; ?>
103 </div>
104
105 <!-- Empty State -->
106 <div class="quality-empty" style="<?php echo $has_results ? 'display: none;' : ''; ?>">
107 <h4><?php esc_html_e( 'Docs quality analysis', 'betterdocs' ); ?></h4>
108 <p><?php esc_html_e( 'Analyze your docs quality with BetterDocs AI to get actionable insights and improvements', 'betterdocs' ); ?></p>
109 <span class="betterdocs-analyze-btn" id="betterdocs-analyze-quality">
110 <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
111 <g clip-path="url(#clip0_13783_29413)">
112 <g filter="url(#filter0_d_13783_29413)">
113 <path d="M3.32812 10.2207C3.32812 9.93383 3.56132 9.70404 3.84675 9.67574C6.30094 9.43242 8.2527 7.48064 8.49602 5.02643C8.52431 4.741 8.75411 4.50781 9.04093 4.50781C9.32776 4.50781 9.55755 4.741 9.58585 5.02643C9.82917 7.48064 11.7809 9.43242 14.2351 9.67574C14.5205 9.70404 14.7537 9.93383 14.7537 10.2207C14.7537 10.5075 14.5205 10.7373 14.2351 10.7656C11.7809 11.0089 9.82917 12.9607 9.58585 15.4149C9.55755 15.7003 9.32776 15.9335 9.04093 15.9335C8.75411 15.9335 8.52431 15.7003 8.49602 15.4149C8.2527 12.9607 6.30094 11.0089 3.84675 10.7656C3.56132 10.7373 3.32812 10.5075 3.32812 10.2207Z" fill="url(#paint0_linear_13783_29413)"></path>
114 </g>
115 <g filter="url(#filter1_d_13783_29413)">
116 <path d="M13.6133 3.93261C13.6133 3.84656 13.6832 3.77762 13.7689 3.76913C14.5051 3.69613 15.0907 3.1106 15.1636 2.37434C15.1721 2.28871 15.2411 2.21875 15.3271 2.21875C15.4132 2.21875 15.4821 2.28871 15.4906 2.37434C15.5636 3.1106 16.1491 3.69613 16.8854 3.76913C16.971 3.77762 17.041 3.84656 17.041 3.93261C17.041 4.01865 16.971 4.08759 16.8854 4.09608C16.1491 4.16908 15.5636 4.75462 15.4906 5.49088C15.4821 5.5765 15.4132 5.64646 15.3271 5.64646C15.2411 5.64646 15.1721 5.5765 15.1636 5.49088C15.0907 4.75462 14.5051 4.16908 13.7689 4.09608C13.6832 4.08759 13.6133 4.01865 13.6133 3.93261Z" fill="url(#paint1_linear_13783_29413)"></path>
117 </g>
118 <g filter="url(#filter2_d_13783_29413)">
119 <path d="M12.4727 15.9336C12.4727 15.8188 12.5659 15.7269 12.6801 15.7156C13.6618 15.6183 14.4425 14.8376 14.5398 13.8559C14.5511 13.7417 14.643 13.6484 14.7578 13.6484C14.8725 13.6484 14.9644 13.7417 14.9757 13.8559C15.0731 14.8376 15.8538 15.6183 16.8355 15.7156C16.9496 15.7269 17.0429 15.8188 17.0429 15.9336C17.0429 16.0483 16.9496 16.1402 16.8355 16.1515C15.8538 16.2489 15.0731 17.0296 14.9757 18.0113C14.9644 18.1254 14.8725 18.2187 14.7578 18.2187C14.643 18.2187 14.5511 18.1254 14.5398 18.0113C14.4425 17.0296 13.6618 16.2489 12.6801 16.1515C12.5659 16.1402 12.4727 16.0483 12.4727 15.9336Z" fill="url(#paint2_linear_13783_29413)"></path>
120 </g>
121 </g>
122 <defs>
123 <filter id="filter0_d_13783_29413" x="-1.24214" y="3.36525" width="20.5663" height="20.5624" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
124 <feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
125 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"></feColorMatrix>
126 <feOffset dy="3.4277"></feOffset>
127 <feGaussianBlur stdDeviation="2.28513"></feGaussianBlur>
128 <feComposite in2="hardAlpha" operator="out"></feComposite>
129 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.00392157 0 0 0 0 0.137255 0 0 0 0.13 0"></feColorMatrix>
130 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_13783_29413"></feBlend>
131 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_13783_29413" result="shape"></feBlend>
132 </filter>
133 <filter id="filter1_d_13783_29413" x="9.04302" y="1.07618" width="12.5663" height="12.5702" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
134 <feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
135 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"></feColorMatrix>
136 <feOffset dy="3.4277"></feOffset>
137 <feGaussianBlur stdDeviation="2.28513"></feGaussianBlur>
138 <feComposite in2="hardAlpha" operator="out"></feComposite>
139 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.00392157 0 0 0 0 0.137255 0 0 0 0.13 0"></feColorMatrix>
140 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_13783_29413"></feBlend>
141 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_13783_29413" result="shape"></feBlend>
142 </filter>
143 <filter id="filter2_d_13783_29413" x="7.90239" y="12.5059" width="13.7108" height="13.7108" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
144 <feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
145 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"></feColorMatrix>
146 <feOffset dy="3.4277"></feOffset>
147 <feGaussianBlur stdDeviation="2.28513"></feGaussianBlur>
148 <feComposite in2="hardAlpha" operator="out"></feComposite>
149 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.00392157 0 0 0 0 0.137255 0 0 0 0.13 0"></feColorMatrix>
150 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_13783_29413"></feBlend>
151 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_13783_29413" result="shape"></feBlend>
152 </filter>
153 <linearGradient id="paint0_linear_13783_29413" x1="9.04093" y1="4.50781" x2="9.04093" y2="15.9335" gradientUnits="userSpaceOnUse">
154 <stop stop-color="#CD93FF"></stop>
155 <stop offset="1" stop-color="#A741FF"></stop>
156 </linearGradient>
157 <linearGradient id="paint1_linear_13783_29413" x1="15.3271" y1="2.21875" x2="15.3271" y2="5.64646" gradientUnits="userSpaceOnUse">
158 <stop stop-color="#D099FF"></stop>
159 <stop offset="1" stop-color="#CE96FF"></stop>
160 </linearGradient>
161 <linearGradient id="paint2_linear_13783_29413" x1="14.7578" y1="13.6484" x2="14.7578" y2="18.2187" gradientUnits="userSpaceOnUse">
162 <stop stop-color="#D099FF"></stop>
163 <stop offset="1" stop-color="#CE96FF"></stop>
164 </linearGradient>
165 <clipPath id="clip0_13783_29413">
166 <rect width="20" height="20" fill="white"></rect>
167 </clipPath>
168 </defs>
169 </svg>
170 <?php esc_html_e( 'Analyze with BetterDocs AI', 'betterdocs' ); ?>
171 </span>
172 </div>
173
174 <!-- Error State -->
175 <div class="quality-error" style="display: none;">
176 <div class="error-content">
177 <div class="error-icon">
178 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings w-6 h-6" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
179 </div>
180 <div class="error-text">
181 <strong><?php esc_html_e( 'Configuration Required', 'betterdocs' ); ?></strong>
182 <div class="error-message"></div>
183 <a href="<?php echo esc_url( admin_url( 'admin.php?page=betterdocs-settings#betterdocs-ai' ) ); ?>" class="error-action-button" style="display: none;">
184 <?php esc_html_e( 'Go to Settings', 'betterdocs' ); ?>
185 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right" aria-hidden="true"><path d="m9 18 6-6-6-6"></path></svg>
186 </a>
187 </div>
188 </div>
189 </div>
190 </div>
191 <?php
192 }
193
194 /**
195 * Render quality results
196 *
197 * @param array $analysis Analysis data
198 */
199 private function render_quality_results( $analysis ) {
200 $overall_score = $analysis['overall_score'] ?? 0;
201 $detailed_scores = $analysis['detailed_scores'] ?? $analysis['scores'] ?? [];
202 $suggestions = $analysis['suggestions'] ?? [];
203
204 // Extract suggestions from feedback if available
205 if ( empty( $suggestions ) && isset( $analysis['feedback'] ) ) {
206 $feedback = $analysis['feedback'];
207 if ( isset( $feedback['strengths'] ) && is_array( $feedback['strengths'] ) ) {
208 $suggestions = array_merge( $suggestions, $feedback['strengths'] );
209 }
210 if ( isset( $feedback['improvements'] ) && is_array( $feedback['improvements'] ) ) {
211 $suggestions = array_merge( $suggestions, $feedback['improvements'] );
212 }
213 if ( isset( $feedback['suggestions'] ) && is_array( $feedback['suggestions'] ) ) {
214 $suggestions = array_merge( $suggestions, $feedback['suggestions'] );
215 }
216 }
217
218 $score_color = $this->get_score_status_class( $overall_score );
219 $circumference = 2 * M_PI * 54; // radius = 54
220 $offset = $circumference - ( $overall_score / 100 ) * $circumference;
221 ?>
222
223 <div class="quality-results-header">
224 <h4><?php esc_html_e( 'Docs quality analysis results', 'betterdocs' ); ?></h4>
225 <button type="button" class="betterdocs-regenerate-quality" id="betterdocs-regenerate-quality">
226 <svg
227 width="16"
228 height="22"
229 viewBox="0 0 16 22"
230 fill="none"
231 xmlns="http://www.w3.org/2000/svg"
232 >
233 <path
234 d="M1.1 15.05C0.733333 14.4167 0.458333 13.7667 0.275 13.1C0.0916667 12.4333 0 11.75 0 11.05C0 8.81667 0.775 6.91667 2.325 5.35C3.875 3.78333 5.76667 3 8 3H8.175L6.575 1.4L7.975 0L11.975 4L7.975 8L6.575 6.6L8.175 5H8C6.33333 5 4.91667 5.5875 3.75 6.7625C2.58333 7.9375 2 9.36667 2 11.05C2 11.4833 2.05 11.9083 2.15 12.325C2.25 12.7417 2.4 13.15 2.6 13.55L1.1 15.05ZM8.025 22L4.025 18L8.025 14L9.425 15.4L7.825 17H8C9.66667 17 11.0833 16.4125 12.25 15.2375C13.4167 14.0625 14 12.6333 14 10.95C14 10.5167 13.95 10.0917 13.85 9.675C13.75 9.25833 13.6 8.85 13.4 8.45L14.9 6.95C15.2667 7.58333 15.5417 8.23333 15.725 8.9C15.9083 9.56667 16 10.25 16 10.95C16 13.1833 15.225 15.0833 13.675 16.65C12.125 18.2167 10.2333 19 8 19H7.825L9.425 20.6L8.025 22Z"
235 fill="#475467"
236 />
237 </svg>
238 </button>
239 </div>
240
241 <div class="quality-results-body">
242 <!-- Left Column: Circular Progress Score -->
243 <div class="score-column">
244 <h3 class="score-column-title"><?php esc_html_e( 'Overall Score', 'betterdocs' ); ?></h3>
245 <div class="progress-ring-wrapper">
246 <svg class="progress-ring" viewBox="0 0 120 120">
247 <circle class="progress-ring-bg" cx="60" cy="60" r="54" fill="none" stroke-width="12"></circle>
248 <circle class="progress-ring-circle <?php echo esc_attr( $score_color ); ?>" cx="60" cy="60" r="54" fill="none" stroke-width="12" stroke-linecap="round" style="stroke-dasharray: <?php echo esc_attr( $circumference ); ?>; stroke-dashoffset: <?php echo esc_attr( $offset ); ?>; transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dashoffset 0.5s ease-in-out;"></circle>
249 </svg>
250 <div class="score-text-wrapper">
251 <span class="score-value <?php echo esc_attr( $score_color ); ?>"><?php echo esc_html( $overall_score ); ?></span>
252 <span class="score-label"><?php esc_html_e( 'out of 100', 'betterdocs' ); ?></span>
253 </div>
254 </div>
255 <div class="score-feedback <?php echo esc_attr( $score_color ); ?>">
256 <?php echo esc_html( $this->get_score_status_text( $overall_score ) ); ?>
257 </div>
258 <p class="score-description">
259 <?php esc_html_e( 'This score is a weighted average of clarity, relevance, structure, and other quality factors.', 'betterdocs' ); ?>
260 </p>
261 </div>
262
263 <!-- Right Column: Score Breakdown & Suggestions -->
264 <div class="details-column">
265 <?php if ( ! empty( $detailed_scores ) ) : ?>
266 <div class="section">
267 <h3 class="section-title"><?php esc_html_e( 'Score Breakdown', 'betterdocs' ); ?></h3>
268 <div class="score-breakdown-list">
269 <?php foreach ( $detailed_scores as $criterion => $score ) : ?>
270 <div class="score-item">
271 <div class="score-name-group">
272 <span class="score-name"><?php echo esc_html( ucfirst( $criterion ) ); ?></span>
273 <span class="score-percentage"><?php echo esc_html( $score ); ?>%</span>
274 </div>
275 <div class="score-bar-group">
276 <div class="score-bar-wrapper">
277 <div class="score-bar-fill <?php echo esc_attr( $this->get_score_status_class( $score ) ); ?>" style="width: <?php echo esc_attr( $score ); ?>%"></div>
278 </div>
279
280 </div>
281 </div>
282 <?php endforeach; ?>
283 </div>
284 </div>
285 <?php endif; ?>
286 </div>
287 </div>
288 <?php if ( ! empty( $suggestions ) ) : ?>
289 <div class="improvement-section section">
290 <h3 class="section-title"><?php esc_html_e( 'Actionable Insights', 'betterdocs' ); ?></h3>
291 <div class="improvements-list">
292 <?php foreach ( $suggestions as $suggestion ) : ?>
293 <div class="improvement-item">
294 <div class="improvement-icon">
295 <svg
296 width="16"
297 height="16"
298 viewBox="0 0 16 16"
299 fill="none"
300 xmlns="http://www.w3.org/2000/svg"
301 >
302 <rect width="16" height="16" rx="8" fill="#D0D5DD" />
303 <path
304 d="M5 8.88L6.67619 10.8L10.8667 6"
305 stroke="white"
306 strokeWidth="1.2"
307 strokeLinecap="round"
308 strokeLinejoin="round"
309 />
310 </svg>
311 </div>
312 <span class="improvement-text"><?php echo esc_html( $suggestion ); ?></span>
313 </div>
314 <?php endforeach; ?>
315 </div>
316 </div>
317 <?php endif; ?>
318 <?php
319 }
320
321
322
323
324
325 /**
326 * Get CSS class for score status
327 *
328 * @param int $score Overall score
329 * @return string CSS class
330 */
331 private function get_score_status_class( $score ) {
332 if ( $score >= 80 ) return 'green';
333 if ( $score >= 50 ) return 'yellow';
334 if ( $score >= 30 ) return 'orange';
335 return 'red';
336 }
337
338 /**
339 * Get text for score status
340 *
341 * @param int $score Overall score
342 * @return string Status text
343 */
344 private function get_score_status_text( $score ) {
345 if ( $score >= 80 ) return __( 'Excellent', 'betterdocs' );
346 if ( $score >= 60 ) return __( 'Good', 'betterdocs' );
347 if ( $score >= 40 ) return __( 'Needs Improvement', 'betterdocs' );
348 return __( 'Poor', 'betterdocs' );
349 }
350
351 /**
352 * AJAX handler for docs quality analysis
353 */
354 public function ajax_analyze_article_quality() {
355 // Verify nonce
356 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
357 if ( ! wp_verify_nonce( $nonce, 'betterdocs_quality_score_nonce' ) ) {
358 wp_send_json_error( [ 'message' => __( 'Invalid nonce', 'betterdocs' ) ] );
359 }
360
361 $post_id = isset( $_POST['post_id'] ) ? intval( wp_unslash( $_POST['post_id'] ) ) : 0;
362
363 if ( empty( $post_id ) ) {
364 wp_send_json_error( [ 'message' => __( 'Invalid post ID', 'betterdocs' ) ] );
365 }
366
367 // SECURITY: Check if user can edit THIS SPECIFIC post (prevents IDOR)
368 if ( ! current_user_can( 'edit_post', $post_id ) ) {
369 wp_send_json_error( [ 'message' => __( 'Insufficient permissions', 'betterdocs' ) ] );
370 }
371
372 $post = get_post( $post_id );
373
374 if ( ! $post || $post->post_type !== 'docs' ) {
375 wp_send_json_error( [ 'message' => __( 'Invalid post or post type', 'betterdocs' ) ] );
376 }
377
378 // Check if current content is provided (from editor, may be unsaved)
379 $current_content = isset( $_POST['current_content'] ) ? wp_kses_post( wp_unslash( $_POST['current_content'] ) ) : '';
380 $current_title = isset( $_POST['current_title'] ) ? sanitize_text_field( wp_unslash( $_POST['current_title'] ) ) : '';
381
382 // Use current content if provided, otherwise use saved post content
383 if ( ! empty( $current_content ) ) {
384 $content = $current_content;
385 $title = ! empty( $current_title ) ? $current_title : $post->post_title;
386 } else {
387 $content = $post->post_content;
388 $title = $post->post_title;
389 }
390
391 if ( empty( $content ) ) {
392 wp_send_json_error( [ 'message' => __( 'Post content is empty. Please add content to your post and try again.', 'betterdocs' ) ] );
393 }
394
395 // Strip HTML tags and shortcodes for analysis
396 $clean_content = wp_strip_all_tags( do_shortcode( $content ) );
397 $clean_content = preg_replace( '/\s+/', ' ', trim( $clean_content ) );
398
399 if ( strlen( $clean_content ) < 50 ) {
400 wp_send_json_error( [ 'message' => __( 'Content is too short for meaningful analysis. Please add more content (at least 50 characters) and try again.', 'betterdocs' ) ] );
401 }
402
403 // Perform AI analysis
404 $analysis_result = $this->ai_helper->analyze_article_quality( $clean_content, $title );
405
406 if ( is_wp_error( $analysis_result ) ) {
407 wp_send_json_error( [ 'message' => $analysis_result->get_error_message() ] );
408 }
409
410 // Save the analysis result
411 $saved = $this->ai_helper->save_article_quality_score( $post_id, $analysis_result );
412
413 if ( ! $saved ) {
414 wp_send_json_error( [ 'message' => __( 'Failed to save analysis result', 'betterdocs' ) ] );
415 }
416
417 // Count a successful fresh analysis.
418 AIUsage::record( 'quality_score', $post_id );
419
420 // Return success with the analysis data
421 wp_send_json_success( [
422 'message' => __( 'Docs analyzed successfully', 'betterdocs' ),
423 'data' => $analysis_result
424 ] );
425 }
426
427 /**
428 * AJAX handler for saving quality analysis results
429 */
430 public function ajax_save_quality_analysis() {
431 // Verify nonce
432 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
433 if ( ! wp_verify_nonce( $nonce, 'betterdocs_quality_score_nonce' ) ) {
434 wp_send_json_error( __( 'Security check failed', 'betterdocs' ) );
435 }
436
437 $post_id = isset( $_POST['post_id'] ) ? intval( wp_unslash( $_POST['post_id'] ) ) : 0;
438 // JSON payload validated/decoded below.
439 $analysis_data = isset( $_POST['analysis_data'] ) ? wp_unslash( $_POST['analysis_data'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- raw JSON parsed via json_decode below.
440
441 if ( ! $post_id || ! $analysis_data ) {
442 wp_send_json_error( __( 'Invalid data provided', 'betterdocs' ) );
443 }
444
445 // SECURITY: Check if user can edit THIS SPECIFIC post (prevents IDOR)
446 if ( ! current_user_can( 'edit_post', $post_id ) ) {
447 wp_send_json_error( __( 'Insufficient permissions', 'betterdocs' ) );
448 }
449
450 // Decode the analysis data
451 $decoded_data = json_decode( $analysis_data, true );
452 if ( ! $decoded_data ) {
453 wp_send_json_error( __( 'Invalid analysis data format', 'betterdocs' ) );
454 }
455
456 // Add timestamp and post modified date to the analysis data
457 $post = get_post( $post_id );
458 $decoded_data['analysis_timestamp'] = current_time( 'timestamp' );
459 $decoded_data['post_modified'] = $post->post_modified;
460
461 // Save the analysis data as post meta
462 $meta_key = '_betterdocs_article_quality_analysis';
463 $saved = update_post_meta( $post_id, $meta_key, $decoded_data );
464
465 if ( $saved !== false ) {
466 wp_send_json_success( [
467 'message' => __( 'Analysis results saved successfully', 'betterdocs' ),
468 'data' => $decoded_data
469 ] );
470 } else {
471 wp_send_json_error( __( 'Failed to save analysis results', 'betterdocs' ) );
472 }
473 }
474
475 /**
476 * AJAX handler for checking cached analysis results
477 */
478 public function ajax_check_cached_analysis() {
479 // Verify nonce
480 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
481 if ( ! wp_verify_nonce( $nonce, 'betterdocs_quality_score_nonce' ) ) {
482 wp_send_json_error( __( 'Security check failed', 'betterdocs' ) );
483 }
484
485 $post_id = isset( $_POST['post_id'] ) ? intval( wp_unslash( $_POST['post_id'] ) ) : 0;
486
487 if ( ! $post_id ) {
488 wp_send_json_error( __( 'Invalid post ID', 'betterdocs' ) );
489 }
490
491 // SECURITY: Check if user can edit THIS SPECIFIC post (prevents IDOR)
492 if ( ! current_user_can( 'edit_post', $post_id ) ) {
493 wp_send_json_error( __( 'Insufficient permissions', 'betterdocs' ) );
494 }
495
496 // Get the post and its last modified time
497 $post = get_post( $post_id );
498 if ( ! $post ) {
499 wp_send_json_error( __( 'Post not found', 'betterdocs' ) );
500 }
501
502 // Get cached analysis data
503 $cached_analysis = get_post_meta( $post_id, '_betterdocs_article_quality_analysis', true );
504
505 // Check if we have cached data and if the post hasn't been modified since analysis
506 if ( $cached_analysis &&
507 isset( $cached_analysis['post_modified'] ) &&
508 $cached_analysis['post_modified'] === $post->post_modified ) {
509
510 // Return cached data
511 wp_send_json_success( [
512 'has_cache' => true,
513 'message' => __( 'Using cached analysis results', 'betterdocs' ),
514 'data' => $cached_analysis
515 ] );
516 } else {
517 // No cache or post has been updated
518 wp_send_json_success( [
519 'has_cache' => false,
520 'message' => __( 'No valid cache found, new analysis required', 'betterdocs' )
521 ] );
522 }
523 }
524
525 }
526