Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.17% covered (danger)
4.17%
8 / 192
4.17% covered (danger)
4.17%
1 / 24
CRAP
n/a
0 / 0
wpcomsh_is_managed_plugin
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_is_marketplace_plugin
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
132
wpcomsh_filter_marketplace_purchases_from_site_purchases
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_deactivate_plugin_cap
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
wpcomsh_managed_plugins_action_links
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
wpcomsh_hide_update_notice_for_managed_plugins
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
hide_vaultpress_from_plugin_list
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_hide_plugin_deactivate_edit_links
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_hide_plugin_remove_link
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_ensure_critical_plugins_active
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
wpcom_hide_jetpack_version_number
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
wpcomsh_show_plugin_auto_managed_notice
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_show_unmanaged_plugin_separator
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_maybe_remove_amp_incorrect_installation_notice
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_remove_vaultpress_wpadmin_notices
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_auto_update_new_plugins_by_default
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
462
wpcomsh_symlinked_plugins_url
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_atomic_managed_plugin_row_auto_update_label
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_atomic_managed_theme_template_auto_update_label
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_atomic_managed_plugin_auto_update_debug_label
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_atomic_managed_theme_auto_update_debug_label
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_remove_managed_plugins_from_update_plugins
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
wpcomsh_update_managed_plugins
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_handle_update_managed_plugins_list
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2/**
3 * Managed plugins file.
4 *
5 * @package wpcomsh
6 */
7
8/**
9 * Plugins that can't be deactivated.
10 */
11const WPCOM_CORE_ATOMIC_PLUGINS = array(
12    'jetpack/jetpack.php',
13    'akismet/akismet.php',
14);
15
16/**
17 * Plugins that can be deactivated.
18 */
19const WPCOM_FEATURE_PLUGINS = array(
20    'coblocks/class-coblocks.php',
21    'full-site-editing/full-site-editing-plugin.php',
22    'gutenberg/gutenberg.php',
23    'layout-grid/index.php',
24    'page-optimize/page-optimize.php',
25);
26
27/**
28 * Check if plugin is managed.
29 *
30 * @param mixed $plugin_file Name of plugin file.
31 *
32 * @return bool
33 */
34function wpcomsh_is_managed_plugin( $plugin_file ) {
35    if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC && class_exists( 'Atomic_Platform_Mu_Plugin' ) ) {
36        return Atomic_Platform_Mu_Plugin::is_managed_plugin( $plugin_file );
37    }
38
39    return false;
40}
41
42/**
43 * Checks if a plugin has been installed from the WP.com marketplace.
44 *
45 * @param string $plugin_file The plugin file name.
46 * @return bool Whether the plugin has a matching marketplace purchase.
47 */
48function wpcomsh_is_marketplace_plugin( $plugin_file ) {
49    if ( ! wpcomsh_is_managed_plugin( $plugin_file ) ) {
50        return false;
51    }
52
53    $persistent_data     = new Atomic_Persistent_Data();
54    $marketplace_plugins = array();
55
56    if ( ! empty( $persistent_data->WPCOM_MARKETPLACE ) ) { // phpcs:ignore WordPress.NamingConventions
57        $marketplace_software = json_decode( $persistent_data->WPCOM_MARKETPLACE, true ); // phpcs:ignore WordPress.NamingConventions
58
59        // If we don't have an array of marketplace plugins, this plugin can't be a marketplace plugin.
60        if ( ! isset( $marketplace_software['plugins'] ) || ! is_array( $marketplace_software['plugins'] ) || array() === $marketplace_software['plugins'] ) {
61            return false;
62        }
63
64        $marketplace_plugins = $marketplace_software['plugins'];
65    } else {
66        /*
67         * Some sites might have an empty `WPCOM_MARKETPLACE` field despite having software installed from
68         * the marketplace (mainly because this field has not been backfilled after its introduction).
69         *
70         * For those cases, we check against the purchases of a site as a fallback, but that only works for
71         * purchases of products with slugs that have not been shortened.
72         */
73        $marketplace_purchases = wpcomsh_filter_marketplace_purchases_from_site_purchases();
74
75        if ( empty( $marketplace_purchases ) ) {
76            return false;
77        }
78
79        foreach ( $marketplace_purchases as $marketplace_purchase ) {
80            $marketplace_plugins[] = preg_replace( array( '/(_monthly|_yearly)$/', '/_/' ), array( '', '-' ), $marketplace_purchase->product_slug );
81        }
82    }
83
84    foreach ( $marketplace_plugins as $marketplace_plugin ) {
85        if ( ( 0 === strpos( $plugin_file, $marketplace_plugin . '/' ) || 0 === strpos( $plugin_file, $marketplace_plugin . '.php' ) ) ) {
86            return true;
87        }
88    }
89
90    return false;
91}
92
93/**
94 * Filter marketplace product purchases from site purchases.
95 *
96 * @return array The filtered marketplace purchases.
97 */
98function wpcomsh_filter_marketplace_purchases_from_site_purchases() {
99    $site_purchases = wpcom_get_site_purchases();
100
101    return array_filter(
102        $site_purchases,
103        function ( $purchase ) {
104            return in_array( $purchase->product_type, array( 'marketplace_plugin', 'saas_plugin' ), true );
105        }
106    );
107}
108
109/**
110 * Disable the capability to deactivate the WPCOM_CORE_ATOMIC_PLUGINS.
111 *
112 * @param array  $caps    Array of required capabilities.
113 * @param string $cap     Capability name.
114 * @param int    $user_id The user ID.
115 * @param array  $args    Adds the context to the cap. For the purpose of this callback: Plugin to be deactivated.
116 * @return array Primitive caps.
117 */
118function wpcomsh_deactivate_plugin_cap( $caps, $cap, $user_id, $args ) {
119    if ( 'deactivate_plugin' === $cap && in_array( $args[0], WPCOM_CORE_ATOMIC_PLUGINS, true ) ) {
120        $caps[] = 'do_not_allow';
121    }
122
123    return $caps;
124}
125add_filter( 'map_meta_cap', 'wpcomsh_deactivate_plugin_cap', 10, 4 );
126
127/**
128 * Add managed plugins action links.
129 */
130function wpcomsh_managed_plugins_action_links() {
131    foreach ( WPCOM_CORE_ATOMIC_PLUGINS as $plugin ) {
132        if ( wpcomsh_is_managed_plugin( $plugin ) ) {
133            add_filter( 'plugin_action_links_' . $plugin, 'wpcomsh_hide_plugin_deactivate_edit_links' );
134            add_action( "after_plugin_row_{$plugin}", 'wpcomsh_show_plugin_auto_managed_notice', 10, 2 );
135        } else {
136            add_action( 'after_plugin_row_' . $plugin, 'wpcomsh_show_unmanaged_plugin_separator', PHP_INT_MAX );
137        }
138    }
139
140    foreach ( WPCOM_FEATURE_PLUGINS as $plugin ) {
141        if ( wpcomsh_is_managed_plugin( $plugin ) ) {
142            add_action( 'after_plugin_row_' . $plugin, 'wpcomsh_show_plugin_auto_managed_notice', 10, 2 );
143        } else {
144            add_action( 'after_plugin_row_' . $plugin, 'wpcomsh_show_unmanaged_plugin_separator', PHP_INT_MAX );
145        }
146    }
147
148    // Remove `delete` link for all managed plugins purchased from WordPress.com Marketplace.
149    $all_plugin_files = array_keys( get_plugins() );
150    foreach ( $all_plugin_files as $plugin_file ) {
151        if ( ! wpcomsh_is_marketplace_plugin( $plugin_file ) ) {
152            continue;
153        }
154
155        add_filter( 'plugin_action_links_' . $plugin_file, 'wpcomsh_hide_plugin_remove_link' );
156        add_action( 'after_plugin_row_' . $plugin_file, 'wpcomsh_show_plugin_auto_managed_notice', 10, 2 );
157    }
158}
159add_action( 'load-plugins.php', 'wpcomsh_managed_plugins_action_links' );
160
161/**
162 * Hide update notice for managed plugins.
163 */
164function wpcomsh_hide_update_notice_for_managed_plugins() {
165    $plugin_files = array_keys( get_plugins() );
166
167    foreach ( $plugin_files as $plugin ) {
168        if ( wpcomsh_is_managed_plugin( $plugin ) ) {
169            remove_action( 'after_plugin_row_' . $plugin, 'wp_plugin_update_row' );
170        }
171    }
172}
173add_action( 'load-plugins.php', 'wpcomsh_hide_update_notice_for_managed_plugins', 25 );
174
175/**
176 * Hide VaultPress from plugin list.
177 */
178function hide_vaultpress_from_plugin_list() {
179    global $wp_list_table;
180    unset( $wp_list_table->items['vaultpress/vaultpress.php'] );
181}
182add_action( 'pre_current_active_plugins', 'hide_vaultpress_from_plugin_list' );
183
184/**
185 * Hides must-use and drop-in plugins in Plugins list.
186 */
187add_filter( 'show_advanced_plugins', '__return_false' );
188
189/**
190 * Hide plugin deactivate edit links.
191 *
192 * @param mixed $links The nav links.
193 *
194 * @return array
195 */
196function wpcomsh_hide_plugin_deactivate_edit_links( $links ) {
197    if ( ! is_array( $links ) ) {
198        return array();
199    }
200
201    unset( $links['deactivate'] );
202    unset( $links['edit'] );
203
204    return $links;
205}
206
207/**
208 * Hide plugin removal link.
209 *
210 * @param mixed $links The nav links.
211 *
212 * @return array
213 */
214function wpcomsh_hide_plugin_remove_link( $links ) {
215    if ( ! is_array( $links ) ) {
216        return array();
217    }
218
219    unset( $links['delete'] );
220
221    return $links;
222}
223
224/**
225 * Ensure critical plugins (Jetpack and Akismet) remain active.
226 *
227 * @param mixed $value The new, unserialized option value.
228 * @return array The filtered array of active plugins.
229 */
230function wpcomsh_ensure_critical_plugins_active( $value ) {
231    if ( ! is_array( $value ) ) {
232        return $value;
233    }
234
235    // Add critical plugins if they're not in the list
236    foreach ( WPCOM_CORE_ATOMIC_PLUGINS as $critical_plugin ) {
237        if ( ! in_array( $critical_plugin, $value, true ) ) {
238            $value[] = $critical_plugin;
239        }
240    }
241
242    return $value;
243}
244add_filter( 'pre_update_option_active_plugins', 'wpcomsh_ensure_critical_plugins_active', 10, 2 );
245
246/**
247 * Hide the Jetpack version number from the plugin list.
248 * That version is managed by the Atomic platform.
249 *
250 * @param string[] $plugin_meta An array of the plugin's metadata, including
251 *                              the version, author, author URI, and plugin URI.
252 * @param string   $plugin_file Path to the plugin file relative to the plugins directory.
253 * @param array    $plugin_data An array of plugin data.
254 *
255 * @return string[]
256 */
257function wpcom_hide_jetpack_version_number( $plugin_meta, $plugin_file, $plugin_data ) {
258    if (
259        is_array( $plugin_meta )
260        && isset( $plugin_data['slug'] )
261        && isset( $plugin_data['Version'] )
262        && 'jetpack' === $plugin_data['slug']
263        && false !== strpos( $plugin_meta[0], $plugin_data['Version'] )
264    ) {
265        unset( $plugin_meta[0] );
266    }
267
268    return $plugin_meta;
269}
270add_filter( 'plugin_row_meta', 'wpcom_hide_jetpack_version_number', 10, 3 );
271
272/**
273 * Show plugin auto managed notice.
274 *
275 * @param string $file        The plugin file.
276 * @param array  $plugin_data The plugin data.
277 */
278function wpcomsh_show_plugin_auto_managed_notice( $file, $plugin_data ) {
279    $plugin_name = 'The plugin';
280    $active      = is_plugin_active( $file ) ? ' active' : '';
281
282    if ( ! empty( $plugin_data['Name'] ) ) {
283        $plugin_name = $plugin_data['Name'];
284    }
285
286    /* translators: %s: plugin name */
287    $message = sprintf( __( '%s is automatically managed for you.', 'wpcomsh' ), $plugin_name );
288
289    if ( in_array( $file, WPCOM_FEATURE_PLUGINS, true ) ) {
290        $message = __( 'This plugin was installed by WordPress.com and provides features offered in your plan subscription.', 'wpcomsh' );
291    }
292
293    echo '<tr class="plugin-update-tr' . esc_attr( $active ) . '">' .
294            '<td colspan="4" class="plugin-update colspanchange">' .
295                '<div class="notice inline notice-success notice-alt">' .
296                    '<p>' . esc_html( $message ) . '</p>' .
297                '</div>' .
298            '</td>' .
299        '</tr>';
300}
301
302/**
303 * Renders a separator row for plugins that are managed by WordPress.com but the user has currently
304 * removed it and added an unmanaged version.
305 *
306 * @param string $file Plugin file name.
307 */
308function wpcomsh_show_unmanaged_plugin_separator( $file ) {
309    $active = is_plugin_active( $file ) ? 'active' : '';
310
311    printf(
312        '<tr class="%s"><th colspan="4" scope="row" class="check-column"></th></tr>',
313        esc_attr( $active )
314    );
315}
316
317/**
318 * The AMP plugin displays an error message in the dashboard when
319 * it's installed in the wrong directory (i.e. not `amp`).
320 *
321 * When the plugin is managed by us, the AMP plugin incorrectly thinks it's
322 * been installed in the wrong directory due to symlinking. So we disable
323 * the error message when the installation directory is correct and managed
324 * by us.
325 *
326 * However, some users might do it wrong and that could affect their
327 * ability to have the plugin updated automatically.
328 * We should keep the warning if that's the case.
329 *
330 * See https://github.com/Automattic/wp-calypso/issues/64104.
331 */
332function wpcomsh_maybe_remove_amp_incorrect_installation_notice() {
333    if ( wpcomsh_is_managed_plugin( 'amp/amp.php' ) ) {
334        remove_action( 'admin_notices', '_amp_incorrect_plugin_slug_admin_notice' );
335    }
336}
337add_action( 'admin_head', 'wpcomsh_maybe_remove_amp_incorrect_installation_notice' );
338
339/**
340 * Remove VaultPress WP Admin notices.
341 */
342function wpcomsh_remove_vaultpress_wpadmin_notices() {
343    if ( ! class_exists( 'VaultPress' ) ) {
344        return;
345    }
346
347    $vp_instance = VaultPress::init();
348
349    remove_action( 'user_admin_notices', array( $vp_instance, 'activated_notice' ) );
350    remove_action( 'admin_notices', array( $vp_instance, 'activated_notice' ) );
351
352    remove_action( 'user_admin_notices', array( $vp_instance, 'connect_notice' ) );
353    remove_action( 'admin_notices', array( $vp_instance, 'connect_notice' ) );
354
355    remove_action( 'user_admin_notices', array( $vp_instance, 'error_notice' ) );
356    remove_action( 'admin_notices', array( $vp_instance, 'error_notice' ) );
357}
358add_action( 'admin_head', 'wpcomsh_remove_vaultpress_wpadmin_notices', 11 ); // Priority 11 so it runs after VaultPress `admin_head` hook.
359
360/**
361 * Disables a Yoast notification that displays when an outdated version of the Gutenberg plugin is installed.
362 */
363if ( wpcomsh_is_managed_plugin( 'gutenberg/gutenberg.php' ) ) {
364    add_filter( 'yoast_display_gutenberg_compat_notification', '__return_false' );
365}
366
367/**
368 * Detects new plugins and defaults them to be auto-updated.
369 *
370 * This is a pre-option filter for the auto_update_plugins option. Its purpose
371 * is to default newly added plugins to being auto-updated. After that, if users
372 * want to disable auto-updates for those plugins, they can.
373 *
374 * @param mixed $pre_auto_update_plugins Pre auto update plugins.
375 *
376 * @return bool
377 */
378function wpcomsh_auto_update_new_plugins_by_default( $pre_auto_update_plugins ) {
379    /*
380     * Don't interfere with Jetpack XMLRPC API requests (e.g., plugin installation from Calypso).
381     *
382     * The Jetpack API endpoint handles auto_update_plugins updates via update_site_option().
383     * If this filter modifies the value, it can interfere with WordPress's old vs new value
384     * comparison, causing the update to be skipped and breaking Jetpack sync.
385     *
386     * By returning early, we let Jetpack handle XMLRPC requests without interference.
387     */
388    if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
389        return $pre_auto_update_plugins;
390    }
391
392    // Listing plugins is a costly operation, so we only want to do this under certain circumstances.
393    $look_for_new_plugins = false;
394
395    // We'd like admin operations via WP-CLI to have the latest auto-updated plugins list.
396    if ( defined( 'WP_CLI' ) && WP_CLI ) {
397        $look_for_new_plugins = true;
398    }
399
400    /*
401     * Is Core doing update-related things?
402     *
403     * @see https://github.com/WordPress/wordpress-develop/blob/98c9ab835e9e1e2195d336fa0ef913debb76edca/src/wp-includes/update.php#L966
404     */
405    if (
406        doing_action( 'load-plugins.php' ) ||
407        doing_action( 'load-update.php' ) ||
408        doing_action( 'load-update-core.php' ) ||
409        doing_action( 'wp_update_plugins' )
410    ) {
411        $look_for_new_plugins = true;
412    }
413
414    if ( ! $look_for_new_plugins ) {
415        return $pre_auto_update_plugins;
416    }
417
418    /*
419     * Remove this pre_option filter immediately because it:
420     * - calls get_option for the same option and will otherwise infinitely recurse.
421     * - updates auto_update_plugins on-demand an only needs to run once.
422     */
423    $filter_removed = remove_filter( 'pre_option_auto_update_plugins', __FUNCTION__ );
424    if ( ! $filter_removed ) {
425        // Return immediately because it's not safe to continue.
426        return $pre_auto_update_plugins;
427    }
428
429    $baseline_plugins_list = get_option( 'wpcomsh_plugins_considered_for_auto_update' );
430    if ( ! function_exists( 'get_plugins' ) ) {
431        require_once ABSPATH . 'wp-admin/includes/plugin.php';
432    }
433
434    $fresh_plugins_list  = array_keys( get_plugins() );
435    $auto_update_plugins = get_option( 'auto_update_plugins', array() );
436
437    $skip_new_plugins = false;
438
439    if ( false === $baseline_plugins_list && ! empty( $auto_update_plugins ) ) {
440        /*
441         * We don't yet have a baseline plugin list, so we can't identify new plugins.
442         *
443         * Since the site already has a non-empty auto_update_plugins option, let's assume it matches the admin's
444         * intention and leave it as-is. This should be the first and only time we are missing a baseline plugin list.
445         * Plugins added in the future should be auto-updated by default.
446         */
447        $skip_new_plugins = true;
448    }
449
450    if ( false === $baseline_plugins_list ) {
451        $baseline_plugins_list = array();
452    }
453
454    $new_unmanaged_plugins = array();
455
456    if ( ! $skip_new_plugins ) {
457        $new_plugins = array_diff( $fresh_plugins_list, $baseline_plugins_list );
458        foreach ( $new_plugins as $new_plugin ) {
459            if ( ! wpcomsh_is_managed_plugin( $new_plugin ) ) {
460                $new_unmanaged_plugins[] = $new_plugin;
461            }
462        }
463    }
464
465    if ( is_array( $auto_update_plugins ) && ! empty( $new_unmanaged_plugins ) ) {
466        $auto_update_plugins = array_unique( array_merge( $auto_update_plugins, $new_unmanaged_plugins ) );
467        update_option( 'auto_update_plugins', $auto_update_plugins );
468    }
469
470    if ( $baseline_plugins_list != $fresh_plugins_list ) { //phpcs:ignore
471        update_option( 'wpcomsh_plugins_considered_for_auto_update', $fresh_plugins_list, false );
472    }
473
474    return $auto_update_plugins;
475}
476add_filter( 'pre_option_auto_update_plugins', 'wpcomsh_auto_update_new_plugins_by_default' );
477
478/**
479 * Filter plugins_url for when __FILE__ is outside of WP_CONTENT_DIR
480 *
481 * @param string $url    The complete URL to the plugins directory including scheme and path.
482 * @param string $path   Path relative to the URL to the plugins directory. Blank string
483 *                       if no path is specified.
484 * @param string $plugin The plugin file path to be relative to. Blank string if no plugin
485 *                       is specified.
486 * @return string Filtered URL.
487 */
488function wpcomsh_symlinked_plugins_url( $url, $path, $plugin ) {
489    $url = preg_replace(
490        '#((?<!/)/[^/]+)*/wp-content/plugins/wordpress/plugins/wpcomsh/([^/]+)/#',
491        '/wp-content/mu-plugins/wpcomsh/',
492        $url
493    );
494
495    if ( 'woocommerce-product-addons.php' === $plugin || 'woocommerce-gateway-stripe.php' === $plugin ) {
496        $url = home_url( '/wp-content/plugins/' . basename( $plugin, '.php' ) );
497    }
498
499    return $url;
500}
501add_filter( 'plugins_url', 'wpcomsh_symlinked_plugins_url', 0, 3 );
502
503/**
504 * Get atomic managed plugin row auto update label
505 *
506 * @return string
507 */
508function wpcomsh_atomic_managed_plugin_row_auto_update_label() {
509    /* translators: Message about how a managed plugin is updated. */
510    return __( 'Updates managed by WordPress.com', 'wpcomsh' );
511}
512add_filter( 'atomic_managed_plugin_row_auto_update_label', 'wpcomsh_atomic_managed_plugin_row_auto_update_label' );
513
514/**
515 * Get atomic managed theme template auto update label
516 *
517 * @return string
518 */
519function wpcomsh_atomic_managed_theme_template_auto_update_label() {
520    /* translators: Message about how a managed theme is updated. */
521    return __( 'Updates managed by WordPress.com', 'wpcomsh' );
522}
523add_filter( 'atomic_managed_theme_template_auto_update_label', 'wpcomsh_atomic_managed_theme_template_auto_update_label' );
524
525/**
526 * Get atomic managed plugin auto update debug label
527 *
528 * @return string
529 */
530function wpcomsh_atomic_managed_plugin_auto_update_debug_label() {
531    /* translators: Information about how a managed plugin is updated, for debugging purposes. */
532    return __( 'Updates managed by WordPress.com', 'wpcomsh' );
533}
534add_filter( 'atomic_managed_plugin_auto_update_debug_label', 'wpcomsh_atomic_managed_plugin_auto_update_debug_label' );
535
536/**
537 * Get atomic managed theme auto update debug label
538 *
539 * @return string
540 */
541function wpcomsh_atomic_managed_theme_auto_update_debug_label() {
542    /* translators: Information about how a managed theme is updated, for debugging purposes. */
543    return __( 'Updates managed by WordPress.com', 'wpcomsh' );
544}
545add_filter( 'atomic_managed_theme_auto_update_debug_label', 'wpcomsh_atomic_managed_theme_auto_update_debug_label' );
546
547/**
548 * Filter to exclude the managed plugin from the list of plugins to update.
549 * It overrides the site transient update_plugins.
550 *
551 * @param mixed $current Transient object with the list of plugins to update.
552 * @return mixed
553 */
554function wpcomsh_remove_managed_plugins_from_update_plugins( $current ) {
555    if ( is_object( $current ) && isset( $current->response ) && is_array( $current->response ) ) {
556        foreach ( array_keys( $current->response ) as $plugin_key ) {
557            if ( wpcomsh_is_managed_plugin( $plugin_key ) ) {
558                unset( $current->response[ $plugin_key ] );
559            }
560        }
561    }
562    return $current;
563}
564
565add_filter( 'site_transient_update_plugins', 'wpcomsh_remove_managed_plugins_from_update_plugins' );
566
567/**
568 * Save the list of managed plugins to an option after the active plugins are updated.
569 * This is used to determine if a plugin is symlinked and should be auto-updated.
570 *
571 * @return void
572 */
573function wpcomsh_update_managed_plugins(): void {
574    if ( ! function_exists( 'get_plugins' ) ) {
575        require_once ABSPATH . 'wp-admin/includes/plugin.php';
576    }
577
578    if ( ! function_exists( 'wpcomsh_is_managed_plugin' ) ) {
579        return;
580    }
581
582    $plugin_files    = array_keys( get_plugins() );
583    $managed_plugins = array_filter( $plugin_files, 'wpcomsh_is_managed_plugin' );
584
585    update_option( 'wpcomsh_at_managed_plugins', $managed_plugins );
586}
587add_action( 'deleted_plugin', 'wpcomsh_update_managed_plugins', 100 );
588
589/**
590 * Update the list of managed plugins after a plugin is installed or updated.
591 * We only update the list if the managed plugins list is not already set for the update action.
592 *
593 * @param WP_Upgrader $upgrader The upgrader object.
594 * @param array       $hook_extra Extra arguments passed to hooked filters.
595 * @return void
596 */
597function wpcomsh_handle_update_managed_plugins_list( $upgrader, $hook_extra ): void {
598    $is_plugin_operation = isset( $hook_extra['type'] ) && 'plugin' === $hook_extra['type'];
599    $is_valid_action     = isset( $hook_extra['action'] ) && in_array( $hook_extra['action'], array( 'install', 'update' ), true );
600    $is_update_action    = isset( $hook_extra['action'] ) && 'update' === $hook_extra['action'];
601    $has_managed_plugins = get_option( 'wpcomsh_at_managed_plugins', false );
602
603    if ( $is_plugin_operation && $is_valid_action ) {
604        // If the plugin is being updated and the managed plugins list is already set, don't update it.
605        if ( $is_update_action && $has_managed_plugins ) {
606            return;
607        }
608
609        wpcomsh_update_managed_plugins();
610    }
611}
612add_action( 'upgrader_process_complete', 'wpcomsh_handle_update_managed_plugins_list', 50, 2 );