Ticket #53255: 53255.3.diff
| File 53255.3.diff, 14.2 KB (added by , 7 weeks ago) |
|---|
-
src/wp-admin/includes/class-wp-plugins-list-table.php
diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php index c4866076f9..4fdcdfb535 100644
a b class WP_Plugins_List_Table extends WP_List_Table { 24 24 */ 25 25 protected $show_autoupdates = true; 26 26 27 /** 28 * Map of plugin file to the number of individual sites it is active on. 29 * 30 * Null until first accessed; populated lazily by get_subsite_active_plugin_counts(). 31 * 32 * @since 6.8.0 33 * 34 * @var array<string,int>|null 35 */ 36 protected $subsite_active_counts = null; 37 27 38 /** 28 39 * Constructor. 29 40 * … … class WP_Plugins_List_Table extends WP_List_Table { 729 740 } 730 741 } 731 742 743 /** 744 * Returns a map of plugin file paths to the number of individual (non-network) sites 745 * they are active on. Computed once per page load and cached in $subsite_active_counts. 746 * 747 * @since 6.8.0 748 * 749 * @return array<string,int> Plugin file => site count. Empty array on non-multisite. 750 */ 751 protected function get_subsite_active_plugin_counts() { 752 if ( null !== $this->subsite_active_counts ) { 753 return $this->subsite_active_counts; 754 } 755 756 $this->subsite_active_counts = array(); 757 758 if ( ! is_multisite() ) { 759 return $this->subsite_active_counts; 760 } 761 762 /** 763 * Filters the maximum number of sites queried when checking whether a plugin 764 * is active on any individual site before showing the delete action in the 765 * Network Admin plugins list. Defaults to 100 to avoid unbounded queries on 766 * very large networks. Increase the limit (or set to 0 for no limit) when 767 * accuracy across all sites is required. 768 * 769 * @since 6.8.0 770 * 771 * @param int $sites_query_limit Maximum number of sites to query. Default 100. 772 */ 773 $sites_query_limit = apply_filters( 'delete_plugin_subsite_check_limit', 100 ); 774 775 $sites = get_sites( 776 array( 777 'fields' => 'ids', 778 'number' => $sites_query_limit, 779 ) 780 ); 781 782 foreach ( $sites as $site_id ) { 783 $site_active_plugins = (array) get_blog_option( $site_id, 'active_plugins', array() ); 784 foreach ( $site_active_plugins as $active_plugin_file ) { 785 if ( ! isset( $this->subsite_active_counts[ $active_plugin_file ] ) ) { 786 $this->subsite_active_counts[ $active_plugin_file ] = 0; 787 } 788 ++$this->subsite_active_counts[ $active_plugin_file ]; 789 } 790 } 791 792 return $this->subsite_active_counts; 793 } 794 732 795 /** 733 796 * @global string $status 734 797 * @global int $page … … class WP_Plugins_List_Table extends WP_List_Table { 878 941 } 879 942 } 880 943 881 if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) { 882 if ( $has_dependents && ! $has_circular_dependency ) { 883 $actions['delete'] = __( 'Delete' ) . 884 '<span class="screen-reader-text">' . 885 __( 'You cannot delete this plugin as other plugins require it.' ) . 886 '</span>'; 887 } else { 888 $delete_url = 'plugins.php?action=delete-selected' . 889 '&checked[]=' . urlencode( $plugin_file ) . 890 '&plugin_status=' . $context . 891 '&paged=' . $page . 892 '&s=' . $s; 893 944 if ( current_user_can( 'delete_plugins' ) ) { 945 $subsite_counts = $this->get_subsite_active_plugin_counts(); 946 $active_site_count = $subsite_counts[ $plugin_file ] ?? 0; 947 948 if ( $active_site_count > 0 ) { 949 /* 950 * Show a non-linked warning whenever the plugin is active on 951 * any individual site — including the main site. This check is 952 * intentionally placed before the is_plugin_active() guard so 953 * that a plugin active on the main site *and* subsites still 954 * shows the warning rather than a live delete link. 955 */ 894 956 $actions['delete'] = sprintf( 895 '<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>', 896 wp_nonce_url( $delete_url, 'bulk-plugins' ), 897 esc_attr( $plugin_id_attr ), 898 /* translators: %s: Plugin name. */ 899 esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ), 900 __( 'Delete' ) 957 '<span aria-label="%1$s">%2$s</span>', 958 esc_attr( 959 sprintf( 960 /* translators: 1: Plugin name. 2: Number of sites the plugin is active on. */ 961 _n( 962 'Cannot delete %1$s. It is active on %2$d site.', 963 'Cannot delete %1$s. It is active on %2$d sites.', 964 $active_site_count 965 ), 966 $plugin_data['Name'], 967 $active_site_count 968 ) 969 ), 970 sprintf( 971 /* translators: %d: Number of sites the plugin is active on. */ 972 _n( 'Active on %d site', 'Active on %d sites', $active_site_count ), 973 $active_site_count 974 ) 901 975 ); 976 } elseif ( ! is_plugin_active( $plugin_file ) ) { 977 if ( $has_dependents && ! $has_circular_dependency ) { 978 $actions['delete'] = __( 'Delete' ) . 979 '<span class="screen-reader-text">' . 980 __( 'You cannot delete this plugin as other plugins require it.' ) . 981 '</span>'; 982 } else { 983 $delete_url = 'plugins.php?action=delete-selected' . 984 '&checked[]=' . urlencode( $plugin_file ) . 985 '&plugin_status=' . $context . 986 '&paged=' . $page . 987 '&s=' . $s; 988 989 $actions['delete'] = sprintf( 990 '<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>', 991 wp_nonce_url( $delete_url, 'bulk-plugins' ), 992 esc_attr( $plugin_id_attr ), 993 /* translators: %s: Plugin name. */ 994 esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ), 995 __( 'Delete' ) 996 ); 997 } 902 998 } 903 999 } 904 1000 } … … class WP_Plugins_List_Table extends WP_List_Table { 1279 1375 1280 1376 echo '</div>'; 1281 1377 1282 if ( $has_dependents ) { 1378 $row_active_site_count = $screen->in_admin( 'network' ) 1379 ? ( $this->get_subsite_active_plugin_counts()[ $plugin_file ] ?? 0 ) 1380 : 0; 1381 1382 if ( $has_dependents && $row_active_site_count > 0 ) { 1383 // Both: dependents note with the subsite sentence appended. 1384 $this->add_dependents_to_dependency_plugin_row( $plugin_file, $row_active_site_count ); 1385 } elseif ( $has_dependents ) { 1283 1386 $this->add_dependents_to_dependency_plugin_row( $plugin_file ); 1387 } elseif ( $row_active_site_count > 0 ) { 1388 $this->add_subsite_active_note_to_plugin_row( $row_active_site_count ); 1284 1389 } 1285 1390 1286 1391 if ( WP_Plugin_Dependencies::has_dependencies( $plugin_file ) ) { … … class WP_Plugins_List_Table extends WP_List_Table { 1547 1652 * 1548 1653 * @param string $dependency The dependency's filepath, relative to the plugins directory. 1549 1654 */ 1550 protected function add_dependents_to_dependency_plugin_row( $dependency ) { 1655 /** 1656 * @since 6.5.0 1657 * @since 6.8.0 Added the `$active_site_count` parameter to optionally append a 1658 * subsite-active sentence to the existing note. 1659 * 1660 * @param string $dependency The dependency plugin's filepath, relative to the plugins directory. 1661 * @param int $active_site_count Number of individual sites the plugin is active on. When > 0, 1662 * a sentence is appended to the note. Default 0. 1663 */ 1664 protected function add_dependents_to_dependency_plugin_row( $dependency, $active_site_count = 0 ) { 1551 1665 $dependent_names = WP_Plugin_Dependencies::get_dependent_names( $dependency ); 1552 1666 1553 1667 if ( empty( $dependent_names ) ) { … … class WP_Plugins_List_Table extends WP_List_Table { 1556 1670 1557 1671 $dependency_note = __( 'Note: This plugin cannot be deactivated or deleted until the plugins that require it are deactivated or deleted.' ); 1558 1672 1673 if ( $active_site_count > 0 ) { 1674 $dependency_note .= ' ' . sprintf( 1675 /* translators: %d: Number of sites the plugin is active on. */ 1676 _n( 1677 'It is also active on %d site.', 1678 'It is also active on %d sites.', 1679 $active_site_count 1680 ), 1681 $active_site_count 1682 ); 1683 } 1684 1559 1685 $comma = wp_get_list_item_separator(); 1560 1686 $required_by = sprintf( 1561 1687 /* translators: %s: List of dependencies. */ … … class WP_Plugins_List_Table extends WP_List_Table { 1570 1696 ); 1571 1697 } 1572 1698 1699 /** 1700 * Prints a note that the plugin cannot be deleted because it is active on individual sites. 1701 * Uses the same HTML structure as {@see add_dependents_to_dependency_plugin_row()}. 1702 * 1703 * @since 6.8.0 1704 * 1705 * @param int $active_site_count Number of individual sites the plugin is active on. 1706 */ 1707 protected function add_subsite_active_note_to_plugin_row( $active_site_count ) { 1708 $note = sprintf( 1709 /* translators: %d: Number of sites the plugin is active on. */ 1710 _n( 1711 'Note: This plugin cannot be deleted because it is active on %d site.', 1712 'Note: This plugin cannot be deleted because it is active on %d sites.', 1713 $active_site_count 1714 ), 1715 $active_site_count 1716 ); 1717 1718 printf( '<div class="required-by"><p>%s</p></div>', $note ); 1719 } 1720 1573 1721 /** 1574 1722 * Prints a list of other plugins that the plugin depends on. 1575 1723 * -
src/wp-admin/includes/plugin.php
diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 94d4a585a8..94f80cbf2e 100644
a b function delete_plugins( $plugins, $deprecated = '' ) { 908 908 return false; 909 909 } 910 910 911 /* 912 * In Multisite, do not delete a plugin that is still active on any individual 913 * site. The check was dropped in 4.6 (changeset 37714 / Shiny Updates) when 914 * the filesystem workflow was restructured, but the protection is still needed. 915 * 916 * @see https://core.trac.wordpress.org/ticket/53255 917 */ 918 if ( is_multisite() ) { 919 $active_on_sites = array(); 920 921 $sites = get_sites( 922 array( 923 'fields' => 'ids', 924 'number' => 0, 925 ) 926 ); 927 928 foreach ( $sites as $site_id ) { 929 $site_active_plugins = (array) get_blog_option( $site_id, 'active_plugins', array() ); 930 foreach ( $plugins as $plugin_file ) { 931 if ( in_array( $plugin_file, $site_active_plugins, true ) ) { 932 $active_on_sites[ $plugin_file ][] = $site_id; 933 } 934 } 935 } 936 937 if ( ! empty( $active_on_sites ) ) { 938 return new WP_Error( 939 'active_plugin', 940 __( 'Cannot delete a plugin while it is active on a site.' ), 941 $active_on_sites 942 ); 943 } 944 } 945 911 946 $checked = array(); 912 947 foreach ( $plugins as $plugin ) { 913 948 $checked[] = 'checked[]=' . $plugin; -
src/wp-admin/plugins.php
diff --git a/src/wp-admin/plugins.php b/src/wp-admin/plugins.php index 6a359c822f..068209fd8f 100644
a b if ( $action ) { 397 397 398 398 ?> 399 399 </p> 400 <?php if ( is_network_admin() ) : ?> 401 <p><?php _e( 'This plugin may be active on individual sites in the network.', 'default' ); ?></p> 402 <?php endif; ?> 400 403 <form method="post" action="<?php echo esc_url( $_SERVER['REQUEST_URI'] ); ?>" style="display:inline;"> 401 404 <input type="hidden" name="verify-delete" value="1" /> 402 405 <input type="hidden" name="action" value="delete-selected" /> -
tests/phpunit/tests/admin/includesPlugin.php
diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index 4d47fb7db0..466c223ede 100644
a b class Tests_Admin_IncludesPlugin extends WP_UnitTestCase { 739 739 rmdir( $di_bu_dir ); 740 740 } 741 741 } 742 743 /** 744 * Tests that delete_plugins() returns a WP_Error when the plugin is active on a subsite. 745 * 746 * @ticket 53255 747 * @group ms-required 748 * 749 * @covers ::delete_plugins 750 */ 751 public function test_delete_plugins_returns_wp_error_when_plugin_active_on_subsite() { 752 $plugin = $this->_create_plugin(); 753 $site_id = self::factory()->blog->create(); 754 755 update_blog_option( $site_id, 'active_plugins', array( $plugin[0] ) ); 756 757 $result = delete_plugins( array( $plugin[0] ) ); 758 759 // Clean up. 760 update_blog_option( $site_id, 'active_plugins', array() ); 761 if ( file_exists( $plugin[1] ) ) { 762 unlink( $plugin[1] ); 763 } 764 765 $this->assertWPError( $result ); 766 $this->assertSame( 'active_plugin', $result->get_error_code() ); 767 $this->assertArrayHasKey( $plugin[0], $result->get_error_data() ); 768 $this->assertContains( $site_id, $result->get_error_data()[ $plugin[0] ] ); 769 } 770 771 /** 772 * Tests that delete_plugins() does not block deletion when the plugin is not active on any subsite. 773 * 774 * @ticket 53255 775 * @group ms-required 776 * 777 * @covers ::delete_plugins 778 */ 779 public function test_delete_plugins_does_not_block_when_plugin_not_active_on_any_subsite() { 780 $plugin = $this->_create_plugin(); 781 782 $result = delete_plugins( array( $plugin[0] ) ); 783 784 // Clean up if the file was not removed (e.g. no filesystem credentials in test env). 785 if ( file_exists( $plugin[1] ) ) { 786 unlink( $plugin[1] ); 787 } 788 789 // The function must not return the active_plugin error. 790 if ( is_wp_error( $result ) ) { 791 $this->assertNotSame( 792 'active_plugin', 793 $result->get_error_code(), 794 'delete_plugins() should not block deletion of an inactive plugin.' 795 ); 796 } else { 797 // null (no credentials) or true (deleted) are both acceptable. 798 $this->assertNotFalse( $result ); 799 } 800 } 801 802 /** 803 * Tests that delete_plugins() returns a WP_Error listing all affected plugins and sites. 804 * 805 * @ticket 53255 806 * @group ms-required 807 * 808 * @covers ::delete_plugins 809 */ 810 public function test_delete_plugins_error_data_contains_all_active_plugins() { 811 $plugin_a = $this->_create_plugin( "<?php\n/*\nPlugin Name: Test A\n*/" ); 812 $plugin_b = $this->_create_plugin( "<?php\n/*\nPlugin Name: Test B\n*/" ); 813 $site_id = self::factory()->blog->create(); 814 815 update_blog_option( $site_id, 'active_plugins', array( $plugin_a[0], $plugin_b[0] ) ); 816 817 $result = delete_plugins( array( $plugin_a[0], $plugin_b[0] ) ); 818 819 // Clean up. 820 update_blog_option( $site_id, 'active_plugins', array() ); 821 foreach ( array( $plugin_a[1], $plugin_b[1] ) as $file ) { 822 if ( file_exists( $file ) ) { 823 unlink( $file ); 824 } 825 } 826 827 $this->assertWPError( $result ); 828 $this->assertSame( 'active_plugin', $result->get_error_code() ); 829 830 $data = $result->get_error_data(); 831 $this->assertArrayHasKey( $plugin_a[0], $data, 'Error data should include plugin A.' ); 832 $this->assertArrayHasKey( $plugin_b[0], $data, 'Error data should include plugin B.' ); 833 } 742 834 }
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)