mirror of
https://github.com/gosticks/wordpress-develop.git
synced 2026-04-05 13:14:29 +00:00
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:
@@ -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 ); ?>&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 ); ?>&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() ) + '&locale=<?php echo urlencode( get_user_locale() ) ?>&version=<?php echo urlencode( get_bloginfo( 'version' ) ) ?>&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");
|
||||
|
||||
Reference in New Issue
Block a user