File: //proc/self/cwd/wp-content/plugins/pixel-caffeine/includes/admin/class-aepc-facebook-adapter.php
<?php
/**
* Adapter for facebook API
*
* @package Pixel Caffeine
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use PixelCaffeine\Dependencies\Firebase\JWT\JWT;
use PixelCaffeine\Admin\Exception\FBAPIException;
use PixelCaffeine\Admin\Exception\FBAPILoginException;
use PixelCaffeine\Dependencies\Ramsey\Uuid\Uuid;
/**
* Adapter for facebook API
*
* @class AEPC_Facebook_Adapter
*/
class AEPC_Facebook_Adapter {
const FEED_SCHEDULE_INTERVAL_HOURLY = 'HOURLY';
const FEED_SCHEDULE_INTERVAL_WEEKLY = 'WEEKLY';
const FEED_SCHEDULE_INTERVAL_DAILY = 'DAILY';
const FEED_SCHEDULE_WEEK_DAY_SUNDAY = 'SUNDAY';
const FEED_SCHEDULE_WEEK_DAY_MONDAY = 'MONDAY';
const FEED_SCHEDULE_WEEK_DAY_TUESDAY = 'TUESDAY';
const FEED_SCHEDULE_WEEK_DAY_WEDNESDAY = 'WEDNESDAY';
const FEED_SCHEDULE_WEEK_DAY_THURSDAY = 'THURSDAY';
const FEED_SCHEDULE_WEEK_DAY_FRIDAY = 'FRIDAY';
const FEED_SCHEDULE_WEEK_DAY_SATURDAY = 'SATURDAY';
const CA_RULE_AND = 'and';
const CA_RULE_OR = 'or';
const CA_RULE_CONTAINS = 'i_contains';
const CA_RULE_NOT_CONTAINS = 'i_not_contains';
const CA_RULE_EQ = 'eq';
const CA_RULE_NEQ = 'neq';
const CA_RULE_GTE = 'gte';
const CA_RULE_GT = 'gt';
const CA_RULE_LT = 'lt';
const CA_RULE_LTE = 'lte';
/**
* The token generated by FB for authorization
*
* @var string|false
*/
public $access_token = false;
/**
* The account ID
*
* @var string
*/
protected $account_id = '';
/**
* The pixel ID
*
* @var string
*/
protected $pixel_id = '';
/**
* The business ID
*
* @var string
*/
protected $business_id = '';
/**
* The base API URL
*
* @var string
*/
protected $api_url = 'https://wca-connector.adespresso.com';
/**
* The base API URL
*
* @var string
*/
protected $api_stage = 'prod';
/**
* Init facebook adapter
*/
public function __construct() {
// Get the access token and connect the website to facebook api through access token.
add_action( 'admin_init', array( $this, 'connect' ), 5 );
// Get the auth-response request to save the access token and login the site for the facebook requests.
add_action( 'admin_init', array( $this, 'login' ), 5 );
// Remove the access token to disconnect the site from facebook api and force the user to make a new login.
add_action( 'admin_init', array( $this, 'disconnect' ), 5 );
}
/**
* Define access token, where we can know if the user did login or not
*
* @return void
*/
public function connect() {
if ( defined( 'AEPC_API_URL' ) && defined( 'AEPC_API_STAGE' ) ) {
$this->api_url = AEPC_API_URL;
$this->api_stage = AEPC_API_STAGE;
}
// Get access token, it means logged in to facebook if it's not empty.
$token = get_option( 'aepc_fb_access_token' );
// Save access token and then login only if the token is not expired.
if ( ! empty( $token ) && ! $this->is_token_expired( $token ) ) {
$this->access_token = $token['access_token'];
// Set account and pixel ID from option set by user.
$this->set_account_id( get_option( 'aepc_account_id', '' ) );
$this->set_pixel_id( get_option( 'aepc_pixel_id', '' ) );
$this->set_business_id( get_option( 'aepc_business_id', '' ) );
}
}
/**
* Check if token expired or it's lifetime if token_expiration is 0
*
* @param array $token The authentication token.
*
* @return bool
*/
protected function is_token_expired( $token ) {
return $token['token_expiration'] > 0 && time() >= $token['token_expiration'];
}
/**
* Get the complete URL of the request to endpoint defined in parameter
*
* @param string $endpoint It must start with /.
* @param array $args The array of arguments for request.
*
* @return string
*/
protected function get_request_api_url( $endpoint, $args = array() ) {
return add_query_arg( $args, $this->api_url . '/' . $this->api_stage . $endpoint );
}
/**
* Return the URL where make the login to facebook
*
* @return string
*/
public function get_login_url() {
return $this->get_request_api_url(
'/auth-request',
array(
'site_id' => $this->get_uuid(),
'redirect_uri' => rawurlencode( add_query_arg( 'action', 'fb-login', AEPC_Admin::get_page( 'general-settings' )->get_view_url() ) ),
)
);
}
/**
* Return the URL where make the logout to facebook
*
* @return string
*/
public function get_logout_url() {
return wp_nonce_url( add_query_arg( 'action', 'fb-disconnect', AEPC_Admin::get_page( 'general-settings' )->get_view_url() ), 'facebook_disconnect' );
}
/**
* Decode the token passed in parameter and save own access token and expiration
*
* @param string $token The authentication token.
*
* @throws FBAPILoginException If the token is empty or expired.
*
* @return void
*/
public function do_login( $token ) {
// Token empty doesn't allowed.
if ( empty( $token ) ) {
throw new FBAPILoginException( __( 'Empty Token', 'pixel-caffeine' ), 400 );
}
try {
JWT::$leeway = defined( 'AEPC_LEEWAY' ) ? AEPC_LEEWAY : 3600;
$jwt = (array) JWT::decode( $token, $this->get_uuid(), array( 'HS256' ) );
// Check if token is expired.
if ( $this->is_token_expired( $jwt ) ) {
$this->set_disconnected( 'expired' );
throw new FBAPILoginException( __( 'Expired Token', 'pixel-caffeine' ), 408 );
}
delete_option( 'aepc_fb_access_expired' );
update_option( 'aepc_fb_access_token', $jwt );
$this->access_token = $jwt['access_token'];
} catch ( Firebase\JWT\BeforeValidException $e ) {
$message = __( 'Invalid login, please try again.', 'pixel-caffeine' );
// Debug information.
if ( PixelCaffeine()->is_debug_mode() ) {
$message .= '<br /><strong>DEBUG:</strong> ' . $e->getCode() . ' - ' . $e->getMessage();
}
throw new FBAPILoginException( $message, 400 );
}
}
/**
* Get the auth-response request to save the access token and login the site for the facebook requests
*
* @throws FBAPILoginException When an error with facebook connection occurs.
*
* @return void
*/
public function login() {
// phpcs:disable WordPress.Security.NonceVerification
if (
empty( $_GET['action'] )
|| 'fb-login' !== $_GET['action']
|| ( empty( $_GET['token'] ) && empty( $_GET['error'] ) )
|| ! current_user_can( 'manage_ads' )
) {
return;
}
// phpcs:enable
// Refresh the page to remove query args.
$to = get_option( 'aepc_account_id', '' ) === '' ? AEPC_Admin::get_page( 'general-settings' )->get_view_url() : false;
try {
$error = filter_input( INPUT_GET, 'error', FILTER_SANITIZE_STRING );
if ( $error ) {
$error_description = filter_input( INPUT_GET, 'error_description', FILTER_SANITIZE_STRING );
if ( 'access_denied' === $error ) {
$error_description = __( 'Please, grant the permissions requested', 'pixel-caffeine' );
} elseif ( 'permissions_denied' === $error ) {
/* translators: %s: the error from facebook when permission errors occurs during the facebook connection. */
$error_description = sprintf( __( 'Please, grant the permissions: %s', 'pixel-caffeine' ), $error_description );
}
throw new FBAPILoginException( sprintf( '<strong>%s</strong> %s', __( 'Facebook authentication error', 'pixel-caffeine' ), $error_description ), 401 );
}
$this->do_login( filter_input( INPUT_GET, 'token', FILTER_SANITIZE_STRING ) );
// Force to refresh the user data.
$this->get_user( true );
// If an account ID already saved, refresh the business ID if not yet.
if ( get_option( 'aepc_account_id', '' ) !== '' ) {
AEPC_Admin::save_business_id( $this->get_business_id_from_account_id() );
}
if ( false === $to ) {
AEPC_Admin_Notices::add_notice( 'success', 'main', __( '<strong>Facebook connected</strong> Your facebook account is properly connected now.', 'pixel-caffeine' ) );
}
} catch ( Exception $e ) {
AEPC_Admin_Notices::add_notice( 'error', 'main', $e->getMessage() );
}
wp_safe_redirect( add_query_arg( 'ref', 'fblogin', remove_query_arg( array( 'action', 'token' ), $to ) ) );
exit();
}
/**
* Set access_token to false and delete the option saved on database, so in the next connect any access token will
* be saved and then the site won't logged in
*
* @param string $why 'logout' or 'expired'.
*
* @return void
*/
public function set_disconnected( $why = 'logout' ) {
$this->access_token = false;
delete_option( 'aepc_fb_access_token' );
update_option( 'aepc_fb_access_expired', 'expired' === $why );
delete_transient( 'aepc_fb_user' );
if ( 'logout' === $why ) {
delete_option( 'aepc_account_id' );
delete_option( 'aepc_pixel_id' );
delete_option( 'aepc_business_id' );
}
}
/**
* Disconnect the user removing the saved access token
*
* @return void
*/
public function disconnect() {
if (
'fb-disconnect' !== filter_input( INPUT_GET, 'action', FILTER_SANITIZE_STRING )
|| ! wp_verify_nonce( filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_STRING ), 'facebook_disconnect' )
|| ! current_user_can( 'manage_ads' )
) {
return;
}
try {
$this->set_disconnected();
} catch ( Exception $e ) {
AEPC_Admin_Notices::add_notice( 'error', 'main', $e->getMessage() );
}
if ( defined( 'DOING_AJAX' ) && DOING_AJAX || isset( $_GET['ajax'] ) && 1 === intval( $_GET['ajax'] ) ) {
wp_send_json_success();
}
// Refresh the page to remove the query args.
wp_safe_redirect( remove_query_arg( array( 'ref', 'action', '_wpnonce' ) ) );
exit();
}
/**
* Set the access token in the instance
*
* @param string $access_token The access token for the facebook API requests.
*
* @return void
*/
public function set_access_token( $access_token ) {
$this->access_token = $access_token;
}
/**
* Check if authorization is right
*
* @return bool
*/
public function is_logged_in() {
return ! empty( $this->access_token );
}
/**
* Check if the user has token expired or the connection is lost
*
* @return bool
*/
public function is_expired() {
return (bool) get_option( 'aepc_fb_access_expired' );
}
/**
* Get the Account ID
*
* @return string
*/
public function get_account_id() {
return $this->account_id;
}
/**
* Set the Account ID
*
* @param string $account_id Set the Facebook Ad Account ID of the user.
*
* @return void
*/
public function set_account_id( $account_id ) {
$this->account_id = ! empty( $account_id ) ? (string) $account_id : '';
}
/**
* Get the Pixel ID
*
* @return string
*/
public function get_pixel_id() {
return $this->pixel_id;
}
/**
* Set the Pixel ID
*
* @param string $pixel_id Set the Facebook Pixel ID.
*
* @return void
*/
public function set_pixel_id( $pixel_id ) {
$this->pixel_id = ! empty( $pixel_id ) ? (string) $pixel_id : '';
}
/**
* Get the Business ID
*
* @return string
*/
public function get_business_id() {
return $this->business_id;
}
/**
* Set the Business ID
*
* @param string $business_id Set the Facebook Business Manager Account ID of the user.
*
* @return void
*/
public function set_business_id( $business_id ) {
$this->business_id = $business_id;
}
// FB Methods.
/**
* Make the facebook api request to connector
*
* @param string $method The Facebook API endpoint to call.
* @param string $endpoint The http method to use for the facebook API call (GET, POST, DELETE).
* @param array $args The list of GET or POST parameters to pass to the Facebook API call (without the fb access_token).
* @param bool $cache Cache the request if another identical request is made, avoid call to facebook api with the same request.
*
* @return array|WP_Error
* @throws FBAPIException When the user is not connected to Facebook.
*/
public function request( $method, $endpoint, $args = array(), $cache = true ) {
if ( ! $this->is_logged_in() ) {
throw new FBAPIException( __( 'Please, connect your facebook account to make operations to the custom audiences of your Ad account.', 'pixel-caffeine' ), 401 );
}
// Key to set/get from cache an already made request.
$cache_key = 'fb_call_' . md5( $method . $endpoint . serialize( $args ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions
$payload = apply_filters(
'aepc_facebook_request_payload',
array(
'endpoint' => $endpoint,
'method' => $method,
'auth_token' => $this->access_token,
'parameters' => $args,
),
$method,
$endpoint,
$this
);
$response = wp_cache_get( $cache_key );
if ( ! $cache || false === $response ) {
// Do request.
$response = $this->send_request( $payload );
wp_cache_set( $cache_key, $response );
}
// Check about errors and throw Exception if one.
$this->check_error_messages( $response, $payload );
return $response;
}
/**
* Send the request
*
* @param array $payload {
* The payload to send.
*
* @type string $method The Facebook API endpoint to call.
* @type string $endpoint The http method to use for the facebook API call (GET, POST, DELETE).
* @type array $args The list of GET or POST parameters to pass to the Facebook API call (without the fb access_token).
* }
*
* @return array|WP_Error
*/
public function send_request( $payload ) {
return wp_remote_post(
$this->get_request_api_url( '/request' ),
array(
// phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
'timeout' => 15,
'headers' => array( 'content-type' => 'application/json' ),
'body' => wp_json_encode(
array(
'request' => JWT::encode( $payload, $this->get_uuid() ),
)
) ?: '{}',
)
);
}
/**
* Check if the response contains some error message and throw an exception
*
* @param array|WP_Error $response HTTP response.
* @param array $payload The payload of the request to validate.
*
* @throws FBAPIException Throw the exception if some error was found.
*
* @return void
*/
protected function check_error_messages( $response, $payload ) {
$body = json_decode( wp_remote_retrieve_body( $response ) );
$code = '';
$message = '';
$general_code = 400;
$general_error = sprintf(
/* translators: 1: opening tag for the link to the current page in order to invite the user to refresh, 2: closing tag. */
__( 'Something went wrong during the connection with Facebook API. Please, try to %1$srefresh the page%2$s.', 'pixel-caffeine' ),
'<a href="' . ( defined( 'DOING_AJAX' ) && DOING_AJAX && filter_input( INPUT_SERVER, 'HTTP_REFERER', FILTER_SANITIZE_URL ) ?: add_query_arg( null, null ) ) . '">',
'</a>'
);
if ( is_wp_error( $response ) ) {
// Check if request fails with wp_error response.
/**
* The response error instance
*
* @var WP_Error $response
*/
$error_code = $response->get_error_code();
$error_message = $response->get_error_message();
// Define error messages.
$wp_error = array(
'http_request_failed' => __( 'Cannot save on facebook account because of something gone wrong during facebook connection.', 'pixel-caffeine' ),
);
// Particular case with cUrl version not supported.
if ( false !== strpos( $error_message, 'cURL error 35' ) ) {
$error_code = 'curl_error';
$wp_error[ $error_code ] = __( 'The request goes in error from your server due by some oldest version of "cUrl" package. Please, ask to your hosting to upgrade it in order to fix it. HINT: enable the "debug mode" by Advanced Settings on bottom of this page. Then refresh and copy the entire message for your hosting support service, to give their more details about the issue.', 'pixel-caffeine' );
}
$message = isset( $wp_error[ $error_code ] ) ? $wp_error[ $error_code ] : $general_error;
// Debug information.
if ( PixelCaffeine()->is_debug_mode() ) {
$message .= '<br /><strong>DEBUG:</strong> ' . $error_code . ' - ' . $error_message;
}
} elseif ( wp_remote_retrieve_response_code( $response ) >= 400 ) {
// Check errors from HTTP request.
// Define error messages.
$http_messages = array(
/* translators: 1: opening tag for the link to the general setting page where he can login again to facebook, 2: closing tag. */
408 => sprintf( __( 'Facebook connection timed out. Please, login again from %1$shere%2$s', 'pixel-caffeine' ), '<a href="' . AEPC_Admin::get_page( 'general-settings' )->get_view_url() . '" target="_blank">', '</a>' ),
);
$code = wp_remote_retrieve_response_code( $response );
$message = isset( $http_messages[ $code ] ) ? $http_messages[ $code ] : $general_error;
// Debug information.
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
if ( PixelCaffeine()->is_debug_mode() && isset( $body->errorMessage ) ) {
$message .= '<br /><strong>DEBUG:</strong> ' . trim( str_replace( (string) $code, '', $body->errorMessage ) );
}
// phpcs:enable
// Run custom actions if occurred an error.
if ( apply_filters( 'aepc_fb_requests_needs_refresh', true ) && in_array( $code, array( 408 ), true ) ) {
$this->set_disconnected( 'expired' );
wp_send_json_error( array( 'refresh' => true ) );
}
} elseif ( isset( $body->error ) ) {
// Check errors in body for facebook api error.
// Define errors to show for user.
$fb_errors = array(
/* translators: 1: opening tag for the link to the general settings where the user can connect to Facebook again, 2: closing tag. */
190 => sprintf( __( 'Facebook connection timed out or you need to authorize again. Please, login again from %1$shere%2$s', 'pixel-caffeine' ), '<a href="' . AEPC_Admin::get_page( 'general-settings' )->get_view_url() . '" target="_blank">', '</a>' ),
/* translators: %s: the error message from the Facebook API. */
200 => sprintf( __( 'Permission error: %s', 'pixel-caffeine' ), $body->error->message ),
272 => __( 'This Ads action requires the user to be admin of the application.', 'pixel-caffeine' ),
273 => __( 'This Ads action requires the user to be admin of the ad account.', 'pixel-caffeine' ),
274 => __( 'The ad account is not enabled for usage in Ads API. Please add it in developers.facebook.com/apps -> select your app -> settings -> advanced -> advertising accounts -> Ads API.', 'pixel-caffeine' ),
275 => __( 'The facebook object you are trying to use does not exist.', 'pixel-caffeine' ),
278 => __( 'You have to extend permission for ads_read to complete the action.', 'pixel-caffeine' ),
294 => __( 'You have to extend permission for ads_management to complete the action.', 'pixel-caffeine' ),
2650 => __( 'Failed to update the custom audience.', 'pixel-caffeine' ),
2651 => __( 'Failed to create lookalike custom audience.', 'pixel-caffeine' ),
2654 => __( 'Failed to create custom audience on your facebook account.', 'pixel-caffeine' ),
2655 => make_clickable( __( 'Terms of service has not been accepted. To accept, go to https://www.facebook.com/ads/manage/customaudiences/tos.php', 'pixel-caffeine' ) ),
2656 => __( 'Failed to delete custom audience because associated lookalikes exist.', 'pixel-caffeine' ),
2663 => make_clickable( __( 'Terms of service has not been accepted. To accept, go to https://www.facebook.com/customaudiences/app/tos', 'pixel-caffeine' ) ),
2664 => make_clickable( __( 'The corporate terms of service has not been accepted. To accept, go to https://business.facebook.com/ads/manage/customaudiences/tos.php', 'pixel-caffeine' ) ),
16000 => __( 'Specified audience is too small.', 'pixel-caffeine' ),
);
// Ignore message if delete action.
if ( 'DELETE' === $payload['method'] && in_array( intval( $body->error->code ), array( 100, 275 ), true ) ) {
return;
}
if ( ! empty( $body->error->error_user_msg ) ) {
$message = $body->error->error_user_msg;
if ( ! empty( $body->error->error_user_title ) ) {
$message = sprintf( '<strong>%s</strong><br> %s', $body->error->error_user_title, $body->error->error_user_msg );
}
} elseif ( ! empty( $fb_errors[ $body->error->code ] ) ) {
$message = $fb_errors[ $body->error->code ];
} else {
$message = $general_error;
}
// Debug information.
if ( PixelCaffeine()->is_debug_mode() ) {
$message .= '<br /><strong>DEBUG:</strong> ' . $body->error->code . ' - ' . $body->error->type . ' - ' . $body->error->message . ' (fbtrace_id: ' . $body->error->fbtrace_id . ')';
}
// Run custom actions if occurred an error.
if ( apply_filters( 'aepc_fb_requests_needs_refresh', true ) && in_array( intval( $body->error->code ), array( 3, 190, 278, 294 ), true ) ) {
$this->set_disconnected( 'expired' );
wp_send_json_error( array( 'refresh' => true ) );
}
}
// Throw the exception if some error was found.
if ( ! empty( $message ) ) {
$exception_code = empty( $code ) ? $general_code : $code;
throw new FBAPIException( $message, $exception_code, $payload, $response );
}
}
/**
* Get the current user
*
* @param bool $force Force to get info from Facebook API.
*
* @return stdClass|WP_Error
*
* @throws Exception When no user info from Facebook API.
*/
public function get_user( $force = false ) {
$user = get_transient( 'aepc_fb_user' );
if ( $force || false === $user ) {
$response = $this->request(
'GET',
'/me',
array(
'fields' => 'id,name,picture,permissions',
)
);
$user = json_decode( wp_remote_retrieve_body( $response ) );
if ( empty( $user ) ) {
return new WP_Error( 'http_request_failed', '' );
}
// Save permissions granted.
$permissions = array();
foreach ( $user->permissions->data as $rule ) {
if ( 'granted' === $rule->status ) {
$permissions[] = $rule->permission;
}
}
$user->permissions = $permissions;
set_transient( 'aepc_fb_user', $user, WEEK_IN_SECONDS );
}
return $user;
}
/**
* Get the user ID of user logged in
*
* @return mixed
* @throws Exception When no user info from Facebook API.
*/
public function get_user_id() {
$user = $this->get_user();
if ( is_wp_error( $user ) ) {
throw new Exception( $user->get_error_message() );
}
return $user->id;
}
/**
* Get the display name of user logged in
*
* @return mixed
* @throws Exception When no user info from Facebook API.
*/
public function get_user_name() {
$user = $this->get_user();
if ( is_wp_error( $user ) ) {
throw new Exception( $user->get_error_message() );
}
return $user->name;
}
/**
* Get the photo url of user current logged in
*
* @return mixed
* @throws Exception When no user info from Facebook API.
*/
public function get_user_photo_uri() {
$user = $this->get_user();
if ( is_wp_error( $user ) ) {
throw new Exception( $user->get_error_message() );
}
return $user->picture->data->url;
}
/**
* Check if a permission is granted in the current login
*
* @param string|array $permissions Permission(s) to check.
*
* @return bool
* @throws Exception When no user info from Facebook API.
*/
public function is_permission_granted( $permissions ) {
$user = $this->get_user();
if ( ! isset( $user->permissions ) || is_wp_error( $user ) ) {
$user = $this->get_user( true );
}
if ( is_wp_error( $user ) ) {
throw new Exception( $user->get_error_message() );
}
$permissions = (array) $permissions;
foreach ( $permissions as $permission ) {
if ( in_array( $permission, isset( $user->permissions ) ? $user->permissions : array(), true ) ) {
return true;
}
}
return false;
}
/**
* Return an array with all account ids of the user
*
* @return stdClass[]
* @throws FBAPIException When the Facebook API request fail.
* @throws Exception When no user info from Facebook API.
*/
public function get_account_ids() {
$response = $this->request(
'GET',
'/' . $this->get_user_id() . '/adaccounts',
array(
'fields' => 'account_id,name',
'limit' => apply_filters( 'aepc_facebook_request_accounts_limit', 100 ),
)
);
$accounts = json_decode( wp_remote_retrieve_body( $response ) );
return $accounts->data;
}
/**
* Get the name of account, checking on option before (set on facebook options saving)
*
* @param string $account_id The Facebook Ad Account ID.
*
* @return mixed|null
* @throws FBAPIException When the Facebook API request fail.
*/
public function get_account_name( $account_id = '' ) {
if ( empty( $account_id ) ) {
$account_id = $this->get_account_id();
if ( empty( $account_id ) ) {
return null;
}
}
$account_name = get_transient( 'aepc_account_name_' . $account_id );
if ( false === $account_name ) {
$accounts = $this->get_account_ids();
foreach ( $accounts as $account ) {
if ( (string) $account->account_id === (string) $account_id ) {
$account_name = $account->name;
break;
}
}
set_transient( 'aepc_account_name_' . $account_id, $account_name );
}
return $account_name;
}
/**
* Get the business ID from User ID and Ad Account ID
*
* @param string $account_id The Facebook Ad Account ID.
*
* @return string
* @throws FBAPIException When the Facebook API request fail.
* @throws Exception When no user info from API.
*/
public function get_business_id_from_account_id( $account_id = '' ) {
$business_id = '';
if ( empty( $account_id ) ) {
$account_id = $this->get_account_id();
$user = $this->get_user();
if ( empty( $account_id ) || empty( $user ) ) {
return $business_id;
}
}
$response = $this->request( 'GET', '/act_' . $account_id, array( 'fields' => 'business' ) );
$data = json_decode( wp_remote_retrieve_body( $response ) );
if ( ! empty( $data->business->id ) ) {
$business_id = $data->business->id;
}
return $business_id;
}
/**
* Return an array with all pixel ids of the user
*
* @param string $account_id The Facebook Ad Account ID.
*
* @return stdClass[]|null
* @throws FBAPIException When the Facebook API request fail.
*/
public function get_pixel_ids( $account_id = '' ) {
if ( empty( $account_id ) ) {
$account_id = $this->get_account_id();
if ( empty( $account_id ) ) {
return null;
}
}
try {
$response = $this->request(
'GET',
'/act_' . $account_id . '/adspixels',
array(
'fields' => 'id,name',
'limit' => apply_filters( 'aepc_facebook_request_pixels_limit', 100 ),
)
);
} catch ( Exception $e ) {
return array();
}
$pixels = json_decode( wp_remote_retrieve_body( $response ) )->data;
$business_id = $this->get_business_id_from_account_id( $account_id );
if ( $business_id ) {
try {
$response = $this->request(
'GET',
'/' . $business_id . '/adspixels',
array(
'fields' => 'id,name',
'limit' => apply_filters( 'aepc_facebook_request_pixels_limit', 100 ),
)
);
// Add into the list only if it's not been fetched before.
foreach ( json_decode( wp_remote_retrieve_body( $response ) )->data as $pixel ) {
if ( ! in_array( $pixel->id, wp_list_pluck( $pixels, 'id' ), true ) ) {
$pixels[] = $pixel;
}
}
} catch ( Exception $e ) {
// Do not do nothing because find by business ID is optional.
return $pixels;
}
}
return $pixels;
}
/**
* Return an array with all pixel ids of the user
*
* @param string $account_id The Facebook Ad Account ID.
*
* @return object[]|null
* @throws FBAPIException When the Facebook API request fail.
*/
public function get_offsite_pixel_ids( $account_id = '' ) {
if ( empty( $account_id ) ) {
$account_id = $this->get_account_id();
if ( empty( $account_id ) ) {
return null;
}
}
$response = $this->request(
'GET',
'/act_' . $account_id . '/offsitepixels',
array(
'fields' => 'id,name',
'limit' => apply_filters( 'aepc_facebook_request_offsite_pixels_limit', 100 ),
)
);
$pixels = json_decode( wp_remote_retrieve_body( $response ) );
return $pixels->data;
}
/**
* Get the name of account, checking on option before (set on facebook options saving)
*
* @param string $pixel_id The Facebook Pixel ID.
*
* @return mixed|null
*/
public function get_pixel_name( $pixel_id = '' ) {
if ( empty( $pixel_id ) ) {
$pixel_id = $this->get_pixel_id();
if ( empty( $pixel_id ) ) {
return null;
}
}
$pixel_name = get_transient( 'aepc_pixel_name_' . $pixel_id );
if ( false === $pixel_name ) {
try {
$response = $this->request( 'GET', '/' . $pixel_id, array( 'fields' => 'name' ) );
$pixel = json_decode( wp_remote_retrieve_body( $response ) );
$pixel_name = $pixel->name;
} catch ( Exception $e ) {
$pixel_name = null;
}
set_transient( 'aepc_pixel_name_' . $pixel_id, $pixel_name );
}
return $pixel_name;
}
/**
* Get Pixel statistic of an account ID
*
* @param string|array|object $args The configuration for the stats query.
* @param string $pixel_id If not defined, get the pixel ID set.
*
* @return array|null
* @throws FBAPIException When API fail.
*/
public function get_pixel_stats( $args = array(), $pixel_id = '' ) {
if ( empty( $pixel_id ) ) {
$pixel_id = $this->get_pixel_id();
if ( empty( $pixel_id ) ) {
return null;
}
}
$args = wp_parse_args(
$args,
array(
'aggregation' => 'pixel_fire',
'start_time' => time() - 2 * WEEK_IN_SECONDS,
'end_time' => time(),
)
);
$response = $this->request( 'GET', '/' . $pixel_id . '/stats', $args );
$pixels = json_decode( wp_remote_retrieve_body( $response ) );
return (array) $pixels->data;
}
/**
* Return an array with all product catalogs ids of a business ID
*
* @param string $business_id The Facebook Business Manager account ID.
*
* @return stdClass[]|null
* @throws FBAPIException When the Facebook Api request fails.
*/
public function get_product_catalogs( $business_id = '' ) {
if ( empty( $business_id ) ) {
$business_id = $this->get_business_id();
if ( empty( $business_id ) ) {
return null;
}
}
$owned_response = $this->request(
'GET',
'/' . $business_id . '/owned_product_catalogs',
array(
'limit' => apply_filters( 'aepc_facebook_request_product_catalogs_limit', 100 ),
)
);
$client_response = $this->request(
'GET',
'/' . $business_id . '/client_product_catalogs',
array(
'limit' => apply_filters( 'aepc_facebook_request_product_catalogs_limit', 100 ),
)
);
$owned_product_catalogs = json_decode( wp_remote_retrieve_body( $owned_response ) )->data;
$client_product_catalogs = json_decode( wp_remote_retrieve_body( $client_response ) )->data;
return array_merge( $owned_product_catalogs, $client_product_catalogs );
}
/**
* Return an array with all product feeds ids of a product catalog ID
*
* @param string $product_catalog_id The Product Catalog ID.
*
* @return stdClass[]|null
* @throws FBAPIException When the Facebook Api request fails.
*/
public function get_product_feeds( $product_catalog_id ) {
if ( empty( $product_catalog_id ) ) {
return null;
}
$response = $this->request(
'GET',
'/' . $product_catalog_id . '/product_feeds',
array(
'limit' => apply_filters( 'aepc_facebook_request_product_feeds_limit', 100 ),
)
);
$product_catalogs = json_decode( wp_remote_retrieve_body( $response ) );
return $product_catalogs->data;
}
/**
* Get the product feed info
*
* @param string $product_feed_id The product feed ID.
*
* @return stdClass|null
* @throws FBAPIException When the Facebook Api request fails.
*/
public function get_product_feed( $product_feed_id ) {
if ( empty( $product_feed_id ) ) {
return null;
}
$response = $this->request(
'GET',
'/' . $product_feed_id,
array(
'fields' => 'schedule,name',
)
);
return json_decode( wp_remote_retrieve_body( $response ) );
}
/**
* Create the audience in Facebook account
*
* @param array $args {
* Arguments to configure the new custom audience.
*
* @type string $name The name for the cluster.
* @type string $description A brief explanation of cluster.
* @type int $retention Number of days to keep the user in this cluster. You can use any value between 1 and 180 days. Defaults to 14 days if not specified.
* @type bool $prefill true - Include website traffic recorded prior to the audience creation. false - Only include website traffic beginning at the time of the audience creation.
* @type array rule Audience rules to be applied on the referrer URL.
* }
*
* @return object
* @throws FBAPIException When the Facebook Api request fails.
*/
public function create_audience( $args = array() ) {
$args = (object) wp_parse_args(
$args,
array(
'name' => '',
'description' => '',
'retention' => 14,
'prefill' => true,
'rule' => array(),
)
);
$args->rule = $this->compose_rule( $args->rule, $args->retention );
// Do request.
$response = $this->request(
'POST',
'/act_' . $this->get_account_id() . '/customaudiences',
array(
'name' => $args->name,
'description' => $args->description,
'prefill' => (int) $args->prefill,
'rule' => $args->rule,
)
);
// Return only the CA ID from facebook.
return json_decode( wp_remote_retrieve_body( $response ) );
}
/**
* Retrieve a custom audience created by user
*
* @param string $ca_id The custom audience ID.
* @param string $fields List of fields you want to have.
*
* @return stdClass
* @throws FBAPIException When the Facebook Api request fails.
*/
public function get_audience( $ca_id, $fields = 'name,description,approximate_count' ) {
$fields = trim( $fields, ", \t\n\r\0\x0B" );
if ( empty( $fields ) ) {
$fields = 'id';
}
$response = $this->request(
'GET',
'/' . $ca_id,
array(
'fields' => $fields,
)
);
return json_decode( wp_remote_retrieve_body( $response ) );
}
/**
* Retrieve all custom audiences created by user
*
* @param string $fields List of fields you want to have.
*
* @return stdClass[]
* @throws FBAPIException When the Facebook Api request fails.
*/
public function get_audiences( $fields = 'name,description,approximate_count' ) {
$fields = trim( $fields, ", \t\n\r\0\x0B" );
if ( empty( $fields ) ) {
$fields = 'id';
}
$response = $this->request(
'GET',
'/act_' . $this->get_account_id() . '/customaudiences',
array(
'fields' => $fields,
)
);
$audiences = json_decode( wp_remote_retrieve_body( $response ) );
return $audiences->data;
}
/**
* Update a custom audience on facebook account
*
* @param string $ca_id The custom audience ID.
* @param array $args The parameters to change to the custom audience.
*
* @return array|mixed|object
* @throws FBAPIException When the Facebook Api request fails.
*/
public function update_audience( $ca_id, $args = array() ) {
// Check keys and values.
foreach ( $args as $key => $fbparam ) {
if ( 'retention' === $key ) {
$args['retention_days'] = $fbparam;
unset( $args[ $key ] );
}
}
// Compose rules for facebook request.
if ( isset( $args['rule'] ) ) {
$args['rule'] = $this->compose_rule( $args['rule'] );
}
// Do request.
$response = $this->request( 'POST', '/' . $ca_id, $args );
// Return only the CA ID from facebook.
return json_decode( wp_remote_retrieve_body( $response ) );
}
/**
* Delete a custom audience on facebook account
*
* @param string $ca_id The custom audience ID to remove.
*
* @return array|mixed|object
* @throws FBAPIException When the Facebook Api request fails.
*/
public function delete_audience( $ca_id ) {
$response = $this->request( 'DELETE', '/' . $ca_id );
return json_decode( wp_remote_retrieve_body( $response ) );
}
/**
* Get business ID of current user
*
* @return object[]
* @throws FBAPIException When the Facebook Api request fails.
*/
public function get_business_ids() {
$response = $this->request( 'GET', '/me/businesses' );
$businesses = json_decode( wp_remote_retrieve_body( $response ) );
return $businesses->data;
}
/**
* Create a product catalog
*
* @param string $name The name of product catalog to create.
*
* @return int
* @throws Exception When no facebook business manager ID available.
*/
public function create_product_catalog( $name ) {
$business_id = $this->get_business_id();
if ( empty( $business_id ) ) {
throw new Exception( __( 'Unable to create a product catalog because there isn\'t any business ID associated to your Ad account.', 'pixel-caffeine' ) );
}
$response = $this->request(
'POST',
'/' . $this->get_business_id() . '/owned_product_catalogs',
array(
'name' => $name,
)
);
$product_catalog = json_decode( wp_remote_retrieve_body( $response ) );
return intval( $product_catalog->id );
}
/**
* Add a product feed in a product catalog
*
* @param string $product_catalog_id The product catalog ID.
* @param string $name The name of product feed to create.
* @param array $config The configuration of the product feed.
*
* @return string
* @throws FBAPIException When the Facebook Api request fails.
*/
public function add_product_feed( $product_catalog_id, $name, array $config ) {
$response = $this->request(
'POST',
'/' . $product_catalog_id . '/product_feeds',
array(
'name' => $name,
'schedule' => $config,
)
);
$product_feed = json_decode( wp_remote_retrieve_body( $response ) );
return $product_feed->id;
}
/**
* Add a product feed in a product catalog
*
* @param string $product_feed_id The product feed ID.
* @param array $config The configuration of the product feed.
*
* @return string
* @throws FBAPIException When the Facebook Api request fails.
*/
public function update_product_feed( $product_feed_id, array $config ) {
$response = $this->request(
'POST',
'/' . $product_feed_id,
array(
'schedule' => $config,
)
);
$product_feed = json_decode( wp_remote_retrieve_body( $response ) );
return $product_feed->id;
}
/**
* Associate the pixel ID to the product catalog
*
* @param string $product_catalog_id The product catalog ID.
*
* @throws FBAPIException When the Facebook Api request fails.
*
* @return void
*/
public function associate_pixel_to_product_catalog( $product_catalog_id ) {
$this->request(
'POST',
'/' . $product_catalog_id . '/external_event_sources',
array(
'external_event_sources' => array( $this->get_pixel_id() ),
)
);
}
/**
* Compose the rules for facebook requests
*
* @param array $filters The filters of the rule to compose.
* @param int $retention_days Since FB Marketing API 3.0.
*
* @return mixed|string|void
*/
public function compose_rule( $filters, $retention_days = 180 ) {
$sample = array(
'operator' => 'or',
'rules' => array(
array(
'event_sources' => array(
array(
'type' => 'pixel',
'id' => $this->get_pixel_id(),
),
),
'retention_seconds' => $retention_days * DAY_IN_SECONDS,
'filter' => array(
'operator' => 'and',
'filters' => array(),
),
),
),
);
$final_rule = array();
foreach ( $filters as $filter ) {
$main_condition = strtr(
isset( $filter['main_condition'] ) ? $filter['main_condition'] : 'include',
array(
'include' => 'inclusions',
'exclude' => 'exclusions',
)
);
if ( ! isset( $final_rule[ $main_condition ] ) ) {
$final_rule[ $main_condition ] = $sample;
}
foreach ( $this->filter_adapter( $filter ) as $formatted_filter ) {
$final_rule[ $main_condition ]['rules'][0]['filter']['filters'][] = $formatted_filter;
}
}
return wp_json_encode( $final_rule ) ?: '{}';
}
/**
* Format a rule with conditions for facebook request
*
* @param array $args The configuration of the filter to adapt.
*
* @return array
*/
public function filter_adapter( $args ) {
$filters = array();
if (
'attributes' === $args['event_type'] && in_array( $args['event'], array( 'login_status' ), true )
|| 'blog' === $args['event_type'] && in_array( $args['event'], array( 'categories', 'tax_post_tag', 'posts', 'pages' ), true )
) {
// Add AdvancedEvents event condition.
$filters[] = $this->add_ca_filter( 'event', self::CA_RULE_CONTAINS, 'AdvancedEvents' );
} elseif ( 'blog' === $args['event_type'] && in_array( $args['event'], array( 'custom_fields' ), true ) ) {
// Add custom fields event condition.
$filters[] = $this->add_ca_filter( 'event', self::CA_RULE_CONTAINS, 'CustomFields' );
} elseif ( 'ecommerce' === $args['event_type'] && in_array( $args['event'], array_keys( AEPC_Track::$standard_events ), true ) ) {
// Add DPA tracking events on condition.
$filters[] = $this->add_ca_filter( 'event', self::CA_RULE_CONTAINS, $args['event'] );
} elseif ( 'events' === $args['event_type'] ) {
// Add custom events.
$filters[] = $this->add_ca_filter( 'event', self::CA_RULE_EQ, $args['event'] );
}
/**
* Then add specific conditions
*/
// Force to add conditions key when it doesn't exist.
if ( ! isset( $args['conditions'] ) ) {
$args['conditions'] = array();
}
foreach ( $args['conditions'] as $condition ) {
// Skip if it's not allowed empty key and empty value.
if ( in_array( $args['event_type'], array( 'attributes', 'blog' ), true ) && ( isset( $condition['key'] ) && empty( $condition['key'] ) || empty( $condition['value'] ) ) ) {
continue;
}
if ( 'blog' === $args['event_type'] && 'posts' === $args['event'] ) {
// Add blog posts condition for object_type, post_type and object_id parameters.
$filters[] = $this->add_ca_filter( 'post_type', self::CA_RULE_CONTAINS, $condition['key'] );
$filters[] = $this->add_ca_filter( 'object_type', self::CA_RULE_CONTAINS, 'single' );
// If the value contains [[any]], don't set object_id in condition, because we want to search all posts.
if ( false === strpos( $condition['value'], '[[any]]' ) ) {
// If the value is an array, separated by comma, set 'or' condition for the values.
if ( false === strpos( $condition['value'], ',' ) ) {
$filters[] = $this->add_ca_filter( 'object_id', self::CA_RULE_CONTAINS, $condition['value'] );
} else {
$or_filters = array();
foreach ( array_map( 'trim', explode( ',', $condition['value'] ) ) as $value ) {
$or_filters[] = $this->add_ca_filter( 'object_id', $condition['operator'], $value );
}
$filters[] = array(
'operator' => 'or',
'filters' => $or_filters,
);
}
}
} elseif ( 'blog' === $args['event_type'] && 'pages' === $args['event'] ) {
// Add blog page condition for object_type, post_type and object_id parameters.
// If [[any]] passed into value, search only for page (all), home, blog.
if ( false !== strpos( $condition['value'], '[[any]]' ) ) {
$or_filters = array();
foreach ( array( 'home', 'blog', 'page' ) as $object_type ) {
$or_filters[] = $this->add_ca_filter( 'object_type', self::CA_RULE_CONTAINS, $object_type );
}
$filters[] = array(
'operator' => 'or',
'filters' => $or_filters,
);
} else {
// If the value is an array, separated by comma, set 'or' condition for the values.
if ( false === strpos( $condition['value'], ',' ) ) {
if ( in_array( $condition['value'], array( 'home', 'blog' ), true ) ) {
$filters[] = $this->add_ca_filter( 'object_type', self::CA_RULE_CONTAINS, $condition['value'] );
} else {
$filters[] = $this->add_ca_filter( 'post_type', self::CA_RULE_CONTAINS, 'page' );
$filters[] = $this->add_ca_filter( 'object_type', self::CA_RULE_CONTAINS, 'page' );
$filters[] = $this->add_ca_filter( 'object_id', self::CA_RULE_CONTAINS, $condition['value'] );
}
} else {
$or_filters = array();
foreach ( array_map( 'trim', explode( ',', $condition['value'] ) ) as $value ) {
if ( in_array( $value, array( 'home', 'blog' ), true ) ) {
$or_filters[] = $this->add_ca_filter( 'object_type', self::CA_RULE_CONTAINS, $value );
} else {
$or_filters[] = array(
'operator' => 'and',
'filters' => array(
$this->add_ca_filter( 'post_type', self::CA_RULE_CONTAINS, 'page' ),
$this->add_ca_filter( 'object_type', self::CA_RULE_CONTAINS, 'page' ),
$this->add_ca_filter( 'object_id', self::CA_RULE_CONTAINS, $value ),
),
);
}
}
$filters[] = array(
'operator' => 'or',
'filters' => $or_filters,
);
}
}
} else {
// Other cases.
$param = isset( $condition['key'] ) ? $condition['key'] : $args['event'];
// If the key is [[all]], it shouldn't have any condition.
if ( '[[any]]' !== $param ) {
if ( '[[any]]' === $condition['value'] ) {
// If the value is [[any]], the condition must be not equal to empty.
$filters[] = $this->add_ca_filter( $param, self::CA_RULE_NEQ, '' );
} elseif ( false === strpos( $condition['value'], ',' ) ) {
// If it's array, set 'or' operator.
$filters[] = $this->add_ca_filter( $param, $condition['operator'], $condition['value'] );
} else {
$or_filters = array();
foreach ( array_map( 'trim', explode( ',', $condition['value'] ) ) as $value ) {
$or_filters[] = $this->add_ca_filter( $param, $condition['operator'], $value );
}
$filters[] = array(
'operator' => 'or',
'filters' => $or_filters,
);
}
}
}
}
return $filters;
}
/**
* Return a common format for a specific filter of a CA rule
*
* @param string $field The field key.
* @param string $operator The operator of the filter.
* @param mixed $value The value.
*
* @return array
*/
protected function add_ca_filter( $field, $operator, $value ) {
return array(
'field' => $field,
'operator' => $operator,
'value' => $value,
);
}
/**
* Check if the product catalog APIs can work
*
* @return bool
* @throws Exception When no user info from Facebook API.
*/
public function is_product_catalog_enabled() {
return $this->is_logged_in() && $this->is_expired() && $this->is_permission_granted( 'business_management' ) && $this->get_business_id();
}
/**
* Get the unique site ID for the
*
* @return string
*/
public function get_uuid() {
$uuid = get_option( 'aepc_fb_uuid' );
if ( ! $uuid ) {
$uuid = $this->generate_uuid();
update_option( 'aepc_fb_uuid', $uuid );
}
return $uuid;
}
/**
* Generate the SITEID useful for fb request in UUIDv4
*
* @return string
*/
protected function generate_uuid() {
$uuid = Uuid::uuid4();
return $uuid->toString();
}
/**
* Check if the debug mode is active
*
* When the debug mode is active, the plugin shouldn't do any facebook request and give ability to use custom audiences
* system.
*
* @return bool
*/
public function is_debug() {
return defined( 'AEPC_DEACTIVE_FB_REQUESTS' ) && AEPC_DEACTIVE_FB_REQUESTS;
}
}
return new AEPC_Facebook_Adapter();