403 ] );
}
$file_path = $this->get_file_path();
// If file doesn't exist yet, generate it now
if ( ! file_exists( $file_path ) ) {
$this->generate_xml_file();
}
// Nothing could be written (for example a read-only uploads folder).
if ( ! file_exists( $file_path ) ) {
wp_die( 'Feed unavailable', 'Feed', [ 'response' => 500 ] );
}
// Serve the static file efficiently
header( 'Content-Type: application/xml; charset=utf-8' );
header( 'Content-Length: ' . filesize( $file_path ) );
// Serving our own generated XML file straight to the client.
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
readfile( $file_path );
exit;
}
public function generate_xml_file() {
$file_path = $this->get_file_path( true );
if ( ! $file_path ) return false;
// Open file for writing (prevents memory exhaustion)
// The feed is streamed to a file in the uploads folder one product at a time to keep memory low on
// large catalogues, which WP_Filesystem cannot do, so direct file functions are intentional here.
// phpcs:disable WordPress.WP.AlternativeFunctions
$file = fopen( $file_path, 'w' );
if ( ! $file ) {
return false;
}
fwrite( $file, '' . PHP_EOL );
fwrite( $file, '' . PHP_EOL );
fwrite( $file, '' . PHP_EOL );
fwrite( $file, '' . esc_xml( get_bloginfo( 'name' ) ) . ' Product Feed' . PHP_EOL );
fwrite( $file, '' . esc_url( home_url('/') ) . '' . PHP_EOL );
fwrite( $file, 'TikTok Product Catalog Feed' . PHP_EOL );
// Process products in chunks of 100 to save RAM
$paged = 1;
while ( true ) {
$products = wc_get_products( [
'status' => 'publish',
'limit' => 100,
'paginate' => true,
'page' => $paged
] );
if ( empty( $products->products ) ) break;
foreach ( $products->products as $product ) {
$xml_item = $this->build_item_xml( $product );
fwrite( $file, $xml_item );
}
$paged++;
// Free memory
if ( function_exists( 'gc_collect_cycles' ) ) gc_collect_cycles();
}
fwrite( $file, '' . PHP_EOL );
fwrite( $file, '' );
fclose( $file );
// phpcs:enable WordPress.WP.AlternativeFunctions
// Remove the file that older private versions saved straight into the uploads root.
$old_file = trailingslashit( wp_upload_dir()['basedir'] ) . 'tiktok-feed.xml';
if ( file_exists( $old_file ) ) {
wp_delete_file( $old_file );
}
update_option( 'pxsync_feed_last_updated', current_time( 'mysql' ) );
return true;
}
private function build_item_xml( $product ) {
$description = wp_strip_all_tags( $product->get_short_description() ?: $product->get_description() );
if ( empty( $description ) ) {
$description = $product->get_name() . ' - Get the best deal at ' . get_bloginfo( 'name' );
}
$category = 'Uncategorized';
$terms = get_the_terms( $product->get_id(), 'product_cat' );
if ( $terms && ! is_wp_error( $terms ) ) {
$category = $terms[0]->name;
}
$xml = '- ' . PHP_EOL;
$xml .= '' . esc_xml( $product->get_sku() ?: $product->get_id() ) . '' . PHP_EOL;
$xml .= '' . esc_xml( $product->get_name() ) . '' . PHP_EOL;
$xml .= '' . esc_xml( $description ) . '' . PHP_EOL;
$xml .= '' . esc_url( $product->get_permalink() ) . '' . PHP_EOL;
$xml .= '' . esc_url( wp_get_attachment_url( $product->get_image_id() ) ) . '' . PHP_EOL;
$xml .= '' . esc_xml( $product->get_price() . ' ' . get_woocommerce_currency() ) . '' . PHP_EOL;
$xml .= 'new' . PHP_EOL;
$xml .= '' . ( $product->is_in_stock() ? 'in_stock' : 'out_of_stock' ) . '' . PHP_EOL;
$xml .= '' . esc_xml( get_bloginfo( 'name' ) ) . '' . PHP_EOL;
$xml .= '' . esc_xml( $category ) . '' . PHP_EOL;
$xml .= '
' . PHP_EOL;
return $xml;
}
/**
* Folder for the feed file: wp-content/uploads/pixelsync/ (named after the plugin slug).
* The folder is closed to direct web access; the feed is only served through the secret-key URL.
*/
public static function get_feed_dir() {
$upload_dir = wp_upload_dir();
return trailingslashit( $upload_dir['basedir'] ) . 'pixelsync';
}
private function get_file_path( $create_dir = false ) {
$dir = self::get_feed_dir();
if ( $create_dir ) {
if ( ! wp_mkdir_p( $dir ) ) return '';
// Keep the folder from being listed or read directly.
// phpcs:disable WordPress.WP.AlternativeFunctions
if ( ! file_exists( $dir . '/index.php' ) ) {
file_put_contents( $dir . '/index.php', "\nRequire all denied\n\n\nDeny from all\n\n" );
}
// phpcs:enable WordPress.WP.AlternativeFunctions
}
// The file name contains a value derived from the private feed key, so it cannot be guessed.
$suffix = substr( hash( 'sha256', (string) self::get_secret_key() ), 0, 16 );
return $dir . '/feed-' . $suffix . '.xml';
}
public static function get_secret_key() {
$key = get_option( 'pxsync_feed_secret' );
if ( ! $key ) {
$key = bin2hex( random_bytes( 16 ) );
update_option( 'pxsync_feed_secret', $key );
}
return $key;
}
}