class-import-manager.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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. $result = call_user_func($cfg['handler'], $csv_data);
  116. $this->emit_import_summary($result);
  117. return $this->redirect_back();
  118. }
  119. /**
  120. * Validate the file upload superglobal entry. Emits a notice and returns null on failure.
  121. *
  122. * @return array|null
  123. */
  124. private function validated_upload($field) {
  125. if (!isset($_FILES[$field]) || !is_array($_FILES[$field])) {
  126. UtilsLog::message(__('No file uploaded.', 'studiou-wc-ord-print-statuses'), 'error');
  127. return null;
  128. }
  129. $file = $_FILES[$field];
  130. if (isset($file['error']) && $file['error'] !== UPLOAD_ERR_OK) {
  131. UtilsLog::message(
  132. sprintf(
  133. /* translators: %d is the PHP upload error code. */
  134. __('File upload failed (error code %d).', 'studiou-wc-ord-print-statuses'),
  135. (int) $file['error']
  136. ),
  137. 'error'
  138. );
  139. return null;
  140. }
  141. if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
  142. UtilsLog::message(__('Uploaded file is not accessible.', 'studiou-wc-ord-print-statuses'), 'error');
  143. return null;
  144. }
  145. // Defense-in-depth: the HTML `accept` attribute is a client-side hint
  146. // and can be bypassed. Verify the uploaded file is named *.csv.
  147. $name = isset($file['name']) ? (string) $file['name'] : '';
  148. $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
  149. if ($ext !== 'csv') {
  150. UtilsLog::message(__('Only .csv files are accepted.', 'studiou-wc-ord-print-statuses'), 'error');
  151. return null;
  152. }
  153. return $file;
  154. }
  155. private function emit_import_summary($result) {
  156. $updated = isset($result['updated_count']) ? (int) $result['updated_count'] : 0;
  157. $errors = isset($result['errors']) && is_array($result['errors']) ? $result['errors'] : array();
  158. UtilsLog::message(
  159. sprintf(
  160. _n(
  161. '%d order updated successfully.',
  162. '%d orders updated successfully.',
  163. $updated,
  164. 'studiou-wc-ord-print-statuses'
  165. ),
  166. $updated
  167. ),
  168. $updated > 0 ? 'success' : 'info'
  169. );
  170. if (!empty($errors)) {
  171. // Dedupe repeated error strings (e.g., "Invalid row format in
  172. // CSV." 100×) so the notice stays readable. The count is the
  173. // total number of errors, not the number of unique messages.
  174. $total = count($errors);
  175. $unique = array_values(array_unique($errors));
  176. UtilsLog::message(
  177. sprintf(
  178. /* translators: 1: error count, 2: comma-separated error messages. */
  179. __('%1$d errors occurred: %2$s', 'studiou-wc-ord-print-statuses'),
  180. $total,
  181. esc_html(implode(', ', $unique))
  182. ),
  183. 'error'
  184. );
  185. }
  186. }
  187. private function redirect_back() {
  188. wp_safe_redirect(admin_url('admin.php?page=studiou-import-protocols'));
  189. exit;
  190. }
  191. }