Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
94f5d24
Combing both abstract feed and implementation
Jmencab Feb 3, 2025
749ab23
Add FeedGenerator and PromotionsFeedGenerator
Jmencab Feb 3, 2025
0d9b92d
Add FeedGenerator and PromotionsFeedGenerator. Add to Job Manager
Jmencab Feb 4, 2025
70d6980
implement promotions regenerate feed using generator factory
Jmencab Feb 5, 2025
ff1cf9a
Added feed handlers and feed writers
Jmencab Feb 5, 2025
4bba8ad
Add FeedFileWriter implementation and uses; add abstract class FeedIn…
Jmencab Feb 6, 2025
7933322
constructor for promotions feed; csv feed file writer implements inte…
Jmencab Feb 6, 2025
9315b80
FeedType Const; Use FeedHandler in FeedGenerator constructor and upda…
Jmencab Feb 6, 2025
5e5503e
use new feed gen constructor, feed type const in feed constructor
Jmencab Feb 6, 2025
a411833
change up generator factory
Jmencab Feb 6, 2025
f208231
use feed info tracker
Jmencab Feb 6, 2025
585b482
manager and factory classes set up
Jmencab Feb 6, 2025
aa1b642
Request and response for Common Feed Upload Create and Read
Jmencab Feb 8, 2025
a57fa22
rebase to latest with common upload v2
Jmencab Feb 8, 2025
462dbf1
combine feedtype and feedfactory into feed manager, create example feed
Jmencab Feb 18, 2025
f9dc4d0
expand on example feed
Jmencab Feb 19, 2025
94da181
example feed generator
Jmencab Feb 19, 2025
c6a5781
removed promotions and condensed classes
Jmencab Feb 19, 2025
a07ef39
modify request and responses
Jmencab Feb 19, 2025
9120a0e
feed immediately and job manager
Jmencab Feb 19, 2025
2a77fe8
since todos
Jmencab Feb 19, 2025
a5f6414
add since tags and fix phpcs issues
Jmencab Feb 19, 2025
954fa0a
Changing comments and phpcs problems
Jmencab Feb 20, 2025
1740f23
Implement Example Feed
Jmencab Feb 21, 2025
ee92045
About halfway implementing feed generator and csv feed handler
Jmencab Feb 25, 2025
1461188
Finish implementing the example feed; todo: manual, unit, integration…
Jmencab Feb 26, 2025
d8e430d
add throws tag
Jmencab Feb 26, 2025
6fdd6e5
Some fixes and some temp code for testing
Jmencab Feb 27, 2025
222a9c5
Remove hard coding, but add comments on how to test
Jmencab Mar 3, 2025
31ccc8a
remove package-lock.json from changes
Jmencab Mar 3, 2025
1c9d8d1
changes to api with comments
Jmencab Mar 3, 2025
6573a4f
add unit test
Jmencab Mar 3, 2025
a555d2b
linter corrections; comment out feed manager instantiation
Jmencab Mar 3, 2025
c9c64f5
Address comments
Jmencab Mar 3, 2025
9c952b5
Prototype; currently running on http://44.243.196.123/
Jmencab Mar 5, 2025
a3a1109
Move most code from ExampleFeedGenerator to FeedGenerator
Jmencab Mar 5, 2025
5c1ba5f
Remove examples
Jmencab Mar 5, 2025
0c5ba3e
Address Noah comments
Jmencab Mar 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion class-wc-facebookcommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion facebook-commerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ] ) ) );
}
Expand Down
18 changes: 16 additions & 2 deletions includes/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -101,7 +102,6 @@ protected function perform_request( $request ): API\Response {
return parent::perform_request( $request );
}


/**
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will not be present in final diff

* Validates a response after it has been parsed and instantiated.
*
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions includes/API/CommonFeedUploads/Create/Request.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* 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
*/

declare( strict_types=1 );

namespace WooCommerce\Facebook\API\CommonFeedUploads\Create;

use WooCommerce\Facebook\API\Request as ApiRequest;

defined( 'ABSPATH' ) || exit;

