Completed
Push — master ( deec93...29d630 )
by Claudio
31:19 queued 22:42
created

WC_Product_CSV_Importer::parse_published_field()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5.583

Importance

Changes 0
Metric Value
cc 5
nc 4
nop 1
dl 0
loc 14
ccs 5
cts 7
cp 0.7143
crap 5.583
rs 9.4888
c 0
b 0
f 0
1
<?php
2
/**
3
 * WooCommerce Product CSV importer
4
 *
5
 * @package WooCommerce/Import
6
 * @version 3.1.0
7
 */
8
9 1
if ( ! defined( 'ABSPATH' ) ) {
10
	exit;
11
}
12
13
/**
14
 * Include dependencies.
15
 */
16 1
if ( ! class_exists( 'WC_Product_Importer', false ) ) {
17 1
	include_once dirname( __FILE__ ) . '/abstract-wc-product-importer.php';
18
}
19
20 1
if ( ! class_exists( 'WC_Product_CSV_Importer_Controller', false ) ) {
21 1
	include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php';
22
}
23
24
/**
25
 * WC_Product_CSV_Importer Class.
26
 */
27
class WC_Product_CSV_Importer extends WC_Product_Importer {
28
29
	/**
30
	 * Tracks current row being parsed.
31
	 *
32
	 * @var integer
33
	 */
34
	protected $parsing_raw_data_index = 0;
35
36
	/**
37
	 * Initialize importer.
38
	 *
39
	 * @param string $file   File to read.
40
	 * @param array  $params Arguments for the parser.
41
	 */
42 5
	public function __construct( $file, $params = array() ) {
43
		$default_args = array(
44 5
			'start_pos'        => 0, // File pointer start.
45
			'end_pos'          => -1, // File pointer end.
46
			'lines'            => -1, // Max lines to read.
47
			'mapping'          => array(), // Column mapping. csv_heading => schema_heading.
48
			'parse'            => false, // Whether to sanitize and format data.
49
			'update_existing'  => false, // Whether to update existing items.
50
			'delimiter'        => ',', // CSV delimiter.
51
			'prevent_timeouts' => true, // Check memory and time usage and abort if reaching limit.
52
			'enclosure'        => '"', // The character used to wrap text in the CSV.
53
			'escape'           => "\0", // PHP uses '\' as the default escape character. This is not RFC-4180 compliant. This disables the escape character.
54
		);
55
56 5
		$this->params = wp_parse_args( $params, $default_args );
57 5
		$this->file   = $file;
58
59 5
		if ( isset( $this->params['mapping']['from'], $this->params['mapping']['to'] ) ) {
60
			$this->params['mapping'] = array_combine( $this->params['mapping']['from'], $this->params['mapping']['to'] );
61
		}
62
63 5
		$this->read_file();
64
	}
65
66
	/**
67
	 * Read file.
68
	 */
69 5
	protected function read_file() {
70 5
		if ( ! WC_Product_CSV_Importer_Controller::is_file_valid_csv( $this->file ) ) {
71
			wp_die( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
72
		}
73
74 5
		$handle = fopen( $this->file, 'r' ); // @codingStandardsIgnoreLine.
75
76 5
		if ( false !== $handle ) {
77 5
			$this->raw_keys = version_compare( PHP_VERSION, '5.3', '>=' ) ? array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ) : array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ) ); // @codingStandardsIgnoreLine
78
79
			// Remove BOM signature from the first item.
80 5
			if ( isset( $this->raw_keys[0] ) ) {
81 5
				$this->raw_keys[0] = $this->remove_utf8_bom( $this->raw_keys[0] );
82
			}
83
84 5
			if ( 0 !== $this->params['start_pos'] ) {
85
				fseek( $handle, (int) $this->params['start_pos'] );
86
			}
87
88 5
			while ( 1 ) {
89 5
				$row = version_compare( PHP_VERSION, '5.3', '>=' ) ? fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) : fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ); // @codingStandardsIgnoreLine
90
91 5
				if ( false !== $row ) {
92 5
					$this->raw_data[]                                 = $row;
93 5
					$this->file_positions[ count( $this->raw_data ) ] = ftell( $handle );
94
95 5
					if ( ( $this->params['end_pos'] > 0 && ftell( $handle ) >= $this->params['end_pos'] ) || 0 === --$this->params['lines'] ) {
96 5
						break;
97
					}
98
				} else {
99 2
					break;
100
				}
101
			}
102
103 5
			$this->file_position = ftell( $handle );
104
		}
105
106 5
		if ( ! empty( $this->params['mapping'] ) ) {
107 3
			$this->set_mapped_keys();
108
		}
109
110 5
		if ( $this->params['parse'] ) {
111 2
			$this->set_parsed_data();
112
		}
