<?php namespace SkyVerge\WooCommerce\Facebook\Feed; if ( ! defined( 'ABSPATH' ) ) { exit; } use Error; use SkyVerge\WooCommerce\Facebook\Utilities\Heartbeat; use SkyVerge\WooCommerce\Facebook\Products\Feed; /** * A class responsible detecting feed configuration. */ class FeedConfigurationDetection { /** * Constructor. */ public function __construct() { add_action( Heartbeat::DAILY, array( $this, 'track_data_source_feed_tracker_info' ) ); } /** * Store config settings for feed-based sync for WooCommerce Tracker. * * Gets various settings related to the feed, and data about recent uploads. * This is formatted into an array of keys/values, and saved to a transient for inclusion in tracker snapshot. * Note this does not send the data to tracker - this happens later (see Tracker class). * * @since 2.6.0 * @return void */ public function track_data_source_feed_tracker_info() { try { $info = $this->get_data_source_feed_tracker_info(); facebook_for_woocommerce()->get_tracker()->track_facebook_feed_config( $info ); } catch ( \Error $error ) { facebook_for_woocommerce()->log( 'Unable to detect valid feed configuration: ' . $error->getMessage() ); } } /** * Get config settings for feed-based sync for WooCommerce Tracker. * * @throws Error Catalog id missing. * @return Array Key-value array of various configuration settings. */ private function get_data_source_feed_tracker_info() { $integration = facebook_for_woocommerce()->get_integration(); $graph_api = $integration->get_graph_api(); $integration_feed_id = $integration->get_feed_id(); $catalog_id = $integration->get_product_catalog_id(); $info = array(); $info['site-feed-id'] = $integration_feed_id; // No catalog id. Most probably means that we don't have a valid connection. if ( '' === $catalog_id ) { throw new Error( 'No catalog ID' ); } // Get all feeds configured for the catalog. $feed_nodes = $this->get_feed_nodes_for_catalog( $catalog_id, $graph_api ); $info['feed-count'] = count( $feed_nodes ); // Check if the catalog has any feed configured. if ( empty( $feed_nodes ) ) { throw new Error( 'No feed nodes for catalog' ); } /* * We will only track settings for one feed config (for now at least). * So we need to determine which is the most relevant feed. * If there is only one, we use that. * If one has the same ID as $integration_feed_id, we use that. * Otherwise we pick the one that was most recently updated. */ $active_feed_metadata = array(); foreach ( $feed_nodes as $feed ) { $metadata = $this->get_feed_metadata( $feed['id'], $graph_api ); if ( $feed['id'] === $integration_feed_id ) { $active_feed_metadata = $metadata; break; } if ( ! array_key_exists( 'latest_upload', $metadata ) || ! array_key_exists( 'start_time', $metadata['latest_upload'] ) ) { continue; } $metadata['latest_upload_time'] = strtotime( $metadata['latest_upload']['start_time'] ); if ( ! $active_feed_metadata || ( $metadata['latest_upload_time'] > $active_feed_metadata['latest_upload_time'] ) ) { $active_feed_metadata = $metadata; } } if ( empty( $active_feed_metadata ) ) { // No active feed available, we don't have data to collect. $info['active-feed'] = null; return $info; } $active_feed = array(); if ( array_key_exists( 'created_time', $active_feed_metadata ) ) { $active_feed['created-time'] = gmdate( 'Y-m-d H:i:s', strtotime( $active_feed_metadata['created_time'] ) ); } if ( array_key_exists( 'product_count', $active_feed_metadata ) ) { $active_feed['product-count'] = $active_feed_metadata['product_count']; } /* * Upload schedule settings can be in two keys: * `schedule` => full replace of catalog with items in feed (including delete). * `update_schedule` => append any new or updated products to catalog. * These may both be configured; we will track settings for each individually (i.e. both). * https://developers.facebook.com/docs/marketing-api/reference/product-feed/ */ if ( array_key_exists( 'schedule', $active_feed_metadata ) ) { $active_feed['schedule']['interval'] = $active_feed_metadata['schedule']['interval']; $active_feed['schedule']['interval-count'] = $active_feed_metadata['schedule']['interval_count']; } if ( array_key_exists( 'update_schedule', $active_feed_metadata ) ) { $active_feed['update-schedule']['interval'] = $active_feed_metadata['update_schedule']['interval']; $active_feed['update-schedule']['interval-count'] = $active_feed_metadata['update_schedule']['interval_count']; } $info['active-feed'] = $active_feed; if ( array_key_exists( 'latest_upload', $active_feed_metadata ) ) { $latest_upload = $active_feed_metadata['latest_upload']; $upload = array(); if ( array_key_exists( 'end_time', $latest_upload ) ) { $upload['end-time'] = gmdate( 'Y-m-d H:i:s', strtotime( $latest_upload['end_time'] ) ); } // Get more detailed metadata about the most recent feed upload. $upload_metadata = $this->get_feed_upload_metadata( $latest_upload['id'], $graph_api ); $upload['error-count'] = $upload_metadata['error_count']; $upload['warning-count'] = $upload_metadata['warning_count']; $upload['num-detected-items'] = $upload_metadata['num_detected_items']; $upload['num-persisted-items'] = $upload_metadata['num_persisted_items']; // True if the feed upload url (Facebook side) matches the feed endpoint URL and secret. // If it doesn't match, it's likely it's unused. $upload['url-matches-site-endpoint'] = wc_bool_to_string( Feed::get_feed_data_url() === $upload_metadata['url'] ); $info['active-feed']['latest-upload'] = $upload; } return $info; } /** * Given catalog id this function fetches all feed configurations defined for this catalog. * * @throws Error Feed configurations fetch was not successful. * @param String $catalog_id Facebook Catalog ID. * @param WC_Facebookcommerce_Graph_API $graph_api Facebook Graph handler instance. * * @return Array Array of feed configurations. */ private function get_feed_nodes_for_catalog( $catalog_id, $graph_api ) { // Read all the feed configurations specified for the catalog. $response = $graph_api->read_feeds( $catalog_id ); $code = (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { throw new Error( 'Reading catalog feeds error', $code ); } $response_body = wp_remote_retrieve_body( $response ); $body = json_decode( $response_body, true ); return $body['data']; } /** * Given feed id fetch this feed configuration metadata. * * @throws Error Feed metadata fetch was not successful. * @param String $feed_id Facebook Feed ID. * @param WC_Facebookcommerce_Graph_API $graph_api Facebook Graph handler instance. * * @return Array Array of feed configurations. */ private function get_feed_metadata( $feed_id, $graph_api ) { $response = $graph_api->read_feed_metadata( $feed_id ); $code = (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { throw new Error( 'Error reading feed metadata', $code ); } $response_body = wp_remote_retrieve_body( $response ); return json_decode( $response_body, true ); } /** * Given upload id fetch this upload execution metadata. * * @throws Error Upload metadata fetch was not successful. * @param String $upload_id Facebook Feed upload ID. * @param WC_Facebookcommerce_Graph_API $graph_api Facebook Graph handler instance. * * @return Array Array of feed configurations. */ private function get_feed_upload_metadata( $upload_id, $graph_api ) { $response = $graph_api->read_upload_metadata( $upload_id ); $code = (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { throw new Error( 'Error reading feed upload metadata', $code ); } $response_body = wp_remote_retrieve_body( $response ); return json_decode( $response_body, true ); } }