Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 200
0.00% covered (danger)
0.00%
0 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
Functions
0.00% covered (danger)
0.00%
0 / 200
0.00% covered (danger)
0.00%
0 / 34
8742
0.00% covered (danger)
0.00%
0 / 1
 get_modules
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_taxonomies
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get_shortcodes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sanitize_taxonomy
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 get_post_types
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 sanitize_post_type
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 expand_synced_post_type
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 get_post_type_features
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_hosting_provider
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 get_hosting_provider_by_known_constant
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 get_hosting_provider_by_known_class
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_hosting_provider_by_known_function
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 rest_api_allowed_post_types
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rest_api_allowed_public_metadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_version_controlled
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 file_system_write_access
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 get_raw_or_filtered_url
n/a
0 / 0
n/a
0 / 0
1
 home_url
n/a
0 / 0
n/a
0 / 0
1
 site_url
n/a
0 / 0
n/a
0 / 0
1
 main_network_site_url
n/a
0 / 0
n/a
0 / 0
1
 main_network_site_wpcom_id
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_protocol_normalized_url
n/a
0 / 0
n/a
0 / 0
1
 get_raw_url
n/a
0 / 0
n/a
0 / 0
1
 normalize_www_in_url
n/a
0 / 0
n/a
0 / 0
1
 get_plugins
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_plugins_action_links
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 wp_version
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 site_icon_url
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 roles
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_timezone
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 get_paused_themes
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_paused_plugins
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_theme_support
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get_is_fse_theme
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 json_wrap
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
72
 get_themes
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 get_active_modules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_loaded_extensions
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 get_jetpack_connection_active_plugins
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_jetpack_sync_active_modules
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 get_jetpack_package_versions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Utility functions to generate data synced to wpcom
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync;
9
10use Automattic\Jetpack\Connection\Manager;
11use Automattic\Jetpack\Connection\Urls;
12use Automattic\Jetpack\Constants;
13use Automattic\Jetpack\Modules as Jetpack_Modules;
14
15/**
16 * Utility functions to generate data synced to wpcom
17 */
18class Functions {
19    const HTTPS_CHECK_OPTION_PREFIX = 'jetpack_sync_https_history_';
20    const HTTPS_CHECK_HISTORY       = 5;
21
22    /**
23     * Return array of Jetpack modules.
24     *
25     * @return array
26     */
27    public static function get_modules() {
28        if ( defined( 'JETPACK__PLUGIN_DIR' ) ) {
29            require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php';
30
31            return \Jetpack_Admin::init()->get_modules();
32        }
33
34        return array();
35    }
36
37    /**
38     * Return array of taxonomies registered on the site.
39     *
40     * @return array
41     */
42    public static function get_taxonomies() {
43        global $wp_taxonomies;
44        $wp_taxonomies_without_callbacks = array();
45        foreach ( $wp_taxonomies as $taxonomy_name => $taxonomy ) {
46            $sanitized_taxonomy = self::sanitize_taxonomy( $taxonomy );
47            if ( ! empty( $sanitized_taxonomy ) ) {
48                $wp_taxonomies_without_callbacks[ $taxonomy_name ] = $sanitized_taxonomy;
49            }
50        }
51        return $wp_taxonomies_without_callbacks;
52    }
53
54    /**
55     * Return array of registered shortcodes.
56     *
57     * @return array
58     */
59    public static function get_shortcodes() {
60        global $shortcode_tags;
61        return array_keys( $shortcode_tags );
62    }
63
64    /**
65     * Removes any callback data since we will not be able to process it on our side anyways.
66     *
67     * @param \WP_Taxonomy $taxonomy \WP_Taxonomy item.
68     *
69     * @return mixed|null
70     */
71    public static function sanitize_taxonomy( $taxonomy ) {
72
73        // Lets clone the taxonomy object instead of modifing the global one.
74        $cloned_taxonomy = json_decode( wp_json_encode( $taxonomy, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
75
76        // recursive taxonomies are no fun.
77        if ( $cloned_taxonomy === null ) {
78            return null;
79        }
80        // Remove any meta_box_cb if they are not the default wp ones.
81        if ( isset( $cloned_taxonomy->meta_box_cb ) &&
82            ! in_array( $cloned_taxonomy->meta_box_cb, array( 'post_tags_meta_box', 'post_categories_meta_box' ), true ) ) {
83            $cloned_taxonomy->meta_box_cb = null;
84        }
85        // Remove update call back.
86        if ( isset( $cloned_taxonomy->update_count_callback ) &&
87            $cloned_taxonomy->update_count_callback !== null ) {
88            $cloned_taxonomy->update_count_callback = null;
89        }
90        // Remove rest_controller_class if it something other then the default.
91        if ( isset( $cloned_taxonomy->rest_controller_class ) &&
92            'WP_REST_Terms_Controller' !== $cloned_taxonomy->rest_controller_class ) {
93            $cloned_taxonomy->rest_controller_class = null;
94        }
95        return $cloned_taxonomy;
96    }
97
98    /**
99     * Return array of registered post types.
100     *
101     * @return array
102     */
103    public static function get_post_types() {
104        global $wp_post_types;
105
106        $post_types_without_callbacks = array();
107        foreach ( $wp_post_types as $post_type_name => $post_type ) {
108            $sanitized_post_type = self::sanitize_post_type( $post_type );
109            if ( ! empty( $sanitized_post_type ) ) {
110                $post_types_without_callbacks[ $post_type_name ] = $sanitized_post_type;
111            }
112        }
113        return $post_types_without_callbacks;
114    }
115
116    /**
117     * Sanitizes by cloning post type object.
118     *
119     * @param object $post_type \WP_Post_Type.
120     *
121     * @return object
122     */
123    public static function sanitize_post_type( $post_type ) {
124        // Lets clone the post type object instead of modifing the global one.
125        $sanitized_post_type = array();
126        foreach ( Defaults::$default_post_type_attributes as $attribute_key => $default_value ) {
127            if ( isset( $post_type->{ $attribute_key } ) ) {
128                $sanitized_post_type[ $attribute_key ] = $post_type->{ $attribute_key };
129            }
130        }
131        return (object) $sanitized_post_type;
132    }
133
134    /**
135     * Return information about a synced post type.
136     *
137     * @param array  $sanitized_post_type Array of args used in constructing \WP_Post_Type.
138     * @param string $post_type Post type name.
139     *
140     * @return object \WP_Post_Type
141     */
142    public static function expand_synced_post_type( $sanitized_post_type, $post_type ) {
143        $post_type        = sanitize_key( $post_type );
144        $post_type_object = new \WP_Post_Type( $post_type, $sanitized_post_type );
145        $post_type_object->add_supports();
146        $post_type_object->add_rewrite_rules();
147        $post_type_object->add_hooks();
148        $post_type_object->register_taxonomies();
149        return $post_type_object;
150    }
151
152    /**
153     * Returns site's post_type_features.
154     *
155     * @return array
156     */
157    public static function get_post_type_features() {
158        global $_wp_post_type_features;
159
160        return $_wp_post_type_features;
161    }
162
163    /**
164     * Return hosting provider.
165     *
166     * Uses a set of known constants, classes, or functions to help determine the hosting platform.
167     *
168     * @return string Hosting provider.
169     */
170    public static function get_hosting_provider() {
171        $hosting_provider_detection_methods = array(
172            'get_hosting_provider_by_known_constant',
173            'get_hosting_provider_by_known_class',
174            'get_hosting_provider_by_known_function',
175        );
176
177        $functions = new Functions();
178        foreach ( $hosting_provider_detection_methods as $method ) {
179            $hosting_provider = call_user_func( array( $functions, $method ) );
180            if ( false !== $hosting_provider ) {
181                return $hosting_provider;
182            }
183        }
184
185        return 'unknown';
186    }
187
188    /**
189     * Return a hosting provider using a set of known constants.
190     *
191     * @return mixed A host identifier string or false.
192     */
193    public function get_hosting_provider_by_known_constant() {
194        $hosting_provider_constants = array(
195            'GD_SYSTEM_PLUGIN_DIR' => 'gd-managed-wp',
196            'MM_BASE_DIR'          => 'bh',
197            'PAGELYBIN'            => 'pagely',
198            'KINSTAMU_VERSION'     => 'kinsta',
199            'FLYWHEEL_CONFIG_DIR'  => 'flywheel',
200            'IS_PRESSABLE'         => 'pressable',
201            'VIP_GO_ENV'           => 'vip-go',
202        );
203
204        foreach ( $hosting_provider_constants as $constant => $constant_value ) {
205            if ( Constants::is_defined( $constant ) ) {
206                if ( 'VIP_GO_ENV' === $constant && false === Constants::get_constant( 'VIP_GO_ENV' ) ) {
207                    continue;
208                }
209                return $constant_value;
210            }
211        }
212
213        return false;
214    }
215
216    /**
217     * Return a hosting provider using a set of known classes.
218     *
219     * @return mixed A host identifier string or false.
220     */
221    public function get_hosting_provider_by_known_class() {
222        $hosting_provider = false;
223
224        switch ( true ) {
225            case ( class_exists( '\\WPaaS\\Plugin' ) ):
226                $hosting_provider = 'gd-managed-wp';
227                break;
228        }
229
230        return $hosting_provider;
231    }
232
233    /**
234     * Return a hosting provider using a set of known functions.
235     *
236     * @return mixed A host identifier string or false.
237     */
238    public function get_hosting_provider_by_known_function() {
239        $hosting_provider = false;
240
241        switch ( true ) {
242            case ( function_exists( 'is_wpe' ) || function_exists( 'is_wpe_snapshot' ) ):
243                $hosting_provider = 'wpe';
244                break;
245        }
246
247        return $hosting_provider;
248    }
249
250    /**
251     * Return array of allowed REST API post types.
252     *
253     * @return array Array of allowed post types.
254     */
255    public static function rest_api_allowed_post_types() {
256        /** This filter is already documented in class.json-api-endpoints.php */
257        return apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) );
258    }
259
260    /**
261     * Return array of allowed REST API public metadata.
262     *
263     * @return array Array of allowed metadata.
264     */
265    public static function rest_api_allowed_public_metadata() {
266        /**
267         * Filters the meta keys accessible by the REST API.
268         *
269         * @see https://developer.wordpress.com/2013/04/26/custom-post-type-and-metadata-support-in-the-rest-api/
270         *
271         * @module json-api
272         *
273         * @since 1.6.3
274         * @since-jetpack 2.2.3
275         *
276         * @param array $whitelisted_meta Array of metadata that is accessible by the REST API.
277         */
278        return apply_filters( 'rest_api_allowed_public_metadata', array() );
279    }
280
281    /**
282     * Finds out if a site is using a version control system.
283     *
284     * @return bool
285     **/
286    public static function is_version_controlled() {
287
288        if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
289            require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
290        }
291        $updater = new \WP_Automatic_Updater();
292
293        return (bool) (string) $updater->is_vcs_checkout( ABSPATH );
294    }
295
296    /**
297     * Returns true if the site has file write access false otherwise.
298     *
299     * @return bool
300     **/
301    public static function file_system_write_access() {
302        if ( ! function_exists( 'get_filesystem_method' ) ) {
303            require_once ABSPATH . 'wp-admin/includes/file.php';
304        }
305
306        require_once ABSPATH . 'wp-admin/includes/template.php';
307
308        $filesystem_method = get_filesystem_method();
309        if ( 'direct' === $filesystem_method ) {
310            return true;
311        }
312
313        ob_start();
314
315        if ( ! function_exists( 'request_filesystem_credentials' ) ) {
316            require_once ABSPATH . 'wp-admin/includes/file.php';
317        }
318
319        $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
320        ob_end_clean();
321        if ( $filesystem_credentials_are_stored ) {
322            return true;
323        }
324
325        return false;
326    }
327
328    /**
329     * Helper function that is used when getting home or siteurl values. Decides
330     * whether to get the raw or filtered value.
331     *
332     * @deprecated 1.23.1
333     *
334     * @param string $url_type URL to get, home or siteurl.
335     * @return string
336     */
337    public static function get_raw_or_filtered_url( $url_type ) {
338        _deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::get_raw_or_filtered_url' );
339        return Urls::get_raw_or_filtered_url( $url_type );
340    }
341
342    /**
343     * Return the escaped home_url.
344     *
345     * @deprecated 1.23.1
346     *
347     * @return string
348     */
349    public static function home_url() {
350        _deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::home_url' );
351        return Urls::home_url();
352    }
353
354    /**
355     * Return the escaped siteurl.
356     *
357     * @deprecated 1.23.1
358     *
359     * @return string
360     */
361    public static function site_url() {
362        _deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::site_url' );
363        return Urls::site_url();
364    }
365
366    /**
367     * Return main site URL with a normalized protocol.
368     *
369     * @deprecated 1.23.1
370     *
371     * @return string
372     */
373    public static function main_network_site_url() {
374        _deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::main_network_site_url' );
375        return Urls::main_network_site_url();
376    }
377
378    /**
379     * Return main site WordPress.com site ID.
380     *
381     * @return string
382     */
383    public static function main_network_site_wpcom_id() {
384        /**
385         * Return the current site WPCOM ID for single site installs
386         */
387        if ( ! is_multisite() ) {
388            return \Jetpack_Options::get_option( 'id' );
389        }
390
391        /**
392         * Return the main network site WPCOM ID for multi-site installs
393         */
394        $current_network = get_network();
395        switch_to_blog( $current_network->blog_id );
396        $wpcom_blog_id = \Jetpack_Options::get_option( 'id' );
397        restore_current_blog();
398        return $wpcom_blog_id;
399    }
400
401    /**
402     * Return URL with a normalized protocol.
403     *
404     * @deprecated 1.23.1
405     *
406     * @param string $callable Function name that was used to retrieve URL option.
407     * @param string $new_value URL Protocol to set URLs to.
408     * @return string Normalized URL.
409     */
410    public static function get_protocol_normalized_url( $callable, $new_value ) {
411        _deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::get_protocol_normalized_url' );
412        return Urls::get_protocol_normalized_url( $callable, $new_value );
413    }
414
415    /**
416     * Return URL from option or PHP constant.
417     *
418     * @deprecated 1.23.1
419     *
420     * @param string $option_name (e.g. 'home').
421     *
422     * @return mixed|null URL.
423     */
424    public static function get_raw_url( $option_name ) {
425        _deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::get_raw_url' );
426        return Urls::get_raw_url( $option_name );
427    }
428
429    /**
430     * Normalize domains by removing www unless declared in the site's option.
431     *
432     * @deprecated 1.23.1
433     *
434     * @param string   $option Option value from the site.
435     * @param callable $url_function Function retrieving the URL to normalize.
436     * @return mixed|string URL.
437     */
438    public static function normalize_www_in_url( $option, $url_function ) {
439        _deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::normalize_www_in_url' );
440        return Urls::normalize_www_in_url( $option, $url_function );
441    }
442
443    /**
444     * Return filtered value of get_plugins.
445     *
446     * @return mixed|void
447     */
448    public static function get_plugins() {
449        if ( ! function_exists( 'get_plugins' ) ) {
450            require_once ABSPATH . 'wp-admin/includes/plugin.php';
451        }
452
453        /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
454        return apply_filters( 'all_plugins', get_plugins() );
455    }
456
457    /**
458     * Get custom action link tags that the plugin is using
459     * Ref: https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name)
460     *
461     * @param string $plugin_file_singular Particular plugin.
462     * @return array of plugin action links (key: link name value: url)
463     */
464    public static function get_plugins_action_links( $plugin_file_singular = null ) {
465        // Some sites may have DOM disabled in PHP fail early.
466        if ( ! class_exists( 'DOMDocument' ) ) {
467            return array();
468        }
469        $plugins_action_links = get_option( 'jetpack_plugin_api_action_links', array() );
470        if ( ! empty( $plugins_action_links ) ) {
471            if ( $plugin_file_singular === null ) {
472                return $plugins_action_links;
473            }
474            return ( $plugins_action_links[ $plugin_file_singular ] ?? null );
475        }
476        return array();
477    }
478
479    /**
480     * Return the WP version as defined in the $wp_version global.
481     *
482     * @return string
483     */
484    public static function wp_version() {
485        global $wp_version;
486        return $wp_version;
487    }
488
489    /**
490     * Return site icon url used on the site.
491     *
492     * @param int $size Size of requested icon in pixels.
493     * @return mixed|string|void
494     */
495    public static function site_icon_url( $size = 512 ) {
496        $site_icon = get_site_icon_url( $size );
497        return $site_icon ? $site_icon : get_option( 'jetpack_site_icon_url' );
498    }
499
500    /**
501     * Return roles registered on the site.
502     *
503     * @return array
504     */
505    public static function roles() {
506        $wp_roles = wp_roles();
507        return $wp_roles->roles;
508    }
509
510    /**
511     * Determine time zone from WordPress' options "timezone_string"
512     * and "gmt_offset".
513     *
514     * 1. Check if `timezone_string` is set and return it.
515     * 2. Check if `gmt_offset` is set, formats UTC-offset from it and return it.
516     * 3. Default to "UTC+0" if nothing is set.
517     *
518     * Note: This function is specifically not using wp_timezone() to keep consistency with
519     * the existing formatting of the timezone string.
520     *
521     * @return string
522     */
523    public static function get_timezone() {
524        $timezone_string = get_option( 'timezone_string' );
525
526        if ( ! empty( $timezone_string ) ) {
527            return str_replace( '_', ' ', $timezone_string );
528        }
529
530        $gmt_offset = get_option( 'gmt_offset', 0 );
531
532        $formatted_gmt_offset = sprintf( '%+g', (float) $gmt_offset );
533
534        $formatted_gmt_offset = str_replace(
535            array( '.25', '.5', '.75' ),
536            array( ':15', ':30', ':45' ),
537            $formatted_gmt_offset
538        );
539
540        /* translators: %s is UTC offset, e.g. "+1" */
541        return sprintf( __( 'UTC%s', 'jetpack-sync' ), $formatted_gmt_offset );
542    }
543
544    /**
545     * Return list of paused themes.
546     *
547     * @return array|bool Array of paused themes or false if unsupported.
548     */
549    public static function get_paused_themes() {
550        $paused_themes = wp_paused_themes();
551        return $paused_themes->get_all();
552    }
553
554    /**
555     * Return list of paused plugins.
556     *
557     * @return array|bool Array of paused plugins or false if unsupported.
558     */
559    public static function get_paused_plugins() {
560        $paused_plugins = wp_paused_plugins();
561        return $paused_plugins->get_all();
562    }
563
564    /**
565     * Return the theme's supported features.
566     * Used for syncing the supported feature that we care about.
567     *
568     * @return array List of features that the theme supports.
569     */
570    public static function get_theme_support() {
571        global $_wp_theme_features;
572
573        $theme_support = array();
574        foreach ( Defaults::$default_theme_support_whitelist as $theme_feature ) {
575            $has_support = current_theme_supports( $theme_feature );
576            if ( $has_support ) {
577                $theme_support[ $theme_feature ] = $_wp_theme_features[ $theme_feature ];
578            }
579        }
580
581        return $theme_support;
582    }
583
584    /**
585     * Returns if the current theme is a Full Site Editing theme.
586     *
587     * @since 1.49.0 Uses wp_is_block_theme() instead of deprecated gutenberg_is_fse_theme().
588     *
589     * @return bool Theme is a Full Site Editing theme.
590     */
591    public static function get_is_fse_theme() {
592        return wp_is_block_theme();
593    }
594
595    /**
596     * Wraps data in a way so that we can distinguish between objects and array and also prevent object recursion.
597     *
598     * @since 1.21.0
599     *
600     * @param mixed $any        Source data to be cleaned up.
601     * @param array $seen_nodes Built array of nodes.
602     *
603     * @return array
604     */
605    public static function json_wrap( &$any, $seen_nodes = array() ) {
606        if ( is_object( $any ) ) {
607            $input = get_object_vars( $any );
608
609            // WordPress 6.9 introduced lazy-loading of WP_User `roles`, `caps`, and `allcaps` properties.
610            // It also made said properties protected, so we need to access them and set them as keys manually.
611            if ( $any instanceof \WP_User ) {
612                $roles            = $any->roles;
613                $caps             = $any->caps;
614                $allcaps          = $any->allcaps;
615                $input['roles']   = $roles;
616                $input['caps']    = $caps;
617                $input['allcaps'] = $allcaps;
618            }
619
620            $input['__o'] = 1;
621        } else {
622            $input = &$any;
623        }
624
625        if ( is_array( $input ) ) {
626            $seen_nodes[] = &$any;
627
628            $return = array();
629
630            foreach ( $input as $k => &$v ) {
631                if ( ( is_array( $v ) || is_object( $v ) ) ) {
632                    if ( in_array( $v, $seen_nodes, true ) ) {
633                        continue;
634                    }
635                    $return[ $k ] = self::json_wrap( $v, $seen_nodes );
636                } else {
637                    $return[ $k ] = $v;
638                }
639            }
640
641            return $return;
642        }
643
644        return $any;
645    }
646
647    /**
648     * Return the list of installed themes
649     *
650     * @since 1.31.0
651     *
652     * @return array
653     */
654    public static function get_themes() {
655        $current_stylesheet = get_stylesheet();
656        $installed_themes   = wp_get_themes();
657        $synced_headers     = array( 'Name', 'ThemeURI', 'Author', 'Version', 'Template', 'Status', 'TextDomain', 'RequiresWP', 'RequiresPHP' );
658        $themes             = array();
659        foreach ( $installed_themes as $stylesheet => $theme ) {
660            $themes[ $stylesheet ] = array();
661            foreach ( $synced_headers as $header ) {
662                $themes[ $stylesheet ][ $header ] = $theme->get( $header );
663            }
664            $themes[ $stylesheet ]['active'] = $stylesheet === $current_stylesheet;
665            if ( method_exists( $theme, 'is_block_theme' ) ) {
666                $themes[ $stylesheet ]['is_block_theme'] = $theme->is_block_theme();
667            }
668        }
669        /**
670         * Filters the output of Sync's get_theme callable
671         *
672         * @since 1.31.0
673         *
674         * @param array $themes The list of installed themes formatted in an array with a collection of information extracted from the Theme's headers
675         */
676        return apply_filters( 'jetpack_sync_get_themes_callable', $themes );
677    }
678
679    /**
680     * Return the list of active Jetpack modules.
681     *
682     * @since 1.34.0
683     *
684     * @return array
685     */
686    public static function get_active_modules() {
687        return ( new Jetpack_Modules() )->get_active();
688    }
689
690    /**
691     * Return a list of PHP modules that we want to track.
692     *
693     * @since 1.50.0
694     *
695     * @return array
696     */
697    public static function get_loaded_extensions() {
698        if ( function_exists( 'get_loaded_extensions' ) ) {
699            return get_loaded_extensions();
700        }
701
702        // If a hosting provider has blocked get_loaded_extensions for any reason,
703        // we check extensions manually.
704
705        $extensions_to_check = array(
706            'libxml' => array( 'class' => 'libXMLError' ),
707            'xml'    => array( 'function' => 'xml_parse' ),
708            'dom'    => array( 'class' => 'DOMDocument' ),
709            'xdebug' => array( 'function' => 'xdebug_break' ),
710        );
711
712        $enabled_extensions = array();
713        foreach ( $extensions_to_check as $extension_name => $extension ) {
714            if (
715                ( isset( $extension['function'] )
716                && function_exists( $extension['function'] ) )
717                || class_exists( $extension['class'] )
718            ) {
719                $enabled_extensions[] = $extension_name;
720            }
721        }
722
723        return $enabled_extensions;
724    }
725
726    /**
727     * Return the list of active connected Jetpack plugins.
728     *
729     * @since 3.2.0
730     *
731     * @return array
732     */
733    public static function get_jetpack_connection_active_plugins() {
734        return ( new Manager() )->get_connected_plugins();
735    }
736
737    /**
738     * Return the list of active sync modules.
739     *
740     * @since 3.6.0
741     *
742     * @return array
743     */
744    public static function get_jetpack_sync_active_modules() {
745
746        /** This filter is documented in projects/packages/sync/src/class-modules.php */
747        $modules = apply_filters( 'jetpack_sync_modules', Modules::DEFAULT_SYNC_MODULES );
748        $modules = array_unique( $modules );
749        $modules = array_map( 'wp_normalize_path', $modules );
750        return $modules;
751    }
752
753    /**
754     * Return the list of Jetpack package versions.
755     *
756     * @since 4.11.1
757     *
758     * @return array
759     */
760    public static function get_jetpack_package_versions() {
761        return apply_filters( 'jetpack_package_versions', array() );
762    }
763}