454 lines
17 KiB
Plaintext
454 lines
17 KiB
Plaintext
function update( WP_REST_Request $request ) {
|
|
$post_id = (int) $request->get_param( 'post_id' );
|
|
$post_status = $request->get_param( 'post_status' );
|
|
$content = $request->get_param( 'content' );
|
|
$page_settings_by_layout = $request->get_param( 'pageSettingsByLayout' );
|
|
$sync_type = $request->get_param( 'syncType' );
|
|
$options = $request->get_param( 'options' );
|
|
$prime_cache_url = $request->get_param( 'prime_cache_url' );
|
|
$main_loop_type = $request->get_param( 'mainLoopType' );
|
|
$main_loop_type = is_string( $main_loop_type ) ? $main_loop_type : 'singular';
|
|
$loop_settings_data = $request->get_param( 'mainLoopSettingsData' );
|
|
$layout_post_ids = $request->get_param( 'layout_post_ids' );
|
|
$layout_post_ids = is_array( $layout_post_ids ) ? $layout_post_ids : [];
|
|
$layout_post_ids = self::sanitize_layout_post_ids( $layout_post_ids );
|
|
$page_settings_by_layout = is_array( $page_settings_by_layout ) ? $page_settings_by_layout : [];
|
|
$layout_post_ids = self::_validate_and_normalize_layout_post_ids( $layout_post_ids, $post_id, $main_loop_type );
|
|
|
|
if ( is_wp_error( $layout_post_ids ) ) {
|
|
return self::response_error(
|
|
$layout_post_ids->get_error_code(),
|
|
$layout_post_ids->get_error_message(),
|
|
$layout_post_ids->get_error_data()
|
|
);
|
|
}
|
|
|
|
foreach ( $page_settings_by_layout as $layout => $layout_page_settings ) {
|
|
if ( ! is_array( $layout_page_settings ) || ! isset( $layout_page_settings['customCss'] ) ) {
|
|
continue;
|
|
}
|
|
|
|
$page_settings_by_layout[ $layout ]['customCss'] = str_replace( '\\', '\\\\', $layout_page_settings['customCss'] );
|
|
}
|
|
|
|
$post_content = isset( $content['post_content'] ) ? $content['post_content'] : '';
|
|
$header_content = isset( $content['header'] ) ? $content['header'] : '';
|
|
$body_content = isset( $content['body'] ) ? $content['body'] : '';
|
|
$footer_content = isset( $content['footer'] ) ? $content['footer'] : '';
|
|
$has_post_content_payload = '' !== $post_content;
|
|
|
|
$post_content_post_id = isset( $layout_post_ids['postContent'] ) ? absint( $layout_post_ids['postContent'] ) : $post_id;
|
|
$header_post_id = isset( $layout_post_ids['header'] ) ? absint( $layout_post_ids['header'] ) : 0;
|
|
$body_post_id = isset( $layout_post_ids['body'] ) ? absint( $layout_post_ids['body'] ) : 0;
|
|
$footer_post_id = isset( $layout_post_ids['footer'] ) ? absint( $layout_post_ids['footer'] ) : 0;
|
|
$post_content_page_settings = isset( $page_settings_by_layout['postContent'] ) && is_array( $page_settings_by_layout['postContent'] )
|
|
? $page_settings_by_layout['postContent']
|
|
: [];
|
|
|
|
// Guardrail: never allow empty placeholder-only payloads to overwrite Theme Builder layouts.
|
|
$header_has_content = SavingUtility::has_meaningful_layout_content( $header_content );
|
|
$body_has_content = SavingUtility::has_meaningful_layout_content( $body_content );
|
|
$footer_has_content = SavingUtility::has_meaningful_layout_content( $footer_content );
|
|
|
|
if ( ! $header_has_content ) {
|
|
$header_post_id = 0;
|
|
}
|
|
|
|
if ( ! $body_has_content ) {
|
|
$body_post_id = 0;
|
|
}
|
|
|
|
if ( ! $footer_has_content ) {
|
|
$footer_post_id = 0;
|
|
}
|
|
|
|
// Extract off-canvas data and make it available for hooks.
|
|
$off_canvas_data = $request->get_param( 'off_canvas_data' );
|
|
if ( $off_canvas_data ) {
|
|
// Normalize to array format - decode JSON string if needed.
|
|
$normalized_canvas_data = self::_normalize_off_canvas_data( $off_canvas_data );
|
|
if ( $normalized_canvas_data ) {
|
|
// Store off-canvas data globally so hooks can access it.
|
|
$GLOBALS['divi_off_canvas_data'] = $normalized_canvas_data;
|
|
}
|
|
}
|
|
|
|
// Check if there is an autosave that is newer.
|
|
$post_author = get_current_user_id();
|
|
|
|
// Store one autosave per author. If there is already an autosave, overwrite it.
|
|
$autosave = wp_get_post_autosave( $post_id, $post_author );
|
|
|
|
if ( ! empty( $autosave ) ) {
|
|
wp_delete_post_revision( $autosave->ID );
|
|
}
|
|
|
|
// Check if what is being synced to server is preview post.
|
|
$is_saving_preview = 'preview' === $sync_type;
|
|
|
|
// Preview post aims to have similar post previewing experience as WordPress block editor.
|
|
// How preview post works in WordPress block editor:
|
|
// 1. Preview button is clicked.
|
|
// 2. REST request is sent to `/wp-json/wp/v2/pages/POST_ID/autosaves?_locale=user` endpoint.
|
|
// 3. An autosave post for current post is created. Autosave post is another record on `wp_post` table that:
|
|
// - has post_status: `inherit`
|
|
// - has post_type: `revision`
|
|
// - has post_parent pointing to the published / draft post ID
|
|
// 4. Basically the autosave post is the most recent revision of the post. A user commonly can have only have
|
|
// one autosave per post.
|
|
// 5. Once autosave post is created, the preview post URL is generated and returned to the client.
|
|
// 6. User is being redirected to the preview post URL
|
|
// For WordPress block editor's preview post mechanism, see: `WP_REST_Autosaves_Controller->create_item()`.
|
|
|
|
if ( $is_saving_preview ) {
|
|
// Create new autosave as post revision that will be used for preview.
|
|
$revision_id = Revision::put_post_revision(
|
|
[
|
|
'ID' => $post_id,
|
|
'post_title' => $post_content_page_settings['postTitle'] ?? '',
|
|
|
|
// `_wp_put_post_revision()` already does the `wp_slash()` for the content. No need apply `wp_slash()` here.
|
|
'post_content' => $post_content,
|
|
],
|
|
true
|
|
);
|
|
|
|
// Create preview post link.
|
|
// Equivalent of this on WordPress block editor can be seen at WP_REST_Autosaves_Controller->prepare_item_for_response().
|
|
$preview_query_args = [
|
|
'preview_id' => $post_id,
|
|
'preview_nonce' => wp_create_nonce( 'post_preview_' . $post_id ),
|
|
];
|
|
|
|
if ( ET_BUILDER_LAYOUT_POST_TYPE === get_post_type( $post_id ) ) {
|
|
$preview_query_args['p'] = $post_id;
|
|
}
|
|
|
|
$preview_url = get_preview_post_link( $post_id, $preview_query_args );
|
|
|
|
return self::response_success(
|
|
[
|
|
'previewId' => $revision_id,
|
|
'previewUrl' => $preview_url,
|
|
'syncType' => 'preview',
|
|
'post_status' => $post_status,
|
|
'save_verification' => $revision_id ? true : false,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Filters whether to proceed with the post save operation.
|
|
*
|
|
* This filter allows modules to intercept the save process and prevent
|
|
* the default wp_update_post() call. If the filter returns an array,
|
|
* that array will be returned as the success response and no save will occur.
|
|
* If the filter returns null, the save will proceed normally.
|
|
*
|
|
* @since ??
|
|
*
|
|
* @param array|null $abort_save_response Response array to return if save should be aborted, or null to proceed.
|
|
* @param int $post_id The post ID being saved.
|
|
* @param string $post_content The post content being saved.
|
|
* @param string $post_status The post status being saved.
|
|
* @param array $options The options array containing conditional_tags and other metadata.
|
|
* @param array $post_content_page_settings The post content page settings array (if any).
|
|
*/
|
|
$abort_save_response = apply_filters(
|
|
'divi_visual_builder_rest_before_save_post',
|
|
null,
|
|
$post_id,
|
|
$post_content,
|
|
$post_status,
|
|
$options,
|
|
$post_content_page_settings
|
|
);
|
|
|
|
if ( is_array( $abort_save_response ) ) {
|
|
// A filter has intercepted the save and provided a custom response.
|
|
return self::response_success( $abort_save_response );
|
|
}
|
|
|
|
// Build the post update array.
|
|
$post_update_data = [
|
|
'ID' => $post_id,
|
|
'post_status' => $post_status,
|
|
];
|
|
|
|
// Ignore invalid empty-string post content payloads, while still allowing status/title/excerpt updates.
|
|
if ( $has_post_content_payload ) {
|
|
$post_update_data['post_content'] = wp_slash( $post_content );
|
|
}
|
|
|
|
// Include post_title and post_excerpt in this update to avoid a second wp_update_post() call.
|
|
// This prevents duplicate revisions and duplicate filter processing.
|
|
// Note: wp_update_post() internally calls sanitize_post() which sanitizes both fields,
|
|
// but we sanitize here for consistency with the existing approach in et_builder_update_settings().
|
|
if ( $post_content_page_settings ) {
|
|
if ( isset( $post_content_page_settings['postTitle'] ) ) {
|
|
$post_update_data['post_title'] = sanitize_text_field( $post_content_page_settings['postTitle'] );
|
|
}
|
|
|
|
if ( isset( $post_content_page_settings['postExcerpt'] ) ) {
|
|
// Use wp_kses_post for excerpt to allow certain HTML tags (same as et_builder_update_settings).
|
|
$post_update_data['post_excerpt'] = wp_kses_post( $post_content_page_settings['postExcerpt'] );
|
|
}
|
|
}
|
|
|
|
// Persist main post content/meta only for singular VB saves, plus builder-enabled WooCommerce shop pages
|
|
// that hydrate post content while keeping `mainLoopType` as `post_type_archive` (#52168).
|
|
// On other non-singular pages (home/archive), request post_id may be the first main-query loop post
|
|
// and must not be converted to Divi (#49679).
|
|
$main_post = get_post( $post_id );
|
|
$should_persist_main_post = $main_post instanceof \WP_Post
|
|
&& self::_should_persist_main_post_content( $post_id, $main_loop_type );
|
|
|
|
if (
|
|
$should_persist_main_post
|
|
&& $has_post_content_payload
|
|
&& GlobalLayout::is_global_layout_template( $post_id )
|
|
&& GlobalLayout::has_component_reference_cycle( $post_id, $post_content )
|
|
) {
|
|
return self::response_error(
|
|
'divi_component_reference_cycle',
|
|
esc_html__( 'A Component cannot be placed inside itself.', 'et_builder_5' ),
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
|
|
$update = $should_persist_main_post ? wp_update_post( $post_update_data ) : 0;
|
|
|
|
$layout_updates = [
|
|
[
|
|
'layout' => 'postContent',
|
|
'id' => $post_content_post_id,
|
|
'content' => $post_content,
|
|
],
|
|
[
|
|
'layout' => 'header',
|
|
'id' => $header_post_id,
|
|
'content' => $header_content,
|
|
],
|
|
[
|
|
'layout' => 'body',
|
|
'id' => $body_post_id,
|
|
'content' => $body_content,
|
|
],
|
|
[
|
|
'layout' => 'footer',
|
|
'id' => $footer_post_id,
|
|
'content' => $footer_content,
|
|
],
|
|
];
|
|
$updated_theme_builder_layout_post_ids = [];
|
|
|
|
foreach ( $layout_updates as $layout_update ) {
|
|
$target_layout = $layout_update['layout'] ?? 'postContent';
|
|
$target_post_id = absint( $layout_update['id'] ?? 0 );
|
|
$target_content = $layout_update['content'] ?? '';
|
|
|
|
// Main post content is already updated above.
|
|
if ( $post_id === $target_post_id ) {
|
|
continue;
|
|
}
|
|
|
|
$layout_update_result = self::_update_layout_post_content( $target_post_id, $target_content );
|
|
|
|
if ( is_wp_error( $layout_update_result ) ) {
|
|
return self::response_error(
|
|
$layout_update_result->get_error_code(),
|
|
$layout_update_result->get_error_message(),
|
|
$layout_update_result->get_error_data()
|
|
);
|
|
}
|
|
|
|
if ( 0 !== (int) $layout_update_result && in_array( $target_layout, [ 'header', 'body', 'footer' ], true ) ) {
|
|
$updated_theme_builder_layout_post_ids[] = $target_post_id;
|
|
}
|
|
}
|
|
|
|
// Ensure Theme Builder layout saves clear template-targeted frontend caches.
|
|
// In non-singular Visual Builder contexts, the main request post ID is not a layout post,
|
|
// so cache clearing must also run for explicitly saved layout post IDs.
|
|
if ( ! empty( $updated_theme_builder_layout_post_ids ) ) {
|
|
foreach ( array_unique( $updated_theme_builder_layout_post_ids ) as $updated_layout_post_id ) {
|
|
et_theme_builder_clear_wp_post_cache( $updated_layout_post_id );
|
|
}
|
|
}
|
|
|
|
// Save page settings by layout post ID.
|
|
if ( ! empty( $page_settings_by_layout ) ) {
|
|
self::_save_page_settings_by_layout(
|
|
$page_settings_by_layout,
|
|
[
|
|
'postContent' => $post_content_post_id,
|
|
'header' => $header_post_id,
|
|
'body' => $body_post_id,
|
|
'footer' => $footer_post_id,
|
|
],
|
|
$post_id
|
|
);
|
|
}
|
|
|
|
// Save non-singular page settings (term properties, author properties, etc.).
|
|
// The frontend syncs field edits back into `mainLoopSettingsData`, so the
|
|
// updated values arrive here directly — no separate property needed.
|
|
if ( 'singular' !== $main_loop_type && is_array( $loop_settings_data ) ) {
|
|
$expected_main_loop_owner_context = self::_get_expected_main_loop_owner_context( $main_loop_type );
|
|
|
|
SavingUtility::save_non_singular_page_settings(
|
|
$loop_settings_data,
|
|
$main_loop_type,
|
|
$expected_main_loop_owner_context
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Action hook to fire when the Post is being saved.
|
|
*
|
|
* This is for backward compatibility with hooks written for Divi version <5.0.0.
|
|
*
|
|
* @since 3.20
|
|
* @deprecated 5.0.0 Use `divi_visual_builder_rest_save_post` hook instead.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
*/
|
|
do_action(
|
|
'et_save_post',
|
|
$post_id
|
|
);
|
|
|
|
/**
|
|
* Action hook to fire when the Post is being saved.
|
|
*
|
|
* @since ??
|
|
*
|
|
* @param int $post_id Post ID.
|
|
*/
|
|
// Pass request context into save hooks.
|
|
// This avoids relying on globals and is extendable for future non-singular owners.
|
|
do_action(
|
|
'divi_visual_builder_rest_save_post',
|
|
$post_id,
|
|
[
|
|
'mainLoopType' => $main_loop_type,
|
|
'mainLoopSettingsData' => $loop_settings_data,
|
|
]
|
|
);
|
|
|
|
// Prime page cache AFTER off-canvas data and other post meta is saved.
|
|
// This ensures that when the cache priming request triggers early detection,
|
|
// all post meta (including appended canvas content) is available.
|
|
SavingUtility::prime_page_cache_on_save( $post_id, $prime_cache_url );
|
|
|
|
if ( ( $update && ! is_wp_error( $update ) ) || ( ! $should_persist_main_post && ! is_wp_error( $update ) ) ) {
|
|
// Update post meta so we know D5 is used and Readiness migrator will skip it.
|
|
if ( $should_persist_main_post ) {
|
|
update_post_meta( $post_id, '_et_pb_use_divi_5', 'on' );
|
|
// Also set the legacy meta for backward compatibility with D4 components.
|
|
update_post_meta( $post_id, '_et_pb_use_builder', 'on' );
|
|
|
|
// Clear the page creation flow flag so it doesn't show again on subsequent loads.
|
|
update_post_meta( $post_id, '_et_pb_show_page_creation', 'off' );
|
|
}
|
|
|
|
$layout_ids_to_mark = [
|
|
$post_content_post_id,
|
|
$header_post_id,
|
|
$body_post_id,
|
|
$footer_post_id,
|
|
];
|
|
|
|
foreach ( $layout_ids_to_mark as $layout_id ) {
|
|
$layout_id = absint( $layout_id );
|
|
|
|
if ( 0 === $layout_id || $post_id === $layout_id ) {
|
|
continue;
|
|
}
|
|
|
|
update_post_meta( $layout_id, '_et_pb_use_divi_5', 'on' );
|
|
update_post_meta( $layout_id, '_et_pb_use_builder', 'on' );
|
|
}
|
|
|
|
/**
|
|
* Action hook to fire when the Post is updated.
|
|
*
|
|
* This is for backward compatibility with hooks written for Divi version <5.0.0.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
*
|
|
* @since 3.29
|
|
* @deprecated 5.0.0 Use `divi_visual_builder_rest_update_post` hook instead.
|
|
*/
|
|
do_action(
|
|
'et_update_post',
|
|
$post_id
|
|
);
|
|
|
|
/**
|
|
* Action hook to fire when the Post is updated.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
*
|
|
* @since ??
|
|
*/
|
|
do_action( 'divi_visual_builder_rest_update_post', $post_id );
|
|
|
|
$saved_post_content = $should_persist_main_post ? get_post_field( 'post_content', $update ) : $post_content;
|
|
$verification = $should_persist_main_post
|
|
? self::_verify_post_content_matches_after_sanitization( $post_content, $saved_post_content )
|
|
: true;
|
|
|
|
/**
|
|
* Filter to modify the save verification result.
|
|
*
|
|
* @since ??
|
|
* @deprecated 5.0.0 Use the {@see 'divi_visual_builder_rest_save_verification'} filter instead.
|
|
*
|
|
* @param bool $verification Whether to save the verification result.
|
|
*/
|
|
$verification = apply_filters(
|
|
'et_fb_ajax_save_verification_result',
|
|
$verification
|
|
);
|
|
|
|
/**
|
|
* Filter to modify the save verification result.
|
|
*
|
|
* @since ??
|
|
*
|
|
* @param bool $verification Whether to save the verification result.
|
|
*/
|
|
$save_verification_filtered = apply_filters(
|
|
'divi_visual_builder_rest_save_post_save_verification',
|
|
$verification
|
|
);
|
|
|
|
$return_rendered_content = $options['return_rendered_content'] ?? false;
|
|
|
|
if ( $return_rendered_content && $should_persist_main_post ) {
|
|
// Replace dynamic data in the content with the actual value.
|
|
$normalized_content = DynamicData::get_processed_dynamic_data( $saved_post_content, $post_id, true );
|
|
|
|
// Apply the_content filter to the content.
|
|
$rendered_content = apply_filters( 'the_content', $normalized_content );
|
|
} else {
|
|
$rendered_content = $saved_post_content;
|
|
}
|
|
|
|
return self::response_success(
|
|
[
|
|
'post_status' => $should_persist_main_post ? get_post_status( $update ) : $post_status,
|
|
'save_verification' => $save_verification_filtered,
|
|
'rendered_content' => $rendered_content,
|
|
]
|
|
);
|
|
}
|
|
|
|
if ( is_wp_error( $update ) ) {
|
|
return self::response_error( $update->get_error_code(), $update->get_error_message(), $update->get_error_data() );
|
|
}
|
|
|
|
return self::response_error( 'unknown_error', esc_html__( 'Unknown error.', 'et_builder_5' ) );
|
|
}
|