Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
16.15% covered (danger)
16.15%
26 / 161
9.52% covered (danger)
9.52%
2 / 21
CRAP
n/a
0 / 0
wpcomsh_is_wpcom_theme
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_is_wpcom_premium_theme
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
3.33
wpcomsh_is_wpcom_pub_theme
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
3.33
wpcomsh_symlink_theme
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
wpcomsh_install_managed_theme
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
wpcomsh_delete_theme_cache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_is_theme_symlinked
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_delete_symlinked_theme
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_get_wpcom_theme_type
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
13.62
wpcomsh_is_wpcom_child_theme
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_count_child_themes
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_symlink_parent_theme
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_get_atomic_site_id
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_get_atomic_client_id
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
wpcomsh_get_wpcom_active_subscriptions
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_get_at_site_info
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_is_xmlrpc_request_matching
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
16.67
wpcomsh_is_wp_rest_request_matching
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
72
wpcomsh_map_block_map_provider
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_disable_fatal_error_emails
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_newsletter_categories_location
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * WPCOMSH functions file.
4 *
5 * @package wpcomsh
6 */
7
8/**
9 * Whether the theme is a wpcom theme.
10 *
11 * @param string $theme_slug Theme slug.
12 * @return bool
13 */
14function wpcomsh_is_wpcom_theme( $theme_slug ) {
15    return wpcomsh_is_wpcom_premium_theme( $theme_slug ) || wpcomsh_is_wpcom_pub_theme( $theme_slug );
16}
17
18/**
19 * Whether the theme is a premium wpcom theme.
20 *
21 * @param string $theme_slug Theme slug.
22 * @return bool
23 */
24function wpcomsh_is_wpcom_premium_theme( $theme_slug ) {
25    if (
26        ! defined( 'WPCOMSH_PREMIUM_THEMES_PATH' ) ||
27        ! file_exists( WPCOMSH_PREMIUM_THEMES_PATH )
28    ) {
29        // phpcs:ignore WordPress.PHP.DevelopmentFunctions
30        error_log(
31            "WPComSH: WPCom premium themes folder couldn't be located. " .
32            'Check whether the ' . WPCOMSH_PREMIUM_THEMES_PATH . ' constant points to the correct directory.'
33        );
34
35        return false;
36    }
37
38    return file_exists(
39        WPCOMSH_PREMIUM_THEMES_PATH . "/{$theme_slug}"
40    );
41}
42
43/**
44 * Whether the theme is a free wpcom theme.
45 *
46 * @param string $theme_slug Theme slug.
47 * @return bool
48 */
49function wpcomsh_is_wpcom_pub_theme( $theme_slug ) {
50    if (
51        ! defined( 'WPCOMSH_PUB_THEMES_PATH' ) ||
52        ! file_exists( WPCOMSH_PUB_THEMES_PATH )
53    ) {
54        // phpcs:ignore WordPress.PHP.DevelopmentFunctions
55        error_log(
56            "WPComSH: WPCom pub themes folder couldn't be located. " .
57            'Check whether the ' . WPCOMSH_PUB_THEMES_PATH . ' constant points to the correct directory.'
58        );
59
60        return false;
61    }
62
63    return file_exists(
64        WPCOMSH_PUB_THEMES_PATH . "/{$theme_slug}"
65    );
66}
67
68/**
69 * Creates a theme's symlink. To install a theme, use wpcomsh_install_managed_theme(), which also handles its parent.
70 *
71 * @param string $theme_slug Theme slug.
72 * @param string $theme_type Type of theme.
73 * @return bool|WP_Error
74 */
75function wpcomsh_symlink_theme( $theme_slug, $theme_type ) {
76    /**
77     * We need to read from the themes_path constants and create the symlink using the themes_symlink constant.
78     *
79     * More context here:
80     * - p1487627624008111-slack-C2PDURDSL
81     * - p1713351585355929-slack-C7YPW6K40
82     */
83    $theme_link_to_path = get_theme_root() . '/' . $theme_slug;
84    $theme_read_path    = '';
85    $theme_symlink_path = '';
86    if ( WPCOMSH_PUB_THEME_TYPE === $theme_type ) {
87        $theme_read_path    = WPCOMSH_PUB_THEMES_PATH . "/$theme_slug";
88        $theme_symlink_path = WPCOMSH_PUB_THEMES_SYMLINK . "/$theme_slug";
89    } elseif ( WPCOMSH_PREMIUM_THEME_TYPE === $theme_type ) {
90        $theme_read_path    = WPCOMSH_PREMIUM_THEMES_PATH . "/$theme_slug";
91        $theme_symlink_path = WPCOMSH_PREMIUM_THEMES_SYMLINK . "/$theme_slug";
92    } elseif ( WPCOMSH_MANAGED_THEME_TYPE === $theme_type ) {
93        $theme_read_path    = THEMES_PATH_BASE . "/$theme_slug/latest";
94        $theme_symlink_path = THEMES_SYMLINK_BASE . "/$theme_slug/latest";
95    }
96
97    if ( file_exists( $theme_read_path ) && symlink( $theme_symlink_path, $theme_link_to_path ) ) {
98        return true;
99    }
100
101    $error_message = "Could not install theme $theme_slug.";
102    $debug_message = "$error_message Read directory: $theme_read_path, symlink directory: $theme_symlink_path, link to: $theme_link_to_path.";
103
104    error_log( 'WPComSH: ' . $debug_message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
105
106    return new WP_Error( 'error_symlinking_theme', $error_message );
107}
108
109/**
110 * Installs a theme WP Cloud manages by symlinking it, and its parent if it is a child theme.
111 *
112 * @param string $theme_slug Theme slug.
113 * @param string $theme_type Type of theme.
114 *
115 * @return true|\WP_Error
116 */
117function wpcomsh_install_managed_theme( $theme_slug, $theme_type ) {
118    if ( wpcomsh_is_theme_symlinked( $theme_slug ) ) {
119        error_log( "WPComSH: WP.com theme with slug: {$theme_slug} is already installed/symlinked." ); //phpcs:ignore
120
121        return new WP_Error( 'wpcom_theme_already_installed', 'The WP.com theme is already installed/symlinked.' );
122    }
123
124    $was_theme_symlinked = wpcomsh_symlink_theme( $theme_slug, $theme_type );
125
126    if ( is_wp_error( $was_theme_symlinked ) ) {
127        return $was_theme_symlinked;
128    }
129
130    wpcomsh_delete_theme_cache( $theme_slug );
131
132    // If the installed WPCom theme is a child theme, we need to symlink its parent theme as well, unless a working copy is already there.
133    if (
134        wpcomsh_is_wpcom_child_theme( $theme_slug ) &&
135        wp_get_theme( wp_get_theme( $theme_slug )->get_template() )->errors()
136    ) {
137        $was_parent_theme_symlinked = wpcomsh_symlink_parent_theme( $theme_slug );
138
139        if ( true !== $was_parent_theme_symlinked ) {
140            // Without its parent the child is broken, and leaving it would make a retry report it as already installed.
141            wpcomsh_delete_symlinked_theme( $theme_slug );
142            wpcomsh_delete_theme_cache( $theme_slug );
143
144            return new WP_Error( 'wpcom_theme_installation_failed', "Can't install specified WP.com theme. Check error log for more details." );
145        }
146    }
147
148    /**
149     * Fires after a WP.com theme has been installed by symlink.
150     *
151     * @param string $theme_slug Theme slug.
152     */
153    do_action( 'wpcomsh_wpcom_theme_installed', $theme_slug );
154
155    return true;
156}
157
158/**
159 * Deletes cache of the passed theme.
160 *
161 * @param string $theme_slug Optional. Slug of the theme to delete cache for.
162 *                           Default: Current theme.
163 */
164function wpcomsh_delete_theme_cache( $theme_slug = null ) {
165    $theme = wp_get_theme( $theme_slug );
166
167    if ( $theme instanceof WP_Theme ) {
168        $theme->cache_delete();
169    }
170}
171
172/**
173 * Checks whether a theme (by theme slug) is symlinked in the themes' directory.
174 *
175 * @param string $theme_slug The slug of a theme.
176 * @return bool Whether a theme is symlinked in the themes' directory.
177 */
178function wpcomsh_is_theme_symlinked( $theme_slug ) {
179    $theme_root  = get_theme_root();
180    $theme_dir   = "$theme_root/$theme_slug";
181    $site_themes = scandir( $theme_root );
182
183    return in_array( $theme_slug, $site_themes, true ) && is_link( $theme_dir );
184}
185
186/**
187 * Deletes a symlinked theme.
188 *
189 * @param string $theme_slug The slug of a theme.
190 * @return bool|WP_Error True on success, WP_Error on error.
191 */
192function wpcomsh_delete_symlinked_theme( $theme_slug ) {
193    $theme_dir = get_theme_root() . "/$theme_slug";
194
195    if ( file_exists( $theme_dir ) && is_link( $theme_dir ) ) {
196        unlink( $theme_dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
197
198        return true;
199    }
200
201    // phpcs:ignore WordPress.PHP.DevelopmentFunctions
202    error_log(
203        "WPComSH: Can't delete the specified symlinked theme: the path or symlink doesn't exist."
204    );
205
206    return new WP_Error(
207        'error_deleting_symlinked_theme',
208        "Can't delete the specified symlinked theme: the path or symlink doesn't exist."
209    );
210}
211
212/**
213 * Returns a theme type.
214 *
215 * @param string $theme_slug The slug of a theme.
216 * @return false|string Theme type or false if not a wpcom theme.
217 */
218function wpcomsh_get_wpcom_theme_type( $theme_slug ) {
219    // The slug becomes a path, so one that leaves its folder would match a WP Cloud directory that isn't a theme.
220    if ( ! is_string( $theme_slug ) || '' === $theme_slug || '.' === $theme_slug[0] || false !== strpos( $theme_slug, '/' ) ) {
221        return false;
222    }
223
224    if ( wpcomsh_is_wpcom_premium_theme( $theme_slug ) ) {
225        return WPCOMSH_PREMIUM_THEME_TYPE;
226    } elseif ( wpcomsh_is_wpcom_pub_theme( $theme_slug ) ) {
227        return WPCOMSH_PUB_THEME_TYPE;
228    } elseif ( file_exists( THEMES_PATH_BASE . "/$theme_slug/latest" ) ) {
229        return WPCOMSH_MANAGED_THEME_TYPE;
230    }
231
232    return false;
233}
234
235/**
236 * Returns whether the theme is a child theme.
237 *
238 * @param string $theme_slug Slug of the theme to check. Default: Active theme.
239 * @return bool
240 */
241function wpcomsh_is_wpcom_child_theme( $theme_slug = null ) {
242    $theme = wp_get_theme( $theme_slug );
243
244    return $theme->get_stylesheet() !== $theme->get_template();
245}
246
247/**
248 * Count the number of child themes with the specified template.
249 *
250 * @param string $template The theme template name.
251 *
252 * @return int
253 */
254function wpcomsh_count_child_themes( $template ) {
255    $child_count = 0;
256
257    foreach ( wp_get_themes() as $theme ) {
258        if (
259            $theme->get_template() === $template &&
260            $theme->get_stylesheet() !== $theme->get_template()
261        ) {
262            ++$child_count;
263        }
264    }
265
266    return $child_count;
267}
268
269/**
270 * Symlinks the theme's parent if it's a child theme.
271 *
272 * @param string $stylesheet Theme slug.
273 * @return bool|WP_Error
274 */
275function wpcomsh_symlink_parent_theme( $stylesheet ) {
276    $theme    = wp_get_theme( $stylesheet );
277    $template = $theme->get_template();
278
279    if ( $template === $stylesheet ) {
280        // phpcs:ignore WordPress.PHP.DevelopmentFunctions
281        error_log( "WPComSH: Can't symlink parent theme. Current theme is not a child theme." );
282
283        return false;
284    }
285
286    // phpcs:ignore WordPress.PHP.DevelopmentFunctions
287    error_log( 'WPComSH: Symlinking parent theme.' );
288
289    return wpcomsh_symlink_theme( $template, wpcomsh_get_wpcom_theme_type( $template ) );
290}
291
292/**
293 * Returns the Atomic site ID.
294 *
295 * @return int
296 */
297function wpcomsh_get_atomic_site_id() {
298    if ( defined( 'ATOMIC_SITE_ID' ) ) {
299        return (int) ATOMIC_SITE_ID;
300    }
301
302    $atomic_site_id = apply_filters( 'wpcomsh_get_atomic_site_id', 0 );
303    if ( ! empty( $atomic_site_id ) ) {
304        return (int) $atomic_site_id;
305    }
306
307    return 0;
308}
309
310/**
311 * Returns the Atomic client ID.
312 *
313 * @return int
314 */
315function wpcomsh_get_atomic_client_id() {
316    if ( defined( 'ATOMIC_CLIENT_ID' ) ) {
317        return (int) ATOMIC_CLIENT_ID;
318    }
319
320    $atomic_client_id = apply_filters( 'wpcomsh_get_atomic_client_id', 0 );
321    if ( ! empty( $atomic_client_id ) ) {
322        return (int) $atomic_client_id;
323    }
324
325    return 0;
326}
327
328/**
329 * Returns an array of active WordPress.com subscriptions. The array keys are the product slugs.
330 *
331 * @return array
332 */
333function wpcomsh_get_wpcom_active_subscriptions() {
334    $persistent_data = new Atomic_Persistent_Data();
335
336    if ( ! $persistent_data->WPCOM_PURCHASES ) { // phpcs:ignore WordPress.NamingConventions
337        return array();
338    }
339
340    $wpcom_purchases = json_decode( $persistent_data->WPCOM_PURCHASES ); // phpcs:ignore WordPress.NamingConventions
341
342    // If ( for any reason ) $wpcom_purchases is not an array, return early an empty array so that array_reduce doesn't throw an error.
343    if ( ! is_array( $wpcom_purchases ) ) {
344        return array();
345    }
346
347    return array_reduce(
348        $wpcom_purchases,
349        function ( $assoc_array, $purchase ) {
350            /**
351             * Check if the billing_product_slug exists, if not, revert to parsing the store product slug.
352             * This happens for sites that don't have the APD updated to the new format.
353             */
354            if ( isset( $purchase->billing_product_slug ) ) {
355                $product_slug = $purchase->billing_product_slug;
356            } else {
357                // 1. Remove _monthly or _yearly ( mainly found in marketplace plugins ).
358                // 2. Transform to product slug pattern with dashes (billing product slug).
359                $product_slug = preg_replace( array( '/(_monthly|_yearly)$/', '/_/' ), array( '', '-' ), $purchase->product_slug );
360            }
361
362            $assoc_array[ $product_slug ] = $purchase;
363
364            return $assoc_array;
365        },
366        array()
367    );
368}
369add_filter( 'pre_option_wpcom_active_subscriptions', 'wpcomsh_get_wpcom_active_subscriptions' );
370
371/**
372 * Get Atomic site information.
373 *
374 * @return array
375 */
376function wpcomsh_get_at_site_info() {
377    $at_site_info_file = sys_get_temp_dir() . '/.at-site-info';
378
379    if ( ! is_file( $at_site_info_file ) ) {
380        return array();
381    }
382
383    $site_info_json = file_get_contents( $at_site_info_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions
384
385    if ( empty( $site_info_json ) ) {
386        return array();
387    }
388
389    $site_info = json_decode( $site_info_json, true );
390    if ( empty( $site_info ) ) {
391        return array();
392    }
393
394    return $site_info;
395}
396
397/**
398 * Whether the current request is an XML-RPC request from Calypso to install a theme or plugin.
399 *
400 * @param string $path_regex Regular expression of paths to allow.
401 * @return bool
402 */
403function wpcomsh_is_xmlrpc_request_matching( $path_regex ) {
404    // Return early for all non-API requests.
405    if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
406        return false;
407    }
408
409    // Return early-ish when it's not a verified XML-RPC request.
410    if (
411        ! method_exists( 'Automattic\Jetpack\Connection\Manager', 'verify_xml_rpc_signature' ) ||
412        ! ( new Automattic\Jetpack\Connection\Manager() )->verify_xml_rpc_signature() ) {
413        return false;
414    }
415
416    return class_exists( 'WPCOM_JSON_API' ) && preg_match( $path_regex, WPCOM_JSON_API::$self->path );
417}
418
419/**
420 * Check if we are handling a WordPress core REST v2 API request where the API path matches
421 * the regular expression in $path_regex and the HTTP method(s) in $request_method.
422 *
423 * @param string          $path_regex      A regular expression for the requested path, which should include the REST prefix available from {@see rest_get_url_prefix()}.
424 * @param string|string[] $request_method  The expected HTTP method(s) of the request, e.g. GET|POST|PUT|DELETE or array( 'PUT', 'POST' ).
425 * @return bool                            Whether the incoming request matches the supplied path and method(s).
426 */
427function wpcomsh_is_wp_rest_request_matching( $path_regex, $request_method = 'GET' ) {
428    if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
429        return false;
430    }
431
432    $request_methods = is_array( $request_method ) ? $request_method : array( $request_method );
433
434    if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || ! in_array( $_SERVER['REQUEST_METHOD'], $request_methods, true ) ) {
435        return false;
436    }
437
438    // REQUEST_URI can't be trusted here: in `/wp-json/foo?rest_route=/bar`, WP serves `/bar`.
439    $route = $GLOBALS['wp']->query_vars['rest_route'] ?? '';
440    if ( ! is_string( $route ) || $route === '' ) {
441        return false;
442    }
443
444    $rest_path = '/' . rest_get_url_prefix() . $route;
445
446    return 1 === preg_match( $path_regex, $rest_path );
447}
448
449/**
450 * Returns the map provider the map block should use.
451 *
452 * @return string
453 */
454function wpcomsh_map_block_map_provider() {
455    return 'mapkit';
456}
457
458add_filter( 'wpcom_map_block_map_provider', 'wpcomsh_map_block_map_provider', 10, 0 );
459
460/**
461 * Disabled fatal error emails if the option is set.
462 *
463 * @param array $email The email arguments.
464 *
465 * @return array The email arguments.
466 */
467function wpcomsh_disable_fatal_error_emails( $email ) {
468    if ( get_option( 'wpcomsh_disable_fatal_error_emails', false ) ) {
469        $email['to'] = '';
470    }
471    return $email;
472}
473
474add_filter( 'recovery_mode_email', 'wpcomsh_disable_fatal_error_emails' );
475
476/**
477 * Returns the location where newsletter categories should appear
478 *
479 * @return string
480 */
481function wpcomsh_newsletter_categories_location() {
482    return 'modal';
483}
484
485add_filter( 'wpcom_newsletter_categories_location', 'wpcomsh_newsletter_categories_location', 10, 0 );
486
487/**
488 * Enables new likes layout on Atomic.
489 */
490add_filter( 'likes_new_layout', '__return_true' );
491
492/**
493 * Disable email notification when a user's email address is changed.
494 *
495 * @see https://github.com/Automattic/wp-calypso/issues/100279
496 */
497add_filter( 'send_email_change_email', '__return_false' );