PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.28.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.28.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-social-media-endpoint.php

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

786 lines 24.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Social Media API Endpoints Class
4 *
5 * REST API endpoints for social media meta management including Open Graph,
6 * Twitter Cards, social media preview, and image optimization with proper
7 * authentication and comprehensive 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 use ThinkRank\SEO\Social_Meta_Manager;
19 use WP_REST_Controller;
20 use WP_REST_Request;
21 use WP_REST_Response;
22 use WP_Error;
23
24 // Prevent direct access
25 if (!defined('ABSPATH')) {
26 exit;
27 }
28
29 /**
30 * Social Media API Endpoints Class
31 *
32 * Provides REST API endpoints for social media operations including
33 * Open Graph generation, Twitter Cards, social media preview, and
34 * image optimization with proper authentication and validation.
35 *
36 * @since 1.0.0
37 */
38 class Social_Media_Endpoint extends WP_REST_Controller {
39
40 /**
41 * Social Meta Manager instance
42 *
43 * @since 1.0.0
44 * @var Social_Meta_Manager
45 */
46 private Social_Meta_Manager $social_manager;
47
48 /**
49 * API namespace
50 *
51 * @since 1.0.0
52 * @var string
53 */
54 protected $namespace = 'thinkrank/v1';
55
56 /**
57 * API resource base
58 *
59 * @since 1.0.0
60 * @var string
61 */
62 protected $rest_base = 'social-media';
63
64 /**
65 * Constructor
66 *
67 * @since 1.0.0
68 */
69 public function __construct() {
70 $this->social_manager = new Social_Meta_Manager();
71 }
72
73 /**
74 * Register API routes
75 *
76 * @since 1.0.0
77 */
78 public function register_routes(): void {
79 // Social media settings management
80 register_rest_route(
81 $this->namespace,
82 '/' . $this->rest_base . '/settings',
83 [
84 [
85 'methods' => 'GET',
86 'callback' => [$this, 'get_settings'],
87 'permission_callback' => [$this, 'check_read_permissions']
88 ],
89 [
90 'methods' => 'POST',
91 'callback' => [$this, 'update_settings'],
92 'permission_callback' => [$this, 'check_manage_permissions'],
93 'args' => $this->get_settings_args()
94 ]
95 ]
96 );
97
98 // Social media settings validation
99 register_rest_route(
100 $this->namespace,
101 '/' . $this->rest_base . '/validate',
102 [
103 [
104 'methods' => 'POST',
105 'callback' => [$this, 'validate_settings'],
106 'permission_callback' => [$this, 'check_read_permissions'],
107 'args' => $this->get_settings_args()
108 ]
109 ]
110 );
111
112 // Generate social media preview
113 register_rest_route(
114 $this->namespace,
115 '/' . $this->rest_base . '/preview',
116 [
117 [
118 'methods' => 'POST',
119 'callback' => [$this, 'generate_preview'],
120 'permission_callback' => [$this, 'check_read_permissions'],
121 'args' => $this->get_preview_args()
122 ]
123 ]
124 );
125
126 // Optimize image for social platforms
127 register_rest_route(
128 $this->namespace,
129 '/' . $this->rest_base . '/optimize-image',
130 [
131 [
132 'methods' => 'POST',
133 'callback' => [$this, 'optimize_image'],
134 'permission_callback' => [$this, 'check_manage_permissions'],
135 'args' => $this->get_optimize_image_args()
136 ]
137 ]
138 );
139
140 // Get social meta for context
141 register_rest_route(
142 $this->namespace,
143 '/' . $this->rest_base . '/(?P<context_type>[a-zA-Z]+)/(?P<context_id>\d+)',
144 [
145 [
146 'methods' => 'GET',
147 'callback' => [$this, 'get_social_meta'],
148 'permission_callback' => [$this, 'check_read_permissions'],
149 'args' => $this->get_context_args()
150 ],
151 [
152 'methods' => 'POST',
153 'callback' => [$this, 'save_social_meta'],
154 'permission_callback' => [$this, 'check_manage_permissions'],
155 'args' => array_merge($this->get_context_args(), $this->get_social_meta_args())
156 ]
157 ]
158 );
159
160 // Generate Open Graph tags
161 register_rest_route(
162 $this->namespace,
163 '/' . $this->rest_base . '/generate-og',
164 [
165 [
166 'methods' => 'POST',
167 'callback' => [$this, 'generate_og_tags'],
168 'permission_callback' => [$this, 'check_read_permissions'],
169 'args' => $this->get_generate_tags_args()
170 ]
171 ]
172 );
173
174 // Generate Twitter Card tags
175 register_rest_route(
176 $this->namespace,
177 '/' . $this->rest_base . '/generate-twitter',
178 [
179 [
180 'methods' => 'POST',
181 'callback' => [$this, 'generate_twitter_tags'],
182 'permission_callback' => [$this, 'check_read_permissions'],
183 'args' => $this->get_generate_tags_args()
184 ]
185 ]
186 );
187 }
188
189 /**
190 * Get social media settings
191 *
192 * @since 1.0.0
193 *
194 * @param WP_REST_Request $request Request object
195 * @return WP_REST_Response Response object
196 */
197 public function get_settings(WP_REST_Request $request): WP_REST_Response {
198 try {
199 $context_type = $request->get_param('context_type') ?? 'site';
200 $context_id = $request->get_param('context_id');
201
202 // Get settings from Social Meta Manager
203 $settings = $this->social_manager->get_settings($context_type, $context_id);
204
205 // Get settings schema for validation
206 $schema = $this->social_manager->get_settings_schema($context_type);
207
208 return new WP_REST_Response([
209 'success' => true,
210 'data' => [
211 'settings' => $settings,
212 'schema' => $schema,
213 'context_type' => $context_type,
214 'context_id' => $context_id
215 ],
216 'message' => 'Social media settings retrieved successfully'
217 ], 200);
218
219 } catch (\Exception $e) {
220 return new WP_REST_Response([
221 'success' => false,
222 'error' => 'Failed to retrieve settings: ' . $e->getMessage()
223 ], 500);
224 }
225 }
226
227 /**
228 * Update social media settings
229 *
230 * @since 1.0.0
231 *
232 * @param WP_REST_Request $request Request object
233 * @return WP_REST_Response|WP_Error Response object or error
234 */
235 public function update_settings(WP_REST_Request $request) {
236 try {
237 $settings = $request->get_param('settings');
238 $context_type = $request->get_param('context_type') ?? 'site';
239 $context_id = $request->get_param('context_id');
240 $validation_context = $request->get_param('validation_context') ?? 'all';
241
242 if (empty($settings)) {
243 return new WP_REST_Response([
244 'success' => false,
245 'error' => 'Settings data is required'
246 ], 400);
247 }
248
249 // Drop unrecognized keys so arbitrary client-supplied keys aren't
250 // persisted (storage bloat / settings drift). The known set is the
251 // context's default settings, exposed through a filter for add-ons.
252 $known = array_keys($this->social_manager->get_default_settings($context_type));
253 $known = apply_filters('thinkrank_social_known_setting_keys', $known, $context_type);
254 $settings = array_intersect_key($settings, array_flip($known));
255 if (empty($settings)) {
256 return new WP_REST_Response([
257 'success' => false,
258 'error' => 'No recognized social settings were provided'
259 ], 400);
260 }
261
262 // Validate settings with context
263 $validation = $this->social_manager->validate_settings($settings, $validation_context);
264 if (!$validation['valid']) {
265 return new WP_Error(
266 'validation_failed',
267 'Settings validation failed',
268 [
269 'status' => 400,
270 'validation_errors' => $validation['errors'],
271 'validation_warnings' => $validation['warnings']
272 ]
273 );
274 }
275
276 // Save settings
277 $result = $this->social_manager->save_settings($context_type, $context_id, $settings);
278
279 if ($result) {
280 // Get updated settings
281 $updated_settings = $this->social_manager->get_settings($context_type, $context_id);
282
283 return new WP_REST_Response([
284 'success' => true,
285 'data' => [
286 'settings' => $updated_settings,
287 'validation' => $validation,
288 'context_type' => $context_type,
289 'context_id' => $context_id
290 ],
291 'message' => 'Social media settings updated successfully'
292 ], 200);
293 } else {
294 throw new \Exception('Failed to save settings');
295 }
296
297 } catch (\Exception $e) {
298 return new WP_REST_Response([
299 'success' => false,
300 'error' => 'Settings update failed: ' . $e->getMessage()
301 ], 500);
302 }
303 }
304
305 /**
306 * Validate social media settings
307 *
308 * @since 1.0.0
309 *
310 * @param WP_REST_Request $request Request object
311 * @return WP_REST_Response|WP_Error Response object
312 */
313 public function validate_settings(WP_REST_Request $request): WP_REST_Response|WP_Error {
314 try {
315 $settings = $request->get_param('settings') ?? [];
316 $context_type = $request->get_param('context_type') ?? 'site';
317 $validation_context = $request->get_param('validation_context') ?? 'all';
318
319 // Validate settings using the enhanced Social Meta Manager with tab-specific context
320 $validation = $this->social_manager->validate_settings($settings, $validation_context);
321
322 return new WP_REST_Response([
323 'success' => true,
324 'data' => $validation
325 ], 200);
326
327 } catch (\Exception $e) {
328 return new WP_Error(
329 'validation_error',
330 'Failed to validate social media settings: ' . $e->getMessage(),
331 ['status' => 500]
332 );
333 }
334 }
335
336 /**
337 * Generate social media preview
338 *
339 * @since 1.0.0
340 *
341 * @param WP_REST_Request $request Request object
342 * @return WP_REST_Response|WP_Error Response object or error
343 */
344 public function generate_preview(WP_REST_Request $request) {
345 try {
346 $data = $request->get_param('data') ?? [];
347 $platform = $request->get_param('platform') ?? 'facebook';
348
349 // Validate platform
350 $supported_platforms = ['facebook', 'twitter', 'linkedin', 'pinterest'];
351 if (!in_array($platform, $supported_platforms, true)) {
352 return new WP_Error(
353 'invalid_platform',
354 'Unsupported platform for preview generation',
355 ['status' => 400]
356 );
357 }
358
359 // Generate preview
360 $preview_data = $this->social_manager->preview_social_post($data, $platform);
361
362 return new WP_REST_Response([
363 'success' => true,
364 'data' => $preview_data,
365 'message' => 'Social media preview generated successfully'
366 ], 200);
367
368 } catch (\Exception $e) {
369 return new WP_Error(
370 'preview_failed',
371 'Social media preview generation failed: ' . $e->getMessage(),
372 ['status' => 500]
373 );
374 }
375 }
376
377 /**
378 * Optimize image for social platforms
379 *
380 * @since 1.0.0
381 *
382 * @param WP_REST_Request $request Request object
383 * @return WP_REST_Response|WP_Error Response object or error
384 */
385 public function optimize_image(WP_REST_Request $request) {
386 try {
387 $image_url = $request->get_param('image_url');
388 $platform = $request->get_param('platform') ?? 'facebook';
389
390 // Validate image URL
391 if (!filter_var($image_url, FILTER_VALIDATE_URL)) {
392 return new WP_Error(
393 'invalid_image_url',
394 'Invalid image URL provided',
395 ['status' => 400]
396 );
397 }
398
399 // Optimize image
400 $optimized_image = $this->social_manager->optimize_social_image($image_url, $platform);
401
402 return new WP_REST_Response([
403 'success' => true,
404 'data' => $optimized_image,
405 'message' => 'Image optimized successfully'
406 ], 200);
407
408 } catch (\Exception $e) {
409 return new WP_Error(
410 'optimization_failed',
411 'Image optimization failed: ' . $e->getMessage(),
412 ['status' => 500]
413 );
414 }
415 }
416
417 /**
418 * Get social meta for context
419 *
420 * @since 1.0.0
421 *
422 * @param WP_REST_Request $request Request object
423 * @return WP_REST_Response|WP_Error Response object or error
424 */
425 public function get_social_meta(WP_REST_Request $request) {
426 try {
427 $context_type = $request->get_param('context_type');
428 $context_id = (int) $request->get_param('context_id');
429
430 // Validate context
431 if (!$this->validate_context($context_type, $context_id)) {
432 return new WP_Error(
433 'invalid_context',
434 'Invalid context type or ID provided',
435 ['status' => 400]
436 );
437 }
438
439 // Get social meta data
440 $social_meta = $this->social_manager->get_output_data($context_type, $context_id);
441
442 return new WP_REST_Response([
443 'success' => true,
444 'data' => $social_meta,
445 'message' => 'Social meta retrieved successfully'
446 ], 200);
447
448 } catch (\Exception $e) {
449 return new WP_Error(
450 'retrieval_failed',
451 'Social meta retrieval failed: ' . $e->getMessage(),
452 ['status' => 500]
453 );
454 }
455 }
456
457 /**
458 * Save social meta for context
459 *
460 * @since 1.0.0
461 *
462 * @param WP_REST_Request $request Request object
463 * @return WP_REST_Response|WP_Error Response object or error
464 */
465 public function save_social_meta(WP_REST_Request $request) {
466 try {
467 $context_type = $request->get_param('context_type');
468 $context_id = (int) $request->get_param('context_id');
469 $social_data = $request->get_param('social_data') ?? [];
470
471 // Validate context
472 if (!$this->validate_context($context_type, $context_id)) {
473 return new WP_Error(
474 'invalid_context',
475 'Invalid context type or ID provided',
476 ['status' => 400]
477 );
478 }
479
480 // SECURITY: This route writes per-post social overrides, so require
481 // object-level edit permission — the global thinkrank_social_media
482 // capability alone would allow writing to any post (IDOR).
483 if (!current_user_can('edit_post', $context_id)) {
484 return new WP_Error(
485 'rest_forbidden',
486 'You are not allowed to edit this content.',
487 ['status' => 403]
488 );
489 }
490
491 // Save social meta data (manager signature is
492 // save_settings(context_type, context_id, settings)).
493 $result = $this->social_manager->save_settings($context_type, $context_id, $social_data);
494
495 if ($result) {
496 return new WP_REST_Response([
497 'success' => true,
498 'data' => $result,
499 'message' => 'Social meta saved successfully'
500 ], 200);
501 } else {
502 throw new \Exception('Failed to save social meta data');
503 }
504
505 } catch (\Throwable $e) {
506 // Catch \Throwable (not just \Exception) so a future TypeError
507 // degrades to a JSON error instead of a fatal.
508 return new WP_Error(
509 'save_failed',
510 'Social meta save failed: ' . $e->getMessage(),
511 ['status' => 500]
512 );
513 }
514 }
515
516 /**
517 * Generate Open Graph tags
518 *
519 * @since 1.0.0
520 *
521 * @param WP_REST_Request $request Request object
522 * @return WP_REST_Response|WP_Error Response object or error
523 */
524 public function generate_og_tags(WP_REST_Request $request) {
525 try {
526 $data = $request->get_param('data') ?? [];
527 $context = $request->get_param('context') ?? 'site';
528 $platform = $request->get_param('platform') ?? 'facebook';
529
530 // Generate Open Graph tags
531 $og_tags = $this->social_manager->generate_og_tags($data, $context, $platform);
532
533 return new WP_REST_Response([
534 'success' => true,
535 'data' => $og_tags,
536 'message' => 'Open Graph tags generated successfully'
537 ], 200);
538
539 } catch (\Exception $e) {
540 return new WP_Error(
541 'og_generation_failed',
542 'Open Graph generation failed: ' . $e->getMessage(),
543 ['status' => 500]
544 );
545 }
546 }
547
548 /**
549 * Generate Twitter Card tags
550 *
551 * @since 1.0.0
552 *
553 * @param WP_REST_Request $request Request object
554 * @return WP_REST_Response|WP_Error Response object or error
555 */
556 public function generate_twitter_tags(WP_REST_Request $request) {
557 try {
558 $data = $request->get_param('data') ?? [];
559 $context = $request->get_param('context') ?? 'site';
560
561 // Generate Twitter Card tags
562 $twitter_tags = $this->social_manager->generate_twitter_tags($data, $context);
563
564 return new WP_REST_Response([
565 'success' => true,
566 'data' => $twitter_tags,
567 'message' => 'Twitter Card tags generated successfully'
568 ], 200);
569
570 } catch (\Exception $e) {
571 return new WP_Error(
572 'twitter_generation_failed',
573 'Twitter Card generation failed: ' . $e->getMessage(),
574 ['status' => 500]
575 );
576 }
577 }
578
579 /**
580 * Check read permissions
581 *
582 * @since 1.0.0
583 *
584 * @return bool Permission status
585 */
586 public function check_read_permissions(): bool {
587 return current_user_can('edit_posts');
588 }
589
590 /**
591 * Check manage permissions
592 *
593 * @since 1.0.0
594 *
595 * @return bool Permission status
596 */
597 public function check_manage_permissions(): bool {
598 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_social_media');
599 }
600
601 /**
602 * Validate context type and ID
603 *
604 * @since 1.0.0
605 *
606 * @param string $context_type Context type
607 * @param int|null $context_id Context ID
608 * @return bool Validation status
609 */
610 private function validate_context(string $context_type, ?int $context_id): bool {
611 $valid_types = ['site', 'post', 'page', 'product'];
612
613 if (!in_array($context_type, $valid_types, true)) {
614 return false;
615 }
616
617 if ($context_type !== 'site' && (!$context_id || $context_id <= 0)) {
618 return false;
619 }
620
621 if ($context_id && !get_post($context_id)) {
622 return false;
623 }
624
625 return true;
626 }
627
628 /**
629 * Get arguments for preview endpoint
630 *
631 * @since 1.0.0
632 *
633 * @return array Arguments array
634 */
635 private function get_preview_args(): array {
636 return [
637 'data' => [
638 'required' => true,
639 'type' => 'object',
640 'description' => 'Content data for preview generation'
641 ],
642 'platform' => [
643 'required' => false,
644 'type' => 'string',
645 'enum' => ['facebook', 'twitter', 'linkedin', 'pinterest'],
646 'default' => 'facebook',
647 'description' => 'Target platform for preview'
648 ]
649 ];
650 }
651
652 /**
653 * Get arguments for image optimization endpoint
654 *
655 * @since 1.0.0
656 *
657 * @return array Arguments array
658 */
659 private function get_optimize_image_args(): array {
660 return [
661 'image_url' => [
662 'required' => true,
663 'type' => 'string',
664 'format' => 'uri',
665 'description' => 'Image URL to optimize'
666 ],
667 'platform' => [
668 'required' => false,
669 'type' => 'string',
670 'enum' => ['facebook', 'twitter', 'linkedin', 'pinterest'],
671 'default' => 'facebook',
672 'description' => 'Target platform for optimization'
673 ]
674 ];
675 }
676
677 /**
678 * Get arguments for context endpoints
679 *
680 * @since 1.0.0
681 *
682 * @return array Arguments array
683 */
684 private function get_context_args(): array {
685 return [
686 'context_type' => [
687 'required' => true,
688 'type' => 'string',
689 'enum' => ['site', 'post', 'page', 'product'],
690 'description' => 'Context type'
691 ],
692 'context_id' => [
693 'required' => true,
694 'type' => 'integer',
695 'minimum' => 1,
696 'description' => 'Context ID'
697 ]
698 ];
699 }
700
701 /**
702 * Get arguments for social meta save endpoint
703 *
704 * @since 1.0.0
705 *
706 * @return array Arguments array
707 */
708 private function get_social_meta_args(): array {
709 return [
710 'social_data' => [
711 'required' => true,
712 'type' => 'object',
713 'description' => 'Social media meta data to save'
714 ]
715 ];
716 }
717
718 /**
719 * Get arguments for tag generation endpoints
720 *
721 * @since 1.0.0
722 *
723 * @return array Arguments array
724 */
725 private function get_generate_tags_args(): array {
726 return [
727 'data' => [
728 'required' => true,
729 'type' => 'object',
730 'description' => 'Content data for tag generation'
731 ],
732 'context' => [
733 'required' => false,
734 'type' => 'string',
735 'enum' => ['site', 'post', 'page', 'product'],
736 'default' => 'site',
737 'description' => 'Context type'
738 ],
739 'platform' => [
740 'required' => false,
741 'type' => 'string',
742 'enum' => ['facebook', 'twitter', 'linkedin', 'pinterest'],
743 'default' => 'facebook',
744 'description' => 'Target platform (for Open Graph only)'
745 ]
746 ];
747 }
748
749 /**
750 * Get arguments for settings endpoints
751 *
752 * @since 1.0.0
753 *
754 * @return array Arguments array
755 */
756 private function get_settings_args(): array {
757 return [
758 'settings' => [
759 'required' => true,
760 'type' => 'object',
761 'description' => 'Social media settings to save'
762 ],
763 'context_type' => [
764 'required' => false,
765 'type' => 'string',
766 'enum' => ['site', 'post', 'page', 'product'],
767 'default' => 'site',
768 'description' => 'Context type'
769 ],
770 'context_id' => [
771 'required' => false,
772 'type' => 'integer',
773 'minimum' => 1,
774 'description' => 'Context ID (required for non-site contexts)'
775 ],
776 'validation_context' => [
777 'required' => false,
778 'type' => 'string',
779 'enum' => ['all', 'open-graph', 'twitter-cards', 'platforms', 'preview'],
780 'default' => 'all',
781 'description' => 'Validation context for focused validation'
782 ]
783 ];
784 }
785 }
786