class-import-manager.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. add_submenu_page(
  13. 'woocommerce',
  14. $title,
  15. $title,
  16. 'manage_woocommerce',
  17. 'studiou-import-protocols',
  18. array($this, 'render_import_page')
  19. );
  20. }
  21. public function render_import_page() {
  22. ?>
  23. <div class="wrap">
  24. <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
  25. <h2><?php esc_html_e('Import InPrint Protocol', 'studiou-wc-ord-print-statuses'); ?></h2>
  26. <p>
  27. <?php
  28. printf(
  29. /* translators: list of required CSV header names. */
  30. esc_html__('Required CSV headers: %s', 'studiou-wc-ord-print-statuses'),
  31. '<code>order_no</code>, <code>externalorder</code>, <code>externalorderdate</code>'
  32. );
  33. ?>
  34. </p>
  35. <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" enctype="multipart/form-data">
  36. <input type="hidden" name="action" value="import_inprint_protocol">
  37. <?php wp_nonce_field('import_inprint_protocol', 'import_inprint_protocol_nonce'); ?>
  38. <input type="file" name="inprint_csv" accept=".csv,text/csv" required>
  39. <?php submit_button(__('Import InPrint Protocol', 'studiou-wc-ord-print-statuses')); ?>
  40. </form>
  41. <h2><?php esc_html_e('Import Delivered Protocol', 'studiou-wc-ord-print-statuses'); ?></h2>
  42. <p>
  43. <?php
  44. printf(
  45. /* translators: list of required CSV header names. */
  46. esc_html__('Required CSV headers: %s', 'studiou-wc-ord-print-statuses'),
  47. '<code>order_no</code>'
  48. );
  49. ?>
  50. </p>
  51. <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" enctype="multipart/form-data">
  52. <input type="hidden" name="action" value="import_delivered_protocol">
  53. <?php wp_nonce_field('import_delivered_protocol', 'import_delivered_protocol_nonce'); ?>
  54. <input type="file" name="delivered_csv" accept=".csv,text/csv" required>
  55. <?php submit_button(__('Import Delivered Protocol', 'studiou-wc-ord-print-statuses')); ?>
  56. </form>
  57. </div>
  58. <?php
  59. }
  60. /**
  61. * Map from admin-post action name → [file field, nonce field/action, DB handler callable].
  62. * Adding a new protocol is a single-entry change here.
  63. */
  64. private function protocol_dispatch() {
  65. return array(
  66. 'import_inprint_protocol' => array(
  67. 'file_field' => 'inprint_csv',
  68. 'nonce' => 'import_inprint_protocol_nonce',
  69. 'handler' => array('Studiou_DB_Manager', 'import_inprint_protocol'),
  70. ),
  71. 'import_delivered_protocol' => array(
  72. 'file_field' => 'delivered_csv',
  73. 'nonce' => 'import_delivered_protocol_nonce',
  74. 'handler' => array('Studiou_DB_Manager', 'import_delivered_protocol'),
  75. ),
  76. );
  77. }
  78. public function handle_import_inprint_protocol() {
  79. $this->run_import('import_inprint_protocol');
  80. }
  81. public function handle_import_delivered_protocol() {
  82. $this->run_import('import_delivered_protocol');
  83. }
  84. /**
  85. * Common pipeline: capability check → nonce → file validation → parse → dispatch → redirect.
  86. * Every non-success branch returns immediately via redirect_back() so static
  87. * analysis can prove that subsequent code is only reached on the happy path.
  88. */
  89. private function run_import($action) {
  90. $dispatch = $this->protocol_dispatch();
  91. if (!isset($dispatch[$action])) {
  92. return $this->redirect_back();
  93. }
  94. $cfg = $dispatch[$action];
  95. if (!current_user_can('manage_woocommerce')) {
  96. wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'studiou-wc-ord-print-statuses'));
  97. }
  98. check_admin_referer($action, $cfg['nonce']);
  99. $file = $this->validated_upload($cfg['file_field']);
  100. if ($file === null) {
  101. return $this->redirect_back();
  102. }
  103. $csv_data = Studiou_DB_Manager::parse_csv($file['tmp_name']);
  104. if (empty($csv_data)) {
  105. UtilsLog::message(
  106. __('CSV file could not be parsed or contained no data rows.', 'studiou-wc-ord-print-statuses'),
  107. 'error'
  108. );
  109. return $this->redirect_back();
  110. }
  111. $result = call_user_func($cfg['handler'], $csv_data);
  112. $this->emit_import_summary($result);
  113. return $this->redirect_back();
  114. }
  115. /**
  116. * Validate the file upload superglobal entry. Emits a notice and returns null on failure.
  117. *
  118. * @return array|null
  119. */
  120. private function validated_upload($field) {
  121. if (!isset($_FILES[$field]) || !is_array($_FILES[$field])) {
  122. UtilsLog::message(__('No file uploaded.', 'studiou-wc-ord-print-statuses'), 'error');
  123. return null;
  124. }
  125. $file = $_FILES[$field];
  126. if (isset($file['error']) && $file['error'] !== UPLOAD_ERR_OK) {
  127. UtilsLog::message(
  128. sprintf(
  129. /* translators: %d is the PHP upload error code. */
  130. __('File upload failed (error code %d).', 'studiou-wc-ord-print-statuses'),
  131. (int) $file['error']
  132. ),
  133. 'error'
  134. );
  135. return null;
  136. }
  137. if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
  138. UtilsLog::message(__('Uploaded file is not accessible.', 'studiou-wc-ord-print-statuses'), 'error');
  139. return null;
  140. }
  141. // Defense-in-depth: the HTML `accept` attribute is a client-side hint
  142. // and can be bypassed. Verify the uploaded file is named *.csv.
  143. $name = isset($file['name']) ? (string) $file['name'] : '';
  144. $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
  145. if ($ext !== 'csv') {
  146. UtilsLog::message(__('Only .csv files are accepted.', 'studiou-wc-ord-print-statuses'), 'error');
  147. return null;
  148. }
  149. return $file;
  150. }
  151. private function emit_import_summary($result) {
  152. $updated = isset($result['updated_count']) ? (int) $result['updated_count'] : 0;
  153. $errors = isset($result['errors']) && is_array($result['errors']) ? $result['errors'] : array();
  154. UtilsLog::message(
  155. sprintf(
  156. _n(
  157. '%d order updated successfully.',
  158. '%d orders updated successfully.',
  159. $updated,
  160. 'studiou-wc-ord-print-statuses'
  161. ),
  162. $updated
  163. ),
  164. $updated > 0 ? 'success' : 'info'
  165. );
  166. if (!empty($errors)) {
  167. UtilsLog::message(
  168. sprintf(
  169. /* translators: 1: error count, 2: comma-separated error messages. */
  170. __('%1$d errors occurred: %2$s', 'studiou-wc-ord-print-statuses'),
  171. count($errors),
  172. esc_html(implode(', ', $errors))
  173. ),
  174. 'error'
  175. );
  176. }
  177. }
  178. private function redirect_back() {
  179. wp_safe_redirect(admin_url('admin.php?page=studiou-import-protocols'));
  180. exit;
  181. }
  182. }