113
	}
114
115
	/**
116
	 * Remove UTF-8 BOM signature.
117
	 *
118
	 * @param string $string String to handle.
119
	 *
120
	 * @return string
121
	 */
122 5
	protected function remove_utf8_bom( $string ) {
123 5
		if ( 'efbbbf' === substr( bin2hex( $string ), 0, 6 ) ) {
124
			$string = substr( $string, 3 );
125
		}
126
127 5
		return $string;
128
	}
129
130
	/**
131
	 * Set file mapped keys.
132
	 */
133 3
	protected function set_mapped_keys() {
134 3
		$mapping = $this->params['mapping'];
135
136 3
		foreach ( $this->raw_keys as $key ) {
137 3
			$this->mapped_keys[] = isset( $mapping[ $key ] ) ? $mapping[ $key ] : $key;
138
		}
139
	}
140
141
	/**
142
	 * Parse relative field and return product ID.
143
	 *
144
	 * Handles `id:xx` and SKUs.
145
	 *
146
	 * If mapping to an id: and the product ID does not exist, this link is not
147
	 * valid.
148
	 *
149
	 * If mapping to a SKU and the product ID does not exist, a temporary object
150
	 * will be created so it can be updated later.
151
	 *
152
	 * @param string $value Field value.
153
	 *
154
	 * @return int|string
155
	 */
156 2
	public function parse_relative_field( $value ) {
157
		global $wpdb;
158
159 2
		if ( empty( $value ) ) {
160 2
			return '';
161
		}
162
163
		// IDs are prefixed with id:.
164 2
		if ( preg_match( '/^id:(\d+)$/', $value, $matches ) ) {
165
			$id = intval( $matches[1] );
166
167
			// If original_id is found, use that instead of the given ID since a new placeholder must have been created already.
168
			$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.
169
170
			if ( $original_id ) {
171
				return absint( $original_id );
172
			}
173
174
			// See if the given ID maps to a valid product allready.
175
			$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.
176
177
			if ( $existing_id ) {
178
				return absint( $existing_id );
179
			}
180
181
			// If we're not updating existing posts, we may need a placeholder product to map to.
182
			if ( ! $this->params['update_existing'] ) {
183
				$product = new WC_Product_Simple();
184
				$product->set_name( 'Import placeholder for ' . $id );
185
				$product->set_status( 'importing' );
186
				$product->add_meta_data( '_original_id', $id, true );
187
				$id = $product->save();
188
			}
189
190
			return $id;
191
		}
192
193 2
		$id = wc_get_product_id_by_sku( $value );
194
195 2
		if ( $id ) {
196 2
			return $id;
197
		}
198
199
		try {
200 2
			$product = new WC_Product_Simple();
201 2
			$product->set_name( 'Import placeholder for ' . $value );
202 2
			$product->set_status( 'importing' );
203 2
			$product->set_sku( $value );
204 2
			$id = $product->save();
205
206 2
			if ( $id && ! is_wp_error( $id ) ) {
207 2
				return $id;
208
			}
209
		} catch ( Exception $e ) {
210
			return '';
211
		}
212
213
		return '';
214
	}
215
216
	/**
217
	 * Parse the ID field.
218
	 *
219
	 * If we're not doing an update, create a placeholder product so mapping works
220
	 * for rows following this one.
221
	 *
222
	 * @param string $value Field value.
223
	 *
224
	 * @return int
225
	 */
226
	public function parse_id_field( $value ) {
227
		global $wpdb;
228
229
		$id = absint( $value );
230
231
		if ( ! $id ) {
232
			return 0;
233
		}
234
235
		// See if this maps to an ID placeholder already.
236
		$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.
237
238
		if ( $original_id ) {
239
			return absint( $original_id );
240
		}
241
242
		// Not updating? Make sure we have a new placeholder for this ID.
243
		if ( ! $this->params['update_existing'] ) {
244
			$mapped_keys      = $this->get_mapped_keys();
245
			$sku_column_index = absint( array_search( 'sku', $mapped_keys, true ) );
246
			$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 ] : '';
247
			$id_from_sku      = $row_sku ? wc_get_product_id_by_sku( $row_sku ) : '';
248
249
			// If row has a SKU, make sure placeholder was not made already.
250
			if ( $id_from_sku ) {
251
				return $id_from_sku;
252
			}
253
254
			$product = new WC_Product_Simple();
255
			$product->set_name( 'Import placeholder for ' . $id );
256
			$product->set_status( 'importing' );
257
			$product->add_meta_data( '_original_id', $id, true );
258
259
			// If row has a SKU, make sure placeholder has it too.
260
			if ( $row_sku ) {
261
				$product->set_sku( $row_sku );
262
			}
263
			$id = $product->save();
264
		}
