diff --git a/includes/admin/class-wc-admin-importers.php b/includes/admin/class-wc-admin-importers.php index dad22a15b74..7b68287beff 100644 --- a/includes/admin/class-wc-admin-importers.php +++ b/includes/admin/class-wc-admin-importers.php @@ -51,10 +51,10 @@ class WC_Admin_Importers { } // includes - require( dirname( __FILE__ ) . '/importers/class-wc-product-importer.php' ); + require( dirname( __FILE__ ) . '/importers/class-wc-product-wp-importer.php' ); // Dispatch - $importer = new WC_Product_Importer(); + $importer = new WC_Product_WP_Importer(); $importer->dispatch(); } diff --git a/includes/admin/importers/class-wc-product-importer.php b/includes/admin/importers/class-wc-product-wp-importer.php similarity index 62% rename from includes/admin/importers/class-wc-product-importer.php rename to includes/admin/importers/class-wc-product-wp-importer.php index 03adadbeb5b..93429b5bd85 100644 --- a/includes/admin/importers/class-wc-product-importer.php +++ b/includes/admin/importers/class-wc-product-wp-importer.php @@ -15,7 +15,7 @@ if ( ! class_exists( 'WP_Importer' ) ) { * @package WooCommerce/Admin/Importers * @version 3.1.0 */ -class WC_Product_Importer extends WP_Importer { +class WC_Product_WP_Importer extends WP_Importer { /** * The current file id. @@ -59,6 +59,7 @@ class WC_Product_Importer extends WP_Importer { * Manages the three separate stages of the CSV import process. */ public function dispatch() { + include_once( WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php' ); $this->header(); @@ -135,15 +136,30 @@ class WC_Product_Importer extends WP_Importer { $args['mapping'] = wp_unslash( $_POST['map_to'] ); } - $data = $this->read_csv( $file, $args ); + $importer = $this->get_importer( $file, $args ); + $data = $importer->import(); + $imported = count( $data['imported'] ); + $failed = count( $data['failed'] ); - // Show Result - echo '

'; - /* translators: %d: products count */ - printf( - __( 'Import complete - imported %s products.', 'woocommerce' ), - '' . count( $data ) . '' + $results = sprintf( + /* translators: %d: products count */ + _n( 'Imported %s product.', 'Imported %s products.', $imported, 'woocommerce' ), + '' . number_format_i18n( $imported ) . '' ); + + // @todo create a view to display errors or log with WC_Logger. + if ( 0 < $failed ) { + $results .= ' ' . sprintf( + /* translators: %d: products count */ + _n( 'Failed %s product.', 'Failed %s products.', $failed, 'woocommerce' ), + '' . number_format_i18n( $failed ) . '' + ); + } + + // Show result. + echo '

'; + /* translators: %d: import results */ + printf( __( 'Import complete: %s', 'woocommerce' ), $results ); echo '

