Make WordPress Core

Changeset 41721


Ignore:
Timestamp:
10/04/2017 12:19:16 AM (7 years ago)
Author:
westonruter
Message:

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.

Location:
trunk/src
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/admin-ajax.php

    r41584 r41721  
    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
  • trunk/src/wp-admin/css/common.css

    r41711 r41721  
    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% );
    2224 }
    2225 #template .active-plugin-edit-warning p {
     2222    margin-right: 3%;
     2223}
     2224#template .notice p {
    22262225    width: auto;
     2226}
     2227#template .submit .spinner {
     2228    float: none;
    22272229}
    22282230
     
    30333035#template .CodeMirror {
    30343036    width: 97%;
    3035     height: calc( 100vh - 220px );
    3036 }
    3037 
    3038 #template label {
     3037    height: calc( 100vh - 280px );
     3038}
     3039#templateside {
     3040    margin-top: 31px;
     3041    overflow: scroll;
     3042}
     3043
     3044#theme-plugin-editor-label {
    30393045    display: inline-block;
    30403046    margin-bottom: 1em;
     
    30463052#docs-list {
    30473053    direction: ltr;
     3054}
     3055
     3056.fileedit-sub #theme,
     3057.fileedit-sub #plugin {
     3058    max-width: 40%;
     3059}
     3060.fileedit-sub .alignright {
     3061    text-align: right;
    30483062}
    30493063
     
    36253639
    36263640    #template > div,
    3627     #template  .active-plugin-edit-warning {
     3641    #template .notice {
    36283642        float: none;
    36293643        margin: 1em 0;
  • trunk/src/wp-admin/includes/ajax-actions.php

    r41611 r41721  
    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}
  • trunk/src/wp-admin/includes/file.php

    r41457 r41721  
    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.
     
    152152    return $files;
    153153}
     154
     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
    154546
    155547/**
  • trunk/src/wp-admin/js/theme-plugin-editor.js

    r41586 r41721  
    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
     
    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).
    2433     * @returns {void}
    2534     */
    26     component.init = function( settings ) {
    27         var codeEditorSettings, noticeContainer, errorNotice = [], editor;
    28 
    29         codeEditorSettings = $.extend( {}, settings );
     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
     224     * @returns {void}
     225     */
     226    component.initCodeEditor = function initCodeEditor() {
     227        var codeEditorSettings, editor;
     228
     229        codeEditorSettings = $.extend( {}, component.codeEditor );
    30230
    31231        /**
    32232         * Handle tabbing to the field before the editor.
     233         *
     234         * @since 4.9.0
    33235         *
    34236         * @returns {void}
     
    41243         * Handle tabbing to the field after the editor.
    42244         *
     245         * @since 4.9.0
     246         *
    43247         * @returns {void}
    44248         */
     
    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.
     272         *
     273         * @since 4.9.0
    58274         *
    59275         * @param {Array} errorAnnotations - Error annotations.
     
    61277         */
    62278        codeEditorSettings.onUpdateErrorNotice = function onUpdateErrorNotice( errorAnnotations ) {
    63             var message;
    64 
    65             $( '#submit' ).prop( 'disabled', 0 !== errorAnnotations.length );
     279            var message, noticeElement;
     280
     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.
  • trunk/src/wp-admin/plugin-editor.php

    r41595 r41721  
    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         }
    91 
    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;
    108         }
    109         wp_redirect( self_admin_url( "plugin-editor.php?file=$file&plugin=$plugin&a=te&scrollto=$scrollto" ) );
    110     } 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" ) );
     77
     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'] );
     87        }
     88    } else {
     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) ) {
     
    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');
     
    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, '.' ) ) ) {
     
    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>
     173
     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; ?>
    260184
    261185<div class="fileedit-sub">
     
    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">
     
    309233    <h2><?php _e( 'Plugin Files' ); ?></h2>
    310234
     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        }
     241    }
     242    ?>
    311243    <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;
    323     }
    324     ?>
    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; ?>
     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>
     
    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 ) ) : ?>
     
    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");
  • trunk/src/wp-admin/theme-editor.php

    r41640 r41721  
    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 );
    106 
    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 ) );
     72
     73$file_types = wp_get_theme_file_editable_extensions( $theme );
    11974
    12075foreach ( $file_types as $type ) {
     
    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();
    161         }
     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'] );
     111        }
     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:
    167 
    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     }
     123}
     124
     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' );
     
    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));
     
    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 ) );
     
    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">
     
    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">
     
    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>
     
    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 ) ) : ?>
     
    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
     
    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>
    343 <?php
    344 break;
    345 }
     311<?php
    346312
    347313include(ABSPATH . 'wp-admin/admin-footer.php' );
  • trunk/src/wp-includes/js/wp-a11y.js

    r41351 r41721  
    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 ) {
  • trunk/src/wp-includes/load.php

    r40992 r41721  
    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}
  • trunk/src/wp-includes/script-loader.php

    r41694 r41721  
    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
  • trunk/src/wp-settings.php

    r41289 r41721  
    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() );
Note: See TracChangeset for help on using the changeset viewer.