class-wc-product-csv-importer.php 27 KB

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