265
266
		return $id && ! is_wp_error( $id ) ? $id : 0;
267
	}
268
269
	/**
270
	 * Parse relative comma-delineated field and return product ID.
271
	 *
272
	 * @param string $value Field value.
273
	 *
274
	 * @return array
275
	 */
276 2
	public function parse_relative_comma_field( $value ) {
277 2
		if ( empty( $value ) ) {
278 2
			return array();
279
		}
280
281 2
		return array_filter( array_map( array( $this, 'parse_relative_field' ), $this->explode_values( $value ) ) );
282
	}
283
284
	/**
285
	 * Parse a comma-delineated field from a CSV.
286
	 *
287
	 * @param string $value Field value.
288
	 *
289
	 * @return array
290
	 */
291 2
	public function parse_comma_field( $value ) {
292 2
		if ( empty( $value ) && '0' !== $value ) {
293 2
			return array();
294
		}
295
296 2
		$value = $this->unescape_data( $value );
297 2
		return array_map( 'wc_clean', $this->explode_values( $value ) );
298
	}
299
300
	/**
301
	 * Parse a field that is generally '1' or '0' but can be something else.
302
	 *
303
	 * @param string $value Field value.
304
	 *
305
	 * @return bool|string
306
	 */
307 2
	public function parse_bool_field( $value ) {
308 2
		if ( '0' === $value ) {
309 2
			return false;
310
		}
311
312 2
		if ( '1' === $value ) {
313 2
			return true;
314
		}
315
316
		// Don't return explicit true or false for empty fields or values like 'notify'.
317 2
		return wc_clean( $value );
318
	}
319
320
	/**
321
	 * Parse a float value field.
322
	 *
323
	 * @param string $value Field value.
324
	 *
325
	 * @return float|string
326
	 */
327 2
	public function parse_float_field( $value ) {
328 2
		if ( '' === $value ) {
329 2
			return $value;
330
		}
331
332
		// Remove the ' prepended to fields that start with - if needed.
333 2
		$value = $this->unescape_data( $value );
334
335 2
		return floatval( $value );
336
	}
337
338
	/**
339
	 * Parse the stock qty field.
340
	 *
341
	 * @param string $value Field value.
342
	 *
343
	 * @return float|string
344
	 */
345 2
	public function parse_stock_quantity_field( $value ) {
346 2
		if ( '' === $value ) {
347 2
			return $value;
348
		}
349
350
		// Remove the ' prepended to fields that start with - if needed.
351 2
		$value = $this->unescape_data( $value );
352
353 2
		return wc_stock_amount( $value );
354
	}
355
356
	/**
357
	 * Parse a category field from a CSV.
358
	 * Categories are separated by commas and subcategories are "parent > subcategory".
359
	 *
360
	 * @param string $value Field value.
361
	 *
362
	 * @return array of arrays with "parent" and "name" keys.
363
	 */
364 2
	public function parse_categories_field( $value ) {
365 2
		if ( empty( $value ) ) {
366 2
			return array();
367
		}
368
369 2
		$row_terms  = $this->explode_values( $value );
370 2
		$categories = array();
371
372 2
		foreach ( $row_terms as $row_term ) {
373 2
			$parent = null;
374 2
			$_terms = array_map( 'trim', explode( '>', $row_term ) );
375 2
			$total  = count( $_terms );
376
377 2
			foreach ( $_terms as $index => $_term ) {
378
				// Check if category exists. Parent must be empty string or null if doesn't exists.
379 2
				$term = term_exists( $_term, 'product_cat', $parent );
380
381 2
				if ( is_array( $term ) ) {
382
					$term_id = $term['term_id'];
383
					// Don't allow users without capabilities to create new categories.
384 2
				} elseif ( ! current_user_can( 'manage_product_terms' ) ) {
385 2
					break;
386
				} else {
387
					$term = wp_insert_term( $_term, 'product_cat', array( 'parent' => intval( $parent ) ) );
388
389
					if ( is_wp_error( $term ) ) {
390
						break; // We cannot continue if the term cannot be inserted.
391
					}
392
393
					$term_id = $term['term_id'];
394
				}
395
396
				// Only requires assign the last category.
397
				if ( ( 1 + $index ) === $total ) {
398
					$categories[] = $term_id;
399
				} else {
400
					// Store parent to be able to insert or query categories based in parent ID.
401
					$parent = $term_id;
402
				}
403
			}
404
		}
405
406 2
		return $categories;
407
	}
408
409
	/**
410
	 * Parse a tag field from a CSV.
411
	 *
412
	 * @param string $value Field value.
413
	 *
414
	 * @return array
415
	 */
