408 lines
11 KiB
PHP
408 lines
11 KiB
PHP
<?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 MetaCommerce
|
|
*/
|
|
|
|
namespace WooCommerce\Facebook\Feed;
|
|
|
|
use WC_Facebookcommerce_Utils;
|
|
use WooCommerce\Facebook\Framework\Plugin\Exception as PluginException;
|
|
use WooCommerce\Facebook\Framework\Logger;
|
|
|
|
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 ( \Exception $exception ) {
|
|
Logger::log(
|
|
'Error while writing feed file.',
|
|
array(
|
|
'event' => 'feed_upload',
|
|
'event_type' => 'write_feed_file',
|
|
'extra_data' => [
|
|
'feed_name' => $this->feed_name,
|
|
],
|
|
),
|
|
array(
|
|
'should_send_log_to_meta' => true,
|
|
'should_save_log_in_woocommerce' => false,
|
|
'woocommerce_log_level' => \WC_Log_Levels::DEBUG,
|
|
),
|
|
$exception,
|
|
);
|
|
// 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.
|
|
* @throws \Exception Caught exception is rethrown.
|
|
* @since 3.5.0
|
|
*/
|
|
public function create_feed_directory(): void {
|
|
$file_directory = $this->get_file_directory();
|
|
try {
|
|
$directory_created = wp_mkdir_p( $file_directory );
|
|
if ( ! $directory_created ) {
|
|
throw new PluginException( "Could not create feed directory at {$file_directory}", 500 );
|
|
}
|
|
} catch ( \Exception $exception ) {
|
|
Logger::log(
|
|
'Error while creating feed directory.',
|
|
array(
|
|
'event' => 'feed_upload',
|
|
'event_type' => 'create_feed_directory',
|
|
'extra_data' => [
|
|
'feed_name' => $this->feed_name,
|
|
'file_directory' => $file_directory,
|
|
],
|
|
),
|
|
array(
|
|
'should_send_log_to_meta' => true,
|
|
'should_save_log_in_woocommerce' => false,
|
|
'woocommerce_log_level' => \WC_Log_Levels::DEBUG,
|
|
),
|
|
$exception,
|
|
);
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates files in the feed directory to prevent directory listing and hotlinking.
|
|
*
|
|
* @throws \Exception Caught exception is rethrown.
|
|
* @since 3.5.0
|
|
*/
|
|
public function create_files_to_protect_feed_directory(): void {
|
|
$feed_directory = trailingslashit( $this->get_file_directory() );
|
|
try {
|
|
$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 ) {
|
|
try {
|
|
fwrite( $file_handle, $file['content'] ); //phpcs:ignore
|
|
} finally {
|
|
fclose( $file_handle ); //phpcs:ignore
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch ( \Exception $exception ) {
|
|
Logger::log(
|
|
'Error while creating files to protect feed directory.',
|
|
array(
|
|
'event' => 'feed_upload',
|
|
'event_type' => 'create_files_to_protect_feed_directory',
|
|
'extra_data' => [
|
|
'feed_name' => $this->feed_name,
|
|
'feed_directory' => $feed_directory,
|
|
],
|
|
),
|
|
array(
|
|
'should_send_log_to_meta' => true,
|
|
'should_save_log_in_woocommerce' => false,
|
|
'woocommerce_log_level' => \WC_Log_Levels::DEBUG,
|
|
),
|
|
$exception,
|
|
);
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @throws \Exception Caught exception is rethrown.
|
|
* @return resource A file pointer resource.
|
|
* @since 3.5.0
|
|
*/
|
|
public function prepare_temporary_feed_file() {
|
|
$temp_file_path = $this->get_temp_file_path();
|
|
$temp_feed_file = false;
|
|
$file_path = $this->get_file_path();
|
|
|
|
try {
|
|
// 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 ) ) {
|
|
throw new PluginException( "Could not open file {$temp_file_path} for writing.", 500 );
|
|
}
|
|
|
|
// 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 ) ) {
|
|
throw new PluginException( "Could not open file {$file_path} for writing.", 500 );
|
|
}
|
|
|
|
if ( ! empty( $this->header_row ) ) {
|
|
$headers = str_getcsv( $this->header_row, $this->delimiter, $this->enclosure, $this->escape_char );
|
|
if ( fputcsv( $temp_feed_file, $headers, $this->delimiter, $this->enclosure, $this->escape_char ) === false ) {
|
|
throw new PluginException( "Failed to write header row to {$temp_file_path}.", 500 );
|
|
}
|
|
}
|
|
|
|
return $temp_feed_file;
|
|
} catch ( \Exception $exception ) {
|
|
Logger::log(
|
|
'Error while creating temporary feed file.',
|
|
array(
|
|
'event' => 'feed_upload',
|
|
'event_type' => 'prepare_temporary_feed_file',
|
|
'extra_data' => [
|
|
'feed_name' => $this->feed_name,
|
|
'temp_file_path' => $temp_file_path,
|
|
'file_path' => $file_path,
|
|
],
|
|
),
|
|
array(
|
|
'should_send_log_to_meta' => true,
|
|
'should_save_log_in_woocommerce' => false,
|
|
'woocommerce_log_level' => \WC_Log_Levels::DEBUG,
|
|
),
|
|
$exception,
|
|
);
|
|
if ( $temp_feed_file ) {
|
|
// phpcs:ignore -- use php file i/o functions
|
|
fclose( $temp_feed_file );
|
|
}
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @throws \Exception Caught exception is rethrown.
|
|
* @since 3.5.0
|
|
*/
|
|
public function promote_temp_file(): void {
|
|
$temp_file_path = $this->get_temp_file_path();
|
|
$file_path = $this->get_file_path();
|
|
|
|
try {
|
|
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 ) ) {
|
|
throw new PluginException( "Could not promote temp file: {$temp_file_path}", 500 );
|
|
}
|
|
}
|
|
} catch ( \Exception $exception ) {
|
|
Logger::log(
|
|
'Error while promoting temporary file.',
|
|
array(
|
|
'event' => 'feed_upload',
|
|
'event_type' => 'promote_temp_file',
|
|
'extra_data' => [
|
|
'feed_name' => $this->feed_name,
|
|
'temp_file_path' => $temp_file_path,
|
|
'file_path' => $file_path,
|
|
],
|
|
),
|
|
array(
|
|
'should_send_log_to_meta' => true,
|
|
'should_save_log_in_woocommerce' => false,
|
|
'woocommerce_log_level' => \WC_Log_Levels::DEBUG,
|
|
),
|
|
$exception,
|
|
);
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 );
|
|
}
|