From 3fcfefd05c0b49e97892b62038d4b69e8ee13eaa Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Oct 2017 00:19:16 +0000 Subject: [PATCH] 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 --- src/wp-admin/admin-ajax.php | 2 +- src/wp-admin/css/common.css | 28 +- src/wp-admin/includes/ajax-actions.php | 23 ++ src/wp-admin/includes/file.php | 394 ++++++++++++++++++++++++- src/wp-admin/js/theme-plugin-editor.js | 267 +++++++++++++++-- src/wp-admin/plugin-editor.php | 219 ++++---------- src/wp-admin/theme-editor.php | 158 ++++------ src/wp-includes/js/wp-a11y.js | 7 +- src/wp-includes/load.php | 43 +++ src/wp-includes/script-loader.php | 13 +- src/wp-settings.php | 2 + 11 files changed, 866 insertions(+), 290 deletions(-) diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index 15c352de94..4a18fcf714 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -64,7 +64,7 @@ $core_actions_post = array( 'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'crop-image', 'generate-password', 'save-wporg-username', 'delete-plugin', 'search-plugins', 'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme', 'install-theme', - 'get-post-thumbnail-html', 'get-community-events', + 'get-post-thumbnail-html', 'get-community-events', 'edit-theme-plugin-file', ); // Deprecated diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 1469097a8a..d91e8a1368 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -2217,14 +2217,16 @@ h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */ #template > div { margin-right: 190px; } -#template .active-plugin-edit-warning { +#template .notice { margin-top: 1em; - margin-right: 30%; - margin-right: calc( 184px + 3% ); + margin-right: 3%; } -#template .active-plugin-edit-warning p { +#template .notice p { width: auto; } +#template .submit .spinner { + float: none; +} .metabox-holder .stuffbox > h3, /* Back-compat for pre-4.4 */ .metabox-holder .postbox > h3, /* Back-compat for pre-4.4 */ @@ -3032,10 +3034,14 @@ img { #template textarea, #template .CodeMirror { width: 97%; - height: calc( 100vh - 220px ); + height: calc( 100vh - 280px ); +} +#templateside { + margin-top: 31px; + overflow: scroll; } -#template label { +#theme-plugin-editor-label { display: inline-block; margin-bottom: 1em; font-weight: 600; @@ -3047,6 +3053,14 @@ img { direction: ltr; } +.fileedit-sub #theme, +.fileedit-sub #plugin { + max-width: 40%; +} +.fileedit-sub .alignright { + text-align: right; +} + #template p { width: 97%; } @@ -3624,7 +3638,7 @@ img { } #template > div, - #template .active-plugin-edit-warning { + #template .notice { float: none; margin: 1em 0; width: auto; diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 53fd8671d1..8dbd75c5a3 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -3966,3 +3966,26 @@ function wp_ajax_search_install_plugins() { wp_send_json_success( $status ); } + +/** + * Ajax handler for editing a theme or plugin file. + * + * @since 4.9.0 + * @see wp_edit_theme_plugin_file() + */ +function wp_ajax_edit_theme_plugin_file() { + $r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) ); // Validation of args is done in wp_edit_theme_plugin_file(). + if ( is_wp_error( $r ) ) { + wp_send_json_error( array_merge( + array( + 'code' => $r->get_error_code(), + 'message' => $r->get_error_message(), + ), + (array) $r->get_error_data() + ) ); + } else { + wp_send_json_success( array( + 'message' => __( 'File edited successfully.' ), + ) ); + } +} diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 05bfde46a4..553880a46c 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -70,7 +70,7 @@ $wp_file_descriptions = array( * @since 1.5.0 * * @global array $wp_file_descriptions Theme file descriptions. - * @global array $allowed_files List of allowed files. + * @global array $allowed_files List of allowed files. * @param string $file Filesystem path or filename * @return string Description of file from $wp_file_descriptions or basename of $file if description doesn't exist. * Appends 'Page Template' to basename of $file if the file is a page template @@ -152,6 +152,398 @@ function list_files( $folder = '', $levels = 100 ) { return $files; } +/** + * Get list of file extensions that are editable in plugins. + * + * @since 4.9.0 + * + * @param string $plugin Plugin. + * @return array File extensions. + */ +function wp_get_plugin_file_editable_extensions( $plugin ) { + + $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 + * @since 4.9.0 Adds $plugin param. + * + * @param string $plugin Plugin file. + * @param array $editable_extensions An array of editable plugin file extensions. + */ + $editable_extensions = (array) apply_filters( 'editable_extensions', $editable_extensions, $plugin ); + + return $editable_extensions; +} + +/** + * Get list of file extensions that are editable for a given theme. + * + * @param WP_Theme $theme Theme. + * @return array File extensions. + */ +function wp_get_theme_file_editable_extensions( $theme ) { + + $default_types = 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 the list of file types allowed for editing in the Theme editor. + * + * @since 4.4.0 + * + * @param array $default_types List of file types. Default types include 'php' and 'css'. + * @param WP_Theme $theme The current Theme object. + */ + $file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme ); + + // Ensure that default types are still there. + return array_unique( array_merge( $file_types, $default_types ) ); +} + +/** + * Print file editor templates (for plugins and themes). + * + * @since 4.9.0 + */ +function wp_print_file_editor_templates() { + ?> + + exists() ) { + return new WP_Error( 'non_existent_theme', __( 'The requested theme does not exist.' ) ); + } + + $real_file = $theme->get_stylesheet_directory() . '/' . $file; + if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $real_file . $stylesheet ) ) { + return new WP_Error( 'nonce_failure' ); + } + + if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) { + return new WP_Error( + 'theme_no_stylesheet', + __( 'The requested theme does not exist.' ) . ' ' . $theme->errors()->get_error_message() + ); + } + + $editable_extensions = wp_get_theme_file_editable_extensions( $theme ); + + $allowed_files = array(); + foreach ( $editable_extensions as $type ) { + switch ( $type ) { + case 'php': + $allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', 1 ) ); + break; + case 'css': + $style_files = $theme->get_files( 'css' ); + $allowed_files['style.css'] = $style_files['style.css']; + $allowed_files = array_merge( $allowed_files, $style_files ); + break; + default: + $allowed_files = array_merge( $allowed_files, $theme->get_files( $type ) ); + break; + } + } + + if ( 0 !== validate_file( $real_file, $allowed_files ) ) { + return new WP_Error( 'disallowed_theme_file', __( 'Sorry, that file cannot be edited.' ) ); + } + + $is_active = ( get_stylesheet() === $stylesheet || get_template() === $stylesheet ); + } else { + return new WP_Error( 'missing_theme_or_plugin' ); + } + + // Ensure file is real. + if ( ! is_file( $real_file ) ) { + return new WP_Error( 'file_does_not_exist', __( 'No such file exists! Double check the name and try again.' ) ); + } + + // Ensure file extension is allowed. + $extension = null; + if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) { + $extension = strtolower( $matches[1] ); + if ( ! in_array( $extension, $editable_extensions, true ) ) { + return new WP_Error( 'illegal_file_type', __( 'Files of this type are not editable.' ) ); + } + } + + $previous_content = file_get_contents( $real_file ); + + if ( ! is_writeable( $real_file ) ) { + return new WP_Error( 'file_not_writable' ); + } + + $f = fopen( $real_file, 'w+' ); + if ( false === $f ) { + return new WP_Error( 'file_not_writable' ); + } + + $written = fwrite( $f, $content ); + fclose( $f ); + if ( false === $written ) { + return new WP_Error( 'unable_to_write', __( 'Unable to write to file.' ) ); + } + if ( 'php' === $extension && function_exists( 'opcache_invalidate' ) ) { + opcache_invalidate( $real_file, true ); + } + + if ( $is_active && 'php' === $extension ) { + + $scrape_key = md5( rand() ); + $transient = 'scrape_key_' . $scrape_key; + $scrape_nonce = strval( rand() ); + set_transient( $transient, $scrape_nonce, 60 ); // It shouldn't take more than 60 seconds to make the two loopback requests. + + $cookies = wp_unslash( $_COOKIE ); + $scrape_params = array( + 'wp_scrape_key' => $scrape_key, + 'wp_scrape_nonce' => $scrape_nonce, + ); + $headers = array( + 'Cache-Control' => 'no-cache', + ); + + $needle = "###### begin_scraped_error:$scrape_key ######"; + + // Attempt loopback request to editor to see if user just whitescreened themselves. + if ( $plugin ) { + $url = add_query_arg( compact( 'plugin', 'file' ), admin_url( 'plugin-editor.php' ) ); + } elseif ( isset( $stylesheet ) ) { + $url = add_query_arg( + array( + 'theme' => $stylesheet, + 'file' => $file, + ), + admin_url( 'theme-editor.php' ) + ); + } else { + $url = admin_url(); + } + $url = add_query_arg( $scrape_params, $url ); + $r = wp_remote_get( $url, compact( 'cookies', 'headers' ) ); + $body = wp_remote_retrieve_body( $r ); + $error_position = strpos( $body, $needle ); + + // Try making request to homepage as well to see if visitors have been whitescreened. + if ( false === $error_position ) { + $url = home_url( '/' ); + $url = add_query_arg( $scrape_params, $url ); + $r = wp_remote_get( $url, compact( 'cookies', 'headers' ) ); + $body = wp_remote_retrieve_body( $r ); + $error_position = strpos( $body, $needle ); + } + + delete_transient( $transient ); + + if ( false !== $error_position ) { + file_put_contents( $real_file, $previous_content ); + if ( function_exists( 'opcache_invalidate' ) ) { + opcache_invalidate( $real_file, true ); + } + + $error_output = trim( substr( $body, $error_position + strlen( $needle ) ) ); + $error = json_decode( $error_output, true ); + if ( ! isset( $error['message'] ) ) { + $message = $error_output; + } else { + $message = $error['message']; + unset( $error['message'] ); + } + return new WP_Error( 'php_error', $message, $error ); + } + } + + if ( $theme instanceof WP_Theme ) { + $theme->cache_delete(); + } + + return true; +} + + /** * Returns a filename of a Temporary unique file. * Please note that the calling function must unlink() this itself. diff --git a/src/wp-admin/js/theme-plugin-editor.js b/src/wp-admin/js/theme-plugin-editor.js index 8e016c3837..3bb0788a6b 100644 --- a/src/wp-admin/js/theme-plugin-editor.js +++ b/src/wp-admin/js/theme-plugin-editor.js @@ -12,25 +12,227 @@ wp.themePluginEditor = (function( $ ) { lintError: { singular: '', plural: '' - } + }, + saveAlert: '' }, - instance: null + codeEditor: {}, + instance: null, + noticeElements: {}, + dirty: false, + lintErrors: [] }; /** * Initialize component. * - * @param {object} settings Settings. + * @since 4.9.0 + * + * @param {jQuery} form - Form element. + * @param {object} settings - Settings. + * @param {object|boolean} settings.codeEditor - Code editor settings (or `false` if syntax highlighting is disabled). * @returns {void} */ - component.init = function( settings ) { - var codeEditorSettings, noticeContainer, errorNotice = [], editor; + component.init = function init( form, settings ) { - codeEditorSettings = $.extend( {}, settings ); + component.form = form; + if ( settings ) { + $.extend( component, settings ); + } + + component.noticeTemplate = wp.template( 'wp-file-editor-notice' ); + component.noticesContainer = component.form.find( '.editor-notices' ); + component.submitButton = component.form.find( ':input[name=submit]' ); + component.spinner = component.form.find( '.submit .spinner' ); + component.form.on( 'submit', component.submit ); + component.textarea = component.form.find( '#newcontent' ); + component.textarea.on( 'change', component.onChange ); + + if ( false !== component.codeEditor ) { + /* + * Defer adding notices until after DOM ready as workaround for WP Admin injecting + * its own managed dismiss buttons and also to prevent the editor from showing a notice + * when the file had linting errors to begin with. + */ + _.defer( function() { + component.initCodeEditor(); + } ); + } + + $( window ).on( 'beforeunload', function() { + if ( component.dirty ) { + return component.l10n.saveAlert; + } + return undefined; + } ); + }; + + /** + * Callback for when a change happens. + * + * @since 4.9.0 + * @returns {void} + */ + component.onChange = function() { + component.dirty = true; + component.removeNotice( 'file_saved' ); + }; + + /** + * Submit file via Ajax. + * + * @since 4.9.0 + * @param {jQuery.Event} event - Event. + * @returns {void} + */ + component.submit = function( event ) { + var data = {}, request; + event.preventDefault(); // Prevent form submission in favor of Ajax below. + $.each( component.form.serializeArray(), function() { + data[ this.name ] = this.value; + } ); + + // Use value from codemirror if present. + if ( component.instance ) { + data.newcontent = component.instance.codemirror.getValue(); + } + + if ( component.isSaving ) { + return; + } + + // Scroll ot the line that has the error. + if ( component.lintErrors.length ) { + component.instance.codemirror.setCursor( component.lintErrors[0].from.line ); + return; + } + + component.isSaving = true; + component.textarea.prop( 'readonly', true ); + if ( component.instance ) { + component.instance.codemirror.setOption( 'readOnly', true ); + } + + component.spinner.addClass( 'is-active' ); + request = wp.ajax.post( 'edit-theme-plugin-file', data ); + + // Remove previous save notice before saving. + if ( component.lastSaveNoticeCode ) { + component.removeNotice( component.lastSaveNoticeCode ); + } + + request.done( function ( response ) { + component.lastSaveNoticeCode = 'file_saved'; + component.addNotice({ + code: component.lastSaveNoticeCode, + type: 'success', + message: response.message, + dismissible: true + }); + component.dirty = false; + } ); + + request.fail( function ( response ) { + var notice = $.extend( + { + code: 'save_error' + }, + response, + { + type: 'error', + dismissible: true + } + ); + component.lastSaveNoticeCode = notice.code; + component.addNotice( notice ); + } ); + + request.always( function() { + component.spinner.removeClass( 'is-active' ); + component.isSaving = false; + + component.textarea.prop( 'readonly', false ); + if ( component.instance ) { + component.instance.codemirror.setOption( 'readOnly', false ); + } + } ); + }; + + /** + * Add notice. + * + * @since 4.9.0 + * + * @param {object} notice - Notice. + * @param {string} notice.code - Code. + * @param {string} notice.type - Type. + * @param {string} notice.message - Message. + * @param {boolean} [notice.dismissible=false] - Dismissible. + * @param {Function} [notice.onDismiss] - Callback for when a user dismisses the notice. + * @returns {jQuery} Notice element. + */ + component.addNotice = function( notice ) { + var noticeElement; + + if ( ! notice.code ) { + throw new Error( 'Missing code.' ); + } + + // Only let one notice of a given type be displayed at a time. + component.removeNotice( notice.code ); + + noticeElement = $( component.noticeTemplate( notice ) ); + noticeElement.hide(); + + noticeElement.find( '.notice-dismiss' ).on( 'click', function() { + component.removeNotice( notice.code ); + if ( notice.onDismiss ) { + notice.onDismiss( notice ); + } + } ); + + wp.a11y.speak( notice.message ); + + component.noticesContainer.append( noticeElement ); + noticeElement.slideDown( 'fast' ); + component.noticeElements[ notice.code ] = noticeElement; + return noticeElement; + }; + + /** + * Remove notice. + * + * @since 4.9.0 + * + * @param {string} code - Notice code. + * @returns {boolean} Whether a notice was removed. + */ + component.removeNotice = function( code ) { + if ( component.noticeElements[ code ] ) { + component.noticeElements[ code ].slideUp( 'fast', function() { + $( this ).remove(); + } ); + delete component.noticeElements[ code ]; + return true; + } + return false; + }; + + /** + * Initialize code editor. + * + * @since 4.9.0 + * @returns {void} + */ + component.initCodeEditor = function initCodeEditor() { + var codeEditorSettings, editor; + + codeEditorSettings = $.extend( {}, component.codeEditor ); /** * Handle tabbing to the field before the editor. * + * @since 4.9.0 + * * @returns {void} */ codeEditorSettings.onTabPrevious = function() { @@ -40,48 +242,67 @@ wp.themePluginEditor = (function( $ ) { /** * Handle tabbing to the field after the editor. * + * @since 4.9.0 + * * @returns {void} */ codeEditorSettings.onTabNext = function() { $( '#template' ).find( ':tabbable:not(.CodeMirror-code)' ).first().focus(); }; - // Create the error notice container. - noticeContainer = $( '
' ); - errorNotice = $( '
' ); - noticeContainer.append( errorNotice ); - noticeContainer.hide(); - $( 'p.submit' ).before( noticeContainer ); + /** + * Handle change to the linting errors. + * + * @since 4.9.0 + * + * @param {Array} errors - List of linting errors. + * @returns {void} + */ + codeEditorSettings.onChangeLintingErrors = function( errors ) { + component.lintErrors = errors; + + // Only disable the button in onUpdateErrorNotice when there are errors so users can still feel they can click the button. + if ( 0 === errors.length ) { + component.submitButton.toggleClass( 'disabled', false ); + } + }; /** * Update error notice. * + * @since 4.9.0 + * * @param {Array} errorAnnotations - Error annotations. * @returns {void} */ codeEditorSettings.onUpdateErrorNotice = function onUpdateErrorNotice( errorAnnotations ) { - var message; + var message, noticeElement; - $( '#submit' ).prop( 'disabled', 0 !== errorAnnotations.length ); + component.submitButton.toggleClass( 'disabled', errorAnnotations.length > 0 ); if ( 0 !== errorAnnotations.length ) { - errorNotice.empty(); if ( 1 === errorAnnotations.length ) { - message = component.l10n.singular.replace( '%d', '1' ); + message = component.l10n.lintError.singular.replace( '%d', '1' ); } else { - message = component.l10n.plural.replace( '%d', String( errorAnnotations.length ) ); + message = component.l10n.lintError.plural.replace( '%d', String( errorAnnotations.length ) ); } - errorNotice.append( $( '

', { - text: message - } ) ); - noticeContainer.slideDown( 'fast' ); - wp.a11y.speak( message ); + noticeElement = component.addNotice({ + code: 'lint_errors', + type: 'error', + message: message, + dismissible: false + }); + noticeElement.find( 'input[type=checkbox]' ).on( 'click', function() { + codeEditorSettings.onChangeLintingErrors( [] ); + component.removeNotice( 'lint_errors' ); + } ); } else { - noticeContainer.slideUp( 'fast' ); + component.removeNotice( 'lint_errors' ); } }; editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings ); + editor.codemirror.on( 'change', component.onChange ); // Improve the editor accessibility. $( editor.codemirror.display.lineDiv ) diff --git a/src/wp-admin/plugin-editor.php b/src/wp-admin/plugin-editor.php index bc21642153..636e1cf739 100644 --- a/src/wp-admin/plugin-editor.php +++ b/src/wp-admin/plugin-editor.php @@ -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('

%s

', __('No such file exists! Double check the name and try again.'))); @@ -212,17 +137,21 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) { '

' . __('Support Forums') . '

' ); - $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 ); ?> - -

- -

fatal error.' ); ?>

- 'error_scrape', - 'plugin' => urlencode( $plugin ), - '_wpnonce' => urlencode( $_GET['_error_nonce'] ), - ), admin_url( 'plugins.php' ) ); - ?> - - -
-

