Make WordPress Core


Ignore:
Timestamp:
10/04/2017 12:19:16 AM (9 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.