social_manager = new Social_Meta_Manager(); } /** * Register API routes * * @since 1.0.0 */ public function register_routes(): void { // Social media settings management register_rest_route( $this->namespace, '/' . $this->rest_base . '/settings', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_settings'], 'permission_callback' => [$this, 'check_read_permissions'], 'args' => $this->get_context_route_args() ], [ 'methods' => 'POST', 'callback' => [$this, 'update_settings'], 'permission_callback' => [$this, 'check_manage_permissions'], 'args' => $this->get_settings_args() ] ] ); // Social media settings validation register_rest_route( $this->namespace, '/' . $this->rest_base . '/validate', [ [ 'methods' => 'POST', 'callback' => [$this, 'validate_settings'], 'permission_callback' => [$this, 'check_read_permissions'], 'args' => $this->get_settings_args() ] ] ); // Generate social media preview register_rest_route( $this->namespace, '/' . $this->rest_base . '/preview', [ [ 'methods' => 'POST', 'callback' => [$this, 'generate_preview'], 'permission_callback' => [$this, 'check_read_permissions'], 'args' => $this->get_preview_args() ] ] ); // Optimize image for social platforms register_rest_route( $this->namespace, '/' . $this->rest_base . '/optimize-image', [ [ 'methods' => 'POST', 'callback' => [$this, 'optimize_image'], 'permission_callback' => [$this, 'check_manage_permissions'], 'args' => $this->get_optimize_image_args() ] ] ); // Get social meta for context register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[a-zA-Z]+)/(?P\d+)', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_social_meta'], 'permission_callback' => [$this, 'check_read_permissions'], 'args' => $this->get_context_args() ], [ 'methods' => 'POST', 'callback' => [$this, 'save_social_meta'], 'permission_callback' => [$this, 'check_manage_permissions'], 'args' => array_merge($this->get_context_args(), $this->get_social_meta_args()) ] ] ); // Generate Open Graph tags register_rest_route( $this->namespace, '/' . $this->rest_base . '/generate-og', [ [ 'methods' => 'POST', 'callback' => [$this, 'generate_og_tags'], 'permission_callback' => [$this, 'check_read_permissions'], 'args' => $this->get_generate_tags_args() ] ] ); // Generate Twitter Card tags register_rest_route( $this->namespace, '/' . $this->rest_base . '/generate-twitter', [ [ 'methods' => 'POST', 'callback' => [$this, 'generate_twitter_tags'], 'permission_callback' => [$this, 'check_read_permissions'], 'args' => $this->get_generate_tags_args() ] ] ); } /** * Get social media settings * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object, or the context error */ public function get_settings(WP_REST_Request $request) { try { // SECURITY: the settings this returns carry the object's social // title, description and image. update_settings() already authorises // the object; the read has to as well (#385). $context = $this->resolve_request_context($request); if (is_wp_error($context)) { return $context; } [$context_type, $context_id] = $context; // Get settings from Social Meta Manager $settings = $this->social_manager->get_settings($context_type, $context_id); // Get settings schema for validation $schema = $this->social_manager->get_settings_schema($context_type); return new WP_REST_Response([ 'success' => true, 'data' => [ 'settings' => $settings, 'schema' => $schema, 'context_type' => $context_type, 'context_id' => $context_id ], 'message' => 'Social media settings retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_REST_Response([ 'success' => false, 'error' => 'Failed to retrieve settings: ' . $e->getMessage() ], 500); } } /** * Update social media settings * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error * * @throws \Exception On failure. */ public function update_settings(WP_REST_Request $request) { try { $settings = $request->get_param('settings'); $context_type = $request->get_param('context_type') ?? 'site'; $context_id = $request->get_param('context_id'); $validation_context = $request->get_param('validation_context') ?? 'all'; if (empty($settings)) { return new WP_REST_Response([ 'success' => false, 'error' => 'Settings data is required' ], 400); } // SECURITY: this route writes the same per-object social overrides // as save_social_meta(), so it needs the same object-level guard — // the section-level thinkrank_social_media capability alone would // let a delegated user write to any post (IDOR). validate_context() // carries that guard for every context route in this class. $context_id = $context_id === null ? null : (int) $context_id; $context_validation = $this->validate_context($context_type, $context_id); if (is_wp_error($context_validation)) { return $context_validation; } // Drop unrecognized keys so arbitrary client-supplied keys aren't // persisted (storage bloat / settings drift). The known set is the // context's default settings, exposed through a filter for add-ons. $known = array_keys($this->social_manager->get_default_settings($context_type)); $known = apply_filters('thinkrank_social_known_setting_keys', $known, $context_type); $settings = array_intersect_key($settings, array_flip($known)); if (empty($settings)) { return new WP_REST_Response([ 'success' => false, 'error' => 'No recognized social settings were provided' ], 400); } // Validate settings with context $validation = $this->social_manager->validate_settings($settings, $validation_context); if (!$validation['valid']) { return new WP_Error( 'validation_failed', 'Settings validation failed', [ 'status' => 400, 'validation_errors' => $validation['errors'], 'validation_warnings' => $validation['warnings'] ] ); } // Save settings $result = $this->social_manager->save_settings($context_type, $context_id, $settings); if ($result) { // Get updated settings $updated_settings = $this->social_manager->get_settings($context_type, $context_id); return new WP_REST_Response([ 'success' => true, 'data' => [ 'settings' => $updated_settings, 'validation' => $validation, 'context_type' => $context_type, 'context_id' => $context_id ], 'message' => 'Social media settings updated successfully' ], 200); } else { throw new \Exception('Failed to save settings'); } } catch (\Exception $e) { return new WP_REST_Response([ 'success' => false, 'error' => 'Settings update failed: ' . $e->getMessage() ], 500); } } /** * Validate social media settings * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function validate_settings(WP_REST_Request $request) { try { // `settings` is registered required, so REST rejects the request // before this runs — the old `?? []` default was unreachable. $settings = $request->get_param('settings'); $context_type = $request->get_param('context_type') ?? 'site'; $validation_context = $request->get_param('validation_context') ?? 'all'; // Validate settings using the enhanced Social Meta Manager with tab-specific context $validation = $this->social_manager->validate_settings($settings, $validation_context); return new WP_REST_Response([ 'success' => true, 'data' => $validation ], 200); } catch (\Exception $e) { return new WP_Error( 'validation_error', 'Failed to validate social media settings: ' . $e->getMessage(), ['status' => 500] ); } } /** * Generate social media preview * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function generate_preview(WP_REST_Request $request) { try { $data = $request->get_param('data') ?? []; $platform = $request->get_param('platform') ?? 'facebook'; // Validate platform $supported_platforms = ['facebook', 'twitter', 'linkedin', 'pinterest']; if (!in_array($platform, $supported_platforms, true)) { return new WP_Error( 'invalid_platform', 'Unsupported platform for preview generation', ['status' => 400] ); } // Generate preview $preview_data = $this->social_manager->preview_social_post($data, $platform); return new WP_REST_Response([ 'success' => true, 'data' => $preview_data, 'message' => 'Social media preview generated successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'preview_failed', 'Social media preview generation failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Optimize image for social platforms * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function optimize_image(WP_REST_Request $request) { try { $image_url = $request->get_param('image_url'); $platform = $request->get_param('platform') ?? 'facebook'; // Validate image URL if (!filter_var($image_url, FILTER_VALIDATE_URL)) { return new WP_Error( 'invalid_image_url', 'Invalid image URL provided', ['status' => 400] ); } // Optimize image $optimized_image = $this->social_manager->optimize_social_image($image_url, $platform); return new WP_REST_Response([ 'success' => true, 'data' => $optimized_image, 'message' => 'Image optimized successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'optimization_failed', 'Image optimization failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get social meta for context * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function get_social_meta(WP_REST_Request $request) { try { $context_type = $request->get_param('context_type'); $context_id = (int) $request->get_param('context_id'); // Validate context and the caller's access to it. Returns true or a // WP_Error carrying the right status (400 shape, 403 authorization). $context_validation = $this->validate_context($context_type, $context_id); if (is_wp_error($context_validation)) { return $context_validation; } // Get social meta data $social_meta = $this->social_manager->get_output_data($context_type, $context_id); return new WP_REST_Response([ 'success' => true, 'data' => $social_meta, 'message' => 'Social meta retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'retrieval_failed', 'Social meta retrieval failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Save social meta for context * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error * * @throws \Exception On failure. */ public function save_social_meta(WP_REST_Request $request) { try { $context_type = $request->get_param('context_type'); $context_id = (int) $request->get_param('context_id'); $social_data = $request->get_param('social_data') ?? []; // Validate context and the caller's access to it. validate_context() // now carries the object-level edit_post guard for non-site contexts, // so this write path inherits the same check it used to make inline. $context_validation = $this->validate_context($context_type, $context_id); if (is_wp_error($context_validation)) { return $context_validation; } // Save social meta data (manager signature is // save_settings(context_type, context_id, settings)). $result = $this->social_manager->save_settings($context_type, $context_id, $social_data); if ($result) { return new WP_REST_Response([ 'success' => true, 'data' => $result, 'message' => 'Social meta saved successfully' ], 200); } else { throw new \Exception('Failed to save social meta data'); } } catch (\Throwable $e) { // Catch \Throwable (not just \Exception) so a future TypeError // degrades to a JSON error instead of a fatal. return new WP_Error( 'save_failed', 'Social meta save failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Generate Open Graph tags * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function generate_og_tags(WP_REST_Request $request) { try { $data = $request->get_param('data') ?? []; $context = $request->get_param('context') ?? 'site'; $platform = $request->get_param('platform') ?? 'facebook'; // Generate Open Graph tags $og_tags = $this->social_manager->generate_og_tags($data, $context, $platform); return new WP_REST_Response([ 'success' => true, 'data' => $og_tags, 'message' => 'Open Graph tags generated successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'og_generation_failed', 'Open Graph generation failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Generate Twitter Card tags * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function generate_twitter_tags(WP_REST_Request $request) { try { $data = $request->get_param('data') ?? []; $context = $request->get_param('context') ?? 'site'; // Generate Twitter Card tags $twitter_tags = $this->social_manager->generate_twitter_tags($data, $context); return new WP_REST_Response([ 'success' => true, 'data' => $twitter_tags, 'message' => 'Twitter Card tags generated successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'twitter_generation_failed', 'Twitter Card generation failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Check read permissions * * @since 1.0.0 * * @return bool Permission status */ public function check_read_permissions(): bool { return current_user_can('edit_posts'); } /** * Check manage permissions * * @since 1.0.0 * * @return bool Permission status */ public function check_manage_permissions(): bool { return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_social_media'); } /** * Get arguments for preview endpoint * * @since 1.0.0 * * @return array Arguments array */ private function get_preview_args(): array { return [ 'data' => [ 'required' => true, 'type' => 'object', 'description' => 'Content data for preview generation' ], 'platform' => [ 'required' => false, 'type' => 'string', 'enum' => ['facebook', 'twitter', 'linkedin', 'pinterest'], 'default' => 'facebook', 'description' => 'Target platform for preview' ] ]; } /** * Get arguments for image optimization endpoint * * @since 1.0.0 * * @return array Arguments array */ private function get_optimize_image_args(): array { return [ 'image_url' => [ 'required' => true, 'type' => 'string', 'format' => 'uri', 'description' => 'Image URL to optimize' ], 'platform' => [ 'required' => false, 'type' => 'string', 'enum' => ['facebook', 'twitter', 'linkedin', 'pinterest'], 'default' => 'facebook', 'description' => 'Target platform for optimization' ] ]; } /** * Get arguments for context endpoints * * @since 1.0.0 * * @return array Arguments array */ private function get_context_args(): array { return [ 'context_type' => [ 'required' => true, 'type' => 'string', 'enum' => ['site', 'post', 'page', 'product'], 'description' => 'Context type' ], 'context_id' => [ 'required' => true, 'type' => 'integer', 'minimum' => 1, 'description' => 'Context ID' ] ]; } /** * Get arguments for social meta save endpoint * * @since 1.0.0 * * @return array Arguments array */ private function get_social_meta_args(): array { return [ 'social_data' => [ 'required' => true, 'type' => 'object', 'description' => 'Social media meta data to save' ] ]; } /** * Get arguments for tag generation endpoints * * @since 1.0.0 * * @return array Arguments array */ private function get_generate_tags_args(): array { return [ 'data' => [ 'required' => true, 'type' => 'object', 'description' => 'Content data for tag generation' ], 'context' => [ 'required' => false, 'type' => 'string', 'enum' => ['site', 'post', 'page', 'product'], 'default' => 'site', 'description' => 'Context type' ], 'platform' => [ 'required' => false, 'type' => 'string', 'enum' => ['facebook', 'twitter', 'linkedin', 'pinterest'], 'default' => 'facebook', 'description' => 'Target platform (for Open Graph only)' ] ]; } /** * Get arguments for settings endpoints * * @since 1.0.0 * * @return array Arguments array */ private function get_settings_args(): array { return [ 'settings' => [ 'required' => true, 'type' => 'object', 'description' => 'Social media settings to save' ], 'context_type' => [ 'required' => false, 'type' => 'string', 'enum' => ['site', 'post', 'page', 'product'], 'default' => 'site', 'description' => 'Context type' ], 'context_id' => [ 'required' => false, 'type' => 'integer', 'minimum' => 1, 'description' => 'Context ID (required for non-site contexts)' ], 'validation_context' => [ 'required' => false, 'type' => 'string', 'enum' => ['all', 'open-graph', 'twitter-cards', 'platforms', 'preview'], 'default' => 'all', 'description' => 'Validation context for focused validation' ] ]; } }