| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- <?php
- // Admin UI + handlers for importing the InPrint and Delivered protocol CSVs.
- defined('ABSPATH') || exit;
- class Import_Manager {
- public function __construct() {
- add_action('admin_menu', array($this, 'add_import_submenu'));
- add_action('admin_post_import_inprint_protocol', array($this, 'handle_import_inprint_protocol'));
- add_action('admin_post_import_delivered_protocol', array($this, 'handle_import_delivered_protocol'));
- }
- public function add_import_submenu() {
- $title = __('Import Protocols', 'studiou-wc-ord-print-statuses');
- // Use edit_shop_orders so Shop Managers (the typical operators of
- // the print pipeline) can run imports. The bulk actions on the
- // orders list use the same capability — keeping the two paths in
- // sync is the goal.
- add_submenu_page(
- 'woocommerce',
- $title,
- $title,
- 'edit_shop_orders',
- 'studiou-import-protocols',
- array($this, 'render_import_page')
- );
- }
- public function render_import_page() {
- ?>
- <div class="wrap">
- <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
- <h2><?php esc_html_e('Import InPrint Protocol', 'studiou-wc-ord-print-statuses'); ?></h2>
- <p>
- <?php
- printf(
- /* translators: list of required CSV header names. */
- esc_html__('Required CSV headers: %s', 'studiou-wc-ord-print-statuses'),
- '<code>order_no</code>, <code>externalorder</code>, <code>externalorderdate</code>'
- );
- ?>
- </p>
- <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" enctype="multipart/form-data">
- <input type="hidden" name="action" value="import_inprint_protocol">
- <?php wp_nonce_field('import_inprint_protocol', 'import_inprint_protocol_nonce'); ?>
- <input type="file" name="inprint_csv" accept=".csv,text/csv" required>
- <?php submit_button(__('Import InPrint Protocol', 'studiou-wc-ord-print-statuses')); ?>
- </form>
- <h2><?php esc_html_e('Import Delivered Protocol', 'studiou-wc-ord-print-statuses'); ?></h2>
- <p>
- <?php
- printf(
- /* translators: list of required CSV header names. */
- esc_html__('Required CSV headers: %s', 'studiou-wc-ord-print-statuses'),
- '<code>order_no</code>'
- );
- ?>
- </p>
- <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" enctype="multipart/form-data">
- <input type="hidden" name="action" value="import_delivered_protocol">
- <?php wp_nonce_field('import_delivered_protocol', 'import_delivered_protocol_nonce'); ?>
- <input type="file" name="delivered_csv" accept=".csv,text/csv" required>
- <?php submit_button(__('Import Delivered Protocol', 'studiou-wc-ord-print-statuses')); ?>
- </form>
- </div>
- <?php
- }
- /**
- * Map from admin-post action name → [file field, nonce field/action, DB handler callable].
- * Adding a new protocol is a single-entry change here.
- */
- private function protocol_dispatch() {
- return array(
- 'import_inprint_protocol' => array(
- 'file_field' => 'inprint_csv',
- 'nonce' => 'import_inprint_protocol_nonce',
- 'handler' => array('Studiou_DB_Manager', 'import_inprint_protocol'),
- ),
- 'import_delivered_protocol' => array(
- 'file_field' => 'delivered_csv',
- 'nonce' => 'import_delivered_protocol_nonce',
- 'handler' => array('Studiou_DB_Manager', 'import_delivered_protocol'),
- ),
- );
- }
- public function handle_import_inprint_protocol() {
- $this->run_import('import_inprint_protocol');
- }
- public function handle_import_delivered_protocol() {
- $this->run_import('import_delivered_protocol');
- }
- /**
- * Common pipeline: capability check → nonce → file validation → parse → dispatch → redirect.
- * Every non-success branch returns immediately via redirect_back() so static
- * analysis can prove that subsequent code is only reached on the happy path.
- */
- private function run_import($action) {
- $dispatch = $this->protocol_dispatch();
- if (!isset($dispatch[$action])) {
- return $this->redirect_back();
- }
- $cfg = $dispatch[$action];
- if (!current_user_can('edit_shop_orders')) {
- wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'studiou-wc-ord-print-statuses'));
- }
- check_admin_referer($action, $cfg['nonce']);
- $file = $this->validated_upload($cfg['file_field']);
- if ($file === null) {
- return $this->redirect_back();
- }
- $csv_data = Studiou_DB_Manager::parse_csv($file['tmp_name']);
- if (empty($csv_data)) {
- UtilsLog::message(
- __('CSV file could not be parsed or contained no data rows.', 'studiou-wc-ord-print-statuses'),
- 'error'
- );
- return $this->redirect_back();
- }
- $handler = $cfg['handler'];
- $result = $handler($csv_data);
- $this->emit_import_summary($result);
- return $this->redirect_back();
- }
- /**
- * Validate the file upload superglobal entry. Emits a notice and returns null on failure.
- *
- * @return array|null
- */
- private function validated_upload($field) {
- if (!isset($_FILES[$field]) || !is_array($_FILES[$field])) {
- UtilsLog::message(__('No file uploaded.', 'studiou-wc-ord-print-statuses'), 'error');
- return null;
- }
- $file = $_FILES[$field];
- if (isset($file['error']) && $file['error'] !== UPLOAD_ERR_OK) {
- UtilsLog::message(
- sprintf(
- /* translators: %d is the PHP upload error code. */
- __('File upload failed (error code %d).', 'studiou-wc-ord-print-statuses'),
- (int) $file['error']
- ),
- 'error'
- );
- return null;
- }
- if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
- UtilsLog::message(__('Uploaded file is not accessible.', 'studiou-wc-ord-print-statuses'), 'error');
- return null;
- }
- // Defense-in-depth: the HTML `accept` attribute is a client-side hint
- // and can be bypassed. Verify the uploaded file is named *.csv.
- $name = isset($file['name']) ? (string) $file['name'] : '';
- $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
- if ($ext !== 'csv') {
- UtilsLog::message(__('Only .csv files are accepted.', 'studiou-wc-ord-print-statuses'), 'error');
- return null;
- }
- return $file;
- }
- private function emit_import_summary($result) {
- $updated = isset($result['updated_count']) ? (int) $result['updated_count'] : 0;
- $errors = isset($result['errors']) && is_array($result['errors']) ? $result['errors'] : array();
- // Skip the "0 orders updated" notice when nothing actually changed —
- // the per-error notice below is more informative on its own and
- // matches the pattern Bulk_Actions_Manager::notify_moved_count uses.
- if ($updated > 0) {
- UtilsLog::message(
- sprintf(
- _n(
- '%d order updated successfully.',
- '%d orders updated successfully.',
- $updated,
- 'studiou-wc-ord-print-statuses'
- ),
- $updated
- ),
- 'success'
- );
- }
- if (!empty($errors)) {
- // Dedupe repeated error strings (e.g., "Invalid row format in
- // CSV." 100×) so the notice stays readable. The count is the
- // total number of errors, not the number of unique messages.
- $total = count($errors);
- $unique = array_values(array_unique($errors));
- UtilsLog::message(
- sprintf(
- /* translators: 1: error count, 2: comma-separated error messages. */
- __('%1$d errors occurred: %2$s', 'studiou-wc-ord-print-statuses'),
- $total,
- esc_html(implode(', ', $unique))
- ),
- 'error'
- );
- }
- }
- private function redirect_back() {
- wp_safe_redirect(admin_url('admin.php?page=studiou-import-protocols'));
- exit;
- }
- }
|