'; $this->import_end(); @@ -184,286 +200,6 @@ class WC_Product_Importer extends WP_Importer { return true; } - /** - * Read a CSV file. - * - * @param mixed $file - * @param array $args See $default_args - * @return array - */ - public function read_csv( $file, $args = array() ) { - - $default_args = array( - 'start_pos' => 0, // File pointer start. - 'end_pos' => -1, // File pointer end. - 'lines' => -1, // Max lines to read. - 'mapping' => array(), // Column mapping. csv_heading => schema_heading. - 'parse' => false, // Whether to sanitize and format data. - ); - $args = wp_parse_args( $args, $default_args ); - - $data = array( - 'raw_headers' => array(), - 'data' => array(), - ); - - if ( false !== ( $handle = fopen( $file, 'r' ) ) ) { - - $data['raw_headers'] = fgetcsv( $handle, 0, $this->delimiter ); - - if ( 0 !== $args['start_pos'] ) { - fseek( $handle, (int) $args['start_pos'] ); - } - - while ( false !== ( $row = fgetcsv( $handle, 0, $this->delimiter ) ) ) { - $data['data'][] = $row; - - if ( ( $args['end_pos'] > 0 && ftell( $handle ) >= $args['end_pos'] ) || 0 === --$args['lines'] ) { - break; - } - } - } - - if ( ! empty( $args['mapping'] ) ) { - $data = $this->map_headers( $data, $args['mapping'] ); - } - - if ( $args['parse'] ) { - $data = $this->parse_data( $data ); - } - - return apply_filters( 'woocommerce_csv_product_import_data', $data, $file, $args ); - } - - /** - * Map raw headers to known headers. - * - * @param array $data - * @param array $mapping 'raw column name' => 'schema column name' - * @return array - */ - public function map_headers( $data, $mapping ) { - $data['headers'] = array(); - foreach ( $data['raw_headers'] as $heading ) { - $data['headers'][] = isset( $mapping[ $heading ] ) ? $mapping[ $heading ] : $heading; - } - - return $data; - } - - /** - * Map and format raw data to known fields. - * - * @param array $data - * @return array - */ - public function parse_data( $data ) { - - /** - * Columns not mentioned here will get parsed with 'esc_attr'. - * column_name => callback. - */ - $data_formatting = array( - 'id' => 'absint', - 'status' => array( $this, 'parse_bool_field' ), - 'featured' => array( $this, 'parse_bool_field' ), - 'date_on_sale_from' => 'strtotime', - 'date_on_sale_to' => 'strtotime', - 'manage_stock' => array( $this, 'parse_bool_field' ), - 'backorders' => array( $this, 'parse_bool_field' ), - 'sold_individually' => array( $this, 'parse_bool_field' ), - 'width' => array( $this, 'parse_float_field' ), - 'length' => array( $this, 'parse_float_field' ), - 'height' => array( $this, 'parse_float_field' ), - 'weight' => array( $this, 'parse_float_field' ), - 'reviews_allowed' => array( $this, 'parse_bool_field' ), - 'purchase_note' => 'wp_kses_post', - 'price' => 'wc_format_decimal', - 'regular_price' => 'wc_format_decimal', - 'stock_quantity' => 'absint', - 'category_ids' => array( $this, 'parse_categories' ), - 'tag_ids' => array( $this, 'parse_comma_field' ), - 'images' => array( $this, 'parse_comma_field' ), - 'upsell_ids' => array( $this, 'parse_comma_field' ), - 'cross_sell_ids' => array( $this, 'parse_comma_field' ), - 'download_limit' => 'absint', - 'download_expiry' => 'absint', - ); - /** - * @todo switch these to some standard, slug format. - */ - $regex_match_data_formatting = array( - '/Attribute * Value\(s\)/' => array( $this, 'parse_comma_field' ), - '/Attribute * Visible/' => array( $this, 'parse_bool_field' ), - '/Download * URL/' => 'esc_url', - ); - - $headers = ! empty( $data['headers'] ) ? $data['headers'] : $data['raw_headers']; - $parse_functions = array(); - $parsed_data = array(); - - // Figure out the parse function for each column. - foreach ( $headers as $index => $heading ) { - - $parse_function = 'esc_attr'; - if ( isset( $data_formatting[ $heading ] ) ) { - $parse_function = $data_formatting[ $heading ]; - } else { - foreach ( $regex_match_data_formatting as $regex => $callback ) { - if ( preg_match( $regex, $heading ) ) { - $parse_function = $callback; - break; - } - } - } - - $parse_functions[] = $parse_function; - } - - // Parse the data. - foreach ( $data['data'] as $row ) { - $item = array(); - foreach ( $row as $index => $field ) { - $item[ $headers[ $index ] ] = call_user_func( $parse_functions[ $index ], $field ); - } - $parsed_data[] = $item; - } - - return apply_filters( 'woocommerce_csv_product_parsed_data', $parsed_data, $data ); - } - - /** - * Get default fields. - * - * @return array - */ - protected function get_default_fields() { - $fields = array( - 'id', - 'type', - 'sku', - 'name', - 'status', - 'featured', - 'catalog_visibility', - 'short_description', - 'description', - 'date_on_sale_from', - 'date_on_sale_to', - 'tax_status', - 'tax_class', - 'stock_status', - 'backorders', - 'sold_individually', - 'weight', - 'length', - 'width', - 'height', - 'reviews_allowed', - 'purchase_note', - 'price', - 'regular_price', - 'manage_stock', - 'stock_quantity', - 'category_ids', - 'tag_ids', - 'shipping_class_id', - 'images', - 'downloads', - 'download_limit', - 'download_expiry', - 'parent_id', - 'upsell_ids', - 'cross_sell_ids', - ); - - return apply_filters( 'woocommerce_csv_product_default_fields', $fields ); - } - - /** - * Parse a comma-delineated field from a CSV. - * - * @param string $field - * @return array - */ - public function parse_comma_field( $field ) { - if ( empty( $field ) ) { - return array(); - } - - return array_map( 'esc_attr', array_map( 'trim', explode( ',', $field ) ) ); - } - - /** - * Parse a field that is generally '1' or '0' but can be something else. - * - * @param string $field - * @return bool|string - */ - public function parse_bool_field( $field ) { - if ( '0' === $field ) { - return false; - } - - if ( '1' === $field ) { - return true; - } - - // Don't return explicit true or false for empty fields or values like 'notify'. - return esc_attr( $field ); - } - - /** - * Parse a float value field. - * - * @param string $field - * @return float|string - */ - public function parse_float_field( $field ) { - if ( '' === $field ) { - return $field; - } - - return floatval( $field ); - } - - /** - * Parse a category field from a CSV. - * Categories are separated by commas and subcategories are "parent > subcategory". - * - * @param string $field - * @return array of arrays with "parent" and "name" keys. - */ - public function parse_categories( $field ) { - if ( empty( $field ) ) { - return array(); - } - - $sections = array_map( 'trim', explode( ',', $field ) ); - $categories = array(); - - foreach ( $sections as $section ) { - - // Top level category. - if ( false === strpos( $section, '>' ) ) { - $categories[] = array( - 'parent' => false, - 'name' => esc_attr( $section ), - ); - - // Subcategory. - } else { - $chunks = array_map( 'trim', explode( '>', $section ) ); - $categories[] = array( - 'parent' => esc_attr( reset( $chunks ) ), - 'name' => esc_attr( end( $chunks ) ), - ); - } - } - - return $categories; - } - /** * Output header html. */ @@ -555,6 +291,54 @@ class WC_Product_Importer extends WP_Importer { die(); } + /** + * Get default fields. + * + * @return array + */ + protected function get_default_fields() { + $fields = array( + 'id', + 'type', + 'sku', + 'name', + 'status', + 'featured', + 'catalog_visibility', + 'short_description', + 'description', + 'date_on_sale_from', + 'date_on_sale_to', + 'tax_status', + 'tax_class', + 'stock_status', + 'backorders', + 'sold_individually', + 'weight', + 'length', + 'width', + 'height', + 'reviews_allowed', + 'purchase_note', + 'price', + 'regular_price', + 'manage_stock', + 'stock_quantity', + 'category_ids', + 'tag_ids', + 'shipping_class_id', + 'images', + 'downloads', + 'download_limit', + 'download_expiry', + 'parent_id', + 'upsell_ids', + 'cross_sell_ids', + ); + + return apply_filters( 'woocommerce_csv_product_default_fields', $fields ); + } + /** * Get mapping options. * @@ -616,7 +400,7 @@ class WC_Product_Importer extends WP_Importer { 'meta:' . $item => __( 'Import as meta', 'woocommerce' ), ); - return apply_filters( 'woocommerce_csv_product_import_mapping_options', $options. $item ); + return apply_filters( 'woocommerce_csv_product_import_mapping_options', $options, $item ); } /** @@ -625,9 +409,13 @@ class WC_Product_Importer extends WP_Importer { * @param string $file File path. */ protected function importer_mapping( $file ) { - $data = $this->read_csv( $file, array( 'lines' => 1 ) ); - $headers = $data['raw_headers']; - $sample = $data['data'][0]; + $importer = $this->get_importer( $file, array( 'lines' => 1 ) ); + $headers = $importer->get_raw_keys(); + $sample = current( $importer->get_raw_data() ); + + if ( empty( $sample ) ) { + $this->import_error( __( 'The file is empty, please try again with a new file.', 'woocommerce' ) ); + } // Check if all fields matches. if ( 0 === count( array_diff( $headers, $this->get_default_fields() ) ) ) { @@ -645,4 +433,16 @@ class WC_Product_Importer extends WP_Importer { include_once( dirname( __FILE__ ) . '/views/html-csv-mapping.php' ); } + + /** + * Get importer instance. + * + * @param string $file File to import. + * @param array $args Importer arguments. + * @return WC_Product_CSV_Importer + */ + protected function get_importer( $file, $args = array() ) { + $importer_class = apply_filters( 'woocommerce_product_csv_impoter_class', 'WC_Product_CSV_Importer' ); + return new $importer_class( $file, $args ); + } } diff --git a/includes/import/abstract-wc-product-importer.php b/includes/import/abstract-wc-product-importer.php new file mode 100644 index 00000000000..56601a12002 --- /dev/null +++ b/includes/import/abstract-wc-product-importer.php @@ -0,0 +1,941 @@ +raw_keys; + } + + /** + * Get file mapped headers. + * + * @return array + */ + public function get_mapped_keys() { + return $this->mapped_keys; + } + + /** + * Get raw data. + * + * @return array + */ + public function get_raw_data() { + return $this->raw_data; + } + + /** + * Get parsed data. + * + * @return array + */ + public function get_parsed_data() { + return apply_filters( 'woocommerce_product_parsed_data', $this->parsed_data, $this->get_raw_data() ); + } + + /** + * Process a single item and save. + * + * @param array $data Raw CSV data. + * @return WC_Product|WC_Error + */ + protected function process_item( $data ) { + // Ignore IDs and create new products. + // @todo Mike said that we should have something to force create. + $force_create = false; + + try { + $object = $this->prepare_product( $data, $force_create ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $object->save(); + + // Clean cache for updated products. + $this->clear_cache( $object ); + + return $object->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( Exception $e ) { + return new WP_Error( 'woocommerce_product_csv_importer_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear product cache. + * + * @param WC_Product $object Product instance. + */ + protected function clear_cache( $object ) { + $id = $object->get_id(); + + if ( 'variation' === $object->get_type() ) { + $id = $object->get_parent_id(); + } + + wc_delete_product_transients( $id ); + wp_cache_delete( 'product-' . $id, 'products' ); + } + + /** + * Prepare a single product for create or update. + * + * @param array $data Row data. + * @param bool $creating If should force create a new product. + * @return WC_Product|WP_Error + */ + protected function prepare_product( $data, $force_create = false ) { + $id = ! $force_create && isset( $data['id'] ) ? absint( $data['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $data['type'] ) ) { + if ( ! in_array( $data['type'], array_keys( wc_get_product_types() ), true ) ) { + return new WP_Error( 'woocommerce_product_csv_importer_invalid_type', __( 'Invalid product type.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $data['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + // @todo need to check first how we'll handle variations. + if ( 'variation' === $product->get_type() ) { + $product = $this->save_variation_data( $product, $data ); + } else { + $product = $this->save_product_data( $product, $data ); + } + + return apply_filters( 'woocommerce_product_csv_import_pre_insert_product_object', $product, $data ); + } + + /** + * Set product data. + * + * @param WC_Product $product Product instance. + * @param array $data Row data. + * + * @return WC_Product + */ + protected function save_product_data( $product, $data ) { + + // Post title. + if ( isset( $data['name'] ) ) { + $product->set_name( wp_filter_post_kses( $data['name'] ) ); + } + + // Post content. + if ( isset( $data['description'] ) ) { + $product->set_description( wp_filter_post_kses( $data['description'] ) ); + } + + // Post excerpt. + if ( isset( $data['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $data['short_description'] ) ); + } + + // Post status. + if ( isset( $data['status'] ) ) { + $product->set_status( $data['status'] ? 'publish' : 'draft' ); + } + + // Post slug. + if ( isset( $data['slug'] ) ) { + $product->set_slug( $data['slug'] ); + } + + // Menu order. + if ( isset( $data['menu_order'] ) ) { + $product->set_menu_order( $data['menu_order'] ); + } + + // Comment status. + if ( isset( $data['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $data['reviews_allowed'] ); + } + + // Virtual. + if ( isset( $data['virtual'] ) ) { + $product->set_virtual( $data['virtual'] ); + } + + // Tax status. + if ( isset( $data['tax_status'] ) ) { + $product->set_tax_status( $data['tax_status'] ); + } + + // Tax Class. + if ( isset( $data['tax_class'] ) ) { + $product->set_tax_class( $data['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $data['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $data['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $data['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $data['featured'] ) ) { + $product->set_featured( $data['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $data ); + + // SKU. + if ( isset( $data['sku'] ) ) { + $product->set_sku( wc_clean( $data['sku'] ) ); + } + + // Attributes. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + + foreach ( $data['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $product->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $product->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $data['date_on_sale_from'] ); + } + + if ( isset( $data['date_on_sale_from_gmt'] ) ) { + $product->set_date_on_sale_from( $data['date_on_sale_from_gmt'] ? strtotime( $data['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $data['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $data['date_on_sale_to'] ); + } + + if ( isset( $data['date_on_sale_to_gmt'] ) ) { + $product->set_date_on_sale_to( $data['date_on_sale_to_gmt'] ? strtotime( $data['date_on_sale_to_gmt'] ) : null ); + } + } + + // Product parent ID for groups. + if ( isset( $data['parent_id'] ) ) { + $product->set_parent_id( $data['parent_id'] ); + } + + // Sold individually. + if ( isset( $data['sold_individually'] ) ) { + $product->set_sold_individually( $data['sold_individually'] ); + } + + // Stock status. + if ( isset( $data['in_stock'] ) ) { + $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $data['manage_stock'] ) ) { + $product->set_manage_stock( $data['manage_stock'] ); + } + + // Backorders. + if ( isset( $data['backorders'] ) ) { + $product->set_backorders( $data['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $data['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $data['upsell_ids'] ) ) { + $upsells = array(); + $ids = $data['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $data['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $data['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $data['categories'] ); + } + + // Product tags. + if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $data['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $data['downloadable'] ) ) { + $product->set_downloadable( $data['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $product->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $product->set_download_expiry( $data['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $data['external_url'] ) ) { + $product->set_product_url( $data['external_url'] ); + } + + if ( isset( $data['button_text'] ) ) { + $product->set_button_text( $data['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $data ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Allow set meta_data. + if ( isset( $data['meta_data'] ) && is_array( $data['meta_data'] ) ) { + foreach ( $data['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + return $product; + } + + /** + * Set variation data. + * + * @param WC_Product $variation Product instance. + * @param array $data Row data. + * + * @return WC_Product + */ + protected function save_variation_data( $variation, $data ) { + if ( isset( $data['product_id'] ) ) { + $variation->set_parent_id( absint( $data['product_id'] ) ); + } else { + return new WP_Error( 'woocommerce_product_importer_missing_variation_parent_id', __( 'Missing variation product parent ID', 'woocommerce' ), array( 'status' => 401 ) ); + } + + // Status. + if ( isset( $data['status'] ) ) { + $variation->set_status( false === $data['status'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $data['sku'] ) ) { + $variation->set_sku( wc_clean( $data['sku'] ) ); + } + + // Thumbnail. + if ( isset( $data['image'] ) ) { + if ( is_array( $data['image'] ) ) { + $image = $data['image']; + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->save_product_images( $variation, array( $image ) ); + } else { + $variation->set_image_id( '' ); + } + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $variation->set_downloadable( $data['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $variation->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $variation->set_download_expiry( $data['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $data ); + + // Stock handling. + if ( isset( $data['manage_stock'] ) ) { + $variation->set_manage_stock( $data['manage_stock'] ); + } + + if ( isset( $data['in_stock'] ) ) { + $variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' ); + } + + if ( isset( $data['backorders'] ) ) { + $variation->set_backorders( $data['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['stock_quantity'] ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $variation->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $variation->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $data['date_on_sale_from'] ); + } + + if ( isset( $data['date_on_sale_from_gmt'] ) ) { + $variation->set_date_on_sale_from( $data['date_on_sale_from_gmt'] ? strtotime( $data['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $data['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $data['date_on_sale_to'] ); + } + + if ( isset( $data['date_on_sale_to_gmt'] ) ) { + $variation->set_date_on_sale_to( $data['date_on_sale_to_gmt'] ? strtotime( $data['date_on_sale_to_gmt'] ) : null ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Description. + if ( isset( $data['description'] ) ) { + $variation->set_description( wp_kses_post( $data['description'] ) ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + $parent = wc_get_product( $variation->get_parent_id() ); + $parent_attributes = $parent->get_attributes(); + + foreach ( $data['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + // Menu order. + if ( isset( $data['menu_order'] ) ) { + $variation->set_menu_order( $data['menu_order'] ); + } + + // Meta data. + if ( isset( $data['meta_data'] ) && is_array( $data['meta_data'] ) ) { + foreach ( $data['meta_data'] as $meta ) { + $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + + return $variation; + } + + /** + * Set product images. + * + * @param WC_Product $product Product instance. + * @param array $images Images data. + * @return WC_Product + */ + protected function save_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new Exception( $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + throw new Exception( sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { + $product->set_image_id( $attachment_id ); + } else { + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Save product shipping data. + * + * @param WC_Product $product Product instance. + * @param array $data Shipping data. + * @return WC_Product + */ + protected function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $data['weight'] ) ) { + $product->set_weight( $data['weight'] ); + } + + // Height. + if ( isset( $data['height'] ) ) { + $product->set_height( $data['height'] ); + } + + // Width. + if ( isset( $data['width'] ) ) { + $product->set_width( $data['width'] ); + } + + // Length. + if ( isset( $data['length'] ) ) { + $product->set_length( $data['length'] ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class_id'] ) ) { + $shipping_class_term = get_term_by( 'id', wc_clean( $data['shipping_class_id'] ), 'product_shipping_class' ); + + if ( $shipping_class_term ) { + $product->set_shipping_class_id( $shipping_class_term->term_id ); + } + } + + return $product; + } + + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads ) { + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( $key ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * + * @param WC_Product $product Product instance. + * @param array $data Row data. + * @return WC_Product + */ + protected function save_default_attributes( $product, $data ) { + if ( isset( $data['default_attributes'] ) && is_array( $data['default_attributes'] ) ) { + + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $data['default_attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } +} diff --git a/includes/import/class-wc-product-csv-importer.php b/includes/import/class-wc-product-csv-importer.php new file mode 100644 index 00000000000..3939ae74c6d --- /dev/null +++ b/includes/import/class-wc-product-csv-importer.php @@ -0,0 +1,277 @@ + 0, // File pointer start. + 'end_pos' => -1, // File pointer end. + 'lines' => -1, // Max lines to read. + 'mapping' => array(), // Column mapping. csv_heading => schema_heading. + 'parse' => false, // Whether to sanitize and format data. + 'delimiter' => ',', // CSV delimiter. + ); + + $this->params = wp_parse_args( $params, $default_args ); + $this->file = $file; + + $this->read_file(); + } + + /** + * Read file. + * + * @return array + */ + protected function read_file() { + if ( false !== ( $handle = fopen( $this->file, 'r' ) ) ) { + $this->raw_keys = fgetcsv( $handle, 0, $this->params['delimiter'] ); + + if ( 0 !== $this->params['start_pos'] ) { + fseek( $handle, (int) $this->params['start_pos'] ); + } + + while ( false !== ( $row = fgetcsv( $handle, 0, $this->params['delimiter'] ) ) ) { + $this->raw_data[] = $row; + + if ( ( $this->params['end_pos'] > 0 && ftell( $handle ) >= $this->params['end_pos'] ) || 0 === --$this->params['lines'] ) { + break; + } + } + } + + if ( ! empty( $this->params['mapping'] ) ) { + $this->set_mapped_keys(); + } + + if ( $this->params['parse'] ) { + $this->set_parsed_data(); + } + } + + /** + * Set file mapped keys. + * + * @return array + */ + protected function set_mapped_keys() { + $mapping = $this->params['mapping']; + + foreach ( $this->raw_keys as $key ) { + $this->mapped_keys[] = isset( $mapping[ $key ] ) ? $mapping[ $key ] : $key; + } + } + + /** + * Parse a comma-delineated field from a CSV. + * + * @param string $field + * @return array + */ + protected function parse_comma_field( $field ) { + if ( empty( $field ) ) { + return array(); + } + + return array_map( 'esc_attr', array_map( 'trim', explode( ',', $field ) ) ); + } + + /** + * Parse a field that is generally '1' or '0' but can be something else. + * + * @param string $field + * @return bool|string + */ + protected function parse_bool_field( $field ) { + if ( '0' === $field ) { + return false; + } + + if ( '1' === $field ) { + return true; + } + + // Don't return explicit true or false for empty fields or values like 'notify'. + return esc_attr( $field ); + } + + /** + * Parse a float value field. + * + * @param string $field + * @return float|string + */ + protected function parse_float_field( $field ) { + if ( '' === $field ) { + return $field; + } + + return floatval( $field ); + } + + /** + * Parse a category field from a CSV. + * Categories are separated by commas and subcategories are "parent > subcategory". + * + * @param string $field + * @return array of arrays with "parent" and "name" keys. + */ + protected function parse_categories( $field ) { + if ( empty( $field ) ) { + return array(); + } + + $sections = array_map( 'trim', explode( ',', $field ) ); + $categories = array(); + + foreach ( $sections as $section ) { + + // Top level category. + if ( false === strpos( $section, '>' ) ) { + $categories[] = array( + 'parent' => false, + 'name' => esc_attr( $section ), + ); + + // Subcategory. + } else { + $chunks = array_map( 'trim', explode( '>', $section ) ); + $categories[] = array( + 'parent' => esc_attr( reset( $chunks ) ), + 'name' => esc_attr( end( $chunks ) ), + ); + } + } + + return $categories; + } + + /** + * Map and format raw data to known fields. + * + * @return array + */ + protected function set_parsed_data() { + + /** + * Columns not mentioned here will get parsed with 'esc_attr'. + * column_name => callback. + */ + $data_formatting = array( + 'id' => 'absint', + 'status' => array( $this, 'parse_bool_field' ), + 'featured' => array( $this, 'parse_bool_field' ), + 'date_on_sale_from' => 'strtotime', + 'date_on_sale_to' => 'strtotime', + 'manage_stock' => array( $this, 'parse_bool_field' ), + 'backorders' => array( $this, 'parse_bool_field' ), + 'sold_individually' => array( $this, 'parse_bool_field' ), + 'width' => array( $this, 'parse_float_field' ), + 'length' => array( $this, 'parse_float_field' ), + 'height' => array( $this, 'parse_float_field' ), + 'weight' => array( $this, 'parse_float_field' ), + 'reviews_allowed' => array( $this, 'parse_bool_field' ), + 'purchase_note' => 'wp_kses_post', + 'price' => 'wc_format_decimal', + 'regular_price' => 'wc_format_decimal', + 'stock_quantity' => 'absint', + 'category_ids' => array( $this, 'parse_categories' ), + 'tag_ids' => array( $this, 'parse_comma_field' ), + 'images' => array( $this, 'parse_comma_field' ), + 'upsell_ids' => array( $this, 'parse_comma_field' ), + 'cross_sell_ids' => array( $this, 'parse_comma_field' ), + 'download_limit' => 'absint', + 'download_expiry' => 'absint', + ); + + /** + * @todo switch these to some standard, slug format. + */ + $regex_match_data_formatting = array( + '/Attribute * Value\(s\)/' => array( $this, 'parse_comma_field' ), + '/Attribute * Visible/' => array( $this, 'parse_bool_field' ), + '/Download * URL/' => 'esc_url', + ); + + $headers = ! empty( $this->mapped_keys ) ? $this->mapped_keys : $this->raw_keys; + $parse_functions = array(); + + // Figure out the parse function for each column. + foreach ( $headers as $index => $heading ) { + + $parse_function = 'esc_attr'; + if ( isset( $data_formatting[ $heading ] ) ) { + $parse_function = $data_formatting[ $heading ]; + } else { + foreach ( $regex_match_data_formatting as $regex => $callback ) { + if ( preg_match( $regex, $heading ) ) { + $parse_function = $callback; + break; + } + } + } + + $parse_functions[] = $parse_function; + } + + // Parse the data. + foreach ( $this->raw_data as $row ) { + $item = array(); + foreach ( $row as $index => $field ) { + $item[ $headers[ $index ] ] = call_user_func( $parse_functions[ $index ], $field ); + } + $this->parsed_data[] = $item; + } + } + + /** + * Process importer. + * + * @return array + */ + public function import() { + $data = array( + 'imported' => array(), + 'failed' => array(), + ); + + foreach ( $this->parsed_data as $parsed_data ) { + $result = $this->process_item( $parsed_data ); + + if ( is_wp_error( $result ) ) { + $data['failed'][] = $result; + } else { + $data['imported'][] = $result; + } + } + + return $data; + } +} diff --git a/includes/interfaces/class-wc-importer-interface.php b/includes/interfaces/class-wc-importer-interface.php new file mode 100644 index 00000000000..1e88284dc8f --- /dev/null +++ b/includes/interfaces/class-wc-importer-interface.php @@ -0,0 +1,63 @@ + [], 'failed' => []] + * + * @return array + */ + public function import(); + + /** + * Get file raw keys. + * + * CSV - Headers. + * XML - Element names. + * JSON - Keys + * + * @return array + */ + public function get_raw_keys(); + + /** + * Get file mapped headers. + * + * @return array + */ + public function get_mapped_keys(); + + /** + * Get raw data. + * + * @return array + */ + public function get_raw_data(); + + /** + * Get parsed data. + * + * @return array + */ + public function get_parsed_data(); +} diff --git a/tests/unit-tests/importer/product.php b/tests/unit-tests/importer/product.php index 41d299ccb74..e145460d5ce 100644 --- a/tests/unit-tests/importer/product.php +++ b/tests/unit-tests/importer/product.php @@ -4,162 +4,171 @@ * Meta * @package WooCommerce\Tests\Importer */ -class WC_Tests_Product_Importer extends WC_Unit_Test_Case { +class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { + + /** + * Test CSV file path. + * + * @var string + */ + protected $csv_file = string; /** * Load up the importer classes since they aren't loaded by default. */ public function setUp() { - require_once ABSPATH . 'wp-admin/includes/import.php'; - if ( ! class_exists( 'WP_Importer' ) ) { - $class_wp_importer = ABSPATH . 'wp-admin/includes/class-wp-importer.php'; - if ( file_exists( $class_wp_importer ) ) { - require $class_wp_importer; - } - } + $this->csv_file = dirname( __FILE__ ) . '/sample.csv'; + $bootstrap = WC_Unit_Tests_Bootstrap::instance(); - require_once $bootstrap->plugin_dir . '/includes/admin/importers/class-wc-product-importer.php'; + require_once $bootstrap->plugin_dir . '/includes/import/class-wc-product-csv-importer.php'; } /** - * Test parse_comma_field. + * Test import. + * @todo enable the importer again after conclude the parser. * @since 3.1.0 */ - public function test_parse_comma_field() { - $importer = new WC_Product_Importer(); - - $field1 = 'thing 1, thing 2, thing 3'; - $field2 = 'thing 1'; - $field3 = ''; - - $expected1 = array( 'thing 1', 'thing 2', 'thing 3' ); - $expected2 = array( 'thing 1' ); - $expected3 = array(); - - $this->assertEquals( $expected1, $importer->parse_comma_field( $field1 ) ); - $this->assertEquals( $expected2, $importer->parse_comma_field( $field2 ) ); - $this->assertEquals( $expected3, $importer->parse_comma_field( $field3 ) ); - } - - /** - * Test parse_bool_field. - * @since 3.1.0 - */ - public function test_parse_bool_field() { - $importer = new WC_Product_Importer(); - - $field1 = '1'; - $field2 = '0'; - $field3 = ''; - $field4 = 'notify'; - - $this->assertEquals( true, $importer->parse_bool_field( $field1 ) ); - $this->assertEquals( false, $importer->parse_bool_field( $field2 ) ); - $this->assertEquals( '', $importer->parse_bool_field( $field3 ) ); - $this->assertEquals( 'notify', $importer->parse_bool_field( $field4 ) ); - } - - /** - * Test parse_float_field. - * @since 3.1.0 - */ - public function test_parse_float_field() { - $importer = new WC_Product_Importer(); - - $field1 = '12.45'; - $field2 = '5'; - $field3 = ''; - - $this->assertEquals( 12.45, $importer->parse_float_field( $field1 ) ); - $this->assertEquals( 5, $importer->parse_float_field( $field2 ) ); - $this->assertEquals( '', $importer->parse_float_field( $field3 ) ); - } - - /** - * Test parse_categories. - * @since 3.1.0 - */ - public function test_parse_categories() { - $importer = new WC_Product_Importer(); - - $field1 = 'category1'; - $field2 = 'category1, category2, category1 > subcategory1, category1 > subcategory2'; - $field3 = ''; - - $expected1 = array( - array( - 'parent' => false, - 'name' => 'category1' - ) - ); - $expected2 = array( - array( - 'parent' => false, - 'name' => 'category1' - ), - array( - 'parent' => false, - 'name' => 'category2' - ), - array( - 'parent' => 'category1', - 'name' => 'subcategory1' - ), - array( - 'parent' => 'category1', - 'name' => 'subcategory2' - ) - ); - $expected3 = array(); - - $this->assertEquals( $expected1, $importer->parse_categories( $field1 ) ); - $this->assertEquals( $expected2, $importer->parse_categories( $field2 ) ); - $this->assertEquals( $expected3, $importer->parse_categories( $field3 ) ); - } - - /** - * Test parse_data. - * @since 3.1.0 - */ - public function test_parse_data() { - $importer = new WC_Product_Importer(); - - $data = array( - 'headers' => array( 'id', 'weight', 'price', 'category_ids', 'tag_ids', 'extra_thing', 'featured', 'Download 1 URL' ), - 'data' => array( - array( '', '12.2', '12.50', 'category1, category1 > subcategory', 'products, things, etc', 'metadata', '1', '' ), - array( '12', '', '5', 'category2', '', '', '0', 'http://www.example.com' ), - ) + public function test_import() { + $mapped = array( + 'Type' => 'type', + 'SKU' => 'sku', + 'Name' => 'name', + 'Status' => 'status', + 'Regular price' => 'regular_price', ); - $expected = array( + $args = array( + 'mapping' => $mapped, + 'parse' => true, + ); + + $importer = new WC_Product_CSV_Importer( $this->csv_file, $args ); + $results = $importer->import(); + + $this->assertEquals( 3, count( $results['imported'] ) ); + $this->assertEquals( 0, count( $results['failed'] ) ); + + // Exclude imported products. + foreach ( $results['imported'] as $id ) { + wp_delete_post( $id ); + } + } + + /** + * Test get_raw_keys. + * @since 3.1.0 + */ + public function test_get_raw_keys() { + $importer = new WC_Product_CSV_Importer( $this->csv_file ); + $raw_keys = array( + 'Type', + 'SKU', + 'Name', + 'Status', + 'Regular price', + ); + + $this->assertEquals( $raw_keys, $importer->get_raw_keys() ); + } + + /** + * Test get_mapped_keys. + * @since 3.1.0 + */ + public function test_get_mapped_keys() { + $mapped = array( + 'Type' => 'type', + 'SKU' => 'sku', + 'Name' => 'name', + 'Status' => 'status', + 'Regular price' => 'regular_price', + ); + + $args = array( + 'mapping' => $mapped, + ); + + $importer = new WC_Product_CSV_Importer( $this->csv_file, $args ); + + $this->assertEquals( array_values( $mapped ), $importer->get_mapped_keys() ); + } + + /** + * Test get_raw_data. + * @since 3.1.0 + */ + public function test_get_raw_data() { + $importer = new WC_Product_CSV_Importer( $this->csv_file, array( 'parse' => false ) ); + $items = array( array( - 'id' => 0, - 'weight' => 12.2, - 'price' => '12.50', - 'category_ids' => array( - array( 'parent' => false, 'name' => 'category1' ), - array( 'parent' => 'category1', 'name' => 'subcategory' ), - ), - 'tag_ids' => array( 'products', 'things', 'etc' ), - 'extra_thing' => 'metadata', - 'featured' => true, - 'Download 1 URL' => '', + 'simple', + 'PRODUCT-01', + 'Imported Product 1', + 1, + 40, ), array( - 'id' => 12, - 'weight' => '', - 'price' => '5', - 'category_ids' => array( - array( 'parent' => false, 'name' => 'category2' ), - ), - 'tag_ids' => array(), - 'extra_thing' => '', - 'featured' => false, - 'Download 1 URL' => 'http://www.example.com', + 'simple', + 'PRODUCT-02', + 'Imported Product 2', + 1, + 41, + ), + array( + 'simple', + 'PRODUCT-03', + 'Imported Product 3', + 1, + 42, ), ); - $this->assertEquals( $expected, $importer->parse_data( $data ) ); + $this->assertEquals( $items, $importer->get_raw_data() ); + } + + /** + * Test get_parsed_data. + * @since 3.1.0 + */ + public function test_get_parsed_data() { + $mapped = array( + 'Type' => 'type', + 'SKU' => 'sku', + 'Name' => 'name', + 'Status' => 'status', + 'Regular price' => 'regular_price', + ); + + $args = array( + 'mapping' => $mapped, + 'parse' => true, + ); + + $importer = new WC_Product_CSV_Importer( $this->csv_file, $args ); + $items = array( + array( + 'type' => 'simple', + 'sku' => 'PRODUCT-01', + 'name' => 'Imported Product 1', + 'status' => 1, + 'regular_price' => 40, + ), + array( + 'type' => 'simple', + 'sku' => 'PRODUCT-02', + 'name' => 'Imported Product 2', + 'status' => 1, + 'regular_price' => 41, + ), + array( + 'type' => 'simple', + 'sku' => 'PRODUCT-03', + 'name' => 'Imported Product 3', + 'status' => 1, + 'regular_price' => 42, + ), + ); + + $this->assertEquals( $items, $importer->get_parsed_data() ); } } diff --git a/tests/unit-tests/importer/sample.csv b/tests/unit-tests/importer/sample.csv new file mode 100644 index 00000000000..7d8b8ab1b34 --- /dev/null +++ b/tests/unit-tests/importer/sample.csv @@ -0,0 +1,4 @@ +Type,SKU,Name,Status,Regular price +simple,PRODUCT-01,Imported Product 1,1,40 +simple,PRODUCT-02,Imported Product 2,1,41 +simple,PRODUCT-03,Imported Product 3,1,42