+ +
+

+
+ +
+

+
get_error_message() ? $edit_error->get_error_message() : $edit_error->get_error_code() ); ?>
+
+ +

@@ -283,7 +207,7 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {

-
+ -
- -
-

Warning: Making changes to active plugins is not recommended. If your changes cause a fatal error, the plugin will be automatically deactivated.'); ?>

+
+ +
+

Warning: Making changes to active plugins is not recommended.'); ?>

- + +

- "; - submit_button( __( 'Update File and Attempt to Reactivate' ), 'primary', 'submit', false ); - } else { - submit_button( __( 'Update File' ), 'primary', 'submit', false ); - } - ?> + +

the Codex for more information.'); ?>

+
- errors() && 'theme_no_stylesheet' == $theme->errors()->get_error_co $allowed_files = $style_files = array(); $has_templates = false; -$default_types = 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 the list of file types allowed for editing in the Theme editor. - * - * @since 4.4.0 - * - * @param array $default_types List of file types. Default types include 'php' and 'css'. - * @param WP_Theme $theme The current Theme object. - */ -$file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme ); - -// Ensure that default types are still there. -$file_types = array_unique( array_merge( $file_types, $default_types ) ); +$file_types = wp_get_theme_file_editable_extensions( $theme ); foreach ( $file_types as $type ) { switch ( $type ) { @@ -143,33 +98,35 @@ if ( empty( $file ) ) { } validate_file_to_edit( $file, $allowed_files ); -$scrollto = isset( $_REQUEST['scrollto'] ) ? (int) $_REQUEST['scrollto'] : 0; -switch( $action ) { -case 'update': - check_admin_referer( 'edit-theme_' . $file . $stylesheet ); - $newcontent = wp_unslash( $_POST['newcontent'] ); - $location = 'theme-editor.php?file=' . urlencode( $relative_file ) . '&theme=' . urlencode( $stylesheet ) . '&scrollto=' . $scrollto; - if ( is_writeable( $file ) ) { - // is_writable() not always reliable, check return value. see comments @ https://secure.php.net/is_writable - $f = fopen( $file, 'w+' ); - if ( $f !== false ) { - fwrite( $f, $newcontent ); - fclose( $f ); - $location .= '&updated=true'; - $theme->cache_delete(); +// 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-theme_' . $file . $stylesheet, 'nonce', false ) && isset( $_POST['newcontent'] ) ) { + $posted_content = wp_unslash( $_POST['newcontent'] ); } + } else { + wp_redirect( add_query_arg( + array( + 'a' => 1, // This means "success" for some reason. + 'theme' => $stylesheet, + 'file' => $relative_file, + ), + admin_url( 'theme-editor.php' ) + ) ); + exit; } - wp_redirect( $location ); - exit; +} -default: - - $settings = wp_enqueue_code_editor( compact( '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( compact( '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' ); @@ -179,7 +136,9 @@ default: $error = true; $content = ''; - if ( ! $error && filesize( $file ) > 0 ) { + if ( ! empty( $posted_content ) ) { + $content = $posted_content; + } elseif ( ! $error && filesize( $file ) > 0 ) { $f = fopen($file, 'r'); $content = fread($f, filesize($file)); @@ -197,10 +156,6 @@ default: $content = esc_textarea( $content ); } - if ( isset( $_GET['updated'] ) ) : ?> -