/**
* Request object for the Common Feed Upload.
*/
class Request extends ApiRequest {
const CPI_ENDPOINT = 'file_update';

/**
* Constructs the request.
*
* @param string $cpi_id Commerce Partner Integration ID.
* @param array $data Feed Metadata for File Update Post endpoint.
* @since 3.5.0
*/
public function __construct( string $cpi_id, array $data ) {
$endpoint = self::CPI_ENDPOINT;
parent::__construct( "/{$cpi_id}/{$endpoint}", 'POST' );
parent::set_data( $data );
}
}
24 changes: 24 additions & 0 deletions includes/API/CommonFeedUploads/Create/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
/**
* 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
*/

declare( strict_types=1 );

namespace WooCommerce\Facebook\API\CommonFeedUploads\Create;

use WooCommerce\Facebook\API\Response as ApiResponse;

defined( 'ABSPATH' ) || exit;

/**
* Response object for Common Feed Upload
*
* @since 3.5.0
*/
class Response extends ApiResponse {}
252 changes: 252 additions & 0 deletions includes/Feed/AbstractFeed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<?php
/**
* 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
*/

namespace WooCommerce\Facebook\Feed;

use WooCommerce\Facebook\Framework\Api\Exception;
use WooCommerce\Facebook\Framework\Helper;
use WooCommerce\Facebook\Framework\Plugin\Exception as PluginException;

defined( 'ABSPATH' ) || exit;

/**
* Abstract class AbstractFeed
*
* Provides the base functionality for handling Metadata feed requests and generation for Facebook integration.
*
* @package WooCommerce\Facebook\Feed
* @since 3.5.0
*/
abstract class AbstractFeed {
/** The action callback for generating a feed */
const GENERATE_FEED_ACTION = 'wc_facebook_regenerate_feed_';
/** The action slug for getting the feed */
const REQUEST_FEED_ACTION = 'wc_facebook_get_feed_data_';
/** The action slug for triggering file upload */
const FEED_GEN_COMPLETE_ACTION = 'wc_facebook_feed_generation_completed_';

/** Schedule feed generation on some interval hook name for children classes. */
const SCHEDULE_CALL_BACK = 'schedule_feed_generation';
/** Schedule an immediate file generator on the scheduler hook name. For testing mostly. */
const REGENERATE_CALL_BACK = 'regenerate_feed';
/** Make upload call to Meta hook name for children classes. */
const UPLOAD_CALL_BACK = 'send_request_to_upload_feed';
/** Stream file to upload endpoint hook name for children classes. */
const STREAM_CALL_BACK = 'handle_feed_data_request';
/** Hook prefix for Legacy REST API hook name */
const LEGACY_API_PREFIX = 'woocommerce_api_';
/** @var string the WordPress option name where the secret included in the feed URL is stored */
const OPTION_FEED_URL_SECRET = 'wc_facebook_feed_url_secret_';


/**
* The feed generator instance for the given feed.
*
* @var FeedGenerator
* @since 3.5.0
*/
protected FeedGenerator $feed_generator;

/**
* The feed handler instance for the given feed.
*
* @var FeedHandler
* @since 3.5.0
*/
protected FeedHandler $feed_handler;

/**
* The name of the data feed.
*
* @var string
*/
protected string $data_stream_name;

/**
* The option name for the feed URL secret.
*
* @var string
*/
protected string $feed_url_secret_option_name;

/**
* The type of feed as per the endpoint requirements.
*
* @var string
*/
protected string $feed_type;

/**
* The interval in seconds for the feed generation.
*
* @var int
*/
protected int $gen_feed_interval;

/**
* Schedules the recurring feed generation.
*
* @since 3.5.0
*/
public function schedule_feed_generation(): void {
$schedule_action_hook_name = self::GENERATE_FEED_ACTION . $this->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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to have a comment here explaining the difference between these two flows

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 );
}
Comment on lines +211 to +213
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly is the secret here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a hash that gets appended to the file names. you verify the secret here to make sure it's the correct file


// 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' );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if fpassthru is enabled then the file just needs to be opened?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ( \WC_Facebookcommerce_Utils::is_fpassthru_disabled() || ! @fpassthru( $file ) ) {
Is doing a lot... if fpassthru returns true, then it was passed to Meta, if it did not work then it needs to be streamed

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;
}
}
Loading