From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh

---
 inc/integrations/services/Facebook.php | 1193 +++++++++++++++++++++++++++-------------------------------
 1 files changed, 555 insertions(+), 638 deletions(-)

diff --git a/inc/integrations/services/Facebook.php b/inc/integrations/services/Facebook.php
index 842cc96..fcf7473 100644
--- a/inc/integrations/services/Facebook.php
+++ b/inc/integrations/services/Facebook.php
@@ -5,9 +5,20 @@
  */
 
 namespace JVBase\integrations\services;
+use Exception;
+use JVBase\integrations\Admin;
+use JVBase\Instance;
 use JVBase\integrations\Integrations;
+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\Meta;
-use WP_Post;
+use JVBase\meta\Sanitizer;
 use WP_Error;
 
 if (!defined('ABSPATH')) {
@@ -16,686 +27,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'];
-	}
 }

--
Gitblit v1.10.0