diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php index 56d3b5b1e98..17ceac62783 100644 --- a/plugins/woocommerce/src/Container.php +++ b/plugins/woocommerce/src/Container.php @@ -32,6 +32,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\BatchP use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\LayoutTemplatesServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ComingSoonServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\StatsServiceProvider; +use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ImportExportServiceProvider; /** * PSR11 compliant dependency injection container for WooCommerce. @@ -83,6 +84,7 @@ final class Container { EnginesServiceProvider::class, ComingSoonServiceProvider::class, StatsServiceProvider::class, + ImportExportServiceProvider::class, ); /** diff --git a/plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php b/plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php new file mode 100644 index 00000000000..c1759bc5e93 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php @@ -0,0 +1,175 @@ +get_import_subdir_name(); + if ( $create ) { + FilesystemUtil::mkdir_p_not_indexable( $upload_dir ); + } + return $upload_dir; + } + + /** + * Handles a CSV file upload. + * + * @param string $import_type Type of upload or context. + * @param string $files_index $_FILES index that contains the file to upload. + * @param array|null $allowed_mime_types List of allowed MIME types. + * @return array { + * Details for the uploaded file. + * + * @type int $id Attachment ID. + * @type string $file Full path to uploaded file. + * } + * + * @throws \Exception In case of error. + */ + public function handle_csv_upload( string $import_type, string $files_index = 'import', ?array $allowed_mime_types = null ): array { + $import_type = sanitize_key( $import_type ); + if ( ! $import_type ) { + throw new \Exception( 'Import type is invalid.' ); + } + + if ( ! $allowed_mime_types ) { + $allowed_mime_types = array( + 'csv' => 'text/csv', + 'txt' => 'text/plain', + ); + } + + $file = $_FILES[ $files_index ] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing + if ( ! isset( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) { + throw new \Exception( esc_html__( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) ); + } + + if ( ! function_exists( 'wp_import_handle_upload' ) ) { + require_once ABSPATH . 'wp-admin/includes/import.php'; + } + + // Make sure upload dir exists. + $this->get_import_dir(); + + // Add prefix. + $file['name'] = $import_type . '-' . $file['name']; + + $overrides_callback = function ( $overrides_ ) use ( $allowed_mime_types ) { + $overrides_['test_form'] = false; + $overrides_['test_type'] = true; + $overrides_['mimes'] = $allowed_mime_types; + return $overrides_; + }; + + self::add_filter( 'upload_dir', array( $this, 'override_upload_dir' ) ); + self::add_filter( 'wp_unique_filename', array( $this, 'override_unique_filename' ), 0, 2 ); + self::add_filter( 'wp_handle_upload_overrides', $overrides_callback, 999 ); + self::add_filter( 'wp_handle_upload_prefilter', array( $this, 'remove_txt_from_uploaded_file' ), 0 ); + + $orig_files_import = $_FILES['import'] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing + $_FILES['import'] = $file; // wp_import_handle_upload() expects the file to be in 'import'. + + $upload = wp_import_handle_upload(); + + remove_filter( 'upload_dir', array( $this, 'override_upload_dir' ) ); + remove_filter( 'wp_unique_filename', array( $this, 'override_unique_filename' ), 0 ); + remove_filter( 'wp_handle_upload_overrides', $overrides_callback, 999 ); + remove_filter( 'wp_handle_upload_prefilter', array( $this, 'remove_txt_from_uploaded_file' ), 0 ); + + if ( $orig_files_import ) { + $_FILES['import'] = $orig_files_import; + } else { + unset( $_FILES['import'] ); + } + + if ( ! empty( $upload['error'] ) ) { + throw new \Exception( esc_html( $upload['error'] ) ); + } + + if ( ! wc_is_file_valid_csv( $upload['file'], false ) ) { + wp_delete_attachment( $file['id'], true ); + throw new \Exception( esc_html__( 'Invalid file type for a CSV import.', 'woocommerce' ) ); + } + + return $upload; + } + + /** + * Hooked onto 'upload_dir' to override the default upload directory for a CSV upload. + * + * @param array $uploads WP upload dir details. + * @return array + */ + private function override_upload_dir( $uploads ): array { + $new_subdir = '/' . $this->get_import_subdir_name(); + + $uploads['path'] = $uploads['basedir'] . $new_subdir; + $uploads['url'] = $uploads['baseurl'] . $new_subdir; + $uploads['subdir'] = $new_subdir; + + return $uploads; + } + + /** + * Adds a random string to the name of an uploaded CSV file to make it less discoverable. Hooked onto 'wp_unique_filename'. + * + * @param string $filename File name. + * @param string $ext File extension. + * @return string + */ + private function override_unique_filename( string $filename, string $ext ): string { + $length = min( 10, 255 - strlen( $filename ) - 1 ); + if ( 1 < $length ) { + $suffix = strtolower( wp_generate_password( $length, false, false ) ); + $filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) ) . '-' . $suffix . $ext; + } + + return $filename; + } + + /** + * `wp_import_handle_upload()` appends .txt to any file name. This function is hooked onto 'wp_handle_upload_prefilter' + * to remove those extra characters. + * + * @param array $file File details in the form of a $_FILES entry. + * @return array Modified file details. + */ + private function remove_txt_from_uploaded_file( array $file ): array { + $file['name'] = substr( $file['name'], 0, -4 ); + return $file; + } +} diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php new file mode 100644 index 00000000000..58984a41fbf --- /dev/null +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php @@ -0,0 +1,35 @@ +share( CSVUploadHelper::class ); + } +}