PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-sync / src / class-functions.php

class-functions.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at jetpack_vendor/automattic/jetpack-sync/src/class-functions.php

764 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Utility functions to generate data synced to wpcom
4 *
5 * @package automattic/jetpack-sync
6 */
7
8 namespace Automattic\Jetpack\Sync;
9
10 use Automattic\Jetpack\Connection\Manager;
11 use Automattic\Jetpack\Connection\Urls;
12 use Automattic\Jetpack\Constants;
13 use Automattic\Jetpack\Modules as Jetpack_Modules;
14
15 /**
16 * Utility functions to generate data synced to wpcom
17 */
18 class 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( (int) $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 }
764