WordPress.org

Make WordPress Core


Ignore:
Timestamp:
10/04/2017 12:19:16 AM (3 years ago)
Author:
westonruter
Message:

File Editors: Introduce sandboxed live editing of PHP files with rollbacks for both themes and plugins.

  • Edits to active plugins which cause PHP fatal errors will no longer auto-deactivate the plugin. Supersedes #39766.
  • Introduce sandboxed PHP file edits for active themes, preventing accidental whitescreening of a user's site when introducing a fatal error.
  • After writing a change to a PHP file for an active theme or plugin, perform loopback requests on the file editor admin screens and the homepage to check for fatal errors. If a fatal error is encountered, roll back the edited file and display the error to the user to fix and try again.
  • Introduce a secure way to scrape PHP fatal errors from a site via wp_start_scraping_edited_file_errors() and wp_finalize_scraping_edited_file_errors().
  • Moves file modifications from theme-editor.php and plugin-editor.php to common wp_edit_theme_plugin_file() function.
  • Refactor themes and plugin editors to submit file changes via Ajax instead of doing full page refreshes when JS is available.
  • Use get method for theme/plugin dropdowns.
  • Improve styling of plugin editors, including width of plugin/theme dropdowns.
  • Improve notices API for theme/plugin editor JS component.
  • Strip common base directory from plugin file list. See #24048.
  • Factor out functions to list editable file types in wp_get_theme_file_editable_extensions() and wp_get_plugin_file_editable_extensions().
  • Scroll to line in editor that has linting error when attempting to save. See #41886.
  • Add checkbox to dismiss lint errors to proceed with saving. See #41887.
  • Only style the Update File button as disabled instead of actually disabling it for accessibility reasons.
  • Ensure that value from CodeMirror is used instead of textarea when CodeMirror is present.
  • Add "Are you sure?" check when leaving editor when there are unsaved changes.

