Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.52% covered (warning)
84.52%
71 / 84
86.67% covered (warning)
86.67%
13 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Data_Settings
84.52% covered (warning)
84.52%
71 / 84
86.67% covered (warning)
86.67%
13 / 15
56.54
0.00% covered (danger)
0.00%
0 / 1
 add_settings_list
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 set_all_defaults
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_default_setting_for_filter
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 add_filters_custom_settings_and_hooks
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 add_filters_for_enabled_module
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 add_filters_for_disabled_module
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 add_custom_filter_setting
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 is_valid_filter_setting
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
7.05
 add_required_settings
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 add_sync_filter_setting
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 add_associative_filter_setting
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 add_indexed_filter_setting
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 sync_data_filter_hook
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 empty_data_settings_and_hooks
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_data_settings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * The Data Settings class.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync;
9
10/**
11 * The Data_Settings class
12 */
13class Data_Settings {
14
15    /**
16     * The data that must be synced for every synced site.
17     */
18    const MUST_SYNC_DATA_SETTINGS = array(
19        'jetpack_sync_modules'             => array(
20            'Automattic\\Jetpack\\Sync\\Modules\\Callables',
21            'Automattic\\Jetpack\\Sync\\Modules\\Constants',
22            'Automattic\\Jetpack\\Sync\\Modules\\Full_Sync_Immediately', // enable Initial Sync on Site Connection.
23            'Automattic\\Jetpack\\Sync\\Modules\\Options',
24            'Automattic\\Jetpack\\Sync\\Modules\\Updates',
25            'Automattic\\Jetpack\\Sync\\Modules\\Stats', // Daily heartbeat data.
26        ),
27        'jetpack_sync_callable_whitelist'  => array(
28            'site_url'                          => array( 'Automattic\\Jetpack\\Connection\\Urls', 'site_url' ),
29            'home_url'                          => array( 'Automattic\\Jetpack\\Connection\\Urls', 'home_url' ),
30            'get_plugins'                       => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_plugins' ),
31            'get_themes'                        => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_themes' ),
32            'jetpack_connection_active_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_connection_active_plugins' ),
33            'jetpack_package_versions'          => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_package_versions' ),
34            'paused_plugins'                    => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_paused_plugins' ),
35            'paused_themes'                     => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_paused_themes' ),
36            'timezone'                          => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_timezone' ),
37            'wp_get_environment_type'           => 'wp_get_environment_type',
38            'wp_max_upload_size'                => 'wp_max_upload_size',
39            'wp_version'                        => array( 'Automattic\\Jetpack\\Sync\\Functions', 'wp_version' ),
40            'jetpack_sync_active_modules'       => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_sync_active_modules' ),
41        ),
42        'jetpack_sync_constants_whitelist' => array(
43            'ABSPATH',
44            'ALTERNATE_WP_CRON',
45            'ATOMIC_CLIENT_ID',
46            'AUTOMATIC_UPDATER_DISABLED',
47            'DISABLE_WP_CRON',
48            'DISALLOW_FILE_EDIT',
49            'DISALLOW_FILE_MODS',
50            'EMPTY_TRASH_DAYS',
51            'FS_METHOD',
52            'IS_PRESSABLE',
53            'PHP_VERSION',
54            'WP_ACCESSIBLE_HOSTS',
55            'WP_AUTO_UPDATE_CORE',
56            'WP_CONTENT_DIR',
57            'WP_CRON_LOCK_TIMEOUT',
58            'WP_DEBUG',
59            'WP_HTTP_BLOCK_EXTERNAL',
60            'WP_MAX_MEMORY_LIMIT',
61            'WP_MEMORY_LIMIT',
62            'WP_POST_REVISIONS',
63        ),
64        'jetpack_sync_options_whitelist'   => array(
65            /**
66             * Sync related options
67             */
68            'jetpack_sync_non_blocking',
69            'jetpack_sync_non_public_post_stati',
70            'jetpack_sync_settings_comment_meta_whitelist',
71            'jetpack_sync_settings_post_meta_whitelist',
72            'jetpack_sync_settings_post_types_blacklist',
73            'jetpack_sync_settings_taxonomies_blacklist',
74            'jetpack_sync_settings_dedicated_sync_enabled',
75            'jetpack_sync_settings_custom_queue_table_enabled',
76            'jetpack_sync_settings_wpcom_rest_api_enabled',
77            /**
78             * Generic site options
79             */
80            'blog_charset',
81            'blog_public',
82            'blogdescription',
83            'blogname',
84            'permalink_structure',
85            'stylesheet',
86            'time_format',
87            'timezone_string',
88            'active_plugins',
89        ),
90    );
91
92    const MODULE_FILTER_MAPPING = array(
93        'Automattic\\Jetpack\\Sync\\Modules\\Options'   => array(
94            'jetpack_sync_options_whitelist',
95            'jetpack_sync_options_contentless',
96        ),
97        'Automattic\\Jetpack\\Sync\\Modules\\Constants' => array(
98            'jetpack_sync_constants_whitelist',
99        ),
100        'Automattic\\Jetpack\\Sync\\Modules\\Callables' => array(
101            'jetpack_sync_callable_whitelist',
102            'jetpack_sync_multisite_callable_whitelist',
103        ),
104        'Automattic\\Jetpack\\Sync\\Modules\\Posts'     => array(
105            'jetpack_sync_post_meta_whitelist',
106        ),
107        'Automattic\\Jetpack\\Sync\\Modules\\Comments'  => array(
108            'jetpack_sync_comment_meta_whitelist',
109        ),
110        'Automattic\\Jetpack\\Sync\\Modules\\Users'     => array(
111            'jetpack_sync_capabilities_whitelist',
112        ),
113        'Automattic\\Jetpack\\Sync\\Modules\\Import'    => array(
114            'jetpack_sync_known_importers',
115        ),
116    );
117
118    const MODULES_FILTER_NAME = 'jetpack_sync_modules';
119
120    /**
121     * The static data settings array which contains the aggregated data settings for
122     * each sync filter.
123     *
124     * @var array
125     */
126    private static $data_settings = array();
127
128    /**
129     * The static array which contains the list of filter hooks that have already been set up.
130     *
131     * @var array
132     */
133    private static $set_filter_hooks = array();
134
135    /**
136     * Adds the data settings provided by a plugin to the Sync data settings.
137     *
138     * @param array $plugin_settings The array provided by the plugin. The array must use filters
139     *                               from the DATA_FILTER_DEFAULTS list as keys.
140     */
141    public function add_settings_list( $plugin_settings = array() ) {
142        if ( empty( $plugin_settings ) || ! is_array( $plugin_settings ) ) {
143            /*
144             * No custom plugin settings, so use defaults for everything and bail early.
145             */
146            $this->set_all_defaults();
147            return;
148        }
149
150        $this->add_filters_custom_settings_and_hooks( $plugin_settings );
151
152        if ( ! did_action( 'jetpack_sync_add_required_data_settings' ) ) {
153            $this->add_required_settings();
154            /**
155             * Fires when the required settings have been adding to the static
156             * data_settings array.
157             *
158             * @since 1.29.2
159             *
160             * @module sync
161             */
162            do_action( 'jetpack_sync_add_required_data_settings' );
163        }
164    }
165
166    /**
167     * Sets the default values for sync modules and all sync data filters.
168     */
169    private function set_all_defaults() {
170        $this->add_sync_filter_setting( self::MODULES_FILTER_NAME, Modules::DEFAULT_SYNC_MODULES );
171
172        foreach ( array_keys( Default_Filter_Settings::DATA_FILTER_DEFAULTS ) as $filter ) {
173            $this->add_sync_filter_setting( $filter, $this->get_default_setting_for_filter( $filter ) );
174        }
175    }
176
177    /**
178     * Returns the default settings for the given filter.
179     *
180     * @param string $filter The filter name.
181     *
182     * @return array The filter's default settings array.
183     */
184    private function get_default_setting_for_filter( $filter ) {
185        if ( self::MODULES_FILTER_NAME === $filter ) {
186            return Modules::DEFAULT_SYNC_MODULES;
187        }
188
189        return ( new Default_Filter_Settings() )->get_default_settings( $filter );
190    }
191
192    /**
193     * Adds the custom settings and sets up the necessary filter hooks.
194     *
195     * @param array $filters_settings The custom settings.
196     */
197    private function add_filters_custom_settings_and_hooks( $filters_settings ) {
198        if ( isset( $filters_settings[ self::MODULES_FILTER_NAME ] ) && is_array( $filters_settings[ self::MODULES_FILTER_NAME ] ) ) {
199            $this->add_custom_filter_setting( self::MODULES_FILTER_NAME, $filters_settings[ self::MODULES_FILTER_NAME ] );
200            $enabled_modules = $filters_settings[ self::MODULES_FILTER_NAME ];
201        } else {
202            $this->add_sync_filter_setting( self::MODULES_FILTER_NAME, Modules::DEFAULT_SYNC_MODULES );
203            $enabled_modules = Modules::DEFAULT_SYNC_MODULES;
204        }
205
206        $all_modules = Modules::DEFAULT_SYNC_MODULES;
207
208        foreach ( $all_modules as $module ) {
209            if ( in_array( $module, $enabled_modules, true ) || in_array( $module, self::MUST_SYNC_DATA_SETTINGS['jetpack_sync_modules'], true ) ) {
210                $this->add_filters_for_enabled_module( $module, $filters_settings );
211            } else {
212                $this->add_filters_for_disabled_module( $module );
213            }
214        }
215    }
216
217    /**
218     * Adds the filters for the provided enabled module. If the settings provided custom filter settings
219     * for the module's filters, those are used. Otherwise, the filter's default settings are used.
220     *
221     * @param string $module The module name.
222     * @param array  $filters_settings The settings for the filters.
223     */
224    private function add_filters_for_enabled_module( $module, $filters_settings ) {
225        $module_mapping     = self::MODULE_FILTER_MAPPING;
226        $filters_for_module = isset( $module_mapping[ $module ] ) ? $module_mapping[ $module ] : array();
227
228        foreach ( $filters_for_module as $filter ) {
229            if ( isset( $filters_settings[ $filter ] ) ) {
230                $this->add_custom_filter_setting( $filter, $filters_settings[ $filter ] );
231            } else {
232                $this->add_sync_filter_setting( $filter, $this->get_default_setting_for_filter( $filter ) );
233            }
234        }
235    }
236
237    /**
238     * Adds the filters for the provided disabled module. The disabled module's associated filter settings are
239     * set to an empty array.
240     *
241     * @param string $module The module name.
242     */
243    private function add_filters_for_disabled_module( $module ) {
244        $module_mapping     = self::MODULE_FILTER_MAPPING;
245        $filters_for_module = isset( $module_mapping[ $module ] ) ? $module_mapping[ $module ] : array();
246
247        foreach ( $filters_for_module as $filter ) {
248            $this->add_custom_filter_setting( $filter, array() );
249        }
250    }
251
252    /**
253     * Adds the provided custom setting for a filter. If the filter setting isn't valid, the default
254     * value is used.
255     *
256     * If the filter's hook hasn't already been set up, it gets set up.
257     *
258     * @param string $filter The filter.
259     * @param array  $setting The filter setting.
260     */
261    private function add_custom_filter_setting( $filter, $setting ) {
262        if ( ! $this->is_valid_filter_setting( $filter, $setting ) ) {
263            /*
264             * The provided setting isn't valid, so use the default for this filter.
265             * We're using the default values so there's no need to set the filter hook.
266             */
267            $this->add_sync_filter_setting( $filter, $this->get_default_setting_for_filter( $filter ) );
268            return;
269        }
270
271        if ( ! isset( static::$set_filter_hooks[ $filter ] ) ) {
272            // First time a custom modules setting is provided, so set the filter hook.
273            add_filter( $filter, array( $this, 'sync_data_filter_hook' ) );
274            static::$set_filter_hooks[ $filter ] = 1;
275        }
276
277        $this->add_sync_filter_setting( $filter, $setting );
278    }
279
280    /**
281     * Determines whether the filter setting is valid. The setting array is in the correct format (associative or indexed).
282     *
283     * @param string $filter The filter to check.
284     * @param array  $filter_settings The filter settings.
285     *
286     * @return bool Whether the filter settings can be used.
287     */
288    private function is_valid_filter_setting( $filter, $filter_settings ) {
289        if ( ! is_array( $filter_settings ) ) {
290            // The settings for each filter must be an array.
291            return false;
292        }
293
294        if ( empty( $filter_settings ) ) {
295            // Empty settings are allowed.
296            return true;
297        }
298
299        $indexed_array = isset( $filter_settings[0] );
300        if ( in_array( $filter, Default_Filter_Settings::ASSOCIATIVE_FILTERS, true ) && ! $indexed_array ) {
301                return true;
302        } elseif ( ! in_array( $filter, Default_Filter_Settings::ASSOCIATIVE_FILTERS, true ) && $indexed_array ) {
303            return true;
304        }
305
306        return false;
307    }
308
309    /**
310     * Adds the data settings that are always required for every plugin that uses Sync.
311     */
312    private function add_required_settings() {
313        foreach ( self::MUST_SYNC_DATA_SETTINGS as $filter => $setting ) {
314            // If the corresponding setting is already set and matches the default one, no need to proceed.
315            if ( isset( static::$data_settings[ $filter ] ) && static::$data_settings[ $filter ] === $this->get_default_setting_for_filter( $filter ) ) {
316                continue;
317            }
318            $this->add_custom_filter_setting( $filter, $setting );
319        }
320    }
321
322    /**
323     * Adds the provided data setting for the provided filter.
324     *
325     * @param string $filter The filter name.
326     * @param array  $value The data setting.
327     */
328    private function add_sync_filter_setting( $filter, $value ) {
329        if ( ! isset( static::$data_settings[ $filter ] ) ) {
330            static::$data_settings[ $filter ] = $value;
331            return;
332        }
333
334        if ( in_array( $filter, Default_Filter_Settings::ASSOCIATIVE_FILTERS, true ) ) {
335            $this->add_associative_filter_setting( $filter, $value );
336        } else {
337            $this->add_indexed_filter_setting( $filter, $value );
338        }
339    }
340
341    /**
342     * Adds the provided data setting for the provided filter. This method handles
343     * adding settings to data that is stored as an associative array.
344     *
345     * @param string $filter  The filter name.
346     * @param array  $settings The data settings.
347     */
348    private function add_associative_filter_setting( $filter, $settings ) {
349        foreach ( $settings as $key => $item ) {
350            if ( ! array_key_exists( $key, static::$data_settings[ $filter ] ) ) {
351                static::$data_settings[ $filter ][ $key ] = $item;
352            }
353        }
354    }
355
356    /**
357     * Adds the provided data setting for the provided filter. This method handles
358     * adding settings to data that is stored as an indexed array.
359     *
360     * @param string $filter  The filter name.
361     * @param array  $settings The data settings.
362     */
363    private function add_indexed_filter_setting( $filter, $settings ) {
364        static::$data_settings[ $filter ] = array_unique(
365            array_merge(
366                static::$data_settings[ $filter ],
367                $settings
368            )
369        );
370    }
371
372    /**
373     * The callback function added to the sync data filters. Combines the list in the $data_settings property
374     * with any non-default values from the received array.
375     *
376     * @param array $filtered_values The data revieved from the filter.
377     *
378     * @return array The data settings for the filter.
379     */
380    public function sync_data_filter_hook( $filtered_values ) {
381        if ( ! is_array( $filtered_values ) ) {
382            // Something is wrong with the input, so set it to an empty array.
383            $filtered_values = array();
384        }
385
386        $current_filter = current_filter();
387
388        if ( ! isset( static::$data_settings[ $current_filter ] ) ) {
389            return $filtered_values;
390        }
391
392        if ( in_array( $current_filter, Default_Filter_Settings::ASSOCIATIVE_FILTERS, true ) ) {
393            $extra_filters = array_diff_key( $filtered_values, $this->get_default_setting_for_filter( $current_filter ) );
394            $this->add_associative_filter_setting( $current_filter, $extra_filters );
395            return static::$data_settings[ $current_filter ];
396        }
397
398        $extra_filters = array_diff( $filtered_values, $this->get_default_setting_for_filter( $current_filter ) );
399        $this->add_indexed_filter_setting( $current_filter, $extra_filters );
400        return static::$data_settings[ $current_filter ];
401    }
402
403    /**
404     * Sets the $data_settings property to an empty array. This is useful for testing.
405     */
406    public function empty_data_settings_and_hooks() {
407        static::$data_settings    = array();
408        static::$set_filter_hooks = array();
409    }
410
411    /**
412     * Returns the $data_settings property.
413     *
414     * @return array The data_settings property.
415     */
416    public function get_data_settings() {
417        return static::$data_settings;
418    }
419}