rm_event if ( ! empty( $language_stats ) ) { // Cache the stats in a transient for the hourly telemetry to use set_transient( \WooCommerce\Facebook\ExternalVersionUpdate\Update::TRANSIENT_LANGUAGE_FEED_STATS, $language_stats, \WooCommerce\Facebook\ExternalVersionUpdate\Update::TRANSIENT_LANGUAGE_FEED_STATS_LIFETIME ); } // Trigger the upload hook if any languages were successful if ( ! empty( $successful_languages ) ) { do_action( self::FEED_GEN_COMPLETE_ACTION . static::get_data_stream_name() ); } } /** * Get the Heartbeat interval to ensure that feed gen is scheduled. Must be shorter than the feed gen interval. * * @return string Heartbeat constant value */ protected static function get_feed_gen_scheduling_interval(): string { return Heartbeat::HOURLY; } /** * Override add_hooks to use the correct REQUEST_FEED_ACTION constant. * This ensures the WooCommerce API hook is registered with the proper action name. * * @since 3.6.0 */ protected function add_hooks(): void { add_action( static::get_feed_gen_scheduling_interval(), array( $this, 'schedule_feed_generation' ) ); add_action( self::GENERATE_FEED_ACTION . static::get_data_stream_name(), array( $this, 'regenerate_all_language_feeds' ) ); add_action( self::FEED_GEN_COMPLETE_ACTION . static::get_data_stream_name(), array( $this, 'upload_language_override_feeds' ) ); add_action( self::LEGACY_API_PREFIX . static::REQUEST_FEED_ACTION, array( $this, 'handle_feed_data_request', ) ); } /** * Gets the feed secret used for feed requests. * Reuses the existing Feed class's secret for consistency. * * @return string */ protected function get_feed_secret(): string { return \WooCommerce\Facebook\Products\Feed::get_feed_secret(); } /** * Checks if language override feed generation is enabled in the admin settings. * * @return bool * @since 3.6.0 */ private function is_language_override_feed_generation_enabled(): bool { $integration = facebook_for_woocommerce()->get_integration(); return $integration && $integration->is_language_override_feed_generation_enabled(); } /** * Get the data feed type for language override feeds. * * @return string */ protected static function get_feed_type(): string { return 'LANGUAGE_OVERRIDE'; } /** * Get the data stream name for language override feeds. * * @return string */ protected static function get_data_stream_name(): string { return 'language_override'; } /** * Override the feed generation interval to match product feeds frequency. * * @return int */ protected static function get_feed_gen_interval(): int { /** * Filters the frequency with which the language override feed data is generated. * * @since 3.6.0 * * @param int $interval the frequency with which the language override feed data is generated, in seconds. */ return apply_filters( 'wc_facebook_language_override_feed_generation_interval', DAY_IN_SECONDS ); } /** * Check if feed generation should be skipped. * * @return bool */ public function should_skip_feed(): bool { // Check if language override feed generation is enabled if ( ! $this->is_language_override_feed_generation_enabled() ) { return true; } $connection_handler = facebook_for_woocommerce()->get_connection_handler(); // Check connection methods $has_valid_connection = ! empty( $connection_handler->get_commerce_partner_integration_id() ) || ! empty( $connection_handler->get_commerce_merchant_settings_id() ) || ! empty( $connection_handler->get_access_token() ); if ( ! $has_valid_connection ) { return true; } // Check localization plugin if ( ! IntegrationRegistry::has_active_localization_plugin() ) { return true; } return false; } /** * Override handle_feed_data_request to add language parameter handling. * This mirrors Feed.php's handle_feed_data_request but adds language support. * * @throws PluginException If the feed secret is invalid, file is not readable, or other errors occur. */ public function handle_feed_data_request(): void { try { // Get the language code from the request $language_code = Helper::get_requested_value( 'language' ); if ( empty( $language_code ) ) { throw new PluginException( 'Language code is required', 400 ); } // Validate the feed secret if ( $this->get_feed_secret() !== Helper::get_requested_value( 'secret' ) ) { throw new PluginException( 'Invalid feed secret provided', 401 ); } // Create language-specific feed writer to get file path $language_feed_writer = new LanguageOverrideFeedWriter( $language_code ); $file_path = $language_feed_writer->get_file_path(); // Regenerate if the file doesn't exist or if explicitly requested $regenerate = Helper::get_requested_value( 'regenerate' ); if ( ! empty( $regenerate ) || ! file_exists( $file_path ) ) { $success = $language_feed_writer->write_language_feed_file( $this->language_feed_data, $language_code ); if ( ! $success ) { throw new PluginException( 'Failed to regenerate language feed file', 500 ); } } // Check if the file can be read if ( ! is_readable( $file_path ) ) { throw new PluginException( 'Language feed file 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 ) ); $file = @fopen( $file_path, 'rb' ); if ( ! $file ) { throw new PluginException( 'Could not open language feed file', 500 ); } // fpassthru might be disabled in some hosts (like Flywheel) if ( \WC_Facebookcommerce_Utils::is_fpassthru_disabled() || ! @fpassthru( $file ) ) { $contents = @stream_get_contents( $file ); if ( ! $contents ) { throw new PluginException( 'Could not get language feed file contents', 500 ); } echo $contents; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } @fclose( $file ); } catch ( \Exception $exception ) { Logger::log( 'Could not serve language override feed. ' . $exception->getMessage() . ' (' . $exception->getCode() . ')', [], array( 'should_send_log_to_meta' => false, 'should_save_log_in_woocommerce' => true, 'woocommerce_log_level' => \WC_Log_Levels::ERROR, ) ); status_header( $exception->getCode() ? $exception->getCode() : 500 ); } exit; } /** * Override get_feed_data_url to add language parameter. * This mirrors Feed.php's get_feed_data_url but adds language support. * * @param string $language_code Language code * @return string */ public function get_language_feed_url( string $language_code ): string { $query_args = array( 'wc-api' => static::REQUEST_FEED_ACTION, 'language' => $language_code, 'secret' => $this->get_feed_secret(), ); return add_query_arg( $query_args, home_url( '/' ) ); } /** * Upload language override feeds to Facebook for all available languages. * This mirrors Feed.php's send_request_to_upload_feed but handles multiple languages. * * @since 3.6.0 */ public function upload_language_override_feeds() { if ( ! IntegrationRegistry::has_active_localization_plugin() ) { return; } $languages = $this->get_language_feed_data()->get_available_languages(); foreach ( $languages as $language_code ) { $this->upload_single_language_feed( $language_code ); } } /** * Upload a single language override feed to Facebook. * This mirrors Feed.php's send_request_to_upload_feed but for a specific language. * Only uploads if the feed file exists and has actual product data. * * @param string $language_code Language code (e.g., 'es_ES', 'fr_FR') * @throws \Exception If feed creation/retrieval fails or API upload fails. * @since 3.6.0 */ private function upload_single_language_feed( string $language_code ) { try { // Check if feed file exists and has data before attempting upload $language_feed_writer = new LanguageOverrideFeedWriter( $language_code ); $file_path = $language_feed_writer->get_file_path(); // Skip upload if file doesn't exist if ( ! file_exists( $file_path ) ) { return; } // Step 1: Create or get the language override feed configuration using trait method $feed_id = $this->retrieve_or_create_language_feed_id( $language_code ); if ( empty( $feed_id ) ) { throw new \Exception( 'Could not create or retrieve language override feed ID' ); } // Step 2: Tell Facebook to fetch the CSV data from our endpoint (feed files are already generated) $data = [ 'url' => $this->get_language_feed_url( $language_code ), ]; facebook_for_woocommerce()->get_api()->create_product_feed_upload( $feed_id, $data ); // Successful upload - no log needed (matches main feed behavior) } catch ( \Exception $exception ) { Logger::log( 'Language override feed upload failed: ' . $exception->getMessage(), array( 'language_code' => $language_code, ), array( 'should_send_log_to_meta' => false, 'should_save_log_in_woocommerce' => true, 'woocommerce_log_level' => \WC_Log_Levels::ERROR, ), $exception ); } } }