PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.2
WooCommerce v10.9.2
11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / import / class-wc-product-csv-importer.php
woocommerce / includes / import Last commit date
abstract-wc-product-importer.php 3 months ago class-wc-product-csv-importer.php 1 year ago
class-wc-product-csv-importer.php
1235 lines
1 <?php
2 /**
3 * WooCommerce Product CSV importer
4 *
5 * @package WooCommerce\Import
6 * @version 10.0.0
7 */
8
9 use Automattic\WooCommerce\Enums\ProductStatus;
10 use Automattic\WooCommerce\Enums\ProductStockStatus;
11 use Automattic\WooCommerce\Enums\ProductTaxStatus;
12 use Automattic\WooCommerce\Enums\ProductType;
13 use Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController;
14 use Automattic\WooCommerce\Utilities\ArrayUtil;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * Include dependencies.
22 */
23 if ( ! class_exists( 'WC_Product_Importer', false ) ) {
24 include_once __DIR__ . '/abstract-wc-product-importer.php';
25 }
26
27 if ( ! class_exists( 'WC_Product_CSV_Importer_Controller', false ) ) {
28 include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php';
29 }
30
31 /**
32 * WC_Product_CSV_Importer Class.
33 */
34 class WC_Product_CSV_Importer extends WC_Product_Importer {
35
36 /**
37 * Tracks current row being parsed.
38 *
39 * @var integer
40 */
41 protected $parsing_raw_data_index = 0;
42
43 /**
44 * Is the Cost of Goods Sold feature enabled?
45 *
46 * @var bool
47 */
48 private $cogs_is_enabled = false;
49
50 /**
51 * Initialize importer.
52 *
53 * @param string $file File to read.
54 * @param array $params Arguments for the parser.
55 */
56 public function __construct( $file, $params = array() ) {
57 $this->cogs_is_enabled = wc_get_container()->get( CostOfGoodsSoldController::class )->feature_is_enabled();
58
59 $default_args = array(
60 'start_pos' => 0, // File pointer start.
61 'end_pos' => -1, // File pointer end.
62 'lines' => -1, // Max lines to read.
63 'mapping' => array(), // Column mapping. csv_heading => schema_heading.
64 'parse' => false, // Whether to sanitize and format data.
65 'update_existing' => false, // Whether to update existing items.
66 'delimiter' => ',', // CSV delimiter.
67 'prevent_timeouts' => true, // Check memory and time usage and abort if reaching limit.
68 'enclosure' => '"', // The character used to wrap text in the CSV.
69 'escape' => "\0", // PHP uses '\' as the default escape character. This is not RFC-4180 compliant. This disables the escape character.
70 );
71
72 $this->params = wp_parse_args( $params, $default_args );
73 $this->file = $file;
74
75 if ( isset( $this->params['mapping']['from'], $this->params['mapping']['to'] ) ) {
76 $this->params['mapping'] = array_combine( $this->params['mapping']['from'], $this->params['mapping']['to'] );
77 }
78
79 // Import mappings for CSV data.
80 include_once dirname( __DIR__ ) . '/admin/importers/mappings/mappings.php';
81
82 $this->read_file();
83 }
84
85 /**
86 * Convert a string from the input encoding to UTF-8.
87 *
88 * @param string $value The string to convert.
89 * @return string The converted string.
90 */
91 private function adjust_character_encoding( $value ) {
92 $encoding = $this->params['character_encoding'];
93 return 'UTF-8' === $encoding ? $value : mb_convert_encoding( $value, 'UTF-8', $encoding );
94 }
95
96 /**
97 * Read file.
98 */
99 protected function read_file() {
100 if ( ! WC_Product_CSV_Importer_Controller::is_file_valid_csv( $this->file ) ) {
101 wp_die( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
102 }
103
104 $handle = fopen( $this->file, 'r' ); // @codingStandardsIgnoreLine.
105
106 if ( false !== $handle ) {
107 $this->raw_keys = array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ); // @codingStandardsIgnoreLine
108
109 if ( ArrayUtil::is_truthy( $this->params, 'character_encoding' ) ) {
110 $this->raw_keys = array_map( array( $this, 'adjust_character_encoding' ), $this->raw_keys );
111 }
112
113 // Remove line breaks in keys, to avoid mismatch mapping of keys.
114 $this->raw_keys = wc_clean( wp_unslash( $this->raw_keys ) );
115
116 // Remove BOM signature from the first item.
117 if ( isset( $this->raw_keys[0] ) ) {
118 $this->raw_keys[0] = $this->remove_utf8_bom( $this->raw_keys[0] );
119 }
120
121 if ( 0 !== $this->params['start_pos'] ) {
122 fseek( $handle, (int) $this->params['start_pos'] );
123 }
124
125 while ( 1 ) {
126 $row = fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ); // @codingStandardsIgnoreLine
127
128 if ( false !== $row ) {
129 if ( ArrayUtil::is_truthy( $this->params, 'character_encoding' ) ) {
130 $row = array_map( array( $this, 'adjust_character_encoding' ), $row );
131 }
132
133 $this->raw_data[] = $row;
134 $this->file_positions[ count( $this->raw_data ) ] = ftell( $handle );
135
136 if ( ( $this->params['end_pos'] > 0 && ftell( $handle ) >= $this->params['end_pos'] ) || 0 === --$this->params['lines'] ) {
137 break;
138 }
139 } else {
140 break;
141 }
142 }
143
144 $this->file_position = ftell( $handle );
145 }
146
147 if ( ! empty( $this->params['mapping'] ) ) {
148 $this->set_mapped_keys();
149 }
150
151 if ( $this->params['parse'] ) {
152 $this->set_parsed_data();
153 }
154 }
155
156 /**
157 * Remove UTF-8 BOM signature.
158 *
159 * @param string $string String to handle.
160 *
161 * @return string
162 */
163 protected function remove_utf8_bom( $string ) {
164 if ( 'efbbbf' === substr( bin2hex( $string ), 0, 6 ) ) {
165 $string = substr( $string, 3 );
166 }
167
168 return $string;
169 }
170
171 /**
172 * Set file mapped keys.
173 */
174 protected function set_mapped_keys() {
175 $mapping = $this->params['mapping'];
176
177 foreach ( $this->raw_keys as $key ) {
178 $this->mapped_keys[] = isset( $mapping[ $key ] ) ? $mapping[ $key ] : $key;
179 }
180 }
181
182 /**
183 * Parse relative field and return product ID.
184 *
185 * Handles `id:xx` and SKUs.
186 *
187 * If mapping to an id: and the product ID does not exist, this link is not
188 * valid.
189 *
190 * If mapping to a SKU and the product ID does not exist, a temporary object
191 * will be created so it can be updated later.
192 *
193 * @param string $value Field value.
194 *
195 * @return int|string
196 */
197 public function parse_relative_field( $value ) {
198 global $wpdb;
199
200 if ( empty( $value ) ) {
201 return '';
202 }
203
204 // IDs are prefixed with id:.
205 if ( preg_match( '/^id:(\d+)$/', $value, $matches ) ) {
206 $id = intval( $matches[1] );
207
208 // If original_id is found, use that instead of the given ID since a new placeholder must have been created already.
209 $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok.
210
211 if ( $original_id ) {
212 return absint( $original_id );
213 }
214
215 // See if the given ID maps to a valid product already.
216 $existing_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type IN ( 'product', 'product_variation' ) AND ID = %d;", $id ) ); // WPCS: db call ok, cache ok.
217
218 if ( $existing_id ) {
219 return absint( $existing_id );
220 }
221
222 // If we're not updating existing posts, we may need a placeholder product to map to.
223 if ( ! $this->params['update_existing'] ) {
224 $product = wc_get_product_object( ProductType::SIMPLE );
225 $product->set_name( 'Import placeholder for ' . $id );
226 $product->set_status( 'importing' );
227 $product->add_meta_data( '_original_id', $id, true );
228 $id = $product->save();
229 }
230
231 return $id;
232 }
233
234 $id = wc_get_product_id_by_sku( $value );
235
236 if ( $id ) {
237 return $id;
238 }
239
240 try {
241 $product = wc_get_product_object( ProductType::SIMPLE );
242 $product->set_name( 'Import placeholder for ' . $value );
243 $product->set_status( 'importing' );
244 $product->set_sku( $value );
245 $id = $product->save();
246
247 if ( $id && ! is_wp_error( $id ) ) {
248 return $id;
249 }
250 } catch ( Exception $e ) {
251 return '';
252 }
253
254 return '';
255 }
256
257 /**
258 * Parse the ID field.
259 *
260 * If we're not doing an update, create a placeholder product so mapping works
261 * for rows following this one.
262 *
263 * @param string $value Field value.
264 *
265 * @return int
266 */
267 public function parse_id_field( $value ) {
268 global $wpdb;
269
270 $id = absint( $value );
271
272 if ( ! $id ) {
273 return 0;
274 }
275
276 // See if this maps to an ID placeholder already.
277 $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok.
278
279 if ( $original_id ) {
280 return absint( $original_id );
281 }
282
283 // Not updating? Make sure we have a new placeholder for this ID.
284 if ( ! $this->params['update_existing'] ) {
285 $mapped_keys = $this->get_mapped_keys();
286 $sku_column_index = absint( array_search( 'sku', $mapped_keys, true ) );
287 $row_sku = isset( $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] ) ? $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] : '';
288 $id_from_sku = $row_sku ? wc_get_product_id_by_sku( $row_sku ) : '';
289
290 // If row has a SKU, make sure placeholder was not made already.
291 if ( $id_from_sku ) {
292 return $id_from_sku;
293 }
294
295 $product = wc_get_product_object( ProductType::SIMPLE );
296 $product->set_name( 'Import placeholder for ' . $id );
297 $product->set_status( 'importing' );
298 $product->add_meta_data( '_original_id', $id, true );
299
300 // If row has a SKU, make sure placeholder has it too.
301 if ( $row_sku ) {
302 $product->set_sku( $row_sku );
303 }
304 $id = $product->save();
305 }
306
307 return $id && ! is_wp_error( $id ) ? $id : 0;
308 }
309
310 /**
311 * Parse relative comma-delineated field and return product ID.
312 *
313 * @param string $value Field value.
314 *
315 * @return array
316 */
317 public function parse_relative_comma_field( $value ) {
318 if ( empty( $value ) ) {
319 return array();
320 }
321
322 return array_filter( array_map( array( $this, 'parse_relative_field' ), $this->explode_values( $value ) ) );
323 }
324
325 /**
326 * Parse a comma-delineated field from a CSV.
327 *
328 * @param string $value Field value.
329 *
330 * @return array
331 */
332 public function parse_comma_field( $value ) {
333 if ( empty( $value ) && '0' !== $value ) {
334 return array();
335 }
336
337 $value = $this->unescape_data( $value );
338 return array_map( 'wc_clean', $this->explode_values( $value ) );
339 }
340
341 /**
342 * Parse a field that is generally '1' or '0' but can be something else.
343 *
344 * @param string $value Field value.
345 *
346 * @return bool|string
347 */
348 public function parse_bool_field( $value ) {
349 if ( '0' === $value ) {
350 return false;
351 }
352
353 if ( '1' === $value ) {
354 return true;
355 }
356
357 // Don't return explicit true or false for empty fields or values like 'notify'.
358 return wc_clean( $value );
359 }
360
361 /**
362 * Parse a float value field.
363 *
364 * @param string $value Field value.
365 *
366 * @return float|string
367 */
368 public function parse_float_field( $value ) {
369 if ( '' === $value ) {
370 return $value;
371 }
372
373 // Remove the ' prepended to fields that start with - if needed.
374 $value = $this->unescape_data( $value );
375
376 return floatval( $value );
377 }
378
379 /**
380 * Parse the stock qty field.
381 *
382 * @param string $value Field value.
383 *
384 * @return float|string
385 */
386 public function parse_stock_quantity_field( $value ) {
387 if ( '' === $value ) {
388 return $value;
389 }
390
391 // Remove the ' prepended to fields that start with - if needed.
392 $value = $this->unescape_data( $value );
393
394 return wc_stock_amount( $value );
395 }
396
397 /**
398 * Parse the tax status field.
399 *
400 * @param string $value Field value.
401 *
402 * @return string
403 */
404 public function parse_tax_status_field( $value ) {
405 if ( '' === $value ) {
406 return $value;
407 }
408
409 // Remove the ' prepended to fields that start with - if needed.
410 $value = $this->unescape_data( $value );
411
412 if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) {
413 $value = wc_string_to_bool( $value ) ? ProductTaxStatus::TAXABLE : ProductTaxStatus::NONE;
414 }
415
416 return wc_clean( $value );
417 }
418
419 /**
420 * Parse a category field from a CSV.
421 * Categories are separated by commas and subcategories are "parent > subcategory".
422 *
423 * @param string $value Field value.
424 *
425 * @return array of arrays with "parent" and "name" keys.
426 */
427 public function parse_categories_field( $value ) {
428 if ( empty( $value ) ) {
429 return array();
430 }
431
432 $row_terms = $this->explode_values( $value );
433 $categories = array();
434
435 foreach ( $row_terms as $row_term ) {
436 $parent = null;
437 $_terms = array_map( 'trim', explode( '>', $row_term ) );
438 $total = count( $_terms );
439
440 foreach ( $_terms as $index => $_term ) {
441 // Don't allow users without capabilities to create new categories.
442 if ( ! current_user_can( 'manage_product_terms' ) ) {
443 break;
444 }
445
446 $term = wp_insert_term( $_term, 'product_cat', array( 'parent' => intval( $parent ) ) );
447
448 if ( is_wp_error( $term ) ) {
449 if ( $term->get_error_code() === 'term_exists' ) {
450 // When term exists, error data should contain existing term id.
451 $term_id = $term->get_error_data();
452 } else {
453 break; // We cannot continue on any other error.
454 }
455 } else {
456 // New term.
457 $term_id = $term['term_id'];
458 }
459
460 // Only requires assign the last category.
461 if ( ( 1 + $index ) === $total ) {
462 $categories[] = $term_id;
463 } else {
464 // Store parent to be able to insert or query categories based in parent ID.
465 $parent = $term_id;
466 }
467 }
468 }
469
470 return $categories;
471 }
472
473 /**
474 * Parse a tag field from a CSV.
475 *
476 * @param string $value Field value.
477 *
478 * @return array
479 */
480 public function parse_tags_field( $value ) {
481 if ( empty( $value ) ) {
482 return array();
483 }
484
485 $value = $this->unescape_data( $value );
486 $names = $this->explode_values( $value );
487 $tags = array();
488
489 foreach ( $names as $name ) {
490 $term = get_term_by( 'name', $name, 'product_tag' );
491
492 if ( ! $term || is_wp_error( $term ) ) {
493 $term = (object) wp_insert_term( $name, 'product_tag' );
494 }
495
496 if ( ! is_wp_error( $term ) ) {
497 $tags[] = $term->term_id;
498 }
499 }
500
501 return $tags;
502 }
503
504 /**
505 * Parse a tag field from a CSV with space separators.
506 *
507 * @param string $value Field value.
508 *
509 * @return array
510 */
511 public function parse_tags_spaces_field( $value ) {
512 if ( empty( $value ) ) {
513 return array();
514 }
515
516 $value = $this->unescape_data( $value );
517 $names = $this->explode_values( $value, ' ' );
518 $tags = array();
519
520 foreach ( $names as $name ) {
521 $term = get_term_by( 'name', $name, 'product_tag' );
522
523 if ( ! $term || is_wp_error( $term ) ) {
524 $term = (object) wp_insert_term( $name, 'product_tag' );
525 }
526
527 if ( ! is_wp_error( $term ) ) {
528 $tags[] = $term->term_id;
529 }
530 }
531
532 return $tags;
533 }
534
535 /**
536 * Parse a shipping class field from a CSV.
537 *
538 * @param string $value Field value.
539 *
540 * @return int
541 */
542 public function parse_shipping_class_field( $value ) {
543 if ( empty( $value ) ) {
544 return 0;
545 }
546
547 $term = get_term_by( 'name', $value, 'product_shipping_class' );
548
549 if ( ! $term || is_wp_error( $term ) ) {
550 $term = (object) wp_insert_term( $value, 'product_shipping_class' );
551 }
552
553 if ( is_wp_error( $term ) ) {
554 return 0;
555 }
556
557 return $term->term_id;
558 }
559
560 /**
561 * Parse images list from a CSV. Images can be filenames or URLs.
562 *
563 * @param string $value Field value.
564 *
565 * @return array
566 */
567 public function parse_images_field( $value ) {
568 if ( empty( $value ) ) {
569 return array();
570 }
571
572 $images = array();
573 $separator = apply_filters( 'woocommerce_product_import_image_separator', ',' );
574
575 foreach ( $this->explode_values( $value, $separator ) as $image ) {
576 if ( stristr( $image, '://' ) ) {
577 $images[] = esc_url_raw( $image );
578 } else {
579 $images[] = sanitize_file_name( $image );
580 }
581 }
582
583 return $images;
584 }
585
586 /**
587 * Parse dates from a CSV.
588 * Dates requires the format YYYY-MM-DD and time is optional.
589 *
590 * @param string $value Field value.
591 *
592 * @return string|null
593 */
594 public function parse_date_field( $value ) {
595 if ( empty( $value ) ) {
596 return null;
597 }
598
599 if ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])([ 01-9:]*)$/', $value ) ) {
600 // Don't include the time if the field had time in it.
601 return current( explode( ' ', $value ) );
602 }
603
604 return null;
605 }
606
607 /**
608 * Parse dates from a CSV.
609 * Dates can be Unix timestamps or in any format supported by strtotime().
610 *
611 * @param string $value Field value.
612 *
613 * @return string|null
614 */
615 public function parse_datetime_field( $value ) {
616 try {
617 // If value is a Unix timestamp, convert it to a datetime string.
618 if ( is_numeric( $value ) ) {
619 $datetime = new DateTime( "@{$value}" );
620 // Return datetime string in ISO8601 format (eg. 2018-01-01T00:00:00Z) to preserve UTC timezone since Unix timestamps are always UTC.
621 return $datetime->format( 'Y-m-d\TH:i:s\Z' );
622 }
623 // Check whether the value is a valid date string.
624 if ( false !== strtotime( $value ) ) {
625 // If the value is a valid date string, return as is.
626 return $value;
627 }
628 } catch ( Exception $e ) {
629 // DateTime constructor throws an exception if the value is not a valid Unix timestamp.
630 return null;
631 }
632 // If value is not valid Unix timestamp or date string, return null.
633 return null;
634 }
635
636 /**
637 * Parse backorders from a CSV.
638 *
639 * @param string $value Field value.
640 *
641 * @return string
642 */
643 public function parse_backorders_field( $value ) {
644 if ( empty( $value ) ) {
645 return 'no';
646 }
647
648 $value = $this->parse_bool_field( $value );
649
650 if ( 'notify' === $value ) {
651 return 'notify';
652 } elseif ( is_bool( $value ) ) {
653 return $value ? 'yes' : 'no';
654 }
655
656 return 'no';
657 }
658
659 /**
660 * Just skip current field.
661 *
662 * By default is applied wc_clean() to all not listed fields
663 * in self::get_formatting_callback(), use this method to skip any formatting.
664 *
665 * @param string $value Field value.
666 *
667 * @return string
668 */
669 public function parse_skip_field( $value ) {
670 return $value;
671 }
672
673 /**
674 * Parse download file urls, we should allow shortcodes here.
675 *
676 * Allow shortcodes if present, otherwise esc_url the value.
677 *
678 * @param string $value Field value.
679 *
680 * @return string
681 */
682 public function parse_download_file_field( $value ) {
683 // Absolute file paths.
684 if ( 0 === strpos( $value, 'http' ) ) {
685 return esc_url_raw( $value );
686 }
687 // Relative and shortcode paths.
688 return wc_clean( $value );
689 }
690
691 /**
692 * Parse an int value field
693 *
694 * @param int $value field value.
695 *
696 * @return int|string
697 */
698 public function parse_int_field( $value ) {
699 // Similar to WC_Meta_Box_Product_Data::save, do not cast the empty value to int.
700 // An empty value indicates that the field should be cleared.
701 if ( '' === $value ) {
702 return $value;
703 }
704
705 // Remove the ' prepended to fields that start with - if needed.
706 $value = $this->unescape_data( $value );
707
708 return intval( $value );
709 }
710
711 /**
712 * Parse a description value field
713 *
714 * @param string $description field value.
715 *
716 * @return string
717 */
718 public function parse_description_field( $description ) {
719 $parts = explode( "\\\\n", $description );
720 foreach ( $parts as $key => $part ) {
721 $parts[ $key ] = str_replace( '\n', "\n", $part );
722 }
723
724 return implode( '\\\n', $parts );
725 }
726
727 /**
728 * Parse the published field. 1 is published, 0 is private, -1 is draft.
729 * Alternatively, 'true' can be used for published and 'false' for draft.
730 *
731 * @param string $value Field value.
732 *
733 * @return float|string
734 */
735 public function parse_published_field( $value ) {
736 if ( '' === $value ) {
737 return $value;
738 }
739
740 // Remove the ' prepended to fields that start with - if needed.
741 $value = $this->unescape_data( $value );
742
743 if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) {
744 return wc_string_to_bool( $value ) ? 1 : -1;
745 }
746
747 return floatval( $value );
748 }
749
750 /**
751 * Parse the Cost of Goods Sold field.
752 *
753 * @param string $value Field value.
754 *
755 * @return float|null
756 */
757 public function parse_cogs_field( $value ) {
758 return '' === $value ? null : (float) wc_format_decimal( $value );
759 }
760
761 /**
762 * Deprecated get formatting callback method.
763 *
764 * @deprecated 4.3.0
765 * @return array
766 */
767 protected function get_formating_callback() {
768 return $this->get_formatting_callback();
769 }
770
771 /**
772 * Get formatting callback.
773 *
774 * @since 4.3.0
775 * @return array
776 */
777 protected function get_formatting_callback() {
778
779 /**
780 * Columns not mentioned here will get parsed with 'wc_clean'.
781 * column_name => callback.
782 */
783 $data_formatting = array(
784 'id' => array( $this, 'parse_id_field' ),
785 'type' => array( $this, 'parse_comma_field' ),
786 'published' => array( $this, 'parse_published_field' ),
787 'featured' => array( $this, 'parse_bool_field' ),
788 'date_on_sale_from' => array( $this, 'parse_datetime_field' ),
789 'date_on_sale_to' => array( $this, 'parse_datetime_field' ),
790 'name' => array( $this, 'parse_skip_field' ),
791 'short_description' => array( $this, 'parse_description_field' ),
792 'description' => array( $this, 'parse_description_field' ),
793 'manage_stock' => array( $this, 'parse_bool_field' ),
794 'low_stock_amount' => array( $this, 'parse_stock_quantity_field' ),
795 'backorders' => array( $this, 'parse_backorders_field' ),
796 'stock_status' => array( $this, 'parse_bool_field' ),
797 'sold_individually' => array( $this, 'parse_bool_field' ),
798 'width' => array( $this, 'parse_float_field' ),
799 'length' => array( $this, 'parse_float_field' ),
800 'height' => array( $this, 'parse_float_field' ),
801 'weight' => array( $this, 'parse_float_field' ),
802 'reviews_allowed' => array( $this, 'parse_bool_field' ),
803 'purchase_note' => 'wp_filter_post_kses',
804 'price' => 'wc_format_decimal',
805 'regular_price' => 'wc_format_decimal',
806 'stock_quantity' => array( $this, 'parse_stock_quantity_field' ),
807 'category_ids' => array( $this, 'parse_categories_field' ),
808 'tag_ids' => array( $this, 'parse_tags_field' ),
809 'tag_ids_spaces' => array( $this, 'parse_tags_spaces_field' ),
810 'shipping_class_id' => array( $this, 'parse_shipping_class_field' ),
811 'images' => array( $this, 'parse_images_field' ),
812 'parent_id' => array( $this, 'parse_relative_field' ),
813 'grouped_products' => array( $this, 'parse_relative_comma_field' ),
814 'upsell_ids' => array( $this, 'parse_relative_comma_field' ),
815 'cross_sell_ids' => array( $this, 'parse_relative_comma_field' ),
816 'download_limit' => array( $this, 'parse_int_field' ),
817 'download_expiry' => array( $this, 'parse_int_field' ),
818 'product_url' => 'esc_url_raw',
819 'menu_order' => 'intval',
820 'tax_status' => array( $this, 'parse_tax_status_field' ),
821 'cogs_value' => array( $this, 'parse_cogs_field' ),
822 );
823
824 /**
825 * Match special column names.
826 */
827 $regex_match_data_formatting = array(
828 '/attributes:value*/' => array( $this, 'parse_comma_field' ),
829 '/attributes:visible*/' => array( $this, 'parse_bool_field' ),
830 '/attributes:taxonomy*/' => array( $this, 'parse_bool_field' ),
831 '/downloads:url*/' => array( $this, 'parse_download_file_field' ),
832 '/meta:*/' => 'wp_kses_post', // Allow some HTML in meta fields.
833 );
834
835 $callbacks = array();
836
837 // Figure out the parse function for each column.
838 foreach ( $this->get_mapped_keys() as $index => $heading ) {
839 $callback = 'wc_clean';
840
841 if ( isset( $data_formatting[ $heading ] ) ) {
842 $callback = $data_formatting[ $heading ];
843 } else {
844 foreach ( $regex_match_data_formatting as $regex => $callback ) {
845 if ( preg_match( $regex, $heading ) ) {
846 $callback = $callback;
847 break;
848 }
849 }
850 }
851
852 $callbacks[] = $callback;
853 }
854
855 return apply_filters( 'woocommerce_product_importer_formatting_callbacks', $callbacks, $this );
856 }
857
858 /**
859 * Check if strings starts with determined word.
860 *
861 * @param string $haystack Complete sentence.
862 * @param string $needle Excerpt.
863 *
864 * @return bool
865 */
866 protected function starts_with( $haystack, $needle ) {
867 return substr( $haystack, 0, strlen( $needle ) ) === $needle;
868 }
869
870 /**
871 * Expand special and internal data into the correct formats for the product CRUD.
872 *
873 * @param array $data Data to import.
874 *
875 * @return array
876 */
877 protected function expand_data( $data ) {
878 $data = apply_filters( 'woocommerce_product_importer_pre_expand_data', $data );
879
880 // Images field maps to image and gallery id fields.
881 if ( isset( $data['images'] ) ) {
882 $images = $data['images'];
883 $data['raw_image_id'] = array_shift( $images );
884
885 if ( ! empty( $images ) ) {
886 $data['raw_gallery_image_ids'] = $images;
887 }
888 unset( $data['images'] );
889 }
890
891 // Type, virtual and downloadable are all stored in the same column.
892 if ( isset( $data['type'] ) ) {
893 $data['type'] = array_map( 'strtolower', $data['type'] );
894 $data['virtual'] = in_array( 'virtual', $data['type'], true );
895 $data['downloadable'] = in_array( 'downloadable', $data['type'], true );
896
897 // Convert type to string.
898 $data['type'] = current( array_diff( $data['type'], array( 'virtual', 'downloadable' ) ) );
899
900 if ( ! $data['type'] ) {
901 $data['type'] = ProductType::SIMPLE;
902 }
903 }
904
905 // Status is mapped from a special published field.
906 if ( isset( $data['published'] ) ) {
907 $published = $data['published'];
908 if ( is_float( $published ) ) {
909 $published = (int) $published;
910 }
911
912 $statuses = array(
913 -1 => ProductStatus::DRAFT,
914 0 => ProductStatus::PRIVATE,
915 1 => ProductStatus::PUBLISH,
916 );
917 $data['status'] = $statuses[ $published ] ?? ProductStatus::DRAFT;
918
919 // Fix draft status of variations.
920 if ( ProductType::VARIATION === ( $data['type'] ?? null ) && -1 === $published ) {
921 $data['status'] = ProductStatus::PUBLISH;
922 }
923
924 unset( $data['published'] );
925 }
926
927 if ( isset( $data['stock_quantity'] ) ) {
928 if ( '' === $data['stock_quantity'] ) {
929 $data['manage_stock'] = false;
930 $data['stock_status'] = isset( $data['stock_status'] ) ? $data['stock_status'] : true;
931 } else {
932 $data['manage_stock'] = true;
933 }
934 }
935
936 // Stock is bool or 'backorder'.
937 if ( isset( $data['stock_status'] ) ) {
938 if ( 'backorder' === $data['stock_status'] ) {
939 $data['stock_status'] = ProductStockStatus::ON_BACKORDER;
940 } else {
941 $data['stock_status'] = $data['stock_status'] ? ProductStockStatus::IN_STOCK : ProductStockStatus::OUT_OF_STOCK;
942 }
943 }
944
945 // Prepare grouped products.
946 if ( isset( $data['grouped_products'] ) ) {
947 $data['children'] = $data['grouped_products'];
948 unset( $data['grouped_products'] );
949 }
950
951 // Tag ids.
952 if ( isset( $data['tag_ids_spaces'] ) ) {
953 $data['tag_ids'] = $data['tag_ids_spaces'];
954 unset( $data['tag_ids_spaces'] );
955 }
956
957 if ( ! $this->cogs_is_enabled ) {
958 unset( $data['cogs_value'] );
959 }
960
961 // Handle special column names which span multiple columns.
962 $attributes = array();
963 $downloads = array();
964 $meta_data = array();
965
966 foreach ( $data as $key => $value ) {
967 if ( $this->starts_with( $key, 'attributes:name' ) ) {
968 if ( ! empty( $value ) ) {
969 $attributes[ str_replace( 'attributes:name', '', $key ) ]['name'] = $value;
970 }
971 unset( $data[ $key ] );
972
973 } elseif ( $this->starts_with( $key, 'attributes:value' ) ) {
974 $attributes[ str_replace( 'attributes:value', '', $key ) ]['value'] = $value;
975 unset( $data[ $key ] );
976
977 } elseif ( $this->starts_with( $key, 'attributes:taxonomy' ) ) {
978 $attributes[ str_replace( 'attributes:taxonomy', '', $key ) ]['taxonomy'] = wc_string_to_bool( $value );
979 unset( $data[ $key ] );
980
981 } elseif ( $this->starts_with( $key, 'attributes:visible' ) ) {
982 $attributes[ str_replace( 'attributes:visible', '', $key ) ]['visible'] = wc_string_to_bool( $value );
983 unset( $data[ $key ] );
984
985 } elseif ( $this->starts_with( $key, 'attributes:default' ) ) {
986 if ( ! empty( $value ) ) {
987 $attributes[ str_replace( 'attributes:default', '', $key ) ]['default'] = $value;
988 }
989 unset( $data[ $key ] );
990
991 } elseif ( $this->starts_with( $key, 'downloads:id' ) ) {
992 if ( ! empty( $value ) ) {
993 $downloads[ str_replace( 'downloads:id', '', $key ) ]['id'] = $value;
994 }
995 unset( $data[ $key ] );
996
997 } elseif ( $this->starts_with( $key, 'downloads:name' ) ) {
998 if ( ! empty( $value ) ) {
999 $downloads[ str_replace( 'downloads:name', '', $key ) ]['name'] = $value;
1000 }
1001 unset( $data[ $key ] );
1002
1003 } elseif ( $this->starts_with( $key, 'downloads:url' ) ) {
1004 if ( ! empty( $value ) ) {
1005 $downloads[ str_replace( 'downloads:url', '', $key ) ]['url'] = $value;
1006 }
1007 unset( $data[ $key ] );
1008
1009 } elseif ( $this->starts_with( $key, 'meta:' ) ) {
1010 $meta_data[] = array(
1011 'key' => str_replace( 'meta:', '', $key ),
1012 'value' => $value,
1013 );
1014 unset( $data[ $key ] );
1015 }
1016 }
1017
1018 if ( ! empty( $attributes ) ) {
1019 // Remove empty attributes and clear indexes.
1020 foreach ( $attributes as $attribute ) {
1021 if ( empty( $attribute['name'] ) ) {
1022 continue;
1023 }
1024
1025 $data['raw_attributes'][] = $attribute;
1026 }
1027 }
1028
1029 if ( ! empty( $downloads ) ) {
1030 $data['downloads'] = array();
1031
1032 foreach ( $downloads as $key => $file ) {
1033 if ( empty( $file['url'] ) ) {
1034 continue;
1035 }
1036
1037 $data['downloads'][] = array(
1038 'download_id' => isset( $file['id'] ) ? $file['id'] : null,
1039 'name' => $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['url'] ),
1040 'file' => $file['url'],
1041 );
1042 }
1043 }
1044
1045 if ( ! empty( $meta_data ) ) {
1046 $data['meta_data'] = $meta_data;
1047 }
1048
1049 return $data;
1050 }
1051
1052 /**
1053 * Map and format raw data to known fields.
1054 */
1055 protected function set_parsed_data() {
1056 $parse_functions = $this->get_formatting_callback();
1057 $mapped_keys = $this->get_mapped_keys();
1058 $use_mb = function_exists( 'mb_convert_encoding' );
1059
1060 // Parse the data.
1061 foreach ( $this->raw_data as $row_index => $row ) {
1062 // Skip empty rows.
1063 if ( ! count( array_filter( $row ) ) ) {
1064 continue;
1065 }
1066
1067 $this->parsing_raw_data_index = $row_index;
1068
1069 $data = array();
1070
1071 do_action( 'woocommerce_product_importer_before_set_parsed_data', $row, $mapped_keys );
1072
1073 foreach ( $row as $id => $value ) {
1074 // Skip ignored columns.
1075 if ( empty( $mapped_keys[ $id ] ) ) {
1076 continue;
1077 }
1078
1079 // Convert UTF8.
1080 if ( $use_mb ) {
1081 $encoding = mb_detect_encoding( $value, mb_detect_order(), true );
1082 if ( $encoding ) {
1083 $value = mb_convert_encoding( $value, 'UTF-8', $encoding );
1084 } else {
1085 $value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' );
1086 }
1087 } else {
1088 $value = wp_check_invalid_utf8( $value, true );
1089 }
1090
1091 $data[ $mapped_keys[ $id ] ] = call_user_func( $parse_functions[ $id ], $value );
1092 }
1093
1094 /**
1095 * Filter product importer parsed data.
1096 *
1097 * @param array $parsed_data Parsed data.
1098 * @param WC_Product_Importer $importer Importer instance.
1099 *
1100 * @since
1101 */
1102 $this->parsed_data[] = apply_filters( 'woocommerce_product_importer_parsed_data', $this->expand_data( $data ), $this );
1103 }
1104 }
1105
1106 /**
1107 * Get a string to identify the row from parsed data.
1108 *
1109 * @param array $parsed_data Parsed data.
1110 *
1111 * @return string
1112 */
1113 protected function get_row_id( $parsed_data ) {
1114 $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0;
1115 $sku = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : '';
1116 $name = isset( $parsed_data['name'] ) ? esc_attr( $parsed_data['name'] ) : '';
1117 $row_data = array();
1118
1119 if ( $name ) {
1120 $row_data[] = $name;
1121 }
1122 if ( $id ) {
1123 /* translators: %d: product ID */
1124 $row_data[] = sprintf( __( 'ID %d', 'woocommerce' ), $id );
1125 }
1126 if ( $sku ) {
1127 /* translators: %s: product SKU */
1128 $row_data[] = sprintf( __( 'SKU %s', 'woocommerce' ), $sku );
1129 }
1130
1131 return implode( ', ', $row_data );
1132 }
1133
1134 /**
1135 * Process importer.
1136 *
1137 * Do not import products with IDs or SKUs that already exist if option
1138 * update existing is false, and likewise, if updating products, do not
1139 * process rows which do not exist if an ID/SKU is provided.
1140 *
1141 * @return array
1142 */
1143 public function import() {
1144 $this->start_time = time();
1145 $index = 0;
1146 $update_existing = $this->params['update_existing'];
1147 $data = array(
1148 'imported' => array(),
1149 'imported_variations' => array(),
1150 'failed' => array(),
1151 'updated' => array(),
1152 'skipped' => array(),
1153 );
1154
1155 foreach ( $this->parsed_data as $parsed_data_key => $parsed_data ) {
1156 do_action( 'woocommerce_product_import_before_import', $parsed_data );
1157
1158 $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0;
1159 $sku = isset( $parsed_data['sku'] ) ? $parsed_data['sku'] : '';
1160 $id_exists = false;
1161 $sku_exists = false;
1162
1163 if ( $id ) {
1164 $product = wc_get_product( $id );
1165 $id_exists = $product && 'importing' !== $product->get_status();
1166 }
1167
1168 if ( $sku ) {
1169 $id_from_sku = wc_get_product_id_by_sku( $sku );
1170 $product = $id_from_sku ? wc_get_product( $id_from_sku ) : false;
1171 $sku_exists = $product && 'importing' !== $product->get_status();
1172 }
1173
1174 if ( $sku_exists && ! $update_existing ) {
1175 $data['skipped'][] = new WP_Error(
1176 'woocommerce_product_importer_error',
1177 esc_html__( 'A product with this SKU already exists.', 'woocommerce' ),
1178 array(
1179 'sku' => esc_attr( $sku ),
1180 'row' => $this->get_row_id( $parsed_data ),
1181 )
1182 );
1183 continue;
1184 }
1185
1186 if ( $id_exists && ! $update_existing ) {
1187 $data['skipped'][] = new WP_Error(
1188 'woocommerce_product_importer_error',
1189 esc_html__( 'A product with this ID already exists.', 'woocommerce' ),
1190 array(
1191 'id' => $id,
1192 'row' => $this->get_row_id( $parsed_data ),
1193 )
1194 );
1195 continue;
1196 }
1197
1198 if ( $update_existing && ( isset( $parsed_data['id'] ) || isset( $parsed_data['sku'] ) ) && ! $id_exists && ! $sku_exists ) {
1199 $data['skipped'][] = new WP_Error(
1200 'woocommerce_product_importer_error',
1201 esc_html__( 'No matching product exists to update.', 'woocommerce' ),
1202 array(
1203 'id' => $id,
1204 'sku' => esc_attr( $sku ),
1205 'row' => $this->get_row_id( $parsed_data ),
1206 )
1207 );
1208 continue;
1209 }
1210
1211 $result = $this->process_item( $parsed_data );
1212
1213 if ( is_wp_error( $result ) ) {
1214 $result->add_data( array( 'row' => $this->get_row_id( $parsed_data ) ) );
1215 $data['failed'][] = $result;
1216 } elseif ( $result['updated'] ) {
1217 $data['updated'][] = $result['id'];
1218 } elseif ( $result['is_variation'] ) {
1219 $data['imported_variations'][] = $result['id'];
1220 } else {
1221 $data['imported'][] = $result['id'];
1222 }
1223
1224 ++$index;
1225
1226 if ( $this->params['prevent_timeouts'] && ( $this->time_exceeded() || $this->memory_exceeded() ) ) {
1227 $this->file_position = $this->file_positions[ $index ];
1228 break;
1229 }
1230 }
1231
1232 return $data;
1233 }
1234 }
1235