Supersedes [41560].
See #39766, #24048, #41886.
Props westonruter, Clorith, melchoyce, johnbillion, jjj, jdgrimes, azaozz.
Fixes #21622, #41887.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/plugin-editor.php

    r41595 r41721  
    6969$plugin_files = get_plugin_files($plugin);
    7070
    71 if ( empty($file) )
     71if ( empty( $file ) ) {
    7272    $file = $plugin_files[0];
     73}
    7374
    7475$file = validate_file_to_edit($file, $plugin_files);
    7576$real_file = WP_PLUGIN_DIR . '/' . $file;
    76 $scrollto = isset($_REQUEST['scrollto']) ? (int) $_REQUEST['scrollto'] : 0;
    77 
    78 if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
    79 
    80     check_admin_referer('edit-plugin_' . $file);
    81 
    82     $newcontent = wp_unslash( $_POST['newcontent'] );
    83     if ( is_writeable($real_file) ) {
    84         $f = fopen($real_file, 'w+');
    85         fwrite($f, $newcontent);
    86         fclose($f);
    87 
    88         if ( preg_match( '/\.php$/', $real_file ) && function_exists( 'opcache_invalidate' ) ) {
    89             opcache_invalidate( $real_file, true );
    90         }
    91 
    92         $network_wide = is_plugin_active_for_network( $file );
    93 
    94         // Deactivate so we can test it.
    95         if ( is_plugin_active( $plugin ) || isset( $_POST['phperror'] ) ) {
    96             if ( is_plugin_active( $plugin ) ) {
    97                 deactivate_plugins( $plugin, true );
    98             }
    99 
    100             if ( ! is_network_admin() ) {
    101                 update_option( 'recently_activated', array( $file => time() ) + (array) get_option( 'recently_activated' ) );
    102             } else {
    103                 update_site_option( 'recently_activated', array( $file => time() ) + (array) get_site_option( 'recently_activated' ) );
    104             }
    105 
    106             wp_redirect( add_query_arg( '_wpnonce', wp_create_nonce( 'edit-plugin-test_' . $file ), "plugin-editor.php?file=$file&plugin=$plugin&liveupdate=1&scrollto=$scrollto&networkwide=" . $network_wide ) );
    107             exit;
    108         }
    109         wp_redirect( self_admin_url( "plugin-editor.php?file=$file&plugin=$plugin&a=te&scrollto=$scrollto" ) );
    110     } else {
    111         wp_redirect( self_admin_url( "plugin-editor.php?file=$file&plugin=$plugin&scrollto=$scrollto" ) );
    112     }
    113     exit;
    114 
    115 } else {
    116 
    117     if ( isset($_GET['liveupdate']) ) {
    118         check_admin_referer('edit-plugin-test_' . $file);
    119 
    120         $error = validate_plugin( $plugin );
    121 
    122         if ( is_wp_error( $error ) ) {
    123             wp_die( $error );
    124         }
    125 
    126         if ( ( ! empty( $_GET['networkwide'] ) && ! is_plugin_active_for_network( $file ) ) || ! is_plugin_active( $file ) ) {
    127             activate_plugin( $plugin, "plugin-editor.php?file=" . urlencode( $file ) . "&phperror=1", ! empty( $_GET['networkwide'] ) );
    128         } // we'll override this later if the plugin can be included without fatal error
    129 
    130         wp_redirect( self_admin_url( 'plugin-editor.php?file=' . urlencode( $file ) . '&plugin=' . urlencode( $plugin ) . "&a=te&scrollto=$scrollto" ) );
     77
     78// Handle fallback editing of file when JavaScript is not available.
     79$edit_error = null;
     80$posted_content = null;
     81if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
     82    $r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) );
     83    if ( is_wp_error( $r ) ) {
     84        $edit_error = $r;
     85        if ( check_ajax_referer( 'edit-plugin_' . $file, 'nonce', false ) && isset( $_POST['newcontent'] ) ) {
     86            $posted_content = wp_unslash( $_POST['newcontent'] );
     87        }
     88    } else {
     89        wp_redirect( add_query_arg(
     90            array(
     91                'a' => 1, // This means "success" for some reason.
     92                'plugin' => $plugin,
     93                'file' => $file,
     94            ),
     95            admin_url( 'plugin-editor.php' )
     96        ) );
    13197        exit;
    13298    }
     99}
    133100
    134101    // List of allowable extensions
    135     $editable_extensions = array(
    136         'bash',
    137         'conf',
    138         'css',
    139         'diff',
    140         'htm',
    141         'html',
    142         'http',
    143         'inc',
    144         'include',
    145         'js',
    146         'json',
    147         'jsx',
    148         'less',
    149         'md',
    150         'patch',
    151         'php',
    152         'php3',
    153         'php4',
    154         'php5',
    155         'php7',
    156         'phps',
    157         'phtml',
    158         'sass',
    159         'scss',
    160         'sh',
    161         'sql',
    162         'svg',
    163         'text',
    164         'txt',
    165         'xml',
    166         'yaml',
    167         'yml',
    168     );
    169 
    170     /**
    171      * Filters file type extensions editable in the plugin editor.
    172      *
    173      * @since 2.8.0
    174      *
    175      * @param array $editable_extensions An array of editable plugin file extensions.
    176      */
    177     $editable_extensions = (array) apply_filters( 'editable_extensions', $editable_extensions );
     102    $editable_extensions = wp_get_plugin_file_editable_extensions( $plugin );
    178103
    179104    if ( ! is_file($real_file) ) {
     
    213138    );
    214139
    215     $settings = wp_enqueue_code_editor( array( 'file' => $real_file ) );
    216     if ( ! empty( $settings ) ) {
    217         wp_enqueue_script( 'wp-theme-plugin-editor' );
    218         wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function() { wp.themePluginEditor.init( %s ); } )', wp_json_encode( $settings ) ) );
    219     }
     140    $settings = array(
     141        'codeEditor' => wp_enqueue_code_editor( array( 'file' => $real_file ) ),
     142    );
     143    wp_enqueue_script( 'wp-theme-plugin-editor' );
     144    wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function( $ ) { wp.themePluginEditor.init( $( "#template" ), %s ); } )', wp_json_encode( $settings ) ) );
    220145
    221146    require_once(ABSPATH . 'wp-admin/admin-header.php');
     
    223148    update_recently_edited(WP_PLUGIN_DIR . '/' . $file);
    224149
    225     $content = file_get_contents( $real_file );
     150    if ( ! empty( $posted_content ) ) {
     151        $content = $posted_content;
     152    } else {
     153        $content = file_get_contents( $real_file );
     154    }
    226155
    227156    if ( '.php' == substr( $real_file, strrpos( $real_file, '.' ) ) ) {
     
    240169    $content = esc_textarea( $content );
    241170    ?>
    242 <?php if (isset($_GET['a'])) : ?>
    243  <div id="message" class="updated notice is-dismissible"><p><?php _e('File edited successfully.') ?></p></div>
    244 <?php elseif (isset($_GET['phperror'])) : ?>
    245  <div id="message" class="notice notice-error"><p><?php _e( 'This plugin has been deactivated because your changes resulted in a <strong>fatal error</strong>.' ); ?></p>
    246     <?php
    247         if ( wp_verify_nonce( $_GET['_error_nonce'], 'plugin-activation-error_' . $plugin ) ) {
    248             $iframe_url = add_query_arg( array(
    249                 'action'   => 'error_scrape',
    250                 'plugin'   => urlencode( $plugin ),
    251                 '_wpnonce' => urlencode( $_GET['_error_nonce'] ),
    252             ), admin_url( 'plugins.php' ) );
    253             ?>
    254     <iframe style="border:0" width="100%" height="70px" src="<?php echo esc_url( $iframe_url ); ?>"></iframe>
    255     <?php } ?>
    256 </div>
    257 <?php endif; ?>
    258171<div class="wrap">
    259172<h1><?php echo esc_html( $title ); ?></h1>
     173
     174<?php if ( isset( $_GET['a'] ) ) : ?>
     175    <div id="message" class="updated notice is-dismissible">
     176        <p><?php _e( 'File edited successfully.' ); ?></p>
     177    </div>
     178<?php elseif ( is_wp_error( $edit_error ) ) : ?>
     179    <div id="message" class="notice notice-error">
     180        <p><?php _e( 'There was an error while trying to update the file. You may need to fix something and try updating again.' ); ?></p>
     181        <pre><?php echo esc_html( $edit_error->get_error_message() ? $edit_error->get_error_message() : $edit_error->get_error_code() ); ?></pre>
     182    </div>
     183<?php endif; ?>
    260184
    261185<div class="fileedit-sub">
     
    284208</div>
    285209<div class="alignright">
    286     <form action="plugin-editor.php" method="post">
     210    <form action="plugin-editor.php" method="get">
    287211        <strong><label for="plugin"><?php _e('Select plugin to edit:'); ?> </label></strong>
    288212        <select name="plugin" id="plugin">
     
    309233    <h2><?php _e( 'Plugin Files' ); ?></h2>
    310234
     235    <?php
     236    $plugin_editable_files = array();
     237    foreach ( $plugin_files as $plugin_file ) {
     238        if ( preg_match('/\.([^.]+)$/', $plugin_file, $matches ) && in_array( $matches[1], $editable_extensions ) ) {
     239            $plugin_editable_files[] = $plugin_file;
     240        }
     241    }
     242    ?>
    311243    <ul>
    312 <?php
    313 foreach ( $plugin_files as $plugin_file ) :
    314     // Get the extension of the file
    315     if ( preg_match('/\.([^.]+)$/', $plugin_file, $matches) ) {
    316         $ext = strtolower($matches[1]);
    317         // If extension is not in the acceptable list, skip it
    318         if ( !in_array( $ext, $editable_extensions ) )
    319             continue;
    320     } else {
    321         // No extension found
    322         continue;
    323     }
    324     ?>
    325     <li class="<?php echo esc_attr( $file === $plugin_file ? 'notice notice-info' : '' ); ?>"><a href="plugin-editor.php?file=<?php echo urlencode( $plugin_file ); ?>&amp;plugin=<?php echo urlencode( $plugin ); ?>"><?php echo esc_html( $plugin_file ); ?></a></li>
    326 <?php endforeach; ?>
     244    <?php foreach ( $plugin_editable_files as $plugin_file ) : ?>
     245        <li class="<?php echo esc_attr( $file === $plugin_file ? 'notice notice-info' : '' ); ?>">
     246            <a href="plugin-editor.php?file=<?php echo urlencode( $plugin_file ); ?>&amp;plugin=<?php echo urlencode( $plugin ); ?>"><?php echo esc_html( preg_replace( '#^.+?/#', '', $plugin_file ) ); ?></a>
     247        </li>
     248    <?php endforeach; ?>
    327249    </ul>
    328250</div>
    329251<form name="template" id="template" action="plugin-editor.php" method="post">
    330     <?php wp_nonce_field('edit-plugin_' . $file) ?>
     252    <?php wp_nonce_field( 'edit-plugin_' . $file, 'nonce' ); ?>
    331253        <div>
    332254            <label for="newcontent" id="theme-plugin-editor-label"><?php _e( 'Selected file content:' ); ?></label>
     
    335257            <input type="hidden" name="file" value="<?php echo esc_attr( $file ); ?>" />
    336258            <input type="hidden" name="plugin" value="<?php echo esc_attr( $plugin ); ?>" />
    337             <input type="hidden" name="scrollto" id="scrollto" value="<?php echo esc_attr( $scrollto ); ?>" />
    338259        </div>
    339260        <?php if ( !empty( $docs_select ) ) : ?>
     
    341262        <?php endif; ?>
    342263<?php if ( is_writeable($real_file) ) : ?>
    343     <?php if ( in_array( $plugin, (array) get_option( 'active_plugins', array() ) ) ) { ?>
    344         <div class="notice notice-warning inline active-plugin-edit-warning">
    345             <p><?php _e('<strong>Warning:</strong> Making changes to active plugins is not recommended. If your changes cause a fatal error, the plugin will be automatically deactivated.'); ?></p>
     264    <div class="editor-notices">
     265        <?php if ( in_array( $plugin, (array) get_option( 'active_plugins', array() ) ) ) { ?>
     266            <div class="notice notice-warning inline active-plugin-edit-warning">
     267            <p><?php _e('<strong>Warning:</strong> Making changes to active plugins is not recommended.'); ?></p>
    346268        </div>
    347     <?php } ?>
     269        <?php } ?>
     270    </div>
    348271    <p class="submit">
    349     <?php
    350         if ( isset($_GET['phperror']) ) {
    351             echo "<input type='hidden' name='phperror' value='1' />";
    352             submit_button( __( 'Update File and Attempt to Reactivate' ), 'primary', 'submit', false );
    353         } else {
    354             submit_button( __( 'Update File' ), 'primary', 'submit', false );
    355         }
    356     ?>
     272        <?php submit_button( __( 'Update File' ), 'primary', 'submit', false ); ?>
     273        <span class="spinner"></span>
    357274    </p>
    358275<?php else : ?>
    359276    <p><em><?php _e('You need to make this file writable before you can save your changes. See <a href="https://codex.wordpress.org/Changing_File_Permissions">the Codex</a> for more information.'); ?></em></p>
    360277<?php endif; ?>
     278<?php wp_print_file_editor_templates(); ?>
    361279</form>
    362280<br class="clear" />
    363281</div>
    364 <script type="text/javascript">
    365 jQuery(document).ready(function($){
    366     $('#template').submit(function(){ $('#scrollto').val( $('#newcontent').scrollTop() ); });
    367     $('#newcontent').scrollTop( $('#scrollto').val() );
    368 });
    369 </script>
    370282<?php
    371 }
    372283
    373284include(ABSPATH . "wp-admin/admin-footer.php");
Note: See TracChangeset for help on using the changeset viewer.