Keine Beschreibung

abstract-wc-csv-exporter.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. <?php
  2. /**
  3. * Handles CSV export.
  4. *
  5. * @package WooCommerce\Export
  6. * @version 3.1.0
  7. */
  8. if ( ! defined( 'ABSPATH' ) ) {
  9. exit;
  10. }
  11. /**
  12. * WC_CSV_Exporter Class.
  13. */
  14. abstract class WC_CSV_Exporter {
  15. /**
  16. * Type of export used in filter names.
  17. *
  18. * @var string
  19. */
  20. protected $export_type = '';
  21. /**
  22. * Filename to export to.
  23. *
  24. * @var string
  25. */
  26. protected $filename = 'wc-export.csv';
  27. /**
  28. * Batch limit.
  29. *
  30. * @var integer
  31. */
  32. protected $limit = 50;
  33. /**
  34. * Number exported.
  35. *
  36. * @var integer
  37. */
  38. protected $exported_row_count = 0;
  39. /**
  40. * Raw data to export.
  41. *
  42. * @var array
  43. */
  44. protected $row_data = array();
  45. /**
  46. * Total rows to export.
  47. *
  48. * @var integer
  49. */
  50. protected $total_rows = 0;
  51. /**
  52. * Columns ids and names.
  53. *
  54. * @var array
  55. */
  56. protected $column_names = array();
  57. /**
  58. * List of columns to export, or empty for all.
  59. *
  60. * @var array
  61. */
  62. protected $columns_to_export = array();
  63. /**
  64. * The delimiter parameter sets the field delimiter (one character only).
  65. *
  66. * @var string
  67. */
  68. protected $delimiter = ',';
  69. /**
  70. * Prepare data that will be exported.
  71. */
  72. abstract public function prepare_data_to_export();
  73. /**
  74. * Return an array of supported column names and ids.
  75. *
  76. * @since 3.1.0
  77. * @return array
  78. */
  79. public function get_column_names() {
  80. return apply_filters( "woocommerce_{$this->export_type}_export_column_names", $this->column_names, $this );
  81. }
  82. /**
  83. * Set column names.
  84. *
  85. * @since 3.1.0
  86. * @param array $column_names Column names array.
  87. */
  88. public function set_column_names( $column_names ) {
  89. $this->column_names = array();
  90. foreach ( $column_names as $column_id => $column_name ) {
  91. $this->column_names[ wc_clean( $column_id ) ] = wc_clean( $column_name );
  92. }
  93. }
  94. /**
  95. * Return an array of columns to export.
  96. *
  97. * @since 3.1.0
  98. * @return array
  99. */
  100. public function get_columns_to_export() {
  101. return $this->columns_to_export;
  102. }
  103. /**
  104. * Return the delimiter to use in CSV file
  105. *
  106. * @since 3.9.0
  107. * @return string
  108. */
  109. public function get_delimiter() {
  110. return apply_filters( "woocommerce_{$this->export_type}_export_delimiter", $this->delimiter );
  111. }
  112. /**
  113. * Set columns to export.
  114. *
  115. * @since 3.1.0
  116. * @param array $columns Columns array.
  117. */
  118. public function set_columns_to_export( $columns ) {
  119. $this->columns_to_export = array_map( 'wc_clean', $columns );
  120. }
  121. /**
  122. * See if a column is to be exported or not.
  123. *
  124. * @since 3.1.0
  125. * @param string $column_id ID of the column being exported.
  126. * @return boolean
  127. */
  128. public function is_column_exporting( $column_id ) {
  129. $column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id;
  130. $columns_to_export = $this->get_columns_to_export();
  131. if ( empty( $columns_to_export ) ) {
  132. return true;
  133. }
  134. if ( in_array( $column_id, $columns_to_export, true ) || 'meta' === $column_id ) {
  135. return true;
  136. }
  137. return false;
  138. }
  139. /**
  140. * Return default columns.
  141. *
  142. * @since 3.1.0
  143. * @return array
  144. */
  145. public function get_default_column_names() {
  146. return array();
  147. }
  148. /**
  149. * Do the export.
  150. *
  151. * @since 3.1.0
  152. */
  153. public function export() {
  154. $this->prepare_data_to_export();
  155. $this->send_headers();
  156. $this->send_content( chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers() . $this->get_csv_data() );
  157. die();
  158. }
  159. /**
  160. * Set the export headers.
  161. *
  162. * @since 3.1.0
  163. */
  164. public function send_headers() {
  165. if ( function_exists( 'gc_enable' ) ) {
  166. gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound
  167. }
  168. if ( function_exists( 'apache_setenv' ) ) {
  169. @apache_setenv( 'no-gzip', 1 ); // @codingStandardsIgnoreLine
  170. }
  171. @ini_set( 'zlib.output_compression', 'Off' ); // @codingStandardsIgnoreLine
  172. @ini_set( 'output_buffering', 'Off' ); // @codingStandardsIgnoreLine
  173. @ini_set( 'output_handler', '' ); // @codingStandardsIgnoreLine
  174. ignore_user_abort( true );
  175. wc_set_time_limit( 0 );
  176. wc_nocache_headers();
  177. header( 'Content-Type: text/csv; charset=utf-8' );
  178. header( 'Content-Disposition: attachment; filename=' . $this->get_filename() );
  179. header( 'Pragma: no-cache' );
  180. header( 'Expires: 0' );
  181. }
  182. /**
  183. * Set filename to export to.
  184. *
  185. * @param string $filename Filename to export to.
  186. */
  187. public function set_filename( $filename ) {
  188. $this->filename = sanitize_file_name( str_replace( '.csv', '', $filename ) . '.csv' );
  189. }
  190. /**
  191. * Generate and return a filename.
  192. *
  193. * @return string
  194. */
  195. public function get_filename() {
  196. return sanitize_file_name( apply_filters( "woocommerce_{$this->export_type}_export_get_filename", $this->filename ) );
  197. }
  198. /**
  199. * Set the export content.
  200. *
  201. * @since 3.1.0
  202. * @param string $csv_data All CSV content.
  203. */
  204. public function send_content( $csv_data ) {
  205. echo $csv_data; // @codingStandardsIgnoreLine
  206. }
  207. /**
  208. * Get CSV data for this export.
  209. *
  210. * @since 3.1.0
  211. * @return string
  212. */
  213. protected function get_csv_data() {
  214. return $this->export_rows();
  215. }
  216. /**
  217. * Export column headers in CSV format.
  218. *
  219. * @since 3.1.0
  220. * @return string
  221. */
  222. protected function export_column_headers() {
  223. $columns = $this->get_column_names();
  224. $export_row = array();
  225. $buffer = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
  226. ob_start();
  227. foreach ( $columns as $column_id => $column_name ) {
  228. if ( ! $this->is_column_exporting( $column_id ) ) {
  229. continue;
  230. }
  231. $export_row[] = $this->format_data( $column_name );
  232. }
  233. $this->fputcsv( $buffer, $export_row );
  234. return ob_get_clean();
  235. }
  236. /**
  237. * Get data that will be exported.
  238. *
  239. * @since 3.1.0
  240. * @return array
  241. */
  242. protected function get_data_to_export() {
  243. return $this->row_data;
  244. }
  245. /**
  246. * Export rows in CSV format.
  247. *
  248. * @since 3.1.0
  249. * @return string
  250. */
  251. protected function export_rows() {
  252. $data = $this->get_data_to_export();
  253. $buffer = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
  254. ob_start();
  255. array_walk( $data, array( $this, 'export_row' ), $buffer );
  256. return apply_filters( "woocommerce_{$this->export_type}_export_rows", ob_get_clean(), $this );
  257. }
  258. /**
  259. * Export rows to an array ready for the CSV.
  260. *
  261. * @since 3.1.0
  262. * @param array $row_data Data to export.
  263. * @param string $key Column being exported.
  264. * @param resource $buffer Output buffer.
  265. */
  266. protected function export_row( $row_data, $key, $buffer ) {
  267. $columns = $this->get_column_names();
  268. $export_row = array();
  269. foreach ( $columns as $column_id => $column_name ) {
  270. if ( ! $this->is_column_exporting( $column_id ) ) {
  271. continue;
  272. }
  273. if ( isset( $row_data[ $column_id ] ) ) {
  274. $export_row[] = $this->format_data( $row_data[ $column_id ] );
  275. } else {
  276. $export_row[] = '';
  277. }
  278. }
  279. $this->fputcsv( $buffer, $export_row );
  280. ++ $this->exported_row_count;
  281. }
  282. /**
  283. * Get batch limit.
  284. *
  285. * @since 3.1.0
  286. * @return int
  287. */
  288. public function get_limit() {
  289. return apply_filters( "woocommerce_{$this->export_type}_export_batch_limit", $this->limit, $this );
  290. }
  291. /**
  292. * Set batch limit.
  293. *
  294. * @since 3.1.0
  295. * @param int $limit Limit to export.
  296. */
  297. public function set_limit( $limit ) {
  298. $this->limit = absint( $limit );
  299. }
  300. /**
  301. * Get count of records exported.
  302. *
  303. * @since 3.1.0
  304. * @return int
  305. */
  306. public function get_total_exported() {
  307. return $this->exported_row_count;
  308. }
  309. /**
  310. * Escape a string to be used in a CSV context
  311. *
  312. * Malicious input can inject formulas into CSV files, opening up the possibility
  313. * for phishing attacks and disclosure of sensitive information.
  314. *
  315. * Additionally, Excel exposes the ability to launch arbitrary commands through
  316. * the DDE protocol.
  317. *
  318. * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
  319. * @see https://hackerone.com/reports/72785
  320. *
  321. * @since 3.1.0
  322. * @param string $data CSV field to escape.
  323. * @return string
  324. */
  325. public function escape_data( $data ) {
  326. $active_content_triggers = array( '=', '+', '-', '@' );
  327. if ( in_array( mb_substr( $data, 0, 1 ), $active_content_triggers, true ) ) {
  328. $data = "'" . $data;
  329. }
  330. return $data;
  331. }
  332. /**
  333. * Format and escape data ready for the CSV file.
  334. *
  335. * @since 3.1.0
  336. * @param string $data Data to format.
  337. * @return string
  338. */
  339. public function format_data( $data ) {
  340. if ( ! is_scalar( $data ) ) {
  341. if ( is_a( $data, 'WC_Datetime' ) ) {
  342. $data = $data->date( 'Y-m-d G:i:s' );
  343. } else {
  344. $data = ''; // Not supported.
  345. }
  346. } elseif ( is_bool( $data ) ) {
  347. $data = $data ? 1 : 0;
  348. }
  349. $use_mb = function_exists( 'mb_convert_encoding' );
  350. if ( $use_mb ) {
  351. $encoding = mb_detect_encoding( $data, 'UTF-8, ISO-8859-1', true );
  352. $data = 'UTF-8' === $encoding ? $data : utf8_encode( $data );
  353. }
  354. return $this->escape_data( $data );
  355. }
  356. /**
  357. * Format term ids to names.
  358. *
  359. * @since 3.1.0
  360. * @param array $term_ids Term IDs to format.
  361. * @param string $taxonomy Taxonomy name.
  362. * @return string
  363. */
  364. public function format_term_ids( $term_ids, $taxonomy ) {
  365. $term_ids = wp_parse_id_list( $term_ids );
  366. if ( ! count( $term_ids ) ) {
  367. return '';
  368. }
  369. $formatted_terms = array();
  370. if ( is_taxonomy_hierarchical( $taxonomy ) ) {
  371. foreach ( $term_ids as $term_id ) {
  372. $formatted_term = array();
  373. $ancestor_ids = array_reverse( get_ancestors( $term_id, $taxonomy ) );
  374. foreach ( $ancestor_ids as $ancestor_id ) {
  375. $term = get_term( $ancestor_id, $taxonomy );
  376. if ( $term && ! is_wp_error( $term ) ) {
  377. $formatted_term[] = $term->name;
  378. }
  379. }
  380. $term = get_term( $term_id, $taxonomy );
  381. if ( $term && ! is_wp_error( $term ) ) {
  382. $formatted_term[] = $term->name;
  383. }
  384. $formatted_terms[] = implode( ' > ', $formatted_term );
  385. }
  386. } else {
  387. foreach ( $term_ids as $term_id ) {
  388. $term = get_term( $term_id, $taxonomy );
  389. if ( $term && ! is_wp_error( $term ) ) {
  390. $formatted_terms[] = $term->name;
  391. }
  392. }
  393. }
  394. return $this->implode_values( $formatted_terms );
  395. }
  396. /**
  397. * Implode CSV cell values using commas by default, and wrapping values
  398. * which contain the separator.
  399. *
  400. * @since 3.2.0
  401. * @param array $values Values to implode.
  402. * @return string
  403. */
  404. protected function implode_values( $values ) {
  405. $values_to_implode = array();
  406. foreach ( $values as $value ) {
  407. $value = (string) is_scalar( $value ) ? $value : '';
  408. $values_to_implode[] = str_replace( ',', '\\,', $value );
  409. }
  410. return implode( ', ', $values_to_implode );
  411. }
  412. /**
  413. * Write to the CSV file, ensuring escaping works across versions of
  414. * PHP.
  415. *
  416. * PHP 5.5.4 uses '\' as the default escape character. This is not RFC-4180 compliant.
  417. * \0 disables the escape character.
  418. *
  419. * @see https://bugs.php.net/bug.php?id=43225
  420. * @see https://bugs.php.net/bug.php?id=50686
  421. * @see https://github.com/woocommerce/woocommerce/issues/19514
  422. * @since 3.4.0
  423. * @see https://github.com/woocommerce/woocommerce/issues/24579
  424. * @since 3.9.0
  425. * @param resource $buffer Resource we are writing to.
  426. * @param array $export_row Row to export.
  427. */
  428. protected function fputcsv( $buffer, $export_row ) {
  429. if ( version_compare( PHP_VERSION, '5.5.4', '<' ) ) {
  430. ob_start();
  431. $temp = fopen( 'php://output', 'w' ); // @codingStandardsIgnoreLine
  432. fputcsv( $temp, $export_row, $this->get_delimiter(), '"' ); // @codingStandardsIgnoreLine
  433. fclose( $temp ); // @codingStandardsIgnoreLine
  434. $row = ob_get_clean();
  435. $row = str_replace( '\\"', '\\""', $row );
  436. fwrite( $buffer, $row ); // @codingStandardsIgnoreLine
  437. } else {
  438. fputcsv( $buffer, $export_row, $this->get_delimiter(), '"', "\0" ); // @codingStandardsIgnoreLine
  439. }
  440. }
  441. }