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-global-seo-endpoint.php

class-global-seo-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-global-seo-endpoint.php

583 lines 19.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Global SEO API Endpoints Class
5 *
6 * REST API endpoints for managing global SEO settings across different WordPress post types.
7 * Provides functionality to save and retrieve SEO settings including title formats,
8 * meta descriptions, schema types, and article types for all registered post types.
9 *
10 * @package ThinkRank
11 * @subpackage API
12 * @since 1.0.0
13 */
14
15 declare(strict_types=1);
16
17 namespace ThinkRank\API;
18
19 use WP_REST_Controller;
20 use WP_REST_Request;
21 use WP_REST_Response;
22 use WP_Error;
23
24 /**
25 * Global SEO API Endpoints Class
26 *
27 * Provides REST API endpoints for managing global SEO settings for different post types
28 * including title formats, meta descriptions, schema types, and article types.
29 *
30 * @since 1.0.0
31 */
32 class Global_SEO_Endpoint extends WP_REST_Controller {
33
34 /**
35 * API namespace
36 *
37 * @since 1.0.0
38 * @var string
39 */
40 protected $namespace = 'thinkrank/v1';
41
42 /**
43 * API resource base
44 *
45 * @since 1.0.0
46 * @var string
47 */
48 protected $rest_base = 'global-seo';
49
50 /**
51 * WordPress option name for storing global SEO settings
52 *
53 * @since 1.0.0
54 * @var string
55 */
56 private const OPTION_NAME = 'thinkrank_global_seo_settings';
57
58 /**
59 * Default settings structure for post types
60 *
61 * @since 1.0.0
62 * @var array
63 */
64 private const DEFAULT_SETTINGS = [
65 'title' => '%title% %sep% %sitename%',
66 'description' => '%excerpt%',
67 'schema_type' => 'WebPage',
68 'article_type' => '',
69 'media_type' => '',
70 'link_suggestions' => true,
71 'robots_meta' => [
72 'index' => true,
73 'noindex' => false,
74 'nofollow' => false,
75 'noarchive' => false,
76 'noimageindex' => false,
77 'nosnippet' => false
78 ],
79 'robots_meta_enabled' => false,
80 'advanced_robots_meta' => [
81 'snippet_enabled' => true,
82 'max_snippet' => -1,
83 'video_preview_enabled' => true,
84 'max_video_preview' => -1,
85 'image_preview_enabled' => true,
86 'max_image_preview' => 'large'
87 ]
88 ];
89
90 /**
91 * Register API routes
92 *
93 * @since 1.0.0
94 */
95 public function register_routes(): void {
96 // Get/Save global SEO settings
97 register_rest_route(
98 $this->namespace,
99 '/' . $this->rest_base . '/settings',
100 [
101 [
102 'methods' => 'GET',
103 'callback' => [$this, 'get_settings'],
104 'permission_callback' => [$this, 'check_read_permissions'],
105 'args' => $this->get_settings_query_args()
106 ],
107 [
108 'methods' => 'POST',
109 'callback' => [$this, 'save_settings'],
110 'permission_callback' => [$this, 'check_manage_permissions'],
111 'args' => $this->get_save_settings_args()
112 ]
113 ]
114 );
115
116 // Get all global SEO settings (all post types)
117 register_rest_route(
118 $this->namespace,
119 '/' . $this->rest_base . '/settings/all',
120 [
121 [
122 'methods' => 'GET',
123 'callback' => [$this, 'get_all_settings'],
124 'permission_callback' => [$this, 'check_read_permissions']
125 ]
126 ]
127 );
128
129 // Reset settings for a specific post type
130 register_rest_route(
131 $this->namespace,
132 '/' . $this->rest_base . '/settings/reset',
133 [
134 [
135 'methods' => 'POST',
136 'callback' => [$this, 'reset_settings'],
137 'permission_callback' => [$this, 'check_manage_permissions'],
138 'args' => [
139 'post_type' => [
140 'required' => true,
141 'type' => 'string',
142 'description' => 'Post type to reset settings for',
143 'sanitize_callback' => 'sanitize_key'
144 ]
145 ]
146 ]
147 ]
148 );
149 }
150
151 /**
152 * Get global SEO settings for a specific post type
153 *
154 * @since 1.0.0
155 *
156 * @param WP_REST_Request $request Request object
157 * @return WP_REST_Response|WP_Error Response object or error
158 */
159 public function get_settings(WP_REST_Request $request) {
160 $post_type = $request->get_param('post_type');
161
162 // Validate post type
163 $validation = $this->validate_post_type($post_type);
164 if (is_wp_error($validation)) {
165 return $validation;
166 }
167
168 // Get all settings
169 $all_settings = get_option(self::OPTION_NAME, []);
170
171 // Get settings for specific post type or return defaults
172 $settings = $all_settings[$post_type] ?? $this->get_default_settings($post_type);
173
174 return new WP_REST_Response([
175 'success' => true,
176 'data' => $settings,
177 'post_type' => $post_type,
178 'message' => sprintf('Settings retrieved successfully for post type: %s', $post_type)
179 ], 200);
180 }
181
182 /**
183 * Save global SEO settings for a specific post type
184 *
185 * @since 1.0.0
186 *
187 * @param WP_REST_Request $request Request object
188 * @return WP_REST_Response|WP_Error Response object or error
189 */
190 public function save_settings(WP_REST_Request $request) {
191 $post_type = $request->get_param('post_type');
192 $settings = $request->get_param('settings');
193
194 // Validate post type
195 $validation = $this->validate_post_type($post_type);
196 if (is_wp_error($validation)) {
197 return $validation;
198 }
199
200 // Validate settings structure
201 if (empty($settings) || !is_array($settings)) {
202 return new WP_Error(
203 'invalid_settings',
204 'Settings must be provided as an array',
205 ['status' => 400]
206 );
207 }
208
209 // Sanitize settings
210 $sanitized_settings = $this->sanitize_settings($settings);
211
212 // Get all existing settings
213 $all_settings = get_option(self::OPTION_NAME, []);
214
215 // Update settings for this post type
216 $all_settings[$post_type] = $sanitized_settings;
217
218 // Save to database
219 $updated = update_option(self::OPTION_NAME, $all_settings);
220
221 if ($updated || $all_settings[$post_type] === $sanitized_settings) {
222 return new WP_REST_Response([
223 'success' => true,
224 'data' => $sanitized_settings,
225 'post_type' => $post_type,
226 'message' => sprintf('Settings saved successfully for post type: %s', $post_type)
227 ], 200);
228 }
229
230 return new WP_Error(
231 'save_failed',
232 'Failed to save settings',
233 ['status' => 500]
234 );
235 }
236
237 /**
238 * Get all global SEO settings for all post types
239 *
240 * @since 1.0.0
241 *
242 * @param WP_REST_Request $request Request object
243 * @return WP_REST_Response Response object
244 */
245 public function get_all_settings(WP_REST_Request $request): WP_REST_Response {
246 $all_settings = get_option(self::OPTION_NAME, []);
247
248 return new WP_REST_Response([
249 'success' => true,
250 'data' => $all_settings,
251 'count' => count($all_settings),
252 'message' => 'All global SEO settings retrieved successfully'
253 ], 200);
254 }
255
256 /**
257 * Reset settings for a specific post type to defaults
258 *
259 * @since 1.0.0
260 *
261 * @param WP_REST_Request $request Request object
262 * @return WP_REST_Response|WP_Error Response object or error
263 */
264 public function reset_settings(WP_REST_Request $request) {
265 $post_type = $request->get_param('post_type');
266
267 // Validate post type
268 $validation = $this->validate_post_type($post_type);
269 if (is_wp_error($validation)) {
270 return $validation;
271 }
272
273 // Get all settings
274 $all_settings = get_option(self::OPTION_NAME, []);
275
276 // Remove settings for this post type (will fall back to defaults)
277 unset($all_settings[$post_type]);
278
279 // Save updated settings
280 update_option(self::OPTION_NAME, $all_settings);
281
282 // Get default settings
283 $default_settings = $this->get_default_settings($post_type);
284
285 return new WP_REST_Response([
286 'success' => true,
287 'data' => $default_settings,
288 'post_type' => $post_type,
289 'message' => sprintf('Settings reset to defaults for post type: %s', $post_type)
290 ], 200);
291 }
292
293
294
295
296 /**
297 * Validate post type
298 *
299 * @since 1.0.0
300 *
301 * @param string $post_type Post type to validate
302 * @return true|WP_Error True if valid, WP_Error otherwise
303 */
304 private function validate_post_type(string $post_type) {
305 if (empty($post_type)) {
306 return new WP_Error(
307 'missing_post_type',
308 'Post type parameter is required',
309 ['status' => 400]
310 );
311 }
312
313 // Check if post type exists
314 if (!post_type_exists($post_type)) {
315 return new WP_Error(
316 'invalid_post_type',
317 sprintf('Post type "%s" does not exist', $post_type),
318 ['status' => 400]
319 );
320 }
321
322 // Check if post type is public
323 $post_type_object = get_post_type_object($post_type);
324 if (!$post_type_object || !$post_type_object->public) {
325 return new WP_Error(
326 'non_public_post_type',
327 sprintf('Post type "%s" is not public', $post_type),
328 ['status' => 400]
329 );
330 }
331
332 return true;
333 }
334
335 /**
336 * Get default settings for a post type
337 *
338 * @since 1.0.0
339 *
340 * @param string $post_type Post type
341 * @return array Default settings
342 */
343 private function get_default_settings(string $post_type): array {
344 $defaults = self::DEFAULT_SETTINGS;
345
346 // Customize defaults based on post type
347 switch ($post_type) {
348 case 'post':
349 $defaults['schema_type'] = 'Article';
350 $defaults['article_type'] = 'BlogPosting';
351 $defaults['media_type'] = '';
352 $defaults['link_suggestions'] = true;
353 break;
354
355 case 'page':
356 $defaults['schema_type'] = 'WebPage';
357 $defaults['article_type'] = '';
358 $defaults['media_type'] = '';
359 $defaults['link_suggestions'] = true;
360 break;
361
362 case 'attachment':
363 $defaults['schema_type'] = 'Media';
364 $defaults['article_type'] = '';
365 $defaults['media_type'] = 'ImageObject';
366 $defaults['title'] = '%title% %sep% %sitename%';
367 $defaults['description'] = '%caption%';
368 break;
369
370 case 'product':
371 $defaults['schema_type'] = 'Product';
372 $defaults['article_type'] = '';
373 $defaults['media_type'] = '';
374 $defaults['link_suggestions'] = false;
375 break;
376
377 default:
378 // For custom post types, use generic defaults
379 $defaults['schema_type'] = 'WebPage';
380 $defaults['article_type'] = '';
381 $defaults['media_type'] = '';
382 $defaults['link_suggestions'] = true;
383 break;
384 }
385
386 return $defaults;
387 }
388
389 /**
390 * Sanitize settings array
391 *
392 * @since 1.0.0
393 *
394 * @param array $settings Settings to sanitize
395 * @return array Sanitized settings
396 */
397 private function sanitize_settings(array $settings): array {
398 $sanitized = [];
399
400 // Sanitize title
401 if (isset($settings['title'])) {
402 $sanitized['title'] = sanitize_text_field($settings['title']);
403 }
404
405 // Sanitize description
406 if (isset($settings['description'])) {
407 $sanitized['description'] = sanitize_text_field($settings['description']);
408 }
409
410 // Sanitize schema_type
411 if (isset($settings['schema_type'])) {
412 $sanitized['schema_type'] = sanitize_text_field($settings['schema_type']);
413 }
414
415 // Sanitize article_type
416 if (isset($settings['article_type'])) {
417 $sanitized['article_type'] = sanitize_text_field($settings['article_type']);
418 }
419
420 // Sanitize media_type
421 if (isset($settings['media_type'])) {
422 $sanitized['media_type'] = sanitize_text_field($settings['media_type']);
423 }
424
425 // Sanitize link_suggestions
426 if (isset($settings['link_suggestions'])) {
427 $sanitized['link_suggestions'] = (bool) $settings['link_suggestions'];
428 }
429 // Sanitize robots_meta
430 if (isset($settings['robots_meta']) && is_array($settings['robots_meta'])) {
431 $sanitized['robots_meta'] = [
432 'index' => isset($settings['robots_meta']['index']) ? (bool) $settings['robots_meta']['index'] : true,
433 'noindex' => isset($settings['robots_meta']['noindex']) ? (bool) $settings['robots_meta']['noindex'] : false,
434 'nofollow' => isset($settings['robots_meta']['nofollow']) ? (bool) $settings['robots_meta']['nofollow'] : false,
435 'noarchive' => isset($settings['robots_meta']['noarchive']) ? (bool) $settings['robots_meta']['noarchive'] : false,
436 'noimageindex' => isset($settings['robots_meta']['noimageindex']) ? (bool) $settings['robots_meta']['noimageindex'] : false,
437 'nosnippet' => isset($settings['robots_meta']['nosnippet']) ? (bool) $settings['robots_meta']['nosnippet'] : false,
438 ];
439 } else {
440 // Ensure defaults if not provided or invalid
441 $sanitized['robots_meta'] = [
442 'index' => true,
443 'noindex' => false,
444 'nofollow' => false,
445 'noarchive' => false,
446 'noimageindex' => false,
447 'nosnippet' => false,
448 ];
449 }
450
451 // Sanitize robots_meta_enabled
452 if (isset($settings['robots_meta_enabled'])) {
453 $sanitized['robots_meta_enabled'] = (bool) $settings['robots_meta_enabled'];
454 } else {
455 $sanitized['robots_meta_enabled'] = false;
456 }
457
458 // Sanitize advanced_robots_meta
459 if (isset($settings['advanced_robots_meta']) && is_array($settings['advanced_robots_meta'])) {
460 $sanitized['advanced_robots_meta'] = [
461 'snippet_enabled' => isset($settings['advanced_robots_meta']['snippet_enabled']) ? (bool) $settings['advanced_robots_meta']['snippet_enabled'] : true,
462 'max_snippet' => isset($settings['advanced_robots_meta']['max_snippet']) ? (int) $settings['advanced_robots_meta']['max_snippet'] : -1,
463 'video_preview_enabled' => isset($settings['advanced_robots_meta']['video_preview_enabled']) ? (bool) $settings['advanced_robots_meta']['video_preview_enabled'] : true,
464 'max_video_preview' => isset($settings['advanced_robots_meta']['max_video_preview']) ? (int) $settings['advanced_robots_meta']['max_video_preview'] : -1,
465 'image_preview_enabled' => isset($settings['advanced_robots_meta']['image_preview_enabled']) ? (bool) $settings['advanced_robots_meta']['image_preview_enabled'] : true,
466 'max_image_preview' => isset($settings['advanced_robots_meta']['max_image_preview']) ? sanitize_text_field($settings['advanced_robots_meta']['max_image_preview']) : 'large',
467 ];
468 } else {
469 $sanitized['advanced_robots_meta'] = [
470 'snippet_enabled' => true,
471 'max_snippet' => -1,
472 'video_preview_enabled' => true,
473 'max_video_preview' => -1,
474 'image_preview_enabled' => true,
475 'max_image_preview' => 'large'
476 ];
477 }
478
479 return $sanitized;
480 }
481
482 /**
483 * Get query arguments for GET settings endpoint
484 *
485 * @since 1.0.0
486 *
487 * @return array Arguments array
488 */
489 private function get_settings_query_args(): array {
490 return [
491 'post_type' => [
492 'required' => true,
493 'type' => 'string',
494 'description' => 'Post type to retrieve settings for',
495 'sanitize_callback' => 'sanitize_key'
496 ]
497 ];
498 }
499
500 /**
501 * Get arguments for POST save settings endpoint
502 *
503 * @since 1.0.0
504 *
505 * @return array Arguments array
506 */
507 private function get_save_settings_args(): array {
508 return [
509 'post_type' => [
510 'required' => true,
511 'type' => 'string',
512 'description' => 'Post type to save settings for',
513 'sanitize_callback' => 'sanitize_key'
514 ],
515 'settings' => [
516 'required' => true,
517 'type' => 'object',
518 'description' => 'Settings object containing title, description, schema_type, article_type, and media_type',
519 'properties' => [
520 'title' => [
521 'type' => 'string',
522 'description' => 'Title format with variables like %title%, %sitename%, %sep%'
523 ],
524 'description' => [
525 'type' => 'string',
526 'description' => 'Description format with variables like %excerpt%'
527 ],
528 'schema_type' => [
529 'type' => 'string',
530 'description' => 'Schema.org type (e.g., Article, WebPage, Media, Product)'
531 ],
532 'article_type' => [
533 'type' => 'string',
534 'description' => 'Article type (e.g., BlogPosting, NewsArticle) - used when schema_type is Article'
535 ],
536 'media_type' => [
537 'type' => 'string',
538 'description' => 'Media type (e.g., ImageObject, VideoObject) - used when schema_type is Media'
539 ],
540 'link_suggestions' => [
541 'type' => 'boolean',
542 'description' => 'Enable link suggestions and pillar content feature'
543 ],
544 'robots_meta' => [
545 'type' => 'object',
546 'description' => 'Robots meta settings',
547 'properties' => [
548 'index' => ['type' => 'boolean'],
549 'noindex' => ['type' => 'boolean'],
550 'nofollow' => ['type' => 'boolean'],
551 'noarchive' => ['type' => 'boolean'],
552 'noimageindex' => ['type' => 'boolean'],
553 'nosnippet' => ['type' => 'boolean']
554 ]
555 ]
556 ]
557 ]
558 ];
559 }
560
561 /**
562 * Check read permissions
563 *
564 * @since 1.0.0
565 *
566 * @return bool True if user has read permissions
567 */
568 public function check_read_permissions(): bool {
569 return current_user_can('edit_posts');
570 }
571
572 /**
573 * Check manage permissions
574 *
575 * @since 1.0.0
576 *
577 * @return bool True if user has manage permissions
578 */
579 public function check_manage_permissions(): bool {
580 return current_user_can('manage_options');
581 }
582 }
583