diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index 8847cda8b9..d57d56b1d4 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -50,7 +50,7 @@ $core_actions_post = array( 'oembed-cache', 'image-editor', 'delete-comment', 'delete-tag', 'delete-link', 'delete-meta', 'delete-post', 'trash-post', 'untrash-post', 'delete-page', 'dim-comment', 'add-link-category', 'add-tag', 'get-tagcloud', 'get-comments', 'replyto-comment', - 'edit-comment', 'add-menu-item', 'add-meta', 'add-user', 'autosave', 'closed-postboxes', + 'edit-comment', 'add-menu-item', 'add-meta', 'add-user', 'closed-postboxes', 'hidden-columns', 'update-welcome-panel', 'menu-get-metabox', 'wp-link-ajax', 'menu-locations-save', 'menu-quick-search', 'meta-box-order', 'get-permalink', 'sample-permalink', 'inline-save', 'inline-save-tax', 'find_posts', 'widgets-order', diff --git a/src/wp-admin/edit-form-advanced.php b/src/wp-admin/edit-form-advanced.php index 91bf275324..64a160cf6b 100644 --- a/src/wp-admin/edit-form-advanced.php +++ b/src/wp-admin/edit-form-advanced.php @@ -402,7 +402,6 @@ if ( 'draft' != get_post_status( $post ) ) echo $form_extra; -wp_nonce_field( 'autosave', 'autosavenonce', false ); wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?> diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 99fc973828..dca18a12a1 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -1089,68 +1089,6 @@ function wp_ajax_add_user( $action ) { $x->send(); } -function wp_ajax_autosave() { - define( 'DOING_AUTOSAVE', true ); - - check_ajax_referer( 'autosave', 'autosavenonce' ); - - if ( ! empty( $_POST['catslist'] ) ) - $_POST['post_category'] = explode( ',', $_POST['catslist'] ); - if ( $_POST['post_type'] == 'page' || empty( $_POST['post_category'] ) ) - unset( $_POST['post_category'] ); - - $data = ''; - $supplemental = array(); - $id = $revision_id = 0; - - $post_id = (int) $_POST['post_id']; - $_POST['ID'] = $_POST['post_ID'] = $post_id; - $post = get_post( $post_id ); - if ( empty( $post->ID ) || ! current_user_can( 'edit_post', $post->ID ) ) - wp_die( __( 'You are not allowed to edit this post.' ) ); - - if ( 'page' == $post->post_type && ! current_user_can( 'edit_page', $post->ID ) ) - wp_die( __( 'You are not allowed to edit this page.' ) ); - - if ( 'auto-draft' == $post->post_status ) - $_POST['post_status'] = 'draft'; - - if ( ! empty( $_POST['autosave'] ) ) { - if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() == $post->post_author && ( 'auto-draft' == $post->post_status || 'draft' == $post->post_status ) ) { - // Drafts and auto-drafts are just overwritten by autosave for the same user if the post is not locked - $id = edit_post(); - } else { - // Non drafts or other users drafts are not overwritten. The autosave is stored in a special post revision for each user. - $revision_id = wp_create_post_autosave( $post->ID ); - if ( is_wp_error($revision_id) ) - $id = $revision_id; - else - $id = $post->ID; - } - - if ( ! is_wp_error($id) ) { - /* translators: draft saved date format, see http://php.net/date */ - $draft_saved_date_format = __('g:i:s a'); - /* translators: %s: date and time */ - $data = sprintf( __('Draft saved at %s.'), date_i18n( $draft_saved_date_format ) ); - } - } else { - if ( ! empty( $_POST['auto_draft'] ) ) - $id = 0; // This tells us it didn't actually save - else - $id = $post->ID; - } - - // @todo Consider exposing any errors, rather than having 'Saving draft...' - $x = new WP_Ajax_Response( array( - 'what' => 'autosave', - 'id' => $id, - 'data' => $data, - 'supplemental' => $supplemental - ) ); - $x->send(); -} - function wp_ajax_closed_postboxes() { check_ajax_referer( 'closedpostboxes', 'closedpostboxesnonce' ); $closed = isset( $_POST['closed'] ) ? explode( ',', $_POST['closed']) : array(); diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index be8e2b218e..10d72d5745 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -734,7 +734,6 @@ function wp_refresh_post_nonces( $response, $data, $screen_id ) { if ( 2 === wp_verify_nonce( $received['post_nonce'], 'update-post_' . $post_id ) ) { $response['wp-refresh-post-nonces'] = array( 'replace' => array( - 'autosavenonce' => wp_create_nonce('autosave'), 'getpermalinknonce' => wp_create_nonce('getpermalink'), 'samplepermalinknonce' => wp_create_nonce('samplepermalink'), 'closedpostboxesnonce' => wp_create_nonce('closedpostboxes'), @@ -768,3 +767,29 @@ function wp_heartbeat_set_suspension( $settings ) { return $settings; } add_filter( 'heartbeat_settings', 'wp_heartbeat_set_suspension' ); + +/** + * Autosave with heartbeat + * + * @since 3.9 + */ +function heartbeat_autosave( $response, $data ) { + if ( ! empty( $data['wp_autosave'] ) ) { + $saved = wp_autosave( $data['wp_autosave'] ); + + if ( is_wp_error( $saved ) ) { + $response['wp_autosave'] = array( 'success' => false, 'message' => $saved->get_error_message() ); + } elseif ( empty( $saved ) ) { + $response['wp_autosave'] = array( 'success' => false, 'message' => __( 'Error while saving.' ) ); + } else { + /* translators: draft saved date format, see http://php.net/date */ + $draft_saved_date_format = __( 'g:i:s a' ); + /* translators: %s: date and time */ + $response['wp_autosave'] = array( 'success' => true, 'message' => sprintf( __( 'Draft saved at %s.' ), date_i18n( $draft_saved_date_format ) ) ); + } + } + + return $response; +} +// Run later as we have to set DOING_AUTOSAVE for back-compat +add_filter( 'heartbeat_received', 'heartbeat_autosave', 500, 2 ); diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index 983e4e7023..b39d87578e 100644 --- a/src/wp-admin/includes/post.php +++ b/src/wp-admin/includes/post.php @@ -79,9 +79,14 @@ function _wp_translate_postdata( $update = false, $post_data = null ) { } } - if ( ! empty( $post_data['post_status'] ) ) + if ( ! empty( $post_data['post_status'] ) ) { $post_data['post_status'] = sanitize_key( $post_data['post_status'] ); + // No longer an auto-draft + if ( 'auto-draft' == $post_data['post_status'] ) + $post_data['post_status'] = 'draft'; + } + // What to do based on which button they pressed if ( isset($post_data['saveasdraft']) && '' != $post_data['saveasdraft'] ) $post_data['post_status'] = 'draft'; @@ -190,9 +195,6 @@ function edit_post( $post_data = null ) { $post_data = _wp_translate_postdata( true, $post_data ); if ( is_wp_error($post_data) ) wp_die( $post_data->get_error_message() ); - if ( ( empty( $post_data['action'] ) || 'autosave' != $post_data['action'] ) && 'auto-draft' == $post_data['post_status'] ) { - $post_data['post_status'] = 'draft'; - } if ( isset($post_data['visibility']) ) { switch ( $post_data['visibility'] ) { @@ -1335,22 +1337,30 @@ function _admin_notice_post_locked() { * @uses _wp_translate_postdata() * @uses _wp_post_revision_fields() * - * @return unknown + * @param mixed $post_data Associative array containing the post data or int post ID. + * @return mixed The autosave revision ID. WP_Error or 0 on error. */ -function wp_create_post_autosave( $post_id ) { - $translated = _wp_translate_postdata( true ); - if ( is_wp_error( $translated ) ) - return $translated; +function wp_create_post_autosave( $post_data ) { + if ( is_numeric( $post_data ) ) { + $post_id = $post_data; + $post_data = &$_POST; + } else { + $post_id = (int) $post_data['post_ID']; + } + + $post_data = _wp_translate_postdata( true, $post_data ); + if ( is_wp_error( $post_data ) ) + return $post_data; $post_author = get_current_user_id(); // Store one autosave per author. If there is already an autosave, overwrite it. if ( $old_autosave = wp_get_post_autosave( $post_id, $post_author ) ) { - $new_autosave = _wp_post_revision_fields( $_POST, true ); + $new_autosave = _wp_post_revision_fields( $post_data, true ); $new_autosave['ID'] = $old_autosave->ID; $new_autosave['post_author'] = $post_author; - // If the new autosave is the same content as the post, delete the old autosave. + // If the new autosave has the same content as the post, delete the autosave. $post = get_post( $post_id ); $autosave_is_different = false; foreach ( array_keys( _wp_post_revision_fields() ) as $field ) { @@ -1362,14 +1372,14 @@ function wp_create_post_autosave( $post_id ) { if ( ! $autosave_is_different ) { wp_delete_post_revision( $old_autosave->ID ); - return; + return 0; } return wp_update_post( $new_autosave ); } // _wp_put_post_revision() expects unescaped. - $post_data = wp_unslash( $_POST ); + $post_data = wp_unslash( $post_data ); // Otherwise create the new autosave as a special post revision return _wp_put_post_revision( $post_data, true ); @@ -1395,58 +1405,82 @@ function wp_create_post_autosave( $post_id ) { function post_preview() { $post_ID = (int) $_POST['post_ID']; - $status = get_post_status( $post_ID ); - if ( 'auto-draft' == $status ) - wp_die( __('Preview not available. Please save as a draft first.') ); - - if ( isset($_POST['catslist']) ) - $_POST['post_category'] = explode(",", $_POST['catslist']); - - if ( isset($_POST['tags_input']) ) - $_POST['tags_input'] = explode(",", $_POST['tags_input']); - - if ( $_POST['post_type'] == 'page' || empty($_POST['post_category']) ) - unset($_POST['post_category']); - $_POST['ID'] = $post_ID; - $post = get_post($post_ID); - if ( 'page' == $post->post_type ) { - if ( ! current_user_can('edit_page', $post_ID) ) - wp_die( __('You are not allowed to edit this page.') ); + if ( ! $post = get_post( $post_ID ) ) + wp_die( __('You attempted to preview a non existing item.') ); + + if ( ! current_user_can( 'edit_post', $post->ID ) ) + wp_die( __('You are not allowed to preview this item.') ); + + $is_autosave = false; + + if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() == $post->post_author && ( 'draft' == $post->post_status || 'auto-draft' == $post->post_status ) ) { + $saved_post_id = edit_post(); } else { - if ( ! current_user_can('edit_post', $post_ID) ) - wp_die( __('You are not allowed to edit this post.') ); + $is_autosave = true; + + if ( 'auto-draft' == $_POST['post_status'] ) + $_POST['post_status'] = 'draft'; + + $saved_post_id = wp_create_post_autosave( $post->ID ); } - $user_id = get_current_user_id(); - $locked = wp_check_post_lock( $post->ID ); - if ( ! $locked && 'draft' == $post->post_status && $user_id == $post->post_author ) { - $id = edit_post(); - } else { // Non drafts are not overwritten. The autosave is stored in a special post revision. - $id = wp_create_post_autosave( $post->ID ); - if ( ! is_wp_error($id) ) - $id = $post->ID; - } + if ( is_wp_error( $saved_post_id ) ) + wp_die( $saved_post_id->get_error_message() ); - if ( is_wp_error($id) ) - wp_die( $id->get_error_message() ); + $query_args = array( 'preview' => 'true' ); - if ( ! $locked && $_POST['post_status'] == 'draft' && $user_id == $post->post_author ) { - $url = add_query_arg( 'preview', 'true', get_permalink($id) ); - } else { - $nonce = wp_create_nonce('post_preview_' . $id); - $args = array( - 'preview' => 'true', - 'preview_id' => $id, - 'preview_nonce' => $nonce, - ); + if ( $is_autosave && $saved_post_id ) { + $query_args['preview_id'] = $post->ID; + $query_args['preview_nonce'] = wp_create_nonce( 'post_preview_' . $post->ID ); if ( isset( $_POST['post_format'] ) ) - $args['post_format'] = empty( $_POST['post_format'] ) ? 'standard' : sanitize_key( $_POST['post_format'] ); - - $url = add_query_arg( $args, get_permalink($id) ); + $query_args['post_format'] = empty( $_POST['post_format'] ) ? 'standard' : sanitize_key( $_POST['post_format'] ); } + $url = add_query_arg( $query_args, get_permalink( $post->ID ) ); return apply_filters( 'preview_post_link', $url ); } + +/** + * Save a post submitted with XHR + * + * Intended for use with heartbeat and autosave.js + * + * @since 3.9 + * + * @param $post_data Associative array of the submitted post data. + * @return mixed The value 0 or WP_Error on failure. The saved post ID on success. + * Te ID can be the draft post_id or the autosave revision post_id. + */ +function wp_autosave( $post_data ) { + // Back-compat + if ( ! defined( 'DOING_AUTOSAVE' ) ) + define( 'DOING_AUTOSAVE', true ); + + $post_id = (int) $post_data['post_id']; + $post_data['ID'] = $post_data['post_ID'] = $post_id; + + if ( false === wp_verify_nonce( $post_data['_wpnonce'], 'update-post_' . $post_id ) ) + return new WP_Error( 'invalid_nonce', __('ERROR: invalid post data.') ); + + $post = get_post( $post_id ); + + if ( ! current_user_can( 'edit_post', $post->ID ) ) + return new WP_Error( 'edit_post', __('You are not allowed to edit this item.') ); + + if ( 'auto-draft' == $post->post_status ) + $post_data['post_status'] = 'draft'; + + if ( $post_data['post_type'] != 'page' && ! empty( $post_data['catslist'] ) ) + $post_data['post_category'] = explode( ',', $post_data['catslist'] ); + + if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() == $post->post_author && ( 'auto-draft' == $post->post_status || 'draft' == $post->post_status ) ) { + // Drafts and auto-drafts are just overwritten by autosave for the same user if the post is not locked + return edit_post( $post_data ); + } else { + // Non drafts or other users drafts are not overwritten. The autosave is stored in a special post revision for each user. + return wp_create_post_autosave( $post_data ); + } +} diff --git a/src/wp-admin/js/post.js b/src/wp-admin/js/post.js index 5e25a7a252..20a796fd87 100644 --- a/src/wp-admin/js/post.js +++ b/src/wp-admin/js/post.js @@ -1,7 +1,9 @@ /* global postL10n, ajaxurl, wpAjax, setPostThumbnailL10n, postboxes, pagenow, tinymce, alert, deleteUserSetting, getUserSetting, setUserSetting */ /* global theList:true, theExtraList:true, autosave:true */ -var tagBox, commentsBox, editPermalink, makeSlugeditClickable, WPSetThumbnailHTML, WPSetThumbnailID, WPRemoveThumbnail, wptitlehint; +var tagBox, commentsBox, WPSetThumbnailHTML, WPSetThumbnailID, WPRemoveThumbnail, wptitlehint; +// Back-compat: prevent fatal errors +makeSlugeditClickable = editPermalink = function(){}; // return an array with any duplicate, whitespace or values removed function array_unique_noempty(a) { @@ -268,10 +270,9 @@ $(document).on( 'heartbeat-send.refresh-lock', function( e, data ) { send.lock = lock; data['wp-refresh-post-lock'] = send; -}); -// Post locks: update the lock string or show the dialog if somebody has taken over editing -$(document).on( 'heartbeat-tick.refresh-lock', function( e, data ) { +}).on( 'heartbeat-tick.refresh-lock', function( e, data ) { + // Post locks: update the lock string or show the dialog if somebody has taken over editing var received, wrap, avatar; if ( data['wp-refresh-post-lock'] ) { @@ -282,19 +283,16 @@ $(document).on( 'heartbeat-tick.refresh-lock', function( e, data ) { wrap = $('#post-lock-dialog'); if ( wrap.length && ! wrap.is(':visible') ) { - if ( typeof autosave == 'function' ) { - $(document).on('autosave-disable-buttons.post-lock', function() { - wrap.addClass('saving'); - }).on('autosave-enable-buttons.post-lock', function() { + if ( typeof wp != 'undefined' && wp.autosave ) { + // Save the latest changes and disable + $(document).one( 'heartbeat-tick', function() { + wp.autosave.server.disable(); wrap.removeClass('saving').addClass('saved'); - window.onbeforeunload = null; + $(window).off( 'beforeunload.edit-post' ); }); - // Save the latest changes and disable - if ( ! autosave() ) - window.onbeforeunload = null; - - autosave = function(){}; + wrap.addClass('saving'); + wp.autosave.server.triggerSave(); } if ( received.lock_error.avatar_src ) { @@ -309,6 +307,22 @@ $(document).on( 'heartbeat-tick.refresh-lock', function( e, data ) { $('#active_post_lock').val( received.new_lock ); } } +}).on( 'after-autosave.update-post-slug', function() { + // create slug area only if not already there + if ( ! $('#edit-slug-box > *').length ) { + $.post( ajaxurl, { + action: 'sample-permalink', + post_id: $('#post_ID').val(), + new_title: typeof fullscreen != 'undefined' && fullscreen.settings.visible ? $('#wp-fullscreen-title').val() : $('#title').val(), + samplepermalinknonce: $('#samplepermalinknonce').val() + }, + function( data ) { + if ( data != '-1' ) { + $('#edit-slug-box').html(data); + } + } + ); + } }); }(jQuery)); @@ -354,8 +368,14 @@ $(document).on( 'heartbeat-tick.refresh-lock', function( e, data ) { }(jQuery)); jQuery(document).ready( function($) { - var stamp, visibility, updateVisibility, updateText, - sticky = '', last = 0, co = $('#content'); + var stamp, visibility, $submitButtons, + sticky = '', + last = 0, + co = $('#content'), + $editSlugWrap = $('#edit-slug-box'), + postId = $('#post_ID').val() || 0, + $submitpost = $('#submitpost'), + releaseLock = true; postboxes.add_postbox_toggles(pagenow); @@ -380,6 +400,142 @@ jQuery(document).ready( function($) { wp.heartbeat.interval( 15 ); } + // The form is being submitted by the user + $submitButtons = $submitpost.find( ':button, :submit, a.submitdelete, #post-preview' ).on( 'click.autosave', function( event ) { + var $button = $(this); + + if ( $button.prop('disabled') ) { + event.preventDefault(); + return; + } + + if ( $button.hasClass('submitdelete') ) { + return; + } + + // The form submission can be blocked from JS or by using HTML 5.0 validation on some fields. + // Run this only on an actual 'submit'. + $('form#post').off( 'submit.edit-post' ).on( 'submit.edit-post', function( event ) { + if ( event.isDefaultPrevented() ) { + return; + } + + wp.autosave.server.disable(); + releaseLock = false; + $(window).off( 'beforeunload.edit-post' ); + + $submitButtons.prop( 'disabled', true ).addClass( 'button-disabled' ); + + if ( $button.attr('id') === 'publish' ) { + $submitpost.find('#major-publishing-actions .spinner').show(); + } else { + $submitpost.find('#minor-publishing .spinner').show(); + } + }); + }); + + // Submit the form saving a draft or an autosave, and show a preview in a new tab + $('#post-preview').on( 'click.post-preview', function( event ) { + var $this = $(this), + $form = $('form#post'), + $previewField = $('input#wp-preview'), + target = $this.attr('target') || 'wp-preview', + ua = navigator.userAgent.toLowerCase(); + + event.preventDefault(); + + if ( $this.prop('disabled') ) { + return; + } + + if ( typeof wp != 'undefined' && wp.autosave ) { + wp.autosave.server.tempBlockSave(); + } + + $previewField.val('dopreview'); + $form.attr( 'target', target ).submit().attr( 'target', '' ); + + // Workaround for WebKit bug preventing a form submitting twice to the same action. + // https://bugs.webkit.org/show_bug.cgi?id=28633 + if ( ua.indexOf('safari') !== -1 && ua.indexOf('chrome') === -1 ) { + $form.attr( 'action', function( index, value ) { + return value + '?t=' + ( new Date() ).getTime(); + }); + } + + $previewField.val(''); + }); + + // This code is meant to allow tabbing from Title to Post content. + $('#title').on( 'keydown.editor-focus', function( event ) { + var editor; + + if ( event.keyCode === 9 && ! event.ctrlKey && ! event.altKey && ! event.shiftKey ) { + editor = typeof tinymce != 'undefined' && tinymce.get('content'); + + if ( editor && ! editor.isHidden() ) { + editor.focus(); + } else { + $('#content').focus(); + } + + event.preventDefault(); + } + }); + + // Autosave new posts after a title is typed + if ( $( '#auto_draft' ).val() ) { + $( '#title' ).blur( function() { + if ( ! this.value || $( '#auto_draft' ).val() !== '1' ) { + return; + } + + if ( typeof wp != 'undefined' && wp.autosave ) { + wp.autosave.server.triggerSave(); + } + }); + } + + $(document).on( 'autosave-disable-buttons.edit-post', function() { + $submitButtons.prop( 'disabled', true ).addClass( 'button-disabled' ); + }).on( 'autosave-enable-buttons.edit-post', function() { + if ( ! window.wp || ! window.wp.heartbeat || ! window.wp.heartbeat.hasConnectionError() ) { + $submitButtons.prop( 'disabled', false ).removeClass( 'button-disabled' ); + } + }); + + $(window).on( 'beforeunload.edit-post', function() { + var editor = typeof tinymce !== 'undefined' && tinymce.get('content'); + + if ( ( editor && ! editor.isHidden() && editor.isDirty() ) || + ( typeof wp !== 'undefined' && wp.autosave && wp.autosave.server.postChanged() ) ) { + + return autosaveL10n.saveAlert; + } + }).on( 'unload.edit-post', function( event ) { + if ( ! releaseLock ) { + return; + } + + // Unload is triggered (by hand) on removing the Thickbox iframe. + // Make sure we process only the main document unload. + if ( event.target && event.target.nodeName != '#document' ) { + return; + } + + $.ajax({ + type: 'POST', + url: ajaxurl, + async: false, + data: { + action: 'wp-remove-post-lock', + _wpnonce: $('#_wpnonce').val(), + post_ID: $('#post_ID').val(), + active_post_lock: $('#active_post_lock').val() + } + }); + }); + // multi-taxonomies if ( $('#tagsdiv-post_tag').length ) { tagBox.init(); @@ -689,28 +845,22 @@ jQuery(document).ready( function($) { } // end submitdiv // permalink - if ( $('#edit-slug-box').length ) { - editPermalink = function(post_id) { - var slug_value, i, - c = 0, - e = $( '#editable-post-name' ), - revert_e = e.html(), - real_slug = $( '#post_name' ), - revert_slug = real_slug.val(), - b = $( '#edit-slug-buttons' ), - revert_b = b.html(), - full = $( '#editable-post-name-full' ).html(); + if ( $editSlugWrap.length ) { + function editPermalink() { + var i, c = 0, e = $('#editable-post-name'), revert_e = e.html(), real_slug = $('#post_name'), + revert_slug = real_slug.val(), b = $('#edit-slug-buttons'), revert_b = b.html(), + full = $('#editable-post-name-full').html(); $('#view-post-btn').hide(); b.html(''+postL10n.ok+' '+postL10n.cancel+''); b.children('.save').click(function() { var new_slug = e.children('input').val(); if ( new_slug == $('#editable-post-name-full').text() ) { - return $('.cancel', '#edit-slug-buttons').click(); + return $('#edit-slug-buttons .cancel').click(); } $.post(ajaxurl, { action: 'sample-permalink', - post_id: post_id, + post_id: postId, new_slug: new_slug, new_title: $('#title').val(), samplepermalinknonce: $('#samplepermalinknonce').val() @@ -724,13 +874,12 @@ jQuery(document).ready( function($) { } b.html(revert_b); real_slug.val(new_slug); - makeSlugeditClickable(); $('#view-post-btn').show(); }); return false; }); - $('.cancel', '#edit-slug-buttons').click(function() { + $('#edit-slug-buttons .cancel').click(function() { $('#view-post-btn').show(); e.html(revert_e); b.html(revert_b); @@ -760,12 +909,13 @@ jQuery(document).ready( function($) { }).focus(); }; - makeSlugeditClickable = function() { - $('#editable-post-name').click(function() { - $('#edit-slug-buttons').children('.edit-slug').click(); - }); - }; - makeSlugeditClickable(); + $editSlugWrap.on( 'click', function( event ) { + var $target = $( event.target ); + + if ( $target.is('#editable-post-name') || $target.hasClass('edit-slug') ) { + editPermalink(); + } + }); } // word count diff --git a/src/wp-admin/post.php b/src/wp-admin/post.php index a4f5cc7408..3df1c5fc6e 100644 --- a/src/wp-admin/post.php +++ b/src/wp-admin/post.php @@ -307,7 +307,7 @@ case 'delete': break; case 'preview': - check_admin_referer( 'autosave', 'autosavenonce' ); + check_admin_referer( 'update-post_' . $post_id ); $url = post_preview(); diff --git a/src/wp-includes/js/autosave.js b/src/wp-includes/js/autosave.js index 4c780a4a8a..ab6952b08f 100644 --- a/src/wp-includes/js/autosave.js +++ b/src/wp-includes/js/autosave.js @@ -1,688 +1,597 @@ -/* global switchEditors, autosaveL10n, tinymce, ajaxurl, wpAjax, makeSlugeditClickable, wpCookies */ -var autosave, autosavePeriodical, fullscreen, doPreview, - autosaveLast = '', - autosaveDelayPreview = false, - notSaved = true, - blockSave = false, - autosaveLockRelease = true; +/* global tinymce, wpCookies, autosaveL10n, switchEditors */ +// Back-compat: prevent fatal errors +window.autosave = function(){}; -jQuery(document).ready( function($) { +( function( $, window ) { + function autosave() { + var initialCompareString, + lastTriggerSave = 0, + isSuspended = false, + $document = $(document); - if ( $('#wp-content-wrap').hasClass('tmce-active') && typeof switchEditors != 'undefined' ) { - autosaveLast = wp.autosave.getCompareString({ - post_title : $('#title').val() || '', - content : switchEditors.pre_wpautop( $('#content').val() ) || '', - excerpt : $('#excerpt').val() || '' - }); - } else { - autosaveLast = wp.autosave.getCompareString(); - } - - autosavePeriodical = $.schedule({time: autosaveL10n.autosaveInterval * 1000, func: function() { autosave(); }, repeat: true, protect: true}); - - //Disable autosave after the form has been submitted - $('#post').submit(function() { - $.cancel(autosavePeriodical); - autosaveLockRelease = false; - }); - - $('input[type="submit"], a.submitdelete', '#submitpost').click(function(){ - blockSave = true; - window.onbeforeunload = null; - $(':button, :submit', '#submitpost').each(function(){ - var t = $(this); - if ( t.hasClass('button-primary') ) - t.addClass('button-primary-disabled'); - else - t.addClass('button-disabled'); - }); - if ( $(this).attr('id') == 'publish' ) - $('#major-publishing-actions .spinner').show(); - else - $('#minor-publishing .spinner').show(); - }); - - window.onbeforeunload = function(){ - var editor = typeof(tinymce) != 'undefined' ? tinymce.activeEditor : false; - - if ( editor && ! editor.isHidden() ) { - if ( editor.isDirty() ) - return autosaveL10n.saveAlert; - } else { - if ( wp.autosave.getCompareString() != autosaveLast ) - return autosaveL10n.saveAlert; - } - }; - - $(window).unload( function(e) { - if ( ! autosaveLockRelease ) - return; - - // unload fires (twice) on removing the Thickbox iframe. Make sure we process only the main document unload. - if ( e.target && e.target.nodeName != '#document' ) - return; - - $.ajax({ - type: 'POST', - url: ajaxurl, - async: false, - data: { - action: 'wp-remove-post-lock', - _wpnonce: $('#_wpnonce').val(), - post_ID: $('#post_ID').val(), - active_post_lock: $('#active_post_lock').val() - } - }); - } ); - - // preview - $('#post-preview').click(function(){ - if ( $('#auto_draft').val() == '1' && notSaved ) { - autosaveDelayPreview = true; - autosave(); - return false; - } - doPreview(); - return false; - }); - - doPreview = function() { - $('input#wp-preview').val('dopreview'); - $('form#post').attr('target', 'wp-preview').submit().attr('target', ''); - - /* - * Workaround for WebKit bug preventing a form submitting twice to the same action. - * https://bugs.webkit.org/show_bug.cgi?id=28633 + /** + * Returns the data saved in both local and remote autosave + * + * @return object Object containing the post data */ - var ua = navigator.userAgent.toLowerCase(); - if ( ua.indexOf('safari') != -1 && ua.indexOf('chrome') == -1 ) { - $('form#post').attr('action', function(index, value) { - return value + '?t=' + new Date().getTime(); + function getPostData( type ) { + var post_name, parent_id, data, + time = ( new Date() ).getTime(), + cats = [], + editor = typeof tinymce !== 'undefined' && tinymce.get('content'); + + // Don't run editor.save() more often than every 3 sec. + // It is resource intensive and might slow down typing in long posts on slow devices. + if ( editor && ! editor.isHidden() && time - 3000 > lastTriggerSave ) { + editor.save(); + lastTriggerSave = time; + } + + data = { + post_id: $( '#post_ID' ).val() || 0, + post_type: $( '#post_type' ).val() || '', + post_author: $( '#post_author' ).val() || '', + post_title: $( '#title' ).val() || '', + content: $( '#content' ).val() || '', + excerpt: $( '#excerpt' ).val() || '' + }; + + if ( type === 'local' ) { + return data; + } + + $( 'input[id^="in-category-"]:checked' ).each( function() { + cats.push( this.value ); }); - } + data.catslist = cats.join(','); - $('input#wp-preview').val(''); - }; - - // This code is meant to allow tabbing from Title to Post content. - $('#title').on( 'keydown.editor-focus', function( event ) { - var editor; - - if ( event.which === 9 && ! event.ctrlKey && ! event.altKey && ! event.shiftKey ) { - if ( typeof tinymce !== 'undefined' ) { - editor = tinymce.get('content'); + if ( post_name = $( '#post_name' ).val() ) { + data.post_name = post_name; } - if ( editor && ! editor.isHidden() ) { - $(this).one( 'keyup', function() { - editor.focus(); - }); - } else { - $('#content').focus(); + if ( parent_id = $( '#parent_id' ).val() ) { + data.parent_id = parent_id; } - event.preventDefault(); - } - }); - - // autosave new posts after a title is typed but not if Publish or Save Draft is clicked - if ( '1' == $('#auto_draft').val() ) { - $('#title').blur( function() { - if ( !this.value || $('#auto_draft').val() != '1' ) - return; - delayed_autosave(); - }); - } - - // When connection is lost, keep user from submitting changes. - $(document).on('heartbeat-connection-lost.autosave', function( e, error, status ) { - if ( 'timeout' === error || 503 == status ) { - var notice = $('#lost-connection-notice'); - if ( ! wp.autosave.local.hasStorage ) { - notice.find('.hide-if-no-sessionstorage').hide(); + if ( $( '#comment_status' ).prop( 'checked' ) ) { + data.comment_status = 'open'; } - notice.show(); - autosave_disable_buttons(); - } - }).on('heartbeat-connection-restored.autosave', function() { - $('#lost-connection-notice').hide(); - autosave_enable_buttons(); - }); -}); -function autosave_parse_response( response ) { - var res = wpAjax.parseAjaxResponse(response, 'autosave'), post_id, sup; + if ( $( '#ping_status' ).prop( 'checked' ) ) { + data.ping_status = 'open'; + } - if ( res && res.responses && res.responses.length ) { - if ( res.responses[0].supplemental ) { - sup = res.responses[0].supplemental; + if ( $( '#auto_draft' ).val() === '1' ) { + data.auto_draft = '1'; + } - jQuery.each( sup, function( selector, value ) { - if ( selector.match(/^replace-/) ) - jQuery( '#' + selector.replace('replace-', '') ).val( value ); - }); + return data; } - // if no errors: add slug UI and update autosave-message - if ( !res.errors ) { - if ( post_id = parseInt( res.responses[0].id, 10 ) ) - autosave_update_slug( post_id ); + // Concatenate title, content and excerpt. Used to track changes when auto-saving. + function getCompareString( postData ) { + if ( typeof postData === 'object' ) { + return ( postData.post_title || '' ) + '::' + ( postData.content || '' ) + '::' + ( postData.excerpt || '' ); + } - if ( res.responses[0].data ) // update autosave message - jQuery('.autosave-message').text( res.responses[0].data ); - } - } - - return res; -} - -// called when autosaving pre-existing post -function autosave_saved(response) { - blockSave = false; - autosave_parse_response(response); // parse the ajax response - autosave_enable_buttons(); // re-enable disabled form buttons -} - -// called when autosaving new post -function autosave_saved_new(response) { - blockSave = false; - var res = autosave_parse_response(response), post_id; - - if ( res && res.responses.length && !res.errors ) { - // An ID is sent only for real auto-saves, not for autosave=0 "keepalive" saves - post_id = parseInt( res.responses[0].id, 10 ); - - if ( post_id ) { - notSaved = false; - jQuery('#auto_draft').val('0'); // No longer an auto-draft + return ( $('#title').val() || '' ) + '::' + ( $('#content').val() || '' ) + '::' + ( $('#excerpt').val() || '' ); } - autosave_enable_buttons(); - - if ( autosaveDelayPreview ) { - autosaveDelayPreview = false; - doPreview(); + function disableButtons() { + $document.trigger('autosave-disable-buttons'); + // Re-enable 5 sec later. Just gives autosave a head start to avoid collisions. + setTimeout( enableButtons, 5000 ); } - } else { - autosave_enable_buttons(); // re-enable disabled form buttons - } -} -function autosave_update_slug(post_id) { - // create slug area only if not already there - if ( 'undefined' != makeSlugeditClickable && jQuery.isFunction(makeSlugeditClickable) && !jQuery('#edit-slug-box > *').size() ) { - jQuery.post( ajaxurl, { - action: 'sample-permalink', - post_id: post_id, - new_title: fullscreen && fullscreen.settings.visible ? jQuery('#wp-fullscreen-title').val() : jQuery('#title').val(), - samplepermalinknonce: jQuery('#samplepermalinknonce').val() - }, - function(data) { - if ( data !== '-1' ) { - var box = jQuery('#edit-slug-box'); - box.html(data); - if (box.hasClass('hidden')) { - box.fadeIn('fast', function () { - box.removeClass('hidden'); + function enableButtons() { + $document.trigger( 'autosave-enable-buttons' ); + } + + function suspend() { + isSuspended = true; + } + + function resume() { + isSuspended = false; + } + + // Autosave in localStorage + function autosaveLocal() { + var restorePostData, undoPostData, blog_id, post_id, hasStorage, intervalTimer, + lastCompareString; + + // Check if the browser supports sessionStorage and it's not disabled + function checkStorage() { + var test = Math.random().toString(), + result = false; + + try { + window.sessionStorage.setItem( 'wp-test', test ); + result = window.sessionStorage.getItem( 'wp-test' ) === test; + window.sessionStorage.removeItem( 'wp-test' ); + } catch(e) {} + + hasStorage = result; + return result; + } + + /** + * Initialize the local storage + * + * @return mixed False if no sessionStorage in the browser or an Object containing all postData for this blog + */ + function getStorage() { + var stored_obj = false; + // Separate local storage containers for each blog_id + if ( hasStorage && blog_id ) { + stored_obj = sessionStorage.getItem( 'wp-autosave-' + blog_id ); + + if ( stored_obj ) { + stored_obj = JSON.parse( stored_obj ); + } else { + stored_obj = {}; + } + } + + return stored_obj; + } + + /** + * Set the storage for this blog + * + * Confirms that the data was saved successfully. + * + * @return bool + */ + function setStorage( stored_obj ) { + var key; + + if ( hasStorage && blog_id ) { + key = 'wp-autosave-' + blog_id; + sessionStorage.setItem( key, JSON.stringify( stored_obj ) ); + return sessionStorage.getItem( key ) !== null; + } + + return false; + } + + /** + * Get the saved post data for the current post + * + * @return mixed False if no storage or no data or the postData as an Object + */ + function getSavedPostData() { + var stored = getStorage(); + + if ( ! stored || ! post_id ) { + return false; + } + + return stored[ 'post_' + post_id ] || false; + } + + /** + * Set (save or delete) post data in the storage. + * + * If stored_data evaluates to 'false' the storage key for the current post will be removed + * + * $param stored_data The post data to store or null/false/empty to delete the key + * @return bool + */ + function setData( stored_data ) { + var stored = getStorage(); + + if ( ! stored || ! post_id ) { + return false; + } + + if ( stored_data ) { + stored[ 'post_' + post_id ] = stored_data; + } else if ( stored.hasOwnProperty( 'post_' + post_id ) ) { + delete stored[ 'post_' + post_id ]; + } else { + return false; + } + + return setStorage( stored ); + } + + /** + * Save post data for the current post + * + * Runs on a 15 sec. interval, saves when there are differences in the post title or content. + * When the optional data is provided, updates the last saved post data. + * + * $param data optional Object The post data for saving, minimum 'post_title' and 'content' + * @return bool + */ + function save( data ) { + var postData, compareString, + result = false; + + if ( isSuspended ) { + return false; + } + + if ( data ) { + postData = getSavedPostData() || {}; + $.extend( postData, data ); + } else { + postData = getPostData('local'); + } + + compareString = getCompareString( postData ); + + if ( typeof lastCompareString === 'undefined' ) { + lastCompareString = initialCompareString; + } + + // If the content, title and excerpt did not change since the last save, don't save again + if ( compareString === lastCompareString ) { + return false; + } + + postData.save_time = ( new Date() ).getTime(); + postData.status = $( '#post_status' ).val() || ''; + result = setData( postData ); + + if ( result ) { + lastCompareString = compareString; + } + + return result; + } + + // Run on DOM ready + function run() { + post_id = $('#post_ID').val() || 0; + + // Check if the local post data is different than the loaded post data. + if ( $( '#wp-content-wrap' ).hasClass( 'tmce-active' ) ) { + // If TinyMCE loads first, check the post 1.5 sec. after it is ready. + // By this time the content has been loaded in the editor and 'saved' to the textarea. + // This prevents false positives. + $document.on( 'tinymce-editor-init.autosave', function() { + window.setTimeout( function() { + checkPost(); + }, 1500 ); + }); + } else { + checkPost(); + } + + // Save every 15 sec. + intervalTimer = window.setInterval( save, 15000 ); + + $( 'form#post' ).on( 'submit.autosave-local', function() { + var editor = typeof tinymce !== 'undefined' && tinymce.get('content'), + post_id = $('#post_ID').val() || 0; + + if ( editor && ! editor.isHidden() ) { + // Last onSubmit event in the editor, needs to run after the content has been moved to the textarea. + editor.on( 'submit', function() { + save({ + post_title: $( '#title' ).val() || '', + content: $( '#content' ).val() || '', + excerpt: $( '#excerpt' ).val() || '' + }); + }); + } else { + save({ + post_title: $( '#title' ).val() || '', + content: $( '#content' ).val() || '', + excerpt: $( '#excerpt' ).val() || '' }); } - makeSlugeditClickable(); + + wpCookies.set( 'wp-saving-post-' + post_id, 'check' ); + }); + } + + // Strip whitespace and compare two strings + function compare( str1, str2 ) { + function removeSpaces( string ) { + return string.toString().replace(/[\x20\t\r\n\f]+/g, ''); } + + return ( removeSpaces( str1 || '' ) === removeSpaces( str2 || '' ) ); } - ); - } -} -function autosave_loading() { - jQuery('.autosave-message').html(autosaveL10n.savingText); -} - -function autosave_enable_buttons() { - jQuery(document).trigger('autosave-enable-buttons'); - if ( ! wp.heartbeat || ! wp.heartbeat.hasConnectionError() ) { - // delay that a bit to avoid some rare collisions while the DOM is being updated. - setTimeout(function(){ - var parent = jQuery('#submitpost'); - parent.find(':button, :submit').removeAttr('disabled'); - parent.find('.spinner').hide(); - }, 500); - } -} - -function autosave_disable_buttons() { - jQuery(document).trigger('autosave-disable-buttons'); - jQuery('#submitpost').find(':button, :submit').prop('disabled', true); - // Re-enable 5 sec later. Just gives autosave a head start to avoid collisions. - setTimeout( autosave_enable_buttons, 5000 ); -} - -function delayed_autosave() { - setTimeout(function(){ - if ( blockSave ) - return; - autosave(); - }, 200); -} - -autosave = function() { - var post_data = wp.autosave.getPostData(), - compareString, - successCallback; - - blockSave = true; - - // post_data.content cannot be retrieved at the moment - if ( ! post_data.autosave ) - return false; - - // No autosave while thickbox is open (media buttons) - if ( jQuery('#TB_window').css('display') == 'block' ) - return false; - - compareString = wp.autosave.getCompareString( post_data ); - - // Nothing to save or no change. - if ( compareString == autosaveLast ) - return false; - - autosaveLast = compareString; - jQuery(document).triggerHandler('wpcountwords', [ post_data.content ]); - - // Disable buttons until we know the save completed. - autosave_disable_buttons(); - - if ( post_data.auto_draft == '1' ) { - successCallback = autosave_saved_new; // new post - } else { - successCallback = autosave_saved; // pre-existing post - } - - jQuery.ajax({ - data: post_data, - beforeSend: autosave_loading, - type: 'POST', - url: ajaxurl, - success: successCallback - }); - - return true; -}; - -// Autosave in localStorage -// set as simple object/mixin for now -window.wp = window.wp || {}; -wp.autosave = wp.autosave || {}; - -(function($){ -// Returns the data for saving in both localStorage and autosaves to the server -wp.autosave.getPostData = function() { - var ed = typeof tinymce != 'undefined' ? tinymce.activeEditor : null, post_name, parent_id, cats = [], - data = { - action: 'autosave', - autosave: true, - post_id: $('#post_ID').val() || 0, - autosavenonce: $('#autosavenonce').val() || '', - post_type: $('#post_type').val() || '', - post_author: $('#post_author').val() || '', - excerpt: $('#excerpt').val() || '' - }; - - if ( ed && !ed.isHidden() ) { - // Don't run while the tinymce spellcheck is on. It resets all found words. - if ( ed.plugins.spellchecker && ed.plugins.spellchecker.active ) { - data.autosave = false; - return data; - } else { - tinymce.triggerSave(); - } - } - - data.post_title = $('#title').val() || ''; - data.content = $('#content').val() || ''; - - /* - // We haven't been saving tags with autosave since 2.8... Start again? - $('.the-tags').each( function() { - data[this.name] = this.value; - }); - */ - - $('input[id^="in-category-"]:checked').each( function() { - cats.push(this.value); - }); - data.catslist = cats.join(','); - - if ( post_name = $('#post_name').val() ) - data.post_name = post_name; - - if ( parent_id = $('#parent_id').val() ) - data.parent_id = parent_id; - - if ( $('#comment_status').prop('checked') ) - data.comment_status = 'open'; - - if ( $('#ping_status').prop('checked') ) - data.ping_status = 'open'; - - if ( $('#auto_draft').val() == '1' ) - data.auto_draft = '1'; - - return data; -}; - -// Concatenate title, content and excerpt. Used to track changes when auto-saving. -wp.autosave.getCompareString = function( post_data ) { - if ( typeof post_data === 'object' ) { - return ( post_data.post_title || '' ) + '::' + ( post_data.content || '' ) + '::' + ( post_data.excerpt || '' ); - } - - return ( $('#title').val() || '' ) + '::' + ( $('#content').val() || '' ) + '::' + ( $('#excerpt').val() || '' ); -}; - -wp.autosave.local = { - - lastSavedData: '', - blog_id: 0, - hasStorage: false, - - // Check if the browser supports sessionStorage and it's not disabled - checkStorage: function() { - var test = Math.random(), result = false; - - try { - sessionStorage.setItem('wp-test', test); - result = sessionStorage.getItem('wp-test') == test; - sessionStorage.removeItem('wp-test'); - } catch(e) {} - - this.hasStorage = result; - return result; - }, - - /** - * Initialize the local storage - * - * @return mixed False if no sessionStorage in the browser or an Object containing all post_data for this blog - */ - getStorage: function() { - var stored_obj = false; - // Separate local storage containers for each blog_id - if ( this.hasStorage && this.blog_id ) { - stored_obj = sessionStorage.getItem( 'wp-autosave-' + this.blog_id ); - - if ( stored_obj ) - stored_obj = JSON.parse( stored_obj ); - else - stored_obj = {}; - } - - return stored_obj; - }, - - /** - * Set the storage for this blog - * - * Confirms that the data was saved successfully. - * - * @return bool - */ - setStorage: function( stored_obj ) { - var key; - - if ( this.hasStorage && this.blog_id ) { - key = 'wp-autosave-' + this.blog_id; - sessionStorage.setItem( key, JSON.stringify( stored_obj ) ); - return sessionStorage.getItem( key ) !== null; - } - - return false; - }, - - /** - * Get the saved post data for the current post - * - * @return mixed False if no storage or no data or the post_data as an Object - */ - getData: function() { - var stored = this.getStorage(), post_id = $('#post_ID').val(); - - if ( !stored || !post_id ) - return false; - - return stored[ 'post_' + post_id ] || false; - }, - - /** - * Set (save or delete) post data in the storage. - * - * If stored_data evaluates to 'false' the storage key for the current post will be removed - * - * $param stored_data The post data to store or null/false/empty to delete the key - * @return bool - */ - setData: function( stored_data ) { - var stored = this.getStorage(), post_id = $('#post_ID').val(); - - if ( !stored || !post_id ) - return false; - - if ( stored_data ) - stored[ 'post_' + post_id ] = stored_data; - else if ( stored.hasOwnProperty( 'post_' + post_id ) ) - delete stored[ 'post_' + post_id ]; - else - return false; - - return this.setStorage(stored); - }, - - /** - * Save post data for the current post - * - * Runs on a 15 sec. schedule, saves when there are differences in the post title or content. - * When the optional data is provided, updates the last saved post data. - * - * $param data optional Object The post data for saving, minimum 'post_title' and 'content' - * @return bool - */ - save: function( data ) { - var result = false, post_data, compareString; - - if ( ! data ) { - post_data = wp.autosave.getPostData(); - } else { - post_data = this.getData() || {}; - $.extend( post_data, data ); - post_data.autosave = true; - } - - // Cannot get the post data at the moment - if ( ! post_data.autosave ) - return false; - - compareString = wp.autosave.getCompareString( post_data ); - - // If the content, title and excerpt did not change since the last save, don't save again - if ( compareString == this.lastSavedData ) - return false; - - post_data.save_time = (new Date()).getTime(); - post_data.status = $('#post_status').val() || ''; - result = this.setData( post_data ); - - if ( result ) - this.lastSavedData = compareString; - - return result; - }, - - // Initialize and run checkPost() on loading the script (before TinyMCE init) - init: function( settings ) { - var self = this; - - // Check if the browser supports sessionStorage and it's not disabled - if ( ! this.checkStorage() ) - return; - - // Don't run if the post type supports neither 'editor' (textarea#content) nor 'excerpt'. - if ( ! $('#content').length && ! $('#excerpt').length ) - return; - - if ( settings ) - $.extend( this, settings ); - - if ( !this.blog_id ) - this.blog_id = typeof window.autosaveL10n != 'undefined' ? window.autosaveL10n.blog_id : 0; - - $(document).ready( function(){ self.run(); } ); - }, - - // Run on DOM ready - run: function() { - var self = this; - - // Check if the local post data is different than the loaded post data. - this.checkPost(); - - // Set the schedule - this.schedule = $.schedule({ - time: 15 * 1000, - func: function() { wp.autosave.local.save(); }, - repeat: true, - protect: true - }); - - $('form#post').on('submit.autosave-local', function() { - var editor = typeof tinymce != 'undefined' && tinymce.get('content'), post_id = $('#post_ID').val() || 0; - - if ( editor && ! editor.isHidden() ) { - // Last onSubmit event in the editor, needs to run after the content has been moved to the textarea. - editor.onSubmit.add( function() { - wp.autosave.local.save({ - post_title: $('#title').val() || '', - content: $('#content').val() || '', - excerpt: $('#excerpt').val() || '' - }); - }); - } else { - self.save({ - post_title: $('#title').val() || '', - content: $('#content').val() || '', - excerpt: $('#excerpt').val() || '' + /** + * Check if the saved data for the current post (if any) is different than the loaded post data on the screen + * + * Shows a standard message letting the user restore the post data if different. + * + * @return void + */ + function checkPost() { + var content, post_title, excerpt, $notice, + postData = getSavedPostData(), + cookie = wpCookies.get( 'wp-saving-post-' + post_id ); + + if ( ! postData ) { + return; + } + + if ( cookie ) { + wpCookies.remove( 'wp-saving-post-' + post_id ); + + if ( cookie === 'saved' ) { + // The post was saved properly, remove old data and bail + setData( false ); + return; + } + } + + // There is a newer autosave. Don't show two "restore" notices at the same time. + if ( $( '#has-newer-autosave' ).length ) { + return; + } + + content = $( '#content' ).val() || ''; + post_title = $( '#title' ).val() || ''; + excerpt = $( '#excerpt' ).val() || ''; + + // cookie == 'check' means the post was not saved properly, always show #local-storage-notice + if ( cookie !== 'check' && compare( content, postData.content ) && + compare( post_title, postData.post_title ) && compare( excerpt, postData.excerpt ) ) { + + return; + } + + restorePostData = postData; + undoPostData = { + content: content, + post_title: post_title, + excerpt: excerpt + }; + + $notice = $( '#local-storage-notice' ); + $('.wrap h2').first().after( $notice.addClass( 'updated' ).show() ); + + $notice.on( 'click.autosae-local', function( event ) { + var $target = $( event.target ); + + if ( $target.hasClass( 'restore-backup' ) ) { + restorePost( restorePostData ); + $target.parent().hide(); + $(this).find( 'p.undo-restore' ).show(); + } else if ( $target.hasClass( 'undo-restore-backup' ) ) { + restorePost( undoPostData ); + $target.parent().hide(); + $(this).find( 'p.local-restore' ).show(); + } + + event.preventDefault(); }); } - wpCookies.set( 'wp-saving-post-' + post_id, 'check' ); - }); - }, + // Restore the current title, content and excerpt from postData. + function restorePost( postData ) { + var editor; - // Strip whitespace and compare two strings - compare: function( str1, str2 ) { - function remove( string ) { - return string.toString().replace(/[\x20\t\r\n\f]+/g, ''); - } + if ( postData ) { + // Set the last saved data + lastCompareString = getCompareString( postData ); - return ( remove( str1 || '' ) == remove( str2 || '' ) ); - }, + if ( $( '#title' ).val() !== postData.post_title ) { + $( '#title' ).focus().val( postData.post_title || '' ); + } - /** - * Check if the saved data for the current post (if any) is different than the loaded post data on the screen - * - * Shows a standard message letting the user restore the post data if different. - * - * @return void - */ - checkPost: function() { - var self = this, post_data = this.getData(), content, post_title, excerpt, notice, - post_id = $('#post_ID').val() || 0, cookie = wpCookies.get( 'wp-saving-post-' + post_id ); + $( '#excerpt' ).val( postData.excerpt || '' ); + editor = typeof tinymce !== 'undefined' && tinymce.get('content'); - if ( ! post_data ) - return; + if ( editor && ! editor.isHidden() && typeof switchEditors !== 'undefined' ) { + // Make sure there's an undo level in the editor + editor.undoManager.add(); + editor.setContent( postData.content ? switchEditors.wpautop( postData.content ) : '' ); + } else { + // Make sure the Text editor is selected + $( '#content-html' ).click(); + $( '#content' ).val( postData.content ); + } - if ( cookie ) { - wpCookies.remove( 'wp-saving-post-' + post_id ); + return true; + } - if ( cookie == 'saved' ) { - // The post was saved properly, remove old data and bail - this.setData( false ); + return false; + } + + // Initialize and run checkPost() on loading the script (before TinyMCE init) + blog_id = typeof window.autosaveL10n !== 'undefined' && window.autosaveL10n.blog_id; + + // Check if the browser supports sessionStorage and it's not disabled + if ( ! checkStorage() ) { return; } - } - // There is a newer autosave. Don't show two "restore" notices at the same time. - if ( $('#has-newer-autosave').length ) - return; - - content = $('#content').val() || ''; - post_title = $('#title').val() || ''; - excerpt = $('#excerpt').val() || ''; - - if ( $('#wp-content-wrap').hasClass('tmce-active') && typeof switchEditors != 'undefined' ) - content = switchEditors.pre_wpautop( content ); - - // cookie == 'check' means the post was not saved properly, always show #local-storage-notice - if ( cookie != 'check' && this.compare( content, post_data.content ) && this.compare( post_title, post_data.post_title ) && this.compare( excerpt, post_data.excerpt ) ) { - return; - } - - this.restore_post_data = post_data; - this.undo_post_data = { - content: content, - post_title: post_title, - excerpt: excerpt - }; - - notice = $('#local-storage-notice'); - $('.wrap h2').first().after( notice.addClass('updated').show() ); - - notice.on( 'click', function(e) { - var target = $( e.target ); - - if ( target.hasClass('restore-backup') ) { - self.restorePost( self.restore_post_data ); - target.parent().hide(); - $(this).find('p.undo-restore').show(); - } else if ( target.hasClass('undo-restore-backup') ) { - self.restorePost( self.undo_post_data ); - target.parent().hide(); - $(this).find('p.local-restore').show(); + // Don't run if the post type supports neither 'editor' (textarea#content) nor 'excerpt'. + if ( ! blog_id || ( ! $('#content').length && ! $('#excerpt').length ) ) { + return; } - e.preventDefault(); + $document.ready( run ); + + return { + hasStorage: hasStorage, + getSavedPostData: getSavedPostData, + save: save + }; + } + + // Autosave on the server + function autosaveServer() { + var _disabled, _blockSave, _blockSaveTimer, previousCompareString, lastCompareString, + nextRun = 0; + + // Block saving for the next 10 sec. + function tempBlockSave() { + _blockSave = true; + window.clearTimeout( _blockSaveTimer ); + + _blockSaveTimer = window.setTimeout( function() { + _blockSave = false; + }, 10000 ); + } + + // Runs on heartbeat-response + function response( data ) { + _schedule(); + _blockSave = false; + lastCompareString = previousCompareString; + previousCompareString = ''; + + $document.trigger( 'after-autosave', [data] ); + $( '.autosave-message' ).text( data.message ); + enableButtons(); + + if ( data.success ) { + // No longer an auto-draft + $( '#auto_draft' ).val(''); + } + } + + /** + * Disable autosave + * + * Intended to run on form.submit + */ + function disable() { + _disabled = true; + } + + /** + * Save immediately + * + * Resets the timing and tells heartbeat to connect now + * + * @return void + */ + function triggerSave() { + nextRun = 0; + wp.heartbeat.connectNow(); + } + + /** + * Checks if the post content in the textarea has changed since page load. + * + * This also happens when TinyMCE is active and editor.save() is triggered by + * wp.autosave.getPostData(). + * + * @return bool + */ + function postChanged() { + return getCompareString() !== initialCompareString; + } + + // Runs on 'heartbeat-send' + function save() { + var postData, compareString; + + if ( isSuspended || _disabled || _blockSave ) { + return false; + } + + if ( ( new Date() ).getTime() < nextRun ) { + return false; + } + + postData = getPostData(); + compareString = getCompareString( postData ); + + // First check + if ( typeof lastCompareString === 'undefined' ) { + lastCompareString = initialCompareString; + } + + // No change + if ( compareString === lastCompareString ) { + return false; + } + + previousCompareString = compareString; + tempBlockSave(); + disableButtons(); + + $document.trigger( 'wpcountwords', [ postData.content ] ) + .trigger( 'before-autosave', [ postData ] ); + + $( '.autosave-message' ).text( autosaveL10n.savingText ); + postData._wpnonce = $( '#_wpnonce' ).val() || ''; + + return postData; + } + + function _schedule() { + nextRun = ( new Date() ).getTime() + ( autosaveL10n.autosaveInterval * 1000 ) || 60000; + } + + $document.on( 'heartbeat-send.autosave', function( event, data ) { + var autosaveData = save(); + + if ( autosaveData ) { + data.wp_autosave = autosaveData; + } + }).on( 'heartbeat-tick.autosave', function( event, data ) { + if ( data.wp_autosave ) { + response( data.wp_autosave ); + } + }).on( 'heartbeat-connection-lost.autosave', function( event, error, status ) { + // When connection is lost, keep user from submitting changes. + if ( 'timeout' === error || 603 === status ) { + var $notice = $('#lost-connection-notice'); + + if ( ! wp.autosave.local.hasStorage ) { + $notice.find('.hide-if-no-sessionstorage').hide(); + } + + $notice.show(); + disableButtons(); + } + }).on( 'heartbeat-connection-restored.autosave', function() { + $('#lost-connection-notice').hide(); + enableButtons(); + }).ready( function() { + _schedule(); + }); + + return { + disable: disable, + tempBlockSave: tempBlockSave, + triggerSave: triggerSave, + postChanged: postChanged + }; + } + + // Wait for TinyMCE to initialize plus 1 sec. for any external css to finish loading, + // then 'save' to the textarea before setting initialCompareString. + // This avoids any insignificant differences between the initial textarea content and the content + // extracted from the editor. + $document.on( 'tinymce-editor-init.autosave', function( event, editor ) { + if ( editor.id === 'content' ) { + window.setTimeout( function() { + editor.save(); + initialCompareString = getCompareString(); + }, 1000 ); + } + }).ready( function() { + // Set the initial compare string in case TinyMCE is not used or not loaded first + initialCompareString = getCompareString(); }); - }, - // Restore the current title, content and excerpt from post_data. - restorePost: function( post_data ) { - var editor; - - if ( post_data ) { - // Set the last saved data - this.lastSavedData = wp.autosave.getCompareString( post_data ); - - if ( $('#title').val() != post_data.post_title ) - $('#title').focus().val( post_data.post_title || '' ); - - $('#excerpt').val( post_data.excerpt || '' ); - editor = typeof tinymce != 'undefined' && tinymce.get('content'); - - if ( editor && ! editor.isHidden() && typeof switchEditors != 'undefined' ) { - // Make sure there's an undo level in the editor - editor.undoManager.add(); - editor.setContent( post_data.content ? switchEditors.wpautop( post_data.content ) : '' ); - } else { - // Make sure the Text editor is selected - $('#content-html').click(); - $('#content').val( post_data.content ); - } - - return true; - } - - return false; + return { + getPostData: getPostData, + getCompareString: getCompareString, + disableButtons: disableButtons, + enableButtons: enableButtons, + suspend: suspend, + resume: resume, + local: autosaveLocal(), + server: autosaveServer() + }; } -}; -wp.autosave.local.init(); + window.wp = window.wp || {}; + window.wp.autosave = autosave(); -}(jQuery)); +}( jQuery, window )); diff --git a/src/wp-includes/revision.php b/src/wp-includes/revision.php index 5f441d3f5a..82b95c31ec 100644 --- a/src/wp-includes/revision.php +++ b/src/wp-includes/revision.php @@ -215,7 +215,7 @@ function wp_is_post_autosave( $post ) { * * @param int|object|array $post Post ID, post object OR post array. * @param bool $autosave Optional. Is the revision an autosave? - * @return mixed Null or 0 if error, new revision ID if success. + * @return mixed WP_Error or 0 if error, new revision ID if success. */ function _wp_put_post_revision( $post = null, $autosave = false ) { if ( is_object($post) ) @@ -223,8 +223,8 @@ function _wp_put_post_revision( $post = null, $autosave = false ) { elseif ( !is_array($post) ) $post = get_post($post, ARRAY_A); - if ( !$post || empty($post['ID']) ) - return; + if ( ! $post || empty($post['ID']) ) + return new WP_Error( 'invalid_post', __( 'Invalid post ID' ) ); if ( isset($post['post_type']) && 'revision' == $post['post_type'] ) return new WP_Error( 'post_type', __( 'Cannot create a revision of a revision' ) ); diff --git a/tests/phpunit/includes/testcase-ajax.php b/tests/phpunit/includes/testcase-ajax.php index 6b14b66bd9..b415976f21 100644 --- a/tests/phpunit/includes/testcase-ajax.php +++ b/tests/phpunit/includes/testcase-ajax.php @@ -42,12 +42,12 @@ abstract class WP_Ajax_UnitTestCase extends WP_UnitTestCase { 'oembed_cache', 'image-editor', 'delete-comment', 'delete-tag', 'delete-link', 'delete-meta', 'delete-post', 'trash-post', 'untrash-post', 'delete-page', 'dim-comment', 'add-link-category', 'add-tag', 'get-tagcloud', 'get-comments', 'replyto-comment', - 'edit-comment', 'add-menu-item', 'add-meta', 'add-user', 'autosave', 'closed-postboxes', + 'edit-comment', 'add-menu-item', 'add-meta', 'add-user', 'closed-postboxes', 'hidden-columns', 'update-welcome-panel', 'menu-get-metabox', 'wp-link-ajax', 'menu-locations-save', 'menu-quick-search', 'meta-box-order', 'get-permalink', 'sample-permalink', 'inline-save', 'inline-save-tax', 'find_posts', 'widgets-order', 'save-widget', 'set-post-thumbnail', 'date_format', 'time_format', 'wp-fullscreen-save-post', - 'wp-remove-post-lock', 'dismiss-wp-pointer', 'nopriv_autosave' + 'wp-remove-post-lock', 'dismiss-wp-pointer', 'heartbeat', 'nopriv_heartbeat', ); /** diff --git a/tests/phpunit/tests/ajax/Autosave.php b/tests/phpunit/tests/ajax/Autosave.php index 4b9c00c390..a96e6cf898 100644 --- a/tests/phpunit/tests/ajax/Autosave.php +++ b/tests/phpunit/tests/ajax/Autosave.php @@ -22,52 +22,128 @@ class Tests_Ajax_Autosave extends WP_Ajax_UnitTestCase { */ protected $_post = null; + /** + * user_id + * @var int + */ + protected $user_id = 0; + /** * Set up the test fixture */ public function setUp() { parent::setUp(); + // Set a user so the $post has 'post_author' + $this->user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $this->user_id ); + $post_id = $this->factory->post->create( array( 'post_status' => 'draft' ) ); $this->_post = get_post( $post_id ); } + /** + * Tear down the test fixture. + * Reset the current user + */ + public function tearDown() { + parent::tearDown(); + wp_set_current_user( 0 ); + } + /** * Test autosaving a post * @return void */ public function test_autosave_post() { - - // Become an admin - $this->_setRole( 'administrator' ); + // The original post_author + wp_set_current_user( $this->user_id ); // Set up the $_POST request $md5 = md5( uniqid() ); $_POST = array( - 'post_id' => $this->_post->ID, - 'autosavenonce' => wp_create_nonce( 'autosave' ), - 'post_content' => $this->_post->post_content . PHP_EOL . $md5, - 'post_type' => 'post', - 'autosave' => 1, + 'action' => 'heartbeat', + '_nonce' => wp_create_nonce( 'heartbeat-nonce' ), + 'data' => array( + 'wp_autosave' => array( + 'post_id' => $this->_post->ID, + '_wpnonce' => wp_create_nonce( 'update-post_' . $this->_post->ID ), + 'post_content' => $this->_post->post_content . PHP_EOL . $md5, + 'post_type' => 'post', + ), + ), ); // Make the request try { - $this->_handleAjax( 'autosave' ); + $this->_handleAjax( 'heartbeat' ); } catch ( WPAjaxDieContinueException $e ) { unset( $e ); } - // Get the response - $xml = simplexml_load_string( $this->_last_response, 'SimpleXMLElement', LIBXML_NOCDATA ); + // Get the response, it is in heartbeat's response + $response = json_decode( $this->_last_response, true ); // Ensure everything is correct - $this->assertEquals( $this->_post->ID, (int) $xml->response[0]->autosave['id'] ); - $this->assertEquals( 'autosave_' . $this->_post->ID, (string) $xml->response['action']); + $this->assertNotEmpty( $response['wp_autosave'] ); + $this->assertTrue( $response['wp_autosave']['success'] ); // Check that the edit happened - $post = get_post( $this->_post->ID) ; + $post = get_post( $this->_post->ID ); $this->assertGreaterThanOrEqual( 0, strpos( $post->post_content, $md5 ) ); } + + /** + * Test autosaving a locked post + * @return void + */ + public function test_autosave_locked_post() { + // Lock the post to another user + $another_user_id = $this->factory->user->create( array( 'role' => 'editor' ) ); + wp_set_current_user( $another_user_id ); + wp_set_post_lock( $this->_post->ID ); + + wp_set_current_user( $this->user_id ); + + // Ensure post is locked + $this->assertEquals( $another_user_id, wp_check_post_lock( $this->_post->ID ) ); + + // Set up the $_POST request + $md5 = md5( uniqid() ); + $_POST = array( + 'action' => 'heartbeat', + '_nonce' => wp_create_nonce( 'heartbeat-nonce' ), + 'data' => array( + 'wp_autosave' => array( + 'post_id' => $this->_post->ID, + '_wpnonce' => wp_create_nonce( 'update-post_' . $this->_post->ID ), + 'post_content' => $this->_post->post_content . PHP_EOL . $md5, + 'post_type' => 'post', + ), + ), + ); + + // Make the request + try { + $this->_handleAjax( 'heartbeat' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + $response = json_decode( $this->_last_response, true ); + + // Ensure everything is correct + $this->assertNotEmpty( $response['wp_autosave'] ); + $this->assertTrue( $response['wp_autosave']['success'] ); + + // Check that the original post was NOT edited + $post = get_post( $this->_post->ID ); + $this->assertFalse( strpos( $post->post_content, $md5 ) ); + + // Check if the autosave post was created + $autosave = wp_get_post_autosave( $this->_post->ID, get_current_user_id() ); + $this->assertNotEmpty( $autosave ); + $this->assertGreaterThanOrEqual( 0, strpos( $autosave->post_content, $md5 ) ); + } /** * Test with an invalid nonce @@ -75,40 +151,30 @@ class Tests_Ajax_Autosave extends WP_Ajax_UnitTestCase { */ public function test_with_invalid_nonce( ) { - // Become an administrator - $this->_setRole( 'administrator' ); + wp_set_current_user( $this->user_id ); // Set up the $_POST request $_POST = array( - 'post_id' => $this->_post->ID, - 'autosavenonce' => md5( uniqid() ), - 'autosave' => 1 + 'action' => 'heartbeat', + '_nonce' => wp_create_nonce( 'heartbeat-nonce' ), + 'data' => array( + 'wp_autosave' => array( + 'post_id' => $this->_post->ID, + '_wpnonce' => substr( md5( uniqid() ), 0, 10 ), + ), + ), ); // Make the request - $this->setExpectedException( 'WPAjaxDieStopException', '-1' ); - $this->_handleAjax( 'autosave' ); - } + try { + $this->_handleAjax( 'heartbeat' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } - /** - * Test with a bad post id - * @return void - */ - public function test_with_invalid_post_id( ) { + $response = json_decode( $this->_last_response, true ); - // Become an administrator - $this->_setRole( 'administrator' ); - - // Set up the $_POST request - $_POST = array( - 'post_id' => 0, - 'autosavenonce' => wp_create_nonce( 'autosave' ), - 'autosave' => 1, - 'post_type' => 'post' - ); - - // Make the request - $this->setExpectedException( 'WPAjaxDieStopException', 'You are not allowed to edit this post.' ); - $this->_handleAjax( 'autosave' ); + $this->assertNotEmpty( $response['wp_autosave'] ); + $this->assertFalse( $response['wp_autosave']['success'] ); } }