'feed', 'photo' => 'photos', 'video' => 'videos', 'event' => 'events', 'offer' => 'offers', 'note' => 'notes', 'milestone' => 'milestones' ]; public function __construct(?int $userID = null) { $this->service_name = 'facebook'; $this->title = 'Facebook'; $this->icon = 'facebook-logo'; // API Configuration $this->apiBase = [ 'graph' => 'https://graph.facebook.com/v18.0', 'auth' => 'https://www.facebook.com/v18.0' ]; $this->apiVersion = 'v18.0'; $this->isOAuthService = true; // OAuth Configuration $this->oauth = [ 'authorize' => 'https://www.facebook.com/v18.0/dialog/oauth', 'token' => 'https://graph.facebook.com/v18.0/oauth/access_token', 'revoke' => 'https://graph.facebook.com/v18.0/me/permissions', 'scopes' => [ 'pages_show_list', 'pages_read_engagement', 'pages_manage_posts', 'pages_manage_metadata', 'pages_manage_engagement', 'pages_read_user_content', 'business_management', 'instagram_basic', 'instagram_content_publish' ] ]; // Sync capabilities $this->canSync = [ 'initial' => true, 'update' => true, 'delete' => true ]; // Rate limits for Facebook Graph API $this->rate_limits = [ 'per_second' => 10, 'per_minute' => 600, 'per_hour' => 200000 ]; $this->fields = [ 'client_id' => [ 'type' => 'text', 'placeholder' => 'Enter Facebook App ID', 'hint' => 'Your Facebook App ID from developers.facebook.com', 'label' => 'App ID', 'required' => true, ], 'client_secret' => [ 'type' => 'text', 'subtype'=> 'password', 'label' => 'App Secret', 'required' => true, 'hint' => 'Your Facebook App Secret (keep this secure!)' ], ]; $this->advanced = [ ]; $this->instructions = [ ]; $this->defaults = [ ]; $this->handleWebhooks = true; parent::__construct($userID); $this->actions = array_merge($this->actions, [ 'sync_pages' => 'getUserPages' ]); } /** * Initialize Facebook-specific properties */ protected function initialize(): void { $this->page_id = $this->credentials['page_id'] ?? ''; $this->page_access_token = $this->credentials['page_access_token'] ?? ''; $this->permissions = $this->credentials['permissions'] ?? []; // Build dynamic endpoints based on page if ($this->page_id) { $this->apiEndpoints = [ "/{$this->page_id}/feed", "/{$this->page_id}/photos", "/{$this->page_id}/videos", "/{$this->page_id}/events", "/{$this->page_id}/insights", "/{$this->page_id}/conversations", "/{$this->page_id}", "/me/accounts" ]; } } /** * Get service description */ public function getServiceDescription(): string { return "Connect your Facebook Page to share posts, events, and engage with your audience."; } /** * Get request headers for Facebook API */ protected function getRequestHeaders(): array { $headers = [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ]; // Use page access token if available, otherwise user token if (!empty($this->page_access_token)) { // Token is sent in URL params for Facebook, not headers } return $headers; } /** * Add Facebook-specific OAuth parameters */ protected function addOAuthParams(array $params): array { $params['display'] = 'popup'; $params['auth_type'] = 'rerequest'; $params['return_scopes'] = 'true'; return $params; } /** * Add Facebook-specific credential data after OAuth */ protected function addCredentialData(array $credentials, array $tokens): array { // Get user's pages after OAuth try { $pages = $this->getUserPages($tokens['access_token']); if (!empty($pages)) { $credentials['pages'] = $pages; // Auto-select first page if not already selected if (empty($credentials['page_id']) && !empty($pages[0])) { $credentials['page_id'] = $pages[0]['id']; $credentials['page_access_token'] = $pages[0]['access_token']; $credentials['page_name'] = $pages[0]['name']; } } // Store permissions granted $permissions = $this->getGrantedPermissions($tokens['access_token']); $credentials['permissions'] = $permissions; } catch (Exception $e) { $this->logError('Failed to fetch Facebook pages', ['error' => $e->getMessage()]); } return $credentials; } /** * Test Facebook connection */ protected function performConnectionTest(): bool { if (empty($this->credentials['access_token'])) { return false; } try { // Test by fetching user info $response = wp_remote_get( $this->apiBase['graph'] . '/me?access_token=' . $this->credentials['access_token'] ); if (is_wp_error($response)) { return false; } $code = wp_remote_retrieve_response_code($response); return $code === 200; } catch (Exception $e) { $this->logError('Connection test failed', ['error' => $e->getMessage()]); return false; } } /** * Render Additional OAuth settings */ protected function renderOAuthConnectedOptions(): void { // Page selection dropdown if (!empty($this->credentials['pages'])) { ?>
credentials['webhook_verify_token'] ?? ''; if ($payload['hub_verify_token'] === $verify_token) { // Echo the challenge for verification echo $payload['hub_challenge']; return true; } return false; } // Validate webhook signature for actual events $headers = $payload['_headers'] ?? []; $signature = $headers['x-hub-signature-256'] ?? ''; if (empty($signature)) { $this->logError('Missing webhook signature'); return false; } // Calculate expected signature $app_secret = $this->credentials['client_secret'] ?? ''; $expected = 'sha256=' . hash_hmac('sha256', json_encode($payload), $app_secret); return hash_equals($expected, $signature); } /** * Process webhook from Facebook */ protected function processWebhook(array $payload): bool { try { $object = $payload['object'] ?? ''; $entry = $payload['entry'] ?? []; foreach ($entry as $item) { $changes = $item['changes'] ?? []; foreach ($changes as $change) { $field = $change['field'] ?? ''; $value = $change['value'] ?? []; switch ($field) { case 'feed': $this->handleFeedUpdate($value); break; case 'conversations': $this->handleMessageUpdate($value); break; case 'photos': $this->handlePhotoUpdate($value); break; default: $this->logDebug('Unhandled webhook field', ['field' => $field]); } } } return true; } catch (Exception $e) { $this->logError('Webhook processing failed', [ 'error' => $e->getMessage(), 'payload' => $payload ]); return false; } } /** * Handle post save for Facebook sync */ protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void { $post_type = $settings['fb_type'] ?? 'post'; $sync_immediately = $settings['immediate'] ?? false; // Determine the Facebook post type $fb_endpoint = self::FB_POST_TYPES[$post_type] ?? 'feed'; $operation_data = [ 'post_id' => $postID, 'fb_type' => $post_type, 'endpoint' => $fb_endpoint, 'page_id' => $this->page_id ]; // Check for scheduled posting $schedule_time = get_post_meta($postID, BASE . 'schedule_facebook', true); $options = []; if ($schedule_time && strtotime($schedule_time) > time()) { $options['scheduled'] = strtotime($schedule_time); } elseif (!$sync_immediately) { $options['delay'] = 300; // 5 minute delay for batching } $operation = $update ? 'update_post' : 'create_post'; $this->queueOperation($operation, $operation_data, $options); } /** * Process queued operations */ public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array { try { $fb = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this; switch ($operation->type) { case 'facebook_create_post': return $fb->createFacebookPost($data); case 'facebook_update_post': return $fb->updateFacebookPost($data); case 'facebook_delete_post': return $fb->deleteFacebookPost($data); case 'facebook_create_event': return $fb->createFacebookEvent($data); default: return $result; } } catch (Exception $e) { $this->logError('Operation failed', [ 'type' => $operation->type, 'error' => $e->getMessage() ]); return new WP_Error('operation_failed', $e->getMessage()); } } /** * Create a Facebook post */ private function createFacebookPost(array $data): array { $post = get_post($data['post_id']); if (!$post) { return ['success' => false, 'error' => 'Post not found']; } // Build Facebook post data $fb_data = [ 'message' => $this->formatPostContent($post), 'access_token' => $this->page_access_token ]; // Add link if present $link = get_post_meta($post->ID, 'link_url', true); if ($link) { $fb_data['link'] = $link; } // Handle featured image if (has_post_thumbnail($post->ID)) { $image_url = get_the_post_thumbnail_url($post->ID, 'large'); $fb_data['picture'] = $image_url; } // Determine endpoint $endpoint = $data['endpoint'] ?? 'feed'; $url = $this->apiBase['graph'] . '/' . $this->page_id . '/' . $endpoint; // Make the API request $response = wp_remote_post($url, [ 'body' => $fb_data, 'timeout' => 30 ]); if (is_wp_error($response)) { return ['success' => false, 'error' => $response->get_error_message()]; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['id'])) { // Store Facebook post ID update_post_meta($post->ID, BASE . '_facebook_post_id', $body['id']); update_post_meta($post->ID, BASE . '_facebook_sync_time', time()); return [ 'success' => true, 'facebook_id' => $body['id'], 'message' => 'Posted to Facebook successfully' ]; } return ['success' => false, 'error' => $body['error']['message'] ?? 'Unknown error']; } /** * Update a Facebook post */ private function updateFacebookPost(array $data): array { $post = get_post($data['post_id']); $fb_post_id = get_post_meta($post->ID, BASE . '_facebook_post_id', true); if (!$fb_post_id) { // No existing post, create new one return $this->createFacebookPost($data); } // Facebook doesn't allow editing most post types after creation // We can only update certain fields like message $fb_data = [ 'message' => $this->formatPostContent($post), 'access_token' => $this->page_access_token ]; $url = $this->apiBase['graph'] . '/' . $fb_post_id; $response = wp_remote_request($url, [ 'method' => 'POST', 'body' => $fb_data ]); if (is_wp_error($response)) { return ['success' => false, 'error' => $response->get_error_message()]; } update_post_meta($post->ID, BASE . '_facebook_sync_time', time()); return ['success' => true, 'message' => 'Updated on Facebook']; } /** * Delete a Facebook post */ private function deleteFacebookPost(array $data): array { $fb_post_id = get_post_meta($data['post_id'], BASE . '_facebook_post_id', true); if (!$fb_post_id) { return ['success' => true, 'message' => 'No Facebook post to delete']; } $url = $this->apiBase['graph'] . '/' . $fb_post_id . '?access_token=' . $this->page_access_token; $response = wp_remote_request($url, ['method' => 'DELETE']); if (!is_wp_error($response)) { delete_post_meta($data['post_id'], BASE . '_facebook_post_id'); delete_post_meta($data['post_id'], BASE . '_facebook_sync_time'); } return ['success' => true, 'message' => 'Deleted from Facebook']; } /** * Create a Facebook event */ private function createFacebookEvent(array $data): array { $post = get_post($data['post_id']); $meta = Meta::forPost($post->ID); $event_data = [ 'name' => $post->post_title, 'description' => $this->formatPostContent($post), 'start_time' => $meta->get('event_start_date'), 'end_time' => $meta->get('event_end_date'), 'access_token' => $this->page_access_token ]; // Add location if available $location = $meta->get('event_location'); if ($location) { $event_data['location'] = $location; } $url = $this->apiBase['graph'] . '/' . $this->page_id . '/events'; $response = wp_remote_post($url, [ 'body' => $event_data ]); if (is_wp_error($response)) { return ['success' => false, 'error' => $response->get_error_message()]; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['id'])) { update_post_meta($post->ID, BASE . '_facebook_event_id', $body['id']); return ['success' => true, 'event_id' => $body['id']]; } return ['success' => false, 'error' => $body['error']['message'] ?? 'Unknown error']; } /** * Helper: Get user's Facebook pages */ private function getUserPages(?string $access_token = null): array { $token = $access_token ?: $this->credentials['access_token']; $url = $this->apiBase['graph'] . '/me/accounts?access_token=' . $token; $response = wp_remote_get($url); if (is_wp_error($response)) { throw new Exception($response->get_error_message()); } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['error'])) { throw new Exception($body['error']['message']); } return $body['data'] ?? []; } /** * Helper: Get granted permissions */ private function getGrantedPermissions(string $access_token): array { $url = $this->apiBase['graph'] . '/me/permissions?access_token=' . $access_token; $response = wp_remote_get($url); if (is_wp_error($response)) { return []; } $body = json_decode(wp_remote_retrieve_body($response), true); $permissions = []; foreach (($body['data'] ?? []) as $permission) { if ($permission['status'] === 'granted') { $permissions[] = $permission['permission']; } } return $permissions; } /** * Helper: Format post content for Facebook */ private function formatPostContent(WP_Post $post): string { $content = $post->post_content; // Strip shortcodes $content = strip_shortcodes($content); // Convert to plain text $content = wp_strip_all_tags($content); // Add link to full post $permalink = get_permalink($post->ID); $content .= "\n\nRead more: " . $permalink; // Trim to Facebook's character limit (63,206 characters) if (strlen($content) > 63000) { $content = substr($content, 0, 63000) . '...'; } return $content; } /** * Helper: Handle feed updates from webhook */ private function handleFeedUpdate(array $data): void { // Process incoming feed updates // Could sync comments, reactions, etc. back to WordPress $this->logDebug('Feed update received', $data); } /** * Helper: Handle message updates from webhook */ private function handleMessageUpdate(array $data): void { // Process incoming messages // Could create notifications or queue responses $this->logDebug('Message received', $data); } /** * Helper: Handle photo updates from webhook */ private function handlePhotoUpdate(array $data): void { // Process photo updates $this->logDebug('Photo update received', $data); } /** * Send test post to Facebook */ private function sendTestPost(): array { $test_data = [ 'message' => 'Test post from ' . get_bloginfo('name') . ' at ' . current_time('mysql'), 'access_token' => $this->page_access_token ]; $url = $this->apiBase['graph'] . '/' . $this->page_id . '/feed'; $response = wp_remote_post($url, [ 'body' => $test_data ]); if (is_wp_error($response)) { return ['success' => false, 'error' => $response->get_error_message()]; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['id'])) { return ['success' => true, 'post_id' => $body['id']]; } return ['success' => false, 'error' => $body['error']['message'] ?? 'Unknown error']; } }