Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
144 lines
5.6 KiB
Plaintext
144 lines
5.6 KiB
Plaintext
function sync( WP_REST_Request $request ) {
|
|
$is_chunked = $request->get_param( 'isChunked' );
|
|
$chunk_index = $request->get_param( 'chunkIndex' );
|
|
$total_chunks = $request->get_param( 'totalChunks' );
|
|
$is_last_chunk = $request->get_param( 'isLastChunk' );
|
|
$request_id = $request->get_param( 'requestId' );
|
|
$action_type = $request->get_param( 'actionType' ) ?? '';
|
|
|
|
// Validate chunked request: if isChunked is true, all chunking parameters must be present.
|
|
if ( true === $is_chunked ) {
|
|
if ( null === $chunk_index || null === $total_chunks || null === $is_last_chunk || null === $request_id ) {
|
|
return RESTController::response_error(
|
|
'invalid_chunked_request',
|
|
esc_html__( 'Invalid chunked request: missing required chunking parameters.', 'et_builder_5' ),
|
|
[
|
|
'status' => 400,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate non-chunked request: if isChunked is false (or null/defaults to false), chunking parameters must not be present.
|
|
if ( false === $is_chunked || null === $is_chunked ) {
|
|
if ( null !== $chunk_index || null !== $total_chunks || null !== $is_last_chunk || null !== $request_id ) {
|
|
return RESTController::response_error(
|
|
'invalid_non_chunked_request',
|
|
esc_html__( 'Invalid non-chunked request: chunking parameters should not be present.', 'et_builder_5' ),
|
|
[
|
|
'status' => 400,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
// Handle chunked requests: accumulate chunks before processing.
|
|
// Check explicit isChunked flag first, then verify all required chunking parameters are present.
|
|
if ( true === $is_chunked ) {
|
|
// Get transient key for this request ID.
|
|
$transient_key = self::_get_chunk_transient_key( $request_id );
|
|
|
|
// Get existing accumulated chunks from transient.
|
|
$accumulated_data = get_transient( $transient_key );
|
|
|
|
// Initialize accumulation for this request ID if not exists.
|
|
if ( false === $accumulated_data ) {
|
|
$accumulated_data = [
|
|
'chunks' => [],
|
|
'actionType' => $action_type,
|
|
'totalChunks' => $total_chunks,
|
|
];
|
|
}
|
|
|
|
// Store this chunk.
|
|
$chunk_presets = $request->get_param( 'presets' );
|
|
$accumulated_data['chunks'][ $chunk_index ] = $chunk_presets;
|
|
|
|
// Update transient with new chunk data.
|
|
set_transient( $transient_key, $accumulated_data, self::CHUNK_TRANSIENT_EXPIRATION );
|
|
|
|
// If this is not the last chunk, return early with success (chunk received).
|
|
if ( ! $is_last_chunk ) {
|
|
return RESTController::response_success( (object) [] );
|
|
}
|
|
|
|
// This is the last chunk - verify we received all expected chunks before merging.
|
|
$accumulated_chunks = $accumulated_data['chunks'];
|
|
$received_chunk_count = count( $accumulated_chunks );
|
|
|
|
// Verify we have all chunks (chunk indices are 0-based, so total should match).
|
|
if ( $received_chunk_count !== $total_chunks ) {
|
|
// Clean up transient on error.
|
|
delete_transient( $transient_key );
|
|
|
|
return RESTController::response_error(
|
|
'incomplete_chunks',
|
|
sprintf(
|
|
// translators: %1$d: received chunks, %2$d: expected chunks.
|
|
esc_html__( 'Incomplete chunk data received. Expected %1$d chunks, received %2$d chunks.', 'et_builder_5' ),
|
|
$total_chunks,
|
|
$received_chunk_count
|
|
),
|
|
[
|
|
'status' => 400,
|
|
'expectedChunks' => $total_chunks,
|
|
'receivedChunks' => $received_chunk_count,
|
|
'receivedIndices' => array_keys( $accumulated_chunks ),
|
|
]
|
|
);
|
|
}
|
|
|
|
// Merge all chunks and process.
|
|
$merged_presets = self::_merge_chunks( $accumulated_chunks );
|
|
|
|
// Clean up transient after successful merge.
|
|
delete_transient( $transient_key );
|
|
|
|
// Use merged presets for processing.
|
|
$prepared_data = GlobalPreset::prepare_data( $merged_presets );
|
|
} else {
|
|
// Non-chunked request - process normally.
|
|
$prepared_data = GlobalPreset::prepare_data( $request->get_param( 'presets' ) );
|
|
}
|
|
|
|
// CRITICAL SAFETY CHECK: Validate that we're not accidentally losing presets during sync.
|
|
// Only DELETE actions should reduce the preset count. All other operations (save, add, update)
|
|
// should preserve or increase the preset count.
|
|
// This prevents bugs where incomplete restore points or data corruption causes preset loss.
|
|
// We compare against the current database state (source of truth), not client-side state.
|
|
$current_presets = GlobalPreset::get_data();
|
|
$validation_error = GlobalPreset::validate_preset_count( $current_presets, $prepared_data, $action_type );
|
|
|
|
if ( is_wp_error( $validation_error ) ) {
|
|
// Clean up transient on validation error.
|
|
if ( null !== $request_id ) {
|
|
$transient_key = self::_get_chunk_transient_key( $request_id );
|
|
delete_transient( $transient_key );
|
|
}
|
|
|
|
return $validation_error;
|
|
}
|
|
|
|
// Handle deletion cleanup for D4 legacy presets.
|
|
// When a preset is deleted in D5, we need to also remove it from the D4 legacy option
|
|
// to prevent re-migration on page refresh.
|
|
if ( 'DELETE_MODULE_PRESET' === $action_type || 'DELETE_OPTION_GROUP_PRESET' === $action_type ) {
|
|
$preset_type = 'DELETE_MODULE_PRESET' === $action_type ? 'module' : 'group';
|
|
|
|
// Get deleted preset IDs by comparing current (before deletion) vs incoming (after deletion).
|
|
$deleted_presets = GlobalPreset::get_deleted_preset_ids( $current_presets, $prepared_data, $preset_type );
|
|
|
|
// Remove each deleted preset from the D4 legacy option.
|
|
foreach ( $deleted_presets as $deleted_preset ) {
|
|
GlobalPreset::remove_preset_from_legacy_option(
|
|
$deleted_preset['id'],
|
|
$deleted_preset['moduleName']
|
|
);
|
|
}
|
|
}
|
|
|
|
$saved_data = GlobalPreset::save_data( $prepared_data );
|
|
|
|
return RESTController::response_success( (object) $saved_data );
|
|
}
|