Jake Vanderwerf
2026-08-28 a9654f00c8fbc9934289d761843ddd071025013f
inc/integrations/services/Facebook.php
@@ -5,9 +5,19 @@
 */
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Admin;
use JVBase\Instance;
use JVBase\integrations\Integrations;
use JVBase\meta\Meta;
use WP_Post;
use JVBase\integrations\OAuth;
use JVBase\integrations\OAuthCredentials;
use JVBase\integrations\OAuthURLs;
use JVBase\integrations\Requests;
use JVBase\integrations\SyncDefaults;
use JVBase\integrations\SyncFrom;
use JVBase\integrations\SyncTo;
use JVBase\integrations\UserConnection;
use JVBase\meta\Sanitizer;
use WP_Error;
if (!defined('ABSPATH')) {
@@ -16,686 +26,592 @@
class Facebook extends Integrations
{
   // Facebook-specific properties
   protected array $allowedContent = [
      'post',
      'photo',
      'video',
      'event',
      'offer',
      'note',
      'milestone'
   ];
   private string $page_id = '';
   private string $page_access_token = '';
   private array $permissions = [];
   private array $pages = [];
   use Admin,
      Instance,
      OAuth,
      UserConnection,
      Requests,
      SyncFrom,
      SyncTo,
      SyncDefaults;
   // Post type mapping for Facebook
   private const FB_POST_TYPES = [
      'post' => 'feed',
      'photo' => 'photos',
      'video' => 'videos',
      'event' => 'events',
      'offer' => 'offers',
      'note' => 'notes',
      'milestone' => 'milestones'
   ];
   protected static array $instances = [];
   public static function getInstance(?int $userID = null):self
   {
      $key = is_null($userID) ? 'base' : $userID;
      if (!array_key_exists($key, self::$instances)) {
         self::$instances[$key] = new self($userID);
      }
      return self::$instances[$key];
   }
   protected function __construct(?int $userID = null)
   protected function __construct()
   {
      $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 = 'v26.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
         'create' => true,
         'update' => false,
         'delete' => false
      ];
      // Rate limits for Facebook Graph API
      $this->rate_limits = [
         'per_second' => 10,
         'per_minute' => 600,
         'per_hour' => 200000
      ];
      $this->defineFields();
      $this->defineInstructions();
      $this->defineOAuth();
      $this->defineActions();
      $this->defineContentTypes();
      $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->baseUrl = 'https://graph.facebook.com/'.$this->apiVersion;
      $this->baseOAuthUrl = 'https://www.facebook.com/'.$this->apiVersion;
      $this->advanced = [
      ];
      $this->instructions = [
      ];
      $this->defaults = [
      ];
      $this->hasWebhooks = true;
      $this->supportsWebp = false;
      parent::__construct();
      $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'] ?? [];
      protected function defineFields():void
      {
         $client_id = $this->addField(
            'client_id',
            'App ID',
            'text',
            'manage_options'
         );
            $client_id->setRequired();
            $client_id->setHint('Your Facebook App ID from developers.facebook.com');
      // 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"
         ];
         $client_secret = $this->addField(
            'client_secret',
            'App Secret',
            'text',
            'manage_options'
         );
            $client_secret->setRequired();
            $client_secret->setSubType('password');
         $pages = $this->addField('pages', 'Pages', 'select', [$this, 'testUserCanConnect']);
            $pages->setHidden();
            $pages->setOptions($this->getPageOptions());
         $page = $this->addField('page_id', 'Connect to Page', 'select', [$this, 'testUserCanConnect']);
            $page->setOptions($this->loadCredentials()['pages']??[]);
            $page->setHint('Select the page you want to connect this site to.');
      }
   }
   /**
    * 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'];
            }
         public function testUserCanConnect():bool
         {
            $user = get_current_user_id();
            return $this->userCanConnect($user);
         }
         protected function getPageOptions():array
         {
            return $this->loadCredentials()['pages']??[];
         }
      protected function defineInstructions():void
      {
         // 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']
      protected function defineActions():void
      {
         $this->addAction(
            'Select Page',
            [$this, 'handleSelectPage'],
            'select_page',
            'plugs-connected'
         );
         if (is_wp_error($response)) {
         $this->addAction(
            'Fetch Pages',
            [$this, 'getPages'],
            'fetch_pages',
            'download-simple'
         );
      }
      public function defineOAuth():void
      {
         $this->oauth = new OAuthURLs(
            'https://www.facebook.com/'.$this->apiVersion.'/dialog/oauth',
            'https://graph.facebook.com/'.$this->apiVersion.'/oauth/access_token',
            'https://graph.facebook.com/'.$this->apiVersion.'/me/permissions'
         );
         $this->oathScopes = [
            'pages_show_list',
            'business_management',
            'pages_manage_metadata',
            'pages_manage_posts'
         ];
      }
      public function getServiceDescription():string
      {
         return 'Connect your Facebook Page to share posts, events, and engage with your audience.';
      }
      protected function defineContentTypes():void
      {
         $base = $this->getBaseFields();
         $this->contentTypes['post'] = array_merge($base, $this->contentTypePostFields());
         $this->contentTypes['image'] = array_merge($base, $this->contentTypeImageFields());
         //Looks like product requires some major heavy lifting. Best to skip for now
//       $this->contentTypes['product'] = array_merge($base, $this->contentTypeProductFields());
      }
         protected function getBaseFields():array
         {
            return  [
            ];
         }
         protected function contentTypePostFields():array
         {
            return [];
         }
         protected function contentTypeImageFields():array
         {
            return [];
         }
   protected function initialize(): void
   {
      // TODO: Implement initialize() method.
   }
   protected function refreshAccessToken(OAuthCredentials $credentials): array
   {
      return $this->response(false, 'Not set up yet');
   }
   protected function getRequestHeaders(): array
   {
      return [
         'Content-Type' => 'application/json',
         'Accept' => 'application/json',
      ];
   }
   protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array
   {
      if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
         return $this->response(false, 'Rate limit exceeded');
      }
      $errors = $this->checkErrors($response);
      if (!empty($errors)) {
         return $errors;
      }
      $endpoint = str_replace($this->baseUrl, '', $endpoint);
      return match ($endpoint) {
         '/me/accounts?access_token='.$this->getAccessToken()  => $this->handleGetPagesResponse($response, $method),
         default => $response
      };
   }
      protected function checkErrors(WP_Error|array $response):array|false
      {
         if (!isset($response['error'])) {
            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;
         $error = (string)$response['error']['code'];
         switch ($error) {
            //Invalid parameter
            case '100':
            //Too many calls by this account. Wait for a bit
            case '80001':
            //Permissions error
            case '200':
            //Action has been deemed abusive, or otherwise disallowed
            case '368':
            //Missing or invalid image file
            case '324':
            //Invalid OAuth 2.0 Access Token
            case '190':
               //Incorrect Signature
            case '104':
            //Desktop applications cannot call this function for other users
            case '240':
            //Requires additional permissions
            case '283':
            //Session is invalid; user has been checkpointed
            case '459':
            default:
               $this->logError(
                  'handleResponse',
                  'Something went wrong: '.$response['Message']??'',
                  $response
               );
               return $this->response(false, 'Something went wrong: '.$response['Message']??'');
         }
         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);
   /************************************************************
    * UTILITY METHODS
   ************************************************************/
   protected function getPageID():string|false
   {
      return $this->loadCredentials()['page_id']??false;
   }
   protected function getClientID():string|false
   {
      return $this->loadCredentials()['client_id']??false;
   }
   protected function getClientSecret():string|false
   {
      return $this->loadCredentials()['client_secret']??false;
   }
   /**
    * Process webhook from Facebook
    */
   protected function processWebhook(array $payload): bool
   protected function getFBUserID():string|false
   {
      $cred = $this->loadCredentials();
      if ($this->isSetUp() && !array_key_exists('user_id', $cred)) {
         $resp = $this->getRequest(
            $this->baseUrl.'/me?access_token='.$cred['access_token'],
         );
         $tmp = [];
         if ($resp && !is_wp_error($resp)) {
            $tmp['user_id'] = $resp['id'];
            $tmp['user_name'] = $resp['name'];
            $this->saveCredentials($tmp, false);
         }
         $cred = array_merge($cred, $tmp);
      }
      return $cred['user_id']??false;
   }
   public function getPages():array|false
   {
      if (!$this->getAccessToken()) {
         return false;
      }
      try {
         return $this->getRequest(
            $this->apiBase.'/me/accounts?access_token='.$this->getAccessToken(),
         );
      } catch (Exception $e) {
         $this->logError(
            'getPages',
            'Failed to load pages',
            [
               'error'  => $e->getMessage(),
            ]
         );
      }
      return [];
   }
      protected function handleGetPagesResponse(WP_Error|array $response, string $method):array
      {
         $options = [];
         foreach ($response as $page) {
            if (array_key_exists('id', $page) && array_key_exists('name', $page)) {
               $options[$page['id']] = $page['name'];
            }
         }
         if (!empty($options)) {
            $this->saveCredentials(['pages' => $options]);
         }
         return $this->response(true, 'Found pages', $options);
      }
   /************************************************************
    * ACTIONS
   ************************************************************/
   public function handleSelectPage(array $data):array
   {
      if (empty($data['page_id']??'')) {
         return $this->response(false, 'No page selected.');
      }
      $cred = [
         'page_id'   => sanitize_text_field($data['page_id']),
      ];
      return $this->saveCredentials($cred, false);
   }
   public function handleFetchPages():array
   {
      try {
         $object = $payload['object'] ?? '';
         $entry = $payload['entry'] ?? [];
         $pages = $this->getPages();
         if (!$pages) {
            return $this->response(false, 'No pages found');
         }
         foreach ($entry as $item) {
            $changes = $item['changes'] ?? [];
         return $this->response(
            true,
            sprintf('Found %d page(s)', count($pages)),
            $pages
         );
      } catch (Exception $e) {
         return $this->response(false, 'Failed to fetch pages: '.$e->getMessage());
      }
   }
            foreach ($changes as $change) {
               $field = $change['field'] ?? '';
               $value = $change['value'] ?? [];
   protected function setContentTypes(): void
   {
      // TODO: Implement setContentTypes() method.
   }
               switch ($field) {
                  case 'feed':
                     $this->handleFeedUpdate($value);
                     break;
   protected function formatForService(int $itemID, string $type = 'post'): array
   {
      return [];
   }
                  case 'conversations':
                     $this->handleMessageUpdate($value);
                     break;
   public function getOrCreateImage(int $imgID, string $caption = '', bool $publish = true, bool $returnResponse = false):string|array|false
   {
      $imgID = $this->getSupportedImage($imgID);
                  case 'photos':
                     $this->handlePhotoUpdate($value);
                     break;
      $id = $this->getServiceItemID($imgID);
      if (!empty($id)) {
         return $id;
      }
                  default:
                     $this->logDebug('Unhandled webhook field', ['field' => $field]);
               }
      $url = wp_get_attachment_url($imgID);
      if (!file_exists($url)) {
         return false;
      }
      $args = [
         'url' => $url
      ];
      if (!empty($caption)) {
         $caption = html_entity_decode(wp_strip_all_tags($caption));
         if (!empty($caption)) {
            $args['caption'] = $caption;
         }
      }
      $alt_text = get_post_meta($imgID, '_wp_attachment_image_alt', true);
      if (!empty($alt_text)) {
         $args['alt_text_custom'] = esc_html($alt_text);
      }
      if (!$publish) {
         $args['published'] = false;
      }
      $args['access_token'] = $this->getAccessToken();
      $response = $this->postRequest(
         sprintf(
            '%s/%s/photos',
            $this->baseUrl,
            $this->getPageID(),
         ),
         $args
      );
      if (isset($response['id'])) {
         $this->setServiceItemID($imgID, $response['id']);
      }
      return $returnResponse ? $response : $response['id'];
   }
   /**********************************************************
    * POSTING UTILITIES
   **********************************************************/
   public function post(string $message):array
   {
      if (!$this->isSetUp()) {
         return $this->response(false, 'Not set up.');
      }
      $message = html_entity_decode(wp_strip_all_tags($message));
      if (empty($message)) {
         return $this->response(false, 'Message cannot be empty.');
      }
      $args = [
         'message'   => $message,
      ];
      $args['access_token'] = $this->getAccessToken();
      return $this->postRequest(
         sprintf(
            '%s/%s/feed',
            $this->baseUrl,
            $this->getPageID(),
         ),
         $args
      );
   }
   /**
    * From https://developers.facebook.com/docs/graph-api/reference/photo/#u_0_o_gP
    *
    * @param int $imgID
    * @param string $caption
    * @return array
    */
   public function postSingleImage(int $imgID, string $caption):array
   {
      if (!$this->isSetUp()) {
         return $this->response(false, 'Not set up yet');
      }
      $response = $this->getOrCreateImage($imgID, $caption);
      return $this->response(true, 'Posted Image', $response);
   }
   public function postGalleryImages(array $imgIDs, string $caption, int $postID = 0):array
   {
      if (!$this->isSetUp()) {
         return $this->response(false, 'Not set up yet');
      }
      $post = [];
      $i = 0;
      foreach ($imgIDs as $imgID) {
         $id = $this->getOrCreateImage($imgID, '', false);
         if ($id) {
            $post['attached_media['.$i.']'] = json_encode(['media_fbid' => $id]);
            $i++;
         }
      }
      if (empty($post)) {
         return $this->response(false, 'No images posted');
      }
      $post['access_token'] = $this->getAccessToken();
      $caption = html_entity_decode(wp_strip_all_tags($caption));
      if (!empty($caption)) {
         $post['message'] = $caption;
      }
      $response = $this->postRequest(
         sprintf(
            '%s/%s/feed',
            $this->baseUrl,
            $this->getPageID(),
         ),
         $post
      );
      if ($postID > 0 && isset($response['id'])) {
         $this->setServiceItemID($postID, $response['id']);
      }
      return $this->response(true, 'Successfully posted to Facebook', $response);
   }
   public function getReviews():array
   {
      if (!$this->isSetUp() || !$this->getPageID()) {
         return [];
      }
      $ratings = $this->getRequest(
         sprintf(
         '%s/%s/ratings',
            $this->baseUrl,
            $this->getPageID(),
         )
      );
      if (!isset($ratings['data'])) {
         return [];
      }
      return $ratings['data'];
   }
   /**
    * https://developers.facebook.com/docs/graph-api/reference/page/#parameters-3
    * @param array $data allowed keys are: about, description, phone, website, emails, location {json object with street, city, state, zip, country}
    * @return array
    * TODO: Cover photo? (cover)
    * TODO: Food style (food_styles)
    */
   public function updateBusiness(array $data):array
   {
      if (!$this->isSetUp() || !$this->getPageID()) {
         return $this->response(false, 'Not set up yet');
      }
      $data = array_filter($data, function($item) use ($data) {
         if (!in_array($item, ['about', 'description', 'phone', 'website', 'emails', 'location', 'hours'])) {
            $this->logError('updateBusiness', 'Attempted to set property that is not allowed: '.$item.' with value: '.print_r($data[$item], true));
            return false;
         }
         return true;
      }, ARRAY_FILTER_USE_KEY);
      $out = [];
      foreach ($data as $prop => $val) {
         $out[$prop] = match($prop) {
            'about', 'description'  => sanitize_text_field($val),
            'phone'  => Sanitizer::sanitizePhone($val),
            'emails' => $this->sanitizeEmails($val),
            'website'   => sanitize_url($val),
            'location'  => $this->sanitizeLocation($val),
            'hours'  => $this->format_hours($val),
            default => null
         };
      }
      $out = array_filter($out);
      if (empty($out)) {
         return $this->response(false, 'No data to update');
      }
      $out['access_token'] = $this->getAccessToken();
      $response = $this->postRequest(
         sprintf(
            '%s/%s',
            $this->baseUrl,
            $this->getPageID()
         ),
         $out
      );
      $success = $response['success']??false;
      return $this->response($success, $success ? 'Successfully updated information' : 'Failed to update information');
   }
      protected function format_hours(array $days):array|null
      {
         //FB expects it to be:
            // - first 3 characters of day of week (mon)
            // - either 1 or 2, to allow for two set hours (ex: noon break)
            // - open or close
         //example: mon_1_open: 11:00, mon_1_close: 18:00
         //To accomplish this, we will assume the hours are formatted as with the SEO schema
         $days = array_filter($days, function ($day) {
            return in_array($day, ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']);
         }, ARRAY_FILTER_USE_KEY);
         $out = [];
         foreach ($days as $day => $conf) {
            if (array_key_exists('isOpen', $conf) && $conf['isOpen'] === 1
               && array_key_exists('opens', $conf) && !empty($conf['opens'])
               && array_key_exists('closes', $conf) && !empty($conf['closes'])
            ) {
               $short = substr($day, 0, 3);
               $out[$short.'_1_open'] = $conf['opens']; //Should be stored in 24hr time already
               $out[$short.'_1_close'] = $conf['closes'];
            }
         }
         return true;
      } catch (Exception $e) {
         $this->logError('Webhook processing failed', [
            'error' => $e->getMessage(),
            'payload' => $payload
         ]);
         return false;
         return empty($out) ? null : $out;
      }
   }
   /**
    * Handle post save for Facebook sync
    */
   protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
   {
      if (!$this->hasOAuthCredentials()) {
         error_log('OAuth Not set up for '.$this->service_name);
         return;
      }
      $this->queueOperation(self::$syncTo, [
         'items'     => [$postID],
         'user'      => user_can($post->post_author, 'manage_options') ? null : $post->post_author
      ], [
         'delay'  => 60
      ]);
      Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
   }
   /**
    * Process queued operations
    */
   public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
   {
      try {
         $fb = (array_key_exists('user', $data) && $data['user'] !== 0) ? $this->getInstance((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;
      protected function sanitizeLocation(array $location):array|null
      {
         if (array_key_exists('province', $location)) {
            $location['state'] = $location['province'];
            unset($location['province']);
         }
      } 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'];
         if (array_key_exists('postal_code', $location)) {
            $location['zip'] = $location['postal_code'];
            unset($location['postal_code']);
         }
         $location = array_filter($location, function ($item) use ($location) {
            if (!in_array($item, ['street', 'city', 'state', 'zip', 'country'])) {
               $this->logError('sanitizeLocation', 'Attempted to set property that is not allowed: '.$item.', with value: '.print_r($location[$item], true));
               return false;
            }
            if (!is_string($location[$item])) {
               $this->logError('sanitizeLocation', 'Unexpected value for '.$item.': '.print_r($location[$item], true));
               return false;
            }
            return true;
         }, ARRAY_FILTER_USE_KEY);
         return empty($location) ? null : $location;
      }
      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) . '...';
      protected function sanitizeEmails(array $emails):array|null
      {
         $emails = array_filter($emails, function($item) {
            return (bool)filter_var($item, FILTER_VALIDATE_EMAIL);
         });
         return empty($emails) ? null : $emails;
      }
      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'];
   }
}