PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.10.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / api / class-seo-score-endpoint.php

class-seo-score-endpoint.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.10.0, at includes/api/class-seo-score-endpoint.php

360 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SEO Score REST API Endpoint
4 *
5 * Handles REST API endpoints for SEO score calculation and history
6 *
7 * @package ThinkRank\API
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 namespace ThinkRank\API;
14
15 use ThinkRank\AI\SEOScoreCalculator;
16 use ThinkRank\Core\Database;
17 use WP_REST_Request;
18 use WP_REST_Response;
19 use WP_Error;
20
21 // Prevent direct access
22 if (!defined('ABSPATH')) {
23 exit;
24 }
25
26 /**
27 * SEO Score Endpoint Class
28 *
29 * Provides REST API endpoints for:
30 * - /wp-json/thinkrank/v1/seo-score/calculate
31 * - /wp-json/thinkrank/v1/seo-score/history
32 *
33 * @since 1.0.0
34 */
35 class SEOScoreEndpoint {
36
37 /**
38 * SEO Score Calculator instance
39 *
40 * @var SEOScoreCalculator
41 */
42 private SEOScoreCalculator $calculator;
43
44 /**
45 * Constructor
46 *
47 * @param SEOScoreCalculator $calculator SEO Score Calculator instance
48 */
49 public function __construct(SEOScoreCalculator $calculator) {
50 $this->calculator = $calculator;
51 }
52
53 /**
54 * Initialize the endpoint
55 *
56 * @return void
57 */
58 public function init(): void {
59 add_action('rest_api_init', [$this, 'register_routes']);
60 }
61
62 /**
63 * Register REST API routes
64 *
65 * @return void
66 */
67 public function register_routes(): void {
68 // Calculate SEO score endpoint
69 register_rest_route('thinkrank/v1', '/seo-score/calculate', [
70 'methods' => 'POST',
71 'callback' => [$this, 'calculate_score'],
72 'permission_callback' => [$this, 'check_permissions'],
73 'args' => [
74 'post_id' => [
75 'required' => true,
76 'type' => 'integer',
77 'validate_callback' => [$this, 'validate_post_id'],
78 ],
79 'target_keyword' => [
80 'required' => false,
81 'type' => 'string',
82 'sanitize_callback' => 'sanitize_text_field',
83 ],
84 'save_score' => [
85 'required' => false,
86 'type' => 'boolean',
87 'default' => true,
88 ],
89 'live_content' => [
90 'required' => false,
91 'type' => 'string',
92 'sanitize_callback' => 'wp_kses_post',
93 ],
94 'readability_score' => [
95 'required' => false,
96 'type' => 'string',
97 'sanitize_callback' => 'sanitize_text_field',
98 ],
99 'content_quality' => [
100 'required' => false,
101 'type' => 'string',
102 'sanitize_callback' => 'sanitize_text_field',
103 ],
104 ],
105 ]);
106
107 // Get existing SEO score endpoint
108 register_rest_route('thinkrank/v1', '/seo-score/get', [
109 'methods' => 'GET',
110 'callback' => [$this, 'get_existing_score'],
111 'permission_callback' => [$this, 'check_permissions'],
112 'args' => [
113 'post_id' => [
114 'required' => true,
115 'type' => 'integer',
116 'validate_callback' => [$this, 'validate_post_id'],
117 ],
118 ],
119 ]);
120
121 // Get score history endpoint
122 register_rest_route('thinkrank/v1', '/seo-score/history', [
123 'methods' => 'GET',
124 'callback' => [$this, 'get_score_history'],
125 'permission_callback' => [$this, 'check_permissions'],
126 'args' => [
127 'post_id' => [
128 'required' => true,
129 'type' => 'integer',
130 'validate_callback' => [$this, 'validate_post_id'],
131 ],
132 'limit' => [
133 'required' => false,
134 'type' => 'integer',
135 'default' => 10,
136 'minimum' => 1,
137 'maximum' => 50,
138 ],
139 ],
140 ]);
141
142 // Get latest score endpoint
143 register_rest_route('thinkrank/v1', '/seo-score/latest', [
144 'methods' => 'GET',
145 'callback' => [$this, 'get_latest_score'],
146 'permission_callback' => [$this, 'check_permissions'],
147 'args' => [
148 'post_id' => [
149 'required' => true,
150 'type' => 'integer',
151 'validate_callback' => [$this, 'validate_post_id'],
152 ],
153 ],
154 ]);
155 }
156
157 /**
158 * Calculate SEO score for a post
159 *
160 * @param WP_REST_Request $request Request object
161 * @return WP_REST_Response|WP_Error Response object
162 */
163 public function calculate_score(WP_REST_Request $request) {
164 try {
165 $post_id = $request->get_param('post_id');
166 $target_keyword = $request->get_param('target_keyword') ?? '';
167 $save_score = $request->get_param('save_score') ?? true;
168 $live_content = $request->get_param('live_content') ?? '';
169 $readability_score = $request->get_param('readability_score') ?? null;
170 $content_quality = $request->get_param('content_quality') ?? null;
171
172 // Analyze content - use live content if provided, otherwise saved content
173 if (!empty($live_content)) {
174 $content_data = $this->calculator->analyze_live_content($live_content, $post_id);
175 } else {
176 $content_data = $this->calculator->analyze_post_content($post_id);
177 }
178
179 if (empty($content_data)) {
180 return new WP_Error(
181 'post_not_found',
182 'Post not found or has no content',
183 ['status' => 404]
184 );
185 }
186
187 // Get post metadata
188 $metadata = [
189 'title' => get_post_meta($post_id, '_thinkrank_seo_title', true) ?: $content_data['title'],
190 'description' => get_post_meta($post_id, '_thinkrank_meta_description', true) ?: '',
191 ];
192
193 // Calculate score
194 $score_data = $this->calculator->calculate_score(
195 $content_data,
196 $metadata,
197 ['target_keyword' => $target_keyword]
198 );
199
200 // Add readability_score and content_quality from frontend if provided
201 if (!empty($readability_score)) {
202 $score_data['readability_score'] = $readability_score;
203 }
204 if (!empty($content_quality)) {
205 $score_data['content_quality'] = $content_quality;
206 }
207
208 // Save score if requested
209 if ($save_score) {
210 $user_id = get_current_user_id();
211 $score_id = $this->calculator->save_score($post_id, $user_id, $score_data);
212
213 if ($score_id) {
214 $score_data['score_id'] = $score_id;
215 }
216 }
217
218 return new WP_REST_Response([
219 'success' => true,
220 'data' => $score_data,
221 ], 200);
222
223 } catch (\Exception $e) {
224 return new WP_Error(
225 'calculation_failed',
226 'Failed to calculate SEO score: ' . $e->getMessage(),
227 ['status' => 500]
228 );
229 }
230 }
231
232 /**
233 * Get existing SEO score for a post
234 *
235 * @param WP_REST_Request $request Request object
236 * @return WP_REST_Response|WP_Error Response object
237 */
238 public function get_existing_score(WP_REST_Request $request) {
239 try {
240 $post_id = $request->get_param('post_id');
241
242 // Get existing score data from database
243 $existing_data = $this->calculator->get_existing_score_data($post_id);
244
245 if ($existing_data) {
246 return new WP_REST_Response([
247 'success' => true,
248 'data' => $existing_data,
249 'message' => __('Existing SEO score retrieved successfully', 'thinkrank')
250 ], 200);
251 } else {
252 return new WP_REST_Response([
253 'success' => false,
254 'data' => null,
255 'message' => __('No existing SEO analysis found', 'thinkrank')
256 ], 200); // 200 because it's not an error, just no data
257 }
258
259 } catch (\Exception $e) {
260 return new WP_Error('get_score_error', $e->getMessage(), ['status' => 500]);
261 }
262 }
263
264 /**
265 * Get score history for a post
266 *
267 * @param WP_REST_Request $request Request object
268 * @return WP_REST_Response|WP_Error Response object
269 */
270 public function get_score_history(WP_REST_Request $request) {
271 try {
272 $post_id = $request->get_param('post_id');
273 $limit = $request->get_param('limit') ?? 10;
274
275 $history = $this->calculator->get_score_history($post_id, $limit);
276
277 return new WP_REST_Response([
278 'success' => true,
279 'data' => $history,
280 ], 200);
281
282 } catch (\Exception $e) {
283 return new WP_Error(
284 'history_failed',
285 'Failed to retrieve score history: ' . $e->getMessage(),
286 ['status' => 500]
287 );
288 }
289 }
290
291 /**
292 * Get latest score for a post
293 *
294 * @param WP_REST_Request $request Request object
295 * @return WP_REST_Response|WP_Error Response object
296 */
297 public function get_latest_score(WP_REST_Request $request) {
298 try {
299 $post_id = $request->get_param('post_id');
300
301 $latest_score = $this->calculator->get_latest_score($post_id);
302
303 return new WP_REST_Response([
304 'success' => true,
305 'data' => $latest_score,
306 ], 200);
307
308 } catch (\Exception $e) {
309 return new WP_Error(
310 'latest_failed',
311 'Failed to retrieve latest score: ' . $e->getMessage(),
312 ['status' => 500]
313 );
314 }
315 }
316
317 /**
318 * Check permissions for API access
319 *
320 * @param WP_REST_Request $request Request object
321 * @return bool True if user has permission
322 */
323 public function check_permissions(WP_REST_Request $request): bool {
324 // Check if user is logged in
325 if (!is_user_logged_in()) {
326 return false;
327 }
328
329 // Check if user can edit posts
330 if (!current_user_can('edit_posts')) {
331 return false;
332 }
333
334 // For specific post operations, check if user can edit the specific post
335 $post_id = $request->get_param('post_id');
336 if ($post_id && !current_user_can('edit_post', $post_id)) {
337 return false;
338 }
339
340 return true;
341 }
342
343 /**
344 * Validate post ID parameter
345 *
346 * @param mixed $value Parameter value
347 * @param WP_REST_Request $request Request object
348 * @param string $param Parameter name
349 * @return bool True if valid
350 */
351 public function validate_post_id($value, WP_REST_Request $request, string $param): bool {
352 if (!is_numeric($value) || $value <= 0) {
353 return false;
354 }
355
356 $post = get_post((int) $value);
357 return $post !== null;
358 }
359 }
360