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