Make WordPress Core


Ignore:
Timestamp:
10/12/2017 04:00:15 AM (9 years ago)
Author:
westonruter
Message:

Customize: Add changeset locking in Customizer to prevent users from overriding each other's changes.

  • Customization locking is checked when changesets are saved and when heartbeat ticks.
  • Lock is lifted immediately upon a user closing the Customizer.
  • Heartbeat is introduced into Customizer.
  • Changes made to user after it was locked by another user are stored as an autosave revision for restoration.
  • Lock notification displays link to preview the other user's changes on the frontend.
  • A user loading a locked Customizer changeset will be presented with an option to take over.
  • Autosave revisions attached to a published changeset are converted into auto-drafts so that they will be presented to users for restoration.
  • Focus constraining is improved in overlay notifications.
  • Escape key is stopped from propagating in overlay notifications, and it dismisses dismissible overlay notifications.
  • Introduces changesetLocked state which is used to disable the Save button and suppress the AYS dialog when leaving the Customizer.
  • Fixes bug where users could be presented with each other's autosave revisions.

Props sayedwp, westonruter, melchoyce.
See #31436, #31897, #39896.
Fixes #42024.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r41825 r41839  
    175175
    176176        /**
    177          * Whether the autosave revision of the changeset should should be loaded.
     177         * Whether the autosave revision of the changeset should be loaded.
    178178         *
    179179         * @since 4.9.0
     
    374374                remove_action( 'admin_init', '_maybe_update_themes' );
    375375
    376                 add_action( 'wp_ajax_customize_save',             array( $this, 'save' ) );
    377                 add_action( 'wp_ajax_customize_trash',            array( $this, 'handle_changeset_trash_request' ) );
    378                 add_action( 'wp_ajax_customize_refresh_nonces',   array( $this, 'refresh_nonces' ) );
    379                 add_action( 'wp_ajax_customize_load_themes',      array( $this, 'handle_load_themes_request' ) );
    380                 add_action( 'wp_ajax_customize_dismiss_autosave', array( $this, 'handle_dismiss_autosave_request' ) );
     376                add_action( 'wp_ajax_customize_save',                     array( $this, 'save' ) );
     377                add_action( 'wp_ajax_customize_trash',                    array( $this, 'handle_changeset_trash_request' ) );
     378                add_action( 'wp_ajax_customize_refresh_nonces',           array( $this, 'refresh_nonces' ) );
     379                add_action( 'wp_ajax_customize_load_themes',              array( $this, 'handle_load_themes_request' ) );
     380                add_filter( 'heartbeat_settings',                         array( $this, 'add_customize_screen_to_heartbeat_settings' ) );
     381                add_filter( 'heartbeat_received',                         array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 );
     382                add_action( 'wp_ajax_customize_override_changeset_lock',  array( $this, 'handle_override_changeset_lock_request' ) );
     383                add_action( 'wp_ajax_customize_dismiss_autosave_or_lock', array( $this, 'handle_dismiss_autosave_or_lock_request' ) );
    381384
    382385                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
     
    630633                        $this->_changeset_uuid = $changeset_uuid;
    631634                }
     635
     636                $this->set_changeset_lock( $this->changeset_post_id() );
    632637        }
    633638
     
    11071112                } else {
    11081113                        if ( $this->autosaved() ) {
    1109                                 $autosave_post = wp_get_post_autosave( $changeset_post_id );
     1114                                $autosave_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
    11101115                                if ( $autosave_post ) {
    11111116                                        $data = $this->get_changeset_post_data( $autosave_post->ID );
     
    23772382                }
    23782383
     2384                $lock_user_id = null;
    23792385                $autosave = ! empty( $_POST['customize_changeset_autosave'] );
     2386                if ( ! $is_new_changeset ) {
     2387                        $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
     2388                }
     2389
     2390                // Force request to autosave when changeset is locked.
     2391                if ( $lock_user_id && ! $autosave ) {
     2392                        $autosave = true;
     2393                        $changeset_status = null;
     2394                        $changeset_date_gmt = null;
     2395                }
     2396
    23802397                if ( $autosave && ! defined( 'DOING_AUTOSAVE' ) ) { // Back-compat.
    23812398                        define( 'DOING_AUTOSAVE', true );
    23822399                }
    23832400
     2401                $autosaved = false;
    23842402                $r = $this->save_changeset_post( array(
    23852403                        'status' => $changeset_status,
     
    23892407                        'autosave' => $autosave,
    23902408                ) );
     2409                if ( $autosave && ! is_wp_error( $r ) ) {
     2410                        $autosaved = true;
     2411                }
     2412
     2413                // If the changeset was locked and an autosave request wasn't itself an error, then now explicitly return with a failure.
     2414                if ( $lock_user_id && ! is_wp_error( $r ) ) {
     2415                        $r = new WP_Error(
     2416                                'changeset_locked',
     2417                                __( 'Changeset is being edited by other user.' ),
     2418                                array(
     2419                                        'lock_user' => $this->get_lock_user_data( $lock_user_id ),
     2420                                )
     2421                        );
     2422                }
     2423
    23912424                if ( is_wp_error( $r ) ) {
    23922425                        $response = array(
     
    24142447                        }
    24152448
     2449                        if ( 'publish' !== $response['changeset_status'] ) {
     2450                                $this->set_changeset_lock( $changeset_post->ID );
     2451                        }
     2452
    24162453                        if ( 'future' === $response['changeset_status'] ) {
    24172454                                $response['changeset_date'] = $changeset_post->post_date;
     
    24212458                                $response['next_changeset_uuid'] = wp_generate_uuid4();
    24222459                        }
     2460                }
     2461
     2462                if ( $autosave ) {
     2463                        $response['autosaved'] = $autosaved;
    24232464                }
    24242465
     
    26852726                                                'type' => $setting->type,
    26862727                                                'user_id' => $args['user_id'],
     2728                                                'date_modified_gmt' => current_time( 'mysql', true ),
    26872729                                        )
    26882730                                );
     
    27992841
    28002842                                // Delete autosave revision when the changeset is updated.
    2801                                 $autosave_draft = wp_get_post_autosave( $changeset_post_id );
     2843                                $autosave_draft = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
    28022844                                if ( $autosave_draft ) {
    28032845                                        wp_delete_post( $autosave_draft->ID, true );
     
    29913033
    29923034        /**
     3035         * Marks the changeset post as being currently edited by the current user.
     3036         *
     3037         * @since 4.9.0
     3038         *
     3039         * @param int  $changeset_post_id Changeset post id.
     3040         * @param bool $take_over Take over the changeset, default is false.
     3041         */
     3042        public function set_changeset_lock( $changeset_post_id, $take_over = false ) {
     3043                if ( $changeset_post_id ) {
     3044                        $can_override = ! (bool) get_post_meta( $changeset_post_id, '_edit_lock', true );
     3045
     3046                        if ( $take_over ) {
     3047                                $can_override = true;
     3048                        }
     3049
     3050                        if ( $can_override ) {
     3051                                $lock = sprintf( '%s:%s', time(), get_current_user_id() );
     3052                                update_post_meta( $changeset_post_id, '_edit_lock', $lock );
     3053                        } else {
     3054                                $this->refresh_changeset_lock( $changeset_post_id );
     3055                        }
     3056                }
     3057        }
     3058
     3059        /**
     3060         * Refreshes changeset lock with the current time if current user edited the changeset before.
     3061         *
     3062         * @since 4.9.0
     3063         *
     3064         * @param int $changeset_post_id Changeset post id.
     3065         */
     3066        public function refresh_changeset_lock( $changeset_post_id ) {
     3067                if ( ! $changeset_post_id ) {
     3068                        return;
     3069                }
     3070                $lock = get_post_meta( $changeset_post_id, '_edit_lock', true );
     3071                $lock = explode( ':', $lock );
     3072
     3073                if ( $lock && ! empty( $lock[1] ) ) {
     3074                        $user_id = intval( $lock[1] );
     3075                        $current_user_id = get_current_user_id();
     3076                        if ( $user_id === $current_user_id ) {
     3077                                $lock = sprintf( '%s:%s', time(), $user_id );
     3078                                update_post_meta( $changeset_post_id, '_edit_lock', $lock );
     3079                        }
     3080                }
     3081        }
     3082
     3083        /**
     3084         * Filter heartbeat settings for the Customizer.
     3085         *
     3086         * @since 4.9.0
     3087         * @param array $settings Current settings to filter.
     3088         * @return array Heartbeat settings.
     3089         */
     3090        public function add_customize_screen_to_heartbeat_settings( $settings ) {
     3091                global $pagenow;
     3092                if ( 'customize.php' === $pagenow ) {
     3093                        $settings['screenId'] = 'customize';
     3094                }
     3095                return $settings;
     3096        }
     3097
     3098        /**
     3099         * Get lock user data.
     3100         *
     3101         * @since 4.9.0
     3102         *
     3103         * @param int $user_id User ID.
     3104         * @return array|null User data formatted for client.
     3105         */
     3106        protected function get_lock_user_data( $user_id ) {
     3107                if ( ! $user_id ) {
     3108                        return null;
     3109                }
     3110                $lock_user = get_userdata( $user_id );
     3111                if ( ! $lock_user ) {
     3112                        return null;
     3113                }
     3114                return array(
     3115                        'id' => $lock_user->ID,
     3116                        'name' => $lock_user->display_name,
     3117                        'avatar' => get_avatar_url( $lock_user->ID, array( 'size' => 128 ) ),
     3118                );
     3119        }
     3120
     3121        /**
     3122         * Check locked changeset with heartbeat API.
     3123         *
     3124         * @since 4.9.0
     3125         *
     3126         * @param array  $response  The Heartbeat response.
     3127         * @param array  $data      The $_POST data sent.
     3128         * @param string $screen_id The screen id.
     3129         * @return array The Heartbeat response.
     3130         */
     3131        public function check_changeset_lock_with_heartbeat( $response, $data, $screen_id ) {
     3132                if ( array_key_exists( 'check_changeset_lock', $data ) && 'customize' === $screen_id && current_user_can( 'customize' ) && $this->changeset_post_id() ) {
     3133                        $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
     3134
     3135                        if ( $lock_user_id ) {
     3136                                $response['customize_changeset_lock_user'] = $this->get_lock_user_data( $lock_user_id );
     3137                        } else {
     3138
     3139                                // Refreshing time will ensure that the user is sitting on customizer and has not closed the customizer tab.
     3140                                $this->refresh_changeset_lock( $this->changeset_post_id() );
     3141                        }
     3142                }
     3143
     3144                return $response;
     3145        }
     3146
     3147        /**
     3148         * Removes changeset lock when take over request is sent via Ajax.
     3149         *
     3150         * @since 4.9.0
     3151         */
     3152        public function handle_override_changeset_lock_request() {
     3153                if ( ! $this->is_preview() ) {
     3154                        wp_send_json_error( 'not_preview', 400 );
     3155                }
     3156
     3157                if ( ! check_ajax_referer( 'customize_override_changeset_lock', 'nonce', false ) ) {
     3158                        wp_send_json_error( array(
     3159                                'code' => 'invalid_nonce',
     3160                                'message' => __( 'Security check failed.' ),
     3161                        ) );
     3162                }
     3163
     3164                $changeset_post_id = $this->changeset_post_id();
     3165
     3166                if ( empty( $changeset_post_id ) ) {
     3167                        wp_send_json_error( array(
     3168                                'code' => 'no_changeset_found_to_take_over',
     3169                                'message' => __( 'No changeset found to take over' ),
     3170                        ) );
     3171                }
     3172
     3173                if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
     3174                        wp_send_json_error( array(
     3175                                'code' => 'cannot_remove_changeset_lock',
     3176                                'message' => __( 'Sorry you are not allowed to take over.' ),
     3177                        ) );
     3178                }
     3179
     3180                $this->set_changeset_lock( $changeset_post_id, true );
     3181
     3182                wp_send_json_success( 'changeset_taken_over' );
     3183        }
     3184
     3185        /**
    29933186         * Whether a changeset revision should be made.
    29943187         *
     
    30343227         * @since 4.7.0
    30353228         * @see _wp_customize_publish_changeset()
     3229         * @global wpdb $wpdb
    30363230         *
    30373231         * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
     
    30393233         */
    30403234        public function _publish_changeset_values( $changeset_post_id ) {
     3235                global $wpdb;
     3236
    30413237                $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
    30423238                if ( is_wp_error( $publishing_changeset_data ) ) {
     
    31763372                $this->_changeset_uuid    = $previous_changeset_uuid;
    31773373
     3374                /*
     3375                 * Convert all autosave revisions into their own auto-drafts so that users can be prompted to
     3376                 * restore them when a changeset is published, but they had been locked out from including
     3377                 * their changes in the changeset.
     3378                 */
     3379                $revisions = wp_get_post_revisions( $changeset_post_id, array( 'check_enabled' => false ) );
     3380                foreach ( $revisions as $revision ) {
     3381                        if ( false !== strpos( $revision->post_name, "{$changeset_post_id}-autosave" ) ) {
     3382                                $wpdb->update(
     3383                                        $wpdb->posts,
     3384                                        array(
     3385                                                'post_status' => 'auto-draft',
     3386                                                'post_type' => 'customize_changeset',
     3387                                                'post_name' => wp_generate_uuid4(),
     3388                                                'post_parent' => 0,
     3389                                        ),
     3390                                        array(
     3391                                                'ID' => $revision->ID,
     3392                                        )
     3393                                );
     3394                                clean_post_cache( $revision->ID );
     3395                        }
     3396                }
     3397
    31783398                return true;
    31793399        }
     
    32303450
    32313451        /**
    3232          * Delete a given auto-draft changeset or the autosave revision for a given changeset.
     3452         * Delete a given auto-draft changeset or the autosave revision for a given changeset or delete changeset lock.
    32333453         *
    32343454         * @since 4.9.0
    32353455         */
    3236         public function handle_dismiss_autosave_request() {
     3456        public function handle_dismiss_autosave_or_lock_request() {
    32373457                if ( ! $this->is_preview() ) {
    32383458                        wp_send_json_error( 'not_preview', 400 );
    32393459                }
    32403460
    3241                 if ( ! check_ajax_referer( 'customize_dismiss_autosave', 'nonce', false ) ) {
     3461                if ( ! check_ajax_referer( 'customize_dismiss_autosave_or_lock', 'nonce', false ) ) {
    32423462                        wp_send_json_error( 'invalid_nonce', 403 );
    32433463                }
    32443464
    32453465                $changeset_post_id = $this->changeset_post_id();
    3246 
    3247                 if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
    3248                         $dismissed = $this->dismiss_user_auto_draft_changesets();
    3249                         if ( $dismissed > 0 ) {
    3250                                 wp_send_json_success( 'auto_draft_dismissed' );
    3251                         } else {
    3252                                 wp_send_json_error( 'no_auto_draft_to_delete', 404 );
    3253                         }
    3254                 } else {
    3255                         $revision = wp_get_post_autosave( $changeset_post_id );
    3256 
    3257                         if ( $revision ) {
    3258                                 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
    3259                                         wp_send_json_error( 'cannot_delete_autosave_revision', 403 );
    3260                                 }
    3261 
    3262                                 if ( ! wp_delete_post( $revision->ID, true ) ) {
    3263                                         wp_send_json_error( 'autosave_revision_deletion_failure', 500 );
     3466                $dismiss_lock = ! empty( $_POST['dismiss_lock'] );
     3467                $dismiss_autosave = ! empty( $_POST['dismiss_autosave'] );
     3468
     3469                if ( $dismiss_lock ) {
     3470                        if ( empty( $changeset_post_id ) && ! $dismiss_autosave ) {
     3471                                wp_send_json_error( 'no_changeset_to_dismiss_lock', 404 );
     3472                        }
     3473                        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) && ! $dismiss_autosave ) {
     3474                                wp_send_json_error( 'cannot_remove_changeset_lock', 403 );
     3475                        }
     3476
     3477                        delete_post_meta( $changeset_post_id, '_edit_lock' );
     3478
     3479                        if ( ! $dismiss_autosave ) {
     3480                                wp_send_json_success( 'changeset_lock_dismissed' );
     3481                        }
     3482                }
     3483
     3484                if ( $dismiss_autosave ) {
     3485                        if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
     3486                                $dismissed = $this->dismiss_user_auto_draft_changesets();
     3487                                if ( $dismissed > 0 ) {
     3488                                        wp_send_json_success( 'auto_draft_dismissed' );
    32643489                                } else {
    3265                                         wp_send_json_success( 'autosave_revision_deleted' );
     3490                                        wp_send_json_error( 'no_auto_draft_to_delete', 404 );
    32663491                                }
    32673492                        } else {
    3268                                 wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
    3269                         }
    3270                 }
     3493                                $revision = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
     3494
     3495                                if ( $revision ) {
     3496                                        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
     3497                                                wp_send_json_error( 'cannot_delete_autosave_revision', 403 );
     3498                                        }
     3499
     3500                                        if ( ! wp_delete_post( $revision->ID, true ) ) {
     3501                                                wp_send_json_error( 'autosave_revision_deletion_failure', 500 );
     3502                                        } else {
     3503                                                wp_send_json_success( 'autosave_revision_deleted' );
     3504                                        }
     3505                                } else {
     3506                                        wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
     3507                                }
     3508                        }
     3509                }
     3510
    32713511                wp_send_json_error( 'unknown_error', 500 );
    32723512        }
     
    38184058                </script>
    38194059
     4060                <script type="text/html" id="tmpl-customize-changeset-locked-notification">
     4061                        <li class="notice notice-{{ data.type || 'info' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
     4062                                <div class="notification-message customize-changeset-locked-message">
     4063                                        <img class="customize-changeset-locked-avatar" src="{{ data.lockUser.avatar }}" alt="{{ data.lockUser.name }}">
     4064                                        <p class="currently-editing">
     4065                                                <# if ( data.message ) { #>
     4066                                                        {{{ data.message }}}
     4067                                                <# } else if ( data.allowOverride ) { #>
     4068                                                        <?php
     4069                                                        /* translators: %s: User who is customizing the changeset in customizer. */
     4070                                                        printf( __( '%s is already customizing this site. Do you want to take over?' ), '{{ data.lockUser.name }}' );
     4071                                                        ?>
     4072                                                <# } else { #>
     4073                                                        <?php
     4074                                                        /* translators: %s: User who is customizing the changeset in customizer. */
     4075                                                        printf( __( '%s is already customizing this site. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ), '{{ data.lockUser.name }}' );
     4076                                                        ?>
     4077                                                <# } #>
     4078                                        </p>
     4079                                        <p class="notice notice-error notice-alt" hidden></p>
     4080                                        <p class="action-buttons">
     4081                                                <# if ( data.returnUrl !== data.previewUrl ) { #>
     4082                                                        <a class="button customize-notice-go-back-button" href="{{ data.returnUrl }}"><?php _e( 'Go back' ); ?></a>
     4083                                                <# } #>
     4084                                                <a class="button customize-notice-preview-button" href="{{ data.frontendPreviewUrl }}"><?php _e( 'Preview' ); ?></a>
     4085                                                <# if ( data.allowOverride ) { #>
     4086                                                        <button class="button button-primary wp-tab-last customize-notice-take-over-button"><?php _e( 'Take over' ); ?></button>
     4087                                                <# } #>
     4088                                        </p>
     4089                                </div>
     4090                        </li>
     4091                </script>
     4092
    38204093                <?php
    38214094                /* The following template is obsolete in core but retained for plugins. */
     
    41894462                        'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
    41904463                        'switch_themes' => wp_create_nonce( 'switch_themes' ),
    4191                         'dismiss_autosave' => wp_create_nonce( 'customize_dismiss_autosave' ),
     4464                        'dismiss_autosave_or_lock' => wp_create_nonce( 'customize_dismiss_autosave_or_lock' ),
     4465                        'override_lock' => wp_create_nonce( 'customize_override_changeset_lock' ),
    41924466                        'trash' => wp_create_nonce( 'trash_customize_changeset' ),
    41934467                );
     
    42324506                if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) {
    42334507                        if ( $changeset_post_id ) {
    4234                                 $autosave_revision_post = wp_get_post_autosave( $changeset_post_id );
     4508                                $autosave_revision_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
    42354509                        } else {
    42364510                                $autosave_autodraft_posts = $this->get_changeset_posts( array(
     
    42764550                } else {
    42774551                        $initial_date = current_time( 'mysql', false );
     4552                }
     4553
     4554                $lock_user_id = false;
     4555                if ( $this->changeset_post_id() ) {
     4556                        $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
    42784557                }
    42794558
     
    42894568                                'publishDate' => $initial_date,
    42904569                                'statusChoices' => $status_choices,
     4570                                'lockUser' => $lock_user_id ? $this->get_lock_user_data( $lock_user_id ) : null,
    42914571                        ),
    42924572                        'initialServerDate' => current_time( 'mysql', false ),
Note: See TracChangeset for help on using the changeset viewer.