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.


git-svn-id: https://develop.svn.wordpress.org/trunk@41721 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Weston Ruter
2017-10-04 00:19:16 +00:00
parent e965140cc9
commit 3fcfefd05c
11 changed files with 866 additions and 290 deletions

View File

@@ -68,113 +68,38 @@ if ( empty( $plugin ) ) {
$plugin_files = get_plugin_files($plugin);
if ( empty($file) )
if ( empty( $file ) ) {
$file = $plugin_files[0];
}
$file = validate_file_to_edit($file, $plugin_files);
$real_file = WP_PLUGIN_DIR . '/' . $file;
$scrollto = isset($_REQUEST['scrollto']) ? (int) $_REQUEST['scrollto'] : 0;
if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
check_admin_referer('edit-plugin_' . $file);
$newcontent = wp_unslash( $_POST['newcontent'] );
if ( is_writeable($real_file) ) {
$f = fopen($real_file, 'w+');
fwrite($f, $newcontent);
fclose($f);
if ( preg_match( '/\.php$/', $real_file ) && function_exists( 'opcache_invalidate' ) ) {
opcache_invalidate( $real_file, true );
// Handle fallback editing of file when JavaScript is not available.
$edit_error = null;
$posted_content = null;
if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
$r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) );
if ( is_wp_error( $r ) ) {
$edit_error = $r;
if ( check_ajax_referer( 'edit-plugin_' . $file, 'nonce', false ) && isset( $_POST['newcontent'] ) ) {
$posted_content = wp_unslash( $_POST['newcontent'] );
}
$network_wide = is_plugin_active_for_network( $file );
// Deactivate so we can test it.
if ( is_plugin_active( $plugin ) || isset( $_POST['phperror'] ) ) {
if ( is_plugin_active( $plugin ) ) {
deactivate_plugins( $plugin, true );
}
if ( ! is_network_admin() ) {
update_option( 'recently_activated', array( $file => time() ) + (array) get_option( 'recently_activated' ) );
} else {
update_site_option( 'recently_activated', array( $file => time() ) + (array) get_site_option( 'recently_activated' ) );
}
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 ) );
exit;
}
wp_redirect( self_admin_url( "plugin-editor.php?file=$file&plugin=$plugin&a=te&scrollto=$scrollto" ) );
} else {
wp_redirect( self_admin_url( "plugin-editor.php?file=$file&plugin=$plugin&scrollto=$scrollto" ) );
}
exit;
} else {
if ( isset($_GET['liveupdate']) ) {
check_admin_referer('edit-plugin-test_' . $file);
$error = validate_plugin( $plugin );
if ( is_wp_error( $error ) ) {
wp_die( $error );
}
if ( ( ! empty( $_GET['networkwide'] ) && ! is_plugin_active_for_network( $file ) ) || ! is_plugin_active( $file ) ) {
activate_plugin( $plugin, "plugin-editor.php?file=" . urlencode( $file ) . "&phperror=1", ! empty( $_GET['networkwide'] ) );
} // we'll override this later if the plugin can be included without fatal error
wp_redirect( self_admin_url( 'plugin-editor.php?file=' . urlencode( $file ) . '&plugin=' . urlencode( $plugin ) . "&a=te&scrollto=$scrollto" ) );
wp_redirect( add_query_arg(
array(
'a' => 1, // This means "success" for some reason.
'plugin' => $plugin,
'file' => $file,
),
admin_url( 'plugin-editor.php' )
) );
exit;
}
}
// List of allowable extensions
$editable_extensions = array(
'bash',
'conf',
'css',
'diff',
'htm',
'html',
'http',
'inc',
'include',
'js',
'json',
'jsx',
'less',
'md',
'patch',
'php',
'php3',
'php4',
'php5',
'php7',
'phps',
'phtml',
'sass',
'scss',
'sh',
'sql',
'svg',
'text',
'txt',
'xml',
'yaml',
'yml',
);
/**
* Filters file type extensions editable in the plugin editor.
*
* @since 2.8.0
*
* @param array $editable_extensions An array of editable plugin file extensions.
*/
$editable_extensions = (array) apply_filters( 'editable_extensions', $editable_extensions );
$editable_extensions = wp_get_plugin_file_editable_extensions( $plugin );
if ( ! is_file($real_file) ) {
wp_die(sprintf('<p>%s</p>', __('No such file exists! Double check the name and try again.')));
@@ -212,17 +137,21 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
'<p>' . __('<a href="https://wordpress.org/support/">Support Forums</a>') . '</p>'
);
$settings = wp_enqueue_code_editor( array( 'file' => $real_file ) );
if ( ! empty( $settings ) ) {
wp_enqueue_script( 'wp-theme-plugin-editor' );
wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function() { wp.themePluginEditor.init( %s ); } )', wp_json_encode( $settings ) ) );
}
$settings = array(
'codeEditor' => wp_enqueue_code_editor( array( 'file' => $real_file ) ),
);
wp_enqueue_script( 'wp-theme-plugin-editor' );
wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function( $ ) { wp.themePluginEditor.init( $( "#template" ), %s ); } )', wp_json_encode( $settings ) ) );
require_once(ABSPATH . 'wp-admin/admin-header.php');
update_recently_edited(WP_PLUGIN_DIR . '/' . $file);
$content = file_get_contents( $real_file );
if ( ! empty( $posted_content ) ) {
$content = $posted_content;
} else {
$content = file_get_contents( $real_file );
}
if ( '.php' == substr( $real_file, strrpos( $real_file, '.' ) ) ) {
$functions = wp_doc_link_parse( $content );
@@ -239,25 +168,20 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
$content = esc_textarea( $content );
?>
<?php if (isset($_GET['a'])) : ?>
<div id="message" class="updated notice is-dismissible"><p><?php _e('File edited successfully.') ?></p></div>
<?php elseif (isset($_GET['phperror'])) : ?>
<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>
<?php
if ( wp_verify_nonce( $_GET['_error_nonce'], 'plugin-activation-error_' . $plugin ) ) {
$iframe_url = add_query_arg( array(
'action' => 'error_scrape',
'plugin' => urlencode( $plugin ),
'_wpnonce' => urlencode( $_GET['_error_nonce'] ),
), admin_url( 'plugins.php' ) );
?>
<iframe style="border:0" width="100%" height="70px" src="<?php echo esc_url( $iframe_url ); ?>"></iframe>
<?php } ?>
</div>
<?php endif; ?>
<div class="wrap">
<h1><?php echo esc_html( $title ); ?></h1>
<?php if ( isset( $_GET['a'] ) ) : ?>
<div id="message" class="updated notice is-dismissible">
<p><?php _e( 'File edited successfully.' ); ?></p>
</div>
<?php elseif ( is_wp_error( $edit_error ) ) : ?>
<div id="message" class="notice notice-error">
<p><?php _e( 'There was an error while trying to update the file. You may need to fix something and try updating again.' ); ?></p>
<pre><?php echo esc_html( $edit_error->get_error_message() ? $edit_error->get_error_message() : $edit_error->get_error_code() ); ?></pre>
</div>
<?php endif; ?>
<div class="fileedit-sub">
<div class="alignleft">
<h2>
@@ -283,7 +207,7 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
</h2>
</div>
<div class="alignright">
<form action="plugin-editor.php" method="post">
<form action="plugin-editor.php" method="get">
<strong><label for="plugin"><?php _e('Select plugin to edit:'); ?> </label></strong>
<select name="plugin" id="plugin">
<?php
@@ -308,66 +232,53 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
<div id="templateside">
<h2><?php _e( 'Plugin Files' ); ?></h2>
<ul>
<?php
foreach ( $plugin_files as $plugin_file ) :
// Get the extension of the file
if ( preg_match('/\.([^.]+)$/', $plugin_file, $matches) ) {
$ext = strtolower($matches[1]);
// If extension is not in the acceptable list, skip it
if ( !in_array( $ext, $editable_extensions ) )
continue;
} else {
// No extension found
continue;
<?php
$plugin_editable_files = array();
foreach ( $plugin_files as $plugin_file ) {
if ( preg_match('/\.([^.]+)$/', $plugin_file, $matches ) && in_array( $matches[1], $editable_extensions ) ) {
$plugin_editable_files[] = $plugin_file;
}
}
?>
<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>
<?php endforeach; ?>
<ul>
<?php foreach ( $plugin_editable_files as $plugin_file ) : ?>
<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( preg_replace( '#^.+?/#', '', $plugin_file ) ); ?></a>
</li>
<?php endforeach; ?>
</ul>
</div>
<form name="template" id="template" action="plugin-editor.php" method="post">
<?php wp_nonce_field('edit-plugin_' . $file) ?>
<?php wp_nonce_field( 'edit-plugin_' . $file, 'nonce' ); ?>
<div>
<label for="newcontent" id="theme-plugin-editor-label"><?php _e( 'Selected file content:' ); ?></label>
<textarea cols="70" rows="25" name="newcontent" id="newcontent" aria-describedby="editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4"><?php echo $content; ?></textarea>
<input type="hidden" name="action" value="update" />
<input type="hidden" name="file" value="<?php echo esc_attr( $file ); ?>" />
<input type="hidden" name="plugin" value="<?php echo esc_attr( $plugin ); ?>" />
<input type="hidden" name="scrollto" id="scrollto" value="<?php echo esc_attr( $scrollto ); ?>" />
</div>
<?php if ( !empty( $docs_select ) ) : ?>
<div id="documentation" class="hide-if-no-js"><label for="docs-list"><?php _e('Documentation:') ?></label> <?php echo $docs_select ?> <input type="button" class="button" value="<?php esc_attr_e( 'Look Up' ) ?> " onclick="if ( '' != jQuery('#docs-list').val() ) { window.open( 'https://api.wordpress.org/core/handbook/1.0/?function=' + escape( jQuery( '#docs-list' ).val() ) + '&amp;locale=<?php echo urlencode( get_user_locale() ) ?>&amp;version=<?php echo urlencode( get_bloginfo( 'version' ) ) ?>&amp;redirect=true'); }" /></div>
<?php endif; ?>
<?php if ( is_writeable($real_file) ) : ?>
<?php if ( in_array( $plugin, (array) get_option( 'active_plugins', array() ) ) ) { ?>
<div class="notice notice-warning inline active-plugin-edit-warning">
<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>
<div class="editor-notices">
<?php if ( in_array( $plugin, (array) get_option( 'active_plugins', array() ) ) ) { ?>
<div class="notice notice-warning inline active-plugin-edit-warning">
<p><?php _e('<strong>Warning:</strong> Making changes to active plugins is not recommended.'); ?></p>
</div>
<?php } ?>
<?php } ?>
</div>
<p class="submit">
<?php
if ( isset($_GET['phperror']) ) {
echo "<input type='hidden' name='phperror' value='1' />";
submit_button( __( 'Update File and Attempt to Reactivate' ), 'primary', 'submit', false );
} else {
submit_button( __( 'Update File' ), 'primary', 'submit', false );
}
?>
<?php submit_button( __( 'Update File' ), 'primary', 'submit', false ); ?>
<span class="spinner"></span>
</p>
<?php else : ?>
<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>
<?php endif; ?>
<?php wp_print_file_editor_templates(); ?>
</form>
<br class="clear" />
</div>
<script type="text/javascript">
jQuery(document).ready(function($){
$('#template').submit(function(){ $('#scrollto').val( $('#newcontent').scrollTop() ); });
$('#newcontent').scrollTop( $('#scrollto').val() );
});
</script>
<?php
}
include(ABSPATH . "wp-admin/admin-footer.php");