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

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