Make WordPress Core

Changeset 44973


Ignore:
Timestamp:
03/21/2019 09:52:07 PM (8 years ago)
Author:
flixos90
Message:

Bootstrap/Load: Introduce a recovery mode for fixing fatal errors.

Using the new fatal handler introduced in [44962], an email is sent to the admin when a fatal error occurs. This email includes a secret link to enter recovery mode. When clicked, the link will be validated and on success a cookie will be placed on the client, enabling recovery mode for that user. This functionality is executed early before plugins and themes are loaded, in order to be unaffected by potential fatal errors these might be causing.

When in recovery mode, broken plugins and themes will be paused for that client, so that they are able to access the admin backend despite of these errors. They are notified about the broken extensions and the errors caused, and can then decide whether they would like to temporarily deactivate the extension or fix the problem and resume the extension.

A link in the admin bar allows the client to exit recovery mode.

Props timothyblynjacobs, afragen, flixos90, nerrad, miss_jwo, schlessera, spacedmonkey, swissspidy.
Fixes #46130, #44458.

Location:
trunk
Files:
9 added
19 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/css/list-tables.css

    r44791 r44973  
    13111311}
    13121312
     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
    13131338.plugin-card .update-now:before {
    13141339        color: #f56e28;
  • trunk/src/wp-admin/includes/admin-filters.php

    r44717 r44973  
    118118
    119119add_action( 'admin_notices', 'update_nag', 3 );
     120add_action( 'admin_notices', 'paused_plugins_notice', 5 );
     121add_action( 'admin_notices', 'paused_themes_notice', 5 );
    120122add_action( 'admin_notices', 'maintenance_nag', 10 );
    121123
  • trunk/src/wp-admin/includes/class-wp-plugins-list-table.php

    r44937 r44973  
    4141
    4242                $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' ) ) ) {
    4444                        $status = $_REQUEST['plugin_status'];
    4545                }
    … …  
    100100                        'mustuse'            => array(),
    101101                        'dropins'            => array(),
     102                        'paused'             => array(),
    102103                );
    103104
    … …  
    184185                        // Extra info if known. array_merge() ensures $plugin_data has precedence if keys collide.
    185186                        if ( isset( $plugin_info->response[ $plugin_file ] ) ) {
    186                                 $plugins['all'][ $plugin_file ] = $plugin_data = array_merge( (array) $plugin_info->response[ $plugin_file ], $plugin_data );
     187                                $plugin_data                    = array_merge( (array) $plugin_info->response[ $plugin_file ], $plugin_data );
     188                                $plugins['all'][ $plugin_file ] = $plugin_data;
    187189                                // Make sure that $plugins['upgrade'] also receives the extra info since it is used on ?plugin_status=upgrade
    188190                                if ( isset( $plugins['upgrade'][ $plugin_file ] ) ) {
    189                                         $plugins['upgrade'][ $plugin_file ] = $plugin_data = array_merge( (array) $plugin_info->response[ $plugin_file ], $plugin_data );
     191                                        $plugins['upgrade'][ $plugin_file ] = $plugin_data;
    190192                                }
    191193                        } elseif ( isset( $plugin_info->no_update[ $plugin_file ] ) ) {
    192                                 $plugins['all'][ $plugin_file ] = $plugin_data = array_merge( (array) $plugin_info->no_update[ $plugin_file ], $plugin_data );
     194                                $plugin_data                    = array_merge( (array) $plugin_info->no_update[ $plugin_file ], $plugin_data );
     195                                $plugins['all'][ $plugin_file ] = $plugin_data;
    193196                                // Make sure that $plugins['upgrade'] also receives the extra info since it is used on ?plugin_status=upgrade
    194197                                if ( isset( $plugins['upgrade'][ $plugin_file ] ) ) {
    195                                         $plugins['upgrade'][ $plugin_file ] = $plugin_data = array_merge( (array) $plugin_info->no_update[ $plugin_file ], $plugin_data );
     198                                        $plugins['upgrade'][ $plugin_file ] = $plugin_data;
    196199                                }
    197200                        }
    … …  
    219222                                // On the network-admin screen, populate the active list with plugins that are network activated
    220223                                $plugins['active'][ $plugin_file ] = $plugin_data;
     224
     225                                if ( ! $screen->in_admin( 'network' ) && is_plugin_paused( $plugin_file ) ) {
     226                                        $plugins['paused'][ $plugin_file ] = $plugin_data;
     227                                }
    221228                        } else {
    222229                                if ( isset( $recently_activated[ $plugin_file ] ) ) {
    … …  
    445452                                        /* translators: %s: plugin count */
    446453                                        $text = _n( 'Drop-ins <span class="count">(%s)</span>', 'Drop-ins <span class="count">(%s)</span>', $count );
     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 );
    447458                                        break;
    448459                                case 'upgrade':
    … …  
    658669                                                $actions['deactivate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'deactivate-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Deactivate' ) . '</a>';
    659670                                        }
     671                                        if ( current_user_can( 'resume_plugin', $plugin_file ) && is_plugin_paused( $plugin_file ) ) {
     672                                                /* translators: %s: plugin name */
     673                                                $actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume' ) . '</a>';
     674                                        }
    660675                                } else {
    661676                                        if ( current_user_can( 'activate_plugin', $plugin_file ) ) {
    … …  
    764779                if ( ! empty( $totals['upgrade'] ) && ! empty( $plugin_data['update'] ) ) {
    765780                        $class .= ' update';
     781                }
     782
     783                $paused = ! $screen->in_admin( 'network' ) && is_plugin_paused( $plugin_file );
     784
     785                if ( $paused ) {
     786                        $class .= ' paused';
    766787                }
    767788
    … …  
    847868                                         * @param string   $status      Status of the plugin. Defaults are 'All', 'Active',
    848869                                         *                              'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    849                                          *                              'Drop-ins', 'Search'.
     870                                         *                              'Drop-ins', 'Search', 'Paused'.
    850871                                         */
    851872                                        $plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status );
    … …  
    853874
    854875                                        echo '</div>';
     876
     877                                        if ( $paused ) {
     878                                                $notice_text = __( 'This plugin failed to load properly and is paused during recovery mode.' );
     879
     880                                                printf( '<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>', $notice_text );
     881
     882                                                $error = wp_get_plugin_error( $plugin_file );
     883
     884                                                if ( false !== $error ) {
     885                                                        printf( '<div class="error-display"><p>%s</p></div>', wp_get_extension_error_description( $error ) );
     886                                                }
     887                                        }
    855888
    856889                                        echo '</td>';
    … …  
    887920                 * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    888921                 *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    889                  *                            'Drop-ins', 'Search'.
     922                 *                            'Drop-ins', 'Search', 'Paused'.
    890923                 */
    891924                do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status );
    … …  
    903936                 * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    904937                 *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    905                  *                            'Drop-ins', 'Search'.
     938                 *                            'Drop-ins', 'Search', 'Paused'.
    906939                 */
    907940                do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status );
  • trunk/src/wp-admin/includes/plugin.php

    r44717 r44973  
    469469function _get_dropins() {
    470470        $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
    477479        );
    478480
    … …  
    21022104        WP_Privacy_Policy_Content::add( $plugin_name, $policy_text );
    21032105}
     2106
     2107/**
     2108 * Determines whether a plugin is technically active but was paused while
     2109 * loading.
     2110 *
     2111 * For more information on this and similar theme functions, check out
     2112 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
     2113 * Conditional Tags} article in the Theme Developer Handbook.
     2114 *
     2115 * @since 5.2.0
     2116 *
     2117 * @param string $plugin Path to the plugin file relative to the plugins directory.
     2118 * @return bool True, if in the list of paused plugins. False, not in the list.
     2119 */
     2120function is_plugin_paused( $plugin ) {
     2121        if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
     2122                return false;
     2123        }
     2124
     2125        if ( ! is_plugin_active( $plugin ) ) {
     2126                return false;
     2127        }
     2128
     2129        list( $plugin ) = explode( '/', $plugin );
     2130
     2131        return array_key_exists( $plugin, $GLOBALS['_paused_plugins'] );
     2132}
     2133
     2134/**
     2135 * Gets the error that was recorded for a paused plugin.
     2136 *
     2137 * @since 5.2.0
     2138 *
     2139 * @param string $plugin Path to the plugin file relative to the plugins
     2140 *                       directory.
     2141 * @return array|false Array of error information as it was returned by
     2142 *                     `error_get_last()`, or false if none was recorded.
     2143 */
     2144function wp_get_plugin_error( $plugin ) {
     2145        if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
     2146                return false;
     2147        }
     2148
     2149        list( $plugin ) = explode( '/', $plugin );
     2150
     2151        if ( ! array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ) ) {
     2152                return false;
     2153        }
     2154
     2155        return $GLOBALS['_paused_plugins'][ $plugin ];
     2156}
     2157
     2158/**
     2159 * Tries to resume a single plugin.
     2160 *
     2161 * If a redirect was provided, we first ensure the plugin does not throw fatal
     2162 * errors anymore.
     2163 *
     2164 * The way it works is by setting the redirection to the error before trying to
     2165 * include the plugin file. If the plugin fails, then the redirection will not
     2166 * be overwritten with the success message and the plugin will not be resumed.
     2167 *
     2168 * @since 5.2.0
     2169 *
     2170 * @param string $plugin       Single plugin to resume.
     2171 * @param string $redirect     Optional. URL to redirect to. Default empty string.
     2172 * @return bool|WP_Error True on success, false if `$plugin` was not paused,
     2173 *                       `WP_Error` on failure.
     2174 */
     2175function resume_plugin( $plugin, $redirect = '' ) {
     2176        /*
     2177         * We'll override this later if the plugin could be resumed without
     2178         * creating a fatal error.
     2179         */
     2180        if ( ! empty( $redirect ) ) {
     2181                wp_redirect(
     2182                        add_query_arg(
     2183                                '_error_nonce',
     2184                                wp_create_nonce( 'plugin-resume-error_' . $plugin ),
     2185                                $redirect
     2186                        )
     2187                );
     2188
     2189                // Load the plugin to test whether it throws a fatal error.
     2190                ob_start();
     2191                plugin_sandbox_scrape( $plugin );
     2192                ob_clean();
     2193        }
     2194
     2195        list( $extension ) = explode( '/', $plugin );
     2196
     2197        $result = wp_paused_plugins()->delete( $extension );
     2198
     2199        if ( ! $result ) {
     2200                return new WP_Error(
     2201                        'could_not_resume_plugin',
     2202                        __( 'Could not resume the plugin.' )
     2203                );
     2204        }
     2205
     2206        return true;
     2207}
     2208
     2209/**
     2210 * Renders an admin notice in case some plugins have been paused due to errors.
     2211 *
     2212 * @since 5.2.0
     2213 */
     2214function paused_plugins_notice() {
     2215        if ( 'plugins.php' === $GLOBALS['pagenow'] ) {
     2216                return;
     2217        }
     2218
     2219        if ( ! current_user_can( 'resume_plugins' ) ) {
     2220                return;
     2221        }
     2222
     2223        if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) {
     2224                return;
     2225        }
     2226
     2227        printf(
     2228                '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p><a href="%s">%s</a></p></div>',
     2229                __( 'One or more plugins failed to load properly.' ),
     2230                __( 'You can find more details and make changes on the Plugins screen.' ),
     2231                esc_url( admin_url( 'plugins.php?plugin_status=paused' ) ),
     2232                __( 'Go to the Plugins screen' )
     2233        );
     2234}
  • trunk/src/wp-admin/includes/theme.php

    r44717 r44973  
    769769        <?php
    770770}
     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 */
     785function 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 */
     807function 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 * Tries to resume a single theme.
     821 *
     822 * If a redirect was provided and a functions.php file was found, we first ensure that
     823 * functions.php file does not throw fatal errors anymore.
     824 *
     825 * The way it works is by setting the redirection to the error before trying to
     826 * include the file. If the theme fails, then the redirection will not be overwritten
     827 * with the success message and the theme will not be resumed.
     828 *
     829 * @since 5.2.0
     830 *
     831 * @param string $theme    Single theme to resume.
     832 * @param string $redirect Optional. URL to redirect to. Default empty string.
     833 * @return bool|WP_Error True on success, false if `$theme` was not paused,
     834 *                       `WP_Error` on failure.
     835 */
     836function resume_theme( $theme, $redirect = '' ) {
     837        list( $extension ) = explode( '/', $theme );
     838
     839        /*
     840         * We'll override this later if the theme could be resumed without
     841         * creating a fatal error.
     842         */
     843        if ( ! empty( $redirect ) ) {
     844                $functions_path = '';
     845                if ( strpos( STYLESHEETPATH, $extension ) ) {
     846                        $functions_path = STYLESHEETPATH . '/functions.php';
     847                } elseif ( strpos( TEMPLATEPATH, $extension ) ) {
     848                        $functions_path = TEMPLATEPATH . '/functions.php';
     849                }
     850
     851                if ( ! empty( $functions_path ) ) {
     852                        wp_redirect(
     853                                add_query_arg(
     854                                        '_error_nonce',
     855                                        wp_create_nonce( 'theme-resume-error_' . $theme ),
     856                                        $redirect
     857                                )
     858                        );
     859
     860                        // Load the theme's functions.php to test whether it throws a fatal error.
     861                        ob_start();
     862                        include $functions_path;
     863                        ob_clean();
     864                }
     865        }
     866
     867        $result = wp_paused_themes()->delete( $extension );
     868
     869        if ( ! $result ) {
     870                return new WP_Error(
     871                        'could_not_resume_theme',
     872                        __( 'Could not resume the theme.' )
     873                );
     874        }
     875
     876        return true;
     877}
     878
     879/**
     880 * Renders an admin notice in case some themes have been paused due to errors.
     881 *
     882 * @since 5.2.0
     883 */
     884function paused_themes_notice() {
     885        if ( 'themes.php' === $GLOBALS['pagenow'] ) {
     886                return;
     887        }
     888
     889        if ( ! current_user_can( 'resume_themes' ) ) {
     890                return;
     891        }
     892
     893        if ( ! isset( $GLOBALS['_paused_themes'] ) || empty( $GLOBALS['_paused_themes'] ) ) {
     894                return;
     895        }
     896
     897        printf(
     898                '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p><a href="%s">%s</a></p></div>',
     899                __( 'One or more themes failed to load properly.' ),
     900                __( 'You can find more details and make changes on the Themes screen.' ),
     901                esc_url( admin_url( 'themes.php' ) ),
     902                __( 'Go to the Themes screen' )
     903        );
     904}
  • trunk/src/wp-admin/plugins.php

    r44717 r44973  
    390390                        break;
    391391
     392                case 'resume':
     393                        if ( is_multisite() ) {
     394                                return;
     395                        }
     396
     397                        if ( ! current_user_can( 'resume_plugin', $plugin ) ) {
     398                                wp_die( __( 'Sorry, you are not allowed to resume this plugin.' ) );
     399                        }
     400
     401                        check_admin_referer( 'resume-plugin_' . $plugin );
     402
     403                        $result = resume_plugin( $plugin, self_admin_url( "plugins.php?error=resuming&plugin_status=$status&paged=$page&s=$s" ) );
     404
     405                        if ( is_wp_error( $result ) ) {
     406                                wp_die( $result );
     407                        }
     408
     409                        wp_redirect( self_admin_url( "plugins.php?resume=true&plugin_status=$status&paged=$page&s=$s" ) );
     410                        exit;
     411
    392412                default:
    393413                        if ( isset( $_POST['checked'] ) ) {
    … …  
    489509                );
    490510                $errmsg .= ' ' . __( 'If you notice &#8220;headers already sent&#8221; messages, problems with syndication feeds or other issues, try deactivating or removing this plugin.' );
     511        } elseif ( 'resuming' === $_GET['error'] ) {
     512                $errmsg = __( 'Plugin could not be resumed because it triggered a <strong>fatal error</strong>.' );
    491513        } else {
    492514                $errmsg = __( 'Plugin could not be activated because it triggered a <strong>fatal error</strong>.' );
    … …  
    542564<?php elseif ( 'update-selected' == $action ) : ?>
    543565        <div id="message" class="updated notice is-dismissible"><p><?php _e( 'All selected plugins are up to date.' ); ?></p></div>
     566<?php elseif ( isset( $_GET['resume'] ) ) : ?>
     567        <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Plugin <strong>resumed</strong>.' ); ?></p></div>
    544568<?php endif; ?>
    545569
  • trunk/src/wp-admin/themes.php

    r44717 r44973  
    3333                switch_theme( $theme->get_stylesheet() );
    3434                wp_redirect( admin_url( 'themes.php?activated=true' ) );
     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_theme', $_GET['stylesheet'] ) ) {
     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(), self_admin_url( 'themes.php?error=resuming' ) );
     49
     50                if ( is_wp_error( $result ) ) {
     51                        wp_die( $result );
     52                }
     53
     54                wp_redirect( admin_url( 'themes.php?resumed=true' ) );
    3555                exit;
    3656        } elseif ( 'delete' == $_GET['action'] ) {
    … …  
    196216        <div id="message4" class="error"><p><?php _e( 'You cannot delete a theme while it has an active child theme.' ); ?></p></div>
    197217        <?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
     222} elseif ( isset( $_GET['error'] ) && 'resuming' === $_GET['error'] ) {
     223        ?>
     224        <div id="message6" class="error"><p><?php _e( 'Theme could not be resumed because it triggered a <strong>fatal error</strong>.' ); ?></p></div>
     225        <?php
    198226}
    199227
    … …  
    349377
    350378        <?php
     379        $can_resume  = current_user_can( 'resume_themes' );
    351380        $can_delete  = current_user_can( 'delete_themes' );
    352381        $can_install = current_user_can( 'install_themes' );
    … …  
    356385                <th><?php _ex( 'Name', 'theme name' ); ?></th>
    357386                <th><?php _e( 'Description' ); ?></th>
     387                <?php if ( $can_resume ) { ?>
     388                        <td></td>
     389                <?php } ?>
    358390                <?php if ( $can_delete ) { ?>
    359391                        <td></td>
    … …  
    368400                        <td><?php echo $broken_theme->errors()->get_error_message(); ?></td>
    369401                        <?php
     402                        if ( $can_resume ) {
     403                                if ( 'theme_paused' === $broken_theme->errors()->get_error_code() ) {
     404                                        $stylesheet = $broken_theme->get_stylesheet();
     405                                        $resume_url = add_query_arg(
     406                                                array(
     407                                                        'action'     => 'resume',
     408                                                        'stylesheet' => urlencode( $stylesheet ),
     409                                                ),
     410                                                admin_url( 'themes.php' )
     411                                        );
     412                                        $resume_url = wp_nonce_url( $resume_url, 'resume-theme_' . $stylesheet );
     413                                        ?>
     414                                        <td><a href="<?php echo esc_url( $resume_url ); ?>" class="button resume-theme"><?php _e( 'Resume' ); ?></a></td>
     415                                        <?php
     416                                } else {
     417                                        ?>
     418                                        <td></td>
     419                                        <?php
     420                                }
     421                        }
     422
    370423                        if ( $can_delete ) {
    371424                                $stylesheet = $broken_theme->get_stylesheet();
  • trunk/src/wp-includes/admin-bar.php

    r44924 r44973  
    10491049
    10501050/**
     1051 * Add a link to exit recovery mode when Recovery Mode is active.
     1052 *
     1053 * @since 5.2.0
     1054 *
     1055 * @param WP_Admin_Bar $wp_admin_bar
     1056 */
     1057function wp_admin_bar_recovery_mode_menu( $wp_admin_bar ) {
     1058        if ( ! wp_is_recovery_mode() ) {
     1059                return;
     1060        }
     1061
     1062        $url = wp_login_url();
     1063        $url = add_query_arg( 'action', WP_Recovery_Mode::EXIT_ACTION, $url );
     1064        $url = wp_nonce_url( $url, WP_Recovery_Mode::EXIT_ACTION );
     1065
     1066        $wp_admin_bar->add_menu(
     1067                array(
     1068                        'parent' => 'top-secondary',
     1069                        'id'     => 'recovery-mode',
     1070                        'title'  => __( 'Exit Recovery Mode' ),
     1071                        'href'   => $url,
     1072                        'meta'   => array(
     1073                                'title' => __( 'Exit Recovery Mode' ),
     1074                        ),
     1075                )
     1076        );
     1077}
     1078
     1079/**
    10511080 * Add secondary menus.
    10521081 *
  • trunk/src/wp-includes/capabilities.php

    r44717 r44973  
    465465                        }
    466466                        break;
     467                case 'resume_plugin':
     468                        $caps[] = 'resume_plugins';
     469                        break;
     470                case 'resume_theme':
     471                        $caps[] = 'resume_themes';
     472                        break;
    467473                case 'delete_user':
    468474                case 'delete_users':
    … …  
    951957        return $allcaps;
    952958}
     959
     960/**
     961 * Filters the user capabilities to grant the 'resume_plugins' and 'resume_themes' capabilities as necessary.
     962 *
     963 * @since 5.2.0
     964 *
     965 * @param bool[] $allcaps An array of all the user's capabilities.
     966 * @return bool[] Filtered array of the user's capabilities.
     967 */
     968function wp_maybe_grant_resume_extensions_caps( $allcaps ) {
     969        // Even in a multisite, regular administrators should be able to resume plugins.
     970        if ( ! empty( $allcaps['activate_plugins'] ) ) {
     971                $allcaps['resume_plugins'] = true;
     972        }
     973
     974        // Even in a multisite, regular administrators should be able to resume themes.
     975        if ( ! empty( $allcaps['switch_themes'] ) ) {
     976                $allcaps['resume_themes'] = true;
     977        }
     978
     979        return $allcaps;
     980}
  • trunk/src/wp-includes/class-wp-admin-bar.php

    r44793 r44973  
    597597                add_action( 'admin_bar_menu', 'wp_admin_bar_search_menu', 4 );
    598598                add_action( 'admin_bar_menu', 'wp_admin_bar_my_account_item', 7 );
     599                add_action( 'admin_bar_menu', 'wp_admin_bar_recovery_mode_menu', 8 );
    599600
    600601                // Site related.
  • trunk/src/wp-includes/class-wp-fatal-error-handler.php

    r44962 r44973  
    3737                        if ( ! $error ) {
    3838                                return;
     39                        }
     40
     41                        if ( ! is_multisite() && wp_recovery_mode()->is_initialized() ) {
     42                                wp_recovery_mode()->handle_error( $error );
    3943                        }
    4044
  • trunk/src/wp-includes/class-wp-theme.php

    r44717 r44973  
    370370                        // Set the parent. Pass the current instance so we can do the crazy checks above and assess errors.
    371371                        $this->parent = new WP_Theme( $this->template, isset( $theme_root_template ) ? $theme_root_template : $this->theme_root, $this );
     372                }
     373
     374                if ( wp_paused_themes()->get( $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.' ) );
    372376                }
    373377
  • trunk/src/wp-includes/default-constants.php

    r44151 r44973  
    303303                define( 'COOKIE_DOMAIN', false );
    304304        }
     305
     306        if ( ! defined( 'RECOVERY_MODE_COOKIE' ) ) {
     307                /**
     308                 * @since 5.2.0
     309                 */
     310                define( 'RECOVERY_MODE_COOKIE', 'wordpress_rec_' . COOKIEHASH );
     311        }
    305312}
    306313
  • trunk/src/wp-includes/default-filters.php

    r44942 r44973  
    580580// Capabilities
    581581add_filter( 'user_has_cap', 'wp_maybe_grant_install_languages_cap', 1 );
     582add_filter( 'user_has_cap', 'wp_maybe_grant_resume_extensions_caps', 1 );
    582583
    583584unset( $filter, $action );
  • trunk/src/wp-includes/error-protection.php

    r44962 r44973  
    66 * @since   5.2.0
    77 */
     8
     9/**
     10 * Get the instance for storing paused plugins.
     11 *
     12 * @return WP_Paused_Extensions_Storage
     13 */
     14function wp_paused_plugins() {
     15        static $storage = null;
     16
     17        if ( null === $storage ) {
     18                $storage = new WP_Paused_Extensions_Storage( 'plugin' );
     19        }
     20
     21        return $storage;
     22}
     23
     24/**
     25 * Get the instance for storing paused extensions.
     26 *
     27 * @return WP_Paused_Extensions_Storage
     28 */
     29function wp_paused_themes() {
     30        static $storage = null;
     31
     32        if ( null === $storage ) {
     33                $storage = new WP_Paused_Extensions_Storage( 'theme' );
     34        }
     35
     36        return $storage;
     37}
     38
     39/**
     40 * Get a human readable description of an extension's error.
     41 *
     42 * @since 5.2.0
     43 *
     44 * @param array $error Error details {@see error_get_last()}
     45 *
     46 * @return string Formatted error description.
     47 */
     48function wp_get_extension_error_description( $error ) {
     49        $constants   = get_defined_constants( true );
     50        $constants   = isset( $constants['Core'] ) ? $constants['Core'] : $constants['internal'];
     51        $core_errors = array();
     52
     53        foreach ( $constants as $constant => $value ) {
     54                if ( 0 === strpos( $constant, 'E_' ) ) {
     55                        $core_errors[ $value ] = $constant;
     56                }
     57        }
     58
     59        if ( isset( $core_errors[ $error['type'] ] ) ) {
     60                $error['type'] = $core_errors[ $error['type'] ];
     61        }
     62
     63        /* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */
     64        $error_message = __( 'An error of type %1$s was caused in line %2$s of the file %3$s. Error message: %4$s' );
     65
     66        return sprintf(
     67                $error_message,
     68                "<code>{$error['type']}</code>",
     69                "<code>{$error['line']}</code>",
     70                "<code>{$error['file']}</code>",
     71                "<code>{$error['message']}</code>"
     72        );
     73}
    874
    975/**
    … …  
    53119        return apply_filters( 'wp_fatal_error_handler_enabled', $enabled );
    54120}
     121
     122/**
     123 * Access the WordPress Recovery Mode instance.
     124 *
     125 * @since 5.2.0
     126 *
     127 * @return WP_Recovery_Mode
     128 */
     129function wp_recovery_mode() {
     130        static $wp_recovery_mode;
     131
     132        if ( ! $wp_recovery_mode ) {
     133                $wp_recovery_mode = new WP_Recovery_Mode();
     134        }
     135
     136        return $wp_recovery_mode;
     137}
  • trunk/src/wp-includes/load.php

    r44717 r44973  
    698698        }
    699699
     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 */
     719function wp_skip_paused_plugins( array $plugins ) {
     720        $paused_plugins = wp_paused_plugins()->get_all();
     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
    700737        return $plugins;
    701738}
    … …  
    726763        $themes[] = TEMPLATEPATH;
    727764
     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
    728778        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 */
     789function wp_skip_paused_themes( array $themes ) {
     790        $paused_themes = wp_paused_themes()->get_all();
     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
     807        return $themes;
     808}
     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 */
     819function wp_is_recovery_mode() {
     820        return wp_recovery_mode()->is_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 */
     830function 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 */
     867function 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;
    729903}
    730904
  • trunk/src/wp-login.php

    r44932 r44973  
    440440
    441441// 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 ) ) {
     442if ( ! in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'confirmaction', WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTERED ), true ) && false === has_filter( 'login_form_' . $action ) ) {
    443443        $action = 'login';
    444444}
    … …  
    10291029                        } elseif ( strpos( $redirect_to, 'about.php?updated' ) ) {
    10301030                                $errors->add( 'updated', __( '<strong>You have successfully updated WordPress!</strong> Please log back in to see what&#8217;s new.' ), 'message' );
     1031                        } elseif ( WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTERED === $action ) {
     1032                                $errors->add( 'enter_recovery_mode', __( 'Recovery Mode Initialized. Please log in to continue.' ), 'message' );
    10311033                        }
    10321034                }
  • trunk/src/wp-settings.php

    r44962 r44973  
    1818// Include files required for initialization.
    1919require( ABSPATH . WPINC . '/load.php' );
     20require( ABSPATH . WPINC . '/class-wp-paused-extensions-storage.php' );
    2021require( ABSPATH . WPINC . '/class-wp-fatal-error-handler.php' );
     22require( ABSPATH . WPINC . '/class-wp-recovery-mode-cookie-service.php' );
     23require( ABSPATH . WPINC . '/class-wp-recovery-mode-key-service.php' );
     24require( ABSPATH . WPINC . '/class-wp-recovery-mode-link-service.php' );
     25require( ABSPATH . WPINC . '/class-wp-recovery-mode-email-service.php' );
     26require( ABSPATH . WPINC . '/class-wp-recovery-mode.php' );
    2127require( ABSPATH . WPINC . '/error-protection.php' );
    2228require( ABSPATH . WPINC . '/default-constants.php' );
    … …  
    346352register_theme_directory( get_theme_root() );
    347353
     354if ( ! is_multisite() ) {
     355        // Handle users requesting a recovery mode link and initiating recovery mode.
     356        wp_recovery_mode()->initialize();
     357}
     358
    348359// Load active plugins.
    349360foreach ( wp_get_active_and_valid_plugins() as $plugin ) {
  • trunk/tests/phpunit/tests/user/capabilities.php

    r44717 r44973  
    102102                        'switch_themes'          => array( 'administrator' ),
    103103                        'edit_dashboard'         => array( 'administrator' ),
     104                        'resume_plugins'         => array( 'administrator' ),
     105                        'resume_themes'          => array( 'administrator' ),
    104106
    105107                        'moderate_comments'      => array( 'administrator', 'editor' ),
    … …  
    182184                        'switch_themes'          => array( 'administrator' ),
    183185                        'edit_dashboard'         => array( 'administrator' ),
     186                        'resume_plugins'         => array( 'administrator' ),
     187                        'resume_themes'          => array( 'administrator' ),
    184188
    185189                        'moderate_comments'      => array( 'administrator', 'editor' ),
    … …  
    393397                        $actual['author'],
    394398                        $actual['subscriber'],
    395                         $actual['contributor']
     399                        $actual['contributor'],
     400                        // the following two are granted via `user_has_cap`:
     401                        $actual['resume_plugins'],
     402                        $actual['resume_themes']
    396403                );
    397404
    … …  
    455462                        $expected['activate_plugin'],
    456463                        $expected['deactivate_plugin'],
     464                        $expected['resume_plugin'],
     465                        $expected['resume_theme'],
    457466                        $expected['remove_user'],
    458467                        $expected['promote_user'],
Note: See TracChangeset for help on using the changeset viewer.