'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'];
}
}