class-import-manager.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. <?php
  2. // Admin UI + handlers for importing the InPrint and Delivered protocol CSVs.
  3. defined('ABSPATH') || exit;
  4. class Import_Manager {
  5. public function __construct() {
  6. add_action('admin_menu', array($this, 'add_import_submenu'));
  7. add_action('admin_post_import_inprint_protocol', array($this, 'handle_import_inprint_protocol'));
  8. add_action('admin_post_import_delivered_protocol', array($this, 'handle_import_delivered_protocol'));
  9. }
  10. public function add_import_submenu() {
  11. $title = __('Import Protocols', 'studiou-wc-ord-print-statuses');
  12. // Use edit_shop_orders so Shop Managers (the typical operators of
  13. // the print pipeline) can run imports. The bulk actions on the
  14. // orders list use the same capability — keeping the two paths in
  15. // sync is the goal.
  16. add_submenu_page(
  17. 'woocommerce',
  18. $title,
  19. $title,
  20. 'edit_shop_orders',
  21. 'studiou-import-protocols',
  22. array($this, 'render_import_page')
  23. );
  24. }
  25. public function render_import_page() {
  26. ?>
  27. <div class="wrap">
  28. <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
  29. <h2><?php esc_html_e('Import InPrint Protocol', 'studiou-wc-ord-print-statuses'); ?></h2>
  30. <p>
  31. <?php
  32. printf(
  33. /* translators: list of required CSV header names. */
  34. esc_html__('Required CSV headers: %s', 'studiou-wc-ord-print-statuses'),
  35. '<code>order_no</code>, <code>externalorder</code>, <code>externalorderdate</code>'
  36. );
  37. ?>
  38. </p>
  39. <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" enctype="multipart/form-data">
  40. <input type="hidden" name="action" value="import_inprint_protocol">
  41. <?php wp_nonce_field('import_inprint_protocol', 'import_inprint_protocol_nonce'); ?>
  42. <input type="file" name="inprint_csv" accept=".csv,text/csv" required>
  43. <?php submit_button(__('Import InPrint Protocol', 'studiou-wc-ord-print-statuses')); ?>
  44. </form>
  45. <h2><?php esc_html_e('Import Delivered Protocol', 'studiou-wc-ord-print-statuses'); ?></h2>
  46. <p>
  47. <?php
  48. printf(
  49. /* translators: list of required CSV header names. */
  50. esc_html__('Required CSV headers: %s', 'studiou-wc-ord-print-statuses'),
  51. '<code>order_no</code>'
  52. );
  53. ?>
  54. </p>
  55. <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" enctype="multipart/form-data">
  56. <input type="hidden" name="action" value="import_delivered_protocol">
  57. <?php wp_nonce_field('import_delivered_protocol', 'import_delivered_protocol_nonce'); ?>
  58. <input type="file" name="delivered_csv" accept=".csv,text/csv" required>
  59. <?php submit_button(__('Import Delivered Protocol', 'studiou-wc-ord-print-statuses')); ?>
  60. </form>
  61. </div>
  62. <?php
  63. }
  64. /**
  65. * Map from admin-post action name → [file field, nonce field/action, DB handler callable].
  66. * Adding a new protocol is a single-entry change here.
  67. */
  68. private function protocol_dispatch() {
  69. return array(
  70. 'import_inprint_protocol' => array(
  71. 'file_field' => 'inprint_csv',
  72. 'nonce' => 'import_inprint_protocol_nonce',
  73. 'handler' => array('Studiou_DB_Manager', 'import_inprint_protocol'),
  74. ),
  75. 'import_delivered_protocol' => array(
  76. 'file_field' => 'delivered_csv',
  77. 'nonce' => 'import_delivered_protocol_nonce',
  78. 'handler' => array('Studiou_DB_Manager', 'import_delivered_protocol'),
  79. ),
  80. );
  81. }
  82. public function handle_import_inprint_protocol() {
  83. $this->run_import('import_inprint_protocol');
  84. }
  85. public function handle_import_delivered_protocol() {
  86. $this->run_import('import_delivered_protocol');
  87. }
  88. /**
  89. * Common pipeline: capability check → nonce → file validation → parse → dispatch → redirect.
  90. * Every non-success branch returns immediately via redirect_back() so static
  91. * analysis can prove that subsequent code is only reached on the happy path.
  92. */
  93. private function run_import($action) {
  94. $dispatch = $this->protocol_dispatch();
  95. if (!isset($dispatch[$action])) {
  96. return $this->redirect_back();
  97. }
  98. $cfg = $dispatch[$action];
  99. if (!current_user_can('edit_shop_orders')) {
  100. wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'studiou-wc-ord-print-statuses'));
  101. }
  102. check_admin_referer($action, $cfg['nonce']);
  103. $file = $this->validated_upload($cfg['file_field']);
  104. if ($file === null) {
  105. return $this->redirect_back();
  106. }
  107. $csv_data = Studiou_DB_Manager::parse_csv($file['tmp_name']);
  108. if (empty($csv_data)) {
  109. UtilsLog::message(
  110. __('CSV file could not be parsed or contained no data rows.', 'studiou-wc-ord-print-statuses'),
  111. 'error'
  112. );
  113. return $this->redirect_back();
  114. }
  115. $handler = $cfg['handler'];
  116. $result = $handler($csv_data);
  117. $this->emit_import_summary($result);
  118. return $this->redirect_back();
  119. }
  120. /**
  121. * Validate the file upload superglobal entry. Emits a notice and returns null on failure.
  122. *
  123. * @return array|null
  124. */
  125. private function validated_upload($field) {
  126. if (!isset($_FILES[$field]) || !is_array($_FILES[$field])) {
  127. UtilsLog::message(__('No file uploaded.', 'studiou-wc-ord-print-statuses'), 'error');
  128. return null;
  129. }
  130. $file = $_FILES[$field];
  131. if (isset($file['error']) && $file['error'] !== UPLOAD_ERR_OK) {
  132. UtilsLog::message(
  133. sprintf(
  134. /* translators: %d is the PHP upload error code. */
  135. __('File upload failed (error code %d).', 'studiou-wc-ord-print-statuses'),
  136. (int) $file['error']
  137. ),
  138. 'error'
  139. );
  140. return null;
  141. }
  142. if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
  143. UtilsLog::message(__('Uploaded file is not accessible.', 'studiou-wc-ord-print-statuses'), 'error');
  144. return null;
  145. }
  146. // Defense-in-depth: the HTML `accept` attribute is a client-side hint
  147. // and can be bypassed. Verify the uploaded file is named *.csv.
  148. $name = isset($file['name']) ? (string) $file['name'] : '';
  149. $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
  150. if ($ext !== 'csv') {
  151. UtilsLog::message(__('Only .csv files are accepted.', 'studiou-wc-ord-print-statuses'), 'error');
  152. return null;
  153. }
  154. return $file;
  155. }
  156. private function emit_import_summary($result) {
  157. $updated = isset($result['updated_count']) ? (int) $result['updated_count'] : 0;
  158. $errors = isset($result['errors']) && is_array($result['errors']) ? $result['errors'] : array();
  159. // Skip the "0 orders updated" notice when nothing actually changed —
  160. // the per-error notice below is more informative on its own and
  161. // matches the pattern Bulk_Actions_Manager::notify_moved_count uses.
  162. if ($updated > 0) {
  163. UtilsLog::message(
  164. sprintf(
  165. _n(
  166. '%d order updated successfully.',
  167. '%d orders updated successfully.',
  168. $updated,
  169. 'studiou-wc-ord-print-statuses'
  170. ),
  171. $updated
  172. ),
  173. 'success'
  174. );
  175. } elseif (empty($errors)) {
  176. // Both counters zero: the CSV parsed and had rows, but
  177. // dedupe_by_order_no filtered everything out — every row had
  178. // an empty order_no column. Surface this so the operator
  179. // isn't left wondering whether the import ran.
  180. UtilsLog::message(
  181. __('The CSV file contained no actionable rows (empty order_no column).', 'studiou-wc-ord-print-statuses'),
  182. 'info'
  183. );
  184. }
  185. if (!empty($errors)) {
  186. // Dedupe repeated error strings (e.g., "Invalid row format in
  187. // CSV." 100×) so the notice stays readable. The count is the
  188. // total number of errors, not the number of unique messages.
  189. $total = count($errors);
  190. $unique = array_values(array_unique($errors));
  191. UtilsLog::message(
  192. sprintf(
  193. /* translators: 1: error count, 2: comma-separated error messages. */
  194. __('%1$d errors occurred: %2$s', 'studiou-wc-ord-print-statuses'),
  195. $total,
  196. esc_html(implode(', ', $unique))
  197. ),
  198. 'error'
  199. );
  200. }
  201. }
  202. private function redirect_back() {
  203. wp_safe_redirect(admin_url('admin.php?page=studiou-import-protocols'));
  204. exit;
  205. }
  206. }