Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2a5b6cd
Setting up navigation menu feed upload flow
nrostrow-meta Apr 30, 2025
88028c3
Fixing test and working on setting up FeedUploadUtils logic for navig…
nrostrow-meta Apr 30, 2025
c15e0d4
Implemented logic to fetch category navigation
nrostrow-meta May 1, 2025
f7e99da
Adding tests for new logic
nrostrow-meta May 1, 2025
524373f
Adding WP_Mock to composer
nrostrow-meta May 1, 2025
4efe818
reverting change to composer.lock and setting up WP_Mock
nrostrow-meta May 1, 2025
b72e442
Ran composer update
nrostrow-meta May 2, 2025
98d7ef3
Getting tests to run locally
nrostrow-meta May 2, 2025
9e81605
Reverting composer.json and composer.lock changes
nrostrow-meta May 2, 2025
9a84439
Removing reference to WP_Mock
nrostrow-meta May 2, 2025
5d16f2e
Reverting change to composer.lock
nrostrow-meta May 2, 2025
699a806
Setting use_enhanced_onboarding back to false
nrostrow-meta May 2, 2025
4424d03
Tweaking R&R feed upload
nrostrow-meta May 2, 2025
493f1ac
Merge main
nrostrow-meta May 5, 2025
545c3dd
Removing newline
nrostrow-meta May 5, 2025
5b1869a
Trying to get site navigation feed upload working
nrostrow-meta May 5, 2025
95430aa
Merge branch 'main' into navigation_menu_feed_upload
nrostrow-meta May 6, 2025
dba93ad
Merge branch 'main' into navigation_menu_feed_upload
nrostrow-meta May 6, 2025
2e4d518
Merge branch 'main' into navigation_menu_feed_upload
nrostrow-meta May 6, 2025
658379d
Using term_taxonomy_id instead of term_id
nrostrow-meta May 6, 2025
7359933
Merge branch 'main' into navigation_menu_feed_upload
nrostrow-meta May 7, 2025
25a7558
Removing commented out code
nrostrow-meta May 7, 2025
6431294
Removing country field from ratings and reviews feed upload
nrostrow-meta May 7, 2025
c3021e0
Merge branch 'main' into navigation_menu_feed_upload
nrostrow-meta May 7, 2025
c9b7681
Iterating on feedback
nrostrow-meta May 7, 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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"autoload": {
"psr-4": {
"WooCommerce\\Facebook\\": "includes",
"WooCommerce\\Facebook\\Feed\\": ["includes/Feed", "includes/Feed/Promotions", "includes/Feed/RatingsAndReviews", "includes/Feed/ShippingProfiles"]
"WooCommerce\\Facebook\\Feed\\": ["includes/Feed", "includes/Feed/Promotions", "includes/Feed/RatingsAndReviews", "includes/Feed/ShippingProfiles", "includes/Feed/NavigationMenu"]
}
},
"autoload-dev": {
Expand Down
21 changes: 13 additions & 8 deletions includes/Feed/AbstractFeed.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

namespace WooCommerce\Facebook\Feed;

use WooCommerce\Facebook\Framework\Api\Exception;
use WooCommerce\Facebook\Framework\Helper;
use WooCommerce\Facebook\Framework\Plugin\Exception as PluginException;
use WooCommerce\Facebook\Utilities\Heartbeat;
Expand Down Expand Up @@ -42,10 +41,10 @@ abstract class AbstractFeed {
/**
* The feed writer instance for the given feed.
*
* @var FeedFileWriter
* @var AbstractFeedFileWriter
* @since 3.5.0
*/
protected FeedFileWriter $feed_writer;
protected AbstractFeedFileWriter $feed_writer;

/**
* The feed generator instance for the given feed.
Expand All @@ -66,11 +65,11 @@ abstract class AbstractFeed {
/**
* Initialize feed properties.
*
* @param FeedFileWriter $feed_writer The feed file writer instance.
* @param AbstractFeedHandler $feed_handler The feed handler instance.
* @param FeedGenerator $feed_generator The feed generator instance.
* @param AbstractFeedFileWriter $feed_writer The feed file writer instance.
* @param AbstractFeedHandler $feed_handler The feed handler instance.
* @param FeedGenerator $feed_generator The feed generator instance.
*/
protected function init( FeedFileWriter $feed_writer, AbstractFeedHandler $feed_handler, FeedGenerator $feed_generator ): void {
protected function init( AbstractFeedFileWriter $feed_writer, AbstractFeedHandler $feed_handler, FeedGenerator $feed_generator ): void {
$this->feed_writer = $feed_writer;
$this->feed_handler = $feed_handler;
$this->feed_generator = $feed_generator;
Expand Down Expand Up @@ -253,8 +252,14 @@ public function handle_feed_data_request(): void {
throw new PluginException( "{$name}: File at path ' . $file_path . ' is not readable.", 404 );
}

if ( $this->feed_writer instanceof JsonFeedFileWriter ) {
$content_type = 'Content-Type: application/json; charset=utf-8';
} else {
$content_type = 'Content-Type: text/csv; charset=utf-8';
}

// set the download headers.
header( 'Content-Type: text/csv; charset=utf-8' );
header( $content_type );
header( 'Content-Description: File Transfer' );
header( 'Content-Disposition: attachment; filename="' . basename( $file_path ) . '"' );
header( 'Expires: 0' );
Expand Down
307 changes: 307 additions & 0 deletions includes/Feed/AbstractFeedFileWriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
<?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 WC_Facebookcommerce_Utils;
use WooCommerce\Facebook\Framework\Plugin\Exception as PluginException;

defined( 'ABSPATH' ) || exit;

/**
* Abstract class AbstractFeedFileWriter
*
* Provides the base functionality for handling Metadata feed file writing for Facebook integration feed uploads.
*
* @package WooCommerce\Facebook\Feed
* @since 3.5.0
*/
abstract class AbstractFeedFileWriter {

/** Feed file directory inside the uploads folder @var string */
const UPLOADS_DIRECTORY = 'facebook_for_woocommerce/%s';

/**
* Use the feed name to distinguish which folder to write to.
*
* @var string
* @since 3.5.0
*/
protected string $feed_name;

/**
* Header row for the feed file.
*
* @var string
* @since 3.5.0
*/
protected string $header_row;

/**
* CSV delimiter.
*
* @var string
* @since 3.5.0
*/
protected string $delimiter;

/**
* CSV enclosure.
*
* @var string
* @since 3.5.0
*/
protected string $enclosure;

/**
* CSV escape character.
*
* @var string
* @since 3.5.0
*/
protected string $escape_char;

/**
* Constructor.
*
* @param string $feed_name The name of the feed.
* @param string $header_row The headers for the feed csv.
* @param string $delimiter Optional. The field delimiter. Default: comma.
* @param string $enclosure Optional. The field enclosure. Default: double quotes.
* @param string $escape_char Optional. The escape character. Default: backslash.
*
* @since 3.5.0
*/
public function __construct( string $feed_name, string $header_row, string $delimiter = ',', string $enclosure = '"', string $escape_char = '\\' ) {
$this->feed_name = $feed_name;
$this->header_row = $header_row;
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
$this->escape_char = $escape_char;
}

/**
* 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 $exception ) {
WC_Facebookcommerce_Utils::log_exception_immediately_to_meta(
$exception,
[
'event' => 'feed_upload',
'event_type' => 'write_feed_file',
'extra_data' => [
'feed_name' => $this->feed_name,
],
]
);
// Close the temporary file if it is still open.
if ( ! empty( $temp_feed_file ) && is_resource( $temp_feed_file ) ) {
fclose( $temp_feed_file ); // phpcs:ignore
}

// Delete the temporary file if it exists.
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 );
}
}

/**
* 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 ) {
$file_path = trailingslashit( $file['base'] ) . $file['file'];
if ( wp_mkdir_p( $file['base'] ) && ! file_exists( $file_path ) ) {
// phpcs:ignore -- use php file i/o functions
$file_handle = @fopen( $file_path, '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( static::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( static::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 );
}

if ( ! empty( $this->header_row ) ) {
$headers = str_getcsv( $this->header_row );
if ( fputcsv( $temp_feed_file, $headers, $this->delimiter, $this->enclosure, $this->escape_char ) === false ) {
// phpcs:ignore -- Escaping function for translated string not available in this context
throw new PluginException( __( "Failed to write header row to {$temp_file_path}.", 'facebook-for-woocommerce' ), 500 );
}
}

return $temp_feed_file;
}

/**
* Rename temporary feed file into the final feed file.
* This is the last step fo the feed generation procedure.
*
* @throws PluginException If the temporary feed file could not be renamed.
* @since 3.5.0
*/
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 );
}
}
}

/**
* Write to the temp feed file.
*
* @param array $data The data to write to the feed file.
* @since 3.5.0
*/
abstract public function write_temp_feed_file( array $data );
}
8 changes: 4 additions & 4 deletions includes/Feed/AbstractFeedHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ abstract class AbstractFeedHandler {
/**
* The feed file writer instance.
*
* @var FeedFileWriter
* @var AbstractFeedFileWriter
* @since 3.5.0
*/
protected FeedFileWriter $feed_writer;
protected AbstractFeedFileWriter $feed_writer;

/**
* The feed type identifier.
Expand All @@ -55,10 +55,10 @@ public function generate_feed_file(): void {
/**
* Get the feed file writer instance.
*
* @return FeedFileWriter
* @return AbstractFeedFileWriter
* @since 3.5.0
*/
public function get_feed_writer(): FeedFileWriter {
public function get_feed_writer(): AbstractFeedFileWriter {
return $this->feed_writer;
}

Expand Down
Loading