From 1a22fb3b60577c720d5b457125983181e0b54098 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 10 Oct 2017 05:27:49 +0000 Subject: [PATCH] File Editor: Increase robustness of fatal error checking when saving PHP file edits. * Increase PHP execution time limit prior to issuing loopback requests where are themselves given timeouts to ensure PHP file can be reverted. * Output scrape messages on success and failure so that absence of either can also be flagged as an error condition. * Forward browser's HTTP Basic Auth credentials in loopback requests to admin and home URL. * Display more helpful message when loopback request fails. Amends [41721]. See #21622. Fixes #42102. Built from https://develop.svn.wordpress.org/trunk@41805 git-svn-id: http://core.svn.wordpress.org/trunk@41639 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/includes/file.php | 71 +++++++++++++++++++++----- wp-admin/js/theme-plugin-editor.js | 6 ++- wp-admin/js/theme-plugin-editor.min.js | 2 +- wp-includes/load.php | 18 +++---- wp-includes/script-loader.php | 1 + wp-includes/version.php | 2 +- 6 files changed, 73 insertions(+), 27 deletions(-) diff --git a/wp-admin/includes/file.php b/wp-admin/includes/file.php index 553880a46c..456114b1bc 100644 --- a/wp-admin/includes/file.php +++ b/wp-admin/includes/file.php @@ -486,7 +486,19 @@ function wp_edit_theme_plugin_file( $args ) { 'Cache-Control' => 'no-cache', ); - $needle = "###### begin_scraped_error:$scrape_key ######"; + // Include Basic auth in loopback requests. + if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) { + $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) ); + } + + // Make sure PHP process doesn't die before loopback requests complete. + @set_time_limit( 300 ); + + // Time to wait for loopback requests to finish. + $timeout = 100; + + $needle_start = "###### wp_scraping_result_start:$scrape_key ######"; + $needle_end = "###### wp_scraping_result_end:$scrape_key ######"; // Attempt loopback request to editor to see if user just whitescreened themselves. if ( $plugin ) { @@ -503,36 +515,67 @@ function wp_edit_theme_plugin_file( $args ) { $url = admin_url(); } $url = add_query_arg( $scrape_params, $url ); - $r = wp_remote_get( $url, compact( 'cookies', 'headers' ) ); + $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout' ) ); $body = wp_remote_retrieve_body( $r ); - $error_position = strpos( $body, $needle ); + $scrape_result_position = strpos( $body, $needle_start ); + + $loopback_request_failure = array( + 'code' => 'loopback_request_failed', + 'message' => __( 'Unable to communicate back with site to check for fatal errors, so the PHP change was reverted. You will need to upload your PHP file change by some other means, such as by using SFTP.' ), + ); + $json_parse_failure = array( + 'code' => 'json_parse_error', + ); + + $result = null; + if ( false === $scrape_result_position ) { + $result = $loopback_request_failure; + } else { + $error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) ); + $error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) ); + $result = json_decode( trim( $error_output ), true ); + if ( empty( $result ) ) { + $result = $json_parse_failure; + } + } // Try making request to homepage as well to see if visitors have been whitescreened. - if ( false === $error_position ) { + if ( true === $result ) { $url = home_url( '/' ); $url = add_query_arg( $scrape_params, $url ); - $r = wp_remote_get( $url, compact( 'cookies', 'headers' ) ); + $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout' ) ); $body = wp_remote_retrieve_body( $r ); - $error_position = strpos( $body, $needle ); + $scrape_result_position = strpos( $body, $needle_start ); + + if ( false === $scrape_result_position ) { + $result = $loopback_request_failure; + } else { + $error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) ); + $error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) ); + $result = json_decode( trim( $error_output ), true ); + if ( empty( $result ) ) { + $result = $json_parse_failure; + } + } } delete_transient( $transient ); - if ( false !== $error_position ) { + if ( true !== $result ) { + + // Roll-back file change. 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; + if ( ! isset( $result['message'] ) ) { + $message = __( 'An unidentified error has occurred.' ); } else { - $message = $error['message']; - unset( $error['message'] ); + $message = $result['message']; + unset( $result['message'] ); } - return new WP_Error( 'php_error', $message, $error ); + return new WP_Error( 'php_error', $message, $result ); } } diff --git a/wp-admin/js/theme-plugin-editor.js b/wp-admin/js/theme-plugin-editor.js index d7a89e0a98..683d7fb562 100644 --- a/wp-admin/js/theme-plugin-editor.js +++ b/wp-admin/js/theme-plugin-editor.js @@ -13,7 +13,8 @@ wp.themePluginEditor = (function( $ ) { singular: '', plural: '' }, - saveAlert: '' + saveAlert: '', + saveError: '' }, codeEditor: {}, instance: null, @@ -161,7 +162,8 @@ wp.themePluginEditor = (function( $ ) { request.fail( function ( response ) { var notice = $.extend( { - code: 'save_error' + code: 'save_error', + message: component.l10n.saveError }, response, { diff --git a/wp-admin/js/theme-plugin-editor.min.js b/wp-admin/js/theme-plugin-editor.min.js index 3f8d34b67e..d14346257f 100644 --- a/wp-admin/js/theme-plugin-editor.min.js +++ b/wp-admin/js/theme-plugin-editor.min.js @@ -1 +1 @@ -window.wp||(window.wp={}),wp.themePluginEditor=function(a){"use strict";var b={l10n:{lintError:{singular:"",plural:""},saveAlert:""},codeEditor:{},instance:null,noticeElements:{},dirty:!1,lintErrors:[]};return b.init=function(c,d){b.form=c,d&&a.extend(b,d),b.noticeTemplate=wp.template("wp-file-editor-notice"),b.noticesContainer=b.form.find(".editor-notices"),b.submitButton=b.form.find(":input[name=submit]"),b.spinner=b.form.find(".submit .spinner"),b.form.on("submit",b.submit),b.textarea=b.form.find("#newcontent"),b.textarea.on("change",b.onChange),b.warning=a(".file-editor-warning"),b.warning.length>0&&(a("body").addClass("modal-open"),b.warning.find(".file-editor-warning-dismiss").focus(),b.warning.on("click",".file-editor-warning-dismiss",b.dismissWarning)),!1!==b.codeEditor&&_.defer(function(){b.initCodeEditor()}),a(window).on("beforeunload",function(){if(b.dirty)return b.l10n.saveAlert})},b.dismissWarning=function(){wp.ajax.post("dismiss-wp-pointer",{pointer:b.themeOrPlugin+"_editor_notice"}),b.warning.remove(),a("body").removeClass("modal-open"),b.instance.codemirror.focus()},b.onChange=function(){b.dirty=!0,b.removeNotice("file_saved")},b.submit=function(c){var d,e={};if(c.preventDefault(),a.each(b.form.serializeArray(),function(){e[this.name]=this.value}),b.instance&&(e.newcontent=b.instance.codemirror.getValue()),!b.isSaving){if(b.lintErrors.length)return void b.instance.codemirror.setCursor(b.lintErrors[0].from.line);b.isSaving=!0,b.textarea.prop("readonly",!0),b.instance&&b.instance.codemirror.setOption("readOnly",!0),b.spinner.addClass("is-active"),d=wp.ajax.post("edit-theme-plugin-file",e),b.lastSaveNoticeCode&&b.removeNotice(b.lastSaveNoticeCode),d.done(function(a){b.lastSaveNoticeCode="file_saved",b.addNotice({code:b.lastSaveNoticeCode,type:"success",message:a.message,dismissible:!0}),b.dirty=!1}),d.fail(function(c){var d=a.extend({code:"save_error"},c,{type:"error",dismissible:!0});b.lastSaveNoticeCode=d.code,b.addNotice(d)}),d.always(function(){b.spinner.removeClass("is-active"),b.isSaving=!1,b.textarea.prop("readonly",!1),b.instance&&b.instance.codemirror.setOption("readOnly",!1)})}},b.addNotice=function(c){var d;if(!c.code)throw new Error("Missing code.");return b.removeNotice(c.code),d=a(b.noticeTemplate(c)),d.hide(),d.find(".notice-dismiss").on("click",function(){b.removeNotice(c.code),c.onDismiss&&c.onDismiss(c)}),wp.a11y.speak(c.message),b.noticesContainer.append(d),d.slideDown("fast"),b.noticeElements[c.code]=d,d},b.removeNotice=function(c){return!!b.noticeElements[c]&&(b.noticeElements[c].slideUp("fast",function(){a(this).remove()}),delete b.noticeElements[c],!0)},b.initCodeEditor=function(){var c,d;c=a.extend({},b.codeEditor),c.onTabPrevious=function(){a("#templateside").find(":tabbable").last().focus()},c.onTabNext=function(){a("#template").find(":tabbable:not(.CodeMirror-code)").first().focus()},c.onChangeLintingErrors=function(a){b.lintErrors=a,0===a.length&&b.submitButton.toggleClass("disabled",!1)},c.onUpdateErrorNotice=function(a){var d,e;b.submitButton.toggleClass("disabled",a.length>0),0!==a.length?(d=1===a.length?b.l10n.lintError.singular.replace("%d","1"):b.l10n.lintError.plural.replace("%d",String(a.length)),e=b.addNotice({code:"lint_errors",type:"error",message:d,dismissible:!1}),e.find("input[type=checkbox]").on("click",function(){c.onChangeLintingErrors([]),b.removeNotice("lint_errors")})):b.removeNotice("lint_errors")},d=wp.codeEditor.initialize(a("#newcontent"),c),d.codemirror.on("change",b.onChange),a(d.codemirror.display.lineDiv).attr({role:"textbox","aria-multiline":"true","aria-labelledby":"theme-plugin-editor-label","aria-describedby":"editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4"}),a("#theme-plugin-editor-label").on("click",function(){d.codemirror.focus()}),b.instance=d},b}(jQuery); \ No newline at end of file +window.wp||(window.wp={}),wp.themePluginEditor=function(a){"use strict";var b={l10n:{lintError:{singular:"",plural:""},saveAlert:"",saveError:""},codeEditor:{},instance:null,noticeElements:{},dirty:!1,lintErrors:[]};return b.init=function(c,d){b.form=c,d&&a.extend(b,d),b.noticeTemplate=wp.template("wp-file-editor-notice"),b.noticesContainer=b.form.find(".editor-notices"),b.submitButton=b.form.find(":input[name=submit]"),b.spinner=b.form.find(".submit .spinner"),b.form.on("submit",b.submit),b.textarea=b.form.find("#newcontent"),b.textarea.on("change",b.onChange),b.warning=a(".file-editor-warning"),b.warning.length>0&&(a("body").addClass("modal-open"),b.warning.find(".file-editor-warning-dismiss").focus(),b.warning.on("click",".file-editor-warning-dismiss",b.dismissWarning)),!1!==b.codeEditor&&_.defer(function(){b.initCodeEditor()}),a(window).on("beforeunload",function(){if(b.dirty)return b.l10n.saveAlert})},b.dismissWarning=function(){wp.ajax.post("dismiss-wp-pointer",{pointer:b.themeOrPlugin+"_editor_notice"}),b.warning.remove(),a("body").removeClass("modal-open"),b.instance.codemirror.focus()},b.onChange=function(){b.dirty=!0,b.removeNotice("file_saved")},b.submit=function(c){var d,e={};if(c.preventDefault(),a.each(b.form.serializeArray(),function(){e[this.name]=this.value}),b.instance&&(e.newcontent=b.instance.codemirror.getValue()),!b.isSaving){if(b.lintErrors.length)return void b.instance.codemirror.setCursor(b.lintErrors[0].from.line);b.isSaving=!0,b.textarea.prop("readonly",!0),b.instance&&b.instance.codemirror.setOption("readOnly",!0),b.spinner.addClass("is-active"),d=wp.ajax.post("edit-theme-plugin-file",e),b.lastSaveNoticeCode&&b.removeNotice(b.lastSaveNoticeCode),d.done(function(a){b.lastSaveNoticeCode="file_saved",b.addNotice({code:b.lastSaveNoticeCode,type:"success",message:a.message,dismissible:!0}),b.dirty=!1}),d.fail(function(c){var d=a.extend({code:"save_error",message:b.l10n.saveError},c,{type:"error",dismissible:!0});b.lastSaveNoticeCode=d.code,b.addNotice(d)}),d.always(function(){b.spinner.removeClass("is-active"),b.isSaving=!1,b.textarea.prop("readonly",!1),b.instance&&b.instance.codemirror.setOption("readOnly",!1)})}},b.addNotice=function(c){var d;if(!c.code)throw new Error("Missing code.");return b.removeNotice(c.code),d=a(b.noticeTemplate(c)),d.hide(),d.find(".notice-dismiss").on("click",function(){b.removeNotice(c.code),c.onDismiss&&c.onDismiss(c)}),wp.a11y.speak(c.message),b.noticesContainer.append(d),d.slideDown("fast"),b.noticeElements[c.code]=d,d},b.removeNotice=function(c){return!!b.noticeElements[c]&&(b.noticeElements[c].slideUp("fast",function(){a(this).remove()}),delete b.noticeElements[c],!0)},b.initCodeEditor=function(){var c,d;c=a.extend({},b.codeEditor),c.onTabPrevious=function(){a("#templateside").find(":tabbable").last().focus()},c.onTabNext=function(){a("#template").find(":tabbable:not(.CodeMirror-code)").first().focus()},c.onChangeLintingErrors=function(a){b.lintErrors=a,0===a.length&&b.submitButton.toggleClass("disabled",!1)},c.onUpdateErrorNotice=function(a){var d,e;b.submitButton.toggleClass("disabled",a.length>0),0!==a.length?(d=1===a.length?b.l10n.lintError.singular.replace("%d","1"):b.l10n.lintError.plural.replace("%d",String(a.length)),e=b.addNotice({code:"lint_errors",type:"error",message:d,dismissible:!1}),e.find("input[type=checkbox]").on("click",function(){c.onChangeLintingErrors([]),b.removeNotice("lint_errors")})):b.removeNotice("lint_errors")},d=wp.codeEditor.initialize(a("#newcontent"),c),d.codemirror.on("change",b.onChange),a(d.codemirror.display.lineDiv).attr({role:"textbox","aria-multiline":"true","aria-labelledby":"theme-plugin-editor-label","aria-describedby":"editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4"}),a("#theme-plugin-editor-label").on("click",function(){d.codemirror.focus()}),b.instance=d},b}(jQuery); \ No newline at end of file diff --git a/wp-includes/load.php b/wp-includes/load.php index 0dcf31cbe5..d7166190f9 100644 --- a/wp-includes/load.php +++ b/wp-includes/load.php @@ -1126,11 +1126,12 @@ function wp_start_scraping_edited_file_errors() { $nonce = wp_unslash( $_REQUEST['wp_scrape_nonce'] ); if ( get_transient( 'scrape_key_' . $key ) !== $nonce ) { - echo "###### begin_scraped_error:$key ######"; + echo "###### wp_scraping_result_start:$key ######"; echo wp_json_encode( array( 'code' => 'scrape_nonce_failure', 'message' => __( 'Scrape nonce check failed. Please try again.' ), ) ); + echo "###### wp_scraping_result_end:$key ######"; die(); } register_shutdown_function( 'wp_finalize_scraping_edited_file_errors', $key ); @@ -1145,13 +1146,12 @@ function wp_start_scraping_edited_file_errors() { */ function wp_finalize_scraping_edited_file_errors( $scrape_key ) { $error = error_get_last(); - if ( empty( $error ) ) { - return; + echo "\n###### wp_scraping_result_start:$scrape_key ######\n"; + if ( ! empty( $error ) && in_array( $error['type'], array( E_CORE_ERROR, E_COMPILE_ERROR, E_ERROR, E_PARSE, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) { + $error = str_replace( ABSPATH, '', $error ); + echo wp_json_encode( $error ); + } else { + echo wp_json_encode( true ); } - 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 ); + echo "\n###### wp_scraping_result_end:$scrape_key ######\n"; } diff --git a/wp-includes/script-loader.php b/wp-includes/script-loader.php index 8989cf0ce3..067b941e6d 100644 --- a/wp-includes/script-loader.php +++ b/wp-includes/script-loader.php @@ -472,6 +472,7 @@ function wp_default_scripts( &$scripts ) { $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.' ), + 'saveError' => __( 'Something went wrong. Your change may not have been saved. Please try again. There is also a chance that you may need to manually fix and upload the file over FTP.' ), '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.' ), diff --git a/wp-includes/version.php b/wp-includes/version.php index b0d3f16a57..0f2cda0c68 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -4,7 +4,7 @@ * * @global string $wp_version */ -$wp_version = '4.9-beta1-41804'; +$wp_version = '4.9-beta1-41805'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.