WordPress.org

Make WordPress Core

Ticket #44458: 44458.8.diff

File 44458.8.diff, 31.4 KB (added by flixos90, 17 months 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 893326ba38..0e8891ec0e 100644
    a b ul.cat-checklist { 
    13011301        text-decoration: underline;
    13021302}
    13031303
     1304.plugins tr.paused th.check-column {
     1305        border-left: 4px solid #d54e21;
     1306}
     1307
     1308.plugins tr.paused th,
     1309.plugins tr.paused td {
     1310        background-color: #fef7f1;
     1311}
     1312
     1313.plugins tr.paused .plugin-title,
     1314.plugins .paused .dashicons-warning {
     1315        color: #dc3232;
     1316}
     1317
     1318.plugins .paused .error-display p,
     1319.plugins .paused .error-display code {
     1320        font-size: 90%;
     1321        font-style: italic;
     1322        color: rgb( 0, 0, 0, 0.7 );
     1323}
     1324
     1325.plugins .resume-link {
     1326        color: #dc3232;
     1327}
     1328
    13041329.plugin-card .update-now:before {
    13051330        color: #f56e28;
    13061331        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 0816b2420b..bc2f769b64 100644
    a b  
    121121add_action( 'load-themes.php', 'wp_theme_update_rows', 20 ); // After wp_update_themes() is called.
    122122
    123123add_action( 'admin_notices', 'update_nag', 3 );
     124add_action( 'admin_notices', 'paused_plugins_notice', 5 );
    124125add_action( 'admin_notices', 'maintenance_nag', 10 );
    125126
    126127add_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 1f540c9ed2..e325fa0193 100644
    a b public function __construct( $args = array() ) { 
    4040                );
    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                }
    4646
    public function prepare_items() { 
    9999                        'upgrade'            => array(),
    100100                        'mustuse'            => array(),
    101101                        'dropins'            => array(),
     102                        'paused'             => array(),
    102103                );
    103104
    104105                $screen = $this->screen;
    public function prepare_items() { 
    209210                                if ( $show_network_active ) {
    210211                                        // On the non-network screen, show network-active plugins if allowed
    211212                                        $plugins['active'][ $plugin_file ] = $plugin_data;
     213                                        if ( is_plugin_paused( $plugin_file ) ) {
     214                                                $plugins['paused'][ $plugin_file ] = $plugin_data;
     215                                        }
    212216                                } else {
    213217                                        // On the non-network screen, filter out network-active plugins
    214218                                        unset( $plugins['all'][ $plugin_file ] );
    public function prepare_items() { 
    218222                                // On the non-network screen, populate the active list with plugins that are individually activated
    219223                                // On the network-admin screen, populate the active list with plugins that are network activated
    220224                                $plugins['active'][ $plugin_file ] = $plugin_data;
     225                                if ( is_plugin_paused( $plugin_file ) ) {
     226                                        $plugins['paused'][ $plugin_file ] = $plugin_data;
     227                                }
    221228                        } else {
    222229                                if ( isset( $recently_activated[ $plugin_file ] ) ) {
    223230                                        // Populate the recently activated list with plugins that have been recently activated
    protected function get_views() { 
    421428
    422429                        switch ( $type ) {
    423430                                case 'all':
     431                                        /* translators: %s: plugin count */
    424432                                        $text = _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $count, 'plugins' );
    425433                                        break;
    426434                                case 'active':
     435                                        /* translators: %s: plugin count */
    427436                                        $text = _n( 'Active <span class="count">(%s)</span>', 'Active <span class="count">(%s)</span>', $count );
    428437                                        break;
    429438                                case 'recently_activated':
     439                                        /* translators: %s: plugin count */
    430440                                        $text = _n( 'Recently Active <span class="count">(%s)</span>', 'Recently Active <span class="count">(%s)</span>', $count );
    431441                                        break;
    432442                                case 'inactive':
     443                                        /* translators: %s: plugin count */
    433444                                        $text = _n( 'Inactive <span class="count">(%s)</span>', 'Inactive <span class="count">(%s)</span>', $count );
    434445                                        break;
    435446                                case 'mustuse':
     447                                        /* translators: %s: plugin count */
    436448                                        $text = _n( 'Must-Use <span class="count">(%s)</span>', 'Must-Use <span class="count">(%s)</span>', $count );
    437449                                        break;
    438450                                case 'dropins':
     451                                        /* translators: %s: plugin count */
    439452                                        $text = _n( 'Drop-ins <span class="count">(%s)</span>', 'Drop-ins <span class="count">(%s)</span>', $count );
    440453                                        break;
     454                                case 'paused':
     455                                        /* translators: %s: plugin count */
     456                                        $text = _n( 'Paused <span class="count">(%s)</span>', 'Paused <span class="count">(%s)</span>', $count );
     457                                        break;
    441458                                case 'upgrade':
     459                                        /* translators: %s: plugin count */
    442460                                        $text = _n( 'Update Available <span class="count">(%s)</span>', 'Update Available <span class="count">(%s)</span>', $count );
    443461                                        break;
    444462                        }
    public function single_row( $item ) { 
    649667                                                /* translators: %s: plugin name */
    650668                                                $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>';
    651669                                        }
     670                                        if ( current_user_can( 'resume_plugin' ) && 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&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 execution of %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume execution' ) . '</a>';
     673                                        }
    652674                                } else {
    653675                                        if ( current_user_can( 'activate_plugin', $plugin_file ) ) {
    654676                                                /* translators: %s: plugin name */
    public function single_row( $item ) { 
    755777                        $class .= ' update';
    756778                }
    757779
     780                $paused = is_plugin_paused( $plugin_file );
     781                if ( $paused ) {
     782                        $class .= ' paused';
     783                }
     784
    758785                $plugin_slug = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_name );
    759786                printf(
    760787                        '<tr class="%s" data-slug="%s" data-plugin="%s">',
    public function single_row( $item ) { 
    833860                                         * @param array    $plugin_data An array of plugin data.
    834861                                         * @param string   $status      Status of the plugin. Defaults are 'All', 'Active',
    835862                                         *                              'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    836                                          *                              'Drop-ins', 'Search'.
     863                                         *                              'Drop-ins', 'Search', 'Paused'
    837864                                         */
    838865                                        $plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status );
    839866                                        echo implode( ' | ', $plugin_meta );
    840867
    841                                         echo '</div></td>';
     868                                        echo '</div>';
     869
     870                                        if ( $paused ) {
     871                                                echo sprintf(
     872                                                        '<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>',
     873                                                        __( 'This plugin failed to load properly and was paused within the admin backend.' )
     874                                                );
     875
     876                                                $error = wp_get_plugin_error( $plugin_file );
     877
     878                                                if ( false !== $error ) {
     879                                                        $constants = get_defined_constants( true );
     880                                                        $constants = isset( $constants['Core'] ) ? $constants['Core'] : $constants['internal'];
     881
     882                                                        foreach ( $constants as $constant => $value ) {
     883                                                                if ( 0 === strpos( $constant, 'E_' ) ) {
     884                                                                        $core_errors[ $value ] = $constant;
     885                                                                }
     886                                                        }
     887
     888                                                        $error['type'] = $core_errors[ $error['type'] ];
     889
     890                                                        echo sprintf(
     891                                                                '<div class="error-display"><p>%s</p></div>',
     892                                                                sprintf(
     893                                                                        /* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */
     894                                                                        __( 'The plugin caused an error of type %1$s in line %2$s of the file %3$s. Error message: %4$s' ),
     895                                                                        "<code>{$error['type']}</code>",
     896                                                                        "<code>{$error['line']}</code>",
     897                                                                        "<code>{$error['file']}</code>",
     898                                                                        "<code>{$error['message']}</code>"
     899                                                                )
     900                                                        );
     901                                                }
     902                                        }
     903
     904                                        echo '</td>';
    842905                                        break;
    843906                                default:
    844907                                        $classes = "$column_name column-$column_name $class";
    public function single_row( $item ) { 
    871934                 * @param array  $plugin_data An array of plugin data.
    872935                 * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    873936                 *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    874                  *                            'Drop-ins', 'Search'.
     937                 *                            'Drop-ins', 'Search', 'Paused'.
    875938                 */
    876939                do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status );
    877940
    public function single_row( $item ) { 
    887950                 * @param array  $plugin_data An array of plugin data.
    888951                 * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    889952                 *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    890                  *                            'Drop-ins', 'Search'.
     953                 *                            'Drop-ins', 'Search', 'Paused'
    891954                 */
    892955                do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status );
    893956        }
  • src/wp-admin/includes/plugin.php

    diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php
    index c898fc5169..0ebfc8830f 100644
    a b function get_dropins() { 
    438438 */
    439439function _get_dropins() {
    440440        $dropins = array(
    441                 'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE
    442                 'db.php'             => array( __( 'Custom database class.' ), true ), // auto on load
    443                 'db-error.php'       => array( __( 'Custom database error message.' ), true ), // auto on error
    444                 'install.php'        => array( __( 'Custom installation script.' ), true ), // auto on installation
    445                 'maintenance.php'    => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance
    446                 'object-cache.php'   => array( __( 'External object cache.' ), true ), // auto on load
     441                'advanced-cache.php'   => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE
     442                'db.php'               => array( __( 'Custom database class.' ), true ), // auto on load
     443                'db-error.php'         => array( __( 'Custom database error message.' ), true ), // auto on error
     444                'install.php'          => array( __( 'Custom installation script.' ), true ), // auto on installation
     445                'maintenance.php'      => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance
     446                'object-cache.php'     => array( __( 'External object cache.' ), true ), // auto on load
     447                'php-error.php'        => array( __( 'Custom PHP error message.' ), true ), // auto on error
     448                'shutdown-handler.php' => array( __( 'Custom PHP shutdown handler.' ), true ), // auto on error
    447449        );
    448450
    449451        if ( is_multisite() ) {
    function is_plugin_inactive( $plugin ) { 
    496498        return ! is_plugin_active( $plugin );
    497499}
    498500
     501/**
     502 * Determines whether a plugin is technically active but was paused while
     503 * loading.
     504 *
     505 * For more information on this and similar theme functions, check out
     506 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
     507 * Conditional Tags} article in the Theme Developer Handbook.
     508 *
     509 * @since 5.0.0
     510 *
     511 * @param string $plugin Path to the plugin file relative to the plugins directory.
     512 * @return bool True, if in the list of paused plugins. False, not in the list.
     513 */
     514function is_plugin_paused( $plugin ) {
     515        if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
     516                return false;
     517        }
     518
     519        if ( ! is_plugin_active( $plugin ) || is_plugin_active_for_network( $plugin ) ) {
     520                return false;
     521        }
     522
     523        list( $plugin ) = explode( '/', $plugin );
     524
     525        return array_key_exists( $plugin, $GLOBALS['_paused_plugins'] );
     526}
     527
     528/**
     529 * Gets the error that was recorded for a paused plugin.
     530 *
     531 * @since 5.0.0
     532 *
     533 * @param string $plugin Path to the plugin file relative to the plugins
     534 *                       directory.
     535 * @return array|false Array of error information as it was returned by
     536 *                     `error_get_last()`, or false if none was recorded.
     537 */
     538function wp_get_plugin_error( $plugin ) {
     539        if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
     540                return false;
     541        }
     542
     543        list( $plugin ) = explode( '/', $plugin );
     544
     545        if ( ! array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ) ) {
     546                return false;
     547        }
     548
     549        return $GLOBALS['_paused_plugins'][ $plugin ];
     550}
     551
    499552/**
    500553 * Determines whether the plugin is active for the entire network.
    501554 *
    function deactivate_plugins( $plugins, $silent = false, $network_wide = null ) { 
    693746                        continue;
    694747                }
    695748
     749                // Clean up the database before deactivating the plugin.
     750                if ( is_plugin_paused( $plugin ) ) {
     751                        resume_plugin( $plugin );
     752                }
     753
    696754                $network_deactivating = false !== $network_wide && is_plugin_active_for_network( $plugin );
    697755
    698756                if ( ! $silent ) {
    function delete_plugins( $plugins, $deprecated = '' ) { 
    887945                        uninstall_plugin( $plugin_file );
    888946                }
    889947
     948                // Clean up the database before removing the plugin.
     949                if ( is_plugin_paused( $plugin_file ) ) {
     950                        resume_plugin( $plugin_file );
     951                }
     952
    890953                /**
    891954                 * Fires immediately before a plugin deletion attempt.
    892955                 *
    function delete_plugins( $plugins, $deprecated = '' ) { 
    9591022        return true;
    9601023}
    9611024
     1025/**
     1026 * Tries to resume a single plugin.
     1027 *
     1028 * Resuming the plugin basically means removing its entry from the
     1029 * `pause_on_admin` database option.
     1030 *
     1031 * If a redirect was provided, we first ensure the plugin does not throw fatal
     1032 * errors anymore.
     1033 *
     1034 * The way it works is by setting the redirection to the error before trying to
     1035 * include the plugin file. If the plugin fails, then the redirection will not
     1036 * be overwritten with the success message and the `pause_on_admin` option
     1037 * will not be updated.
     1038 *
     1039 * @since 5.0.0
     1040 *
     1041 * @param string $plugin   Single plugin to resume.
     1042 * @param string $redirect Optional. URL to redirect to.
     1043 *
     1044 * @return bool|WP_Error True on success, false if `$plugin` was not paused,
     1045 *                       `WP_Error` on failure.
     1046 */
     1047function resume_plugin( $plugin, $redirect = '' ) {
     1048        /*
     1049         * We'll override this later if the plugin could be included without
     1050         * creating a fatal error.
     1051         */
     1052        if ( ! empty( $redirect ) ) {
     1053                wp_redirect(
     1054                        add_query_arg(
     1055                                '_error_nonce',
     1056                                wp_create_nonce( 'plugin-resume-error_' . $plugin ),
     1057                                $redirect
     1058                        )
     1059                );
     1060
     1061                // Load the plugin to test whether it throws a fatal error.
     1062                ob_start();
     1063                $plugin_path = WP_PLUGIN_DIR . '/' . $plugin;
     1064                wp_register_plugin_realpath( $plugin_path );
     1065                include_once $plugin_path;
     1066                ob_clean();
     1067        }
     1068
     1069        $result = wp_forget_extension_error( 'plugins', $plugin );
     1070
     1071        if ( ! $result ) {
     1072                return new WP_Error(
     1073                        'could_not_resume_plugin',
     1074                        __( 'Could not resume execution of the plugin.' )
     1075                );
     1076        }
     1077
     1078        return true;
     1079}
     1080
    9621081/**
    9631082 * Validate active plugins
    9641083 *
    function wp_add_privacy_policy_content( $plugin_name, $policy_text ) { 
    20662185
    20672186        WP_Privacy_Policy_Content::add( $plugin_name, $policy_text );
    20682187}
     2188
     2189/**
     2190 * Renders an admin notice in case some plugins have been paused due to errors.
     2191 *
     2192 * @since 5.0.0
     2193 *
     2194 * @return void
     2195 */
     2196function paused_plugins_notice() {
     2197        if ( 'plugins.php' === $GLOBALS['pagenow'] ) {
     2198                return;
     2199        }
     2200
     2201        if ( ! current_user_can( 'deactivate_plugins' ) ) {
     2202                return;
     2203        }
     2204
     2205        if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) {
     2206                return;
     2207        }
     2208
     2209        echo sprintf(
     2210                '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p>%s</p></div>',
     2211                __( 'One or more plugins failed to load properly.' ),
     2212                __( 'You can find more details and make changes on the Plugins screen.' ),
     2213                sprintf(
     2214                        '<a href="%s">%s</a>',
     2215                        admin_url( 'plugins.php?plugin_status=paused' ),
     2216                        'Go to the Plugins screen'
     2217                )
     2218        );
     2219}
  • src/wp-admin/plugins.php

    diff --git a/src/wp-admin/plugins.php b/src/wp-admin/plugins.php
    index 4d8c797b04..db0af2cc9b 100644
    a b  
    389389                        }
    390390                        break;
    391391
     392                case 'resume':
     393                        if ( ! current_user_can( 'resume_plugin', $plugin ) ) {
     394                                wp_die( __( 'Sorry, you are not allowed to resume execution of 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' ) );
     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
    392413                default:
    393414                        if ( isset( $_POST['checked'] ) ) {
    394415                                check_admin_referer( 'bulk-plugins' );
     
    480501                $errmsg = __( 'You cannot delete a plugin while it is active on the main site.' );
    481502        } elseif ( isset( $_GET['charsout'] ) ) {
    482503                $errmsg = sprintf( __( 'The plugin generated %d characters of <strong>unexpected output</strong> during activation. If you notice &#8220;headers already sent&#8221; messages, problems with syndication feeds or other issues, try deactivating or removing this plugin.' ), $_GET['charsout'] );
     504        } elseif ( 'resuming' === $_GET['error'] ) {
     505                $errmsg = __( 'Plugin could not be resumed because it triggered a <strong>fatal error</strong>.' );
    483506        } else {
    484507                $errmsg = __( 'Plugin could not be activated because it triggered a <strong>fatal error</strong>.' );
    485508        }
     
    533556        <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Selected plugins <strong>deactivated</strong>.' ); ?></p></div>
    534557<?php elseif ( 'update-selected' == $action ) : ?>
    535558        <div id="message" class="updated notice is-dismissible"><p><?php _e( 'All selected plugins are up to date.' ); ?></p></div>
     559<?php elseif ( isset( $_GET['resume'] ) ) : ?>
     560        <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Execution of plugin <strong>resumed</strong>.' ); ?></p></div>
    536561<?php endif; ?>
    537562
    538563<div class="wrap">
  • src/wp-includes/capabilities.php

    diff --git a/src/wp-includes/capabilities.php b/src/wp-includes/capabilities.php
    index 375648e768..e65ff0b5a2 100644
    a b function map_meta_cap( $cap, $user_id ) { 
    455455                case 'deactivate_plugins':
    456456                case 'activate_plugin':
    457457                case 'deactivate_plugin':
     458                case 'resume_plugin':
    458459                        $caps[] = 'activate_plugins';
    459460                        if ( is_multisite() ) {
    460461                                // update_, install_, and delete_ are handled above with is_super_admin().
  • src/wp-includes/load.php

    diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php
    index 79e445021b..345087fbf5 100644
    a b function wp_get_active_and_valid_plugins() { 
    687687                        $plugins[] = WP_PLUGIN_DIR . '/' . $plugin;
    688688                }
    689689        }
     690
     691        /*
     692         * Remove plugins from the list of active plugins when we're on an endpoint
     693         * that should be protected against WSODs and the plugin appears in the
     694         * `pause_on_admin` list.
     695         */
     696        if ( is_protected_endpoint() ) {
     697                $pause_on_admin = (array) get_option( 'pause_on_admin', array() );
     698
     699                if ( ! array_key_exists( 'plugins', $pause_on_admin ) ) {
     700                        return $plugins;
     701                }
     702
     703                foreach ( $plugins as $index => $plugin ) {
     704                        $parts = explode(
     705                                '/',
     706                                str_replace( wp_normalize_path( WP_CONTENT_DIR . '/' ), '', wp_normalize_path( $plugin ) )
     707                        );
     708
     709                        $type   = array_shift( $parts );
     710                        $plugin = array_shift( $parts );
     711
     712                        if ( array_key_exists( $plugin, $pause_on_admin[ $type ] ) ) {
     713                                unset( $plugins[ $index ] );
     714                                // Store list of paused plugins for displaying an admin notice.
     715                                $GLOBALS['_paused_plugins'][ $plugin ] = $pause_on_admin[ $type ][ $plugin ];
     716                        }
     717                }
     718        }
     719
    690720        return $plugins;
    691721}
    692722
    function wp_doing_ajax() { 
    11541184        return apply_filters( 'wp_doing_ajax', defined( 'DOING_AJAX' ) && DOING_AJAX );
    11551185}
    11561186
     1187/**
     1188 * Determines whether we are currently on an endpoint that should be protected
     1189 * against WSODs.
     1190 *
     1191 * @since 5.0.0
     1192 *
     1193 * @return bool True if the current endpoint should be protected.
     1194 */
     1195function is_protected_endpoint() {
     1196        // Protect login pages.
     1197        if ( 'wp-login.php' === $GLOBALS['pagenow'] ) {
     1198                return true;
     1199        }
     1200
     1201        // Protect the admin backend.
     1202        if ( is_admin() && ! wp_doing_ajax() ) {
     1203                return true;
     1204        }
     1205
     1206        // Protect AJAX actions that could help resolve a fatal error should be available.
     1207        if ( wp_doing_ajax() && is_protected_ajax_action() ) {
     1208                return true;
     1209        }
     1210
     1211        /**
     1212         * Filters whether the current request is against a protected endpoint.
     1213         *
     1214         * This filter is only fired when an endpoint is requested which is not already protected by
     1215         * WordPress core. As such, it exclusively allows providing further protected endpoints in
     1216         * addition to the admin backend, login pages and protected AJAX actions.
     1217         *
     1218         * @since 5.0.0
     1219         *
     1220         * @param bool $is_protected_endpoint Whether the currently requested endpoint is protected.
     1221         *                                    Default false.
     1222         */
     1223        return (bool) apply_filters( 'is_protected_endpoint', false );
     1224}
     1225
     1226/**
     1227 * Determines whether we are currently handling an AJAX action that should be
     1228 * protected against WSODs.
     1229 *
     1230 * @since 5.0.0
     1231 *
     1232 * @return bool True if the current AJAX action should be protected.
     1233 */
     1234function is_protected_ajax_action() {
     1235        $actions_to_protect = array(
     1236                'edit-theme-plugin-file', // Saving changes in the core code editor.
     1237                'heartbeat',              // Keep the heart beating.
     1238                'install-plugin',         // Installing a new plugin.
     1239                'install-theme',          // Installing a new theme.
     1240                'search-plugins',         // Searching in the list of plugins.
     1241                'search-install-plugins', // Searching for a plugin in the plugin install screen.
     1242                'update-plugin',          // Update an existing plugin.
     1243                'update-theme',           // Update an existing theme.
     1244        );
     1245
     1246        if ( ! wp_doing_ajax() ) {
     1247                return false;
     1248        }
     1249
     1250        if ( ! isset( $_REQUEST['action'] ) ) {
     1251                return false;
     1252        }
     1253
     1254        if ( ! in_array( $_REQUEST['action'], $actions_to_protect, true ) ) {
     1255                return false;
     1256        }
     1257
     1258        return true;
     1259}
     1260
    11571261/**
    11581262 * Determines whether the current request is a WordPress cron request.
    11591263 *
    function wp_finalize_scraping_edited_file_errors( $scrape_key ) { 
    12501354        }
    12511355        echo "\n###### wp_scraping_result_end:$scrape_key ######\n";
    12521356}
     1357
     1358/**
     1359 * Prunes the array of recorded extension errors.
     1360 *
     1361 * @since 5.0.0
     1362 *
     1363 * @param array $errors Array of errors to prune.
     1364 * @return array Pruned array of errors.
     1365 */
     1366function wp_prune_extension_errors( $errors ) {
     1367        foreach ( array( 'plugins', 'mu-plugins', 'themes' ) as $type ) {
     1368                if ( ! array_key_exists( $type, $errors ) ) {
     1369                        continue;
     1370                }
     1371
     1372                switch ( $type ) {
     1373                        case 'plugins':
     1374                                $active_plugins = array_merge(
     1375                                        (array) get_option( 'active_plugins', array() ),
     1376                                        (array) get_option( 'active_sitewide_plugins', array() )
     1377                                );
     1378
     1379                                foreach ( $errors[ $type ] as $plugin => $error ) {
     1380                                        $found = false;
     1381
     1382                                        foreach ( $active_plugins as $active_plugin ) {
     1383                                                list( $active_plugin ) = explode( '/', $active_plugin );
     1384
     1385                                                if ( $active_plugin === $plugin ) {
     1386                                                        $found = true;
     1387                                                        break;
     1388                                                }
     1389                                        }
     1390
     1391                                        if ( ! $found ) {
     1392                                                unset( $errors[ $type ][ $plugin ] );
     1393                                        }
     1394                                }
     1395
     1396                                break;
     1397                        case 'mu-plugins':
     1398                                // TODO: Implement MU-plugin-specific behavior.
     1399                                break;
     1400                        case 'themes':
     1401                                // TODO: Implement theme-specific behavior.
     1402                                break;
     1403                }
     1404
     1405                if ( 0 === count( $errors[ $type ] ) ) {
     1406                        unset( $errors[ $type ] );
     1407                }
     1408        }
     1409
     1410        return $errors;
     1411}
     1412
     1413/**
     1414 * Records the extension error as a database option.
     1415 *
     1416 * @since 5.0.0
     1417 *
     1418 * @global array $wp_theme_directories
     1419 *
     1420 * @param array $error Error that was triggered.
     1421 * @return bool Whether the error was correctly recorded.
     1422 */
     1423function wp_record_extension_error( $error ) {
     1424        global $wp_theme_directories;
     1425
     1426        if ( ! function_exists( 'get_option' ) ) {
     1427                return false;
     1428        }
     1429
     1430        $path = '';
     1431
     1432        $error_file      = wp_normalize_path( $error['file'] );
     1433        $wp_plugin_dir   = wp_normalize_path( WP_PLUGIN_DIR );
     1434        $wpmu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR );
     1435
     1436        if ( 0 === strpos( $error_file, $wp_plugin_dir ) ) {
     1437                $type = 'plugins';
     1438                $path = str_replace( $wp_plugin_dir . '/', '', $error_file );
     1439        } elseif ( 0 === strpos( $error_file, $wpmu_plugin_dir ) ) {
     1440                $type = 'mu-plugins';
     1441                $path = str_replace( $wpmu_plugin_dir . '/', '', $error_file );
     1442        } else {
     1443                foreach ( $wp_theme_directories as $theme_directory ) {
     1444                        $theme_directory = wp_normalize_path( $theme_directory );
     1445                        if ( 0 === strpos( $error_file, $theme_directory ) ) {
     1446                                $type = 'themes';
     1447                                $path = str_replace( $theme_directory . '/', '', $error_file );
     1448                        }
     1449                }
     1450        }
     1451
     1452        if ( empty( $type ) || empty( $path ) ) {
     1453                return false;
     1454        }
     1455
     1456        $parts     = explode( '/', $path );
     1457        $extension = array_shift( $parts );
     1458
     1459        $errors = (array) get_option( 'pause_on_admin', array() );
     1460
     1461        $modified_errors = $errors;
     1462
     1463        if ( ! array_key_exists( $type, $modified_errors ) ) {
     1464                $modified_errors[ $type ] = array();
     1465        }
     1466
     1467        $modified_errors[ $type ][ $extension ] = $error;
     1468
     1469        $modified_errors = wp_prune_extension_errors( $modified_errors );
     1470
     1471        if ( $modified_errors === $errors ) {
     1472                return true;
     1473        }
     1474
     1475        return update_option( 'pause_on_admin', $modified_errors );
     1476}
     1477
     1478/**
     1479 * Forgets a previously recorded extension error again.
     1480 *
     1481 * @since 5.0.0
     1482 *
     1483 * @param string $type Type of the extension.
     1484 * @param string $extension Relative path of the extension.
     1485 * @return bool Whether the extension error was successfully forgotten.
     1486 */
     1487function wp_forget_extension_error( $type, $extension ) {
     1488        $errors = (array) get_option( 'pause_on_admin', array() );
     1489
     1490        if ( ! array_key_exists( $type, $errors ) ) {
     1491                return false;
     1492        }
     1493
     1494        $modified_errors = $errors;
     1495
     1496        switch ( $type ) {
     1497                case 'plugins':
     1498                        list( $extension ) = explode( '/', $extension );
     1499        }
     1500
     1501        if ( array_key_exists( $extension, $modified_errors[ $type ] ) ) {
     1502                unset( $modified_errors[ $type ][ $extension ] );
     1503        }
     1504
     1505        $modified_errors = wp_prune_extension_errors( $modified_errors );
     1506
     1507        if ( $modified_errors === $errors ) {
     1508                return true;
     1509        }
     1510
     1511        return update_option( 'pause_on_admin', $modified_errors );
     1512}
     1513
     1514/**
     1515 * Determines whether we are dealing with an error that WordPress should handle
     1516 * in order to protect the admin backend against WSODs.
     1517 *
     1518 * @param array $error Error information retrieved from error_get_last().
     1519 *
     1520 * @return bool Whether WordPress should handle this error.
     1521 */
     1522function wp_should_handle_error( $error ) {
     1523        if ( ! isset( $error['type'] ) ) {
     1524                return false;
     1525        }
     1526
     1527        $error_types_to_handle = array(
     1528                E_ERROR,
     1529                E_PARSE,
     1530                E_USER_ERROR,
     1531                E_COMPILE_ERROR,
     1532                E_RECOVERABLE_ERROR,
     1533        );
     1534
     1535        return in_array( $error['type'], $error_types_to_handle, true );
     1536}
     1537
     1538/**
     1539 * Wraps the shutdown handler function so it can be made pluggable at a later
     1540 * stage.
     1541 *
     1542 * @since 5.0.0
     1543 *
     1544 * @return void
     1545 */
     1546function wp_shutdown_handler_wrapper() {
     1547        if ( defined( 'WP_EXECUTION_SUCCEEDED' ) && WP_EXECUTION_SUCCEEDED ) {
     1548                return;
     1549        }
     1550
     1551        // Load the pluggable shutdown handler in case we found one.
     1552        if ( function_exists( 'wp_handle_shutdown' ) ) {
     1553                $stop_propagation = false;
     1554
     1555                try {
     1556                        $stop_propagation = (bool) wp_handle_shutdown();
     1557                } catch ( Exception $exception ) {
     1558                        // Catch exceptions and remain silent.
     1559                }
     1560
     1561                if ( $stop_propagation ) {
     1562                        return;
     1563                }
     1564        }
     1565
     1566        $error = error_get_last();
     1567
     1568        // No error, just skip the error handling code.
     1569        if ( null === $error ) {
     1570                return;
     1571        }
     1572
     1573        // Bail early if this error should not be handled.
     1574        if ( ! wp_should_handle_error( $error ) ) {
     1575                return;
     1576        }
     1577
     1578        try {
     1579                // Persist the detected error.
     1580                wp_record_extension_error( $error );
     1581
     1582                /*
     1583                 * If we happen to be on a protected endpoint, we try to redirect to
     1584                 * catch multiple errors in one go.
     1585                 */
     1586                if ( is_protected_endpoint() ) {
     1587                        /*
     1588                         * Pluggable is usually loaded after plugins, so we manually
     1589                         * include it here for redirection functionality.
     1590                         */
     1591                        if ( ! function_exists( 'wp_redirect' ) ) {
     1592                                include ABSPATH . WPINC . '/pluggable.php';
     1593                        }
     1594
     1595                        $scheme = is_ssl() ? 'https://' : 'http://';
     1596
     1597                        $url = "{$scheme}{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";
     1598                        wp_redirect( $url );
     1599                        exit;
     1600                }
     1601
     1602                // Load custom PHP error template, if present.
     1603                $php_error_pluggable = WP_CONTENT_DIR . '/php-error.php';
     1604                if ( is_readable( $php_error_pluggable ) ) {
     1605                        /*
     1606                         * This drop-in should control the HTTP status code and print the
     1607                         * HTML markup indicating that a PHP error occurred. Alternatively,
     1608                         * `wp_die()` can be used.
     1609                         */
     1610                        require_once $php_error_pluggable;
     1611                        die();
     1612                }
     1613
     1614                // Otherwise, fall back to a default wp_die() message.
     1615                $message = sprintf(
     1616                        '<p>%s</p>',
     1617                        __( 'The site is experiencing technical difficulties.' )
     1618                );
     1619
     1620                if ( function_exists( 'get_admin_url' ) ) {
     1621                        $message .= sprintf(
     1622                                '<hr><p><em>%s <a href="%s">%s</a></em></p>',
     1623                                __( 'Are you the site owner?' ),
     1624                                get_admin_url(),
     1625                                __( 'Log into the admin backend to fix this.' )
     1626                        );
     1627                }
     1628
     1629                if ( function_exists( 'apply_filters' ) ) {
     1630                        /**
     1631                         * Filters the message that the default PHP error page displays.
     1632                         *
     1633                         * @since 5.0.0
     1634                         *
     1635                         * @param string $message HTML error message to display.
     1636                         */
     1637                        $message = apply_filters( 'wp_technical_issues_display', $message );
     1638                }
     1639
     1640                wp_die( $message, '', 500 );
     1641
     1642        } catch ( Exception $exception ) {
     1643                // Catch exceptions and remain silent.
     1644        }
     1645}
     1646
     1647/**
     1648 * Registers the WordPress premature shutdown handler.
     1649 *
     1650 * @since 5.0.0
     1651 *
     1652 * @return void
     1653 */
     1654function wp_register_premature_shutdown_handler() {
     1655        register_shutdown_function( 'wp_shutdown_handler_wrapper' );
     1656}
  • src/wp-settings.php

    diff --git a/src/wp-settings.php b/src/wp-settings.php
    index eb632238f0..c633f05b1d 100644
    a b  
    2020require( ABSPATH . WPINC . '/default-constants.php' );
    2121require_once( ABSPATH . WPINC . '/plugin.php' );
    2222
     23// Make sure we register the premature shutdown handler as soon as possible.
     24wp_register_premature_shutdown_handler();
     25
    2326/*
    2427 * These can't be directly globalized in version.php. When updating,
    2528 * we're including version.php from another installation and don't want
     
    4043// Set initial default constants including WP_MEMORY_LIMIT, WP_MAX_MEMORY_LIMIT, WP_DEBUG, SCRIPT_DEBUG, WP_CONTENT_DIR and WP_CACHE.
    4144wp_initial_constants();
    4245
     46/*
     47 * Allow an optional shutdown handler to be included through a pluggable file.
     48 * This file should register a function `wp_handle_shutdown( $context )` that
     49 * returns a boolean value. If the return value evaluates to false, the default
     50 * shutdown handler will not be executed.
     51 */
     52if ( is_readable( WP_CONTENT_DIR . '/shutdown-handler.php' ) ) {
     53        include WP_CONTENT_DIR . '/shutdown-handler.php';
     54}
     55
    4356// Check for the required PHP version and for the MySQL extension or a database drop-in.
    4457wp_check_php_mysql_versions();
    4558
     
    482495 * @since 3.0.0
    483496 */
    484497do_action( 'wp_loaded' );
     498
     499/*
     500 * Store the fact that we could successfully execute the entire WordPress
     501 * lifecycle. This is used to skip the premature shutdown handler, as it cannot
     502 * be unregistered.
     503 */
     504if ( ! defined( 'WP_EXECUTION_SUCCEEDED' ) ) {
     505        define( 'WP_EXECUTION_SUCCEEDED', true );
     506}