admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('evqra_event_import'), 'batchSize' => self::BATCH_SIZE, 'presets' => $presets, 'strings' => [ 'uploading' => __('Uploading CSV…', 'eventqora'), 'validating' => __('Validating rows…', 'eventqora'), 'importing' => __('Importing events…', 'eventqora'), 'done' => __('Import completed.', 'eventqora'), 'error' => __('The importer encountered an unexpected error.', 'eventqora'), 'requestFailed' => __('Request failed.', 'eventqora'), 'ignoreColumn' => __('Ignore column', 'eventqora'), 'eventTitleRequired' => __('Event title (required)', 'eventqora'), 'description' => __('Description', 'eventqora'), 'startDateRequired' => __('Start date (required)', 'eventqora'), 'startTime' => __('Start time', 'eventqora'), 'endDate' => __('End date', 'eventqora'), 'endTime' => __('End time', 'eventqora'), 'timezone' => __('Timezone', 'eventqora'), 'allDay' => __('All day', 'eventqora'), 'categories' => __('Categories', 'eventqora'), 'tags' => __('Tags', 'eventqora'), 'venue' => __('Venue', 'eventqora'), 'organizer' => __('Organizer', 'eventqora'), 'externalEventId' => __('External Event ID', 'eventqora'), 'featured' => __('Featured', 'eventqora'), 'postStatus' => __('Post status', 'eventqora'), 'bookingMode' => __('Booking mode', 'eventqora'), 'bookingStart' => __('Booking start', 'eventqora'), 'bookingEnd' => __('Booking end', 'eventqora'), 'featuredImageUrl' => __('Featured image URL', 'eventqora'), 'galleryImageUrls' => __('Gallery image URLs', 'eventqora'), 'recurrenceRule' => __('Recurrence rule / RRULE', 'eventqora'), 'valid' => __('Valid', 'eventqora'), 'warnings' => __('Warnings', 'eventqora'), 'errors' => __('Errors', 'eventqora'), 'duplicates' => __('Duplicates', 'eventqora'), 'new' => __('New', 'eventqora'), 'updates' => __('Updates', 'eventqora'), 'csvColumn' => __('CSV column', 'eventqora'), 'sample' => __('Sample', 'eventqora'), 'mapToMemField' => __('Map to EventQora field', 'eventqora'), 'noRows' => __('No rows were found.', 'eventqora'), 'row' => __('Row', 'eventqora'), 'event' => __('Event', 'eventqora'), 'status' => __('Status', 'eventqora'), 'messages' => __('Messages', 'eventqora'), 'noRowErrors' => __('No row errors were found.', 'eventqora'), // translators: %d is the number of validation issues in the row. 'rowIssues' => __('%d row issue(s)', 'eventqora'), // translators: %s is the row number. 'rowPrefix' => __('Row %s:', 'eventqora'), 'pleaseSelectCsv' => __('Please select a CSV file.', 'eventqora'), 'needsAttention' => __('Needs attention', 'eventqora'), 'ready' => __('Ready', 'eventqora'), // translators: %s is the number of imported data rows. 'dataRows' => __('%s data row(s)', 'eventqora'), 'presetName' => __('Preset name', 'eventqora'), 'errorRows' => __('Row', 'eventqora'), 'errorEvents' => __('Event', 'eventqora'), 'errorMessages' => __('Messages', 'eventqora'), // translators: %s is the number of imported events. 'importedEvents' => __('Imported %s event(s).', 'eventqora'), 'created' => __('Created', 'eventqora'), 'updated' => __('Updated', 'eventqora'), 'skipped' => __('Skipped', 'eventqora'), ], ]); } public function render_import_page() { $presets = get_user_meta(get_current_user_id(), '_evqra_event_import_presets', true); $presets = is_array($presets) ? $presets : []; ?>
__('You do not have permission to import events.', 'eventqora')], 403); } check_ajax_referer('evqra_event_import', 'nonce'); $this->cleanup_temp_imports(); if (empty($_FILES['file'])) { wp_send_json_error(['message' => __('No CSV file was uploaded.', 'eventqora')], 400); } $file = [ 'name' => isset($_FILES['file']['name']) ? sanitize_file_name(wp_unslash((string) $_FILES['file']['name'])) : '', 'type' => isset($_FILES['file']['type']) ? sanitize_text_field(wp_unslash((string) $_FILES['file']['type'])) : '', 'tmp_name' => isset($_FILES['file']['tmp_name']) ? sanitize_text_field(wp_unslash((string) $_FILES['file']['tmp_name'])) : '', 'error' => isset($_FILES['file']['error']) ? absint($_FILES['file']['error']) : UPLOAD_ERR_NO_FILE, 'size' => isset($_FILES['file']['size']) ? absint($_FILES['file']['size']) : 0, ]; if (!empty($file['error']) || empty($file['tmp_name'])) { wp_send_json_error(['message' => __('The CSV upload failed.', 'eventqora')], 400); } if ((int) $file['size'] <= 0 || (int) $file['size'] > self::MAX_FILE_SIZE) { wp_send_json_error(['message' => __('CSV files must be between 1 byte and 20 MB.', 'eventqora')], 400); } $extension = strtolower(pathinfo((string) $file['name'], PATHINFO_EXTENSION)); if ($extension !== 'csv') { wp_send_json_error(['message' => __('Only .csv files are supported.', 'eventqora')], 400); } $contents = file_get_contents($file['tmp_name']); if ($contents === false || strlen($contents) === 0) { wp_send_json_error(['message' => __('The uploaded CSV could not be read.', 'eventqora')], 400); } $sample = preg_replace('/^\xEF\xBB\xBF/', '', substr($contents, 0, 4096)); if ($sample === false || strpos($sample, "\0") !== false) { wp_send_json_error(['message' => __('The uploaded file does not appear to be a valid text CSV.', 'eventqora')], 400); } $token = wp_generate_password(32, false, false); // Keep the uploaded CSV outside the public uploads directory while the import session is active. $path = wp_tempnam('evqra-event-import-' . $token . '.csv'); require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; $filesystem = new WP_Filesystem_Direct(false); if (!$path || !$filesystem->move($file['tmp_name'], $path, true)) { if ($path && file_exists($path)) wp_delete_file($path); wp_send_json_error(['message' => __('Unable to store the uploaded CSV.', 'eventqora')], 500); } $headers = $this->read_headers($path); if (is_wp_error($headers)) { wp_delete_file($path); wp_send_json_error(['message' => $headers->get_error_message()], 400); } $session = [ 'path' => $path, 'name' => sanitize_file_name($file['name']), 'headers' => $headers, 'total' => $this->count_data_rows($path), 'created' => time(), ]; set_transient($this->session_key($token), $session, self::SESSION_TTL); wp_send_json_success([ 'token' => $token, 'name' => $session['name'], 'size' => (int) $file['size'], 'headers' => $headers, 'total' => $session['total'], 'samples' => $this->sample_values($path, count($headers)), 'mapping' => $this->auto_map($headers), ]); } public function ajax_preview() { $this->guard_ajax(); $session = $this->get_session(); $mapping = $this->sanitize_mapping($this->request_array('mapping')); $options = $this->sanitize_options($this->request_array('options')); $result = $this->validate_rows($session, $mapping, $options, 0, 100); wp_send_json_success($result); } public function ajax_batch() { $this->guard_ajax(); $session = $this->get_session(); $mapping = $this->sanitize_mapping($this->request_array('mapping')); $options = $this->sanitize_options($this->request_array('options')); $offset = max(0, absint(filter_input(INPUT_POST, 'offset', FILTER_SANITIZE_SPECIAL_CHARS) ?: 0)); $batch = min(100, max(1, absint(filter_input(INPUT_POST, 'batch', FILTER_SANITIZE_SPECIAL_CHARS) ?: self::BATCH_SIZE))); $result = $this->import_rows($session, $mapping, $options, $offset, $batch); if ($result['done']) { wp_delete_file($session['path']); delete_transient($this->session_key(sanitize_key(filter_input(INPUT_POST, 'token', FILTER_SANITIZE_SPECIAL_CHARS) ?: ''))); } wp_send_json_success($result); } public function ajax_save_preset() { $this->guard_ajax(); $name = sanitize_text_field(wp_unslash(filter_input(INPUT_POST, 'name', FILTER_SANITIZE_SPECIAL_CHARS) ?: '')); if ($name === '') { wp_send_json_error(['message' => __('Enter a preset name.', 'eventqora')], 400); } $mapping = $this->sanitize_mapping($this->request_array('mapping')); $options = $this->sanitize_options($this->request_array('options')); $presets = get_user_meta(get_current_user_id(), '_evqra_event_import_presets', true); $presets = is_array($presets) ? $presets : []; $presets[$name] = ['mapping' => $mapping, 'options' => $options]; update_user_meta(get_current_user_id(), '_evqra_event_import_presets', $presets); wp_send_json_success(['message' => __('Mapping preset saved.', 'eventqora')]); } public function ajax_delete_preset() { $this->guard_ajax(); $name = sanitize_text_field(wp_unslash(filter_input(INPUT_POST, 'name', FILTER_SANITIZE_SPECIAL_CHARS) ?: '')); $presets = get_user_meta(get_current_user_id(), '_evqra_event_import_presets', true); if (is_array($presets) && isset($presets[$name])) { unset($presets[$name]); update_user_meta(get_current_user_id(), '_evqra_event_import_presets', $presets); } wp_send_json_success(); } /** * Download the canonical EventQora CSV import template. * * The template is generated here rather than maintained as a second static * file, so its columns stay aligned with the importer contract. */ public function download_csv_template() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to download the event import template.', 'eventqora'), 403); } check_admin_referer('evqra_event_import_template'); $headers = array_keys($this->target_labels()); $headers = array_values(array_filter($headers, static function ($value) { return $value !== ''; })); $rows = [ [ 'WordPress Meetup 2026', 'Join us for talks, networking, and practical WordPress development tips.', '2026-10-15', '18:30', '2026-10-15', '21:00', 'Asia/Kolkata', '0', 'Technology, Meetup', 'WordPress, PHP', 'EventQora Convention Center', 'EventQora', 'meetup-2026-001', '1', 'publish', 'ticket', '2026-09-01 00:00', '2026-10-15 17:30', '', '', '', ], [ 'Community Open Day', 'A free all-day community event with talks, networking and demos.', '2026-11-07', '', '2026-11-07', '', 'Asia/Kolkata', '1', 'Community', 'Open Day, Community', 'EventQora Community Hall', 'EventQora Community Team', 'community-open-day-2026', '0', 'publish', 'free', '', '', '', '', '', ], [ 'PHP Training Series', 'Weekly PHP training session for developers.', '2026-12-05', '10:00', '2026-12-05', '12:00', 'Asia/Kolkata', '0', 'Training', 'PHP, Development', 'EventQora Training Center', 'EventQora Academy', 'php-training-2026', '0', 'draft', 'ticket', '2026-11-01 00:00', '2026-12-05 09:00', '', '', 'FREQ=WEEKLY;COUNT=4', ], ]; // Build the CSV in memory so the template download does not rely on // direct PHP filesystem operations. This keeps the response self-contained // while preserving RFC 4180-compatible CSV quoting. $csv_escape = static function ($value) { $value = (string) $value; return '"' . str_replace('"', '""', $value) . '"'; }; // UTF-8 BOM improves Excel compatibility while remaining valid UTF-8. $csv = "\xEF\xBB\xBF"; $csv .= implode(',', array_map($csv_escape, $headers)) . "\r\n"; foreach ($rows as $row) { $row = array_pad($row, count($headers), ''); $csv .= implode(',', array_map($csv_escape, $row)) . "\r\n"; } nocache_headers(); header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="evqra-event-import-template.csv"'); header('Content-Length: ' . strlen($csv)); echo esc_html($csv); exit; } public function handle_csv_upload() { if (!current_user_can('manage_options') || !check_admin_referer('evqra_import_csv_nonce')) { wp_die(esc_html__('Unauthorized request', 'eventqora')); } if (empty($_FILES['evqra_csv_file']) || !empty($_FILES['evqra_csv_file']['error'])) { wp_die(esc_html__('Upload failed.', 'eventqora')); } $filename = isset($_FILES['evqra_csv_file']['name']) && is_string($_FILES['evqra_csv_file']['name']) ? sanitize_file_name(wp_unslash($_FILES['evqra_csv_file']['name'])) : ''; if ($filename === '' || strtolower(pathinfo($filename, PATHINFO_EXTENSION)) !== 'csv') { wp_die(esc_html__('Only CSV files are allowed.', 'eventqora')); } $file = [ 'name' => sanitize_file_name(wp_unslash((string) $_FILES['evqra_csv_file']['name'])), 'type' => isset($_FILES['evqra_csv_file']['type']) ? sanitize_text_field(wp_unslash((string) $_FILES['evqra_csv_file']['type'])) : '', 'tmp_name' => isset($_FILES['evqra_csv_file']['tmp_name']) ? sanitize_text_field(wp_unslash((string) $_FILES['evqra_csv_file']['tmp_name'])) : '', 'error' => absint($_FILES['evqra_csv_file']['error'] ?? UPLOAD_ERR_NO_FILE), 'size' => absint($_FILES['evqra_csv_file']['size'] ?? 0), ]; $this->legacy_import($file); } private function legacy_import($file) { if ((int) $file['size'] > self::MAX_FILE_SIZE) { wp_die(esc_html__('CSV file is too large. Maximum size is 20 MB.', 'eventqora')); } $path = $file['tmp_name']; $headers = $this->read_headers($path); if (is_wp_error($headers)) { wp_die(esc_html($headers->get_error_message())); } $session = ['path' => $path, 'headers' => $headers, 'total' => $this->count_data_rows($path)]; $mapping = $this->sanitize_mapping($this->auto_map($headers)); $options = $this->sanitize_options(['duplicate_mode' => 'create']); $offset = 0; $imported = 0; while ($offset < $session['total']) { $result = $this->import_rows($session, $mapping, $options, $offset, self::BATCH_SIZE); $imported += (int) $result['created']; $imported += (int) $result['updated']; $offset = $result['next_offset']; if ($result['done']) break; } wp_safe_redirect(add_query_arg(['post_type' => 'evqra_event', 'page' => 'evqra-event-import', 'import' => 'success', 'imported' => $imported], admin_url('edit.php'))); exit; } private function cleanup_temp_imports() { $dir = get_temp_dir(); if (!is_dir($dir)) return; foreach (glob(trailingslashit($dir) . 'evqra-event-import-*') ?: [] as $path) { if (is_file($path) && filemtime($path) < (time() - self::SESSION_TTL)) { wp_delete_file($path); } } } private function guard_ajax() { if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('You do not have permission to import events.', 'eventqora')], 403); } check_ajax_referer('evqra_event_import', 'nonce'); } private function request_array($key) { $raw = filter_input(INPUT_POST, $key, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_REQUIRE_ARRAY); if (false === $raw || null === $raw) { $raw = filter_input(INPUT_POST, $key, FILTER_SANITIZE_SPECIAL_CHARS); } $raw = null !== $raw ? wp_unslash($raw) : []; if (is_array($raw)) return $raw; if (is_string($raw)) { $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : []; } return []; } private function get_session() { $token = sanitize_key(wp_unslash(filter_input(INPUT_POST, 'token', FILTER_SANITIZE_SPECIAL_CHARS) ?: '')); if (!$token) { wp_send_json_error(['message' => __('Import session is missing.', 'eventqora')], 400); } $session = get_transient($this->session_key($token)); if (!is_array($session) || empty($session['path']) || !is_readable($session['path'])) { wp_send_json_error(['message' => __('The import session expired. Please upload the CSV again.', 'eventqora')], 410); } return $session; } private function session_key($token) { return 'evqra_import_' . get_current_user_id() . '_' . sanitize_key($token); } private function read_headers($path) { $fp = new SplFileObject($path, 'r'); $fp->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY); $fp->setCsvControl(',', '"', ''); $row = $fp->fgetcsv(); if (!is_array($row) || !$row) { return new WP_Error('empty_csv', __('The CSV does not contain a header row.', 'eventqora')); } if (isset($row[0])) { $row[0] = preg_replace('/^\xEF\xBB\xBF/', '', (string) $row[0]); } $headers = []; foreach ($row as $header) { $header = trim((string) $header); if ($header === '') { $header = 'Column ' . (count($headers) + 1); } $headers[] = $header; } if (count(array_unique(array_map([$this, 'normalize_header'], $headers))) !== count($headers)) { $headers = $this->make_unique_headers($headers); } return $headers; } private function make_unique_headers($headers) { $seen = []; foreach ($headers as &$header) { $base = $header; $key = $this->normalize_header($base); $seen[$key] = ($seen[$key] ?? 0) + 1; if ($seen[$key] > 1) $header = $base . ' ' . $seen[$key]; } return $headers; } private function sample_values($path, $columns) { $fp = new SplFileObject($path, 'r'); $fp->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY); $fp->setCsvControl(',', '"', ''); $fp->fgetcsv(); $row = $fp->fgetcsv(); if (!is_array($row)) $row = []; $row = array_pad($row, $columns, ''); return array_slice($row, 0, $columns); } private function count_data_rows($path) { $fp = new SplFileObject($path, 'r'); $fp->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY); $fp->setCsvControl(',', '"', ''); $count = 0; $first = true; while (!$fp->eof()) { $row = $fp->fgetcsv(); if ($first) { $first = false; continue; } if (is_array($row) && !(count($row) === 1 && trim((string) $row[0]) === '')) $count++; } return $count; } private function rows($path, $offset, $limit) { $fp = new SplFileObject($path, 'r'); $fp->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY); $fp->setCsvControl(',', '"', ''); $header = $fp->fgetcsv(); $rows = []; $index = 0; while (!$fp->eof() && count($rows) < $limit) { $row = $fp->fgetcsv(); if (!is_array($row) || (count($row) === 1 && trim((string) $row[0]) === '')) continue; if ($index++ < $offset) continue; $rows[] = array_pad($row, count($header), ''); $rows[count($rows) - 1] = array_slice($rows[count($rows) - 1], 0, count($header)); } return $rows; } private function auto_map($headers) { $aliases = [ 'title' => ['event name','event title','title','name','event'], 'description' => ['event description','description','content','details'], 'start_date' => ['event date','start date','date','start'], 'start_time' => ['event time','start time','time'], 'end_date' => ['end date','event end date'], 'end_time' => ['end time','event end time'], 'timezone' => ['timezone','time zone','event timezone'], 'all_day' => ['all day','all-day','allday'], 'category' => ['category','categories','category slugs','event category'], 'tags' => ['tag','tags','event tags'], 'venue' => ['venue','venue id','location','location name'], 'organizer' => ['organizer','organizer id','event organizer'], 'external_id' => ['external id','event id','event external id','source id','import id'], 'featured' => ['featured','is featured'], 'status' => ['status','post status'], 'booking_mode' => ['booking mode','booking'], 'booking_start' => ['booking start'], 'booking_end' => ['booking end'], 'image' => ['image','featured image','image url','featured image url'], 'gallery' => ['gallery','gallery image urls','gallery images','image urls'], 'recurrence_rule' => ['rrule','recurrence rule','recurrence'], ]; $mapping = []; foreach ($headers as $header) { $normalized = $this->normalize_header($header); $target = ''; foreach ($aliases as $candidate => $names) { foreach ($names as $alias) { if ($normalized === $this->normalize_header($alias)) { $target = $candidate; break 2; } } } if ($target === '' && strpos($normalized, 'meta ') === 0) { $target = 'meta:' . sanitize_key(trim(substr($header, 5))); } $mapping[$header] = $target; } return $mapping; } private function normalize_header($value) { $value = strtolower(trim((string) $value)); $value = preg_replace('/\x{EF}\x{BB}\x{BF}/u', '', $value); $value = preg_replace('/[^a-z0-9]+/i', ' ', $value); return trim(preg_replace('/\s+/', ' ', $value)); } private function sanitize_mapping($mapping) { $allowed = array_keys($this->target_labels()); $out = []; if (!is_array($mapping)) return $out; foreach ($mapping as $header => $target) { $header = sanitize_text_field((string) $header); $target = sanitize_text_field((string) $target); if ($header === '') continue; if ($target !== '' && !in_array($target, $allowed, true) && strpos($target, 'meta:') !== 0) $target = ''; $out[$header] = $target; } return $out; } private function target_labels() { return [ '' => __('Ignore column', 'eventqora'), 'title' => __('Event title (required)', 'eventqora'), 'description' => __('Description', 'eventqora'), 'start_date' => __('Start date (required)', 'eventqora'), 'start_time' => __('Start time', 'eventqora'), 'end_date' => __('End date', 'eventqora'), 'end_time' => __('End time', 'eventqora'), 'timezone' => __('Timezone', 'eventqora'), 'all_day' => __('All day', 'eventqora'), 'category' => __('Categories', 'eventqora'), 'tags' => __('Tags', 'eventqora'), 'venue' => __('Venue', 'eventqora'), 'organizer' => __('Organizer', 'eventqora'), 'external_id' => __('External Event ID', 'eventqora'), 'featured' => __('Featured', 'eventqora'), 'status' => __('Post status', 'eventqora'), 'booking_mode' => __('Booking mode', 'eventqora'), 'booking_start' => __('Booking start', 'eventqora'), 'booking_end' => __('Booking end', 'eventqora'), 'image' => __('Featured image URL', 'eventqora'), 'gallery' => __('Gallery image URLs', 'eventqora'), 'recurrence_rule' => __('Recurrence rule / RRULE', 'eventqora'), ]; } private function sanitize_options($options) { $options = is_array($options) ? $options : []; $mode = sanitize_key($options['duplicate_mode'] ?? 'create'); if (!in_array($mode, ['create','skip','update'], true)) $mode = 'create'; $format = sanitize_text_field($options['date_format'] ?? 'auto'); $allowed_formats = ['auto','Y-m-d','d/m/Y','m/d/Y','d-m-Y','m-d-Y','F j, Y','j F Y']; if (!in_array($format, $allowed_formats, true)) $format = 'auto'; $timezone = sanitize_text_field($options['timezone'] ?? ''); if ($timezone !== '' && !in_array($timezone, timezone_identifiers_list(), true)) $timezone = ''; return [ 'duplicate_mode' => $mode, 'date_format' => $format, 'timezone' => $timezone, 'meta_prefix' => sanitize_key(str_replace(':','', (string) ($options['meta_prefix'] ?? 'meta'))), ]; } private function validate_rows($session, $mapping, $options, $offset, $limit) { $rows = $this->rows($session['path'], $offset, $limit); $results = ['rows' => [], 'valid' => 0, 'warnings' => 0, 'errors' => 0, 'duplicates' => 0, 'new' => 0, 'updates' => 0, 'offset' => $offset]; foreach ($rows as $row_index => $row) { $data = $this->row_data($session['headers'], $row); $check = $this->validate_row($data, $mapping, $options); $results['rows'][] = ['row' => $offset + $row_index + 2, 'title' => $check['title'], 'status' => $check['status'], 'messages' => $check['messages']]; if ($check['status'] === 'error') $results['errors']++; elseif ($check['status'] === 'warning') { $results['warnings']++; $results['valid']++; } else $results['valid']++; if ($check['duplicate']) { $results['duplicates']++; if ($options['duplicate_mode'] === 'update') $results['updates']++; } else { $results['new']++; } } $results['offset'] = $offset + count($rows); return $results; } private function validate_row($data, $mapping, $options) { $values = $this->mapped_values($data, $mapping); $messages = []; $status = 'ok'; $title = trim((string) ($values['title'] ?? '')); $date = trim((string) ($values['start_date'] ?? '')); $start = null; $end = null; if ($title === '') { $messages[] = __('Event title is required.', 'eventqora'); $status = 'error'; } if ($date === '') { $messages[] = __('Start date is required.', 'eventqora'); $status = 'error'; } else { $start = $this->parse_date($date, $options['date_format']); if (is_wp_error($start)) { $messages[] = $start->get_error_message(); $status = 'error'; } } if (array_key_exists('start_time', $values) && trim((string) $values['start_time']) !== '') { if (is_wp_error($this->parse_time($values['start_time']))) { // translators: %s is the invalid start-time value from the CSV. $messages[] = sprintf(__('Invalid start time "%s".', 'eventqora'), $values['start_time']); $status = 'error'; } } if (array_key_exists('end_date', $values) && trim((string) $values['end_date']) !== '') { $end = $this->parse_date($values['end_date'], $options['date_format']); if (is_wp_error($end)) { $messages[] = $end->get_error_message(); $status = 'error'; } } if (array_key_exists('end_time', $values) && trim((string) $values['end_time']) !== '') { if (is_wp_error($this->parse_time($values['end_time']))) { // translators: %s is the invalid end-time value from the CSV. $messages[] = sprintf(__('Invalid end time "%s".', 'eventqora'), $values['end_time']); $status = 'error'; } } if ($start instanceof DateTime && $end instanceof DateTime && $end < $start) { $messages[] = __('End date cannot be earlier than the start date.', 'eventqora'); $status = 'error'; } if (!empty($values['timezone']) && !in_array($values['timezone'], timezone_identifiers_list(), true)) { $messages[] = __('Invalid timezone; the selected default timezone will be used.', 'eventqora'); $status = 'warning'; } $duplicate = $this->find_existing($values); if ($duplicate) { // translators: %d is the ID of the existing duplicate event. $messages[] = sprintf(__('Duplicate of existing event #%d.', 'eventqora'), $duplicate); if ($status !== 'error' && $options['duplicate_mode'] === 'create') { $messages[] = __('Create New is selected, so the duplicate will still be imported as a new event.', 'eventqora'); $status = 'warning'; } elseif ($status !== 'error') { $status = 'warning'; } } if (!empty($values['recurrence_rule']) && !has_action('evqra_event_import_recurrence') && !class_exists('EVQRA_Recurring_Events')) { $messages[] = __('RRULE supplied, but no recurrence import extension is active; the rule will be stored as event metadata only.', 'eventqora'); if ($status !== 'error') $status = 'warning'; } return ['status'=>$status,'messages'=>$messages,'title'=>$title,'duplicate'=>(bool)$duplicate]; } private function import_rows($session, $mapping, $options, $offset, $limit) { $rows = $this->rows($session['path'], $offset, $limit); $result = ['offset'=>$offset,'next_offset'=>$offset + count($rows),'processed'=>count($rows),'created'=>0,'updated'=>0,'skipped'=>0,'warnings'=>0,'errors'=>0,'error_rows'=>[],'done'=>($offset + count($rows) >= $session['total'])]; foreach ($rows as $row_index => $row) { $line = $offset + $row_index + 2; $data = $this->row_data($session['headers'], $row); $check = $this->validate_row($data, $mapping, $options); if ($check['status'] === 'error') { $result['errors']++; $result['error_rows'][] = ['row'=>$line,'title'=>$check['title'],'messages'=>$check['messages']]; continue; } $values = $this->mapped_values($data, $mapping); $existing = $this->find_existing($values); if ($existing && $options['duplicate_mode'] === 'skip') { $result['skipped']++; continue; } $post_id = $existing && $options['duplicate_mode'] === 'update' ? $existing : 0; $created = !$post_id; $post = $this->build_post($values, $post_id); $saved = $post_id ? wp_update_post($post, true) : wp_insert_post($post, true); if (is_wp_error($saved)) { $result['errors']++; $result['error_rows'][] = ['row'=>$line,'title'=>$check['title'],'messages'=>[$saved->get_error_message()]]; continue; } $post_id = (int) $saved; $meta_errors = $this->save_event_data($post_id, $values, $data, $mapping, $options); if ($meta_errors) { $result['warnings']++; $result['error_rows'][] = ['row'=>$line,'title'=>$check['title'],'messages'=>$meta_errors]; } if ($created) $result['created']++; else $result['updated']++; } return $result; } private function row_data($headers, $row) { $data = []; foreach ($headers as $i => $header) $data[$header] = isset($row[$i]) ? trim((string) $row[$i]) : ''; return $data; } private function mapped_values($data, $mapping) { $values = []; foreach ($mapping as $header => $target) { if ($target === '' || !array_key_exists($header, $data)) continue; $values[$target] = $data[$header]; } return $values; } private function build_post($values, $post_id = 0) { $status = sanitize_key($values['status'] ?? 'publish'); $allowed = ['publish','draft','pending','private']; if (!in_array($status, $allowed, true)) $status = 'publish'; return [ 'ID' => absint($post_id), 'post_type' => 'evqra_event', 'post_status' => $status, 'post_title' => sanitize_text_field($values['title'] ?? ''), 'post_content' => wp_kses_post($values['description'] ?? ''), ]; } private function save_event_data($post_id, $values, $data, $mapping, $options) { $warnings = []; $date = $this->parse_date($values['start_date'] ?? '', $options['date_format']); if (is_wp_error($date)) return [$date->get_error_message()]; update_post_meta($post_id, '_evqra_event_date', $date->format('Y-m-d')); if (!empty($values['start_time'])) update_post_meta($post_id, '_evqra_event_time', $this->parse_time($values['start_time'])); if (!empty($values['end_date'])) { $end = $this->parse_date($values['end_date'], $options['date_format']); if (!is_wp_error($end)) update_post_meta($post_id, '_evqra_event_end_date', $end->format('Y-m-d')); } if (!empty($values['end_time'])) update_post_meta($post_id, '_evqra_event_end_time', $this->parse_time($values['end_time'])); $timezone = $values['timezone'] ?? $options['timezone']; if ($timezone && in_array($timezone, timezone_identifiers_list(), true)) update_post_meta($post_id, '_evqra_event_timezone', $timezone); if (array_key_exists('all_day', $values)) update_post_meta($post_id, '_evqra_event_all_day', $this->truthy($values['all_day']) ? '1' : '0'); if (!empty($values['featured'])) update_post_meta($post_id, '_evqra_event_featured', $this->truthy($values['featured']) ? '1' : '0'); if (array_key_exists('booking_mode', $values)) { $booking_mode = sanitize_key($values['booking_mode']); // Backward compatibility: accept the legacy CSV value "tickets" but persist // the canonical EventQora booking mode "ticket". if ($booking_mode === 'tickets') { $booking_mode = 'ticket'; } update_post_meta($post_id, '_evqra_booking_mode', $booking_mode); } if (!empty($values['booking_start'])) update_post_meta($post_id, '_evqra_booking_start', EVQRA_Core::sanitize_datetime_local($values['booking_start'])); if (!empty($values['booking_end'])) update_post_meta($post_id, '_evqra_booking_end', EVQRA_Core::sanitize_datetime_local($values['booking_end'])); if (!empty($values['external_id'])) update_post_meta($post_id, '_evqra_import_external_id', sanitize_text_field($values['external_id'])); if (!empty($values['category'])) $this->assign_terms($post_id, $values['category'], 'evqra_event_category'); if (!empty($values['tags'])) $this->assign_terms($post_id, $values['tags'], 'post_tag'); if (!empty($values['venue'])) { $venue_id = $this->resolve_related_post($values['venue'], 'evqra_venue', 'venue'); if ($venue_id) update_post_meta($post_id, '_evqra_event_venue_id', $venue_id); } if (!empty($values['organizer'])) { $organizer_id = $this->resolve_related_post($values['organizer'], 'evqra_organizer', 'organizer'); if ($organizer_id) update_post_meta($post_id, '_evqra_event_organizer_id', $organizer_id); } if (!empty($values['image'])) { $id = $this->import_image($values['image'], $post_id); if ($id) set_post_thumbnail($post_id, $id); elseif ($id === false) $warnings[] = __('Featured image could not be imported.', 'eventqora'); } if (!empty($values['gallery'])) { $ids = []; foreach (preg_split('/[|\n,]+/', $values['gallery']) as $url) { $url = trim($url); if ($url === '') continue; $id = $this->import_image($url, $post_id); if ($id) $ids[] = $id; } if ($ids) update_post_meta($post_id, '_evqra_event_gallery', implode(',', $ids)); } foreach ($mapping as $header => $target) { if (strpos($target, 'meta:') !== 0) continue; $key = sanitize_key(substr($target, 5)); if ($key !== '' && isset($data[$header])) update_post_meta($post_id, '_' . ltrim($key, '_'), sanitize_textarea_field($data[$header])); } if (!empty($values['recurrence_rule'])) { $rrule = sanitize_text_field($values['recurrence_rule']); update_post_meta($post_id, '_evqra_event_rrule', $rrule); if (has_action('evqra_event_import_recurrence')) { do_action('evqra_event_import_recurrence', $post_id, $rrule, $values); } elseif (class_exists('EVQRA_Recurring_Events')) { // The EventQora Recurring Events extension owns occurrence generation. // The canonical _evqra_event_rrule field is enough for the extension to consume it. } else { $warnings[] = __('RRULE was saved, but the EventQora Recurring Events extension is not active.', 'eventqora'); } if (class_exists('EVQRA_Occurrence_Engine')) { EVQRA_Occurrence_Engine::invalidate_event($post_id); } } do_action('evqra_event_imported', $post_id, $values, $data); return $warnings; } private function assign_terms($post_id, $raw, $taxonomy) { $terms = []; foreach (preg_split('/[,|]+/', $raw) as $value) { $value = trim($value); if ($value === '') continue; $term = term_exists($value, $taxonomy); if (!$term) $term = wp_insert_term($value, $taxonomy); if (!is_wp_error($term)) $terms[] = (int) (is_array($term) ? $term['term_id'] : $term); } if ($terms) wp_set_post_terms($post_id, $terms, $taxonomy, false); } private function resolve_related_post($value, $post_type, $kind) { if (is_numeric($value)) { $id = absint($value); return get_post_type($id) === $post_type ? $id : 0; } $name = sanitize_text_field($value); $posts = get_posts(['post_type'=>$post_type,'title'=>$name,'post_status'=>'any','numberposts'=>1]); if ($posts) return (int) $posts[0]->ID; $id = wp_insert_post(['post_type'=>$post_type,'post_title'=>$name,'post_status'=>'publish'], true); return is_wp_error($id) ? 0 : (int) $id; } private function find_existing($values) { if (!empty($values['external_id'])) { $ids = get_posts([ 'post_type'=>'evqra_event','post_status'=>'any','posts_per_page'=>1,'fields'=>'ids', 'meta_key'=>'_evqra_import_external_id','meta_value'=>sanitize_text_field($values['external_id']), ]); if ($ids) return (int) $ids[0]; } if (!empty($values['title']) && !empty($values['start_date'])) { $date = $this->parse_date($values['start_date'], 'auto'); if (!is_wp_error($date)) { $ids = get_posts([ 'post_type'=>'evqra_event','post_status'=>'any','posts_per_page'=>1,'fields'=>'ids', 'title'=>sanitize_text_field($values['title']), 'meta_query'=>[['key'=>'_evqra_event_date','value'=>$date->format('Y-m-d')]], ]); if ($ids) return (int) $ids[0]; } } return 0; } private function parse_date($value, $format) { $value = trim((string) $value); if ($value === '') return new WP_Error('missing_date', __('Start date is required.', 'eventqora')); if ($format !== 'auto') { $date = DateTime::createFromFormat('!' . $format, $value); if ($date instanceof DateTime) return $date; } if ($format === 'auto' && preg_match('/^(\\d{1,2})[\\/-](\\d{1,2})[\\/-](\\d{4})$/', $value, $parts)) { if ((int) $parts[1] <= 12 && (int) $parts[2] <= 12 && (int) $parts[1] !== (int) $parts[2]) { // translators: %s is the ambiguous date value from the CSV. return new WP_Error('ambiguous_date', sprintf(__('Date "%s" is ambiguous. Select an explicit date format before importing.', 'eventqora'), $value)); } } $formats = ['Y-m-d','Y/m/d','d/m/Y','m/d/Y','d-m-Y','m-d-Y','F j, Y','j F Y','M j, Y','j M Y']; foreach ($formats as $candidate) { $date = DateTime::createFromFormat('!' . $candidate, $value); if ($date instanceof DateTime) return $date; } $timestamp = strtotime($value); // translators: %s is the date value that could not be parsed. return $timestamp ? (new DateTime())->setTimestamp($timestamp) : new WP_Error('invalid_date', sprintf(__('Could not parse date "%s".', 'eventqora'), $value)); } private function parse_time($value) { $value = trim((string) $value); if ($value === '') return new WP_Error('missing_time', __('Time value is empty.', 'eventqora')); foreach (['H:i:s','H:i','g:i A','g:i a','g A','G:i'] as $format) { $time = DateTime::createFromFormat('!' . $format, $value); $errors = DateTime::getLastErrors(); if ($time instanceof DateTime && ($errors === false || (empty($errors['warning_count']) && empty($errors['error_count'])))) { return $time->format('H:i:s'); } } // translators: %s is the time value that could not be parsed. return new WP_Error('invalid_time', sprintf(__('Could not parse time "%s".', 'eventqora'), $value)); } private function truthy($value) { return in_array(strtolower(trim((string)$value)), ['1','yes','true','on','y'], true); } private function import_image($url, $post_id) { $url = esc_url_raw(trim((string)$url)); if (!$url || !wp_http_validate_url($url)) return false; $parts = wp_parse_url($url); if (!$parts || empty($parts['host']) || !in_array(strtolower($parts['scheme'] ?? ''), ['http','https'], true)) return false; $tmp = wp_tempnam($url); if (!$tmp) return false; $response = wp_safe_remote_get($url, [ 'timeout'=>20,'redirection'=>3,'stream'=>true,'filename'=>$tmp, 'limit_response_size'=>10 * MB_IN_BYTES,'headers'=>['Accept'=>'image/*'], ]); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) < 200 || wp_remote_retrieve_response_code($response) >= 300) { wp_delete_file($tmp); return false; } $info = @getimagesize($tmp); $allowed = ['image/jpeg'=>'jpg','image/png'=>'png','image/gif'=>'gif','image/webp'=>'webp']; if (!$info || empty($allowed[strtolower($info['mime'])])) { wp_delete_file($tmp); return false; } $existing = get_posts([ 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_key' => '_evqra_imported_source_url', 'meta_value' => $url, ]); if ($existing) { return (int) $existing[0]; } $name = sanitize_file_name(wp_basename($parts['path'] ?? 'evqra-event-image')); $name = pathinfo($name, PATHINFO_FILENAME); $file = ['name'=>($name ?: 'evqra-event-image') . '.' . $allowed[strtolower($info['mime'])], 'tmp_name'=>$tmp, 'type'=>strtolower($info['mime'])]; $id = media_handle_sideload($file, $post_id); if (is_wp_error($id)) { wp_delete_file($tmp); return false; } update_post_meta($id, '_evqra_imported_attachment', 1); update_post_meta($id, '_evqra_imported_source_url', $url); return (int) $id; } } new EVQRA_Event_Import();