get_blacklist_file_ext(), true ); } public function __construct() { register_rest_route( GROUP . '/v1', '/dropzone/upload', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'upload' ], 'permission_callback' => '__return_true', 'args' => [ 'file_name' => [ 'required' => true, 'sanitize_callback' => 'sanitize_file_name', ], 'uniq_id' => [ 'required' => true, 'sanitize_callback' => 'sanitize_key', ], 'chunk_index' => [ 'required' => true, 'sanitize_callback' => 'absint', ], 'total_chunks' => [ 'required' => true, 'sanitize_callback' => 'absint', ], ], ] ); } /** * Ensure the given absolute path stays inside the upload temporary directory. * * @param string $path * @param string $dir * * @return bool */ private function is_path_within_dir( string $path, string $dir ): bool { $path = wp_normalize_path( $path ); $dir = wp_normalize_path( trailingslashit( $dir ) ); return strpos( $path, $dir ) === 0; } /** * @param WP_REST_Request $request * * @return WP_Error|WP_HTTP_Response|WP_REST_Response */ public function upload( WP_REST_Request $request ) { $params = $request->get_params(); $files = $request->get_file_params(); // Get file data from the request $file_name = sanitize_file_name( (string) $params['file_name'] ); $uniq_id = sanitize_key( (string) $params['uniq_id'] ); $chunk_index = (int) $params['chunk_index']; $total_chunks = (int) $params['total_chunks']; // Basic sanity checks on the supplied parameters. if ( '' === $file_name || '' === $uniq_id || $total_chunks < 1 ) { return new WP_Error( 'invalid_parameters', esc_attr__( 'Invalid upload parameters.', 'advanced-fields-for-elementor-forms' ), [ 'status' => 400 ] ); } // Reject file names with forbidden extensions. if ( $this->has_forbidden_extension( $file_name ) ) { return new WP_Error( 'forbidden_extension', esc_attr__( 'This file type is not allowed.', 'advanced-fields-for-elementor-forms' ), [ 'status' => 400 ] ); } // Reject unreasonable chunk indexes / chunk counts. $max_chunks = $this->get_max_chunks(); if ( $chunk_index < 0 || $chunk_index >= $max_chunks || $total_chunks > $max_chunks ) { return new WP_Error( 'invalid_chunk_count', esc_attr__( 'Invalid chunk parameters.', 'advanced-fields-for-elementor-forms' ), [ 'status' => 400 ] ); } // Ensure a valid uploaded file chunk was provided. if ( empty( $files['file_chunk'] ) || empty( $files['file_chunk']['tmp_name'] ) ) { return new WP_Error( 'missing_file_chunk', esc_attr__( 'No file chunk provided.', 'advanced-fields-for-elementor-forms' ), [ 'status' => 400 ] ); } // Enforce a per-chunk size limit to prevent disk exhaustion. $chunk_size = wp_filesize( $files['file_chunk']['tmp_name'] ); $max_chunk_size = $this->get_max_chunk_size(); if ( $chunk_size < 0 || $chunk_size > $max_chunk_size ) { return new WP_Error( 'file_too_large', esc_attr__( 'The uploaded chunk exceeds the allowed size.', 'advanced-fields-for-elementor-forms' ), [ 'status' => 400 ] ); } $file_system = Filesystem::getInstance(); // Create a temporary directory if not exists $temp_dir = trailingslashit( FileUtil::upload_tmp_dir() ); if ( ! $file_system->exists( $temp_dir ) && ! $file_system->mkdir( $temp_dir, 0755 ) ) { return new WP_REST_Response( [ 'message' => esc_attr__( 'Failed to create temporary directory', 'advanced-fields-for-elementor-forms' ) ], 400 ); } // The file name has been sanitized (no path separators or traversal) and // we additionally verify the resolved path stays inside the temp dir. $temp_file_path = sprintf( '%s-%s.part%d', path_join( $temp_dir, $file_name ), $uniq_id, $chunk_index ); if ( ! $this->is_path_within_dir( $temp_file_path, $temp_dir ) ) { return new WP_Error( 'invalid_file_name', esc_attr__( 'Invalid file name.', 'advanced-fields-for-elementor-forms' ), [ 'status' => 400 ] ); } // Save the chunk $chunk_data = $file_system->get_contents( $files['file_chunk']['tmp_name'] ); if ( ! $file_system->put_contents( $temp_file_path, $chunk_data ) ) { return new WP_REST_Response( [ 'message' => esc_attr__( 'Failed to upload chunk!', 'advanced-fields-for-elementor-forms' ) ], 400 ); } // Check if all chunks are uploaded if ( ($chunk_index + 1) === $total_chunks ) { // Merge chunks using WP_Filesystem methods. $temp_file = wp_tempnam( $file_name, $temp_dir ); $merged_content = ''; for ( $i = 0; $i < $total_chunks; $i++ ) { $chunk_path = sprintf( '%s-%s.part%d', path_join( $temp_dir, $file_name ), $uniq_id, $i ); $chunk_data = $file_system->get_contents( $chunk_path ); $merged_content .= $chunk_data; $file_system->delete( $chunk_path ); // Remove chunk after merging // make sure the temp file is deleted on shutdown register_shutdown_function( // Remove chunk after merging static fn() => $file_system->delete( $chunk_path ) ); } if ( ! $file_system->put_contents( $temp_file, $merged_content ) ) { return new WP_REST_Response( [ 'message' => esc_attr__( 'Failed to upload file!', 'advanced-fields-for-elementor-forms' ) ], 400 ); } unset( $merged_content ); $result = [ 'message' => esc_attr__( 'File uploaded successfully', 'advanced-fields-for-elementor-forms' ), 'tempName' => basename( $temp_file ), 'name' => $file_name, ]; $result['value'] = wp_json_encode( [ 'tempName' => $result['tempName'], 'name' => $result['name'], ] ); return rest_ensure_response( $result ); } return rest_ensure_response( [ 'message' => esc_attr__( 'Chunk uploaded successfully', 'advanced-fields-for-elementor-forms' ), ] ); } }