416 2
	public function parse_tags_field( $value ) {
417 2
		if ( empty( $value ) ) {
418 2
			return array();
419
		}
420
421 2
		$value = $this->unescape_data( $value );
422 2
		$names = $this->explode_values( $value );
423 2
		$tags  = array();
424
425 2
		foreach ( $names as $name ) {
426 2
			$term = get_term_by( 'name', $name, 'product_tag' );
427
428 2
			if ( ! $term || is_wp_error( $term ) ) {
429 2
				$term = (object) wp_insert_term( $name, 'product_tag' );
430
			}
431
432 2
			if ( ! is_wp_error( $term ) ) {
433 2
				$tags[] = $term->term_id;
434
			}
435
		}
436
437 2
		return $tags;
438
	}
439
440
	/**
441
	 * Parse a shipping class field from a CSV.
442
	 *
443
	 * @param string $value Field value.
444
	 *
445
	 * @return int
446
	 */
447 2
	public function parse_shipping_class_field( $value ) {
448 2
		if ( empty( $value ) ) {
449 2
			return 0;
450
		}
451
452
		$term = get_term_by( 'name', $value, 'product_shipping_class' );
453
454
		if ( ! $term || is_wp_error( $term ) ) {
455
			$term = (object) wp_insert_term( $value, 'product_shipping_class' );
456
		}
457
458
		if ( is_wp_error( $term ) ) {
459
			return 0;
460
		}
461
462
		return $term->term_id;
463
	}
464
465
	/**
466
	 * Parse images list from a CSV. Images can be filenames or URLs.
467
	 *
468
	 * @param string $value Field value.
469
	 *
470
	 * @return array
471
	 */
472 2
	public function parse_images_field( $value ) {
473 2
		if ( empty( $value ) ) {
474 2
			return array();
475
		}
476
477 2
		$images = array();
478
479 2
		foreach ( $this->explode_values( $value ) as $image ) {
480 2
			if ( stristr( $image, '://' ) ) {
481 2
				$images[] = esc_url_raw( $image );
482
			} else {
483
				$images[] = sanitize_file_name( $image );
484
			}
485
		}
486
487 2
		return $images;
488
	}
489
490
	/**
491
	 * Parse dates from a CSV.
492
	 * Dates requires the format YYYY-MM-DD and time is optional.
493
	 *
494
	 * @param string $value Field value.
495
	 *
496
	 * @return string|null
497
	 */
498 2
	public function parse_date_field( $value ) {
499 2
		if ( empty( $value ) ) {
500 2
			return null;
501
		}
502
503 2
		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 ) ) {
504
			// Don't include the time if the field had time in it.
505 2
			return current( explode( ' ', $value ) );
506
		}
507
508
		return null;
509
	}
510
511
	/**
512
	 * Parse backorders from a CSV.
513
	 *
514
	 * @param string $value Field value.
515
	 *
516
	 * @return string
517
	 */
518 2
	public function parse_backorders_field( $value ) {
519 2
		if ( empty( $value ) ) {
520 2
			return 'no';
521
		}
522
523 2
		$value = $this->parse_bool_field( $value );
524
525 2
		if ( 'notify' === $value ) {
526 2
			return 'notify';
527 2
		} elseif ( is_bool( $value ) ) {
528 2
			return $value ? 'yes' : 'no';
529
		}
530
531
		return 'no';
532
	}
533
534
	/**
535
	 * Just skip current field.
536
	 *
537
	 * By default is applied wc_clean() to all not listed fields
538
	 * in self::get_formating_callback(), use this method to skip any formating.
539
	 *
540
	 * @param string $value Field value.
541
	 *
542
	 * @return string
543
	 */
544 2
	public function parse_skip_field( $value ) {
545 2
		return $value;
546
	}
547
548
	/**
549
	 * Parse download file urls, we should allow shortcodes here.
550
	 *
551
	 * Allow shortcodes if present, othersiwe esc_url the value.
552
	 *
553
	 * @param string $value Field value.
554
	 *
555
	 * @return string
556
	 */
557 2
	public function parse_download_file_field( $value ) {
558
		// Absolute file paths.
559 2
		if ( 0 === strpos( $value, 'http' ) ) {
560 2
			return esc_url_raw( $value );
561
		}
562
		// Relative and shortcode paths.
563 2
		return wc_clean( $value );
564
	}
565
566
	/**
567
	 * Parse an int value field
568
	 *
569
	 * @param int $value field value.
570
	 *
571
	 * @return int
572
	 */
573 2
	public function parse_int_field( $value ) {
574
		// Remove the ' prepended to fields that start with - if needed.
575 2
		$value = $this->unescape_data( $value );
576
577 2
		return intval( $value );
578
	}
579
580
	/**
581
	 * Parse a description value field
582
	 *
583
	 * @param string $description field value.
584
	 *
585
	 * @return string
586
	 */