-

+ +
+

+
+ +
+

+
get_error_message() ? $edit_error->get_error_message() : $edit_error->get_error_code() ); ?>
+
+ +

display( 'Name' ); if ( $description ) echo ': ' . $description; ?>

-
+ -
@@ -316,32 +281,33 @@ else : ?>
-
- get_stylesheet() == get_template() ) : ?> -

-

- - -

the Codex for more information.'); ?>

- +
+
+ get_stylesheet() == get_template() ) : ?> +
+

+ + +

+
+
+ +

+ + +

+ +

the Codex for more information.'); ?>

+ +
+
- 'scrape_nonce_failure', + 'message' => __( 'Scrape nonce check failed. Please try again.' ), + ) ); + die(); + } + register_shutdown_function( 'wp_finalize_scraping_edited_file_errors', $key ); +} + +/** + * Finalize scraping for edited file errors. + * + * @since 4.9.0 + * + * @param string $scrape_key Scrape key. + */ +function wp_finalize_scraping_edited_file_errors( $scrape_key ) { + $error = error_get_last(); + if ( empty( $error ) ) { + return; + } + if ( ! in_array( $error['type'], array( E_CORE_ERROR, E_COMPILE_ERROR, E_ERROR, E_PARSE, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) { + return; + } + $error = str_replace( ABSPATH, '', $error ); + echo "###### begin_scraped_error:$scrape_key ######"; + echo wp_json_encode( $error ); +} diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 250089dc25..7f0e75dfe5 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -471,11 +471,14 @@ function wp_default_scripts( &$scripts ) { $scripts->add( 'htmlhint', '/wp-includes/js/codemirror/htmlhint.js', array(), '0.9.14-xwp' ); $scripts->add( 'htmlhint-kses', '/wp-includes/js/codemirror/htmlhint-kses.js', array( 'htmlhint' ) ); $scripts->add( 'code-editor', "/wp-admin/js/code-editor$suffix.js", array( 'jquery', 'wp-codemirror' ) ); - $scripts->add( 'wp-theme-plugin-editor', "/wp-admin/js/theme-plugin-editor$suffix.js", array( 'code-editor', 'jquery', 'jquery-ui-core', 'wp-a11y', 'underscore' ) ); - did_action( 'init' ) && $scripts->add_inline_script( 'wp-theme-plugin-editor', sprintf( 'wp.themePluginEditor.l10n = %s;', wp_json_encode( wp_array_slice_assoc( - /* translators: %d: error count */ - _n_noop( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.' ), - array( 'singular', 'plural' ) + $scripts->add( 'wp-theme-plugin-editor', "/wp-admin/js/theme-plugin-editor$suffix.js", array( 'wp-util', 'jquery', 'jquery-ui-core', 'wp-a11y', 'underscore' ) ); + did_action( 'init' ) && $scripts->add_inline_script( 'wp-theme-plugin-editor', sprintf( 'wp.themePluginEditor.l10n = %s;', wp_json_encode( array( + 'saveAlert' => __( 'The changes you made will be lost if you navigate away from this page.' ), + 'lintError' => wp_array_slice_assoc( + /* translators: %d: error count */ + _n_noop( 'There is %d error which must be fixed before you can update this file.', 'There are %d errors which must be fixed before you can update this file.' ), + array( 'singular', 'plural' ) + ), ) ) ) ); $scripts->add( 'wp-playlist', "/wp-includes/js/mediaelement/wp-playlist$suffix.js", array( 'wp-util', 'backbone', 'mediaelement' ), false, 1 ); diff --git a/src/wp-settings.php b/src/wp-settings.php index 3d4c210338..bacf4cfddd 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -294,6 +294,8 @@ require( ABSPATH . WPINC . '/vars.php' ); create_initial_taxonomies(); create_initial_post_types(); +wp_start_scraping_edited_file_errors(); + // Register the default theme directory root register_theme_directory( get_theme_root() );