Make WordPress Core


Ignore:
Timestamp:
10/12/2017 04:00:15 AM (7 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.