mirror of
https://github.com/gosticks/wordpress-develop.git
synced 2026-05-25 21:54:28 +00:00
File Editors: Introduce sandboxed live editing of PHP files with rollbacks for both themes and plugins.
* Edits to active plugins which cause PHP fatal errors will no longer auto-deactivate the plugin. Supersedes #39766. * Introduce sandboxed PHP file edits for active themes, preventing accidental whitescreening of a user's site when introducing a fatal error. * After writing a change to a PHP file for an active theme or plugin, perform loopback requests on the file editor admin screens and the homepage to check for fatal errors. If a fatal error is encountered, roll back the edited file and display the error to the user to fix and try again. * Introduce a secure way to scrape PHP fatal errors from a site via `wp_start_scraping_edited_file_errors()` and `wp_finalize_scraping_edited_file_errors()`. * Moves file modifications from `theme-editor.php` and `plugin-editor.php` to common `wp_edit_theme_plugin_file()` function. * Refactor themes and plugin editors to submit file changes via Ajax instead of doing full page refreshes when JS is available. * Use `get` method for theme/plugin dropdowns. * Improve styling of plugin editors, including width of plugin/theme dropdowns. * Improve notices API for theme/plugin editor JS component. * Strip common base directory from plugin file list. See #24048. * Factor out functions to list editable file types in `wp_get_theme_file_editable_extensions()` and `wp_get_plugin_file_editable_extensions()`. * Scroll to line in editor that has linting error when attempting to save. See #41886. * Add checkbox to dismiss lint errors to proceed with saving. See #41887. * Only style the Update File button as disabled instead of actually disabling it for accessibility reasons. * Ensure that value from CodeMirror is used instead of `textarea` when CodeMirror is present. * Add "Are you sure?" check when leaving editor when there are unsaved changes. Supersedes [41560]. See #39766, #24048, #41886. Props westonruter, Clorith, melchoyce, johnbillion, jjj, jdgrimes, azaozz. Fixes #21622, #41887. git-svn-id: https://develop.svn.wordpress.org/trunk@41721 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
@@ -12,25 +12,227 @@ wp.themePluginEditor = (function( $ ) {
|
||||
lintError: {
|
||||
singular: '',
|
||||
plural: ''
|
||||
}
|
||||
},
|
||||
saveAlert: ''
|
||||
},
|
||||
instance: null
|
||||
codeEditor: {},
|
||||
instance: null,
|
||||
noticeElements: {},
|
||||
dirty: false,
|
||||
lintErrors: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize component.
|
||||
*
|
||||
* @param {object} settings Settings.
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param {jQuery} form - Form element.
|
||||
* @param {object} settings - Settings.
|
||||
* @param {object|boolean} settings.codeEditor - Code editor settings (or `false` if syntax highlighting is disabled).
|
||||
* @returns {void}
|
||||
*/
|
||||
component.init = function( settings ) {
|
||||
var codeEditorSettings, noticeContainer, errorNotice = [], editor;
|
||||
component.init = function init( form, settings ) {
|
||||
|
||||
codeEditorSettings = $.extend( {}, settings );
|
||||
component.form = form;
|
||||
if ( settings ) {
|
||||
$.extend( component, settings );
|
||||
}
|
||||
|
||||
component.noticeTemplate = wp.template( 'wp-file-editor-notice' );
|
||||
component.noticesContainer = component.form.find( '.editor-notices' );
|
||||
component.submitButton = component.form.find( ':input[name=submit]' );
|
||||
component.spinner = component.form.find( '.submit .spinner' );
|
||||
component.form.on( 'submit', component.submit );
|
||||
component.textarea = component.form.find( '#newcontent' );
|
||||
component.textarea.on( 'change', component.onChange );
|
||||
|
||||
if ( false !== component.codeEditor ) {
|
||||
/*
|
||||
* Defer adding notices until after DOM ready as workaround for WP Admin injecting
|
||||
* its own managed dismiss buttons and also to prevent the editor from showing a notice
|
||||
* when the file had linting errors to begin with.
|
||||
*/
|
||||
_.defer( function() {
|
||||
component.initCodeEditor();
|
||||
} );
|
||||
}
|
||||
|
||||
$( window ).on( 'beforeunload', function() {
|
||||
if ( component.dirty ) {
|
||||
return component.l10n.saveAlert;
|
||||
}
|
||||
return undefined;
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback for when a change happens.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @returns {void}
|
||||
*/
|
||||
component.onChange = function() {
|
||||
component.dirty = true;
|
||||
component.removeNotice( 'file_saved' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Submit file via Ajax.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @param {jQuery.Event} event - Event.
|
||||
* @returns {void}
|
||||
*/
|
||||
component.submit = function( event ) {
|
||||
var data = {}, request;
|
||||
event.preventDefault(); // Prevent form submission in favor of Ajax below.
|
||||
$.each( component.form.serializeArray(), function() {
|
||||
data[ this.name ] = this.value;
|
||||
} );
|
||||
|
||||
// Use value from codemirror if present.
|
||||
if ( component.instance ) {
|
||||
data.newcontent = component.instance.codemirror.getValue();
|
||||
}
|
||||
|
||||
if ( component.isSaving ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll ot the line that has the error.
|
||||
if ( component.lintErrors.length ) {
|
||||
component.instance.codemirror.setCursor( component.lintErrors[0].from.line );
|
||||
return;
|
||||
}
|
||||
|
||||
component.isSaving = true;
|
||||
component.textarea.prop( 'readonly', true );
|
||||
if ( component.instance ) {
|
||||
component.instance.codemirror.setOption( 'readOnly', true );
|
||||
}
|
||||
|
||||
component.spinner.addClass( 'is-active' );
|
||||
request = wp.ajax.post( 'edit-theme-plugin-file', data );
|
||||
|
||||
// Remove previous save notice before saving.
|
||||
if ( component.lastSaveNoticeCode ) {
|
||||
component.removeNotice( component.lastSaveNoticeCode );
|
||||
}
|
||||
|
||||
request.done( function ( response ) {
|
||||
component.lastSaveNoticeCode = 'file_saved';
|
||||
component.addNotice({
|
||||
code: component.lastSaveNoticeCode,
|
||||
type: 'success',
|
||||
message: response.message,
|
||||
dismissible: true
|
||||
});
|
||||
component.dirty = false;
|
||||
} );
|
||||
|
||||
request.fail( function ( response ) {
|
||||
var notice = $.extend(
|
||||
{
|
||||
code: 'save_error'
|
||||
},
|
||||
response,
|
||||
{
|
||||
type: 'error',
|
||||
dismissible: true
|
||||
}
|
||||
);
|
||||
component.lastSaveNoticeCode = notice.code;
|
||||
component.addNotice( notice );
|
||||
} );
|
||||
|
||||
request.always( function() {
|
||||
component.spinner.removeClass( 'is-active' );
|
||||
component.isSaving = false;
|
||||
|
||||
component.textarea.prop( 'readonly', false );
|
||||
if ( component.instance ) {
|
||||
component.instance.codemirror.setOption( 'readOnly', false );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Add notice.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param {object} notice - Notice.
|
||||
* @param {string} notice.code - Code.
|
||||
* @param {string} notice.type - Type.
|
||||
* @param {string} notice.message - Message.
|
||||
* @param {boolean} [notice.dismissible=false] - Dismissible.
|
||||
* @param {Function} [notice.onDismiss] - Callback for when a user dismisses the notice.
|
||||
* @returns {jQuery} Notice element.
|
||||
*/
|
||||
component.addNotice = function( notice ) {
|
||||
var noticeElement;
|
||||
|
||||
if ( ! notice.code ) {
|
||||
throw new Error( 'Missing code.' );
|
||||
}
|
||||
|
||||
// Only let one notice of a given type be displayed at a time.
|
||||
component.removeNotice( notice.code );
|
||||
|
||||
noticeElement = $( component.noticeTemplate( notice ) );
|
||||
noticeElement.hide();
|
||||
|
||||
noticeElement.find( '.notice-dismiss' ).on( 'click', function() {
|
||||
component.removeNotice( notice.code );
|
||||
if ( notice.onDismiss ) {
|
||||
notice.onDismiss( notice );
|
||||
}
|
||||
} );
|
||||
|
||||
wp.a11y.speak( notice.message );
|
||||
|
||||
component.noticesContainer.append( noticeElement );
|
||||
noticeElement.slideDown( 'fast' );
|
||||
component.noticeElements[ notice.code ] = noticeElement;
|
||||
return noticeElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove notice.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param {string} code - Notice code.
|
||||
* @returns {boolean} Whether a notice was removed.
|
||||
*/
|
||||
component.removeNotice = function( code ) {
|
||||
if ( component.noticeElements[ code ] ) {
|
||||
component.noticeElements[ code ].slideUp( 'fast', function() {
|
||||
$( this ).remove();
|
||||
} );
|
||||
delete component.noticeElements[ code ];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize code editor.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @returns {void}
|
||||
*/
|
||||
component.initCodeEditor = function initCodeEditor() {
|
||||
var codeEditorSettings, editor;
|
||||
|
||||
codeEditorSettings = $.extend( {}, component.codeEditor );
|
||||
|
||||
/**
|
||||
* Handle tabbing to the field before the editor.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
codeEditorSettings.onTabPrevious = function() {
|
||||
@@ -40,48 +242,67 @@ wp.themePluginEditor = (function( $ ) {
|
||||
/**
|
||||
* Handle tabbing to the field after the editor.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
codeEditorSettings.onTabNext = function() {
|
||||
$( '#template' ).find( ':tabbable:not(.CodeMirror-code)' ).first().focus();
|
||||
};
|
||||
|
||||
// Create the error notice container.
|
||||
noticeContainer = $( '<div id="file-editor-linting-error"></div>' );
|
||||
errorNotice = $( '<div class="inline notice notice-error"></div>' );
|
||||
noticeContainer.append( errorNotice );
|
||||
noticeContainer.hide();
|
||||
$( 'p.submit' ).before( noticeContainer );
|
||||
/**
|
||||
* Handle change to the linting errors.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param {Array} errors - List of linting errors.
|
||||
* @returns {void}
|
||||
*/
|
||||
codeEditorSettings.onChangeLintingErrors = function( errors ) {
|
||||
component.lintErrors = errors;
|
||||
|
||||
// Only disable the button in onUpdateErrorNotice when there are errors so users can still feel they can click the button.
|
||||
if ( 0 === errors.length ) {
|
||||
component.submitButton.toggleClass( 'disabled', false );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update error notice.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param {Array} errorAnnotations - Error annotations.
|
||||
* @returns {void}
|
||||
*/
|
||||
codeEditorSettings.onUpdateErrorNotice = function onUpdateErrorNotice( errorAnnotations ) {
|
||||
var message;
|
||||
var message, noticeElement;
|
||||
|
||||
$( '#submit' ).prop( 'disabled', 0 !== errorAnnotations.length );
|
||||
component.submitButton.toggleClass( 'disabled', errorAnnotations.length > 0 );
|
||||
|
||||
if ( 0 !== errorAnnotations.length ) {
|
||||
errorNotice.empty();
|
||||
if ( 1 === errorAnnotations.length ) {
|
||||
message = component.l10n.singular.replace( '%d', '1' );
|
||||
message = component.l10n.lintError.singular.replace( '%d', '1' );
|
||||
} else {
|
||||
message = component.l10n.plural.replace( '%d', String( errorAnnotations.length ) );
|
||||
message = component.l10n.lintError.plural.replace( '%d', String( errorAnnotations.length ) );
|
||||
}
|
||||
errorNotice.append( $( '<p></p>', {
|
||||
text: message
|
||||
} ) );
|
||||
noticeContainer.slideDown( 'fast' );
|
||||
wp.a11y.speak( message );
|
||||
noticeElement = component.addNotice({
|
||||
code: 'lint_errors',
|
||||
type: 'error',
|
||||
message: message,
|
||||
dismissible: false
|
||||
});
|
||||
noticeElement.find( 'input[type=checkbox]' ).on( 'click', function() {
|
||||
codeEditorSettings.onChangeLintingErrors( [] );
|
||||
component.removeNotice( 'lint_errors' );
|
||||
} );
|
||||
} else {
|
||||
noticeContainer.slideUp( 'fast' );
|
||||
component.removeNotice( 'lint_errors' );
|
||||
}
|
||||
};
|
||||
|
||||
editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings );
|
||||
editor.codemirror.on( 'change', component.onChange );
|
||||
|
||||
// Improve the editor accessibility.
|
||||
$( editor.codemirror.display.lineDiv )
|
||||
|
||||
Reference in New Issue
Block a user