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-schema-endpoint.php

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

1,933 lines 65.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Schema API Endpoints Class
4 *
5 * REST API endpoints for schema markup generation, validation, and management.
6 * Provides comprehensive API access to Schema Management System functionality
7 * with proper authentication, validation, and error handling.
8 *
9 * @package ThinkRank
10 * @subpackage API
11 * @since 1.0.0
12 */
13
14 declare(strict_types=1);
15
16 namespace ThinkRank\API;
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 use ThinkRank\SEO\Schema_Management_System;
24 use ThinkRank\SEO\Schema_Input_Validator;
25 use ThinkRank\API\Traits\Rate_Limiter;
26 use WP_REST_Controller;
27 use WP_REST_Request;
28 use WP_REST_Response;
29 use WP_Error;
30
31 // Load Rate Limiter trait
32 require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-rate-limiter.php';
33
34 /**
35 * Schema API Endpoints Class
36 *
37 * Provides REST API endpoints for schema markup operations including
38 * generation, validation, deployment, and performance tracking with
39 * proper authentication and comprehensive error handling.
40 *
41 * @since 1.0.0
42 */
43 class Schema_Endpoint extends WP_REST_Controller {
44
45 use Rate_Limiter;
46
47 /**
48 * Schema Management System instance
49 *
50 * @since 1.0.0
51 * @var Schema_Management_System
52 */
53 private Schema_Management_System $schema_manager;
54
55 /**
56 * Schema Input Validator instance
57 *
58 * @since 1.0.0
59 * @var Schema_Input_Validator
60 */
61 private Schema_Input_Validator $input_validator;
62
63 /**
64 * API namespace
65 *
66 * @since 1.0.0
67 * @var string
68 */
69 protected $namespace = 'thinkrank/v1';
70
71 /**
72 * API resource base
73 *
74 * @since 1.0.0
75 * @var string
76 */
77 protected $rest_base = 'schema';
78
79 /**
80 * Constructor
81 *
82 * @since 1.0.0
83 */
84 public function __construct() {
85 $this->schema_manager = new Schema_Management_System();
86 $this->input_validator = new Schema_Input_Validator();
87 }
88
89 /**
90 * Register API routes
91 *
92 * @since 1.0.0
93 */
94 public function register_routes(): void {
95 // Generate schema markup
96 register_rest_route(
97 $this->namespace,
98 '/' . $this->rest_base . '/generate',
99 [
100 [
101 'methods' => 'POST',
102 'callback' => [$this, 'generate_schema'],
103 'permission_callback' => [$this, 'check_generate_permissions'],
104 'args' => $this->get_generate_schema_args()
105 ]
106 ]
107 );
108
109 // Validate schema markup
110 register_rest_route(
111 $this->namespace,
112 '/' . $this->rest_base . '/validate',
113 [
114 [
115 'methods' => 'POST',
116 'callback' => [$this, 'validate_schema'],
117 'permission_callback' => [$this, 'check_validate_permissions'],
118 'args' => $this->get_validate_schema_args()
119 ]
120 ]
121 );
122
123 // Deploy schema markup
124 register_rest_route(
125 $this->namespace,
126 '/' . $this->rest_base . '/deploy',
127 [
128 [
129 'methods' => 'POST',
130 'callback' => [$this, 'deploy_schema'],
131 'permission_callback' => [$this, 'check_deploy_permissions'],
132 'args' => $this->get_deploy_schema_args()
133 ]
134 ]
135 );
136
137 // Get schema types
138 register_rest_route(
139 $this->namespace,
140 '/' . $this->rest_base . '/types',
141 [
142 [
143 'methods' => 'GET',
144 'callback' => [$this, 'get_schema_types'],
145 'permission_callback' => [$this, 'check_read_permissions']
146 ]
147 ]
148 );
149
150 // Get deployed schemas
151 register_rest_route(
152 $this->namespace,
153 '/' . $this->rest_base . '/deployed',
154 [
155 [
156 'methods' => 'GET',
157 'callback' => [$this, 'get_deployed_schemas'],
158 'permission_callback' => [$this, 'check_read_permissions']
159 ]
160 ]
161 );
162
163 // Get schema for context
164 register_rest_route(
165 $this->namespace,
166 '/' . $this->rest_base . '/(?P<context_type>[a-zA-Z0-9_-]+)/(?P<context_id>\d+)',
167 [
168 [
169 'methods' => 'GET',
170 'callback' => [$this, 'get_context_schema'],
171 'permission_callback' => [$this, 'check_read_permissions'],
172 'args' => [
173 'context_type' => [
174 'required' => true,
175 'type' => 'string',
176 'enum' => ['site', 'post', 'page', 'product']
177 ],
178 'context_id' => [
179 'required' => true,
180 'type' => 'integer',
181 'minimum' => 1
182 ]
183 ]
184 ]
185 ]
186 );
187
188 // Optimize rich snippets
189 register_rest_route(
190 $this->namespace,
191 '/' . $this->rest_base . '/optimize',
192 [
193 [
194 'methods' => 'POST',
195 'callback' => [$this, 'optimize_rich_snippets'],
196 'permission_callback' => [$this, 'check_optimize_permissions'],
197 'args' => $this->get_optimize_schema_args()
198 ]
199 ]
200 );
201
202 // Track schema performance
203 register_rest_route(
204 $this->namespace,
205 '/' . $this->rest_base . '/performance/(?P<context_type>[a-zA-Z0-9_-]+)/(?P<context_id>\d+)',
206 [
207 [
208 'methods' => 'GET',
209 'callback' => [$this, 'get_schema_performance'],
210 'permission_callback' => [$this, 'check_read_permissions'],
211 'args' => [
212 'context_type' => [
213 'required' => true,
214 'type' => 'string',
215 'enum' => ['site', 'post', 'page', 'product']
216 ],
217 'context_id' => [
218 'required' => true,
219 'type' => 'integer',
220 'minimum' => 1
221 ]
222 ]
223 ]
224 ]
225 );
226
227 // Get schema preview
228 register_rest_route(
229 $this->namespace,
230 '/' . $this->rest_base . '/preview',
231 [
232 [
233 'methods' => 'POST',
234 'callback' => [$this, 'get_schema_preview'],
235 'permission_callback' => [$this, 'check_read_permissions'],
236 'args' => $this->get_preview_schema_args()
237 ]
238 ]
239 );
240
241 // Bulk operations
242 register_rest_route(
243 $this->namespace,
244 '/' . $this->rest_base . '/bulk',
245 [
246 [
247 'methods' => 'POST',
248 'callback' => [$this, 'bulk_operations'],
249 'permission_callback' => [$this, 'check_bulk_permissions'],
250 'args' => $this->get_bulk_operations_args()
251 ]
252 ]
253 );
254
255 // Schema settings management
256 register_rest_route(
257 $this->namespace,
258 '/' . $this->rest_base . '/settings',
259 [
260 [
261 'methods' => 'GET',
262 'callback' => [$this, 'get_settings'],
263 'permission_callback' => [$this, 'check_read_permissions']
264 ],
265 [
266 'methods' => 'POST',
267 'callback' => [$this, 'save_settings'],
268 'permission_callback' => [$this, 'check_manage_permissions'],
269 'args' => $this->get_settings_args()
270 ]
271 ]
272 );
273
274 // Import schema from URL
275 register_rest_route(
276 $this->namespace,
277 '/' . $this->rest_base . '/import',
278 [
279 [
280 'methods' => 'POST',
281 'callback' => [$this, 'import_schema_from_url'],
282 'permission_callback' => [$this, 'check_manage_permissions'],
283 'args' => [
284 'url' => [
285 'required' => true,
286 'type' => 'string',
287 'format' => 'uri'
288 ]
289 ]
290 ]
291 ]
292 );
293 }
294
295 /**
296 * Import schema from URL
297 *
298 * @since 1.0.0
299 *
300 * @param WP_REST_Request $request Request object
301 * @return WP_REST_Response|WP_Error Response object or error
302 */
303 public function import_schema_from_url(WP_REST_Request $request) {
304 try {
305 $url = esc_url_raw($request->get_param('url'));
306
307 if (empty($url)) {
308 return new WP_Error(
309 'invalid_url',
310 'A valid URL is required',
311 ['status' => 400]
312 );
313 }
314
315 // Fetch the URL
316 $response = wp_remote_get($url, [
317 'timeout' => 15,
318 'user-agent' => 'ThinkRank/1.0.0 (WordPress Schema Plugin)',
319 'sslverify' => false // Relaxed SSL verification for broader compatibility
320 ]);
321
322 if (is_wp_error($response)) {
323 return new WP_Error(
324 'fetch_failed',
325 'Failed to fetch data from URL: ' . $response->get_error_message(),
326 ['status' => 500]
327 );
328 }
329
330 $response_code = wp_remote_retrieve_response_code($response);
331 if ($response_code !== 200) {
332 return new WP_Error(
333 'fetch_error',
334 'Failed to fetch data from URL (HTTP ' . $response_code . ')',
335 ['status' => 400]
336 );
337 }
338
339 $body = wp_remote_retrieve_body($response);
340
341 if (empty($body)) {
342 return new WP_Error(
343 'empty_response',
344 'Returned content is empty',
345 ['status' => 400]
346 );
347 }
348
349 // Suppress DOM errors for malformed HTML
350 libxml_use_internal_errors(true);
351
352 $dom = new \DOMDocument();
353 // Using loadHTML with minimal options to handle various encodings/formats better
354 $dom->loadHTML(mb_convert_encoding($body, 'HTML-ENTITIES', 'UTF-8'), LIBXML_NOERROR | LIBXML_NOWARNING);
355
356 libxml_clear_errors();
357
358 $xpath = new \DOMXPath($dom);
359 $scripts = $xpath->query('//script[@type="application/ld+json"]');
360
361 $found_schemas = [];
362
363 if ($scripts->length > 0) {
364 foreach ($scripts as $script) {
365 $json = trim($script->nodeValue);
366 $data = json_decode($json, true);
367
368 if (json_last_error() === JSON_ERROR_NONE && !empty($data)) {
369 // Strictly set @context to https://schema.org
370 $data['@context'] = 'https://schema.org';
371 $found_schemas[] = $data;
372 }
373 }
374 }
375
376 if (empty($found_schemas)) {
377 return new WP_Error(
378 'no_schema_found',
379 'No valid JSON-LD schema markup found on this page',
380 ['status' => 404]
381 );
382 }
383
384 return new WP_REST_Response([
385 'success' => true,
386 'data' => $found_schemas[0],
387 'all_found' => $found_schemas,
388 'message' => 'Schema imported successfully'
389 ], 200);
390
391 } catch (\Exception $e) {
392 return new WP_Error(
393 'import_failed',
394 'Schema import failed: ' . $e->getMessage(),
395 ['status' => 500]
396 );
397 }
398 }
399
400 /**
401 * Generate schema markup
402 *
403 * @since 1.0.0
404 *
405 * @param WP_REST_Request $request Request object
406 * @return WP_REST_Response|WP_Error Response object or error
407 */
408 public function generate_schema(WP_REST_Request $request) {
409 try {
410 $user_id = get_current_user_id();
411
412 // SECURITY: Check rate limits first
413 $rate_limit_check = $this->check_rate_limit('generate_schema', $user_id);
414 if (is_wp_error($rate_limit_check)) {
415 return $rate_limit_check;
416 }
417
418 // SECURITY: Validate user permissions and rate limiting
419 $permission_check = $this->input_validator->validate_user_permissions('generate', $user_id);
420 if (!$permission_check['valid']) {
421 return new WP_Error(
422 'permission_denied',
423 implode(', ', $permission_check['errors']),
424 ['status' => 403]
425 );
426 }
427
428 // SECURITY: Validate and sanitize context parameters with ownership checks
429 $context_type = $request->get_param('context_type');
430 $context_id = $request->get_param('context_id');
431 $context_validation = $this->input_validator->validate_context_parameters($context_type, $context_id, $user_id);
432
433 if (!$context_validation['valid']) {
434 return new WP_Error(
435 'invalid_context',
436 implode(', ', $context_validation['errors']),
437 ['status' => 400]
438 );
439 }
440
441 $context_type = $context_validation['sanitized_data']['context_type'];
442 $context_id = $context_validation['sanitized_data']['context_id'];
443
444 // SECURITY: Validate and sanitize schema types
445 $schema_types = $request->get_param('schema_types') ?? [];
446 if (empty($schema_types) || !is_array($schema_types)) {
447 return new WP_Error(
448 'missing_schema_types',
449 'Schema types array is required',
450 ['status' => 400]
451 );
452 }
453
454 // Sanitize schema types
455 $sanitized_schema_types = [];
456 foreach ($schema_types as $type) {
457 $sanitized_type = sanitize_text_field($type);
458 if (!empty($sanitized_type)) {
459 $sanitized_schema_types[] = $sanitized_type;
460 }
461 }
462
463 if (empty($sanitized_schema_types)) {
464 return new WP_Error(
465 'invalid_schema_types',
466 'No valid schema types provided',
467 ['status' => 400]
468 );
469 }
470
471 // SECURITY: Sanitize options
472 $options = $this->input_validator->sanitize_options($request->get_param('options') ?? []);
473
474 // SECURITY: Sanitize content_data if provided
475 $content_data = $request->get_param('content_data');
476 if ($content_data && is_array($content_data)) {
477 $content_data = [
478 'title' => isset($content_data['title']) ? sanitize_text_field($content_data['title']) : '',
479 'description' => isset($content_data['description']) ? sanitize_textarea_field($content_data['description']) : '',
480 'content' => isset($content_data['content']) ? wp_kses_post($content_data['content']) : '',
481 'word_count' => isset($content_data['word_count']) ? (int) $content_data['word_count'] : 0,
482 'focus_keyword' => isset($content_data['focus_keyword']) ? sanitize_text_field($content_data['focus_keyword']) : '',
483 'post_type' => isset($content_data['post_type']) ? sanitize_text_field($content_data['post_type']) : '',
484 'post_url' => isset($content_data['post_url']) ? esc_url_raw($content_data['post_url']) : ''
485 ];
486
487 // Add content_data to options so schema manager can use it
488 $options['content_data'] = $content_data;
489 }
490
491 // SECURITY: Sanitize schema_form_data if provided
492 $schema_form_data = $request->get_param('schema_form_data');
493 if ($schema_form_data && is_array($schema_form_data)) {
494 // Sanitize all form fields
495 $sanitized_form_data = [];
496 foreach ($schema_form_data as $key => $value) {
497 if (is_string($value)) {
498 $sanitized_form_data[sanitize_key($key)] = sanitize_text_field($value);
499 } elseif (is_array($value)) {
500 // Handle array values (like features, steps, etc.)
501 $sanitized_form_data[sanitize_key($key)] = array_map('sanitize_text_field', $value);
502 } elseif (is_numeric($value)) {
503 $sanitized_form_data[sanitize_key($key)] = floatval($value);
504 }
505 }
506
507 // Add schema_form_data to options so schema manager can use it
508 $options['schema_form_data'] = $sanitized_form_data;
509 }
510
511 // Generate schema markup with sanitized inputs
512 $generation_results = $this->schema_manager->generate_schema_markup(
513 $context_type,
514 $context_id,
515 $sanitized_schema_types,
516 $options
517 );
518
519 return new WP_REST_Response([
520 'success' => true,
521 'data' => $generation_results,
522 'message' => 'Schema markup generated successfully'
523 ], 200);
524
525 } catch (\Exception $e) {
526 return new WP_Error(
527 'generation_failed',
528 'Schema generation failed: ' . $e->getMessage(),
529 ['status' => 500]
530 );
531 }
532 }
533
534 /**
535 * Validate schema markup
536 *
537 * @since 1.0.0
538 *
539 * @param WP_REST_Request $request Request object
540 * @return WP_REST_Response|WP_Error Response object or error
541 */
542 public function validate_schema(WP_REST_Request $request) {
543 try {
544 $user_id = get_current_user_id();
545
546 // SECURITY: Validate user permissions and rate limiting
547 $permission_check = $this->input_validator->validate_user_permissions('validate', $user_id);
548 if (!$permission_check['valid']) {
549 return new WP_Error(
550 'permission_denied',
551 implode(', ', $permission_check['errors']),
552 ['status' => 403]
553 );
554 }
555
556 $schema_data = $request->get_param('schema_data');
557 $schema_type = $request->get_param('schema_type');
558 $options = $request->get_param('options') ?? [];
559
560 // SECURITY: Validate input parameters
561 if (empty($schema_data) || empty($schema_type)) {
562 return new WP_Error(
563 'missing_parameters',
564 'Schema data and type are required',
565 ['status' => 400]
566 );
567 }
568
569 // SECURITY: Sanitize schema type
570 $schema_type = sanitize_text_field($schema_type);
571
572 // SECURITY: Validate and sanitize schema data using input validator
573 if (!is_array($schema_data)) {
574 return new WP_Error(
575 'invalid_schema_data',
576 'Schema data must be an array/object',
577 ['status' => 400]
578 );
579 }
580
581 $input_validation = $this->input_validator->validate_schema_data($schema_data, $schema_type);
582 if (!$input_validation['valid']) {
583 return new WP_Error(
584 'schema_validation_failed',
585 'Schema data validation failed: ' . implode(', ', $input_validation['errors']),
586 [
587 'status' => 400,
588 'validation_errors' => $input_validation['errors'],
589 'validation_warnings' => $input_validation['warnings']
590 ]
591 );
592 }
593
594 // Use sanitized data for validation
595 $sanitized_schema_data = $input_validation['sanitized_data'];
596
597 // SECURITY: Sanitize options
598 $options = $this->input_validator->sanitize_options($options);
599
600 // Validate schema markup with sanitized data
601 $validation_results = $this->schema_manager->validate_schema_markup(
602 $sanitized_schema_data,
603 $schema_type,
604 $options
605 );
606
607 return new WP_REST_Response([
608 'success' => true,
609 'data' => $validation_results,
610 'message' => 'Schema validation completed'
611 ], 200);
612
613 } catch (\Exception $e) {
614 return new WP_Error(
615 'validation_failed',
616 'Schema validation failed: ' . $e->getMessage(),
617 ['status' => 500]
618 );
619 }
620 }
621
622 /**
623 * Deploy schema markup
624 *
625 * @since 1.0.0
626 *
627 * @param WP_REST_Request $request Request object
628 * @return WP_REST_Response|WP_Error Response object or error
629 */
630 public function deploy_schema(WP_REST_Request $request) {
631 try {
632 $user_id = get_current_user_id();
633
634 // SECURITY: Validate user permissions and rate limiting
635 $permission_check = $this->input_validator->validate_user_permissions('deploy', $user_id);
636 if (!$permission_check['valid']) {
637 return new WP_Error(
638 'permission_denied',
639 implode(', ', $permission_check['errors']),
640 ['status' => 403]
641 );
642 }
643
644 // SECURITY: Validate and sanitize context parameters with ownership checks
645 $context_type = $request->get_param('context_type');
646 $context_id = $request->get_param('context_id');
647 $context_validation = $this->input_validator->validate_context_parameters($context_type, $context_id, $user_id);
648
649 if (!$context_validation['valid']) {
650 return new WP_Error(
651 'invalid_context',
652 implode(', ', $context_validation['errors']),
653 ['status' => 400]
654 );
655 }
656
657 $context_type = $context_validation['sanitized_data']['context_type'];
658 $context_id = $context_validation['sanitized_data']['context_id'];
659
660 // SECURITY: Validate schema data
661 $schema_data = $request->get_param('schema_data');
662 if (empty($schema_data) || !is_array($schema_data)) {
663 return new WP_Error(
664 'invalid_schema_data',
665 'Valid schema data array is required',
666 ['status' => 400]
667 );
668 }
669
670 // SECURITY: Validate each schema in the data
671 $sanitized_schema_data = [];
672 foreach ($schema_data as $schema_key => $schema_content) {
673 $schema_key = sanitize_text_field($schema_key);
674
675 if (!is_array($schema_content)) {
676 return new WP_Error(
677 'invalid_schema_content',
678 "Schema content for {$schema_key} must be an array",
679 ['status' => 400]
680 );
681 }
682
683 // Ensure schema has required structure fields before validation
684 // Use @type from schema content if available, otherwise fall back to key
685 $schema_type = isset($schema_content['@type']) ? sanitize_text_field($schema_content['@type']) : $schema_key;
686
687 if (!isset($schema_content['@type'])) {
688 $schema_content['@type'] = $schema_type;
689 }
690 if (!isset($schema_content['@context'])) {
691 $schema_content['@context'] = 'https://schema.org';
692 }
693
694 // Validate using the actual schema type, not the key
695 $input_validation = $this->input_validator->validate_schema_data($schema_content, $schema_type);
696 if (!$input_validation['valid']) {
697 return new WP_Error(
698 'schema_validation_failed',
699 "Schema validation failed for {$schema_type}: " . implode(', ', $input_validation['errors']),
700 [
701 'status' => 400,
702 'schema_type' => $schema_type,
703 'validation_errors' => $input_validation['errors']
704 ]
705 );
706 }
707
708 // Store using the key (which may be unique like "Article-1")
709 $sanitized_schema_data[$schema_key] = $input_validation['sanitized_data'];
710 }
711
712 // SECURITY: Sanitize options
713 $options = $this->input_validator->sanitize_options($request->get_param('options') ?? []);
714
715 // Deploy schema markup with sanitized data
716 $deployment_results = $this->schema_manager->deploy_schema_markup(
717 $context_type,
718 $context_id,
719 $sanitized_schema_data,
720 $options
721 );
722
723 return new WP_REST_Response([
724 'success' => true,
725 'data' => $deployment_results,
726 'message' => 'Schema markup deployed successfully'
727 ], 200);
728
729 } catch (\Exception $e) {
730 return new WP_Error(
731 'deployment_failed',
732 'Schema deployment failed: ' . $e->getMessage(),
733 ['status' => 500]
734 );
735 }
736 }
737
738 /**
739 * Get available schema types
740 *
741 * @since 1.0.0
742 *
743 * @param WP_REST_Request $request Request object
744 * @return WP_REST_Response Response object
745 */
746 public function get_schema_types(WP_REST_Request $request): WP_REST_Response {
747 // Get context parameter to determine which schema types to return
748 $context = $request->get_param('context') ?? 'site';
749
750 // Site-level schema types only (post/page schemas handled by metabox)
751 $site_schema_types = [
752 'Organization' => [
753 'name' => 'Organization',
754 'description' => 'Company or organization information (site-wide)',
755 'context_types' => ['site'],
756 'priority' => 'high'
757 ],
758 'LocalBusiness' => [
759 'name' => 'LocalBusiness',
760 'description' => 'Local businesses and service providers (site-wide)',
761 'context_types' => ['site'],
762 'priority' => 'high'
763 ],
764 'Person' => [
765 'name' => 'Person',
766 'description' => 'Individual person or author information (site-wide)',
767 'context_types' => ['site'],
768 'priority' => 'medium'
769 ],
770 'WebSite' => [
771 'name' => 'WebSite',
772 'description' => 'Website-level information and search functionality',
773 'context_types' => ['site'],
774 'priority' => 'high'
775 ]
776 ];
777
778 // All schema types for metabox context
779 $all_schema_types = [
780 'Article' => [
781 'name' => 'Article',
782 'description' => 'News articles, blog posts, and editorial content',
783 'context_types' => ['post', 'page'],
784 'priority' => 'high'
785 ],
786 'BlogPosting' => [
787 'name' => 'BlogPosting',
788 'description' => 'Blog posts and personal articles',
789 'context_types' => ['post', 'page'],
790 'priority' => 'high'
791 ],
792 'TechnicalArticle' => [
793 'name' => 'TechnicalArticle',
794 'description' => 'Technical documentation and tutorials',
795 'context_types' => ['post', 'page'],
796 'priority' => 'high'
797 ],
798 'NewsArticle' => [
799 'name' => 'NewsArticle',
800 'description' => 'News articles and press releases',
801 'context_types' => ['post', 'page'],
802 'priority' => 'high'
803 ],
804 'ScholarlyArticle' => [
805 'name' => 'ScholarlyArticle',
806 'description' => 'Academic and research articles',
807 'context_types' => ['post', 'page'],
808 'priority' => 'high'
809 ],
810 'Report' => [
811 'name' => 'Report',
812 'description' => 'Reports and analytical content',
813 'context_types' => ['post', 'page'],
814 'priority' => 'medium'
815 ],
816 'HowTo' => [
817 'name' => 'HowTo',
818 'description' => 'Step-by-step instructions and tutorials',
819 'context_types' => ['post', 'page'],
820 'priority' => 'medium'
821 ],
822 'FAQPage' => [
823 'name' => 'FAQPage',
824 'description' => 'Frequently Asked Questions pages',
825 'context_types' => ['page', 'post'],
826 'priority' => 'high'
827 ],
828 'Event' => [
829 'name' => 'Event',
830 'description' => 'Events, conferences, and gatherings',
831 'context_types' => ['post', 'page'],
832 'priority' => 'medium'
833 ],
834 'Product' => [
835 'name' => 'Product',
836 'description' => 'Products for e-commerce and retail',
837 'context_types' => ['product', 'post', 'page'],
838 'priority' => 'critical'
839 ],
840 'SoftwareApplication' => [
841 'name' => 'SoftwareApplication',
842 'description' => 'Software applications and web apps',
843 'context_types' => ['post', 'page'],
844 'priority' => 'high'
845 ]
846 ] + $site_schema_types;
847
848 // Return appropriate schema types based on context
849 $schema_types = ($context === 'metabox') ? $all_schema_types : $site_schema_types;
850
851 return new WP_REST_Response([
852 'success' => true,
853 'data' => $schema_types,
854 'message' => 'Schema types retrieved successfully'
855 ], 200);
856 }
857
858 /**
859 * Get deployed schemas
860 *
861 * @since 1.0.0
862 *
863 * @param WP_REST_Request $request Request object
864 * @return WP_REST_Response|WP_Error Response object or error
865 */
866 public function get_deployed_schemas(WP_REST_Request $request) {
867 try {
868 $context_type = $request->get_param('context_type') ?? 'site';
869 $context_id = $request->get_param('context_id');
870
871 // Convert context_id to int if it's a valid numeric string, otherwise null
872 if ($context_id !== null && is_numeric($context_id)) {
873 $context_id = (int) $context_id;
874 } else {
875 $context_id = null;
876 }
877
878 $deployed_schemas = $this->schema_manager->get_deployed_schemas($context_type, $context_id);
879
880 return new WP_REST_Response([
881 'success' => true,
882 'data' => $deployed_schemas,
883 'message' => 'Deployed schemas retrieved successfully'
884 ], 200);
885
886 } catch (\Exception $e) {
887 return new WP_Error(
888 'deployed_schemas_failed',
889 'Failed to retrieve deployed schemas: ' . $e->getMessage(),
890 ['status' => 500]
891 );
892 }
893 }
894
895 /**
896 * Get schema for specific context
897 *
898 * @since 1.0.0
899 *
900 * @param WP_REST_Request $request Request object
901 * @return WP_REST_Response|WP_Error Response object or error
902 */
903 public function get_context_schema(WP_REST_Request $request) {
904 try {
905 $context_type = $request->get_param('context_type');
906 $context_id = (int) $request->get_param('context_id');
907
908 // Validate context
909 if (!$this->validate_context($context_type, $context_id)) {
910 return new WP_Error(
911 'invalid_context',
912 'Invalid context type or ID provided',
913 ['status' => 400]
914 );
915 }
916
917 // Get schema output data
918 $schema_data = $this->schema_manager->get_output_data($context_type, $context_id);
919
920 return new WP_REST_Response([
921 'success' => true,
922 'data' => $schema_data,
923 'message' => 'Context schema retrieved successfully'
924 ], 200);
925
926 } catch (\Exception $e) {
927 return new WP_Error(
928 'retrieval_failed',
929 'Schema retrieval failed: ' . $e->getMessage(),
930 ['status' => 500]
931 );
932 }
933 }
934
935 /**
936 * Optimize rich snippets
937 *
938 * @since 1.0.0
939 *
940 * @param WP_REST_Request $request Request object
941 * @return WP_REST_Response|WP_Error Response object or error
942 */
943 public function optimize_rich_snippets(WP_REST_Request $request) {
944 try {
945 $user_id = get_current_user_id();
946
947 // SECURITY: Validate user permissions and rate limiting
948 $permission_check = $this->input_validator->validate_user_permissions('optimize', $user_id);
949 if (!$permission_check['valid']) {
950 return new WP_Error(
951 'permission_denied',
952 implode(', ', $permission_check['errors']),
953 ['status' => 403]
954 );
955 }
956
957 $schema_data = $request->get_param('schema_data');
958 $schema_type = $request->get_param('schema_type');
959 $options = $request->get_param('options') ?? [];
960
961 // Validate input
962 if (empty($schema_data) || empty($schema_type)) {
963 return new WP_Error(
964 'missing_parameters',
965 'Schema data and type are required',
966 ['status' => 400]
967 );
968 }
969
970 // Optimize rich snippets
971 $optimization_results = $this->schema_manager->optimize_rich_snippets(
972 $schema_data,
973 $schema_type,
974 $options
975 );
976
977 return new WP_REST_Response([
978 'success' => true,
979 'data' => $optimization_results,
980 'message' => 'Rich snippets optimization completed'
981 ], 200);
982
983 } catch (\Exception $e) {
984 return new WP_Error(
985 'optimization_failed',
986 'Rich snippets optimization failed: ' . $e->getMessage(),
987 ['status' => 500]
988 );
989 }
990 }
991
992 /**
993 * Get schema performance data
994 *
995 * @since 1.0.0
996 *
997 * @param WP_REST_Request $request Request object
998 * @return WP_REST_Response|WP_Error Response object or error
999 */
1000 public function get_schema_performance(WP_REST_Request $request) {
1001 try {
1002 $context_type = $request->get_param('context_type');
1003 $context_id = (int) $request->get_param('context_id');
1004 $options = $request->get_param('options') ?? [];
1005
1006 // Validate context
1007 if (!$this->validate_context($context_type, $context_id)) {
1008 return new WP_Error(
1009 'invalid_context',
1010 'Invalid context type or ID provided',
1011 ['status' => 400]
1012 );
1013 }
1014
1015 // Track schema performance
1016 $performance_data = $this->schema_manager->track_schema_performance(
1017 $context_type,
1018 $context_id,
1019 $options
1020 );
1021
1022 return new WP_REST_Response([
1023 'success' => true,
1024 'data' => $performance_data,
1025 'message' => 'Schema performance data retrieved successfully'
1026 ], 200);
1027
1028 } catch (\Exception $e) {
1029 return new WP_Error(
1030 'performance_tracking_failed',
1031 'Schema performance tracking failed: ' . $e->getMessage(),
1032 ['status' => 500]
1033 );
1034 }
1035 }
1036
1037 /**
1038 * Get schema preview
1039 *
1040 * @since 1.0.0
1041 *
1042 * @param WP_REST_Request $request Request object
1043 * @return WP_REST_Response|WP_Error Response object or error
1044 */
1045 public function get_schema_preview(WP_REST_Request $request) {
1046 try {
1047 $schema_data = $request->get_param('schema_data');
1048 $schema_type = $request->get_param('schema_type');
1049
1050 // Validate input
1051 if (empty($schema_data) || empty($schema_type)) {
1052 return new WP_Error(
1053 'missing_parameters',
1054 'Schema data and type are required',
1055 ['status' => 400]
1056 );
1057 }
1058
1059 // Generate preview
1060 $preview_data = $this->generate_preview($schema_data, $schema_type);
1061
1062 return new WP_REST_Response([
1063 'success' => true,
1064 'data' => $preview_data,
1065 'message' => 'Schema preview generated successfully'
1066 ], 200);
1067
1068 } catch (\Exception $e) {
1069 return new WP_Error(
1070 'preview_failed',
1071 'Schema preview generation failed: ' . $e->getMessage(),
1072 ['status' => 500]
1073 );
1074 }
1075 }
1076
1077 /**
1078 * Bulk operations for schema management
1079 *
1080 * @since 1.0.0
1081 *
1082 * @param WP_REST_Request $request Request object
1083 * @return WP_REST_Response|WP_Error Response object or error
1084 */
1085 public function bulk_operations(WP_REST_Request $request) {
1086 try {
1087 $user_id = get_current_user_id();
1088
1089 // SECURITY: Validate user permissions and rate limiting
1090 $permission_check = $this->input_validator->validate_user_permissions('bulk_operations', $user_id);
1091 if (!$permission_check['valid']) {
1092 return new WP_Error(
1093 'permission_denied',
1094 implode(', ', $permission_check['errors']),
1095 ['status' => 403]
1096 );
1097 }
1098
1099 $operation = $request->get_param('operation');
1100 $items = $request->get_param('items') ?? [];
1101 $options = $request->get_param('options') ?? [];
1102
1103 // Validate input
1104 if (empty($operation) || empty($items)) {
1105 return new WP_Error(
1106 'missing_parameters',
1107 'Operation and items are required',
1108 ['status' => 400]
1109 );
1110 }
1111
1112 $results = [];
1113 $errors = [];
1114
1115 foreach ($items as $item) {
1116 try {
1117 switch ($operation) {
1118 case 'generate':
1119 $result = $this->schema_manager->generate_schema_markup(
1120 $item['context_type'],
1121 $item['context_id'],
1122 $item['schema_types'] ?? [],
1123 $options
1124 );
1125 break;
1126 case 'validate':
1127 $result = $this->schema_manager->validate_schema_markup(
1128 $item['schema_data'],
1129 $item['schema_type'],
1130 $options
1131 );
1132 break;
1133 case 'deploy':
1134 $result = $this->schema_manager->deploy_schema_markup(
1135 $item['context_type'],
1136 $item['context_id'],
1137 $item['schema_data'],
1138 $options
1139 );
1140 break;
1141 default:
1142 throw new \Exception("Unsupported operation: {$operation}");
1143 }
1144
1145 $results[] = [
1146 'item' => $item,
1147 'success' => true,
1148 'data' => $result
1149 ];
1150
1151 } catch (\Exception $e) {
1152 $errors[] = [
1153 'item' => $item,
1154 'error' => $e->getMessage()
1155 ];
1156 }
1157 }
1158
1159 return new WP_REST_Response([
1160 'success' => empty($errors),
1161 'data' => [
1162 'results' => $results,
1163 'errors' => $errors,
1164 'total_processed' => count($items),
1165 'successful' => count($results),
1166 'failed' => count($errors)
1167 ],
1168 'message' => "Bulk {$operation} operation completed"
1169 ], 200);
1170
1171 } catch (\Exception $e) {
1172 return new WP_Error(
1173 'bulk_operation_failed',
1174 'Bulk operation failed: ' . $e->getMessage(),
1175 ['status' => 500]
1176 );
1177 }
1178 }
1179
1180 /**
1181 * Permission callbacks
1182 */
1183
1184 /**
1185 * Check permissions for schema generation with CSRF protection
1186 *
1187 * @since 1.0.0
1188 *
1189 * @param WP_REST_Request $request Request object
1190 * @return bool Permission status
1191 */
1192 public function check_generate_permissions(WP_REST_Request $request): bool {
1193 // Check user capability
1194 if (!current_user_can('edit_posts')) {
1195 return false;
1196 }
1197
1198 // SECURITY: Verify nonce for CSRF protection
1199 return $this->verify_request_nonce($request);
1200 }
1201
1202 /**
1203 * Check permissions for schema validation with CSRF protection
1204 *
1205 * @since 1.0.0
1206 *
1207 * @param WP_REST_Request $request Request object
1208 * @return bool Permission status
1209 */
1210 public function check_validate_permissions(WP_REST_Request $request): bool {
1211 // Check user capability
1212 if (!current_user_can('edit_posts')) {
1213 return false;
1214 }
1215
1216 // SECURITY: Verify nonce for CSRF protection
1217 return $this->verify_request_nonce($request);
1218 }
1219
1220 /**
1221 * Check permissions for schema deployment with CSRF protection
1222 *
1223 * @since 1.0.0
1224 *
1225 * @param WP_REST_Request $request Request object
1226 * @return bool Permission status
1227 */
1228 public function check_deploy_permissions(WP_REST_Request $request): bool {
1229 // Check user capability
1230 if (!current_user_can('publish_posts')) {
1231 return false;
1232 }
1233
1234 // SECURITY: Verify nonce for CSRF protection
1235 return $this->verify_request_nonce($request);
1236 }
1237
1238 /**
1239 * Check permissions for reading schema data
1240 *
1241 * @since 1.0.0
1242 *
1243 * @param WP_REST_Request $request Request object
1244 * @return bool Permission status
1245 */
1246 public function check_read_permissions(WP_REST_Request $request): bool {
1247 // Read operations only require basic capability
1248 return current_user_can('read');
1249 }
1250
1251 /**
1252 * Check permissions for schema optimization with CSRF protection
1253 *
1254 * @since 1.0.0
1255 *
1256 * @param WP_REST_Request $request Request object
1257 * @return bool Permission status
1258 */
1259 public function check_optimize_permissions(WP_REST_Request $request): bool {
1260 // Check user capability
1261 if (!current_user_can('edit_posts')) {
1262 return false;
1263 }
1264
1265 // SECURITY: Verify nonce for CSRF protection
1266 return $this->verify_request_nonce($request);
1267 }
1268
1269 /**
1270 * Check permissions for bulk operations with CSRF protection
1271 *
1272 * @since 1.0.0
1273 *
1274 * @param WP_REST_Request $request Request object
1275 * @return bool Permission status
1276 */
1277 public function check_bulk_permissions(WP_REST_Request $request): bool {
1278 // Check user capability
1279 if (!current_user_can('manage_options')) {
1280 return false;
1281 }
1282
1283 // SECURITY: Verify nonce for CSRF protection
1284 return $this->verify_request_nonce($request);
1285 }
1286
1287 /**
1288 * Check permissions for managing schema settings with CSRF protection
1289 *
1290 * @since 1.0.0
1291 *
1292 * @param WP_REST_Request $request Request object
1293 * @return bool Permission status
1294 */
1295 public function check_manage_permissions(WP_REST_Request $request): bool {
1296 // Check user capability
1297 if (!current_user_can('manage_options')) {
1298 return false;
1299 }
1300
1301 // SECURITY: Verify nonce for CSRF protection (only for POST requests)
1302 if ($request->get_method() === 'POST') {
1303 return $this->verify_request_nonce($request);
1304 }
1305
1306 return true;
1307 }
1308
1309 /**
1310 * Helper methods
1311 */
1312
1313 /**
1314 * Verify request nonce for CSRF protection
1315 *
1316 * @since 1.0.0
1317 *
1318 * @param WP_REST_Request $request Request object
1319 * @return bool Whether nonce is valid
1320 */
1321 private function verify_request_nonce(WP_REST_Request $request): bool {
1322 // Get nonce from header (preferred method for REST API)
1323 $nonce = $request->get_header('X-WP-Nonce');
1324
1325 // Fallback to parameter if header not present
1326 if (!$nonce) {
1327 $nonce = $request->get_param('_wpnonce');
1328 }
1329
1330 // Verify nonce
1331 if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
1332 return false;
1333 }
1334
1335 return true;
1336 }
1337
1338 /**
1339 * Validate context type and ID
1340 *
1341 * @since 1.0.0
1342 *
1343 * @param string $context_type Context type
1344 * @param int|null $context_id Context ID
1345 * @return bool Validation status
1346 */
1347 private function validate_context(string $context_type, ?int $context_id): bool {
1348 $valid_types = ['site', 'post', 'page', 'product'];
1349
1350 if (!in_array($context_type, $valid_types, true)) {
1351 return false;
1352 }
1353
1354 if ($context_type !== 'site' && (!$context_id || $context_id <= 0)) {
1355 return false;
1356 }
1357
1358 if ($context_id && !get_post($context_id)) {
1359 return false;
1360 }
1361
1362 return true;
1363 }
1364
1365 /**
1366 * Generate schema preview
1367 *
1368 * @since 1.0.0
1369 *
1370 * @param array $schema_data Schema data
1371 * @param string $schema_type Schema type
1372 * @return array Preview data
1373 */
1374 private function generate_preview(array $schema_data, string $schema_type): array {
1375 return [
1376 'rich_snippets' => [
1377 $schema_type => $this->format_rich_snippet_preview($schema_data, $schema_type)
1378 ],
1379 'json_ld' => wp_json_encode($schema_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
1380 'validation_status' => 'pending'
1381 ];
1382 }
1383
1384 /**
1385 * Format rich snippet preview for specific schema type
1386 *
1387 * @since 1.0.0
1388 *
1389 * @param array $schema_data Schema data
1390 * @param string $schema_type Schema type
1391 * @return array Formatted preview data
1392 */
1393 private function format_rich_snippet_preview(array $schema_data, string $schema_type): array {
1394 switch ($schema_type) {
1395 case 'Organization':
1396 return [
1397 'title' => $schema_data['name'] ?? 'Organization Name',
1398 'url' => $schema_data['url'] ?? home_url(),
1399 'description' => $schema_data['description'] ?? 'Organization description',
1400 'additional_info' => $this->format_organization_info($schema_data)
1401 ];
1402
1403 case 'LocalBusiness':
1404 return [
1405 'title' => $schema_data['name'] ?? 'Business Name',
1406 'url' => $schema_data['url'] ?? home_url(),
1407 'description' => $schema_data['description'] ?? 'Business description',
1408 'additional_info' => $this->format_local_business_info($schema_data)
1409 ];
1410
1411 case 'Article':
1412 return [
1413 'title' => $schema_data['headline'] ?? $schema_data['name'] ?? 'Article Title',
1414 'url' => $schema_data['url'] ?? home_url(),
1415 'description' => $schema_data['description'] ?? 'Article description',
1416 'additional_info' => $this->format_article_info($schema_data)
1417 ];
1418
1419 default:
1420 return [
1421 'title' => $schema_data['headline'] ?? $schema_data['name'] ?? 'Title',
1422 'url' => $schema_data['url'] ?? home_url(),
1423 'description' => $schema_data['description'] ?? 'Description',
1424 'additional_info' => ''
1425 ];
1426 }
1427 }
1428
1429 /**
1430 * Extract additional preview information
1431 *
1432 * @since 1.0.0
1433 *
1434 * @param array $schema_data Schema data
1435 * @param string $schema_type Schema type
1436 * @return array Additional information
1437 */
1438 private function extract_preview_info(array $schema_data, string $schema_type): array {
1439 $info = [];
1440
1441 switch ($schema_type) {
1442 case 'Article':
1443 if (isset($schema_data['author']['name'])) {
1444 $info['author'] = $schema_data['author']['name'];
1445 }
1446 if (isset($schema_data['datePublished'])) {
1447 $info['date'] = gmdate('M j, Y', strtotime($schema_data['datePublished']));
1448 }
1449 if (isset($schema_data['wordCount'])) {
1450 $info['word_count'] = $schema_data['wordCount'];
1451 }
1452 break;
1453 case 'Product':
1454 if (isset($schema_data['offers']['price'])) {
1455 $currency = $schema_data['offers']['priceCurrency'] ?? '';
1456 $info['price'] = $currency . $schema_data['offers']['price'];
1457 }
1458 if (isset($schema_data['brand']['name'])) {
1459 $info['brand'] = $schema_data['brand']['name'];
1460 }
1461 if (isset($schema_data['offers']['availability'])) {
1462 $info['availability'] = str_replace('https://schema.org/', '', $schema_data['offers']['availability']);
1463 }
1464 break;
1465 case 'LocalBusiness':
1466 if (isset($schema_data['address']['addressLocality'])) {
1467 $info['location'] = $schema_data['address']['addressLocality'];
1468 }
1469 if (isset($schema_data['telephone'])) {
1470 $info['phone'] = $schema_data['telephone'];
1471 }
1472 break;
1473 }
1474
1475 return $info;
1476 }
1477
1478 /**
1479 * Format organization additional info
1480 *
1481 * @since 1.0.0
1482 *
1483 * @param array $schema_data Schema data
1484 * @return string Formatted info
1485 */
1486 private function format_organization_info(array $schema_data): string {
1487 $info = [];
1488
1489 if (!empty($schema_data['contactPoint']['telephone'])) {
1490 $info[] = '📞 ' . $schema_data['contactPoint']['telephone'];
1491 }
1492
1493 if (!empty($schema_data['contactPoint']['email'])) {
1494 $info[] = '✉️ ' . $schema_data['contactPoint']['email'];
1495 }
1496
1497 if (!empty($schema_data['address']['streetAddress'])) {
1498 $info[] = '📍 ' . $schema_data['address']['streetAddress'];
1499 }
1500
1501 return implode('', $info);
1502 }
1503
1504 /**
1505 * Format local business additional info
1506 *
1507 * @since 1.0.0
1508 *
1509 * @param array $schema_data Schema data
1510 * @return string Formatted info
1511 */
1512 private function format_local_business_info(array $schema_data): string {
1513 $info = [];
1514
1515 // Address
1516 if (!empty($schema_data['address'])) {
1517 $address = $schema_data['address'];
1518 $address_parts = [];
1519
1520 if (!empty($address['streetAddress'])) {
1521 $address_parts[] = $address['streetAddress'];
1522 }
1523 if (!empty($address['addressLocality'])) {
1524 $address_parts[] = $address['addressLocality'];
1525 }
1526
1527 if (!empty($address_parts)) {
1528 $info[] = '📍 ' . implode(', ', $address_parts);
1529 }
1530 }
1531
1532 // Phone
1533 if (!empty($schema_data['telephone'])) {
1534 $info[] = '📞 ' . $schema_data['telephone'];
1535 }
1536
1537 // Opening hours
1538 if (!empty($schema_data['openingHours'])) {
1539 $hours = is_array($schema_data['openingHours'])
1540 ? implode(', ', $schema_data['openingHours'])
1541 : $schema_data['openingHours'];
1542 $info[] = '🕒 ' . $hours;
1543 }
1544
1545 return implode('', $info);
1546 }
1547
1548 /**
1549 * Format article additional info
1550 *
1551 * @since 1.0.0
1552 *
1553 * @param array $schema_data Schema data
1554 * @return string Formatted info
1555 */
1556 private function format_article_info(array $schema_data): string {
1557 $info = [];
1558
1559 if (!empty($schema_data['author']['name'])) {
1560 $info[] = '👤 By ' . $schema_data['author']['name'];
1561 }
1562
1563 if (!empty($schema_data['datePublished'])) {
1564 $info[] = '�
1565 ' . gmdate('M j, Y', strtotime($schema_data['datePublished']));
1566 }
1567
1568 if (!empty($schema_data['publisher']['name'])) {
1569 $info[] = '🏢 ' . $schema_data['publisher']['name'];
1570 }
1571
1572 return implode(' ', $info);
1573 }
1574
1575 /**
1576 * Argument validation methods
1577 */
1578
1579 /**
1580 * Get arguments for schema generation endpoint
1581 *
1582 * @since 1.0.0
1583 *
1584 * @return array Arguments array
1585 */
1586 private function get_generate_schema_args(): array {
1587 return [
1588 'context_type' => [
1589 'required' => true,
1590 'type' => 'string',
1591 'enum' => ['site', 'post', 'page', 'product'],
1592 'description' => 'Context type for schema generation'
1593 ],
1594 'context_id' => [
1595 'required' => false,
1596 'type' => 'integer',
1597 'minimum' => 1,
1598 'description' => 'Context ID (not required for site context)'
1599 ],
1600 'schema_types' => [
1601 'required' => false,
1602 'type' => 'array',
1603 'items' => [
1604 'type' => 'string',
1605 'enum' => [
1606 'Article', 'BlogPosting', 'TechnicalArticle', 'NewsArticle',
1607 'ScholarlyArticle', 'Report', 'Product', 'Organization',
1608 'LocalBusiness', 'Person', 'WebSite', 'FAQPage',
1609 'Event', 'HowTo', 'SoftwareApplication'
1610 ]
1611 ],
1612 'description' => 'Schema types to generate'
1613 ],
1614 'options' => [
1615 'required' => false,
1616 'type' => 'object',
1617 'description' => 'Additional generation options'
1618 ],
1619 'content_data' => [
1620 'required' => false,
1621 'type' => 'object',
1622 'description' => 'Custom content data to use for schema generation (overrides post data)',
1623 'properties' => [
1624 'title' => ['type' => 'string'],
1625 'description' => ['type' => 'string'],
1626 'content' => ['type' => 'string'],
1627 'focus_keyword' => ['type' => 'string'],
1628 'post_type' => ['type' => 'string'],
1629 'post_url' => ['type' => 'string']
1630 ]
1631 ]
1632 ];
1633 }
1634
1635 /**
1636 * Get arguments for schema validation endpoint
1637 *
1638 * @since 1.0.0
1639 *
1640 * @return array Arguments array
1641 */
1642 private function get_validate_schema_args(): array {
1643 return [
1644 'schema_data' => [
1645 'required' => true,
1646 'type' => 'object',
1647 'description' => 'Schema data to validate'
1648 ],
1649 'schema_type' => [
1650 'required' => true,
1651 'type' => 'string',
1652 'enum' => [
1653 'Article', 'BlogPosting', 'TechnicalArticle', 'NewsArticle',
1654 'ScholarlyArticle', 'Report', 'Product', 'Organization',
1655 'LocalBusiness', 'Person', 'WebSite', 'WebPage', 'FAQPage',
1656 'SoftwareApplication', 'Event', 'Recipe', 'HowTo'
1657 ],
1658 'description' => 'Schema type'
1659 ],
1660 'options' => [
1661 'required' => false,
1662 'type' => 'object',
1663 'description' => 'Validation options'
1664 ]
1665 ];
1666 }
1667
1668 /**
1669 * Get arguments for schema deployment endpoint
1670 *
1671 * @since 1.0.0
1672 *
1673 * @return array Arguments array
1674 */
1675 private function get_deploy_schema_args(): array {
1676 return [
1677 'context_type' => [
1678 'required' => true,
1679 'type' => 'string',
1680 'enum' => ['site', 'post', 'page', 'product'],
1681 'description' => 'Context type for deployment'
1682 ],
1683 'context_id' => [
1684 'required' => false,
1685 'type' => 'integer',
1686 'minimum' => 1,
1687 'description' => 'Context ID (not required for site context)'
1688 ],
1689 'schema_data' => [
1690 'required' => true,
1691 'type' => 'object',
1692 'description' => 'Schema data to deploy'
1693 ],
1694 'options' => [
1695 'required' => false,
1696 'type' => 'object',
1697 'description' => 'Deployment options'
1698 ]
1699 ];
1700 }
1701
1702 /**
1703 * Get arguments for schema optimization endpoint
1704 *
1705 * @since 1.0.0
1706 *
1707 * @return array Arguments array
1708 */
1709 private function get_optimize_schema_args(): array {
1710 return [
1711 'schema_data' => [
1712 'required' => true,
1713 'type' => 'object',
1714 'description' => 'Schema data to optimize'
1715 ],
1716 'schema_type' => [
1717 'required' => true,
1718 'type' => 'string',
1719 'enum' => [
1720 'Article', 'BlogPosting', 'Product', 'Organization', 'LocalBusiness',
1721 'Person', 'WebSite', 'WebPage', 'FAQPage', 'SoftwareApplication',
1722 'BreadcrumbList', 'Event', 'Recipe', 'HowTo'
1723 ],
1724 'description' => 'Schema type'
1725 ],
1726 'options' => [
1727 'required' => false,
1728 'type' => 'object',
1729 'description' => 'Optimization options'
1730 ]
1731 ];
1732 }
1733
1734 /**
1735 * Get arguments for schema preview endpoint
1736 *
1737 * @since 1.0.0
1738 *
1739 * @return array Arguments array
1740 */
1741 private function get_preview_schema_args(): array {
1742 return [
1743 'schema_data' => [
1744 'required' => true,
1745 'type' => 'object',
1746 'description' => 'Schema data to preview'
1747 ],
1748 'schema_type' => [
1749 'required' => true,
1750 'type' => 'string',
1751 'enum' => [
1752 'Article', 'BlogPosting', 'Product', 'Organization', 'LocalBusiness',
1753 'Person', 'WebSite', 'WebPage', 'FAQPage', 'SoftwareApplication',
1754 'BreadcrumbList', 'Event', 'Recipe', 'HowTo'
1755 ],
1756 'description' => 'Schema type'
1757 ]
1758 ];
1759 }
1760
1761 /**
1762 * Get arguments for bulk operations endpoint
1763 *
1764 * @since 1.0.0
1765 *
1766 * @return array Arguments array
1767 */
1768 private function get_bulk_operations_args(): array {
1769 return [
1770 'operation' => [
1771 'required' => true,
1772 'type' => 'string',
1773 'enum' => ['generate', 'validate', 'deploy'],
1774 'description' => 'Bulk operation type'
1775 ],
1776 'items' => [
1777 'required' => true,
1778 'type' => 'array',
1779 'items' => [
1780 'type' => 'object'
1781 ],
1782 'description' => 'Items to process in bulk'
1783 ],
1784 'options' => [
1785 'required' => false,
1786 'type' => 'object',
1787 'description' => 'Bulk operation options'
1788 ]
1789 ];
1790 }
1791
1792 /**
1793 * Get schema settings
1794 *
1795 * @since 1.0.0
1796 *
1797 * @param WP_REST_Request $request Request object
1798 * @return WP_REST_Response|WP_Error Response object or error
1799 */
1800 public function get_settings(WP_REST_Request $request) {
1801 try {
1802 $context_type = $request->get_param('context_type') ?? 'site';
1803 $context_id = $request->get_param('context_id') ?? null;
1804
1805 // Get settings from schema manager
1806 $settings = $this->schema_manager->get_settings($context_type, $context_id);
1807
1808 return new WP_REST_Response([
1809 'success' => true,
1810 'data' => [
1811 'settings' => $settings,
1812 'context_type' => $context_type,
1813 'context_id' => $context_id
1814 ],
1815 'message' => 'Schema settings retrieved successfully'
1816 ], 200);
1817
1818 } catch (\Exception $e) {
1819 return new WP_Error(
1820 'settings_fetch_failed',
1821 'Failed to retrieve schema settings: ' . $e->getMessage(),
1822 ['status' => 500]
1823 );
1824 }
1825 }
1826
1827 /**
1828 * Save schema settings
1829 *
1830 * @since 1.0.0
1831 *
1832 * @param WP_REST_Request $request Request object
1833 * @return WP_REST_Response|WP_Error Response object or error
1834 */
1835 public function save_settings(WP_REST_Request $request) {
1836 try {
1837 $settings = $request->get_param('settings');
1838 $context_type = $request->get_param('context_type') ?? 'site';
1839 $context_id = $request->get_param('context_id') ?? null;
1840
1841 // Validate input parameters
1842 if (empty($settings) || !is_array($settings)) {
1843 return new WP_Error(
1844 'invalid_settings',
1845 'Settings parameter is required and must be an array',
1846 ['status' => 400]
1847 );
1848 }
1849
1850 // Get validation results for detailed error reporting
1851 $validation = $this->schema_manager->validate_settings($settings);
1852
1853 if (!$validation['valid']) {
1854 // Schema settings validation failed - details available in validation response
1855
1856 return new WP_Error(
1857 'validation_failed',
1858 'Schema settings validation failed',
1859 [
1860 'status' => 400,
1861 'validation_errors' => $validation['errors'],
1862 'validation_warnings' => $validation['warnings'] ?? [],
1863 'validation_suggestions' => $validation['suggestions'] ?? []
1864 ]
1865 );
1866 }
1867
1868 // Save settings using schema manager
1869 $success = $this->schema_manager->save_settings($context_type, $context_id, $settings);
1870
1871 if (!$success) {
1872 // Schema settings save failed - database operation unsuccessful
1873
1874 return new WP_Error(
1875 'settings_save_failed',
1876 'Failed to save schema settings to database',
1877 ['status' => 500]
1878 );
1879 }
1880
1881 return new WP_REST_Response([
1882 'success' => true,
1883 'data' => [
1884 'settings' => $settings,
1885 'context_type' => $context_type,
1886 'context_id' => $context_id,
1887 'validation' => $validation
1888 ],
1889 'message' => 'Schema settings saved successfully'
1890 ], 200);
1891
1892 } catch (\Exception $e) {
1893 // Schema settings save exception - error details in response
1894
1895 return new WP_Error(
1896 'settings_update_failed',
1897 'Failed to update schema settings: ' . $e->getMessage(),
1898 ['status' => 500]
1899 );
1900 }
1901 }
1902
1903 /**
1904 * Get arguments for settings endpoints
1905 *
1906 * @since 1.0.0
1907 *
1908 * @return array Arguments array
1909 */
1910 private function get_settings_args(): array {
1911 return [
1912 'settings' => [
1913 'required' => true,
1914 'type' => 'object',
1915 'description' => 'Schema settings object'
1916 ],
1917 'context_type' => [
1918 'required' => false,
1919 'type' => 'string',
1920 'default' => 'site',
1921 'enum' => ['site', 'post', 'page', 'product'],
1922 'description' => 'Context type for settings'
1923 ],
1924 'context_id' => [
1925 'required' => false,
1926 'type' => 'integer',
1927 'minimum' => 1,
1928 'description' => 'Context ID for settings'
1929 ]
1930 ];
1931 }
1932 }
1933