587 2
	public function parse_description_field( $description ) {
588 2
		$parts = explode( "\\\\n", $description );
589 2
		foreach ( $parts as $key => $part ) {
590 2
			$parts[ $key ] = str_replace( '\n', "\n", $part );
591
		}
592
593 2
		return implode( '\\\n', $parts );
594
	}
595
596
	/**
597
	 * Parse the published field. 1 is published, 0 is private, -1 is draft.
598
	 * Alternatively, 'true' can be used for published and 'false' for draft.
599
	 *
600
	 * @param string $value Field value.
601
	 *
602
	 * @return float|string
603
	 */
604 2
	public function parse_published_field( $value ) {
605 2
		if ( '' === $value ) {
606
			return $value;
607
		}
608
609
		// Remove the ' prepended to fields that start with - if needed.
610 2
		$value = $this->unescape_data( $value );
611
612 2
		if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) {
613
			return wc_string_to_bool( $value ) ? 1 : -1;
614
		}
615
616 2
		return floatval( $value );
617
	}
618
619
	/**
620
	 * Get formatting callback.
621
	 *
622
	 * @return array
623
	 */
624 2
	protected function get_formating_callback() {
625
626
		/**
627
		 * Columns not mentioned here will get parsed with 'wc_clean'.
628
		 * column_name => callback.
629
		 */
630
		$data_formatting = array(
631 2
			'id'                => array( $this, 'parse_id_field' ),
632 2
			'type'              => array( $this, 'parse_comma_field' ),
633 2
			'published'         => array( $this, 'parse_published_field' ),
634 2
			'featured'          => array( $this, 'parse_bool_field' ),
635 2
			'date_on_sale_from' => array( $this, 'parse_date_field' ),
636 2
			'date_on_sale_to'   => array( $this, 'parse_date_field' ),
637 2
			'name'              => array( $this, 'parse_skip_field' ),
638 2
			'short_description' => array( $this, 'parse_description_field' ),
639 2
			'description'       => array( $this, 'parse_description_field' ),
640 2
			'manage_stock'      => array( $this, 'parse_bool_field' ),
641 2
			'low_stock_amount'  => array( $this, 'parse_stock_quantity_field' ),
642 2
			'backorders'        => array( $this, 'parse_backorders_field' ),
643 2
			'stock_status'      => array( $this, 'parse_bool_field' ),
644 2
			'sold_individually' => array( $this, 'parse_bool_field' ),
645 2
			'width'             => array( $this, 'parse_float_field' ),
646 2
			'length'            => array( $this, 'parse_float_field' ),
647 2
			'height'            => array( $this, 'parse_float_field' ),
648 2
			'weight'            => array( $this, 'parse_float_field' ),
649 2
			'reviews_allowed'   => array( $this, 'parse_bool_field' ),
650 2
			'purchase_note'     => 'wp_filter_post_kses',
651 2
			'price'             => 'wc_format_decimal',
652 2
			'regular_price'     => 'wc_format_decimal',
653 2
			'stock_quantity'    => array( $this, 'parse_stock_quantity_field' ),
654 2
			'category_ids'      => array( $this, 'parse_categories_field' ),
655 2
			'tag_ids'           => array( $this, 'parse_tags_field' ),
656 2
			'shipping_class_id' => array( $this, 'parse_shipping_class_field' ),
657 2
			'images'            => array( $this, 'parse_images_field' ),
658 2
			'parent_id'         => array( $this, 'parse_relative_field' ),
659 2
			'grouped_products'  => array( $this, 'parse_relative_comma_field' ),
660 2
			'upsell_ids'        => array( $this, 'parse_relative_comma_field' ),
661 2
			'cross_sell_ids'    => array( $this, 'parse_relative_comma_field' ),
662 2
			'download_limit'    => array( $this, 'parse_int_field' ),
663 2
			'download_expiry'   => array( $this, 'parse_int_field' ),
664 2
			'product_url'       => 'esc_url_raw',
665 2
			'menu_order'        => 'intval',
666
		);
667
668
		/**
669
		 * Match special column names.
670
		 */
671
		$regex_match_data_formatting = array(
672 2
			'/attributes:value*/'    => array( $this, 'parse_comma_field' ),
673 2
			'/attributes:visible*/'  => array( $this, 'parse_bool_field' ),
674 2
			'/attributes:taxonomy*/' => array( $this, 'parse_bool_field' ),
675 2
			'/downloads:url*/'       => array( $this, 'parse_download_file_field' ),
676 2
			'/meta:*/'               => 'wp_kses_post', // Allow some HTML in meta fields.
677
		);
678
679 2
		$callbacks = array();
680
681
		// Figure out the parse function for each column.
682 2
		foreach ( $this->get_mapped_keys() as $index => $heading ) {
683 2
			$callback = 'wc_clean';
684
685 2
			if ( isset( $data_formatting[ $heading ] ) ) {
686 2
				$callback = $data_formatting[ $heading ];
687
			} else {
688 2
				foreach ( $regex_match_data_formatting as $regex => $callback ) {
689 2
					if ( preg_match( $regex, $heading ) ) {
690 2
						$callback = $callback;
0 ignored issues
show
Bug introduced by
Why assign $callback to itself?

This checks looks for cases where a variable has been assigned to itself.

This assignement can be removed without consequences.

Loading history...
691 2
						break;
692
					}
693
				}
694
			}
695
696 2
			$callbacks[] = $callback;
697
		}
698
699 2
		return apply_filters( 'woocommerce_product_importer_formatting_callbacks', $callbacks, $this );
700
	}
