Ticket #46130: 46130.2.diff
File 46130.2.diff, 87.5 KB (added by , 6 years ago) |
---|
-
src/wp-admin/css/list-tables.css
diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index 87cc3f2f8a..91b1a2c9ce 100644
a b ul.cat-checklist { 1310 1310 text-decoration: underline; 1311 1311 } 1312 1312 1313 .plugins tr.paused th.check-column { 1314 border-left: 4px solid #d54e21; 1315 } 1316 1317 .plugins tr.paused th, 1318 .plugins tr.paused td { 1319 background-color: #fef7f1; 1320 } 1321 1322 .plugins tr.paused .plugin-title, 1323 .plugins .paused .dashicons-warning { 1324 color: #dc3232; 1325 } 1326 1327 .plugins .paused .error-display p, 1328 .plugins .paused .error-display code { 1329 font-size: 90%; 1330 font-style: italic; 1331 color: rgb( 0, 0, 0, 0.7 ); 1332 } 1333 1334 .plugins .resume-link { 1335 color: #dc3232; 1336 } 1337 1313 1338 .plugin-card .update-now:before { 1314 1339 color: #f56e28; 1315 1340 content: "\f463"; -
src/wp-admin/includes/admin-filters.php
diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index 4da6469d5f..3407e93171 100644
a b add_action( 'load-plugins.php', 'wp_plugin_update_rows', 20 ); // After wp_updat 117 117 add_action( 'load-themes.php', 'wp_theme_update_rows', 20 ); // After wp_update_themes() is called. 118 118 119 119 add_action( 'admin_notices', 'update_nag', 3 ); 120 add_action( 'admin_notices', 'paused_plugins_notice', 5 ); 121 add_action( 'admin_notices', 'paused_themes_notice', 5 ); 120 122 add_action( 'admin_notices', 'maintenance_nag', 10 ); 121 123 122 124 add_filter( 'update_footer', 'core_update_footer' ); -
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 fa40ba9a36..6ddae381bd 100644
a b class WP_Plugins_List_Table extends WP_List_Table { 40 40 ); 41 41 42 42 $status = 'all'; 43 if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search' ) ) ) {43 if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused' ) ) ) { 44 44 $status = $_REQUEST['plugin_status']; 45 45 } 46 46 … … class WP_Plugins_List_Table extends WP_List_Table { 99 99 'upgrade' => array(), 100 100 'mustuse' => array(), 101 101 'dropins' => array(), 102 'paused' => array(), 102 103 ); 103 104 104 105 $screen = $this->screen; … … class WP_Plugins_List_Table extends WP_List_Table { 209 210 if ( $show_network_active ) { 210 211 // On the non-network screen, show network-active plugins if allowed 211 212 $plugins['active'][ $plugin_file ] = $plugin_data; 213 if ( is_plugin_paused( $plugin_file ) ) { 214 $plugins['paused'][ $plugin_file ] = $plugin_data; 215 } 212 216 } else { 213 217 // On the non-network screen, filter out network-active plugins 214 218 unset( $plugins['all'][ $plugin_file ] ); … … class WP_Plugins_List_Table extends WP_List_Table { 218 222 // On the non-network screen, populate the active list with plugins that are individually activated 219 223 // On the network-admin screen, populate the active list with plugins that are network activated 220 224 $plugins['active'][ $plugin_file ] = $plugin_data; 225 if ( is_plugin_paused( $plugin_file ) ) { 226 $plugins['paused'][ $plugin_file ] = $plugin_data; 227 } 221 228 } else { 222 229 if ( isset( $recently_activated[ $plugin_file ] ) ) { 223 230 // Populate the recently activated list with plugins that have been recently activated … … class WP_Plugins_List_Table extends WP_List_Table { 445 452 /* translators: %s: plugin count */ 446 453 $text = _n( 'Drop-ins <span class="count">(%s)</span>', 'Drop-ins <span class="count">(%s)</span>', $count ); 447 454 break; 455 case 'paused': 456 /* translators: %s: plugin count */ 457 $text = _n( 'Paused <span class="count">(%s)</span>', 'Paused <span class="count">(%s)</span>', $count ); 458 break; 448 459 case 'upgrade': 449 460 /* translators: %s: plugin count */ 450 461 $text = _n( 'Update Available <span class="count">(%s)</span>', 'Update Available <span class="count">(%s)</span>', $count ); … … class WP_Plugins_List_Table extends WP_List_Table { 633 644 /* translators: %s: plugin name */ 634 645 $actions['deactivate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'deactivate-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Deactivate' ) . '</a>'; 635 646 } 647 if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) { 648 /* translators: %s: plugin name */ 649 $actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Network Resume %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Resume' ) . '</a>'; 650 } 636 651 } else { 637 652 if ( current_user_can( 'manage_network_plugins' ) ) { 638 653 /* translators: %s: plugin name */ 639 654 $actions['activate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'activate-plugin_' . $plugin_file ) . '" class="edit" aria-label="' . esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Activate' ) . '</a>'; 640 655 } 656 if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) { 657 /* translators: %s: plugin name */ 658 $actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Network Resume %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Resume' ) . '</a>'; 659 } 641 660 if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) { 642 661 /* translators: %s: plugin name */ 643 662 $actions['delete'] = '<a href="' . wp_nonce_url( 'plugins.php?action=delete-selected&checked[]=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'bulk-plugins' ) . '" class="delete" aria-label="' . esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Delete' ) . '</a>'; … … class WP_Plugins_List_Table extends WP_List_Table { 648 667 $actions = array( 649 668 'network_active' => __( 'Network Active' ), 650 669 ); 670 if ( ! $restrict_network_only && current_user_can( 'resume_plugin', $plugin_file ) && is_plugin_paused( $plugin_file ) ) { 671 /* translators: %s: plugin name */ 672 $actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume' ) . '</a>'; 673 } 651 674 } elseif ( $restrict_network_only ) { 652 675 $actions = array( 653 676 'network_only' => __( 'Network Only' ), … … class WP_Plugins_List_Table extends WP_List_Table { 657 680 /* translators: %s: plugin name */ 658 681 $actions['deactivate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'deactivate-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Deactivate' ) . '</a>'; 659 682 } 683 if ( current_user_can( 'resume_plugin', $plugin_file ) && is_plugin_paused( $plugin_file ) ) { 684 /* translators: %s: plugin name */ 685 $actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume' ) . '</a>'; 686 } 660 687 } else { 661 688 if ( current_user_can( 'activate_plugin', $plugin_file ) ) { 662 689 /* translators: %s: plugin name */ … … class WP_Plugins_List_Table extends WP_List_Table { 764 791 $class .= ' update'; 765 792 } 766 793 794 $paused = is_plugin_paused( $plugin_file ); 795 796 if ( $paused ) { 797 $class .= ' paused'; 798 } 799 767 800 $plugin_slug = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_name ); 768 801 printf( 769 802 '<tr class="%s" data-slug="%s" data-plugin="%s">', … … class WP_Plugins_List_Table extends WP_List_Table { 845 878 * @param array $plugin_data An array of plugin data. 846 879 * @param string $status Status of the plugin. Defaults are 'All', 'Active', 847 880 * 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use', 848 * 'Drop-ins', 'Search' .881 * 'Drop-ins', 'Search', 'Paused'. 849 882 */ 850 883 $plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status ); 851 884 echo implode( ' | ', $plugin_meta ); 852 885 853 886 echo '</div>'; 854 887 888 if ( $paused ) { 889 $notice_text = __( 'This plugin failed to load properly and is paused during recovery mode.' ); 890 891 printf( '<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>', $notice_text ); 892 893 $error = wp_get_plugin_error( $plugin_file ); 894 895 if ( false !== $error ) { 896 printf( '<div class="error-display"><p>%s</p></div>', wp_get_plugin_error_description( $error ) ); 897 } 898 } 899 855 900 echo '</td>'; 856 901 break; 857 902 default: … … class WP_Plugins_List_Table extends WP_List_Table { 885 930 * @param array $plugin_data An array of plugin data. 886 931 * @param string $status Status of the plugin. Defaults are 'All', 'Active', 887 932 * 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use', 888 * 'Drop-ins', 'Search' .933 * 'Drop-ins', 'Search', 'Paused'. 889 934 */ 890 935 do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status ); 891 936 … … class WP_Plugins_List_Table extends WP_List_Table { 901 946 * @param array $plugin_data An array of plugin data. 902 947 * @param string $status Status of the plugin. Defaults are 'All', 'Active', 903 948 * 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use', 904 * 'Drop-ins', 'Search' .949 * 'Drop-ins', 'Search', 'Paused'. 905 950 */ 906 951 do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status ); 907 952 } -
src/wp-admin/includes/plugin.php
diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 5873a33466..01de19ce70 100644
a b function get_dropins() { 468 468 */ 469 469 function _get_dropins() { 470 470 $dropins = array( 471 'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE 472 'db.php' => array( __( 'Custom database class.' ), true ), // auto on load 473 'db-error.php' => array( __( 'Custom database error message.' ), true ), // auto on error 474 'install.php' => array( __( 'Custom installation script.' ), true ), // auto on installation 475 'maintenance.php' => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance 476 'object-cache.php' => array( __( 'External object cache.' ), true ), // auto on load 471 'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE 472 'db.php' => array( __( 'Custom database class.' ), true ), // auto on load 473 'db-error.php' => array( __( 'Custom database error message.' ), true ), // auto on error 474 'install.php' => array( __( 'Custom installation script.' ), true ), // auto on installation 475 'maintenance.php' => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance 476 'object-cache.php' => array( __( 'External object cache.' ), true ), // auto on load 477 'php-error.php' => array( __( 'Custom PHP error message.' ), true ), // auto on error 478 'fatal-error-handler.php' => array( __( 'Custom PHP fatal error handler.' ), true ), // auto on error 479 'recovery-mode-controller.php' => array( __( 'Custom recovery mode controller.' ), true ), // auto on error 477 480 ); 478 481 479 482 if ( is_multisite() ) { … … function wp_add_privacy_policy_content( $plugin_name, $policy_text ) { 2101 2104 2102 2105 WP_Privacy_Policy_Content::add( $plugin_name, $policy_text ); 2103 2106 } 2107 2108 /** 2109 * Determines whether a plugin is technically active but was paused while 2110 * loading. 2111 * 2112 * For more information on this and similar theme functions, check out 2113 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ 2114 * Conditional Tags} article in the Theme Developer Handbook. 2115 * 2116 * @since 5.2.0 2117 * 2118 * @param string $plugin Path to the plugin file relative to the plugins directory. 2119 * @return bool True, if in the list of paused plugins. False, not in the list. 2120 */ 2121 function is_plugin_paused( $plugin ) { 2122 if ( ! isset( $GLOBALS['_paused_plugins'] ) ) { 2123 return false; 2124 } 2125 2126 if ( ! is_plugin_active( $plugin ) && ! is_plugin_active_for_network( $plugin ) ) { 2127 return false; 2128 } 2129 2130 list( $plugin ) = explode( '/', $plugin ); 2131 2132 return array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ); 2133 } 2134 2135 /** 2136 * Gets the error that was recorded for a paused plugin. 2137 * 2138 * @since 5.2.0 2139 * 2140 * @param string $plugin Path to the plugin file relative to the plugins 2141 * directory. 2142 * @return array|false Array of error information as it was returned by 2143 * `error_get_last()`, or false if none was recorded. 2144 */ 2145 function wp_get_plugin_error( $plugin ) { 2146 if ( ! isset( $GLOBALS['_paused_plugins'] ) ) { 2147 return false; 2148 } 2149 2150 list( $plugin ) = explode( '/', $plugin ); 2151 2152 if ( ! array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ) ) { 2153 return false; 2154 } 2155 2156 return $GLOBALS['_paused_plugins'][ $plugin ]; 2157 } 2158 2159 /** 2160 * Get a human readable description of the plugin error. 2161 * 2162 * @since 5.2.0 2163 * 2164 * @param array $error Error from {@see wp_get_plugin_error()} 2165 * 2166 * @return string Formatted error description. 2167 */ 2168 function wp_get_plugin_error_description( $error ) { 2169 $constants = get_defined_constants( true ); 2170 $constants = isset( $constants['Core'] ) ? $constants['Core'] : $constants['internal']; 2171 $core_errors = array(); 2172 2173 foreach ( $constants as $constant => $value ) { 2174 if ( 0 === strpos( $constant, 'E_' ) ) { 2175 $core_errors[ $value ] = $constant; 2176 } 2177 } 2178 2179 if ( isset( $core_errors[ $error['type'] ] ) ) { 2180 $error['type'] = $core_errors[ $error['type'] ]; 2181 } 2182 2183 /* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */ 2184 $error_message = __( 'The plugin caused an error of type %1$s in line %2$s of the file %3$s. Error message: %4$s' ); 2185 2186 return sprintf( 2187 $error_message, 2188 "<code>{$error['type']}</code>", 2189 "<code>{$error['line']}</code>", 2190 "<code>{$error['file']}</code>", 2191 "<code>{$error['message']}</code>" 2192 ); 2193 } 2194 2195 /** 2196 * Gets the number of sites on which a specific plugin is paused. 2197 * 2198 * @since 5.2.0 2199 * 2200 * @param string $plugin Path to the plugin file relative to the plugins directory. 2201 * @return int Site count. 2202 */ 2203 function count_paused_plugin_sites_for_network( $plugin ) { 2204 if ( ! is_multisite() ) { 2205 return is_plugin_paused( $plugin ) ? 1 : 0; 2206 } 2207 2208 list( $plugin ) = explode( '/', $plugin ); 2209 2210 $query_args = array( 2211 'count' => true, 2212 'number' => 0, 2213 'network_id' => get_current_network_id(), 2214 'meta_query' => array( 2215 wp_paused_extensions()->get_site_meta_query_clause( 'plugin', $plugin ), 2216 ), 2217 ); 2218 2219 return get_sites( $query_args ); 2220 } 2221 2222 /** 2223 * Tries to resume a single plugin. 2224 * 2225 * If a redirect was provided, we first ensure the plugin does not throw fatal 2226 * errors anymore. 2227 * 2228 * The way it works is by setting the redirection to the error before trying to 2229 * include the plugin file. If the plugin fails, then the redirection will not 2230 * be overwritten with the success message and the plugin will not be resumed. 2231 * 2232 * @since 5.2.0 2233 * 2234 * @param string $plugin Single plugin to resume. 2235 * @param string $redirect Optional. URL to redirect to. Default empty string. 2236 * @param bool $network_wide Optional. Whether to resume the plugin for the entire 2237 * network. Default false. 2238 * @return bool|WP_Error True on success, false if `$plugin` was not paused, 2239 * `WP_Error` on failure. 2240 */ 2241 function resume_plugin( $plugin, $redirect = '', $network_wide = false ) { 2242 /* 2243 * We'll override this later if the plugin could be included without 2244 * creating a fatal error. 2245 */ 2246 if ( ! empty( $redirect ) ) { 2247 wp_redirect( 2248 add_query_arg( 2249 '_error_nonce', 2250 wp_create_nonce( 'plugin-resume-error_' . $plugin ), 2251 $redirect 2252 ) 2253 ); 2254 2255 // Load the plugin to test whether it throws a fatal error. 2256 ob_start(); 2257 plugin_sandbox_scrape( $plugin ); 2258 ob_clean(); 2259 } 2260 2261 $result = wp_forget_extension_error( 'plugin', $plugin, $network_wide ); 2262 2263 if ( ! $result ) { 2264 return new WP_Error( 2265 'could_not_resume_plugin', 2266 __( 'Could not resume the plugin.' ) 2267 ); 2268 } 2269 2270 return true; 2271 } 2272 2273 /** 2274 * Renders an admin notice in case some plugins have been paused due to errors. 2275 * 2276 * @since 5.2.0 2277 */ 2278 function paused_plugins_notice() { 2279 if ( 'plugins.php' === $GLOBALS['pagenow'] ) { 2280 return; 2281 } 2282 2283 if ( ! current_user_can( 'deactivate_plugins' ) ) { 2284 return; 2285 } 2286 2287 if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) { 2288 return; 2289 } 2290 2291 printf( 2292 '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p>%s</p></div>', 2293 __( 'One or more plugins failed to load properly.' ), 2294 __( 'You can find more details and make changes on the Plugins screen.' ), 2295 sprintf( 2296 '<a href="%s">%s</a>', 2297 admin_url( 'plugins.php?plugin_status=paused' ), 2298 'Go to the Plugins screen' 2299 ) 2300 ); 2301 } -
src/wp-admin/includes/theme.php
diff --git a/src/wp-admin/includes/theme.php b/src/wp-admin/includes/theme.php index 91a3577906..a24f4dc938 100644
a b function customize_themes_print_templates() { 768 768 </script> 769 769 <?php 770 770 } 771 772 /** 773 * Determines whether a theme is technically active but was paused while 774 * loading. 775 * 776 * For more information on this and similar theme functions, check out 777 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ 778 * Conditional Tags} article in the Theme Developer Handbook. 779 * 780 * @since 5.2.0 781 * 782 * @param string $theme Path to the theme directory relative to the themes directory. 783 * @return bool True, if in the list of paused themes. False, not in the list. 784 */ 785 function is_theme_paused( $theme ) { 786 if ( ! isset( $GLOBALS['_paused_themes'] ) ) { 787 return false; 788 } 789 790 if ( get_stylesheet() !== $theme && get_template() !== $theme ) { 791 return false; 792 } 793 794 return array_key_exists( $theme, $GLOBALS['_paused_themes'] ); 795 } 796 797 /** 798 * Gets the error that was recorded for a paused theme. 799 * 800 * @since 5.2.0 801 * 802 * @param string $theme Path to the theme directory relative to the themes 803 * directory. 804 * @return array|false Array of error information as it was returned by 805 * `error_get_last()`, or false if none was recorded. 806 */ 807 function wp_get_theme_error( $theme ) { 808 if ( ! isset( $GLOBALS['_paused_themes'] ) ) { 809 return false; 810 } 811 812 if ( ! array_key_exists( $theme, $GLOBALS['_paused_themes'] ) ) { 813 return false; 814 } 815 816 return $GLOBALS['_paused_themes'][ $theme ]; 817 } 818 819 /** 820 * Gets the number of sites on which a specific theme is paused. 821 * 822 * @since 5.2.0 823 * 824 * @param string $theme Path to the theme directory relative to the themes directory. 825 * @return int Site count. 826 */ 827 function count_paused_theme_sites_for_network( $theme ) { 828 if ( ! is_multisite() ) { 829 return is_theme_paused( $theme ) ? 1 : 0; 830 } 831 832 $query_args = array( 833 'count' => true, 834 'number' => 0, 835 'network_id' => get_current_network_id(), 836 'meta_query' => array( 837 wp_paused_extensions()->get_site_meta_query_clause( 'theme', $theme ), 838 ), 839 ); 840 841 return get_sites( $query_args ); 842 } 843 844 /** 845 * Tries to resume a single theme. 846 * 847 * @since 5.2.0 848 * 849 * @param string $theme Single theme to resume. 850 * @return bool|WP_Error True on success, false if `$theme` was not paused, 851 * `WP_Error` on failure. 852 */ 853 function resume_theme( $theme ) { 854 $result = wp_forget_extension_error( 'theme', $theme ); 855 856 if ( ! $result ) { 857 return new WP_Error( 858 'could_not_resume_theme', 859 __( 'Could not resume the theme.' ) 860 ); 861 } 862 863 return true; 864 } 865 866 /** 867 * Renders an admin notice in case some themes have been paused due to errors. 868 * 869 * @since 5.2.0 870 */ 871 function paused_themes_notice() { 872 if ( 'themes.php' === $GLOBALS['pagenow'] ) { 873 return; 874 } 875 876 if ( ! current_user_can( 'switch_themes' ) ) { 877 return; 878 } 879 880 if ( ! isset( $GLOBALS['_paused_themes'] ) || empty( $GLOBALS['_paused_themes'] ) ) { 881 return; 882 } 883 884 printf( 885 '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p>%s</p></div>', 886 __( 'One or more themes failed to load properly.' ), 887 __( 'You can find more details and make changes on the Themes screen.' ), 888 sprintf( 889 '<a href="%s">%s</a>', 890 admin_url( 'themes.php' ), 891 'Go to the Themes screen' 892 ) 893 ); 894 } -
src/wp-admin/plugins.php
diff --git a/src/wp-admin/plugins.php b/src/wp-admin/plugins.php index c80e96831f..6c41e31b2f 100644
a b if ( $action ) { 389 389 } 390 390 break; 391 391 392 case 'resume': 393 if ( ! current_user_can( 'resume_plugin', $plugin ) ) { 394 wp_die( __( 'Sorry, you are not allowed to resume this plugin.' ) ); 395 } 396 397 if ( is_multisite() && ! is_network_admin() && is_network_only_plugin( $plugin ) ) { 398 wp_redirect( self_admin_url( "plugins.php?plugin_status=$status&paged=$page&s=$s" ) ); 399 exit; 400 } 401 402 check_admin_referer( 'resume-plugin_' . $plugin ); 403 404 $result = resume_plugin( $plugin, self_admin_url( 'plugins.php?error=resuming' ), is_network_admin() ); 405 406 if ( is_wp_error( $result ) ) { 407 wp_die( $result ); 408 } 409 410 wp_redirect( self_admin_url( "plugins.php?resume=true&plugin_status=$status&paged=$page&s=$s" ) ); 411 exit; 412 392 413 default: 393 414 if ( isset( $_POST['checked'] ) ) { 394 415 check_admin_referer( 'bulk-plugins' ); … … if ( isset( $_GET['error'] ) ) : 488 509 $_GET['charsout'] 489 510 ); 490 511 $errmsg .= ' ' . __( 'If you notice “headers already sent” messages, problems with syndication feeds or other issues, try deactivating or removing this plugin.' ); 512 } elseif ( 'resuming' === $_GET['error'] ) { 513 $errmsg = __( 'Plugin could not be resumed because it triggered a <strong>fatal error</strong>.' ); 491 514 } else { 492 515 $errmsg = __( 'Plugin could not be activated because it triggered a <strong>fatal error</strong>.' ); 493 516 } … … elseif ( isset( $_GET['deleted'] ) ) : 541 564 <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Selected plugins <strong>deactivated</strong>.' ); ?></p></div> 542 565 <?php elseif ( 'update-selected' == $action ) : ?> 543 566 <div id="message" class="updated notice is-dismissible"><p><?php _e( 'All selected plugins are up to date.' ); ?></p></div> 567 <?php elseif ( isset( $_GET['resume'] ) ) : ?> 568 <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Plugin <strong>resumed</strong>.' ); ?></p></div> 544 569 <?php endif; ?> 545 570 546 571 <div class="wrap"> -
src/wp-admin/themes.php
diff --git a/src/wp-admin/themes.php b/src/wp-admin/themes.php index 494e952184..754b75808d 100644
a b if ( current_user_can( 'switch_themes' ) && isset( $_GET['action'] ) ) { 33 33 switch_theme( $theme->get_stylesheet() ); 34 34 wp_redirect( admin_url( 'themes.php?activated=true' ) ); 35 35 exit; 36 } elseif ( 'resume' === $_GET['action'] ) { 37 check_admin_referer( 'resume-theme_' . $_GET['stylesheet'] ); 38 $theme = wp_get_theme( $_GET['stylesheet'] ); 39 40 if ( ! current_user_can( 'resume_themes' ) ) { 41 wp_die( 42 '<h1>' . __( 'You need a higher level of permission.' ) . '</h1>' . 43 '<p>' . __( 'Sorry, you are not allowed to resume this theme.' ) . '</p>', 44 403 45 ); 46 } 47 48 $result = resume_theme( $theme->get_stylesheet() ); 49 50 if ( is_wp_error( $result ) ) { 51 wp_die( $result ); 52 } 53 54 wp_redirect( admin_url( 'themes.php?resumed=true' ) ); 55 exit; 36 56 } elseif ( 'delete' == $_GET['action'] ) { 37 57 check_admin_referer( 'delete-theme_' . $_GET['stylesheet'] ); 38 58 $theme = wp_get_theme( $_GET['stylesheet'] ); … … if ( ! validate_current_theme() || isset( $_GET['broken'] ) ) { 195 215 ?> 196 216 <div id="message4" class="error"><p><?php _e( 'You cannot delete a theme while it has an active child theme.' ); ?></p></div> 197 217 <?php 218 } elseif ( isset( $_GET['resumed'] ) ) { 219 ?> 220 <div id="message5" class="updated notice is-dismissible"><p><?php _e( 'Theme resumed.' ); ?></p></div> 221 <?php 198 222 } 199 223 200 224 $ct = wp_get_theme(); … … if ( ! is_multisite() && current_user_can( 'edit_themes' ) && $broken_themes = w 348 372 <p><?php _e( 'The following themes are installed but incomplete.' ); ?></p> 349 373 350 374 <?php 375 $can_resume = current_user_can( 'resume_themes' ); 351 376 $can_delete = current_user_can( 'delete_themes' ); 352 377 $can_install = current_user_can( 'install_themes' ); 353 378 ?> … … if ( ! is_multisite() && current_user_can( 'edit_themes' ) && $broken_themes = w 355 380 <tr> 356 381 <th><?php _ex( 'Name', 'theme name' ); ?></th> 357 382 <th><?php _e( 'Description' ); ?></th> 383 <?php if ( $can_resume ) { ?> 384 <td></td> 385 <?php } ?> 358 386 <?php if ( $can_delete ) { ?> 359 387 <td></td> 360 388 <?php } ?> … … if ( ! is_multisite() && current_user_can( 'edit_themes' ) && $broken_themes = w 367 395 <td><?php echo $broken_theme->get( 'Name' ) ? $broken_theme->display( 'Name' ) : $broken_theme->get_stylesheet(); ?></td> 368 396 <td><?php echo $broken_theme->errors()->get_error_message(); ?></td> 369 397 <?php 398 if ( $can_resume ) { 399 if ( 'theme_paused' === $broken_theme->errors()->get_error_code() ) { 400 $stylesheet = $broken_theme->get_stylesheet(); 401 $resume_url = add_query_arg( 402 array( 403 'action' => 'resume', 404 'stylesheet' => urlencode( $stylesheet ), 405 ), 406 admin_url( 'themes.php' ) 407 ); 408 $resume_url = wp_nonce_url( $resume_url, 'resume-theme_' . $stylesheet ); 409 ?> 410 <td><a href="<?php echo esc_url( $resume_url ); ?>" class="button resume-theme"><?php _e( 'Resume' ); ?></a></td> 411 <?php 412 } else { 413 ?> 414 <td></td> 415 <?php 416 } 417 } 418 370 419 if ( $can_delete ) { 371 420 $stylesheet = $broken_theme->get_stylesheet(); 372 421 $delete_url = add_query_arg( -
src/wp-includes/capabilities.php
diff --git a/src/wp-includes/capabilities.php b/src/wp-includes/capabilities.php index c192639608..17d0fd1869 100644
a b function map_meta_cap( $cap, $user_id ) { 464 464 } 465 465 } 466 466 break; 467 case 'resume_plugin': 468 $caps[] = 'resume_plugins'; 469 break; 467 470 case 'delete_user': 468 471 case 'delete_users': 469 472 // If multisite only super admins can delete users. … … function wp_maybe_grant_install_languages_cap( $allcaps ) { 950 953 951 954 return $allcaps; 952 955 } 956 957 /** 958 * Filters the user capabilities to grant the 'resume_plugins' and 'resume_themes' capabilities as necessary. 959 * 960 * @since 5.2.0 961 * 962 * @param bool[] $allcaps An array of all the user's capabilities. 963 * @return bool[] Filtered array of the user's capabilities. 964 */ 965 function wp_maybe_grant_resume_extensions_caps( $allcaps ) { 966 // Even in a multisite, regular administrators should be able to resume plugins. 967 if ( ! empty( $allcaps['activate_plugins'] ) ) { 968 $allcaps['resume_plugins'] = true; 969 } 970 971 // Even in a multisite, regular administrators should be able to resume themes. 972 if ( ! empty( $allcaps['switch_themes'] ) ) { 973 $allcaps['resume_themes'] = true; 974 } 975 976 return $allcaps; 977 } -
new file src/wp-includes/class-wp-fatal-error-handler.php
diff --git a/src/wp-includes/class-wp-fatal-error-handler.php b/src/wp-includes/class-wp-fatal-error-handler.php new file mode 100644 index 0000000000..8baa6d2f2a
- + 1 <?php 2 /** 3 * Error Protection API: WP_Fatal_Error_Handler class 4 * 5 * @package WordPress 6 * @since 5.2.0 7 */ 8 9 /** 10 * Core class used as the default shutdown handler for fatal errors. 11 * 12 * A drop-in 'fatal-error-handler.php' can be used to override the instance of this class and use a custom 13 * implementation for the fatal error handler that WordPress registers. The custom class should extend this class and 14 * can override its methods individually as necessary. The file must return the instance of the class that should be 15 * registered. 16 * 17 * @since 5.2.0 18 */ 19 class WP_Fatal_Error_Handler { 20 21 /** 22 * Runs the shutdown handler. 23 * 24 * This method is registered via `register_shutdown_function()`. 25 * 26 * @since 5.2.0 27 */ 28 public function handle() { 29 // Bail if WordPress executed successfully. 30 if ( defined( 'WP_EXECUTION_SUCCEEDED' ) && WP_EXECUTION_SUCCEEDED ) { 31 return; 32 } 33 34 try { 35 // Bail if no error found. 36 $error = $this->detect_error(); 37 if ( ! $error ) { 38 return; 39 } 40 41 if ( wp_is_recovery_mode() ) { 42 // If the error was stored and thus the extension paused, 43 // redirect the request to catch multiple errors in one go. 44 if ( $this->store_error( $error ) ) { 45 $this->redirect_protected(); 46 } 47 } else { 48 wp_recovery_mode()->handle_error( $error ); 49 } 50 51 // Display the PHP error template. 52 $this->display_error_template(); 53 } catch ( Exception $e ) { 54 // Catch exceptions and remain silent. 55 } 56 } 57 58 /** 59 * Detects the error causing the crash if it should be handled. 60 * 61 * @since 5.2.0 62 * 63 * @return array|null Error that was triggered, or null if no error received or if the error should not be handled. 64 */ 65 protected function detect_error() { 66 $error = error_get_last(); 67 68 // No error, just skip the error handling code. 69 if ( null === $error ) { 70 return null; 71 } 72 73 // Bail if this error should not be handled. 74 if ( ! wp_should_handle_error( $error ) ) { 75 return null; 76 } 77 78 return $error; 79 } 80 81 /** 82 * Stores the given error so that the extension causing it is paused. 83 * 84 * @since 5.2.0 85 * 86 * @param array $error Error that was triggered. 87 * 88 * @return bool True if the error was stored successfully, false otherwise. 89 */ 90 protected function store_error( $error ) { 91 return wp_record_extension_error( $error ); 92 } 93 94 /** 95 * Redirects the current request to allow recovering multiple errors in one go. 96 * 97 * The redirection will only happen when on a protected endpoint. 98 * 99 * It must be ensured that this method is only called when an error actually occurred and will not occur on the 100 * next request again. Otherwise it will create a redirect loop. 101 * 102 * @since 5.2.0 103 */ 104 protected function redirect_protected() { 105 // Pluggable is usually loaded after plugins, so we manually include it here for redirection functionality. 106 if ( ! function_exists( 'wp_redirect' ) ) { 107 include ABSPATH . WPINC . '/pluggable.php'; 108 } 109 110 $scheme = is_ssl() ? 'https://' : 'http://'; 111 112 $url = "{$scheme}{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}"; 113 wp_redirect( $url ); 114 exit; 115 } 116 117 /** 118 * Displays the PHP error template and sends the HTTP status code, typically 500. 119 * 120 * A drop-in 'php-error.php' can be used as a custom template. This drop-in should control the HTTP status code and 121 * print the HTML markup indicating that a PHP error occurred. Note that this drop-in may potentially be executed 122 * very early in the WordPress bootstrap process, so any core functions used that are not part of 123 * `wp-includes/load.php` should be checked for before being called. 124 * 125 * If no such drop-in is available, this will call {@see WP_Fatal_Error_Handler::display_default_error_template()}. 126 * 127 * @since 5.2.0 128 */ 129 protected function display_error_template() { 130 if ( defined( 'WP_CONTENT_DIR' ) ) { 131 // Load custom PHP error template, if present. 132 $php_error_pluggable = WP_CONTENT_DIR . '/php-error.php'; 133 if ( is_readable( $php_error_pluggable ) ) { 134 require_once $php_error_pluggable; 135 136 return; 137 } 138 } 139 140 // Otherwise, display the default error template. 141 $this->display_default_error_template(); 142 } 143 144 /** 145 * Displays the default PHP error template. 146 * 147 * This method is called conditionally if no 'php-error.php' drop-in is available. 148 * 149 * It calls {@see wp_die()} with a message indicating that the site is experiencing technical difficulties and a 150 * login link to the admin backend. The {@see 'wp_php_error_message'} and {@see 'wp_php_error_args'} filters can 151 * be used to modify these parameters. 152 * 153 * @since 5.2.0 154 */ 155 protected function display_default_error_template() { 156 if ( ! function_exists( '__' ) ) { 157 wp_load_translations_early(); 158 } 159 160 if ( ! function_exists( 'wp_die' ) ) { 161 require_once ABSPATH . WPINC . '/functions.php'; 162 } 163 164 $message = __( 'The site is experiencing technical difficulties.' ); 165 166 $args = array( 167 'response' => 500, 168 'exit' => false, 169 ); 170 171 /** 172 * Filters the message that the default PHP error template displays. 173 * 174 * @since 5.2.0 175 * 176 * @param string $message HTML error message to display. 177 */ 178 $message = apply_filters( 'wp_php_error_message', $message ); 179 180 /** 181 * Filters the arguments passed to {@see wp_die()} for the default PHP error template. 182 * 183 * @since 5.2.0 184 * 185 * @param array $args Associative array of arguments passed to `wp_die()`. By default these contain a 186 * 'response' key, and optionally 'link_url' and 'link_text' keys. 187 */ 188 $args = apply_filters( 'wp_php_error_args', $args ); 189 190 wp_die( $message, '', $args ); 191 } 192 } -
new file src/wp-includes/class-wp-paused-extensions-storage.php
diff --git a/src/wp-includes/class-wp-paused-extensions-storage.php b/src/wp-includes/class-wp-paused-extensions-storage.php new file mode 100644 index 0000000000..00c2668b86
- + 1 <?php 2 /** 3 * Error Protection API: WP_Paused_Extensions_Storage class 4 * 5 * @package WordPress 6 * @since 5.2.0 7 */ 8 9 /** 10 * Core class used for storing paused extensions. 11 * 12 * @since 5.2.0 13 */ 14 class WP_Paused_Extensions_Storage { 15 16 /** 17 * Option name for storing paused extensions. 18 * 19 * @since 5.2.0 20 * @var string 21 */ 22 protected $option_name; 23 24 /** 25 * Constructor. 26 * 27 * @since 5.2.0 28 */ 29 public function __construct() { 30 $this->option_name = wp_recovery_mode()->get_recovery_mode_session_id() . '_paused_extensions'; 31 } 32 33 /** 34 * Records an extension error. 35 * 36 * Only one error is stored per extension, with subsequent errors for the same extension overriding the 37 * previously stored error. 38 * 39 * @since 5.2.0 40 * 41 * @param string $type Extension type. Either 'plugin' or 'theme'. 42 * @param string $extension Plugin or theme directory name. 43 * @param array $error { 44 * Error that was triggered. 45 * 46 * @type string $type The error type. 47 * @type string $file The name of the file in which the error occurred. 48 * @type string $line The line number in which the error occurred. 49 * @type string $message The error message. 50 * } 51 * @return bool True on success, false on failure. 52 */ 53 public function record( $type, $extension, $error ) { 54 if ( ! $this->is_api_loaded() ) { 55 return false; 56 } 57 58 if ( is_multisite() && is_site_meta_supported() ) { 59 $meta_key = $this->get_site_meta_key( $type, $extension ); 60 61 // Do not update if the error is already stored. 62 if ( get_site_meta( get_current_blog_id(), $meta_key, true ) === $error ) { 63 return true; 64 } 65 66 return (bool) update_site_meta( get_current_blog_id(), $meta_key, $error ); 67 } 68 69 $paused_extensions = $this->get_all(); 70 71 // Do not update if the error is already stored. 72 if ( isset( $paused_extensions[ $type ][ $extension ] ) && $paused_extensions[ $type ][ $extension ] === $error ) { 73 return true; 74 } 75 76 $paused_extensions[ $type ][ $extension ] = $error; 77 78 return update_option( $this->option_name, $paused_extensions ); 79 } 80 81 /** 82 * Forgets a previously recorded extension error. 83 * 84 * @since 5.2.0 85 * 86 * @param string $type Extension type. Either 'plugin' or 'theme'. 87 * @param string $extension Plugin or theme directory name. 88 * @return bool True on success, false on failure. 89 */ 90 public function forget( $type, $extension ) { 91 if ( ! $this->is_api_loaded() ) { 92 return false; 93 } 94 95 if ( is_multisite() && is_site_meta_supported() ) { 96 $meta_key = $this->get_site_meta_key( $type, $extension ); 97 98 // Do not delete if no error is stored. 99 if ( get_site_meta( get_current_blog_id(), $meta_key ) === array() ) { 100 return true; 101 } 102 103 return delete_site_meta( get_current_blog_id(), $meta_key ); 104 } 105 106 $paused_extensions = $this->get_all(); 107 108 // Do not delete if no error is stored. 109 if ( ! isset( $paused_extensions[ $type ][ $extension ] ) ) { 110 return true; 111 } 112 113 unset( $paused_extensions[ $type ][ $extension ] ); 114 115 if ( empty( $paused_extensions[ $type ] ) ) { 116 unset( $paused_extensions[ $type ] ); 117 } 118 119 // Clean up the entire option if we're removing the only error. 120 if ( ! $paused_extensions ) { 121 return delete_option( $this->option_name ); 122 } 123 124 return update_option( $this->option_name, $paused_extensions ); 125 } 126 127 /** 128 * Gets the error for an extension, if paused. 129 * 130 * @since 5.2.0 131 * 132 * @param string $type Extension type. Either 'plugin' or 'theme'. 133 * @param string $extension Plugin or theme directory name. 134 * @return array|null Error that is stored, or null if the extension is not paused. 135 */ 136 public function get( $type, $extension ) { 137 if ( ! $this->is_api_loaded() ) { 138 return null; 139 } 140 141 if ( is_multisite() && is_site_meta_supported() ) { 142 $error = get_site_meta( get_current_blog_id(), $this->get_site_meta_key( $type, $extension ), true ); 143 if ( ! $error ) { 144 return null; 145 } 146 147 return $error; 148 } 149 150 $paused_extensions = $this->get_all( $type ); 151 152 if ( ! isset( $paused_extensions[ $extension ] ) ) { 153 return null; 154 } 155 156 return $paused_extensions[ $extension ]; 157 } 158 159 /** 160 * Gets the paused extensions with their errors. 161 * 162 * @since 5.2.0 163 * 164 * @param string $type Optionally, limit to extensions of the given type. 165 * 166 * @return array Associative array of $type => array( $extension => $error ). 167 * If the extension type is provided, just the error entries are returned. 168 */ 169 public function get_all( $type = '' ) { 170 if ( ! $this->is_api_loaded() ) { 171 return array(); 172 } 173 174 if ( is_multisite() && is_site_meta_supported() ) { 175 $site_metadata = get_site_meta( get_current_blog_id() ); 176 177 $paused_extensions = array(); 178 foreach ( $site_metadata as $meta_key => $meta_values ) { 179 if ( 0 !== strpos( $meta_key, $this->option_name . '_' ) ) { 180 continue; 181 } 182 183 $error = maybe_unserialize( array_shift( $meta_values ) ); 184 185 $without_prefix = substr( $meta_key, strlen( $this->option_name . '_' ) ); 186 $parts = explode( '_', $without_prefix, 2 ); 187 188 if ( ! isset( $parts[1] ) ) { 189 continue; 190 } 191 192 $paused_extensions[ $parts[0] ][ $parts[1] ] = $error; 193 } 194 } else { 195 $paused_extensions = (array) get_option( $this->option_name, array() ); 196 } 197 198 if ( $type ) { 199 return isset( $paused_extensions[ $type ] ) ? $paused_extensions[ $type ] : array(); 200 } 201 202 return $paused_extensions; 203 } 204 205 /** 206 * Gets the site meta query clause for querying sites with paused extensions. 207 * 208 * @since 5.2.0 209 * 210 * @param string $type Extension type. Either 'plugin' or 'theme'. 211 * @param string $extension Plugin or theme directory name. 212 * @return array A single clause to add to a meta query. 213 */ 214 public function get_site_meta_query_clause( $type, $extension ) { 215 return array( 216 'key' => $this->get_site_meta_key( $type, $extension ), 217 'compare_key' => '=', 218 ); 219 } 220 221 /** 222 * Checks whether the underlying API to store paused extensions is loaded. 223 * 224 * @since 5.2.0 225 * 226 * @return bool True if the API is loaded, false otherwise. 227 */ 228 protected function is_api_loaded() { 229 if ( is_multisite() ) { 230 return function_exists( 'is_site_meta_supported' ) && function_exists( 'get_site_meta' ); 231 } 232 233 return function_exists( 'get_option' ); 234 } 235 236 /** 237 * Get the site meta key for storing extension errors on Multisite. 238 * 239 * @since 5.2.0 240 * 241 * @param string $type 242 * @param string $extension 243 * 244 * @return string 245 */ 246 private function get_site_meta_key( $type, $extension ) { 247 return $this->option_name . '_' . $type . '_' . $extension; 248 } 249 } -
new file src/wp-includes/class-wp-recovery-mode-controller.php
diff --git a/src/wp-includes/class-wp-recovery-mode-controller.php b/src/wp-includes/class-wp-recovery-mode-controller.php new file mode 100644 index 0000000000..c232a930ff
- + 1 <?php 2 3 /** 4 * Interface WP_Recovery_Mode_Controller 5 */ 6 interface WP_Recovery_Mode_Controller { 7 8 /** 9 * Run the processor. 10 * 11 * This can be used for adding hooks, parsing the global request data, 12 * exiting the request due to errors, etc.. 13 * 14 * @since 5.2.0 15 * 16 * @return void 17 */ 18 public function run(); 19 20 /** 21 * Handle a fatal error occurring when recovery mode is not yet active. 22 * 23 * @param array $error Error details {@see error_get_last()} 24 * 25 * @return void 26 */ 27 public function handle_error( array $error ); 28 29 /** 30 * Is recovery mode active. 31 * 32 * @since 5.2.0 33 * 34 * @return bool 35 */ 36 public function is_recovery_mode_active(); 37 38 /** 39 * Get the recovery mode session ID. 40 * 41 * @since 5.2.0 42 * 43 * @return string|false 44 */ 45 public function get_recovery_mode_session_id(); 46 } -
new file src/wp-includes/class-wp-recovery-mode-cookie-service.php
diff --git a/src/wp-includes/class-wp-recovery-mode-cookie-service.php b/src/wp-includes/class-wp-recovery-mode-cookie-service.php new file mode 100644 index 0000000000..642c3705d2
- + 1 <?php 2 3 final class WP_Recovery_Mode_Cookie_Service { 4 5 /** @var string */ 6 private $name; 7 8 /** @var string */ 9 private $domain; 10 11 /** @var string */ 12 private $path; 13 14 /** @var string */ 15 private $site_path; 16 17 /** 18 * WP_Recovery_Mode_Cookie_Service constructor. 19 * 20 * @param array $opts 21 */ 22 public function __construct( array $opts = array() ) { 23 $opts = wp_parse_args( $opts, array( 24 'name' => RECOVERY_MODE_COOKIE, 25 'domain' => COOKIE_DOMAIN, 26 'path' => COOKIEPATH, 27 'site_path' => SITECOOKIEPATH, 28 ) ); 29 30 $this->name = $opts['name']; 31 $this->domain = $opts['domain']; 32 $this->path = $opts['path']; 33 $this->site_path = $opts['site_path']; 34 } 35 36 /** 37 * Is the recovery mode cookie set. 38 * 39 * @since 5.2.0 40 * 41 * @return bool 42 */ 43 public function is_cookie_set() { 44 return ! empty( $_COOKIE[ $this->name ] ); 45 } 46 47 /** 48 * Set the recovery mode cookie. 49 * 50 * This must be immediately followed by exiting the request. 51 * 52 * @since 5.2.0 53 */ 54 public function set_cookie() { 55 56 $value = $this->generate_cookie(); 57 58 setcookie( $this->name, $value, 0, $this->path, $this->domain, is_ssl(), true ); 59 60 if ( $this->path !== $this->site_path ) { 61 setcookie( $this->name, $value, 0, $this->site_path, $this->domain, is_ssl(), true ); 62 } 63 } 64 65 /** 66 * Clear the recovery mode cookie. 67 * 68 * @sicne 5.2.0 69 */ 70 public function clear_cookie() { 71 setcookie( $this->name, ' ', time() - YEAR_IN_SECONDS, $this->path, $this->domain ); 72 setcookie( $this->name, ' ', time() - YEAR_IN_SECONDS, $this->site_path, $this->domain ); 73 } 74 75 /** 76 * Validate the recovery mode cookie. 77 * 78 * @since 5.2.0 79 * 80 * @param string $cookie Optionally specify the cookie string. 81 * If omitted, it will be retrieved from the super global. 82 * 83 * @return true|WP_Error 84 */ 85 public function validate_cookie( $cookie = '' ) { 86 87 if ( ! $cookie ) { 88 if ( empty( $_COOKIE[ $this->name ] ) ) { 89 return new WP_Error( 'no_cookie', __( 'No cookie present.' ) ); 90 } 91 92 $cookie = $_COOKIE[ $this->name ]; 93 } 94 95 $parts = $this->parse_cookie( $cookie ); 96 97 if ( is_wp_error( $parts ) ) { 98 return $parts; 99 } 100 101 list( , $created_at, $random, $signature ) = $parts; 102 103 if ( ! ctype_digit( $created_at ) ) { 104 return new WP_Error( 'invalid_created_at', __( 'Invalid cookie format.' ) ); 105 } 106 107 /** 108 * Filter the length of time a Recovery Mode cookie is valid for. 109 * 110 * @since 5.2.0 111 * 112 * @param int $length Length in seconds. 113 */ 114 $length = apply_filters( 'recovery_mode_cookie_length', WEEK_IN_SECONDS ); 115 116 if ( time() > $created_at + $length ) { 117 return new WP_Error( 'expired', __( 'Cookie expired.' ) ); 118 } 119 120 $to_sign = sprintf( 'recovery_mode|%s|%s', $created_at, $random ); 121 $hashed = $this->recovery_mode_hash( $to_sign ); 122 123 if ( ! hash_equals( $signature, $hashed ) ) { 124 return new WP_Error( 'signature_mismatch', __( 'Invalid cookie.' ) ); 125 } 126 127 return true; 128 } 129 130 /** 131 * Get the session identifier from the cookie. 132 * 133 * The cookie should be validated before calling this API. 134 * 135 * @since 5.2.0 136 * 137 * @param string $cookie Optionally specify the cookie string. 138 * If omitted, it will be retrieved from the super global. 139 * 140 * @return string|WP_Error 141 */ 142 public function get_session_id_from_cookie( $cookie = '' ) { 143 if ( ! $cookie ) { 144 if ( empty( $_COOKIE[ $this->name ] ) ) { 145 return new WP_Error( 'no_cookie' ); 146 } 147 148 $cookie = $_COOKIE[ $this->name ]; 149 } 150 151 $parts = $this->parse_cookie( $cookie ); 152 if ( is_wp_error( $parts ) ) { 153 return $parts; 154 } 155 156 list( , , $random ) = $parts; 157 158 return sha1( $random ); 159 } 160 161 /** 162 * Parse the cookie into its four parts. 163 * 164 * @param string $cookie 165 * 166 * @return string[]|WP_Error 167 */ 168 private function parse_cookie( $cookie ) { 169 $cookie = base64_decode( $cookie ); 170 $parts = explode( '|', $cookie ); 171 172 if ( 4 !== count( $parts ) ) { 173 return new WP_Error( 'invalid_format', __( 'Invalid cookie format.' ) ); 174 } 175 176 return $parts; 177 } 178 179 /** 180 * Generate the recovery mode cookie value. 181 * 182 * The cookie is a base64 encoded string with the following format: 183 * 184 * recovery_mode|iat|rand|signature 185 * 186 * Where "recovery_mode" is a constant string, 187 * iat is the time the cookie was generated at, 188 * rand is a randomly generated password that is also used as a session identifier 189 * and signature is an hmac of the preceding 3 parts. 190 * 191 * @since 5.2.0 192 * 193 * @return string 194 */ 195 private function generate_cookie() { 196 197 if ( ! function_exists( 'wp_generate_password' ) ) { 198 require_once ABSPATH . WPINC . '/pluggable.php'; 199 } 200 201 $to_sign = sprintf( 'recovery_mode|%s|%s', time(), wp_generate_password( 20, false ) ); 202 $signed = $this->recovery_mode_hash( $to_sign ); 203 204 return base64_encode( sprintf( '%s|%s', $to_sign, $signed ) ); 205 } 206 207 /** 208 * A form of `wp_hash()` specific to Recovery Mode. 209 * 210 * We cannot use `wp_hash()` because it is defined in `pluggable.php` which is not loaded until after plugins are loaded, 211 * which is too late to verify the recovery mode cookie. 212 * 213 * This tries to use the `AUTH` salts first, but if they aren't valid specific salts will be generated and stored. 214 * 215 * @param string $data 216 * 217 * @return string|false 218 */ 219 private function recovery_mode_hash( $data ) { 220 221 if ( ! defined( 'AUTH_KEY' ) || 'put your unique phrase here' === AUTH_KEY ) { 222 $auth_key = get_site_option( 'recovery_mode_auth_key' ); 223 224 if ( ! $auth_key ) { 225 if ( ! function_exists( 'wp_generate_password' ) ) { 226 require_once ABSPATH . WPINC . '/pluggable.php'; 227 } 228 229 $auth_key = wp_generate_password( 64, true, true ); 230 update_site_option( 'recovery_mode_auth_key', $auth_key ); 231 } 232 } else { 233 $auth_key = AUTH_KEY; 234 } 235 236 if ( ! defined( 'AUTH_SALT' ) || 'put your unique phrase here' === AUTH_SALT || $auth_key === AUTH_SALT ) { 237 $auth_salt = get_site_option( 'recovery_mode_auth_salt' ); 238 239 if ( ! $auth_salt ) { 240 if ( ! function_exists( 'wp_generate_password' ) ) { 241 require_once ABSPATH . WPINC . '/pluggable.php'; 242 } 243 244 $auth_salt = wp_generate_password( 64, true, true ); 245 update_site_option( 'recovery_mode_auth_salt', $auth_salt ); 246 } 247 } else { 248 $auth_salt = AUTH_SALT; 249 } 250 251 $secret = $auth_key . $auth_salt; 252 253 return hash_hmac( 'sha1', $data, $secret ); 254 } 255 } -
new file src/wp-includes/class-wp-recovery-mode-email-controller.php
diff --git a/src/wp-includes/class-wp-recovery-mode-email-controller.php b/src/wp-includes/class-wp-recovery-mode-email-controller.php new file mode 100644 index 0000000000..2dbf92cc95
- + 1 <?php 2 3 final class WP_Recovery_Mode_Email_Controller implements WP_Recovery_Mode_Controller { 4 5 const LOGIN_ACTION_ENTER = 'enter_recovery_mode'; 6 const LOGIN_ACTION_ENTERED = 'entered_recovery_mode'; 7 8 /** @var WP_Recovery_Mode_Cookie_Service */ 9 private $cookies; 10 11 /** @var WP_Recovery_Mode_Key_Service */ 12 private $keys; 13 14 /** @var bool */ 15 private $is_active; 16 17 /** @var string|false */ 18 private $session_id = false; 19 20 /** 21 * WP_Recovery_Mode_Email_Processor constructor. 22 * 23 * @param WP_Recovery_Mode_Cookie_Service $cookies 24 * @param WP_Recovery_Mode_Key_Service $keys 25 */ 26 public function __construct( WP_Recovery_Mode_Cookie_Service $cookies, WP_Recovery_Mode_Key_Service $keys ) { 27 $this->cookies = $cookies; 28 $this->keys = $keys; 29 } 30 31 /** 32 * @inheritdoc 33 */ 34 public function is_recovery_mode_active() { 35 return $this->is_active; 36 } 37 38 /** 39 * @inheritdoc 40 */ 41 public function get_recovery_mode_session_id() { 42 return $this->session_id; 43 } 44 45 /** 46 * @inheritdoc 47 */ 48 public function run() { 49 add_action( 'clear_auth_cookie', array( $this, 'on_clear_auth_cookie' ) ); 50 51 if ( $this->cookies->is_cookie_set() ) { 52 $this->handle_cookie(); 53 54 return; 55 } 56 57 if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) { 58 $this->handle_begin_link(); 59 } 60 } 61 62 /** 63 * When a fatal error occurs, send the recovery mode email. 64 * 65 * @since 5.2.0 66 * 67 * @param array $error Error details from {@see error_get_last()} 68 */ 69 public function handle_error( array $error ) { 70 if ( is_protected_endpoint() ) { 71 $this->maybe_send_recovery_mode_email( $error ); 72 } 73 } 74 75 /** 76 * Clear the recovery mode cookie when the auth cookies are cleared. 77 * 78 * @since 5.2.0 79 */ 80 public function on_clear_auth_cookie() { 81 /** This filter is documented in wp-includes/pluggable.php */ 82 if ( ! apply_filters( 'send_auth_cookies', true ) ) { 83 return; 84 } 85 86 $this->cookies->clear_cookie(); 87 } 88 89 /** 90 * Handle checking for the recovery mode cookie and validating it. 91 * 92 * @since 5.2.0 93 */ 94 private function handle_cookie() { 95 $validated = $this->cookies->validate_cookie(); 96 97 if ( is_wp_error( $validated ) ) { 98 $this->cookies->clear_cookie(); 99 100 wp_die( $validated, '' ); 101 } 102 103 $this->is_active = true; 104 $this->session_id = $this->cookies->get_session_id_from_cookie(); 105 } 106 107 /** 108 * Enter recovery mode when the user hits wp-login.php with a valid recovery mode link. 109 * 110 * @since 5.2.0 111 */ 112 private function handle_begin_link() { 113 if ( ! isset( $_GET['action'], $_GET['rm_key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) { 114 return; 115 } 116 117 $validated = $this->keys->validate_recovery_mode_key( $_GET['rm_key'], $this->get_link_valid_for_interval() ); 118 119 if ( is_wp_error( $validated ) ) { 120 wp_die( $validated, '' ); 121 } 122 123 $this->cookies->set_cookie(); 124 125 // This should be loaded by set_recovery_mode_cookie() but load it again to be safe. 126 if ( ! function_exists( 'wp_redirect' ) ) { 127 require_once ABSPATH . WPINC . '/pluggable.php'; 128 } 129 130 $url = add_query_arg( 'action', self::LOGIN_ACTION_ENTERED, wp_login_url() ); 131 wp_redirect( $url ); 132 die; 133 } 134 135 /** 136 * Get a URL to begin recovery mode. 137 * 138 * @since 5.2.0 139 * 140 * @param string $key Recovery Mode key created by {@see generate_and_store_recovery_mode_key()} 141 * 142 * @return string 143 */ 144 private function get_recovery_mode_begin_url( $key ) { 145 146 $url = add_query_arg( 147 array( 148 'action' => self::LOGIN_ACTION_ENTER, 149 'rm_key' => $key, 150 ), 151 wp_login_url() 152 ); 153 154 /** 155 * Filter the URL to begin recovery mode. 156 * 157 * @since 5.2.0 158 * 159 * @param string $url 160 * @param string $key 161 */ 162 return apply_filters( 'recovery_mode_begin_url', $url, $key ); 163 } 164 165 /** 166 * Get the interval the recovery mode email key is valid for. 167 * 168 * @since 5.2.0 169 * 170 * @return int Interval in seconds. 171 */ 172 private function get_link_valid_for_interval() { 173 174 $rate_limit = $valid_for = $this->get_email_rate_limit(); 175 176 /** 177 * Filter the amount of time the recovery mode email link is valid for. 178 * 179 * The interval time must be at least as long as the email rate limit. 180 * 181 * @since 5.2.0 182 * 183 * @param int $valid_for The number of seconds the link is valid for. 184 */ 185 $valid_for = apply_filters( 'recovery_mode_email_link_valid_for_interval', $valid_for ); 186 187 return max( $valid_for, $rate_limit ); 188 } 189 190 /** 191 * The rate limit between sending new recovery mode email links. 192 * 193 * @since 5.2.0 194 * 195 * @return int Rate limit in seconds. 196 */ 197 private function get_email_rate_limit() { 198 /** 199 * Filter the rate limit between sending new recovery mode email links. 200 * 201 * @since 5.2.0 202 * 203 * @param int $rate_limit Time to wait in seconds. Defaults to 4 hours. 204 */ 205 return apply_filters( 'recovery_mode_email_rate_limit', 4 * HOUR_IN_SECONDS ); 206 } 207 208 /** 209 * Send the recovery mode email if the rate limit has not been sent. 210 * 211 * @since 5.2.0 212 * 213 * @param array $error Error details from {@see error_get_last()} 214 * 215 * @return true|WP_Error True if email sent, WP_Error otherwise. 216 */ 217 private function maybe_send_recovery_mode_email( $error ) { 218 219 $rate_limit = $this->get_email_rate_limit(); 220 221 $last_sent = get_site_option( 'recovery_mode_email_last_sent' ); 222 223 if ( ! $last_sent || time() > $last_sent + $rate_limit ) { 224 $sent = $this->send_recovery_mode_email( $error ); 225 update_site_option( 'recovery_mode_email_last_sent', time() ); 226 227 if ( $sent ) { 228 return true; 229 } 230 231 return new WP_Error( 'email_failed', __( 'The email could not be sent. Possible reason: your host may have disabled the mail() function.' ) ); 232 } 233 234 $err_message = sprintf( 235 /* translators: 1. Last sent as a human time diff 2. Wait time as a human time diff. */ 236 __( 'A recovery link was already sent %1$s ago. Please wait another %2$s before requesting a new email.' ), 237 human_time_diff( $last_sent ), 238 human_time_diff( $last_sent + $rate_limit ) 239 ); 240 241 return new WP_Error( 'email_sent_already', $err_message ); 242 } 243 244 /** 245 * Send the Recovery Mode email to the site admin email address. 246 * 247 * @since 5.2.0 248 * 249 * @param array $error Error details from {@see error_get_last()} 250 * 251 * @return bool Whether the email was sent successfully. 252 */ 253 private function send_recovery_mode_email( $error ) { 254 255 $key = $this->keys->generate_and_store_recovery_mode_key(); 256 $url = $this->get_recovery_mode_begin_url( $key ); 257 $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); 258 259 $switched_locale = false; 260 261 // The switch_to_locale() function is loaded before it can actually be used. 262 if ( function_exists( 'switch_to_locale' ) && isset( $GLOBALS['wp_locale_switcher'] ) ) { 263 $switched_locale = switch_to_locale( get_locale() ); 264 } 265 266 $extension = wp_get_extension_for_error( $error ); 267 268 if ( $extension ) { 269 $cause = $this->get_cause( $extension ); 270 $details = $this->get_error_details( $error ); 271 272 if ( $details ) { 273 $header = __( 'Error Details' ); 274 $details = "\n\n" . $header . "\n" . str_pad( '', strlen( $header ), '=' ) . "\n" . $details; 275 } 276 } else { 277 $cause = $details = ''; 278 } 279 280 $message = __( 281 'Howdy, 282 283 Your site recently crashed on ###LOCATION### and may not be working as expected. 284 ###CAUSE### 285 Click the link below to initiate recovery mode and fix the problem. 286 287 This link expires in ###EXPIRES###. 288 289 ###LINK### ###DETAILS### 290 ' 291 ); 292 $message = str_replace( 293 array( 294 '###LINK###', 295 '###LOCATION###', 296 '###EXPIRES###', 297 '###CAUSE###', 298 '###DETAILS###', 299 ), 300 array( 301 $url, 302 'TBD', 303 human_time_diff( time() + $this->get_link_valid_for_interval() ), 304 $cause ? "\n{$cause}\n" : "\n", 305 $details, 306 ), 307 $message 308 ); 309 310 $email = array( 311 'to' => $this->get_recovery_mode_email_address(), 312 'subject' => __( '[%s] Your Site Experienced an Issue' ), 313 'message' => $message, 314 'headers' => '', 315 ); 316 317 /** 318 * Filter the contents of the Recovery Mode email. 319 * 320 * @since 5.2.0 321 * 322 * @param array $email Used to build wp_mail(). 323 * @param string $key Recovery mode key. 324 */ 325 $email = apply_filters( 'recovery_mode_email', $email, $key ); 326 327 $sent = wp_mail( 328 $email['to'], 329 wp_specialchars_decode( sprintf( $email['subject'], $blogname ) ), 330 $email['message'], 331 $email['headers'] 332 ); 333 334 if ( $switched_locale ) { 335 restore_previous_locale(); 336 } 337 338 return $sent; 339 } 340 341 /** 342 * Get the email address to send the recovery mode link to. 343 * 344 * @since 5.2.0 345 * 346 * @return string 347 */ 348 private function get_recovery_mode_email_address() { 349 if ( defined( 'RECOVERY_MODE_EMAIL' ) && is_email( RECOVERY_MODE_EMAIL ) ) { 350 return RECOVERY_MODE_EMAIL; 351 } 352 353 return get_option( 'admin_email' ); 354 } 355 356 /** 357 * Get a human readable description of the error. 358 * 359 * @since 5.2.0 360 * 361 * @param array $error Error details from {@see error_get_last()} 362 * 363 * @return string 364 */ 365 private function get_error_details( $error ) { 366 $constants = get_defined_constants( true ); 367 $constants = isset( $constants['Core'] ) ? $constants['Core'] : $constants['internal']; 368 $core_errors = array(); 369 370 foreach ( $constants as $constant => $value ) { 371 if ( 0 === strpos( $constant, 'E_' ) ) { 372 $core_errors[ $value ] = $constant; 373 } 374 } 375 376 if ( isset( $core_errors[ $error['type'] ] ) ) { 377 $error['type'] = $core_errors[ $error['type'] ]; 378 } 379 380 /* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */ 381 $error_message = __( "An error of type %1\$s in line %2\$s of the file %3\$s. \nError message: %4\$s" ); 382 383 return sprintf( 384 $error_message, 385 $error['type'], 386 $error['line'], 387 $error['file'], 388 $error['message'] 389 ); 390 } 391 392 /** 393 * Get the description indicating the possible cause for the error. 394 * 395 * @since 5.2.0 396 * 397 * @param array $extension The extension that caused the error. 398 * 399 * @return string 400 */ 401 private function get_cause( $extension ) { 402 403 if ( 'plugin' === $extension['type'] ) { 404 if ( ! function_exists( 'get_plugins' ) ) { 405 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 406 } 407 408 $names = array(); 409 410 foreach ( get_plugins() as $file => $plugin ) { 411 if ( 0 === strpos( $file, "{$extension['slug']}/" ) ) { 412 $names[] = $plugin['Name']; 413 } 414 } 415 416 if ( ! $names ) { 417 $names[] = $extension['slug']; 418 } 419 420 // Multiple plugins can technically be in the same directory. 421 $cause = wp_sprintf( _n( 'This may be caused by the %l plugin.', 'This may be caused by the %l plugins.', count( $names ) ), $names ); 422 } else { 423 $theme = wp_get_theme( $extension['slug'] ); 424 $name = $theme->exists() ? $theme->display( 'Name' ) : $extension['slug']; 425 426 $cause = sprintf( __( 'This may be caused by the %s theme.' ), $name ); 427 } 428 429 return $cause; 430 } 431 } -
new file src/wp-includes/class-wp-recovery-mode-key-service.php
diff --git a/src/wp-includes/class-wp-recovery-mode-key-service.php b/src/wp-includes/class-wp-recovery-mode-key-service.php new file mode 100644 index 0000000000..2e169ba9c2
- + 1 <?php 2 3 final class WP_Recovery_Mode_Key_Service { 4 5 /** 6 * Create a recovery mode key. 7 * 8 * @since 5.2.0 9 * 10 * @global PasswordHash $wp_hasher 11 * 12 * @return string Recovery mode key. 13 */ 14 public function generate_and_store_recovery_mode_key() { 15 16 global $wp_hasher; 17 18 if ( ! function_exists( 'wp_generate_password' ) ) { 19 require_once ABSPATH . WPINC . '/pluggable.php'; 20 } 21 22 $key = wp_generate_password( 20, false ); 23 24 /** 25 * Fires when a recovery mode key is generated for a user. 26 * 27 * @since 5.2.0 28 * 29 * @param string $key The recovery mode key. 30 */ 31 do_action( 'generate_recovery_mode_key', $key ); 32 33 if ( empty( $wp_hasher ) ) { 34 require_once ABSPATH . WPINC . '/class-phpass.php'; 35 $wp_hasher = new PasswordHash( 8, true ); 36 } 37 38 $hashed = $wp_hasher->HashPassword( $key ); 39 40 update_site_option( 'recovery_key', array( 41 'hashed_key' => $hashed, 42 'created_at' => time(), 43 ) ); 44 45 return $key; 46 } 47 48 /** 49 * Verify if the recovery mode key is correct. 50 * 51 * @since 5.2.0 52 * 53 * @param string $key The unhashed key. 54 * @param int $ttl Time in seconds for the key to be valid for. 55 * 56 * @return true|WP_Error 57 */ 58 public function validate_recovery_mode_key( $key, $ttl ) { 59 60 $record = get_site_option( 'recovery_key' ); 61 62 if ( ! $record ) { 63 return new WP_Error( 'no_recovery_key_set', __( 'Recovery Mode not initialized.' ) ); 64 } 65 66 if ( ! is_array( $record ) || ! isset( $record['hashed_key'], $record['created_at'] ) ) { 67 return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) ); 68 } 69 70 if ( ! function_exists( 'wp_check_password' ) ) { 71 require_once ABSPATH . WPINC . '/pluggable.php'; 72 } 73 74 if ( ! wp_check_password( $key, $record['hashed_key'] ) ) { 75 return new WP_Error( 'hash_mismatch', __( 'Invalid recovery key.' ) ); 76 } 77 78 if ( time() > $record['created_at'] + $ttl ) { 79 return new WP_Error( 'key_expired', __( 'Recovery key expired.' ) ); 80 } 81 82 return true; 83 } 84 } -
src/wp-includes/class-wp-theme.php
diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php index 22a5768448..f190eeb968 100644
a b final class WP_Theme implements ArrayAccess { 371 371 $this->parent = new WP_Theme( $this->template, isset( $theme_root_template ) ? $theme_root_template : $this->theme_root, $this ); 372 372 } 373 373 374 if ( wp_paused_extensions()->get( 'theme', $this->stylesheet ) && ( ! is_wp_error( $this->errors ) || ! isset( $this->errors->errors['theme_paused'] ) ) ) { 375 $this->errors = new WP_Error( 'theme_paused', __( 'This theme failed to load properly and was paused within the admin backend.' ) ); 376 } 377 374 378 // We're good. If we didn't retrieve from cache, set it. 375 379 if ( ! is_array( $cache ) ) { 376 380 $cache = array( -
src/wp-includes/default-constants.php
diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php index 1d3fd5df98..b1830ac76d 100644
a b function wp_cookie_constants() { 302 302 if ( ! defined( 'COOKIE_DOMAIN' ) ) { 303 303 define( 'COOKIE_DOMAIN', false ); 304 304 } 305 306 if ( ! defined( 'RECOVERY_MODE_COOKIE' ) ) { 307 /** 308 * @since 5.2.0 309 */ 310 define( 'RECOVERY_MODE_COOKIE', 'wordpress_rec_' . COOKIEHASH ); 311 } 305 312 } 306 313 307 314 /** -
src/wp-includes/default-filters.php
diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 1319826212..7242a11282 100644
a b add_filter( 'pre_oembed_result', 'wp_filter_pre_oembed_result', 10, 3 ); 578 578 579 579 // Capabilities 580 580 add_filter( 'user_has_cap', 'wp_maybe_grant_install_languages_cap', 1 ); 581 add_filter( 'user_has_cap', 'wp_maybe_grant_resume_extensions_caps', 1 ); 581 582 582 583 unset( $filter, $action ); -
new file src/wp-includes/error-protection.php
diff --git a/src/wp-includes/error-protection.php b/src/wp-includes/error-protection.php new file mode 100644 index 0000000000..b1b961bb27
- + 1 <?php 2 /** 3 * Error Protection API: Functions 4 * 5 * @package WordPress 6 * @since 5.2.0 7 */ 8 9 /** 10 * Get the instance for storing paused extensions. 11 * 12 * @return WP_Paused_Extensions_Storage 13 */ 14 function wp_paused_extensions() { 15 static $wp_paused_extensions_storage = null; 16 17 if ( null === $wp_paused_extensions_storage ) { 18 $wp_paused_extensions_storage = new WP_Paused_Extensions_Storage(); 19 } 20 21 return $wp_paused_extensions_storage; 22 } 23 24 /** 25 * Records the extension error as a database option. 26 * 27 * @since 5.2.0 28 * 29 * @param array $error Error that was triggered. 30 * 31 * @return bool Whether the error was correctly recorded. 32 */ 33 function wp_record_extension_error( $error ) { 34 35 $extension = wp_get_extension_for_error( $error ); 36 37 if ( ! $extension ) { 38 return false; 39 } 40 41 return wp_paused_extensions()->record( $extension['type'], $extension['slug'], $error ); 42 } 43 44 /** 45 * Get the extension that the error occurred in. 46 * 47 * @since 5.2.0 48 * 49 * @global array $wp_theme_directories 50 * 51 * @param array $error Error that was triggered. 52 * 53 * @return array|false array( 'slug' => (string), 'type' => 'plugin' | 'theme' ) 54 * Slug is the plugin or theme directory as opposed to the full file. 55 * Or false on error. 56 */ 57 function wp_get_extension_for_error( $error ) { 58 global $wp_theme_directories; 59 60 if ( ! isset( $error['file'] ) ) { 61 return false; 62 } 63 64 if ( ! defined( 'WP_PLUGIN_DIR' ) ) { 65 return false; 66 } 67 68 $error_file = wp_normalize_path( $error['file'] ); 69 $wp_plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ); 70 71 if ( 0 === strpos( $error_file, $wp_plugin_dir ) ) { 72 $path = str_replace( $wp_plugin_dir . '/', '', $error_file ); 73 $parts = explode( '/', $path ); 74 75 return array( 'type' => 'plugin', 'slug' => $parts[0] ); 76 } 77 78 if ( empty( $wp_theme_directories ) ) { 79 return false; 80 } 81 82 foreach ( $wp_theme_directories as $theme_directory ) { 83 $theme_directory = wp_normalize_path( $theme_directory ); 84 85 if ( 0 === strpos( $error_file, $theme_directory ) ) { 86 $path = str_replace( $theme_directory . '/', '', $error_file ); 87 $parts = explode( '/', $path ); 88 89 return array( 'type' => 'theme', 'slug' => $parts[0] ); 90 } 91 } 92 93 return false; 94 } 95 96 /** 97 * Forgets a previously recorded extension error again. 98 * 99 * @since 5.2.0 100 * 101 * @param string $type Type of the extension. 102 * @param string $extension Relative path of the extension. 103 * @param bool $network_wide Optional. Whether to resume the plugin for the entire 104 * network. Default false. 105 * 106 * @return bool Whether the extension error was successfully forgotten. 107 */ 108 function wp_forget_extension_error( $type, $extension, $network_wide = false ) { 109 110 list( $extension ) = explode( '/', $extension ); 111 112 if ( empty( $extension ) ) { 113 return false; 114 } 115 116 $storage = wp_paused_extensions(); 117 118 // Handle manually since the regular APIs do not expose this functionality. 119 if ( $network_wide && is_site_meta_supported() ) { 120 $site_meta_query_clause = $storage->get_site_meta_query_clause( $type, $extension ); 121 return delete_metadata( 'blog', 0, $site_meta_query_clause['key'], '', true ); 122 } 123 124 return $storage->forget( $type, $extension ); 125 } 126 127 /** 128 * Determines whether we are dealing with an error that WordPress should handle 129 * in order to protect the admin backend against WSODs. 130 * 131 * @param array $error Error information retrieved from error_get_last(). 132 * 133 * @return bool Whether WordPress should handle this error. 134 */ 135 function wp_should_handle_error( $error ) { 136 if ( ! isset( $error['type'] ) ) { 137 return false; 138 } 139 140 $error_types_to_handle = array( 141 E_ERROR, 142 E_PARSE, 143 E_USER_ERROR, 144 E_COMPILE_ERROR, 145 E_RECOVERABLE_ERROR, 146 ); 147 148 return in_array( $error['type'], $error_types_to_handle, true ); 149 } 150 151 /** 152 * Registers the shutdown handler for fatal errors. 153 * 154 * The handler will only be registered if {@see wp_is_fatal_error_handler_enabled()} returns true. 155 * 156 * @since 5.2.0 157 */ 158 function wp_register_fatal_error_handler() { 159 if ( ! wp_is_fatal_error_handler_enabled() ) { 160 return; 161 } 162 163 $handler = null; 164 if ( defined( 'WP_CONTENT_DIR' ) && is_readable( WP_CONTENT_DIR . '/fatal-error-handler.php' ) ) { 165 $handler = include WP_CONTENT_DIR . '/fatal-error-handler.php'; 166 } 167 168 if ( ! is_object( $handler ) || ! is_callable( array( $handler, 'handle' ) ) ) { 169 $handler = new WP_Fatal_Error_Handler(); 170 } 171 172 register_shutdown_function( array( $handler, 'handle' ) ); 173 } 174 175 /** 176 * Checks whether the fatal error handler is enabled. 177 * 178 * A constant `WP_DISABLE_FATAL_ERROR_HANDLER` can be set in `wp-config.php` to disable it, or alternatively the 179 * {@see 'wp_fatal_error_handler_enabled'} filter can be used to modify the return value. 180 * 181 * @since 5.2.0 182 * 183 * @return bool True if the fatal error handler is enabled, false otherwise. 184 */ 185 function wp_is_fatal_error_handler_enabled() { 186 $enabled = ! defined( 'WP_DISABLE_FATAL_ERROR_HANDLER' ) || ! WP_DISABLE_FATAL_ERROR_HANDLER; 187 188 /** 189 * Filters whether the fatal error handler is enabled. 190 * 191 * @since 5.2.0 192 * 193 * @param bool $enabled True if the fatal error handler is enabled, false otherwise. 194 */ 195 return apply_filters( 'wp_fatal_error_handler_enabled', $enabled ); 196 } 197 198 /** 199 * Access the WordPress Recovery Mode controller. 200 * 201 * @since 5.2.0 202 * 203 * @return WP_Recovery_Mode_Controller 204 */ 205 function wp_recovery_mode() { 206 static $wp_recovery_mode; 207 208 if ( ! $wp_recovery_mode ) { 209 $default = new WP_Recovery_Mode_Email_Controller( 210 new WP_Recovery_Mode_Cookie_Service(), 211 new WP_Recovery_Mode_Key_Service() 212 ); 213 214 if ( defined( 'WP_CONTENT_DIR' ) && is_readable( WP_CONTENT_DIR . '/recovery-mode-controller.php' ) ) { 215 $wp_recovery_mode = include WP_CONTENT_DIR . '/recovery-mode-controller.php'; 216 } 217 218 if ( ! $wp_recovery_mode instanceof WP_Recovery_Mode_Controller ) { 219 $wp_recovery_mode = $default; 220 } 221 222 /** 223 * Filter the recovery mode controller. 224 * 225 * This filter can only be used by mu-plugins. 226 * 227 * @since 5.2.0 228 * 229 * @param WP_Recovery_Mode_Controller $wp_recovery_mode 230 */ 231 $wp_recovery_mode = apply_filters( 'wp_recovery_mode_controller', $wp_recovery_mode ); 232 233 if ( ! $wp_recovery_mode instanceof WP_Recovery_Mode_Controller ) { 234 $wp_recovery_mode = $default; 235 } 236 } 237 238 return $wp_recovery_mode; 239 } -
src/wp-includes/load.php
diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 245ecb1e61..1b0af818e3 100644
a b function wp_get_active_and_valid_plugins() { 697 697 } 698 698 } 699 699 700 /* 701 * Remove plugins from the list of active plugins when we're on an endpoint 702 * that should be protected against WSODs and the plugin is paused. 703 */ 704 if ( wp_is_recovery_mode() ) { 705 $plugins = wp_skip_paused_plugins( $plugins ); 706 } 707 708 return $plugins; 709 } 710 711 /** 712 * Filters a given list of plugins, removing any paused plugins from it. 713 * 714 * @since 5.2.0 715 * 716 * @param array $plugins List of absolute plugin main file paths. 717 * @return array Filtered value of $plugins, without any paused plugins. 718 */ 719 function wp_skip_paused_plugins( array $plugins ) { 720 $paused_plugins = wp_paused_extensions()->get_all( 'plugin' ); 721 722 if ( empty( $paused_plugins ) ) { 723 return $plugins; 724 } 725 726 foreach ( $plugins as $index => $plugin ) { 727 list( $plugin ) = explode( '/', plugin_basename( $plugin ) ); 728 729 if ( array_key_exists( $plugin, $paused_plugins ) ) { 730 unset( $plugins[ $index ] ); 731 732 // Store list of paused plugins for displaying an admin notice. 733 $GLOBALS['_paused_plugins'][ $plugin ] = $paused_plugins[ $plugin ]; 734 } 735 } 736 700 737 return $plugins; 701 738 } 702 739 … … function wp_get_active_and_valid_themes() { 725 762 726 763 $themes[] = TEMPLATEPATH; 727 764 765 /* 766 * Remove themes from the list of active themes when we're on an endpoint 767 * that should be protected against WSODs and the theme is paused. 768 */ 769 if ( wp_is_recovery_mode() ) { 770 $themes = wp_skip_paused_themes( $themes ); 771 772 // If no active and valid themes exist, skip loading themes. 773 if ( empty( $themes ) ) { 774 add_filter( 'wp_using_themes', '__return_false' ); 775 } 776 } 777 778 return $themes; 779 } 780 781 /** 782 * Filters a given list of themes, removing any paused themes from it. 783 * 784 * @since 5.2.0 785 * 786 * @param array $themes List of absolute theme directory paths. 787 * @return array Filtered value of $themes, without any paused themes. 788 */ 789 function wp_skip_paused_themes( array $themes ) { 790 $paused_themes = wp_paused_extensions()->get_all( 'theme' ); 791 792 if ( empty( $paused_themes ) ) { 793 return $themes; 794 } 795 796 foreach ( $themes as $index => $theme ) { 797 $theme = basename( $theme ); 798 799 if ( array_key_exists( $theme, $paused_themes ) ) { 800 unset( $themes[ $index ] ); 801 802 // Store list of paused themes for displaying an admin notice. 803 $GLOBALS['_paused_themes'][ $theme ] = $paused_themes[ $theme ]; 804 } 805 } 806 728 807 return $themes; 729 808 } 730 809 810 /** 811 * Is WordPress in Recovery Mode. 812 * 813 * In this mode, plugins or themes that cause WSODs will be paused. 814 * 815 * @since 5.2.0 816 * 817 * @return bool 818 */ 819 function wp_is_recovery_mode() { 820 return wp_recovery_mode()->is_recovery_mode_active(); 821 } 822 823 /** 824 * Determines whether we are currently on an endpoint that should be protected against WSODs. 825 * 826 * @since 5.2.0 827 * 828 * @return bool True if the current endpoint should be protected. 829 */ 830 function is_protected_endpoint() { 831 // Protect login pages. 832 if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) { 833 return true; 834 } 835 836 // Protect the admin backend. 837 if ( is_admin() && ! wp_doing_ajax() ) { 838 return true; 839 } 840 841 // Protect AJAX actions that could help resolve a fatal error should be available. 842 if ( is_protected_ajax_action() ) { 843 return true; 844 } 845 846 /** 847 * Filters whether the current request is against a protected endpoint. 848 * 849 * This filter is only fired when an endpoint is requested which is not already protected by 850 * WordPress core. As such, it exclusively allows providing further protected endpoints in 851 * addition to the admin backend, login pages and protected AJAX actions. 852 * 853 * @since 5.2.0 854 * 855 * @param bool $is_protected_endpoint Whether the currently requested endpoint is protected. Default false. 856 */ 857 return (bool) apply_filters( 'is_protected_endpoint', false ); 858 } 859 860 /** 861 * Determines whether we are currently handling an AJAX action that should be protected against WSODs. 862 * 863 * @since 5.2.0 864 * 865 * @return bool True if the current AJAX action should be protected. 866 */ 867 function is_protected_ajax_action() { 868 if ( ! wp_doing_ajax() ) { 869 return false; 870 } 871 872 if ( ! isset( $_REQUEST['action'] ) ) { 873 return false; 874 } 875 876 $actions_to_protect = array( 877 'edit-theme-plugin-file', // Saving changes in the core code editor. 878 'heartbeat', // Keep the heart beating. 879 'install-plugin', // Installing a new plugin. 880 'install-theme', // Installing a new theme. 881 'search-plugins', // Searching in the list of plugins. 882 'search-install-plugins', // Searching for a plugin in the plugin install screen. 883 'update-plugin', // Update an existing plugin. 884 'update-theme', // Update an existing theme. 885 ); 886 887 /** 888 * Filters the array of protected AJAX actions. 889 * 890 * This filter is only fired when doing AJAX and the AJAX request has an 'action' property. 891 * 892 * @since 5.2.0 893 * 894 * @param array $actions_to_protect Array of strings with AJAX actions to protect. 895 */ 896 $actions_to_protect = (array) apply_filters( 'wp_protected_ajax_actions', $actions_to_protect ); 897 898 if ( ! in_array( $_REQUEST['action'], $actions_to_protect, true ) ) { 899 return false; 900 } 901 902 return true; 903 } 904 731 905 /** 732 906 * Set internal encoding. 733 907 * -
src/wp-login.php
diff --git a/src/wp-login.php b/src/wp-login.php index b02a2b9e70..c162c610cb 100644
a b if ( isset( $_GET['key'] ) ) { 439 439 } 440 440 441 441 // Validate action so as to default to the login screen. 442 if ( ! in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'confirmaction' ), true ) && false === has_filter( 'login_form_' . $action ) ) {442 if ( ! in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'confirmaction', WP_Recovery_Mode_Email_Controller::LOGIN_ACTION_ENTERED ), true ) && false === has_filter( 'login_form_' . $action ) ) { 443 443 $action = 'login'; 444 444 } 445 445 … … switch ( $action ) { 1029 1029 $errors->add( 'registered', __( 'Registration complete. Please check your email.' ), 'message' ); 1030 1030 } elseif ( strpos( $redirect_to, 'about.php?updated' ) ) { 1031 1031 $errors->add( 'updated', __( '<strong>You have successfully updated WordPress!</strong> Please log back in to see what’s new.' ), 'message' ); 1032 } elseif ( WP_Recovery_Mode_Email_Controller::LOGIN_ACTION_ENTERED === $action ) { 1033 $errors->add( 'enter_recovery_mode', __( 'Recovery Mode Initialized. Please login to continue.' ), 'message' ); 1032 1034 } 1033 1035 } 1034 1036 -
src/wp-settings.php
diff --git a/src/wp-settings.php b/src/wp-settings.php index a520ff96c8..393aa7de0d 100644
a b define( 'WPINC', 'wp-includes' ); 17 17 18 18 // Include files required for initialization. 19 19 require( ABSPATH . WPINC . '/load.php' ); 20 require( ABSPATH . WPINC . '/class-wp-paused-extensions-storage.php' ); 21 require( ABSPATH . WPINC . '/class-wp-fatal-error-handler.php' ); 22 require( ABSPATH . WPINC . '/error-protection.php' ); 20 23 require( ABSPATH . WPINC . '/default-constants.php' ); 21 24 require_once( ABSPATH . WPINC . '/plugin.php' ); 22 25 26 // Make sure we register the shutdown handler for fatal errors as soon as possible. 27 wp_register_fatal_error_handler(); 28 23 29 /* 24 30 * These can't be directly globalized in version.php. When updating, 25 31 * we're including version.php from another installation and don't want … … require( ABSPATH . WPINC . '/blocks/rss.php' ); 261 267 require( ABSPATH . WPINC . '/blocks/search.php' ); 262 268 require( ABSPATH . WPINC . '/blocks/shortcode.php' ); 263 269 require( ABSPATH . WPINC . '/blocks/tag-cloud.php' ); 270 require( ABSPATH . WPINC . '/class-wp-recovery-mode-cookie-service.php' ); 271 require( ABSPATH . WPINC . '/class-wp-recovery-mode-key-service.php' ); 272 require( ABSPATH . WPINC . '/class-wp-recovery-mode-controller.php' ); 273 require( ABSPATH . WPINC . '/class-wp-recovery-mode-email-controller.php' ); 264 274 265 275 $GLOBALS['wp_embed'] = new WP_Embed(); 266 276 … … wp_start_scraping_edited_file_errors(); 340 350 // Register the default theme directory root 341 351 register_theme_directory( get_theme_root() ); 342 352 353 // Handle users requesting a recovery mode link and initiating recovery mode. 354 wp_recovery_mode()->run(); 355 343 356 // Load active plugins. 344 357 foreach ( wp_get_active_and_valid_plugins() as $plugin ) { 345 358 wp_register_plugin_realpath( $plugin ); … … if ( is_multisite() ) { 528 541 * @since 3.0.0 529 542 */ 530 543 do_action( 'wp_loaded' ); 544 545 /* 546 * Store the fact that we could successfully execute the entire WordPress 547 * lifecycle. This is used to skip the premature shutdown handler, as it cannot 548 * be unregistered. 549 */ 550 if ( ! defined( 'WP_EXECUTION_SUCCEEDED' ) ) { 551 define( 'WP_EXECUTION_SUCCEEDED', true ); 552 } -
new file tests/phpunit/tests/recovery-mode.php
diff --git a/tests/phpunit/tests/recovery-mode.php b/tests/phpunit/tests/recovery-mode.php new file mode 100644 index 0000000000..777430a377
- + 1 <?php 2 3 class Tests_Recovery_Mode extends WP_UnitTestCase { 4 5 private static $subscriber; 6 private static $administrator; 7 8 public static function setUpBeforeClass() { 9 self::$subscriber = self::factory()->user->create( array( 'role' => 'subscriber' ) ); 10 self::$administrator = self::factory()->user->create( array( 'role' => 'administrator' ) ); 11 12 return parent::setUpBeforeClass(); 13 } 14 15 public static function tearDownAfterClass() { 16 wp_delete_user( self::$subscriber ); 17 wp_delete_user( self::$administrator ); 18 19 return parent::tearDownAfterClass(); 20 } 21 22 public function test_generate_and_store_returns_recovery_key() { 23 $service = new WP_Recovery_Mode_Key_Service(); 24 $key = $service->generate_and_store_recovery_mode_key(); 25 26 $this->assertNotWPError( $key ); 27 } 28 29 public function test_verify_recovery_mode_key_returns_wp_error_if_no_key_set() { 30 $service = new WP_Recovery_Mode_Key_Service(); 31 $error = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS ); 32 33 $this->assertWPError( $error ); 34 $this->assertEquals( 'no_recovery_key_set', $error->get_error_code() ); 35 } 36 37 public function test_verify_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() { 38 update_site_option( 'recovery_key', 'gibberish' ); 39 40 $service = new WP_Recovery_Mode_Key_Service(); 41 $error = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS ); 42 43 $this->assertWPError( $error ); 44 $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() ); 45 } 46 47 public function test_verify_recovery_mode_key_returns_wp_error_if_empty_key() { 48 $service = new WP_Recovery_Mode_Key_Service(); 49 $service->generate_and_store_recovery_mode_key(); 50 $error = $service->validate_recovery_mode_key( '', HOUR_IN_SECONDS ); 51 52 $this->assertWPError( $error ); 53 $this->assertEquals( 'hash_mismatch', $error->get_error_code() ); 54 } 55 56 public function test_verify_recovery_mode_key_returns_wp_error_if_hash_mismatch() { 57 $service = new WP_Recovery_Mode_Key_Service(); 58 $service->generate_and_store_recovery_mode_key(); 59 $error = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS ); 60 61 $this->assertWPError( $error ); 62 $this->assertEquals( 'hash_mismatch', $error->get_error_code() ); 63 } 64 65 public function test_verify_recovery_mode_key_returns_wp_error_if_expired() { 66 $service = new WP_Recovery_Mode_Key_Service(); 67 $key = $service->generate_and_store_recovery_mode_key(); 68 69 $record = get_site_option( 'recovery_key' ); 70 $record['created_at'] = time() - HOUR_IN_SECONDS - 30; 71 update_site_option( 'recovery_key', $record ); 72 73 $error = $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS ); 74 75 $this->assertWPError( $error ); 76 $this->assertEquals( 'key_expired', $error->get_error_code() ); 77 } 78 79 public function test_verify_recovery_mode_key_returns_true_for_valid_key() { 80 $service = new WP_Recovery_Mode_Key_Service(); 81 $key = $service->generate_and_store_recovery_mode_key(); 82 $this->assertTrue( $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS ) ); 83 } 84 85 public function test_validate_recovery_mode_cookie_returns_wp_error_if_invalid_format() { 86 87 $service = new WP_Recovery_Mode_Cookie_Service(); 88 89 $error = $service->validate_cookie( 'gibbersih' ); 90 $this->assertWPError( $error ); 91 $this->assertEquals( 'invalid_format', $error->get_error_code() ); 92 93 $error = $service->validate_cookie( base64_encode( 'test|data|format' ) ); 94 $this->assertWPError( $error ); 95 $this->assertEquals( 'invalid_format', $error->get_error_code() ); 96 97 $error = $service->validate_cookie( base64_encode( 'test|data|format|to|long' ) ); 98 $this->assertWPError( $error ); 99 $this->assertEquals( 'invalid_format', $error->get_error_code() ); 100 } 101 102 public function test_validate_recovery_mode_cookie_returns_wp_error_if_expired() { 103 $service = new WP_Recovery_Mode_Cookie_Service(); 104 $reflection = new ReflectionMethod( $service, 'recovery_mode_hash' ); 105 $reflection->setAccessible( true ); 106 107 $to_sign = sprintf( 'recovery_mode|%s|%s', time() - WEEK_IN_SECONDS - 30, wp_generate_password( 20, false ) ); 108 $signed = $reflection->invoke( $service, $to_sign ); 109 $cookie = base64_encode( sprintf( '%s|%s', $to_sign, $signed ) ); 110 111 $error = $service->validate_cookie( $cookie ); 112 $this->assertWPError( $error ); 113 $this->assertEquals( 'expired', $error->get_error_code() ); 114 } 115 116 public function test_validate_recovery_mode_cookie_returns_wp_error_if_signature_mismatch() { 117 $service = new WP_Recovery_Mode_Cookie_Service(); 118 $reflection = new ReflectionMethod( $service, 'generate_cookie' ); 119 $reflection->setAccessible( true ); 120 121 $cookie = $reflection->invoke( $service ); 122 $cookie .= 'gibbersih'; 123 124 $error = $service->validate_cookie( $cookie ); 125 $this->assertWPError( $error ); 126 $this->assertEquals( 'signature_mismatch', $error->get_error_code() ); 127 } 128 129 public function test_validate_recovery_mode_cookie_returns_wp_error_if_created_at_is_invalid_format() { 130 $service = new WP_Recovery_Mode_Cookie_Service(); 131 $reflection = new ReflectionMethod( $service, 'recovery_mode_hash' ); 132 $reflection->setAccessible( true ); 133 134 $to_sign = sprintf( 'recovery_mode|%s|%s', 'month', wp_generate_password( 20, false ) ); 135 $signed = $reflection->invoke( $service, $to_sign ); 136 $cookie = base64_encode( sprintf( '%s|%s', $to_sign, $signed ) ); 137 138 $error = $service->validate_cookie( $cookie ); 139 $this->assertWPError( $error ); 140 $this->assertEquals( 'invalid_created_at', $error->get_error_code() ); 141 } 142 143 public function test_generate_and_validate_recovery_mode_cookie_returns_true_for_valid_cookie() { 144 145 $service = new WP_Recovery_Mode_Cookie_Service(); 146 $reflection = new ReflectionMethod( $service, 'generate_cookie' ); 147 $reflection->setAccessible( true ); 148 149 $this->assertTrue( $service->validate_cookie( $reflection->invoke( $service ) ) ); 150 } 151 } -
tests/phpunit/tests/user/capabilities.php
diff --git a/tests/phpunit/tests/user/capabilities.php b/tests/phpunit/tests/user/capabilities.php index 3bc5264d52..a326d8ad86 100644
a b class Tests_User_Capabilities extends WP_UnitTestCase { 257 257 'export_others_personal_data' => array( 'administrator' ), 258 258 'erase_others_personal_data' => array( 'administrator' ), 259 259 'manage_privacy_options' => array( 'administrator' ), 260 'resume_plugins' => array( 'administrator' ), 261 'resume_themes' => array( 'administrator' ), 260 262 261 263 'edit_categories' => array( 'administrator', 'editor' ), 262 264 'delete_categories' => array( 'administrator', 'editor' ), … … class Tests_User_Capabilities extends WP_UnitTestCase { 296 298 'customize' => array( 'administrator' ), 297 299 'delete_site' => array( 'administrator' ), 298 300 'add_users' => array( 'administrator' ), 301 'resume_plugins' => array( 'administrator' ), 302 'resume_themes' => array( 'administrator' ), 299 303 300 304 'edit_categories' => array( 'administrator', 'editor' ), 301 305 'delete_categories' => array( 'administrator', 'editor' ), … … class Tests_User_Capabilities extends WP_UnitTestCase { 454 458 // Singular object meta capabilities (where an object ID is passed) are not tested: 455 459 $expected['activate_plugin'], 456 460 $expected['deactivate_plugin'], 461 $expected['resume_plugin'], 457 462 $expected['remove_user'], 458 463 $expected['promote_user'], 459 464 $expected['edit_user'],