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 | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 347 insertions(+), 7 deletions(-)
diff --git a/inc/integrations/services/Facebook.php b/inc/integrations/services/Facebook.php
index 04c03f7..fcf7473 100644
--- a/inc/integrations/services/Facebook.php
+++ b/inc/integrations/services/Facebook.php
@@ -7,7 +7,7 @@
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Admin;
-use JVBase\integrations\Instance;
+use JVBase\Instance;
use JVBase\integrations\Integrations;
use JVBase\integrations\OAuth;
use JVBase\integrations\OAuthCredentials;
@@ -17,6 +17,8 @@
use JVBase\integrations\SyncFrom;
use JVBase\integrations\SyncTo;
use JVBase\integrations\UserConnection;
+use JVBase\meta\Meta;
+use JVBase\meta\Sanitizer;
use WP_Error;
if (!defined('ABSPATH')) {
@@ -40,22 +42,25 @@
$this->title = 'Facebook';
$this->icon = 'facebook-logo';
- $this->apiVersion = 'v25.0';
+ $this->apiVersion = 'v26.0';
$this->canSync = [
'create' => true,
- 'update' => true,
- 'delete' => true
+ 'update' => false,
+ 'delete' => false
];
$this->defineFields();
$this->defineInstructions();
$this->defineOAuth();
$this->defineActions();
+ $this->defineContentTypes();
$this->baseUrl = 'https://graph.facebook.com/'.$this->apiVersion;
$this->baseOAuthUrl = 'https://www.facebook.com/'.$this->apiVersion;
+ $this->supportsWebp = false;
+
parent::__construct();
}
@@ -134,6 +139,31 @@
{
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.
@@ -148,23 +178,64 @@
{
return [
'Content-Type' => 'application/json',
- 'Accept' => 'application/json'
+ 'Accept' => 'application/json',
];
}
- protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
+ 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 => $this->response(false, 'Unhandled Response for ' . $endpoint . ' endpoint, with method: ' . $method),
+ default => $response
};
}
+ protected function checkErrors(WP_Error|array $response):array|false
+ {
+ if (!isset($response['error'])) {
+ return false;
+ }
+ $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']??'');
+ }
+ }
/************************************************************
* UTILITY METHODS
************************************************************/
@@ -188,6 +259,7 @@
$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'];
@@ -275,4 +347,272 @@
{
return [];
}
+
+ public function getOrCreateImage(int $imgID, string $caption = '', bool $publish = true, bool $returnResponse = false):string|array|false
+ {
+ $imgID = $this->getSupportedImage($imgID);
+
+ $id = $this->getServiceItemID($imgID);
+ if (!empty($id)) {
+ return $id;
+ }
+
+ $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 empty($out) ? null : $out;
+ }
+ protected function sanitizeLocation(array $location):array|null
+ {
+ if (array_key_exists('province', $location)) {
+ $location['state'] = $location['province'];
+ unset($location['province']);
+ }
+ 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;
+ }
+ 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;
+ }
}
--
Gitblit v1.10.0