701
702
	/**
703
	 * Check if strings starts with determined word.
704
	 *
705
	 * @param string $haystack Complete sentence.
706
	 * @param string $needle   Excerpt.
707
	 *
708
	 * @return bool
709
	 */
710 2
	protected function starts_with( $haystack, $needle ) {
711 2
		return substr( $haystack, 0, strlen( $needle ) ) === $needle;
712
	}
713
714
	/**
715
	 * Expand special and internal data into the correct formats for the product CRUD.
716
	 *
717
	 * @param array $data Data to import.
718
	 *
719
	 * @return array
720
	 */
721 2
	protected function expand_data( $data ) {
722 2
		$data = apply_filters( 'woocommerce_product_importer_pre_expand_data', $data );
723
724
		// Images field maps to image and gallery id fields.
725 2
		if ( isset( $data['images'] ) ) {
726 2
			$images               = $data['images'];
727 2
			$data['raw_image_id'] = array_shift( $images );
728
729 2
			if ( ! empty( $images ) ) {
730 2
				$data['raw_gallery_image_ids'] = $images;
731
			}
732 2
			unset( $data['images'] );
733
		}
734
735
		// Type, virtual and downloadable are all stored in the same column.
736 2
		if ( isset( $data['type'] ) ) {
737 2
			$data['type']         = array_map( 'strtolower', $data['type'] );
738 2
			$data['virtual']      = in_array( 'virtual', $data['type'], true );
739 2
			$data['downloadable'] = in_array( 'downloadable', $data['type'], true );
740
741
			// Convert type to string.
742 2
			$data['type'] = current( array_diff( $data['type'], array( 'virtual', 'downloadable' ) ) );
743
		}
744
745
		// Status is mapped from a special published field.
746 2
		if ( isset( $data['published'] ) ) {
747
			$statuses       = array(
748 2
				-1 => 'draft',
749
				0  => 'private',
750
				1  => 'publish',
751
			);
752 2
			$data['status'] = isset( $statuses[ $data['published'] ] ) ? $statuses[ $data['published'] ] : -1;
753
754 2
			unset( $data['published'] );
755
		}
756
757 2
		if ( isset( $data['stock_quantity'] ) ) {
758 2
			if ( '' === $data['stock_quantity'] ) {
759 2
				$data['manage_stock'] = false;
760 2
				$data['stock_status'] = isset( $data['stock_status'] ) ? $data['stock_status'] : true;
761
			} else {
762 2
				$data['manage_stock'] = true;
763
			}
764
		}
765
766
		// Stock is bool or 'backorder'.
767 2
		if ( isset( $data['stock_status'] ) ) {
768 2
			if ( 'backorder' === $data['stock_status'] ) {
769
				$data['stock_status'] = 'onbackorder';
770
			} else {
771 2
				$data['stock_status'] = $data['stock_status'] ? 'instock' : 'outofstock';
772
			}
773
		}
774
775
		// Prepare grouped products.
776 2
		if ( isset( $data['grouped_products'] ) ) {
777 2
			$data['children'] = $data['grouped_products'];
778 2
			unset( $data['grouped_products'] );
779
		}
780
781
		// Handle special column names which span multiple columns.
782 2
		$attributes = array();
783 2
		$downloads  = array();
784 2
		$meta_data  = array();
785
786 2
		foreach ( $data as $key => $value ) {
787 2
			if ( $this->starts_with( $key, 'attributes:name' ) ) {
788 2
				if ( ! empty( $value ) ) {
789 2
					$attributes[ str_replace( 'attributes:name', '', $key ) ]['name'] = $value;
790
				}
791 2
				unset( $data[ $key ] );
792
793 2
			} elseif ( $this->starts_with( $key, 'attributes:value' ) ) {
794 2
				$attributes[ str_replace( 'attributes:value', '', $key ) ]['value'] = $value;
795 2
				unset( $data[ $key ] );
796
797 2
			} elseif ( $this->starts_with( $key, 'attributes:taxonomy' ) ) {
798
				$attributes[ str_replace( 'attributes:taxonomy', '', $key ) ]['taxonomy'] = wc_string_to_bool( $value );
799
				unset( $data[ $key ] );
800
801 2
			} elseif ( $this->starts_with( $key, 'attributes:visible' ) ) {
802
				$attributes[ str_replace( 'attributes:visible', '', $key ) ]['visible'] = wc_string_to_bool( $value );
803
				unset( $data[ $key ] );
804
805 2
			} elseif ( $this->starts_with( $key, 'attributes:default' ) ) {
806 2
				if ( ! empty( $value ) ) {
807 2
					$attributes[ str_replace( 'attributes:default', '', $key ) ]['default'] = $value;
808
				}
809 2
				unset( $data[ $key ] );
810
811 2
			} elseif ( $this->starts_with( $key, 'downloads:name' ) ) {
812 2
				if ( ! empty( $value ) ) {
813 2
					$downloads[ str_replace( 'downloads:name', '', $key ) ]['name'] = $value;
814
				}
815 2
				unset( $data[ $key ] );
816
817 2
			} elseif ( $this->starts_with( $key, 'downloads:url' ) ) {
818 2
				if ( ! empty( $value ) ) {
819 2
					$downloads[ str_replace( 'downloads:url', '', $key ) ]['url'] = $value;
820
				}
821 2
				unset( $data[ $key ] );
822
823 2
			} elseif ( $this->starts_with( $key, 'meta:' ) ) {
824
				$meta_data[] = array(
825
					'key'   => str_replace( 'meta:', '', $key ),
826
					'value' => $value,
827
				);
828
				unset( $data[ $key ] );
829
			}
830
		}
831
832 2
		if ( ! empty( $attributes ) ) {
833
			// Remove empty attributes and clear indexes.
834 2
			foreach ( $attributes as $attribute ) {
835 2
				if ( empty( $attribute['name'] ) ) {
836 2
					continue;
837
				}
838
839 2
				$data['raw_attributes'][] = $attribute;
840
			}
841
		}
842
843 2
		if ( ! empty( $downloads ) ) {
844 2
			$data['downloads'] = array();
845
846 2
			foreach ( $downloads as $key => $file ) {
847 2
				if ( empty( $file['url'] ) ) {
848
					continue;
849
				}
850
851 2
				$data['downloads'][] = array(
852 2
					'name' => $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['url'] ),
853 2
					'file' => $file['url'],
854
				);
855
			}
856
		}
