diff --git a/class-wc-facebookcommerce.php b/class-wc-facebookcommerce.php index 4362137d2..8b436ea64 100644 --- a/class-wc-facebookcommerce.php +++ b/class-wc-facebookcommerce.php @@ -69,6 +69,9 @@ class WC_Facebookcommerce extends WooCommerce\Facebook\Framework\Plugin { /** @var WooCommerce\Facebook\Products\Feed product feed handler */ private $product_feed; + /** @var WooCommerce\Facebook\Feed\FeedManager Entrypoint and creates all other feeds */ + public $feed_manager; + /** @var Background_Handle_Virtual_Products_Variations instance */ protected $background_handle_virtual_products_variations; @@ -187,7 +190,8 @@ public function init() { $this->heartbeat = new Heartbeat( WC()->queue() ); $this->heartbeat->init(); - $this->checkout = new WooCommerce\Facebook\Checkout(); + $this->feed_manager = new WooCommerce\Facebook\Feed\FeedManager(); + $this->checkout = new WooCommerce\Facebook\Checkout(); $this->product_feed = new WooCommerce\Facebook\Products\Feed(); $this->products_stock_handler = new WooCommerce\Facebook\Products\Stock(); $this->products_sync_handler = new WooCommerce\Facebook\Products\Sync(); diff --git a/facebook-commerce.php b/facebook-commerce.php index ba6f83855..fefec06e7 100644 --- a/facebook-commerce.php +++ b/facebook-commerce.php @@ -870,7 +870,7 @@ private function save_product_settings( WC_Product $product ) { $woo_product->set_description( sanitize_text_field( wp_unslash( $_POST[ self::FB_PRODUCT_DESCRIPTION ] ) ) ); $woo_product->set_rich_text_description( $_POST[ self::FB_PRODUCT_DESCRIPTION ] ); } - + if ( isset( $_POST[ WC_Facebook_Product::FB_PRODUCT_PRICE ] ) ) { $woo_product->set_price( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_PRODUCT_PRICE ] ) ) ); } diff --git a/includes/API.php b/includes/API.php index 29160aa85..2f5e231bc 100644 --- a/includes/API.php +++ b/includes/API.php @@ -13,6 +13,7 @@ defined( 'ABSPATH' ) or exit; +use WooCommerce\Facebook\API\Exceptions\Request_Limit_Reached; use WooCommerce\Facebook\API\Request; use WooCommerce\Facebook\API\Response; use WooCommerce\Facebook\Events\Event; @@ -101,7 +102,6 @@ protected function perform_request( $request ): API\Response { return parent::perform_request( $request ); } - /** * Validates a response after it has been parsed and instantiated. * @@ -546,12 +546,26 @@ public function read_upload( string $product_feed_upload_id ) { * @throws ApiException * @throws API\Exceptions\Request_Limit_Reached */ - public function create_upload( string $product_feed_id, array $data ) { + public function create_product_feed_upload( string $product_feed_id, array $data ): Response { $request = new API\ProductCatalog\ProductFeedUploads\Create\Request( $product_feed_id, $data ); $this->set_response_handler( API\ProductCatalog\ProductFeedUploads\Create\Response::class ); return $this->perform_request( $request ); } + /** + * @param string $cpi_id The commerce partner integration id. + * @param array $data The json body for the Generic Feed Upload endpoint. + * + * @return Response + * @throws Request_Limit_Reached + * @throws ApiException + */ + public function create_common_data_feed_upload( string $cpi_id, array $data ): Response { + $request = new API\CommonFeedUploads\Create\Request( $cpi_id, $data ); + $this->set_response_handler( API\CommonFeedUploads\Create\Response::class ); + return $this->perform_request( $request ); + } + /** * @param string $external_merchant_settings_id diff --git a/includes/API/CommonFeedUploads/Create/Request.php b/includes/API/CommonFeedUploads/Create/Request.php new file mode 100644 index 000000000..32c4536c2 --- /dev/null +++ b/includes/API/CommonFeedUploads/Create/Request.php @@ -0,0 +1,37 @@ +data_stream_name; + if ( ! as_next_scheduled_action( $schedule_action_hook_name ) ) { + as_schedule_recurring_action( + time(), + $this->gen_feed_interval, + $schedule_action_hook_name, + array(), + facebook_for_woocommerce()->get_id_dasherized() + ); + } + } + + /** + * Regenerates the example feed based on the defined schedule. + * New style feed will use the FeedGenerator to queue the feed generation. Use for batched feed generation. + * Old style feed will use the FeedHandler to generate the feed file. Use if batch not needed or new style not enabled. + * + * @since 3.5.0 + */ + public function regenerate_feed(): void { + // Maybe use new ( experimental ), feed generation framework. + if ( \WC_Facebookcommerce::instance()->get_integration()->is_new_style_feed_generation_enabled() ) { + $this->feed_generator->queue_start(); + } else { + $this->feed_handler->generate_feed_file(); + } + } + + /** + * Trigger the upload flow + * Once feed regenerated, trigger upload via create_upload API + * This will hit the url defined in the class and trigger handle_feed_data_request + * + * @since 3.5.0 + */ + public function send_request_to_upload_feed(): void { + $name = $this->data_stream_name; + $data = array( + 'url' => self::get_feed_data_url(), + 'feed_type' => $this->feed_type, + 'update_type' => 'CREATE', + ); + + try { + $cpi_id = get_option( 'wc_facebook_commerce_partner_integration_id', '' ); + facebook_for_woocommerce()-> + get_api()-> + create_common_data_feed_upload( $cpi_id, $data ); + } catch ( Exception $e ) { + // Log the error and continue. + \WC_Facebookcommerce_Utils::log( "{$name} feed: Failed to create feed upload request: " . $e->getMessage() ); + } + } + + /** + * Gets the URL for retrieving the feed data using legacy WooCommerce REST API. + * Sample url: + * https://your-site-url.com/?wc-api=wc_facebook_get_feed_data_example&secret=your_generated_secret + * + * @since 3.5.0 + * @return string + */ + public function get_feed_data_url(): string { + $query_args = array( + 'wc-api' => self::REQUEST_FEED_ACTION . $this->data_stream_name, + 'secret' => self::get_feed_secret(), + ); + + // phpcs:ignore + // nosemgrep: audit.php.wp.security.xss.query-arg + return add_query_arg( $query_args, home_url( '/' ) ); + } + + + /** + * Gets the secret value that should be included in the legacy WooCommerce REST API URL. + * + * @since 3.5.0 + * @return string + */ + public function get_feed_secret(): string { + $secret = get_option( $this->feed_url_secret_option_name, '' ); + if ( ! $secret ) { + $secret = wp_hash( 'example-feed-' . time() ); + update_option( $this->feed_url_secret_option_name, $secret ); + } + + return $secret; + } + + /** + * Callback function that streams the feed file to the GraphPartnerIntegrationFileUpdatePost + * Ex: https://your-site-url.com/?wc-api=wc_facebook_get_feed_data_example&secret=your_generated_secret + * The above WooC Legacy REST API will trigger the handle_feed_data_request method + * See LegacyRequestApiStub.php for more details + * + * @throws PluginException If file issue comes up. + * @since 3.5.0 + */ + public function handle_feed_data_request(): void { + $name = $this->data_stream_name; + \WC_Facebookcommerce_Utils::log( "{$name} feed: Meta is requesting feed file." ); + + $file_path = $this->feed_handler->get_feed_writer()->get_file_path(); + + // regenerate if the file doesn't exist. + if ( ! file_exists( $file_path ) ) { + $this->feed_handler->generate_feed_file(); + } + + try { + // bail early if the feed secret is not included or is not valid. + if ( self::get_feed_secret() !== Helper::get_requested_value( 'secret' ) ) { + throw new PluginException( "{$name} feed: Invalid secret provided.", 401 ); + } + + // bail early if the file can't be read. + if ( ! is_readable( $file_path ) ) { + throw new PluginException( "{$name}: File at path ' . $file_path . ' is not readable.", 404 ); + } + + // set the download headers. + header( 'Content-Type: text/csv; charset=utf-8' ); + header( 'Content-Description: File Transfer' ); + header( 'Content-Disposition: attachment; filename="' . basename( $file_path ) . '"' ); + header( 'Expires: 0' ); + header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' ); + header( 'Pragma: public' ); + header( 'Content-Length:' . filesize( $file_path ) ); + + // phpcs:ignore + $file = @fopen( $file_path, 'rb' ); + if ( ! $file ) { + throw new PluginException( "{$name} feed: Could not open feed file.", 500 ); + } + + // fpassthru might be disabled in some hosts (like Flywheel). + // phpcs:ignore + if ( \WC_Facebookcommerce_Utils::is_fpassthru_disabled() || ! @fpassthru( $file ) ) { + \WC_Facebookcommerce_Utils::log( "{$name} feed: fpassthru is disabled: getting file contents" ); + //phpcs:ignore + $contents = @stream_get_contents( $file ); + if ( ! $contents ) { + throw new PluginException( 'Could not get feed file contents.', 500 ); + } + echo $contents; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } catch ( \Exception $exception ) { + \WC_Facebookcommerce_Utils::log( "{$name} feed: Could not serve feed. " . $exception->getMessage() . ' (' . $exception->getCode() . ')' ); + status_header( $exception->getCode() ); + } + exit; + } +} diff --git a/includes/Feed/CsvFeedFileWriter.php b/includes/Feed/CsvFeedFileWriter.php new file mode 100644 index 000000000..d1380d211 --- /dev/null +++ b/includes/Feed/CsvFeedFileWriter.php @@ -0,0 +1,283 @@ +feed_name = $feed_name; + $this->header_row = $header_row; + } + + /** + * Write the feed file. + * + * @param array $data The data to write to the feed file. + * @return void + * @since 3.5.0 + */ + public function write_feed_file( array $data ): void { + try { + $this->create_feed_directory(); + $this->create_files_to_protect_feed_directory(); + + // Step 1: Prepare the temporary empty feed file with header row. + $temp_feed_file = $this->prepare_temporary_feed_file(); + + // Step 2: Write feed into the temporary feed file. + $this->write_temp_feed_file( $data ); + + // Step 3: Rename temporary feed file to final feed file. + $this->promote_temp_file(); + } catch ( PluginException $e ) { + WC_Facebookcommerce_Utils::log( wp_json_encode( $e->getMessage() ) ); + // Close the temporary file. + if ( ! empty( $temp_feed_file ) && is_resource( $temp_feed_file ) ) { + fclose( $temp_feed_file ); //phpcs:ignore + } + + // Delete the temporary file. + if ( ! empty( $temp_file_path ) && file_exists( $temp_file_path ) ) { + unlink( $temp_file_path ); //phpcs:ignore + } + } + } + + /** + * Generates the feed file. + * + * @throws PluginException If the directory could not be created. + * @since 3.5.0 + */ + public function create_feed_directory(): void { + $file_directory = $this->get_file_directory(); + $directory_created = wp_mkdir_p( $file_directory ); + if ( ! $directory_created ) { + //phpcs:ignore -- Escaping function for translated string not available in this context + throw new PluginException( __( "Could not create feed directory at {$file_directory}", 'facebook-for-woocommerce' ), 500 ); + } + } + + /** + * Write the feed data to the temporary feed file. + * + * @param array $data The data to write to the feed file. + * @since 3.5.0 + */ + public function write_temp_feed_file( array $data ): void { + //phpcs:ignore -- use php file i/o functions + $temp_feed_file = fopen( $this->get_temp_file_path(), 'a' ); + if ( ! empty( $this->get_temp_file_path() ) ) { + // Turn headers into an array of string accessors + $accessors = str_getcsv( $this->header_row ); + // For each object, turn into a csv row via accessors + foreach ( $data as $obj ) { + $line = ''; + foreach ( $accessors as $accessor ) { + $val = $obj[ $accessor ]; + $line .= "{$val},"; + } + + $line = rtrim( $line, ',' ); + $line .= PHP_EOL; + fwrite( $temp_feed_file, $line ); //phpcs:ignore + } + } + + if ( ! empty( $temp_feed_file ) ) { + fclose( $temp_feed_file ); //phpcs:ignore + } + } + + /** + * Creates files in the feed directory to prevent directory listing and hotlinking. + * + * @since 3.5.0 + */ + public function create_files_to_protect_feed_directory(): void { + $feed_directory = trailingslashit( $this->get_file_directory() ); + + $files = array( + array( + 'base' => $feed_directory, + 'file' => 'index.html', + 'content' => '', + ), + array( + 'base' => $feed_directory, + 'file' => '.htaccess', + 'content' => 'deny from all', + ), + ); + + foreach ( $files as $file ) { + + if ( wp_mkdir_p( $file['base'] ) && ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) { + // phpcs:ignore -- use php file i/o functions + $file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'w' ); + if ( $file_handle ) { + fwrite( $file_handle, $file['content'] ); //phpcs:ignore + fclose( $file_handle ); //phpcs:ignore + } + } + } + } + + /** + * Gets the feed file path of given feed. + * + * @return string + * @since 3.5.0 + */ + public function get_file_path(): string { + return "{$this->get_file_directory()}/{$this->get_file_name()}"; + } + + + /** + * Gets the temporary feed file path. + * + * @return string + * @since 3.5.0 + */ + public function get_temp_file_path(): string { + return "{$this->get_file_directory()}/{$this->get_temp_file_name()}"; + } + + /** + * Gets the feed file directory. + * + * @return string + * @since 3.5.0 + */ + public function get_file_directory(): string { + $uploads_directory = wp_upload_dir( null, false ); + return trailingslashit( $uploads_directory['basedir'] ) . sprintf( self::UPLOADS_DIRECTORY, $this->feed_name ); + } + + + /** + * Gets the feed file name. + * + * @return string + * @since 3.5.0 + */ + public function get_file_name(): string { + $feed_secret = facebook_for_woocommerce()->feed_manager->get_feed_secret( $this->feed_name ); + return sprintf( self::FILE_NAME, $this->feed_name, $feed_secret ); + } + + /** + * Gets the temporary feed file name. + * + * @return string + * @since 3.5.0 + */ + public function get_temp_file_name(): string { + $feed_secret = facebook_for_woocommerce()->feed_manager->get_feed_secret( $this->feed_name ); + return sprintf( self::FILE_NAME, $this->feed_name, 'temp_' . wp_hash( $feed_secret ) ); + } + + /** + * Prepare a fresh empty temporary feed file with the header row. + * + * @throws PluginException We can't open the file or the file is not writable. + * @return resource A file pointer resource. + * @since 3.5.0 + */ + public function prepare_temporary_feed_file() { + $temp_file_path = $this->get_temp_file_path(); + //phpcs:ignore -- use php file i/o functions + $temp_feed_file = @fopen( $temp_file_path, 'w' ); + + // Check if we can open the temporary feed file. + // phpcs:ignore + if ( false === $temp_feed_file || ! is_writable( $temp_file_path ) ) { + // phpcs:ignore -- Escaping function for translated string not available in this context + throw new PluginException( __( "Could not open file {$temp_file_path} for writing.", 'facebook-for-woocommerce' ), 500 ); + } + + $file_path = $this->get_file_path(); + + // Check if we will be able to write to the final feed file. + //phpcs:ignore -- use php file i/o functions + if ( file_exists( $file_path ) && ! is_writable( $file_path ) ) { + // phpcs:ignore -- Escaping function for translated string not available in this context + throw new PluginException( __( "Could not open file {$file_path} for writing.", 'facebook-for-woocommerce' ), 500 ); + } + + //phpcs:ignore -- use php file i/o functions + fwrite( $temp_feed_file, $this->header_row); + return $temp_feed_file; + } + + /** + * Rename temporary feed file into the final feed file. + * This is the last step fo the feed generation procedure. + * + * @since 3.5.0 + * @throws PluginException If the temporary feed file could not be renamed. + */ + public function promote_temp_file(): void { + $file_path = $this->get_file_path(); + $temp_file_path = $this->get_temp_file_path(); + if ( ! empty( $temp_file_path ) && ! empty( $file_path ) ) { + + // phpcs:ignore -- use php file i/o functions + $renamed = rename( $temp_file_path, $file_path ); + + if ( empty( $renamed ) ) { + // phpcs:ignore -- Escaping function for translated string not available in this context + throw new PluginException( __( "Could not promote temp file: {$temp_file_path}", 'facebook-for-woocommerce' ), 500 ); + } + } + } +} diff --git a/includes/Feed/FeedFileWriter.php b/includes/Feed/FeedFileWriter.php new file mode 100644 index 000000000..adbb2282e --- /dev/null +++ b/includes/Feed/FeedFileWriter.php @@ -0,0 +1,102 @@ +feed_writer = $feed_writer; + $this->feed_name = $feed_name; + } + + /** + * Handles the start of the feed generation process. + * + * @inheritdoc + * @since 3.5.0 + */ + protected function handle_start(): void { + // Create directory if not available and then the files to protect the directory. + $this->feed_writer->create_files_to_protect_feed_directory(); + $this->feed_writer->prepare_temporary_feed_file(); + } + + /** + * Handles the end of the feed generation process. + * + * @inheritdoc + * @since 3.5.0 + */ + protected function handle_end(): void { + $this->feed_writer->promote_temp_file(); + + /** + * Trigger upload from ExampleFeed instance + * + * @since 3.5.0 + */ + do_action( AbstractFeed::FEED_GEN_COMPLETE_ACTION . $this->feed_name ); + } + + /** + * Get a set of items for the batch. + * + * NOTE: when using an OFFSET based query to retrieve items it's recommended to order by the item ID while + * ASCENDING. This is so that any newly added items will not disrupt the query offset. + * Override with your custom SQL logic. + * + * @param int $batch_number The batch number increments for each new batch in the job cycle. + * @param array $args The args for the job. + * @since 3.5.0 + * @throws Exception On error. The failure will be logged by Action Scheduler and the job chain will stop. + */ + protected function get_items_for_batch( int $batch_number, array $args ): array { + return array(); + } + + /** + * Processes a batch of items. + * + * @param array $items The items to process. + * @param array $args Additional arguments. + * @inheritdoc + * @since 3.5.0 + */ + protected function process_items( array $items, array $args ): void { + $this->feed_writer->write_temp_feed_file( $items ); + } + + /** + * The single item processing logic. Might not need if only using the whole batch. + * + * @param object $item the singular item to process. This method might not be used but needed to extend parent. + * @param array $args the args for the job. + * + * @since 3.5.0 + */ + protected function process_item( $item, array $args ) { + } + + /** + * Get the name/slug of the job. + * + * @return string + * @since 3.5.0 + */ + public function get_name(): string { + return $this->feed_name . '_feed_generator'; + } + + /** + * Get the name/slug of the plugin that owns the job. + * + * @return string + * @since 3.5.0 + */ + public function get_plugin_name(): string { + return WC_Facebookcommerce::PLUGIN_ID; + } + + /** + * Get the job's batch size. + * + * @return int + * @since 3.5.0 + */ + protected function get_batch_size(): int { + return 1; + } +} diff --git a/includes/Feed/FeedHandler.php b/includes/Feed/FeedHandler.php new file mode 100644 index 000000000..588a8519d --- /dev/null +++ b/includes/Feed/FeedHandler.php @@ -0,0 +1,36 @@ + The list of feed types as named strings. + * @since 3.5.0 + */ + private array $feed_types; + + /** + * The map of feed types to their instances. + * + * @var array The map of feed types to their instances. + * @since 3.5.0 + */ + private array $feed_instances = array(); + + /** + * FeedManager constructor. + * Instantiates all the registered feed types and keeps in map. + * + * @since 3.5.0 + */ + public function __construct() { + $this->feed_types = $this->get_feed_types(); + foreach ( $this->feed_types as $feed_type ) { + $this->feed_instances[ $feed_type ] = $this->create_feed( $feed_type ); + } + } + + /** + * Create a feed based on the data stream name. + * + * @param string $data_stream_name The name of the data stream. + * + * phpcs:ignore -- Method to be implemented when new feed types are added. + * @return AbstractFeed The created feed instance derived from AbstractFeed. + * @throws \InvalidArgumentException If the data stream doesn't correspond to a FeedType. + * @since 3.5.0 + */ + private function create_feed( string $data_stream_name ): AbstractFeed { + throw new \InvalidArgumentException( "Invalid feed type {$data_stream_name}" ); + } + + /** + * Get the list of feed types. + * + * @return array + * @since 3.5.0 + */ + public static function get_feed_types(): array { + return array(); + } + + /** + * Get the feed instance for the given feed type. + * + * @param string $feed_type the specific feed in question. + * @return string + * @since 3.5.0 + */ + public function get_feed_secret( string $feed_type ): string { + $instance = $this->feed_instances[ $feed_type ]; + return $instance->get_feed_secret(); + } +} diff --git a/includes/Products/Feed.php b/includes/Products/Feed.php index 75114dd79..2aef89fa8 100644 --- a/includes/Products/Feed.php +++ b/includes/Products/Feed.php @@ -204,7 +204,7 @@ public function send_request_to_upload_feed() { ]; try { - facebook_for_woocommerce()->get_api()->create_upload( $feed_id, $data ); + facebook_for_woocommerce()->get_api()->create_product_feed_upload( $feed_id, $data ); } catch ( Exception $exception ) { facebook_for_woocommerce()->log( 'Failed to create feed upload request: ' . $exception->getMessage() ); } diff --git a/includes/fbproductfeed.php b/includes/fbproductfeed.php index 4f14f625c..1f59b754c 100644 --- a/includes/fbproductfeed.php +++ b/includes/fbproductfeed.php @@ -202,6 +202,7 @@ public function generate_productfeed_file() { /** * Creates files in the catalog feed directory to prevent directory listing and hotlinking. + * Will create directory if not already available * * @since 1.11.0 */ diff --git a/includes/fbutils.php b/includes/fbutils.php index 48ad0e382..b5a9bc64d 100644 --- a/includes/fbutils.php +++ b/includes/fbutils.php @@ -864,10 +864,10 @@ public static function prepare_product_variation_data_items_batch( $product ) { public static function logExceptionImmediatelyToMeta(Throwable $error, array $context = []) { /** * WIP: This is a dummy function to send exception logs to Meta. - * $context is an array of data that will be sent to Meta, includes commerce_merchant_settings_id, + * $context is an array of data that will be sent to Meta, includes commerce_merchant_settings_id, * catalog_id, order_id, promotion_id, flow_name, flow_step, extra_data and etc. */ - + // TODO: Implement enqueue logging to Meta function. $response = null; @@ -880,16 +880,36 @@ public static function logExceptionImmediatelyToMeta(Throwable $error, array $co public static function logTelemetryToMeta(string $message, array $context = []) { /** * WIP: This is a dummy function to send telemetry logs to Meta. - * $context is an array of data that will be sent to Meta, includes commerce_merchant_settings_id, + * $context is an array of data that will be sent to Meta, includes commerce_merchant_settings_id, * catalog_id, order_id, promotion_id, flow_name, flow_step, extra_data and etc. */ - + // TODO: Implement push logging request to global message queue function. $response = null; - + return $response; } + /** + * Checks whether fpassthru has been disabled in PHP. + * + * @since 3.5.0 + * @return bool + */ + public static function is_fpassthru_disabled(): bool { + $disabled = false; + if ( function_exists( 'ini_get' ) ) { + // phpcs:ignore + $disabled_functions = @ini_get( 'disable_functions' ); + + $disabled = + is_string( $disabled_functions ) && + //phpcs:ignore + in_array( 'fpassthru', explode( ',', $disabled_functions ), false ); + } + return $disabled; + } + } endif; diff --git a/tests/Unit/ApiTest.php b/tests/Unit/ApiTest.php index e19f6a91c..4a19ea36f 100644 --- a/tests/Unit/ApiTest.php +++ b/tests/Unit/ApiTest.php @@ -772,7 +772,38 @@ public function test_create_upload_request() { }; add_filter( 'pre_http_request', $response, 10, 3 ); - $response = $this->api->create_upload( $product_feed_id, $data ); + $response = $this->api->create_product_feed_upload( $product_feed_id, $data ); + $this->assertFalse( $response->has_api_error() ); + } + + /** + * Tests create common upload feed request to Facebook. + * + * @return void + * @throws ApiException In case of network request error. + */ + public function test_create_common_upload_request() { + $cpi = '24316596247984028'; + + $data = array( + 'url' => 'http://example.com/?wc-api=wc_facebook_get_feed_data_example&secret=c4b8c3c46145aac6519e3f8a28bc86f2', + 'feed_type' => 'PRODUCT_RATINGS_AND_REVIEWS', + 'update_type' => 'CREATE', + ); + + $response = function( $result, $parsed_args, $url ) use ( $cpi ) { + $this->assertEquals( 'POST', $parsed_args['method'] ); + $this->assertEquals( "{$this->endpoint}{$this->version}/{$cpi}/file_update", $url ); + return [ + 'response' => [ + 'code' => 200, + 'message' => 'OK', + ], + ]; + }; + add_filter( 'pre_http_request', $response, 10, 3 ); + + $response = $this->api->create_common_data_feed_upload( $cpi, $data ); $this->assertFalse( $response->has_api_error() ); } }