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

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