WordPress.org

Make WordPress Core

Ticket #21622: 21622.0.diff

File 21622.0.diff, 48.8 KB (added by westonruter, 2 years ago)

https://github.com/xwp/wordpress-develop/pull/272

  • src/wp-admin/admin-ajax.php

    diff --git src/wp-admin/admin-ajax.php src/wp-admin/admin-ajax.php
    index 15c352de94..4a18fcf714 100644
    $core_actions_post = array( 
    6464        'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'crop-image',
    6565        'generate-password', 'save-wporg-username', 'delete-plugin', 'search-plugins',
    6666        'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme', 'install-theme',
    67         'get-post-thumbnail-html', 'get-community-events',
     67        'get-post-thumbnail-html', 'get-community-events', 'edit-theme-plugin-file',
    6868);
    6969
    7070// Deprecated
  • src/wp-admin/css/common.css

    diff --git src/wp-admin/css/common.css src/wp-admin/css/common.css
    index 1469097a8a..d91e8a1368 100644
    h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */ 
    22172217#template > div {
    22182218        margin-right: 190px;
    22192219}
    2220 #template .active-plugin-edit-warning {
     2220#template .notice {
    22212221        margin-top: 1em;
    2222         margin-right: 30%;
    2223         margin-right: calc( 184px + 3% );
     2222        margin-right: 3%;
    22242223}
    2225 #template .active-plugin-edit-warning p {
     2224#template .notice p {
    22262225        width: auto;
    22272226}
     2227#template .submit .spinner {
     2228        float: none;
     2229}
    22282230
    22292231.metabox-holder .stuffbox > h3, /* Back-compat for pre-4.4 */
    22302232.metabox-holder .postbox > h3, /* Back-compat for pre-4.4 */
    img { 
    30323034#template textarea,
    30333035#template .CodeMirror {
    30343036        width: 97%;
    3035         height: calc( 100vh - 220px );
     3037        height: calc( 100vh - 280px );
     3038}
     3039#templateside {
     3040        margin-top: 31px;
     3041        overflow: scroll;
    30363042}
    30373043
    3038 #template label {
     3044#theme-plugin-editor-label {
    30393045        display: inline-block;
    30403046        margin-bottom: 1em;
    30413047        font-weight: 600;
    img { 
    30473053        direction: ltr;
    30483054}
    30493055
     3056.fileedit-sub #theme,
     3057.fileedit-sub #plugin {
     3058        max-width: 40%;
     3059}
     3060.fileedit-sub .alignright {
     3061        text-align: right;
     3062}
     3063
    30503064#template p {
    30513065        width: 97%;
    30523066}
    img { 
    36243638        }
    36253639
    36263640        #template > div,
    3627         #template  .active-plugin-edit-warning {
     3641        #template .notice {
    36283642                float: none;
    36293643                margin: 1em 0;
    36303644                width: auto;
  • src/wp-admin/includes/ajax-actions.php

    diff --git src/wp-admin/includes/ajax-actions.php src/wp-admin/includes/ajax-actions.php
    index 53fd8671d1..8dbd75c5a3 100644
    function wp_ajax_search_install_plugins() { 
    39663966
    39673967        wp_send_json_success( $status );
    39683968}
     3969
     3970/**
     3971 * Ajax handler for editing a theme or plugin file.
     3972 *
     3973 * @since 4.9.0
     3974 * @see wp_edit_theme_plugin_file()
     3975 */
     3976function wp_ajax_edit_theme_plugin_file() {
     3977        $r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) ); // Validation of args is done in wp_edit_theme_plugin_file().
     3978        if ( is_wp_error( $r ) ) {
     3979                wp_send_json_error( array_merge(
     3980                        array(
     3981                                'code' => $r->get_error_code(),
     3982                                'message' => $r->get_error_message(),
     3983                        ),
     3984                        (array) $r->get_error_data()
     3985                ) );
     3986        } else {
     3987                wp_send_json_success( array(
     3988                        'message' => __( 'File edited successfully.' ),
     3989                ) );
     3990        }
     3991}
  • src/wp-admin/includes/file.php

    diff --git src/wp-admin/includes/file.php src/wp-admin/includes/file.php
    index 05bfde46a4..553880a46c 100644
    $wp_file_descriptions = array( 
    7070 * @since 1.5.0
    7171 *
    7272 * @global array $wp_file_descriptions Theme file descriptions.
    73  * @global array $allowed_files        List of allowed files. 
     73 * @global array $allowed_files        List of allowed files.
    7474 * @param string $file Filesystem path or filename
    7575 * @return string Description of file from $wp_file_descriptions or basename of $file if description doesn't exist.
    7676 *                Appends 'Page Template' to basename of $file if the file is a page template
    function list_files( $folder = '', $levels = 100 ) { 
    152152        return $files;
    153153}
    154154
     155/**
     156 * Get list of file extensions that are editable in plugins.
     157 *
     158 * @since 4.9.0
     159 *
     160 * @param string $plugin Plugin.
     161 * @return array File extensions.
     162 */
     163function wp_get_plugin_file_editable_extensions( $plugin ) {
     164
     165        $editable_extensions = array(
     166                'bash',
     167                'conf',
     168                'css',
     169                'diff',
     170                'htm',
     171                'html',
     172                'http',
     173                'inc',
     174                'include',
     175                'js',
     176                'json',
     177                'jsx',
     178                'less',
     179                'md',
     180                'patch',
     181                'php',
     182                'php3',
     183                'php4',
     184                'php5',
     185                'php7',
     186                'phps',
     187                'phtml',
     188                'sass',
     189                'scss',
     190                'sh',
     191                'sql',
     192                'svg',
     193                'text',
     194                'txt',
     195                'xml',
     196                'yaml',
     197                'yml',
     198        );
     199
     200        /**
     201         * Filters file type extensions editable in the plugin editor.
     202         *
     203         * @since 2.8.0
     204         * @since 4.9.0 Adds $plugin param.
     205         *
     206         * @param string $plugin Plugin file.
     207         * @param array $editable_extensions An array of editable plugin file extensions.
     208         */
     209        $editable_extensions = (array) apply_filters( 'editable_extensions', $editable_extensions, $plugin );
     210
     211        return $editable_extensions;
     212}
     213
     214/**
     215 * Get list of file extensions that are editable for a given theme.
     216 *
     217 * @param WP_Theme $theme Theme.
     218 * @return array File extensions.
     219 */
     220function wp_get_theme_file_editable_extensions( $theme ) {
     221
     222        $default_types = array(
     223                'bash',
     224                'conf',
     225                'css',
     226                'diff',
     227                'htm',
     228                'html',
     229                'http',
     230                'inc',
     231                'include',
     232                'js',
     233                'json',
     234                'jsx',
     235                'less',
     236                'md',
     237                'patch',
     238                'php',
     239                'php3',
     240                'php4',
     241                'php5',
     242                'php7',
     243                'phps',
     244                'phtml',
     245                'sass',
     246                'scss',
     247                'sh',
     248                'sql',
     249                'svg',
     250                'text',
     251                'txt',
     252                'xml',
     253                'yaml',
     254                'yml',
     255        );
     256
     257        /**
     258         * Filters the list of file types allowed for editing in the Theme editor.
     259         *
     260         * @since 4.4.0
     261         *
     262         * @param array    $default_types List of file types. Default types include 'php' and 'css'.
     263         * @param WP_Theme $theme         The current Theme object.
     264         */
     265        $file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme );
     266
     267        // Ensure that default types are still there.
     268        return array_unique( array_merge( $file_types, $default_types ) );
     269}
     270
     271/**
     272 * Print file editor templates (for plugins and themes).
     273 *
     274 * @since 4.9.0
     275 */
     276function wp_print_file_editor_templates() {
     277        ?>
     278        <script type="text/html" id="tmpl-wp-file-editor-notice">
     279                <div class="notice inline notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.classes || '' }}">
     280                        <# if ( 'php_error' === data.code ) { #>
     281                                <p>
     282                                        <?php
     283                                        printf(
     284                                                /* translators: %$1s is line number and %1$s is file path. */
     285                                                __( 'Your PHP code changes were rolled back due to an error on line %1$s of file %2$s. Please fix and try saving again.' ),
     286                                                '{{ data.line }}',
     287                                                '{{ data.file }}'
     288                                        );
     289                                        ?>
     290                                </p>
     291                                <pre>{{ data.message }}</pre>
     292                        <# } else if ( 'file_not_writable' === data.code ) { #>
     293                                <p><?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.' ); ?></p>
     294                        <# } else { #>
     295                                <p>{{ data.message || data.code }}</p>
     296
     297                                <# if ( 'lint_errors' === data.code ) { #>
     298                                        <p>
     299                                                <# var elementId = 'el-' + String( Math.random() ); #>
     300                                                <input id="{{ elementId }}"  type="checkbox">
     301                                                <label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label>
     302                                        </p>
     303                                <# } #>
     304                        <# } #>
     305                        <# if ( data.dismissible ) { #>
     306                                <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
     307                        <# } #>
     308                </div>
     309        </script>
     310        <?php
     311}
     312
     313/**
     314 * Attempt to edit a file for a theme or plugin.
     315 *
     316 * When editing a PHP file, loopback requests will be made to the admin and the homepage
     317 * to attempt to see if there is a fatal error introduced. If so, the PHP change will be
     318 * reverted.
     319 *
     320 * @since 4.9.0
     321 *
     322 * @param array $args {
     323 *     Args. Note that all of the arg values are already unslashed. They are, however,
     324 *     coming straight from $_POST and are not validated or sanitized in any way.
     325 *
     326 *     @type string $file       Relative path to file.
     327 *     @type string $plugin     Plugin being edited.
     328 *     @type string $theme      Theme being edited.
     329 *     @type string $newcontent New content for the file.
     330 *     @type string $nonce      Nonce.
     331 * }
     332 * @return true|WP_Error True on success or `WP_Error` on failure.
     333 */
     334function wp_edit_theme_plugin_file( $args ) {
     335        if ( empty( $args['file'] ) ) {
     336                return new WP_Error( 'missing_file' );
     337        }
     338        $file = $args['file'];
     339        if ( 0 !== validate_file( $file ) ) {
     340                return new WP_Error( 'bad_file' );
     341        }
     342
     343        if ( ! isset( $args['newcontent'] ) ) {
     344                return new WP_Error( 'missing_content' );
     345        }
     346        $content = $args['newcontent'];
     347
     348        if ( ! isset( $args['nonce'] ) ) {
     349                return new WP_Error( 'missing_nonce' );
     350        }
     351
     352        $plugin = null;
     353        $theme = null;
     354        $real_file = null;
     355        if ( ! empty( $args['plugin'] ) ) {
     356                $plugin = $args['plugin'];
     357
     358                if ( ! current_user_can( 'edit_plugins' ) ) {
     359                        return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit plugins for this site.' ) );
     360                }
     361
     362                if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) {
     363                        return new WP_Error( 'nonce_failure' );
     364                }
     365
     366                if ( ! array_key_exists( $plugin, get_plugins() ) ) {
     367                        return new WP_Error( 'invalid_plugin' );
     368                }
     369
     370                if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) {
     371                        return new WP_Error( 'bad_plugin_file_path', __( 'Sorry, that file cannot be edited.' ) );
     372                }
     373
     374                $editable_extensions = wp_get_plugin_file_editable_extensions( $plugin );
     375
     376                $real_file = WP_PLUGIN_DIR . '/' . $file;
     377
     378                $is_active = in_array(
     379                        $plugin,
     380                        (array) get_option( 'active_plugins', array() ),
     381                        true
     382                );
     383
     384        } elseif ( ! empty( $args['theme'] ) ) {
     385                $stylesheet = $args['theme'];
     386                if ( 0 !== validate_file( $stylesheet ) ) {
     387                        return new WP_Error( 'bad_theme_path' );
     388                }
     389
     390                if ( ! current_user_can( 'edit_themes' ) ) {
     391                        return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit templates for this site.' ) );
     392                }
     393
     394                $theme = wp_get_theme( $stylesheet );
     395                if ( ! $theme->exists() ) {
     396                        return new WP_Error( 'non_existent_theme', __( 'The requested theme does not exist.' ) );
     397                }
     398
     399                $real_file = $theme->get_stylesheet_directory() . '/' . $file;
     400                if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $real_file . $stylesheet ) ) {
     401                        return new WP_Error( 'nonce_failure' );
     402                }
     403
     404                if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) {
     405                        return new WP_Error(
     406                                'theme_no_stylesheet',
     407                                __( 'The requested theme does not exist.' ) . ' ' . $theme->errors()->get_error_message()
     408                        );
     409                }
     410
     411                $editable_extensions = wp_get_theme_file_editable_extensions( $theme );
     412
     413                $allowed_files = array();
     414                foreach ( $editable_extensions as $type ) {
     415                        switch ( $type ) {
     416                                case 'php':
     417                                        $allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', 1 ) );
     418                                        break;
     419                                case 'css':
     420                                        $style_files = $theme->get_files( 'css' );
     421                                        $allowed_files['style.css'] = $style_files['style.css'];
     422                                        $allowed_files = array_merge( $allowed_files, $style_files );
     423                                        break;
     424                                default:
     425                                        $allowed_files = array_merge( $allowed_files, $theme->get_files( $type ) );
     426                                        break;
     427                        }
     428                }
     429
     430                if ( 0 !== validate_file( $real_file, $allowed_files ) ) {
     431                        return new WP_Error( 'disallowed_theme_file', __( 'Sorry, that file cannot be edited.' ) );
     432                }
     433
     434                $is_active = ( get_stylesheet() === $stylesheet || get_template() === $stylesheet );
     435        } else {
     436                return new WP_Error( 'missing_theme_or_plugin' );
     437        }
     438
     439        // Ensure file is real.
     440        if ( ! is_file( $real_file ) ) {
     441                return new WP_Error( 'file_does_not_exist', __( 'No such file exists! Double check the name and try again.' ) );
     442        }
     443
     444        // Ensure file extension is allowed.
     445        $extension = null;
     446        if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) {
     447                $extension = strtolower( $matches[1] );
     448                if ( ! in_array( $extension, $editable_extensions, true ) ) {
     449                        return new WP_Error( 'illegal_file_type', __( 'Files of this type are not editable.' ) );
     450                }
     451        }
     452
     453        $previous_content = file_get_contents( $real_file );
     454
     455        if ( ! is_writeable( $real_file ) ) {
     456                return new WP_Error( 'file_not_writable' );
     457        }
     458
     459        $f = fopen( $real_file, 'w+' );
     460        if ( false === $f ) {
     461                return new WP_Error( 'file_not_writable' );
     462        }
     463
     464        $written = fwrite( $f, $content );
     465        fclose( $f );
     466        if ( false === $written ) {
     467                return new WP_Error( 'unable_to_write', __( 'Unable to write to file.' ) );
     468        }
     469        if ( 'php' === $extension && function_exists( 'opcache_invalidate' ) ) {
     470                opcache_invalidate( $real_file, true );
     471        }
     472
     473        if ( $is_active && 'php' === $extension ) {
     474
     475                $scrape_key = md5( rand() );
     476                $transient = 'scrape_key_' . $scrape_key;
     477                $scrape_nonce = strval( rand() );
     478                set_transient( $transient, $scrape_nonce, 60 ); // It shouldn't take more than 60 seconds to make the two loopback requests.
     479
     480                $cookies = wp_unslash( $_COOKIE );
     481                $scrape_params = array(
     482                        'wp_scrape_key' => $scrape_key,
     483                        'wp_scrape_nonce' => $scrape_nonce,
     484                );
     485                $headers = array(
     486                        'Cache-Control' => 'no-cache',
     487                );
     488
     489                $needle = "###### begin_scraped_error:$scrape_key ######";
     490
     491                // Attempt loopback request to editor to see if user just whitescreened themselves.
     492                if ( $plugin ) {
     493                        $url = add_query_arg( compact( 'plugin', 'file' ), admin_url( 'plugin-editor.php' ) );
     494                } elseif ( isset( $stylesheet ) ) {
     495                        $url = add_query_arg(
     496                                array(
     497                                        'theme' => $stylesheet,
     498                                        'file' => $file,
     499                                ),
     500                                admin_url( 'theme-editor.php' )
     501                        );
     502                } else {
     503                        $url = admin_url();
     504                }
     505                $url = add_query_arg( $scrape_params, $url );
     506                $r = wp_remote_get( $url, compact( 'cookies', 'headers' ) );
     507                $body = wp_remote_retrieve_body( $r );
     508                $error_position = strpos( $body, $needle );
     509
     510                // Try making request to homepage as well to see if visitors have been whitescreened.
     511                if ( false === $error_position ) {
     512                        $url = home_url( '/' );
     513                        $url = add_query_arg( $scrape_params, $url );
     514                        $r = wp_remote_get( $url, compact( 'cookies', 'headers' ) );
     515                        $body = wp_remote_retrieve_body( $r );
     516                        $error_position = strpos( $body, $needle );
     517                }
     518
     519                delete_transient( $transient );
     520
     521                if ( false !== $error_position ) {
     522                        file_put_contents( $real_file, $previous_content );
     523                        if ( function_exists( 'opcache_invalidate' ) ) {
     524                                opcache_invalidate( $real_file, true );
     525                        }
     526
     527                        $error_output = trim( substr( $body, $error_position + strlen( $needle ) ) );
     528                        $error = json_decode( $error_output, true );
     529                        if ( ! isset( $error['message'] ) ) {
     530                                $message = $error_output;
     531                        } else {
     532                                $message = $error['message'];
     533                                unset( $error['message'] );
     534                        }
     535                        return new WP_Error( 'php_error', $message, $error );
     536                }
     537        }
     538
     539        if ( $theme instanceof WP_Theme ) {
     540                $theme->cache_delete();
     541        }
     542
     543        return true;
     544}
     545
     546
    155547/**
    156548 * Returns a filename of a Temporary unique file.
    157549 * Please note that the calling function must unlink() this itself.
  • src/wp-admin/js/theme-plugin-editor.js

    diff --git src/wp-admin/js/theme-plugin-editor.js src/wp-admin/js/theme-plugin-editor.js
    index 8e016c3837..3bb0788a6b 100644
    wp.themePluginEditor = (function( $ ) { 
    1212                        lintError: {
    1313                                singular: '',
    1414                                plural: ''
    15                         }
     15                        },
     16                        saveAlert: ''
    1617                },
    17                 instance: null
     18                codeEditor: {},
     19                instance: null,
     20                noticeElements: {},
     21                dirty: false,
     22                lintErrors: []
    1823        };
    1924
    2025        /**
    2126         * Initialize component.
    2227         *
    23          * @param {object} settings Settings.
     28         * @since 4.9.0
     29         *
     30         * @param {jQuery}         form - Form element.
     31         * @param {object}         settings - Settings.
     32         * @param {object|boolean} settings.codeEditor - Code editor settings (or `false` if syntax highlighting is disabled).
     33         * @returns {void}
     34         */
     35        component.init = function init( form, settings ) {
     36
     37                component.form = form;
     38                if ( settings ) {
     39                        $.extend( component, settings );
     40                }
     41
     42                component.noticeTemplate = wp.template( 'wp-file-editor-notice' );
     43                component.noticesContainer = component.form.find( '.editor-notices' );
     44                component.submitButton = component.form.find( ':input[name=submit]' );
     45                component.spinner = component.form.find( '.submit .spinner' );
     46                component.form.on( 'submit', component.submit );
     47                component.textarea = component.form.find( '#newcontent' );
     48                component.textarea.on( 'change', component.onChange );
     49
     50                if ( false !== component.codeEditor ) {
     51                        /*
     52                         * Defer adding notices until after DOM ready as workaround for WP Admin injecting
     53                         * its own managed dismiss buttons and also to prevent the editor from showing a notice
     54                         * when the file had linting errors to begin with.
     55                         */
     56                        _.defer( function() {
     57                                component.initCodeEditor();
     58                        } );
     59                }
     60
     61                $( window ).on( 'beforeunload', function() {
     62                        if ( component.dirty ) {
     63                                return component.l10n.saveAlert;
     64                        }
     65                        return undefined;
     66                } );
     67        };
     68
     69        /**
     70         * Callback for when a change happens.
     71         *
     72         * @since 4.9.0
     73         * @returns {void}
     74         */
     75        component.onChange = function() {
     76                component.dirty = true;
     77                component.removeNotice( 'file_saved' );
     78        };
     79
     80        /**
     81         * Submit file via Ajax.
     82         *
     83         * @since 4.9.0
     84         * @param {jQuery.Event} event - Event.
     85         * @returns {void}
     86         */
     87        component.submit = function( event ) {
     88                var data = {}, request;
     89                event.preventDefault(); // Prevent form submission in favor of Ajax below.
     90                $.each( component.form.serializeArray(), function() {
     91                        data[ this.name ] = this.value;
     92                } );
     93
     94                // Use value from codemirror if present.
     95                if ( component.instance ) {
     96                        data.newcontent = component.instance.codemirror.getValue();
     97                }
     98
     99                if ( component.isSaving ) {
     100                        return;
     101                }
     102
     103                // Scroll ot the line that has the error.
     104                if ( component.lintErrors.length ) {
     105                        component.instance.codemirror.setCursor( component.lintErrors[0].from.line );
     106                        return;
     107                }
     108
     109                component.isSaving = true;
     110                component.textarea.prop( 'readonly', true );
     111                if ( component.instance ) {
     112                        component.instance.codemirror.setOption( 'readOnly', true );
     113                }
     114
     115                component.spinner.addClass( 'is-active' );
     116                request = wp.ajax.post( 'edit-theme-plugin-file', data );
     117
     118                // Remove previous save notice before saving.
     119                if ( component.lastSaveNoticeCode ) {
     120                        component.removeNotice( component.lastSaveNoticeCode );
     121                }
     122
     123                request.done( function ( response ) {
     124                        component.lastSaveNoticeCode = 'file_saved';
     125                        component.addNotice({
     126                                code: component.lastSaveNoticeCode,
     127                                type: 'success',
     128                                message: response.message,
     129                                dismissible: true
     130                        });
     131                        component.dirty = false;
     132                } );
     133
     134                request.fail( function ( response ) {
     135                        var notice = $.extend(
     136                                {
     137                                        code: 'save_error'
     138                                },
     139                                response,
     140                                {
     141                                        type: 'error',
     142                                        dismissible: true
     143                                }
     144                        );
     145                        component.lastSaveNoticeCode = notice.code;
     146                        component.addNotice( notice );
     147                } );
     148
     149                request.always( function() {
     150                        component.spinner.removeClass( 'is-active' );
     151                        component.isSaving = false;
     152
     153                        component.textarea.prop( 'readonly', false );
     154                        if ( component.instance ) {
     155                                component.instance.codemirror.setOption( 'readOnly', false );
     156                        }
     157                } );
     158        };
     159
     160        /**
     161         * Add notice.
     162         *
     163         * @since 4.9.0
     164         *
     165         * @param {object}   notice - Notice.
     166         * @param {string}   notice.code - Code.
     167         * @param {string}   notice.type - Type.
     168         * @param {string}   notice.message - Message.
     169         * @param {boolean}  [notice.dismissible=false] - Dismissible.
     170         * @param {Function} [notice.onDismiss] - Callback for when a user dismisses the notice.
     171         * @returns {jQuery} Notice element.
     172         */
     173        component.addNotice = function( notice ) {
     174                var noticeElement;
     175
     176                if ( ! notice.code ) {
     177                        throw new Error( 'Missing code.' );
     178                }
     179
     180                // Only let one notice of a given type be displayed at a time.
     181                component.removeNotice( notice.code );
     182
     183                noticeElement = $( component.noticeTemplate( notice ) );
     184                noticeElement.hide();
     185
     186                noticeElement.find( '.notice-dismiss' ).on( 'click', function() {
     187                        component.removeNotice( notice.code );
     188                        if ( notice.onDismiss ) {
     189                                notice.onDismiss( notice );
     190                        }
     191                } );
     192
     193                wp.a11y.speak( notice.message );
     194
     195                component.noticesContainer.append( noticeElement );
     196                noticeElement.slideDown( 'fast' );
     197                component.noticeElements[ notice.code ] = noticeElement;
     198                return noticeElement;
     199        };
     200
     201        /**
     202         * Remove notice.
     203         *
     204         * @since 4.9.0
     205         *
     206         * @param {string} code - Notice code.
     207         * @returns {boolean} Whether a notice was removed.
     208         */
     209        component.removeNotice = function( code ) {
     210                if ( component.noticeElements[ code ] ) {
     211                        component.noticeElements[ code ].slideUp( 'fast', function() {
     212                                $( this ).remove();
     213                        } );
     214                        delete component.noticeElements[ code ];
     215                        return true;
     216                }
     217                return false;
     218        };
     219
     220        /**
     221         * Initialize code editor.
     222         *
     223         * @since 4.9.0
    24224         * @returns {void}
    25225         */
    26         component.init = function( settings ) {
    27                 var codeEditorSettings, noticeContainer, errorNotice = [], editor;
     226        component.initCodeEditor = function initCodeEditor() {
     227                var codeEditorSettings, editor;
    28228
    29                 codeEditorSettings = $.extend( {}, settings );
     229                codeEditorSettings = $.extend( {}, component.codeEditor );
    30230
    31231                /**
    32232                 * Handle tabbing to the field before the editor.
    33233                 *
     234                 * @since 4.9.0
     235                 *
    34236                 * @returns {void}
    35237                 */
    36238                codeEditorSettings.onTabPrevious = function() {
    wp.themePluginEditor = (function( $ ) { 
    40242                /**
    41243                 * Handle tabbing to the field after the editor.
    42244                 *
     245                 * @since 4.9.0
     246                 *
    43247                 * @returns {void}
    44248                 */
    45249                codeEditorSettings.onTabNext = function() {
    46250                        $( '#template' ).find( ':tabbable:not(.CodeMirror-code)' ).first().focus();
    47251                };
    48252
    49                 // Create the error notice container.
    50                 noticeContainer = $( '<div id="file-editor-linting-error"></div>' );
    51                 errorNotice = $( '<div class="inline notice notice-error"></div>' );
    52                 noticeContainer.append( errorNotice );
    53                 noticeContainer.hide();
    54                 $( 'p.submit' ).before( noticeContainer );
     253                /**
     254                 * Handle change to the linting errors.
     255                 *
     256                 * @since 4.9.0
     257                 *
     258                 * @param {Array} errors - List of linting errors.
     259                 * @returns {void}
     260                 */
     261                codeEditorSettings.onChangeLintingErrors = function( errors ) {
     262                        component.lintErrors = errors;
     263
     264                        // Only disable the button in onUpdateErrorNotice when there are errors so users can still feel they can click the button.
     265                        if ( 0 === errors.length ) {
     266                                component.submitButton.toggleClass( 'disabled', false );
     267                        }
     268                };
    55269
    56270                /**
    57271                 * Update error notice.
    58272                 *
     273                 * @since 4.9.0
     274                 *
    59275                 * @param {Array} errorAnnotations - Error annotations.
    60276                 * @returns {void}
    61277                 */
    62278                codeEditorSettings.onUpdateErrorNotice = function onUpdateErrorNotice( errorAnnotations ) {
    63                         var message;
     279                        var message, noticeElement;
    64280
    65                         $( '#submit' ).prop( 'disabled', 0 !== errorAnnotations.length );
     281                        component.submitButton.toggleClass( 'disabled', errorAnnotations.length > 0 );
    66282
    67283                        if ( 0 !== errorAnnotations.length ) {
    68                                 errorNotice.empty();
    69284                                if ( 1 === errorAnnotations.length ) {
    70                                         message = component.l10n.singular.replace( '%d', '1' );
     285                                        message = component.l10n.lintError.singular.replace( '%d', '1' );
    71286                                } else {
    72                                         message = component.l10n.plural.replace( '%d', String( errorAnnotations.length ) );
     287                                        message = component.l10n.lintError.plural.replace( '%d', String( errorAnnotations.length ) );
    73288                                }
    74                                 errorNotice.append( $( '<p></p>', {
    75                                         text: message
    76                                 } ) );
    77                                 noticeContainer.slideDown( 'fast' );
    78                                 wp.a11y.speak( message );
     289                                noticeElement = component.addNotice({
     290                                        code: 'lint_errors',
     291                                        type: 'error',
     292                                        message: message,
     293                                        dismissible: false
     294                                });
     295                                noticeElement.find( 'input[type=checkbox]' ).on( 'click', function() {
     296                                        codeEditorSettings.onChangeLintingErrors( [] );
     297                                        component.removeNotice( 'lint_errors' );
     298                                } );
    79299                        } else {
    80                                 noticeContainer.slideUp( 'fast' );
     300                                component.removeNotice( 'lint_errors' );
    81301                        }
    82302                };
    83303
    84304                editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings );
     305                editor.codemirror.on( 'change', component.onChange );
    85306
    86307                // Improve the editor accessibility.
    87308                $( editor.codemirror.display.lineDiv )
  • src/wp-admin/plugin-editor.php

    diff --git src/wp-admin/plugin-editor.php src/wp-admin/plugin-editor.php
    index bc21642153..636e1cf739 100644
    if ( empty( $plugin ) ) { 
    6868
    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                 }
    9177
    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;
     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'] );
    10887                }
    109                 wp_redirect( self_admin_url( "plugin-editor.php?file=$file&plugin=$plugin&a=te&scrollto=$scrollto" ) );
    11088        } 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" ) );
     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) ) {
    180105                wp_die(sprintf('<p>%s</p>', __('No such file exists! Double check the name and try again.')));
    if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) { 
    212137                '<p>' . __('<a href="https://wordpress.org/support/">Support Forums</a>') . '</p>'
    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');
    222147
    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, '.' ) ) ) {
    228157                $functions = wp_doc_link_parse( $content );
    if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) { 
    239168
    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>
    260173
     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; ?>
     184
    261185<div class="fileedit-sub">
    262186<div class="alignleft">
    263187<h2>
    if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) { 
    283207</h2>
    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">
    289213<?php
    if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) { 
    308232<div id="templateside">
    309233        <h2><?php _e( 'Plugin Files' ); ?></h2>
    310234
    311         <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;
     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                }
    323241        }
    324242        ?>
    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; ?>
     243        <ul>
     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>
    333255                        <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>
    334256                        <input type="hidden" name="action" value="update" />
    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 ) ) : ?>
    340261                <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>
    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");
  • src/wp-admin/theme-editor.php

    diff --git src/wp-admin/theme-editor.php src/wp-admin/theme-editor.php
    index 2a593dee64..b49013ffd8 100644
    if ( $theme->errors() && 'theme_no_stylesheet' == $theme->errors()->get_error_co 
    6969
    7070$allowed_files = $style_files = array();
    7171$has_templates = false;
    72 $default_types = array(
    73         'bash',
    74         'conf',
    75         'css',
    76         'diff',
    77         'htm',
    78         'html',
    79         'http',
    80         'inc',
    81         'include',
    82         'js',
    83         'json',
    84         'jsx',
    85         'less',
    86         'md',
    87         'patch',
    88         'php',
    89         'php3',
    90         'php4',
    91         'php5',
    92         'php7',
    93         'phps',
    94         'phtml',
    95         'sass',
    96         'scss',
    97         'sh',
    98         'sql',
    99         'svg',
    100         'text',
    101         'txt',
    102         'xml',
    103         'yaml',
    104         'yml',
    105 );
    10672
    107 /**
    108  * Filters the list of file types allowed for editing in the Theme editor.
    109  *
    110  * @since 4.4.0
    111  *
    112  * @param array    $default_types List of file types. Default types include 'php' and 'css'.
    113  * @param WP_Theme $theme         The current Theme object.
    114  */
    115 $file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme );
    116 
    117 // Ensure that default types are still there.
    118 $file_types = array_unique( array_merge( $file_types, $default_types ) );
     73$file_types = wp_get_theme_file_editable_extensions( $theme );
    11974
    12075foreach ( $file_types as $type ) {
    12176        switch ( $type ) {
    if ( empty( $file ) ) { 
    14398}
    14499
    145100validate_file_to_edit( $file, $allowed_files );
    146 $scrollto = isset( $_REQUEST['scrollto'] ) ? (int) $_REQUEST['scrollto'] : 0;
    147 
    148 switch( $action ) {
    149 case 'update':
    150         check_admin_referer( 'edit-theme_' . $file . $stylesheet );
    151         $newcontent = wp_unslash( $_POST['newcontent'] );
    152         $location = 'theme-editor.php?file=' . urlencode( $relative_file ) . '&theme=' . urlencode( $stylesheet ) . '&scrollto=' . $scrollto;
    153         if ( is_writeable( $file ) ) {
    154                 // is_writable() not always reliable, check return value. see comments @ https://secure.php.net/is_writable
    155                 $f = fopen( $file, 'w+' );
    156                 if ( $f !== false ) {
    157                         fwrite( $f, $newcontent );
    158                         fclose( $f );
    159                         $location .= '&updated=true';
    160                         $theme->cache_delete();
     101
     102// Handle fallback editing of file when JavaScript is not available.
     103$edit_error = null;
     104$posted_content = null;
     105if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
     106        $r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) );
     107        if ( is_wp_error( $r ) ) {
     108                $edit_error = $r;
     109                if ( check_ajax_referer( 'edit-theme_' . $file . $stylesheet, 'nonce', false ) && isset( $_POST['newcontent'] ) ) {
     110                        $posted_content = wp_unslash( $_POST['newcontent'] );
    161111                }
     112        } else {
     113                wp_redirect( add_query_arg(
     114                        array(
     115                                'a' => 1, // This means "success" for some reason.
     116                                'theme' => $stylesheet,
     117                                'file' => $relative_file,
     118                        ),
     119                        admin_url( 'theme-editor.php' )
     120                ) );
     121                exit;
    162122        }
    163         wp_redirect( $location );
    164         exit;
    165 
    166 default:
     123}
    167124
    168         $settings = wp_enqueue_code_editor( compact( 'file' ) );
    169         if ( ! empty( $settings ) ) {
    170                 wp_enqueue_script( 'wp-theme-plugin-editor' );
    171                 wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function() { wp.themePluginEditor.init( %s ); } )', wp_json_encode( $settings ) ) );
    172         }
     125        $settings = array(
     126                'codeEditor' => wp_enqueue_code_editor( compact( 'file' ) ),
     127        );
     128        wp_enqueue_script( 'wp-theme-plugin-editor' );
     129        wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function( $ ) { wp.themePluginEditor.init( $( "#template" ), %s ); } )', wp_json_encode( $settings ) ) );
    173130
    174131        require_once( ABSPATH . 'wp-admin/admin-header.php' );
    175132
    default: 
    179136                $error = true;
    180137
    181138        $content = '';
    182         if ( ! $error && filesize( $file ) > 0 ) {
     139        if ( ! empty( $posted_content ) ) {
     140                $content = $posted_content;
     141        } elseif ( ! $error && filesize( $file ) > 0 ) {
    183142                $f = fopen($file, 'r');
    184143                $content = fread($f, filesize($file));
    185144
    default: 
    197156                $content = esc_textarea( $content );
    198157        }
    199158
    200         if ( isset( $_GET['updated'] ) ) : ?>
    201  <div id="message" class="updated notice is-dismissible"><p><?php _e( 'File edited successfully.' ) ?></p></div>
    202 <?php endif;
    203 
    204159$file_description = get_file_description( $relative_file );
    205160$file_show = array_search( $file, array_filter( $allowed_files ) );
    206161$description = esc_html( $file_description );
    if ( $file_description != $file_show ) { 
    211166<div class="wrap">
    212167<h1><?php echo esc_html( $title ); ?></h1>
    213168
     169<?php if ( isset( $_GET['a'] ) ) : ?>
     170        <div id="message" class="updated notice is-dismissible">
     171                <p><?php _e( 'File edited successfully.' ); ?></p>
     172        </div>
     173<?php elseif ( is_wp_error( $edit_error ) ) : ?>
     174        <div id="message" class="notice notice-error">
     175                <p><?php _e( 'There was an error while trying to update the file. You may need to fix something and try updating again.' ); ?></p>
     176                <pre><?php echo esc_html( $edit_error->get_error_message() ? $edit_error->get_error_message() : $edit_error->get_error_code() ); ?></pre>
     177        </div>
     178<?php endif; ?>
     179
    214180<div class="fileedit-sub">
    215181<div class="alignleft">
    216182<h2><?php echo $theme->display( 'Name' ); if ( $description ) echo ': ' . $description; ?></h2>
    217183</div>
    218184<div class="alignright">
    219         <form action="theme-editor.php" method="post">
     185        <form action="theme-editor.php" method="get">
    220186                <strong><label for="theme"><?php _e('Select theme to edit:'); ?> </label></strong>
    221187                <select name="theme" id="theme">
    222188<?php
    if ( $allowed_files ) : 
    299265        echo '<div class="error"><p>' . __('Oops, no such file exists! Double check the name and try again, merci.') . '</p></div>';
    300266else : ?>
    301267        <form name="template" id="template" action="theme-editor.php" method="post">
    302         <?php wp_nonce_field( 'edit-theme_' . $file . $stylesheet ); ?>
     268                <?php wp_nonce_field( 'edit-theme_' . $file . $stylesheet, 'nonce' ); ?>
    303269                <div>
    304270                        <label for="newcontent" id="theme-plugin-editor-label"><?php _e( 'Selected file content:' ); ?></label>
    305271                        <textarea cols="70" rows="30" 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>
    306272                        <input type="hidden" name="action" value="update" />
    307273                        <input type="hidden" name="file" value="<?php echo esc_attr( $relative_file ); ?>" />
    308274                        <input type="hidden" name="theme" value="<?php echo esc_attr( $theme->get_stylesheet() ); ?>" />
    309                         <input type="hidden" name="scrollto" id="scrollto" value="<?php echo esc_attr( $scrollto ); ?>" />
    310275                </div>
    311276        <?php if ( ! empty( $functions ) ) : ?>
    312277                <div id="documentation" class="hide-if-no-js">
    else : ?> 
    316281                </div>
    317282        <?php endif; ?>
    318283
    319                 <div>
    320                 <?php if ( is_child_theme() && $theme->get_stylesheet() == get_template() ) : ?>
    321                         <p><?php if ( is_writeable( $file ) ) { ?><strong><?php _e( 'Caution:' ); ?></strong><?php } ?>
    322                         <?php _e( 'This is a file in your current parent theme.' ); ?></p>
    323                 <?php endif; ?>
    324 <?php
    325         if ( is_writeable( $file ) ) :
    326                 submit_button( __( 'Update File' ), 'primary', 'submit', true );
    327         else : ?>
    328 <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>
    329 <?php endif; ?>
     284        <div>
     285                <div class="editor-notices">
     286                        <?php if ( is_child_theme() && $theme->get_stylesheet() == get_template() ) : ?>
     287                                <div class="notice notice-warning inline">
     288                                        <p>
     289                                                <?php if ( is_writeable( $file ) ) { ?><strong><?php _e( 'Caution:' ); ?></strong><?php } ?>
     290                                                <?php _e( 'This is a file in your current parent theme.' ); ?>
     291                                        </p>
     292                                </div>
     293                        <?php endif; ?>
    330294                </div>
     295        <?php if ( is_writeable( $file ) ) : ?>
     296                <p class="submit">
     297                        <?php submit_button( __( 'Update File' ), 'primary', 'submit', false ); ?>
     298                        <span class="spinner"></span>
     299                </p>
     300        <?php else : ?>
     301                <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>
     302        <?php endif; ?>
     303        </div>
     304        <?php wp_print_file_editor_templates(); ?>
    331305        </form>
    332306<?php
    333307endif; // $error
    334308?>
    335309<br class="clear" />
    336310</div>
    337 <script type="text/javascript">
    338 jQuery(document).ready(function($){
    339         $('#template').submit(function(){ $('#scrollto').val( $('#newcontent').scrollTop() ); });
    340         $('#newcontent').scrollTop( $('#scrollto').val() );
    341 });
    342 </script>
    343311<?php
    344 break;
    345 }
    346312
    347313include(ABSPATH . 'wp-admin/admin-footer.php' );
  • src/wp-includes/js/wp-a11y.js

    diff --git src/wp-includes/js/wp-a11y.js src/wp-includes/js/wp-a11y.js
    index 8639650a8f..18d6db579f 100644
    window.wp = window.wp || {}; 
    1414         * @since 4.2.0
    1515         * @since 4.3.0 Introduced the 'ariaLive' argument.
    1616         *
    17          * @param {String} message  The message to be announced by Assistive Technologies.
    18          * @param {String} ariaLive Optional. The politeness level for aria-live. Possible values:
    19          *                          polite or assertive. Default polite.
     17         * @param {String} message    The message to be announced by Assistive Technologies.
     18         * @param {String} [ariaLive] The politeness level for aria-live. Possible values:
     19         *                            polite or assertive. Default polite.
     20         * @returns {void}
    2021         */
    2122        function speak( message, ariaLive ) {
    2223                // Clear previous messages to allow repeated strings being read out.
  • src/wp-includes/load.php

    diff --git src/wp-includes/load.php src/wp-includes/load.php
    index 63a4b0f64c..0dcf31cbe5 100644
    function wp_is_file_mod_allowed( $context ) { 
    11121112         */
    11131113        return apply_filters( 'file_mod_allowed', ! defined( 'DISALLOW_FILE_MODS' ) || ! DISALLOW_FILE_MODS, $context );
    11141114}
     1115
     1116/**
     1117 * Start scraping edited file errors.
     1118 *
     1119 * @since 4.9.0
     1120 */
     1121function wp_start_scraping_edited_file_errors() {
     1122        if ( ! isset( $_REQUEST['wp_scrape_key'] ) || ! isset( $_REQUEST['wp_scrape_nonce'] ) ) {
     1123                return;
     1124        }
     1125        $key = substr( sanitize_key( wp_unslash( $_REQUEST['wp_scrape_key'] ) ), 0, 32 );
     1126        $nonce = wp_unslash( $_REQUEST['wp_scrape_nonce'] );
     1127
     1128        if ( get_transient( 'scrape_key_' . $key ) !== $nonce ) {
     1129                echo "###### begin_scraped_error:$key ######";
     1130                echo wp_json_encode( array(
     1131                        'code' => 'scrape_nonce_failure',
     1132                        'message' => __( 'Scrape nonce check failed. Please try again.' ),
     1133                ) );
     1134                die();
     1135        }
     1136        register_shutdown_function( 'wp_finalize_scraping_edited_file_errors', $key );
     1137}
     1138
     1139/**
     1140 * Finalize scraping for edited file errors.
     1141 *
     1142 * @since 4.9.0
     1143 *
     1144 * @param string $scrape_key Scrape key.
     1145 */
     1146function wp_finalize_scraping_edited_file_errors( $scrape_key ) {
     1147        $error = error_get_last();
     1148        if ( empty( $error ) ) {
     1149                return;
     1150        }
     1151        if ( ! in_array( $error['type'], array( E_CORE_ERROR, E_COMPILE_ERROR, E_ERROR, E_PARSE, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) {
     1152                return;
     1153        }
     1154        $error = str_replace( ABSPATH, '', $error );
     1155        echo "###### begin_scraped_error:$scrape_key ######";
     1156        echo wp_json_encode( $error );
     1157}
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index 250089dc25..7f0e75dfe5 100644
    function wp_default_scripts( &$scripts ) { 
    471471        $scripts->add( 'htmlhint', '/wp-includes/js/codemirror/htmlhint.js', array(), '0.9.14-xwp' );
    472472        $scripts->add( 'htmlhint-kses', '/wp-includes/js/codemirror/htmlhint-kses.js', array( 'htmlhint' ) );
    473473        $scripts->add( 'code-editor', "/wp-admin/js/code-editor$suffix.js", array( 'jquery', 'wp-codemirror' ) );
    474         $scripts->add( 'wp-theme-plugin-editor', "/wp-admin/js/theme-plugin-editor$suffix.js", array( 'code-editor', 'jquery', 'jquery-ui-core', 'wp-a11y', 'underscore' ) );
    475         did_action( 'init' ) && $scripts->add_inline_script( 'wp-theme-plugin-editor', sprintf( 'wp.themePluginEditor.l10n = %s;', wp_json_encode( wp_array_slice_assoc(
    476                 /* translators: %d: error count */
    477                 _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.' ),
    478                 array( 'singular', 'plural' )
     474        $scripts->add( 'wp-theme-plugin-editor', "/wp-admin/js/theme-plugin-editor$suffix.js", array( 'wp-util', 'jquery', 'jquery-ui-core', 'wp-a11y', 'underscore' ) );
     475        did_action( 'init' ) && $scripts->add_inline_script( 'wp-theme-plugin-editor', sprintf( 'wp.themePluginEditor.l10n = %s;', wp_json_encode( array(
     476                'saveAlert' => __( 'The changes you made will be lost if you navigate away from this page.' ),
     477                'lintError' => wp_array_slice_assoc(
     478                        /* translators: %d: error count */
     479                        _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.' ),
     480                        array( 'singular', 'plural' )
     481                ),
    479482        ) ) ) );
    480483
    481484        $scripts->add( 'wp-playlist', "/wp-includes/js/mediaelement/wp-playlist$suffix.js", array( 'wp-util', 'backbone', 'mediaelement' ), false, 1 );
  • src/wp-settings.php

    diff --git src/wp-settings.php src/wp-settings.php
    index 3d4c210338..bacf4cfddd 100644
    require( ABSPATH . WPINC . '/vars.php' ); 
    294294create_initial_taxonomies();
    295295create_initial_post_types();
    296296
     297wp_start_scraping_edited_file_errors();
     298
    297299// Register the default theme directory root
    298300register_theme_directory( get_theme_root() );
    299301