<?php // phpcs:ignoreFile /** * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * * @package FacebookCommerce */ if ( ! defined( 'ABSPATH' ) ) { exit; } use SkyVerge\WooCommerce\PluginFramework\v5_10_0 as Framework; if ( ! class_exists( 'WC_Facebookcommerce_Graph_API' ) ) : if ( ! class_exists( 'WC_Facebookcommerce_Async_Request' ) ) { include_once 'fbasync.php'; } /** * FB Graph API helper functions */ class WC_Facebookcommerce_Graph_API { const GRAPH_API_URL = 'https://graph.facebook.com/'; const API_VERSION = 'v13.0'; const CURL_TIMEOUT = 500; /** * Cache the api_key */ var $api_key; /** * Init */ public function __construct( $api_key ) { $this->api_key = $api_key; } /** * Issues a GET request to the Graph API. * * @param string $url request URL * @param string $api_key Graph API key * @return array|\WP_Error */ public function _get( $url, $api_key = '' ) { $api_key = $api_key ?: $this->api_key; $request_args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, ), 'timeout' => self::CURL_TIMEOUT, ); $response = wp_remote_get( $url, $request_args ); $this->log_request( $url, $request_args, $response ); return $response; } /** * Performs a Graph API request to the given URL. * * Throws an exception if a WP_Error is returned or we receive a 401 Not Authorized response status. * * @since 1.10.2 * * @param string $url * @throws Framework\SV_WC_API_Exception * @return array */ public function perform_request( $url ) { $request_args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $this->api_key, ), 'timeout' => self::CURL_TIMEOUT, ); $response = wp_remote_get( $url, $request_args ); $this->log_request( $url, $request_args, $response ); if ( is_wp_error( $response ) ) { throw new Framework\SV_WC_API_Exception( $response->get_error_message(), $response->get_error_code() ); } elseif ( 401 === (int) wp_remote_retrieve_response_code( $response ) ) { $response_body = json_decode( wp_remote_retrieve_body( $response ) ); if ( isset( $response_body->error->code, $response_body->error->message ) ) { throw new Framework\SV_WC_API_Exception( $response_body->error->message, $response_body->error->code ); } else { throw new Framework\SV_WC_API_Exception( sprintf( __( 'HTTP %1$s: %2$s', 'facebook-for-woocommerce' ), wp_remote_retrieve_response_code( $response ), wp_remote_retrieve_response_message( $response ) ) ); } } return $response; } public function _post( $url, $data, $api_key = '' ) { if ( class_exists( 'WC_Facebookcommerce_Async_Request' ) ) { return self::_post_async( $url, $data ); } else { return self::_post_sync( $url, $data ); } } public function _post_sync( $url, $data, $api_key = '' ) { $api_key = $api_key ?: $this->api_key; $request_args = array( 'body' => $data, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, ), 'timeout' => self::CURL_TIMEOUT, ); $response = wp_remote_post( $url, $request_args ); $this->log_request( $url, $request_args, $response, 'POST' ); return $response; } /** * Issues an asynchronous POST request to the Graph API. * * @param string $url request URL * @param array $data request data * @param string $api_key Graph API key * @return array|\WP_Error */ public function _post_async( $url, $data, $api_key = '' ) { if ( ! class_exists( 'WC_Facebookcommerce_Async_Request' ) ) { return; } $api_key = $api_key ?: $this->api_key; $request_args = array( 'body' => $data, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, ), 'timeout' => self::CURL_TIMEOUT, ); $fbasync = new WC_Facebookcommerce_Async_Request(); $fbasync->query_url = $url; $fbasync->query_args = array(); $fbasync->post_args = $request_args; $response = $fbasync->dispatch(); $this->log_request( $url, $request_args, $response, 'POST' ); return $response; } /** * Issues a DELETE request to the Graph API. * * @param string $url request URL * @param string $api_key Graph API key * @return array|\WP_Error */ public function _delete( $url, $api_key = '' ) { $api_key = $api_key ?: $this->api_key; $request_args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, ), 'timeout' => self::CURL_TIMEOUT, 'method' => 'DELETE', ); $response = wp_remote_request( $url, $request_args ); $this->log_request( $url, $request_args, $response, 'DELETE' ); return $response; } /** * Logs the request and response data. * * @since 1.10.2 * * @param $url * @param $request_args * @param array|\WP_Error $response WordPress response object * @param string $method */ private function log_request( $url, $request_args, $response, $method = '' ) { // bail if this class is loaded incorrectly or logging is disabled if ( ! function_exists( 'facebook_for_woocommerce' ) || ! facebook_for_woocommerce()->get_integration()->is_debug_mode_enabled() ) { return; } // add the URI to the data $request_data = array_merge( array( 'uri' => $url, ), $request_args ); // the request args may not include the method, so allow it to be set if ( $method ) { $request_data['method'] = $method; } // mask the page access token if ( ! empty( $request_data['headers']['Authorization'] ) ) { $auth_value = $request_data['headers']['Authorization']; $request_data['headers']['Authorization'] = str_replace( $auth_value, str_repeat( '*', strlen( $auth_value ) ), $auth_value ); } // if there was a problem if ( is_wp_error( $response ) ) { $code = $response->get_error_code(); $message = $response->get_error_message(); $headers = array(); $body = ''; } else { $headers = wp_remote_retrieve_headers( $response ); if ( is_object( $headers ) ) { $headers = $headers->getAll(); } elseif ( ! is_array( $headers ) ) { $headers = array(); } $code = wp_remote_retrieve_response_code( $response ); $message = wp_remote_retrieve_response_message( $response ); $body = wp_remote_retrieve_body( $response ); } $response_data = array( 'code' => $code, 'message' => $message, 'headers' => $headers, 'body' => $body, ); facebook_for_woocommerce()->log_api_request( $request_data, $response_data ); } // GET https://graph.facebook.com/vX.X/{page-id}/?fields=name public function get_page_name( $page_id, $api_key = '' ) { $api_key = $api_key ?: $this->api_key; $url = $this->build_url( $page_id, '/?fields=name' ); $response = self::_get( $url, $api_key ); if ( is_wp_error( $response ) ) { WC_Facebookcommerce_Utils::log( $response->get_error_message() ); return ''; } if ( $response['response']['code'] != '200' ) { return ''; } $response_body = json_decode( wp_remote_retrieve_body( $response ) ); return isset( $response_body->name ) ? $response_body->name : ''; } /** * Gets a Facebook Page URL. * * Endpoint: https://graph.facebook.com/vX.X/{page-id}/?fields=link * * @param string|int $page_id page identifier * @param string $api_key API key * @return string URL */ public function get_page_url( $page_id, $api_key = '' ) { $api_key = $api_key ?: $this->api_key; $request = $this->build_url( $page_id, '/?fields=link' ); $response = $this->_get( $request, $api_key ); $page_url = ''; if ( is_wp_error( $response ) ) { \WC_Facebookcommerce_Utils::log( $response->get_error_message() ); } elseif ( 200 === (int) $response['response']['code'] ) { $response_body = wp_remote_retrieve_body( $response ); $page_url = json_decode( $response_body )->link; } return $page_url; } /** * Determines whether the product catalog ID is valid. * * Returns true if the product catalog ID can be successfully retrieved using the Graph API. * * TODO: deprecate this methid in 1.11.0 or newer {WV 2020-03-12} * * @param int $product_catalog_id the ID of the product catalog * @return bool */ public function validate_product_catalog( $product_catalog_id ) { try { $is_valid = $this->is_product_catalog_valid( $product_catalog_id ); } catch ( Framework\SV_WC_API_Exception $e ) { $is_valid = false; } return $is_valid; } /** * Determines whether the product catalog ID is valid. * * Returns true if the product catalog ID can be successfully retrieved using the Graph API. * * @since 1.10.2 * * @param int $product_catalog_id the ID of the product catalog * @return boolean * @throws Framework\SV_WC_API_Exception */ public function is_product_catalog_valid( $product_catalog_id ) { $response = $this->perform_request( $this->build_url( $product_catalog_id ) ); return 200 === (int) wp_remote_retrieve_response_code( $response ); } // POST https://graph.facebook.com/vX.X/{product-catalog-id}/product_groups public function create_product_group( $product_catalog_id, $data ) { $url = $this->build_url( $product_catalog_id, '/product_groups' ); return self::_post( $url, $data ); } // POST https://graph.facebook.com/vX.X/{product-group-id}/products public function create_product_item( $product_group_id, $data ) { $url = $this->build_url( $product_group_id, '/products' ); return self::_post( $url, $data ); } public function update_product_group( $product_catalog_id, $data ) { $url = $this->build_url( $product_catalog_id ); return self::_post( $url, $data ); } public function update_product_item( $product_id, $data ) { $url = $this->build_url( $product_id ); return self::_post( $url, $data ); } public function delete_product_item( $product_item_id ) { $product_item_url = $this->build_url( $product_item_id ); return self::_delete( $product_item_url ); } public function delete_product_group( $product_group_id ) { $product_group_url = $this->build_url( $product_group_id, '?deletion_method=delete_items' ); return self::_delete( $product_group_url ); } // POST https://graph.facebook.com/vX.X/{product-catalog-id}/product_sets public function create_product_set_item( $product_catalog_id, $data ) { $url = $this->build_url( $product_catalog_id, '/product_sets' ); return self::_post( $url, $data ); } // POST https://graph.facebook.com/vX.X/{product-set-id} public function update_product_set_item( $product_set_id, $data ) { $url = $this->build_url( $product_set_id, '' ); return self::_post( $url, $data ); } public function delete_product_set_item( $product_set_id ) { $params = ( true === apply_filters( 'wc_facebook_commerce_allow_live_product_set_deletion', true, $product_set_id ) ) ? '?allow_live_product_set_deletion=true' : ''; $url = $this->build_url( $product_set_id, $params ); return self::_delete( $url ); } public function log( $ems_id, $message, $error ) { $log_url = $this->build_url( $ems_id, '/log_events' ); $data = array( 'message' => $message, 'error' => $error, ); self::_post( $log_url, $data ); } public function log_tip_event( $tip_id, $channel_id, $event ) { $tip_event_log_url = $this->build_url( '', '/log_tip_events' ); $data = array( 'tip_id' => $tip_id, 'channel_id' => $channel_id, 'event' => $event, ); self::_post( $tip_event_log_url, $data ); } public function create_upload( $facebook_feed_id, $path_to_feed_file ) { $url = $this->build_url( $facebook_feed_id, '/uploads?access_token=' . $this->api_key ); $data = array( 'file' => new CurlFile( $path_to_feed_file, 'text/csv' ), 'update_only' => true, ); $curl = curl_init(); curl_setopt_array( $curl, array( CURLOPT_URL => $url, CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $data, CURLOPT_RETURNTRANSFER => 1, ) ); $response = curl_exec( $curl ); if ( curl_errno( $curl ) ) { WC_Facebookcommerce_Utils::fblog( $response ); return null; } return WC_Facebookcommerce_Utils::decode_json( $response, true ); } public function create_feed( $facebook_catalog_id, $data ) { $url = $this->build_url( $facebook_catalog_id, '/product_feeds' ); $url = $this->get_feed_endpoint_url( $facebook_catalog_id ); // success API call will return {id: <product feed id>} // failure API will return {error: <error message>} return self::_post( $url, $data ); } /** * Get all feed configurations for a given catalog id. * * @see https://developers.facebook.com/docs/marketing-api/reference/product-feed/ * @since 2.6.0 * * @param String $facebook_catalog_id Facebook Catalog Id. * @return Array Facebook feeds configurations. */ public function read_feeds( $facebook_catalog_id ) { $url = $this->get_feed_endpoint_url( $facebook_catalog_id ); return $this->_get( $url ); } /** * Get general info about a feed (data source) configured in Facebook Business. * * @see https://developers.facebook.com/docs/marketing-api/reference/product-feed/ * @since 2.6.0 * * @param String $feed_id Feed Id. * @return Array Facebook feeds configurations. */ public function read_feed_information( $feed_id ) { $url = $this->build_url( $feed_id, '/?fields=id,name,schedule,update_schedule,uploads' ); return $this->_get( $url ); } /** * Get metadata about a feed (data source) configured in Facebook Business. * * @see https://developers.facebook.com/docs/marketing-api/reference/product-feed/ * @since 2.6.0 * * @param String $feed_id Facebook Catalog Id. * @return Array Facebook feed metadata. */ public function read_feed_metadata( $feed_id ) { $url = $this->build_url( $feed_id, '/?fields=created_time,latest_upload,product_count,schedule,update_schedule' ); return $this->_get( $url ); } /** * Get metadata about a recent feed upload. * * @see https://developers.facebook.com/docs/marketing-api/reference/product-feed-upload/ * @since 2.6.0 * * @param String $upload_id Feed Upload Id. * @return Array Feed upload metadata. */ public function read_upload_metadata( $upload_id ) { $url = $this->build_url( $upload_id, '/?fields=error_count,warning_count,num_detected_items,num_persisted_items,url' ); return $this->_get( $url ); } /** * Create product_feeds graph edge url. * * @since 2.6.0 * * @param String $facebook_catalog_id Facebook Catalog Id. * @return String Graph edge url. */ public function get_feed_endpoint_url( $facebook_catalog_id ) { return $this->build_url( $facebook_catalog_id, '/product_feeds' ); } public function get_upload_status( $facebook_upload_id ) { $url = $this->build_url( $facebook_upload_id, '/?fields=end_time' ); // success API call will return // {id: <upload id>, end_time: <time when upload completes>} // failure API will return {error: <error message>} return self::_get( $url ); } // success API call will return a JSON of tip info public function get_tip_info( $external_merchant_settings_id ) { $url = $this->build_url( $external_merchant_settings_id, '/?fields=connect_woo' ); $response = self::_get( $url, $this->api_key ); $data = array( 'response' => $response, ); if ( is_wp_error( $response ) ) { $data['error_type'] = 'is_wp_error'; WC_Facebookcommerce_Utils::fblog( 'Failed to get AYMT tip info via API.', $data, true ); return; } if ( $response['response']['code'] != '200' ) { $data['error_type'] = 'Non-200 error code from FB'; WC_Facebookcommerce_Utils::fblog( 'Failed to get AYMT tip info via API.', $data, true ); return; } $response_body = wp_remote_retrieve_body( $response ); $connect_woo = WC_Facebookcommerce_Utils::decode_json( $response_body )->connect_woo; if ( ! isset( $connect_woo ) ) { $data['error_type'] = 'Response body not set'; WC_Facebookcommerce_Utils::fblog( 'Failed to get AYMT tip info via API.', $data, true ); } return $connect_woo; } public function get_facebook_id( $facebook_catalog_id, $product_id ) { $param = 'catalog:' . (string) $facebook_catalog_id . ':' . base64_encode( $product_id ) . '/?fields=id,product_group{id}'; $url = $this->build_url( '', $param ); // success API call will return // {id: <fb product id>, product_group{id} <fb product group id>} // failure API will return {error: <error message>} return self::_get( $url ); } public function check_product_info( $facebook_catalog_id, $product_id, $pr_v ) { $param = 'catalog:' . (string) $facebook_catalog_id . ':' . base64_encode( $product_id ) . '/?fields=id,name,description,price,' . 'sale_price,sale_price_start_date,sale_price_end_date,image_url,' . 'visibility'; if ( $pr_v ) { $param = $param . ',additional_variant_attributes{value}'; } $url = $this->build_url( '', $param ); // success API call will return // {id: <fb product id>, name,description,price,sale_price,sale_price_start_date // sale_price_end_date // failure API will return {error: <error message>} return self::_get( $url ); } /** * Gets the connected asset IDs. * * These will be things like pixel & page ID. * * @since 2.0.0 * * @param string $external_business_id the connected external business ID * @return array * @throws Framework\SV_WC_API_Exception */ public function get_asset_ids( $external_business_id ) { $url = $this->build_url( 'fbe_business/fbe_installs?fbe_external_business_id=', $external_business_id ); $response = $this->perform_request( $url ); $data = wp_remote_retrieve_body( $response ); $data = json_decode( $data, true ); if ( ! is_array( $data ) || empty( $data['data'][0] ) ) { throw new Framework\SV_WC_API_Exception( 'Data is missing' ); } $ids = $data['data'][0]; // normalize the page ID to match the others if ( ! empty( $ids['profiles'] ) && is_array( $ids['profiles'] ) ) { $ids['page_id'] = current( $ids['profiles'] ); } return $ids; } public function set_default_variant( $product_group_id, $data ) { $url = $this->build_url( $product_group_id ); return self::_post( $url, $data ); } private function build_url( $field_id, $param = '', $api_version = '' ) { $api_url = self::GRAPH_API_URL; if ( ! empty( $api_version ) ) { $api_url = $api_url . $api_version . '/'; } else { $api_url = $api_url . self::API_VERSION . '/'; } return $api_url . (string) $field_id . $param; } } endif;