857
858 2
		if ( ! empty( $meta_data ) ) {
859
			$data['meta_data'] = $meta_data;
860
		}
861
862 2
		return $data;
863
	}
864
865
	/**
866
	 * Map and format raw data to known fields.
867
	 */
868 2
	protected function set_parsed_data() {
869 2
		$parse_functions = $this->get_formating_callback();
870 2
		$mapped_keys     = $this->get_mapped_keys();
871 2
		$use_mb          = function_exists( 'mb_convert_encoding' );
872
873
		// Parse the data.
874 2
		foreach ( $this->raw_data as $row_index => $row ) {
875
			// Skip empty rows.
876 2
			if ( ! count( array_filter( $row ) ) ) {
877
				continue;
878
			}
879
880 2
			$this->parsing_raw_data_index = $row_index;
0 ignored issues
show
Documentation Bug introduced by
It seems like $row_index can also be of type string. However, the property $parsing_raw_data_index is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
881
882 2
			$data = array();
883
884 2
			do_action( 'woocommerce_product_importer_before_set_parsed_data', $row, $mapped_keys );
885
886 2
			foreach ( $row as $id => $value ) {
887
				// Skip ignored columns.
888 2
				if ( empty( $mapped_keys[ $id ] ) ) {
889
					continue;
890
				}
891
892
				// Convert UTF8.
893 2
				if ( $use_mb ) {
894 2
					$encoding = mb_detect_encoding( $value, mb_detect_order(), true );
895 2
					if ( $encoding ) {
896 2
						$value = mb_convert_encoding( $value, 'UTF-8', $encoding );
897
					} else {
898 2
						$value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' );
899
					}
900
				} else {
901
					$value = wp_check_invalid_utf8( $value, true );
902
				}
903
904 2
				$data[ $mapped_keys[ $id ] ] = call_user_func( $parse_functions[ $id ], $value );
905
			}
906
907 2
			$this->parsed_data[] = apply_filters( 'woocommerce_product_importer_parsed_data', $this->expand_data( $data ), $this );
908
		}
909
	}
