From a239ef33ec489ce6d0f25ac2f256df0ac54a7e50 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 16 Sep 2026 00:07:34 +0000
Subject: [PATCH] =Tackling the integrations again. This time Helcim, with some minnor edits to GMB and Square

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

diff --git a/inc/integrations/services/Facebook.php b/inc/integrations/services/Facebook.php
index 842cc96..3c54f91 100644
--- a/inc/integrations/services/Facebook.php
+++ b/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'];
-	}
 }

--
Gitblit v1.10.0