PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.31.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.31.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.31.0, at includes/api/class-social-media-endpoint.php

811 lines 25.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 * @throws \Exception On failure.
236 */
237 public function update_settings(WP_REST_Request $request) {
238 try {
239 $settings = $request->get_param('settings');
240 $context_type = $request->get_param('context_type') ?? 'site';
241 $context_id = $request->get_param('context_id');
242 $validation_context = $request->get_param('validation_context') ?? 'all';
243
244 if (empty($settings)) {
245 return new WP_REST_Response([
246 'success' => false,
247 'error' => 'Settings data is required'
248 ], 400);
249 }
250
251 // SECURITY: this route writes the same per-object social overrides
252 // as save_social_meta(), so it needs the same object-level guard —
253 // the section-level thinkrank_social_media capability alone would
254 // let a delegated user write to any post (IDOR).
255 $context_id = $context_id === null ? null : (int) $context_id;
256 if (!$this->validate_context($context_type, $context_id)) {
257 return new WP_Error(
258 'invalid_context',
259 'Invalid context type or ID provided',
260 ['status' => 400]
261 );
262 }
263
264 if ($context_type !== 'site' && !current_user_can('edit_post', $context_id)) {
265 return new WP_Error(
266 'rest_forbidden',
267 'You are not allowed to edit this content.',
268 ['status' => 403]
269 );
270 }
271
272 // Drop unrecognized keys so arbitrary client-supplied keys aren't
273 // persisted (storage bloat / settings drift). The known set is the
274 // context's default settings, exposed through a filter for add-ons.
275 $known = array_keys($this->social_manager->get_default_settings($context_type));
276 $known = apply_filters('thinkrank_social_known_setting_keys', $known, $context_type);
277 $settings = array_intersect_key($settings, array_flip($known));
278 if (empty($settings)) {
279 return new WP_REST_Response([
280 'success' => false,
281 'error' => 'No recognized social settings were provided'
282 ], 400);
283 }
284
285 // Validate settings with context
286 $validation = $this->social_manager->validate_settings($settings, $validation_context);
287 if (!$validation['valid']) {
288 return new WP_Error(
289 'validation_failed',
290 'Settings validation failed',
291 [
292 'status' => 400,
293 'validation_errors' => $validation['errors'],
294 'validation_warnings' => $validation['warnings']
295 ]
296 );
297 }
298
299 // Save settings
300 $result = $this->social_manager->save_settings($context_type, $context_id, $settings);
301
302 if ($result) {
303 // Get updated settings
304 $updated_settings = $this->social_manager->get_settings($context_type, $context_id);
305
306 return new WP_REST_Response([
307 'success' => true,
308 'data' => [
309 'settings' => $updated_settings,
310 'validation' => $validation,
311 'context_type' => $context_type,
312 'context_id' => $context_id
313 ],
314 'message' => 'Social media settings updated successfully'
315 ], 200);
316 } else {
317 throw new \Exception('Failed to save settings');
318 }
319
320 } catch (\Exception $e) {
321 return new WP_REST_Response([
322 'success' => false,
323 'error' => 'Settings update failed: ' . $e->getMessage()
324 ], 500);
325 }
326 }
327
328 /**
329 * Validate social media settings
330 *
331 * @since 1.0.0
332 *
333 * @param WP_REST_Request $request Request object
334 * @return WP_REST_Response|WP_Error Response object
335 */
336 public function validate_settings(WP_REST_Request $request) {
337 try {
338 $settings = $request->get_param('settings') ?? [];
339 $context_type = $request->get_param('context_type') ?? 'site';
340 $validation_context = $request->get_param('validation_context') ?? 'all';
341
342 // Validate settings using the enhanced Social Meta Manager with tab-specific context
343 $validation = $this->social_manager->validate_settings($settings, $validation_context);
344
345 return new WP_REST_Response([
346 'success' => true,
347 'data' => $validation
348 ], 200);
349
350 } catch (\Exception $e) {
351 return new WP_Error(
352 'validation_error',
353 'Failed to validate social media settings: ' . $e->getMessage(),
354 ['status' => 500]
355 );
356 }
357 }
358
359 /**
360 * Generate social media preview
361 *
362 * @since 1.0.0
363 *
364 * @param WP_REST_Request $request Request object
365 * @return WP_REST_Response|WP_Error Response object or error
366 */
367 public function generate_preview(WP_REST_Request $request) {
368 try {
369 $data = $request->get_param('data') ?? [];
370 $platform = $request->get_param('platform') ?? 'facebook';
371
372 // Validate platform
373 $supported_platforms = ['facebook', 'twitter', 'linkedin', 'pinterest'];
374 if (!in_array($platform, $supported_platforms, true)) {
375 return new WP_Error(
376 'invalid_platform',
377 'Unsupported platform for preview generation',
378 ['status' => 400]
379 );
380 }
381
382 // Generate preview
383 $preview_data = $this->social_manager->preview_social_post($data, $platform);
384
385 return new WP_REST_Response([
386 'success' => true,
387 'data' => $preview_data,
388 'message' => 'Social media preview generated successfully'
389 ], 200);
390
391 } catch (\Exception $e) {
392 return new WP_Error(
393 'preview_failed',
394 'Social media preview generation failed: ' . $e->getMessage(),
395 ['status' => 500]
396 );
397 }
398 }
399
400 /**
401 * Optimize image for social platforms
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 optimize_image(WP_REST_Request $request) {
409 try {
410 $image_url = $request->get_param('image_url');
411 $platform = $request->get_param('platform') ?? 'facebook';
412
413 // Validate image URL
414 if (!filter_var($image_url, FILTER_VALIDATE_URL)) {
415 return new WP_Error(
416 'invalid_image_url',
417 'Invalid image URL provided',
418 ['status' => 400]
419 );
420 }
421
422 // Optimize image
423 $optimized_image = $this->social_manager->optimize_social_image($image_url, $platform);
424
425 return new WP_REST_Response([
426 'success' => true,
427 'data' => $optimized_image,
428 'message' => 'Image optimized successfully'
429 ], 200);
430
431 } catch (\Exception $e) {
432 return new WP_Error(
433 'optimization_failed',
434 'Image optimization failed: ' . $e->getMessage(),
435 ['status' => 500]
436 );
437 }
438 }
439
440 /**
441 * Get social meta for context
442 *
443 * @since 1.0.0
444 *
445 * @param WP_REST_Request $request Request object
446 * @return WP_REST_Response|WP_Error Response object or error
447 */
448 public function get_social_meta(WP_REST_Request $request) {
449 try {
450 $context_type = $request->get_param('context_type');
451 $context_id = (int) $request->get_param('context_id');
452
453 // Validate context
454 if (!$this->validate_context($context_type, $context_id)) {
455 return new WP_Error(
456 'invalid_context',
457 'Invalid context type or ID provided',
458 ['status' => 400]
459 );
460 }
461
462 // Get social meta data
463 $social_meta = $this->social_manager->get_output_data($context_type, $context_id);
464
465 return new WP_REST_Response([
466 'success' => true,
467 'data' => $social_meta,
468 'message' => 'Social meta retrieved successfully'
469 ], 200);
470
471 } catch (\Exception $e) {
472 return new WP_Error(
473 'retrieval_failed',
474 'Social meta retrieval failed: ' . $e->getMessage(),
475 ['status' => 500]
476 );
477 }
478 }
479
480 /**
481 * Save social meta for context
482 *
483 * @since 1.0.0
484 *
485 * @param WP_REST_Request $request Request object
486 * @return WP_REST_Response|WP_Error Response object or error
487 *
488 * @throws \Exception On failure.
489 */
490 public function save_social_meta(WP_REST_Request $request) {
491 try {
492 $context_type = $request->get_param('context_type');
493 $context_id = (int) $request->get_param('context_id');
494 $social_data = $request->get_param('social_data') ?? [];
495
496 // Validate context
497 if (!$this->validate_context($context_type, $context_id)) {
498 return new WP_Error(
499 'invalid_context',
500 'Invalid context type or ID provided',
501 ['status' => 400]
502 );
503 }
504
505 // SECURITY: This route writes per-post social overrides, so require
506 // object-level edit permission — the global thinkrank_social_media
507 // capability alone would allow writing to any post (IDOR).
508 if (!current_user_can('edit_post', $context_id)) {
509 return new WP_Error(
510 'rest_forbidden',
511 'You are not allowed to edit this content.',
512 ['status' => 403]
513 );
514 }
515
516 // Save social meta data (manager signature is
517 // save_settings(context_type, context_id, settings)).
518 $result = $this->social_manager->save_settings($context_type, $context_id, $social_data);
519
520 if ($result) {
521 return new WP_REST_Response([
522 'success' => true,
523 'data' => $result,
524 'message' => 'Social meta saved successfully'
525 ], 200);
526 } else {
527 throw new \Exception('Failed to save social meta data');
528 }
529
530 } catch (\Throwable $e) {
531 // Catch \Throwable (not just \Exception) so a future TypeError
532 // degrades to a JSON error instead of a fatal.
533 return new WP_Error(
534 'save_failed',
535 'Social meta save failed: ' . $e->getMessage(),
536 ['status' => 500]
537 );
538 }
539 }
540
541 /**
542 * Generate Open Graph tags
543 *
544 * @since 1.0.0
545 *
546 * @param WP_REST_Request $request Request object
547 * @return WP_REST_Response|WP_Error Response object or error
548 */
549 public function generate_og_tags(WP_REST_Request $request) {
550 try {
551 $data = $request->get_param('data') ?? [];
552 $context = $request->get_param('context') ?? 'site';
553 $platform = $request->get_param('platform') ?? 'facebook';
554
555 // Generate Open Graph tags
556 $og_tags = $this->social_manager->generate_og_tags($data, $context, $platform);
557
558 return new WP_REST_Response([
559 'success' => true,
560 'data' => $og_tags,
561 'message' => 'Open Graph tags generated successfully'
562 ], 200);
563
564 } catch (\Exception $e) {
565 return new WP_Error(
566 'og_generation_failed',
567 'Open Graph generation failed: ' . $e->getMessage(),
568 ['status' => 500]
569 );
570 }
571 }
572
573 /**
574 * Generate Twitter Card tags
575 *
576 * @since 1.0.0
577 *
578 * @param WP_REST_Request $request Request object
579 * @return WP_REST_Response|WP_Error Response object or error
580 */
581 public function generate_twitter_tags(WP_REST_Request $request) {
582 try {
583 $data = $request->get_param('data') ?? [];
584 $context = $request->get_param('context') ?? 'site';
585
586 // Generate Twitter Card tags
587 $twitter_tags = $this->social_manager->generate_twitter_tags($data, $context);
588
589 return new WP_REST_Response([
590 'success' => true,
591 'data' => $twitter_tags,
592 'message' => 'Twitter Card tags generated successfully'
593 ], 200);
594
595 } catch (\Exception $e) {
596 return new WP_Error(
597 'twitter_generation_failed',
598 'Twitter Card generation failed: ' . $e->getMessage(),
599 ['status' => 500]
600 );
601 }
602 }
603
604 /**
605 * Check read permissions
606 *
607 * @since 1.0.0
608 *
609 * @return bool Permission status
610 */
611 public function check_read_permissions(): bool {
612 return current_user_can('edit_posts');
613 }
614
615 /**
616 * Check manage permissions
617 *
618 * @since 1.0.0
619 *
620 * @return bool Permission status
621 */
622 public function check_manage_permissions(): bool {
623 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_social_media');
624 }
625
626 /**
627 * Validate context type and ID
628 *
629 * @since 1.0.0
630 *
631 * @param string $context_type Context type
632 * @param int|null $context_id Context ID
633 * @return bool Validation status
634 */
635 private function validate_context(string $context_type, ?int $context_id): bool {
636 $valid_types = ['site', 'post', 'page', 'product'];
637
638 if (!in_array($context_type, $valid_types, true)) {
639 return false;
640 }
641
642 if ($context_type !== 'site' && (!$context_id || $context_id <= 0)) {
643 return false;
644 }
645
646 if ($context_id && !get_post($context_id)) {
647 return false;
648 }
649
650 return true;
651 }
652
653 /**
654 * Get arguments for preview endpoint
655 *
656 * @since 1.0.0
657 *
658 * @return array Arguments array
659 */
660 private function get_preview_args(): array {
661 return [
662 'data' => [
663 'required' => true,
664 'type' => 'object',
665 'description' => 'Content data for preview generation'
666 ],
667 'platform' => [
668 'required' => false,
669 'type' => 'string',
670 'enum' => ['facebook', 'twitter', 'linkedin', 'pinterest'],
671 'default' => 'facebook',
672 'description' => 'Target platform for preview'
673 ]
674 ];
675 }
676
677 /**
678 * Get arguments for image optimization endpoint
679 *
680 * @since 1.0.0
681 *
682 * @return array Arguments array
683 */
684 private function get_optimize_image_args(): array {
685 return [
686 'image_url' => [
687 'required' => true,
688 'type' => 'string',
689 'format' => 'uri',
690 'description' => 'Image URL to optimize'
691 ],
692 'platform' => [
693 'required' => false,
694 'type' => 'string',
695 'enum' => ['facebook', 'twitter', 'linkedin', 'pinterest'],
696 'default' => 'facebook',
697 'description' => 'Target platform for optimization'
698 ]
699 ];
700 }
701
702 /**
703 * Get arguments for context endpoints
704 *
705 * @since 1.0.0
706 *
707 * @return array Arguments array
708 */
709 private function get_context_args(): array {
710 return [
711 'context_type' => [
712 'required' => true,
713 'type' => 'string',
714 'enum' => ['site', 'post', 'page', 'product'],
715 'description' => 'Context type'
716 ],
717 'context_id' => [
718 'required' => true,
719 'type' => 'integer',
720 'minimum' => 1,
721 'description' => 'Context ID'
722 ]
723 ];
724 }
725
726 /**
727 * Get arguments for social meta save endpoint
728 *
729 * @since 1.0.0
730 *
731 * @return array Arguments array
732 */
733 private function get_social_meta_args(): array {
734 return [
735 'social_data' => [
736 'required' => true,
737 'type' => 'object',
738 'description' => 'Social media meta data to save'
739 ]
740 ];
741 }
742
743 /**
744 * Get arguments for tag generation endpoints
745 *
746 * @since 1.0.0
747 *
748 * @return array Arguments array
749 */
750 private function get_generate_tags_args(): array {
751 return [
752 'data' => [
753 'required' => true,
754 'type' => 'object',
755 'description' => 'Content data for tag generation'
756 ],
757 'context' => [
758 'required' => false,
759 'type' => 'string',
760 'enum' => ['site', 'post', 'page', 'product'],
761 'default' => 'site',
762 'description' => 'Context type'
763 ],
764 'platform' => [
765 'required' => false,
766 'type' => 'string',
767 'enum' => ['facebook', 'twitter', 'linkedin', 'pinterest'],
768 'default' => 'facebook',
769 'description' => 'Target platform (for Open Graph only)'
770 ]
771 ];
772 }
773
774 /**
775 * Get arguments for settings endpoints
776 *
777 * @since 1.0.0
778 *
779 * @return array Arguments array
780 */
781 private function get_settings_args(): array {
782 return [
783 'settings' => [
784 'required' => true,
785 'type' => 'object',
786 'description' => 'Social media settings to save'
787 ],
788 'context_type' => [
789 'required' => false,
790 'type' => 'string',
791 'enum' => ['site', 'post', 'page', 'product'],
792 'default' => 'site',
793 'description' => 'Context type'
794 ],
795 'context_id' => [
796 'required' => false,
797 'type' => 'integer',
798 'minimum' => 1,
799 'description' => 'Context ID (required for non-site contexts)'
800 ],
801 'validation_context' => [
802 'required' => false,
803 'type' => 'string',
804 'enum' => ['all', 'open-graph', 'twitter-cards', 'platforms', 'preview'],
805 'default' => 'all',
806 'description' => 'Validation context for focused validation'
807 ]
808 ];
809 }
810 }
811