Files
2026-04-28 15:13:50 +02:00

260 lines
8.9 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\ExternalVersionUpdate;
defined( 'ABSPATH' ) || exit;
use Exception;
use WC_Facebookcommerce_Utils;
use WooCommerce\Facebook\Utilities\Heartbeat;
use WooCommerce\Facebook\Framework\Logger;
use WooCommerce\Facebook\Framework\LogHandlerBase;
use WooCommerce\Facebook\Handlers\PluginRender;
use WooCommerce\Facebook\Integrations\IntegrationRegistry;
/**
* Meta for WooCommerce External Plugin Version Update.
*
* Whenever this plugin gets updated, we need to inform the Meta server of the new version.
* This is done by sending a request to the Meta server with the new version number.
*
* @since 3.0.10
*/
class Update {
/** @var string Name of the option that stores the latest version that was sent to the Meta server. */
const LATEST_VERSION_SENT = 'facebook_for_woocommerce_latest_version_sent_to_server';
/** @var string master sync option */
const MASTER_SYNC_OPT_OUT_TIME = 'wc_facebook_master_sync_opt_out_time';
/** @var string Transient key for caching language feed statistics */
const TRANSIENT_LANGUAGE_FEED_STATS = 'facebook_for_woocommerce_language_feed_stats';
/** @var int Transient lifetime for language feed stats cache (2 weeks) */
const TRANSIENT_LANGUAGE_FEED_STATS_LIFETIME = 2 * WEEK_IN_SECONDS;
/** @var string Option name for caching the collection page compatibility check */
const COLLECTIONPAGE_COMPAT_OPTION = 'wc_facebook_collectionpage_compat';
/**
* Update class constructor.
*
* @since 3.0.10
*/
public function __construct() {
add_action( Heartbeat::DAILY, array( $this, 'send_new_version_to_facebook_server' ) );
add_action( Heartbeat::HOURLY, array( $this, 'send_plugin_config_to_facebook_server' ) );
}
/**
* Sends the plugin configs to the Meta server.
*
* @since 3.5.3
*/
public function send_plugin_config_to_facebook_server() {
$flag_name = '_wc_facebook_for_woocommerce_send_plugin_config_flag';
if ( 'yes' === get_transient( $flag_name ) ) {
return;
}
set_transient( $flag_name, 'yes', 3 * HOUR_IN_SECONDS );
try {
$excluded_product_categories = (array) apply_filters( 'wc_facebook_excluded_product_category_ids', get_option( 'wc_facebook_excluded_product_category_ids', [] ), $this );
if ( ! empty( $excluded_product_categories ) ) {
$term_query = new \WP_Term_Query(
array(
'taxonomy' => 'product_cat',
'include' => $excluded_product_categories,
'hide_empty' => true,
'fields' => 'id=>name',
)
);
$excluded_product_categories = $term_query->get_terms();
}
$excluded_product_tags = (array) apply_filters( 'wc_facebook_excluded_product_tag_ids', get_option( 'wc_facebook_excluded_product_tag_ids', [] ), $this );
if ( ! empty( $excluded_product_tags ) ) {
$term_query = new \WP_Term_Query(
array(
'taxonomy' => 'product_tag',
'include' => $excluded_product_tags,
'hide_empty' => true,
'hierarchical' => false,
'fields' => 'id=>name',
)
);
$excluded_product_tags = $term_query->get_terms();
}
$language_feed_stats = get_transient( self::TRANSIENT_LANGUAGE_FEED_STATS );
$context = array(
'flow_name' => 'plugin_updates',
'flow_step' => 'send_plugin_updates',
'extra_data' => [
'is_multisite' => is_multisite(),
'is_product_sync_enabled' => facebook_for_woocommerce()->get_integration()->is_product_sync_enabled(),
'excluded_product_categories' => wp_json_encode( $excluded_product_categories ),
'excluded_product_tags' => wp_json_encode( $excluded_product_tags ),
'published_product_count' => facebook_for_woocommerce()->get_integration()->get_product_count(),
'opted_out_woo_all_products' => get_option( self::MASTER_SYNC_OPT_OUT_TIME ),
'active_plugins' => wp_json_encode( IntegrationRegistry::get_all_active_plugin_data() ),
'language_override_enabled' => get_option( \WC_Facebookcommerce_Integration::OPTION_LANGUAGE_OVERRIDE_FEED_GENERATION_ENABLED, 'no' ),
'language_feed_stats' => wp_json_encode( is_array( $language_feed_stats ) ? $language_feed_stats : [] ),
'is_collectionpage_reliably_available' => $this->is_collectionpage_reliably_available(),
],
);
$context = [ LogHandlerBase::set_core_log_context( $context ) ];
$context = [
'event' => 'persist_meta_logs',
'extra_data' => [ 'meta_logs' => wp_json_encode( $context ) ],
];
$response = facebook_for_woocommerce()->get_api()->log_to_meta( $context );
if ( ! $response->success ) {
Logger::log(
'Bad response from log_to_meta request',
[],
array(
'should_send_log_to_meta' => false,
'should_save_log_in_woocommerce' => true,
'woocommerce_log_level' => \WC_Log_Levels::ERROR,
)
);
}
} catch ( \Exception $e ) {
Logger::log(
'Error persisting error logs: ' . $e->getMessage(),
[],
array(
'should_send_log_to_meta' => false,
'should_save_log_in_woocommerce' => true,
'woocommerce_log_level' => \WC_Log_Levels::ERROR,
)
);
}
}
/**
* Sends the latest plugin version to the Meta server.
*
* @since 3.0.10
* @return bool
*/
public function send_new_version_to_facebook_server() {
$plugin = facebook_for_woocommerce();
if ( ! $plugin->get_connection_handler()->is_connected() ) {
// If the plugin is not connected, we don't need to send the version to the Meta server.
return;
}
$flag_name = '_wc_facebook_for_woocommerce_external_version_update_flag';
if ( 'yes' === get_transient( $flag_name ) ) {
return;
}
set_transient( $flag_name, 'yes', 12 * HOUR_IN_SECONDS );
// Send the request to the Meta server with the latest plugin version.
try {
$external_business_id = $plugin->get_connection_handler()->get_external_business_id();
$is_woo_all_product_opted_out = PluginRender::is_master_sync_on() === false;
$response = $plugin->get_api()->update_plugin_version_configuration( $external_business_id, $is_woo_all_product_opted_out, WC_Facebookcommerce_Utils::PLUGIN_VERSION );
if ( $response->has_api_error() ) {
// If the request fails, we should retry it in the next heartbeat.
return false;
}
return update_option( self::LATEST_VERSION_SENT, WC_Facebookcommerce_Utils::PLUGIN_VERSION );
} catch ( Exception $e ) {
Logger::log(
$e->getMessage(),
[],
array(
'should_send_log_to_meta' => false,
'should_save_log_in_woocommerce' => true,
'woocommerce_log_level' => \WC_Log_Levels::ERROR,
)
);
// If the request fails, we should retry it in the next heartbeat.
return false;
}
}
/**
* Returns whether the /fbcollection/ page will reliably render on this site.
* Result is computed once per plugin version and cached in a WP option.
*
* @return bool
*/
private function is_collectionpage_reliably_available(): bool {
$current_version = defined( '\WooCommerce\Facebook\PLUGIN_VERSION' )
? \WooCommerce\Facebook\PLUGIN_VERSION
: facebook_for_woocommerce()->get_version();
$cached = get_option( self::COLLECTIONPAGE_COMPAT_OPTION, [] );
if ( is_array( $cached ) && ( $cached['version'] ?? '' ) === $current_version && isset( $cached['result'] ) ) {
return (bool) $cached['result'];
}
$result = $this->check_collectionpage_compatibility();
update_option(
self::COLLECTIONPAGE_COMPAT_OPTION,
[
'version' => $current_version,
'result' => $result,
],
false
);
return $result;
}
/**
* Runs the actual compatibility checks for the collection page.
*
* @return bool True if the collection page is expected to work reliably.
*/
private function check_collectionpage_compatibility(): bool {
// Plain permalinks — rewrite rules won't work at all.
if ( '' === get_option( 'permalink_structure' ) ) {
return false;
}
// Block/FSE themes bypass the PHP archive template and woocommerce_product_query hook.
if ( wp_is_block_theme() ) {
return false;
}
// Headless — no server-side frontend rendering.
if ( class_exists( 'WPGraphQL' ) || defined( 'FAUSTWP_FILE' ) ) {
return false;
}
// Elementor Pro has a custom template for the product archive.
if ( class_exists( '\ElementorPro\Modules\ThemeBuilder\Module' ) ) {
$conditions = get_option( 'elementor_pro_theme_builder_conditions', [] );
foreach ( $conditions as $template_conditions ) {
foreach ( (array) $template_conditions as $condition ) {
if ( is_string( $condition )
&& str_contains( $condition, 'product' )
&& str_contains( $condition, 'archive' ) ) {
return false;
}
}
}
}
return true;
}
}