class-bulk-actions-manager.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. <?php
  2. // Handles custom bulk actions for orders.
  3. defined('ABSPATH') || exit;
  4. class Bulk_Actions_Manager {
  5. public function __construct() {
  6. add_filter('bulk_actions-woocommerce_page_wc-orders', array($this, 'register_bulk_actions'));
  7. add_filter('handle_bulk_actions-woocommerce_page_wc-orders', array($this, 'handle_bulk_actions'), 10, 3);
  8. }
  9. public function register_bulk_actions($bulk_actions) {
  10. $bulk_actions['prepare_to_printing_export'] = __('Prepare to Printing Export', 'studiou-wc-ord-print-statuses');
  11. $bulk_actions['set_status_to_prepare_to_printing'] = __('Set Status to Prepare to Printing', 'studiou-wc-ord-print-statuses');
  12. $bulk_actions['set_status_to_in_printing'] = __('Set Status to In Printing', 'studiou-wc-ord-print-statuses');
  13. return $bulk_actions;
  14. }
  15. public function handle_bulk_actions($redirect_to, $action, $post_ids) {
  16. $known = array(
  17. 'prepare_to_printing_export',
  18. 'set_status_to_prepare_to_printing',
  19. 'set_status_to_in_printing',
  20. );
  21. if (!in_array($action, $known, true)) {
  22. return $redirect_to;
  23. }
  24. if (!current_user_can('edit_shop_orders')) {
  25. wp_die(esc_html__('You do not have sufficient permissions to perform this action.', 'studiou-wc-ord-print-statuses'));
  26. }
  27. $post_ids = array_filter(array_map('intval', (array) $post_ids));
  28. if (empty($post_ids)) {
  29. return $redirect_to;
  30. }
  31. switch ($action) {
  32. case 'prepare_to_printing_export':
  33. return $this->prepare_to_printing_export($post_ids, $redirect_to);
  34. case 'set_status_to_prepare_to_printing':
  35. return $this->set_status_to_prepare_to_printing($post_ids, $redirect_to);
  36. case 'set_status_to_in_printing':
  37. return $this->set_status_to_in_printing($post_ids, $redirect_to);
  38. }
  39. return $redirect_to;
  40. }
  41. /**
  42. * Atomic order: pull rows, build the CSV in memory, then flip statuses, then
  43. * stream the file. If the export query returns nothing, statuses are still
  44. * advanced (the spec requires it) but the user is told no CSV was produced.
  45. */
  46. private function prepare_to_printing_export($post_ids, $redirect_to) {
  47. $orders_data = Studiou_DB_Manager::get_orders_for_printing($post_ids);
  48. $csv_data = !empty($orders_data) ? $this->generate_csv($orders_data) : '';
  49. $updated = Studiou_DB_Manager::set_orders_to_prepare_printing($post_ids);
  50. $this->notify_moved_count($updated, 'to-print');
  51. if ($csv_data === '') {
  52. UtilsLog::message(
  53. __('No exportable line items were found for the selected orders. CSV was not generated.', 'studiou-wc-ord-print-statuses'),
  54. 'warning'
  55. );
  56. return $redirect_to;
  57. }
  58. $this->output_csv($csv_data, 'prepare_to_printing_export.csv'); // exits
  59. }
  60. private function set_status_to_prepare_to_printing($post_ids, $redirect_to) {
  61. $updated = Studiou_DB_Manager::set_orders_to_prepare_printing($post_ids);
  62. $this->notify_moved_count($updated, 'to-print');
  63. return add_query_arg('bulk_action', 'marked_prepare_to_printing', $redirect_to);
  64. }
  65. private function set_status_to_in_printing($post_ids, $redirect_to) {
  66. $updated = Studiou_DB_Manager::set_orders_to_in_printing($post_ids);
  67. $this->notify_moved_count($updated, 'in-print');
  68. return add_query_arg('bulk_action', 'marked_in_printing', $redirect_to);
  69. }
  70. /**
  71. * Emit a "%d orders moved to …" notice unless zero orders were actually
  72. * changed — in that case the "skipped because terminal status" warning
  73. * from Studiou_DB_Manager is more informative on its own.
  74. */
  75. private function notify_moved_count($count, $slug) {
  76. if ($count <= 0) {
  77. return;
  78. }
  79. if ($slug === 'to-print') {
  80. $message = _n(
  81. '%d order moved to "Prepare to Printing".',
  82. '%d orders moved to "Prepare to Printing".',
  83. $count,
  84. 'studiou-wc-ord-print-statuses'
  85. );
  86. } else {
  87. $message = _n(
  88. '%d order moved to "In Printing".',
  89. '%d orders moved to "In Printing".',
  90. $count,
  91. 'studiou-wc-ord-print-statuses'
  92. );
  93. }
  94. UtilsLog::message(sprintf($message, $count), 'success');
  95. }
  96. private function generate_csv($data) {
  97. $output = fopen('php://temp', 'w+');
  98. // Explicit empty-string escape suppresses the PHP 8.4 deprecation
  99. // notice for the implicit '\\' default.
  100. fputcsv($output, array_keys((array) $data[0]), ',', '"', '');
  101. foreach ($data as $row) {
  102. fputcsv($output, (array) $row, ',', '"', '');
  103. }
  104. rewind($output);
  105. $csv = stream_get_contents($output);
  106. fclose($output);
  107. return $csv;
  108. }
  109. /**
  110. * Stream CSV with UTF-8 BOM so Excel on Windows reads diacritics correctly.
  111. * If output has already started (some upstream code printed something) we
  112. * cannot reliably stream a CSV download — bail with a clear error rather
  113. * than echoing CSV bytes into an HTML page.
  114. */
  115. private function output_csv($csv_data, $filename) {
  116. if (headers_sent($file, $line)) {
  117. UtilsLog::log(sprintf(
  118. 'output_csv: headers already sent from %s:%d — aborting CSV stream.',
  119. $file,
  120. $line
  121. ));
  122. wp_die(esc_html__(
  123. 'The CSV export could not be streamed because output had already started. Check for plugin/theme code that prints before headers are sent.',
  124. 'studiou-wc-ord-print-statuses'
  125. ));
  126. }
  127. nocache_headers();
  128. header('Content-Type: text/csv; charset=utf-8');
  129. header('Content-Disposition: attachment; filename="' . $filename . '"');
  130. echo "\xEF\xBB\xBF" . $csv_data; // UTF-8 BOM
  131. exit;
  132. }
  133. }