WordPress.org

Make WordPress Core

Changeset 41721


Ignore:
Timestamp:
10/04/17 00:19:16 (4 months 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.