Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.10% covered (warning)
88.10%
37 / 42
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Configuration
90.24% covered (success)
90.24%
37 / 41
85.71% covered (warning)
85.71%
6 / 7
14.18
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 is_woocommerce_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 configure_sync
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 get_jetpack_sync_config
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 remove_duplicate_woocommerce_analytics_module
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 expand_full_sync_config
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 add_meta_to_sync_post_meta_whitelist
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Premium Analytics glue for the shared WooCommerce Analytics sync module.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8namespace Automattic\Jetpack\PremiumAnalytics\Sync;
9
10use Automattic\Jetpack\Config;
11use Automattic\Jetpack\Sync\Data_Settings;
12use Automattic\Jetpack\Sync\Modules\Meta as Meta_Module;
13use Automattic\Jetpack\Sync\Modules\Posts as Posts_Module;
14use Automattic\Jetpack\Sync\Modules\Term_Relationships as Term_Relationships_Module;
15use Automattic\Jetpack\Sync\Modules\Terms as Terms_Module;
16use Automattic\Jetpack\Sync\Modules\WooCommerce_Analytics as WooCommerce_Analytics_Module;
17
18defined( 'ABSPATH' ) || exit;
19
20/**
21 * Opts in to the shared WooCommerce Analytics sync module and registers the
22 * Premium Analytics-specific sync configuration.
23 */
24class Configuration {
25
26    /**
27     * FQCN of the Analytics module shipped by the standalone WooCommerce Analytics plugin.
28     *
29     * Must track that plugin's class: if it drifts, both modules load under the same
30     * name and every analytics event syncs twice. Moot once that plugin consumes the
31     * shared module, since the class strings then match.
32     *
33     * @since 0.9.0
34     * @var string
35     */
36    const ANALYTICS_PLUGIN_MODULE_FQCN = 'Automattic\\WooCommerce\\Analytics\\Internal\\Jetpack\\Sync\\Modules\\Analytics';
37
38    /**
39     * Bookings post meta to add to Sync's post meta whitelist. Bookings are synced
40     * via the Posts + Meta modules; there is no dedicated bookings sync module.
41     *
42     * Product meta needed by analytics reports is whitelisted by the shared module.
43     *
44     * @static
45     * @var array
46     */
47    private static $postmeta_to_sync = array(
48        '_booking_parent_id',
49        '_booking_duplicate_of',
50        '_booking_product_id',
51        '_booking_resource_id',
52        '_booking_order_id',
53        '_booking_order_item_id',
54        '_booking_customer_id',
55        '_booking_start',
56        '_booking_end',
57        '_booking_all_day',
58        '_booking_persons',
59        '_booking_cost',
60        '_booking_date_cancelled',
61        '_booking_attendance_status',
62    );
63
64    /**
65     * Entry point called from Analytics::init(). Schedules the Sync hookups on
66     * plugins_loaded; the actual registration is a no-op unless WooCommerce is active
67     * (see {@see configure_sync()}).
68     *
69     * Call it before plugins_loaded completes: the Config built in configure_sync() wires
70     * Sync\Main::configure() from a plugins_loaded priority 2 handler that never fires later.
71     *
72     * @return void
73     */
74    public static function register(): void {
75        $instance = new self();
76
77        // plugins_loaded priority 1: every plugin has loaded for the WooCommerce guard, and the
78        // Config constructed in configure_sync() still gets its priority 2 handler in this cycle.
79        if ( did_action( 'plugins_loaded' ) ) {
80            $instance->configure_sync();
81        } else {
82            add_action( 'plugins_loaded', array( $instance, 'configure_sync' ), 1 );
83        }
84    }
85
86    /**
87     * Whether WooCommerce is active in the current request.
88     *
89     * @return bool
90     */
91    private static function is_woocommerce_active(): bool {
92        return class_exists( 'WooCommerce' ) || function_exists( 'WC' );
93    }
94
95    /**
96     * Register the Jetpack Sync filters and ensure the Sync feature when WooCommerce
97     * is active.
98     *
99     * @return void
100     */
101    public function configure_sync(): void {
102        if ( ! self::is_woocommerce_active() ) {
103            return;
104        }
105
106        // The shared module is registered through the Sync config below; this only drops a duplicate.
107        add_filter( 'jetpack_sync_modules', array( $this, 'remove_duplicate_woocommerce_analytics_module' ), PHP_INT_MAX );
108        add_filter( 'jetpack_full_sync_config', array( $this, 'expand_full_sync_config' ) );
109        add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_meta_to_sync_post_meta_whitelist' ) );
110
111        ( new Config() )->ensure( 'sync', $this->get_jetpack_sync_config() );
112    }
113
114    /**
115     * Jetpack Sync module configuration.
116     *
117     * MUST_SYNC_DATA_SETTINGS is merged in because Data_Settings falls back to the full default
118     * whitelist for any filter a consumer leaves out, which would widen standalone sites.
119     *
120     * @return array Jetpack Sync config array.
121     */
122    private function get_jetpack_sync_config(): array {
123        return array_merge_recursive(
124            Data_Settings::MUST_SYNC_DATA_SETTINGS,
125            array(
126                'jetpack_sync_modules'             => array(
127                    WooCommerce_Analytics_Module::class,
128                    Meta_Module::class,
129                    Posts_Module::class,
130                    Terms_Module::class,
131                    Term_Relationships_Module::class,
132                ),
133                // Listed explicitly so the contract does not depend on which other Sync modules load.
134                'jetpack_sync_options_whitelist'   => array(
135                    'woocommerce_custom_orders_table_enabled', // Required for HPOS checksums.
136                    'woocommerce_excluded_report_order_statuses', // Required for generating analytics reports.
137                    'woocommerce_date_type', // Date used to determine the date range for analytics reports.
138                ),
139                'jetpack_sync_constants_whitelist' => array(
140                    // Syncing it makes WPCOM provision the WC Analytics tables (WOOA7S-1643). WC_ANALYTICS_VERSION
141                    // belongs to the standalone plugin and would only sync null on a PA-only store.
142                    'JETPACK_PREMIUM_ANALYTICS__VERSION',
143                ),
144            )
145        );
146    }
147
148    /**
149     * Drop the standalone plugin's Analytics module in favor of the shared one.
150     *
151     * Sync dedups by class name only, so both would load and sync every event twice. The shared
152     * module wins because the released standalone one syncs no lookup data, while the sync package
153     * advertises the lookup checksum tables for any module of this name.
154     *
155     * Runs at PHP_INT_MAX because Data_Settings re-asserts its whole module list at priority 10.
156     *
157     * @param array|mixed $modules Current Sync module class names.
158     * @return array|mixed Updated Sync module class names.
159     */
160    public function remove_duplicate_woocommerce_analytics_module( $modules ) {
161        // An emptied list is a kill switch (Jetpack's uninstaller uses one); leave it alone.
162        if ( ! is_array( $modules ) || empty( $modules ) ) {
163            return $modules;
164        }
165
166        return array_values( array_diff( $modules, array( self::ANALYTICS_PLUGIN_MODULE_FQCN ) ) );
167    }
168
169    /**
170     * Add the Analytics module to full sync, first in line.
171     *
172     * @param array $config Current full-sync configuration.
173     * @return array Updated full-sync configuration.
174     */
175    public function expand_full_sync_config( array $config ): array {
176        // Terms and term relationships must be synced before posts.
177        if ( isset( $config['posts'] ) ) {
178            unset( $config['posts'] );
179            $config += array( 'posts' => 1 );
180        }
181
182        if ( ! isset( $config['woocommerce_analytics'] ) ) {
183            $config = array( 'woocommerce_analytics' => 1 ) + $config;
184        }
185
186        return $config;
187    }
188
189    /**
190     * Add Bookings post meta to Sync's post meta whitelist.
191     * Any changes to these meta will be synced to WordPress.com.
192     *
193     * @param array $whitelist Existing post meta whitelist.
194     * @return array Updated post meta whitelist.
195     */
196    public function add_meta_to_sync_post_meta_whitelist( array $whitelist ): array {
197        return array_merge( self::$postmeta_to_sync, $whitelist );
198    }
199}