<?php
|
/**
|
* Facebook Integration
|
* Handles Facebook posts, page information, events, and OAuth authentication
|
*/
|
namespace JVBase\integrations;
|
|
use Exception;
|
use JVBase\meta\Meta;
|
use WP_Error;
|
use WP_Post;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class Facebook extends Integrations
|
{
|
// Facebook-specific properties
|
private string $page_id = '';
|
private string $page_access_token = '';
|
private array $permissions = [];
|
private array $pages = [];
|
|
// Post type mapping for Facebook
|
private const FB_POST_TYPES = [
|
'post' => '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'])) {
|
?>
|
<div class="form-field">
|
<label for="fb_page">Active Facebook Page</label>
|
<select name="credentials[page_id]" id="fb_page">
|
<option value="">Select a page...</option>
|
<?php foreach ($this->credentials['pages'] as $page): ?>
|
<option value="<?php echo esc_attr($page['id']); ?>"
|
<?php selected($this->credentials['page_id'] ?? '', $page['id']); ?>>
|
<?php echo esc_html($page['name']); ?>
|
</option>
|
<?php endforeach; ?>
|
</select>
|
</div>
|
<?php
|
}
|
}
|
|
/**
|
* Validate webhook from Facebook
|
*/
|
protected function validateWebhook(array $payload): bool
|
{
|
// Handle webhook verification challenge
|
if (isset($payload['hub_mode']) && $payload['hub_mode'] === 'subscribe') {
|
$verify_token = $this->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'];
|
}
|
}
|