910
911
	/**
912
	 * Get a string to identify the row from parsed data.
913
	 *
914
	 * @param array $parsed_data Parsed data.
915
	 *
916
	 * @return string
917
	 */
918
	protected function get_row_id( $parsed_data ) {
919
		$id       = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0;
920
		$sku      = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : '';
921
		$name     = isset( $parsed_data['name'] ) ? esc_attr( $parsed_data['name'] ) : '';
922
		$row_data = array();
923
924
		if ( $name ) {
925
			$row_data[] = $name;
926
		}
927
		if ( $id ) {
928
			/* translators: %d: product ID */
929
			$row_data[] = sprintf( __( 'ID %d', 'woocommerce' ), $id );
930
		}
931
		if ( $sku ) {
932
			/* translators: %s: product SKU */
933
			$row_data[] = sprintf( __( 'SKU %s', 'woocommerce' ), $sku );
934
		}
935
936
		return implode( ', ', $row_data );
937
	}
938
939
	/**
940
	 * Process importer.
941
	 *
942
	 * Do not import products with IDs or SKUs that already exist if option
943
	 * update existing is false, and likewise, if updating products, do not
944
	 * process rows which do not exist if an ID/SKU is provided.
945
	 *
946
	 * @return array
947
	 */
948 1
	public function import() {
949 1
		$this->start_time = time();
950 1
		$index            = 0;
951 1
		$update_existing  = $this->params['update_existing'];
952
		$data             = array(
953 1
			'imported' => array(),
954
			'failed'   => array(),
955
			'updated'  => array(),
956
			'skipped'  => array(),
957
		);
958
959 1
		foreach ( $this->parsed_data as $parsed_data_key => $parsed_data ) {
960 1
			do_action( 'woocommerce_product_import_before_import', $parsed_data );
961
962 1
			$id         = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0;
963 1
			$sku        = isset( $parsed_data['sku'] ) ? $parsed_data['sku'] : '';
964 1
			$id_exists  = false;
965 1
			$sku_exists = false;
966
967 1
			if ( $id ) {
968
				$product   = wc_get_product( $id );
969
				$id_exists = $product && 'importing' !== $product->get_status();
970
			}
971
972 1
			if ( $sku ) {
973 1
				$id_from_sku = wc_get_product_id_by_sku( $sku );
974 1
				$product     = $id_from_sku ? wc_get_product( $id_from_sku ) : false;
975 1
				$sku_exists  = $product && 'importing' !== $product->get_status();
976
			}
977
978 1 View Code Duplication
			if ( $id_exists && ! $update_existing ) {
979
				$data['skipped'][] = new WP_Error(
980
					'woocommerce_product_importer_error',
981
					esc_html__( 'A product with this ID already exists.', 'woocommerce' ),
982
					array(
983
						'id'  => $id,
984
						'row' => $this->get_row_id( $parsed_data ),
985
					)
986
				);
987
				continue;
988
			}
989
990 1 View Code Duplication
			if ( $sku_exists && ! $update_existing ) {
991
				$data['skipped'][] = new WP_Error(
992
					'woocommerce_product_importer_error',
993
					esc_html__( 'A product with this SKU already exists.', 'woocommerce' ),
994
					array(
995
						'sku' => esc_attr( $sku ),
996
						'row' => $this->get_row_id( $parsed_data ),
997
					)
998
				);
999
				continue;
1000
			}
1001
1002 1
			if ( $update_existing && ( $id || $sku ) && ! $id_exists && ! $sku_exists ) {
1003
				$data['skipped'][] = new WP_Error(
1004
					'woocommerce_product_importer_error',
1005
					esc_html__( 'No matching product exists to update.', 'woocommerce' ),
1006
					array(
1007
						'id'  => $id,
1008
						'sku' => esc_attr( $sku ),
1009
						'row' => $this->get_row_id( $parsed_data ),
1010
					)
1011
				);
1012
				continue;
1013
			}
1014
1015 1
			$result = $this->process_item( $parsed_data );
1016
1017 1
			if ( is_wp_error( $result ) ) {
1018
				$result->add_data( array( 'row' => $this->get_row_id( $parsed_data ) ) );
1019
				$data['failed'][] = $result;
1020 1
			} elseif ( $result['updated'] ) {
1021
				$data['updated'][] = $result['id'];
1022
			} else {
1023 1
				$data['imported'][] = $result['id'];
1024
			}
1025
1026 1
			$index ++;
1027
1028 1
			if ( $this->params['prevent_timeouts'] && ( $this->time_exceeded() || $this->memory_exceeded() ) ) {
1029
				$this->file_position = $this->file_positions[ $index ];
1030
				break;
1031
			}
1032
		}
1033
1034 1
		return $data;
1035
	}
1036
}
1037