Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
22.62% covered (danger)
22.62%
38 / 168
13.33% covered (danger)
13.33%
2 / 15
CRAP
n/a
0 / 0
wpcomsh_map_feature_cap
51.11% covered (warning)
51.11%
23 / 45
0.00% covered (danger)
0.00%
0 / 1
112.18
wpcomsh_is_plugin_list_request
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wpcomsh_is_theme_install_request
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_is_woocommerce_onboarding_plugin_request
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
wpcomsh_is_woocommerce_connect_request
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_get_rest_methods_as_array
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_maybe_remove_permalinks_menu_item
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_maybe_disable_permalink_page
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_plupload_file_restrictions
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
wpcomsh_maybe_restrict_mimetypes
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
wpcomsh_maybe_redirect_to_calypso_plugin_pages
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
wpcomsh_gate_footer_credit_feature
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_gate_jetpack_forms_integrations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_maybe_remove_jetpack_dashboard_menu_item
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_remove_jetpack_manage_menu_item
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Collection of hooks that apply feature checks on Atomic sites.
4 *
5 * @package wpcomsh
6 */
7
8// Load Permalinks upsell screen for Atomic sites without the feature.
9require_once __DIR__ . '/permalinks/upsell-permalinks.php';
10
11/**
12 * Disables theme and plugin related capabilities if the site doesn't have the required features.
13 *
14 * @param string[] $caps Primitive capabilities required of the user.
15 * @param string   $cap  Capability being checked.
16 * @return string[] Filtered primitive caps.
17 */
18function wpcomsh_map_feature_cap( $caps, $cap ) {
19
20    switch ( $cap ) {
21        case 'update_core':
22        case 'update_languages':
23            // Restrict access to Home > Updates on sites that can't manage plugins.
24            if ( ! wpcom_site_has_feature( WPCOM_Features::MANAGE_PLUGINS ) ) {
25                $caps[] = 'do_not_allow';
26            }
27            break;
28
29        case 'update_themes':
30        case 'delete_themes':
31            if ( ! wpcom_site_has_feature( WPCOM_Features::INSTALL_THEMES ) ) {
32                $caps[] = 'do_not_allow';
33            }
34            break;
35
36        case 'install_themes':
37            // Don't restrict `install_themes` when installing from WP.com.
38            if ( wpcomsh_is_theme_install_request() ) {
39                break;
40            }
41
42            if ( ! wpcom_site_has_feature( WPCOM_Features::INSTALL_THEMES ) ) {
43                $caps[] = 'do_not_allow';
44            }
45            break;
46
47        case 'edit_themes':
48            if ( ! wpcom_site_has_feature( WPCOM_Features::EDIT_THEMES ) ) {
49                $caps[] = 'do_not_allow';
50            }
51            break;
52
53        case 'upload_themes':
54            if ( ! wpcom_site_has_feature( WPCOM_Features::UPLOAD_THEMES ) ) {
55                $caps[] = 'do_not_allow';
56            }
57            break;
58
59        case 'activate_plugins':
60        case 'install_plugins':
61        case 'update_plugins':
62            /*
63             * Requests like /sites/207323956/plugins rely on the activate_plugins capability.
64             * Allow sites with the LIST_INSTALLED_PLUGINS feature to list the installed plugins.
65             */
66            if ( wpcomsh_is_plugin_list_request() && wpcom_site_has_feature( WPCOM_Features::LIST_INSTALLED_PLUGINS ) ) {
67                break;
68            }
69
70            /*
71             * Specifically allow install and activate permissions for WooCommerce onboarding plugins.
72             */
73            if (
74                wpcom_site_has_feature( WPCOM_Features::INSTALL_WOO_ONBOARDING_PLUGINS )
75                && (
76                    wpcomsh_is_woocommerce_onboarding_plugin_request()
77                    || wpcomsh_is_woocommerce_connect_request()
78                )
79            ) {
80                break;
81            }
82
83            if ( ! wpcom_site_has_feature( WPCOM_Features::INSTALL_PLUGINS ) ) {
84                $caps[] = 'do_not_allow';
85            }
86            break;
87
88        case 'upload_plugins':
89            if ( ! wpcom_site_has_feature( WPCOM_Features::UPLOAD_PLUGINS ) ) {
90                $caps[] = 'do_not_allow';
91            }
92            break;
93
94        case 'edit_plugins':
95            if ( ! wpcom_site_has_feature( WPCOM_Features::EDIT_PLUGINS ) ) {
96                $caps[] = 'do_not_allow';
97            }
98            break;
99    }
100
101    return $caps;
102}
103add_filter( 'map_meta_cap', 'wpcomsh_map_feature_cap', 10, 2 );
104
105/**
106 * Whether the current request is an XML-RPC request from Calypso to list plugins.
107 *
108 * @return bool
109 */
110function wpcomsh_is_plugin_list_request() {
111    return wpcomsh_is_xmlrpc_request_matching( '@^/sites/([^/]+)/plugins$@' );
112}
113
114/**
115 * Whether the current request is an XML-RPC request from Calypso to install a WP.com theme.
116 *
117 * @return bool
118 */
119function wpcomsh_is_theme_install_request() {
120    return wpcomsh_is_xmlrpc_request_matching( '@/sites/(.+)/themes/(.+)/install@' );
121}
122
123/**
124 * Whether the current request is a REST API request from the WooCommerce onboarding tasks
125 * trying to fetch a recommended payment gateway, or perform installation/activation of a plugin.
126 *
127 * @return bool
128 */
129function wpcomsh_is_woocommerce_onboarding_plugin_request() {
130    $wp_json_prefix = preg_quote( rest_get_url_prefix(), '@' );
131
132    // Check if we're looking up payment gateway suggestions.
133    if ( wpcomsh_is_wp_rest_request_matching( '@^/' . $wp_json_prefix . '/wc-admin/payment-gateway-suggestions@' ) ) {
134        return true;
135    }
136
137    $editable_methods = wpcomsh_get_rest_methods_as_array( \WP_REST_Server::EDITABLE );
138
139    if ( ! wpcomsh_is_wp_rest_request_matching( '@^/' . $wp_json_prefix . '/wc-admin/plugins/install@', $editable_methods ) && ! wpcomsh_is_wp_rest_request_matching( '@^/' . $wp_json_prefix . '/wc-admin/plugins/activate@', $editable_methods ) ) {
140        return false;
141    }
142
143    $wp_referer = wp_get_referer();
144
145    if ( empty( $wp_referer ) ) {
146        return false;
147    }
148
149    // Check if we're requesting a plugin installation or activation from WooCommerce onboarding tasks.
150
151    // User is retrying install from the payment gateway install/setup page.
152    if ( str_starts_with( $wp_referer, admin_url( 'admin.php?page=wc-admin&task=payments&id=' ) ) ) {
153        return true;
154    }
155
156    $permitted_admin_paths = array(
157        // Payments onboarding task
158        'admin.php?page=wc-admin&task=payments',
159        // WooCommerce Payments onboarding task
160        'admin.php?page=wc-admin&task=woocommerce-payments',
161        // Tax onboarding task
162        'admin.php?page=wc-admin&task=tax',
163        // WooCommerce Settings -> Payments tab
164        'admin.php?page=wc-settings&tab=checkout',
165    );
166
167    foreach ( $permitted_admin_paths as $permitted_admin_path ) {
168        if ( $wp_referer === admin_url( $permitted_admin_path ) ) {
169            return true;
170        }
171    }
172
173    return false;
174}
175
176/**
177 * Whether the current request is a REST API request to perform a
178 * WooCommerce connection activity.
179 *
180 * @return bool
181 */
182function wpcomsh_is_woocommerce_connect_request() {
183    $wp_json_prefix = preg_quote( rest_get_url_prefix(), '@' );
184
185    $editable_methods = wpcomsh_get_rest_methods_as_array( \WP_REST_Server::EDITABLE );
186
187    $permitted_connect_api_paths = array(
188        '/wc-admin/plugins/connect-jetpack' => \WP_REST_Server::READABLE,
189        '/wc-admin/plugins/connect-square'  => $editable_methods,
190        '/wc-admin/plugins/connect-wcpay'   => $editable_methods,
191    );
192
193    foreach ( $permitted_connect_api_paths as $permitted_connect_api_path => $supported_methods ) {
194        if ( wpcomsh_is_wp_rest_request_matching( '@^/' . $wp_json_prefix . $permitted_connect_api_path . '@', $supported_methods ) ) {
195            return true;
196        }
197    }
198
199    return false;
200}
201
202/**
203 * Helper method to split an HTTP method string from one of the REST constants in {@see \WP_REST_Server}
204 *
205 * @param string $method The HTTP method(s), generally drawn from constants in \WP_REST_Server.
206 * @return string[]
207 */
208function wpcomsh_get_rest_methods_as_array( $method ) {
209    return array_map(
210        'trim',
211        explode( ',', $method )
212    );
213}
214
215/**
216 * If this site does NOT have the 'options-permalink' feature, remove the Settings > Permalinks submenu item.
217 */
218function wpcomsh_maybe_remove_permalinks_menu_item() {
219    if ( wpcom_site_has_feature( WPCOM_Features::OPTIONS_PERMALINK ) ) {
220        return;
221    }
222    remove_submenu_page( 'options-general.php', 'options-permalink.php' );
223    // Add replacement upsell submenu on Atomic sites without the feature.
224    if ( function_exists( 'wpcomsh_permalinks_upsell_page_on_atomic_sites' ) ) {
225        wpcomsh_permalinks_upsell_page_on_atomic_sites();
226    }
227}
228add_action( 'admin_menu', 'wpcomsh_maybe_remove_permalinks_menu_item' );
229
230/**
231 * If this site does NOT have the 'options-permalink' feature, disable the /wp-admin/options-permalink.php page.
232 * But always allow proxied users to access the permalink options page.
233 */
234function wpcomsh_maybe_disable_permalink_page() {
235    if ( wpcom_site_has_feature( WPCOM_Features::OPTIONS_PERMALINK ) ) {
236        return;
237    }
238    if ( ! ( defined( 'AT_PROXIED_REQUEST' ) && AT_PROXIED_REQUEST ) ) {
239        wp_die(
240            esc_html__( 'You do not have permission to access this page.', 'wpcomsh' ),
241            '',
242            array(
243                'back_link' => true,
244                'response'  => 403,
245            )
246        );
247    } else {
248        add_action(
249            'admin_notices',
250            function () {
251                echo '<div class="notice notice-warning"><p>' . esc_html__( 'Proxied only: You can see this because you are proxied. Do not use this if you don\'t know why you are here.', 'wpcomsh' ) . '</p></div>';
252            }
253        );
254    }
255}
256add_action( 'load-options-permalink.php', 'wpcomsh_maybe_disable_permalink_page' );
257
258/**
259 * Restrict selectable files in the Media uploader by setting Plupload filters only.
260 * Minimal change: selection-only; no server-side mime policy changes.
261 *
262 * @param array $options Plupload options.
263 * @return array Plupload options with restricted mime types.
264 */
265function wpcomsh_plupload_file_restrictions( $options ) {
266    $mimes_map = get_allowed_mime_types();
267
268    $allowed_extensions = array();
269    $allowed_mime_types = array();
270
271    foreach ( $mimes_map as $ext_pattern => $mime ) {
272        // Extract real file extensions (filter out 'x-' prefixed ones)
273        // This prevents issues with non-standard extensions like 'x-wav' that aren't real file extensions
274        $extensions = explode( '|', $ext_pattern );
275        foreach ( $extensions as $ext ) {
276            // Only include extensions that don't start with 'x-'
277            if ( ! str_starts_with( $ext, 'x-' ) ) {
278                $allowed_extensions[] = $ext;
279            }
280        }
281
282        $allowed_mime_types[] = $mime;
283    }
284
285    if ( empty( $allowed_extensions ) ) {
286        return $options;
287    }
288
289    if ( ! isset( $options['filters'] ) || ! is_array( $options['filters'] ) ) {
290        $options['filters'] = array();
291    }
292    $options['filters']['mime_types'] = array(
293        array( 'extensions' => implode( ',', $allowed_extensions ) ),
294    );
295
296    // Store MIME types for potential use in HTML file inputs
297    $options['allowed_mime_types'] = $allowed_mime_types;
298
299    return $options;
300}
301
302add_action( 'plupload_init', 'wpcomsh_plupload_file_restrictions' );
303
304/**
305 * Restricts the allowed mime types if the site have does NOT have access to the required feature.
306 *
307 * @param array $mimes Mime types keyed by the file extension regex corresponding to those types.
308 * @return array Allowed mime types.
309 */
310function wpcomsh_maybe_restrict_mimetypes( $mimes ) {
311    $disallowed_mimes = array();
312    if ( ! wpcom_site_has_feature( WPCOM_Features::UPGRADED_UPLOAD_FILETYPES ) ) {
313        // Copied from WPCOM (see `WPCOM_UPLOAD_FILETYPES_FOR_UPGRADES` in `.config/wpcom-options.php`).
314        $upgraded_upload_filetypes = 'mp3 m4a wav ogg zip txt tiff bmp';
315        $disallowed_mimes          = array_merge( $disallowed_mimes, explode( ' ', $upgraded_upload_filetypes ) );
316    }
317
318    // Allow video uploads if site has either VIDEOPRESS or UPLOAD_VIDEO_FILES feature.
319    // Sites with UPLOAD_VIDEO_FILES can upload videos without VideoPress (e.g. Premium plans with gating-business-q1 sticker).
320    if ( ! wpcom_site_can_upload_videos() ) {
321        // Copied from WPCOM (see `WPCOM_UPLOAD_FILETYPES_FOR_VIDEOS` in `.config/wpcom-options.php`).
322        $video_upload_filetypes = 'ogv mp4 m4v mov wmv avi mpg 3gp 3g2';
323        $disallowed_mimes       = array_merge( $disallowed_mimes, explode( ' ', $video_upload_filetypes ) );
324    }
325
326    // TTML subtitles require VideoPress specifically (not just video upload capability).
327    if ( ! wpcom_site_has_feature( WPCOM_Features::VIDEOPRESS ) ) {
328        $disallowed_mimes[] = 'ttml';
329    }
330
331    foreach ( $disallowed_mimes as $disallowed_mime ) {
332        foreach ( $mimes as $ext_pattern => $mime ) {
333            if ( strpos( $ext_pattern, $disallowed_mime ) !== false ) {
334                unset( $mimes[ $ext_pattern ] );
335            }
336        }
337    }
338
339    return $mimes;
340}
341add_filter( 'upload_mimes', 'wpcomsh_maybe_restrict_mimetypes', PHP_INT_MAX );
342
343/**
344 * Redirect plugins.php and plugin-install.php to their Calypso counterparts if this site doesn't have the
345 * MANAGE_PLUGINS feature.
346 */
347function wpcomsh_maybe_redirect_to_calypso_plugin_pages() {
348    $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore
349    // Quick exit if on non-plugin page.
350    if ( false === strpos( $request_uri, '/wp-admin/plugin' ) ) {
351        return;
352    }
353
354    if ( wpcom_site_has_feature( WPCOM_Features::MANAGE_PLUGINS ) ) {
355        return;
356    }
357
358    if ( ! class_exists( 'Automattic\Jetpack\Status' ) ) {
359        return;
360    }
361
362    $site = ( new Automattic\Jetpack\Status() )->get_site_suffix();
363
364    // Redirect to calypso when user is trying to install plugin.
365    if ( 0 === strpos( $request_uri, '/wp-admin/plugin-install.php' ) ) {
366        wp_safe_redirect( 'https://wordpress.com/plugins/' . $site );
367        exit( 0 );
368    }
369}
370add_action( 'plugins_loaded', 'wpcomsh_maybe_redirect_to_calypso_plugin_pages' );
371
372/**
373 * This function manages the feature that allows the user to hide the "WP.com Footer Credit".
374 * The footer credit feature lives in a separate platform-agnostic repository, so we rely on filters to manage it.
375 * Pressable Footer Credit repository: https://github.com/Automattic/at-pressable-footer-credit
376 *
377 * @return bool
378 */
379function wpcomsh_gate_footer_credit_feature() {
380    return wpcom_site_has_feature( WPCOM_Features::NO_WPCOM_BRANDING );
381}
382add_filter( 'wpcom_better_footer_credit_can_customize', 'wpcomsh_gate_footer_credit_feature' );
383
384/**
385 * Controls whether Jetpack Forms integrations feature is enabled based on site plan.
386 *
387 * @return bool
388 */
389function wpcomsh_gate_jetpack_forms_integrations() {
390    return wpcom_site_has_feature( WPCOM_Features::FORM_INTEGRATIONS );
391}
392add_filter( 'jetpack_forms_is_integrations_enabled', 'wpcomsh_gate_jetpack_forms_integrations' );
393
394/**
395 * Remove the Jetpack > Dashboard menu if the site doesn't have the required feature.
396 */
397function wpcomsh_maybe_remove_jetpack_dashboard_menu_item() {
398    if ( wpcom_site_has_feature( WPCOM_Features::JETPACK_DASHBOARD ) ) {
399        return;
400    }
401
402    remove_submenu_page( 'jetpack', 'jetpack#/dashboard' );
403}
404add_action( 'admin_menu', 'wpcomsh_maybe_remove_jetpack_dashboard_menu_item', 1000 ); // Jetpack uses 998.
405
406/**
407 * Remove Jetpack > Manage menu item as part of the wpcom navigation redesign.
408 * For more context, see https://github.com/Automattic/dotcom-forge/issues/5824.
409 */
410function wpcomsh_remove_jetpack_manage_menu_item() {
411    if ( ! class_exists( 'Jetpack' ) || ! class_exists( 'Jetpack_Options' ) || get_option( 'wpcom_admin_interface' ) !== 'wp-admin' ) {
412        return;
413    }
414    $blog_id = Jetpack_Options::get_option( 'id' );
415    remove_submenu_page( 'jetpack', 'https://jetpack.com/redirect/?source=cloud-manage-dashboard-wp-menu&#038;site=' . $blog_id );
416}
417
418add_action( 'admin_menu', 'wpcomsh_remove_jetpack_manage_menu_item', 1001 ); // Automattic\Jetpack\Admin_UI\Admin_Menu uses 1000.