['ad unit name', 'name', 'ad unit', 'ad unit display name'], 'code' => ['ad unit code', 'code', 'id', 'ad unit id code'], 'sizes' => ['sizes', 'size', 'size(s)', 'ad unit sizes', 'creative sizes'], 'device' => ['device'], 'label' => ['label', 'description'], 'placement' => ['placement', 'placements', 'gam placement', 'slotname', 'slot name'], 'status' => ['status'], ]; public static function parse($file_path) { if (!file_exists($file_path) || !is_readable($file_path)) { return new WP_Error('file_error', 'File not found or not readable'); } $csv = file_get_contents($file_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- a local uploaded file, not a URL. if ($csv === false) { return new WP_Error('file_error', 'Could not open file'); } return self::parse_string($csv); } public static function parse_string($csv) { $rows = []; foreach (self::csv_rows((string) $csv) as $row) { if (isset($row[0])) { $row[0] = preg_replace('/^\xEF\xBB\xBF/', '', (string) $row[0]); } if (count(array_filter($row, function ($c) { return trim((string) $c) !== ''; })) === 0) { continue; } $rows[] = array_map('trim', $row); } if (!$rows) { return new WP_Error('parse_error', 'Empty CSV file'); } $map = null; foreach (array_slice($rows, 0, 10, true) as $i => $row) { $candidate = self::header_map($row); if (isset($candidate['sizes']) && (isset($candidate['name']) || isset($candidate['code']))) { $map = $candidate; $rows = array_slice($rows, $i + 1); break; } } if ($map === null) { if (!preg_match('/^[A-Za-z0-9_.\-\/]+$/', $rows[0][0] ?? '')) { return new WP_Error('format_error', 'CSV format not recognized. Expected AdGhost rows (code,name,sizes,label,placement,target — as in ad-units-example.csv) or a Google Ad Manager export with Ad unit name, Ad unit code and Sizes columns.'); } $map = ['code' => 0, 'name' => 1, 'sizes' => 2, 'label' => 3, 'placement' => 4]; } $units = []; $seen = []; foreach ($rows as $row) { $get = function ($field) use ($row, $map) { return isset($map[$field]) ? trim((string) ($row[$map[$field]] ?? '')) : ''; }; $name = $get('name'); $code = AdGhost_Helper::sanitize_gam_code($get('code')); if ($name === '' && $code === '') { continue; } if (in_array(strtolower($get('status')), ['archived', 'inactive', 'deactivated'], true)) { continue; } if ($name === '') { $name = basename($code); } $name = trim(str_replace('_', ' ', $name)); if ($code === '') { $code = AdGhost_Helper::generate_id($name); } if (isset($seen[$code])) { continue; } $seen[$code] = true; $sizes = self::sizes($get('sizes')); $type = 'standard'; if ($sizes === 'out-of-page' || preg_match('/interstitial/i', $name . ' ' . $code)) { $type = preg_match('/reward/i', $name . ' ' . $code) ? 'reward' : 'interstitial'; } elseif (preg_match('/reward/i', $name . ' ' . $code)) { $type = 'reward'; } $device = strtolower($get('device')); $unit = [ 'id' => $code, 'name' => sanitize_text_field($name), 'sizes' => sanitize_text_field($type === 'standard' ? ($sizes ?: '300x250') : ($type === 'interstitial' ? 'out-of-page' : '1x1')), 'type' => $type, 'device' => in_array($device, ['both', 'desktop', 'mobile'], true) ? $device : 'both', 'placementMode' => 'auto', 'isImported' => true, ]; $position = $type === 'standard' ? self::position_from($get('placement') . ' ' . $get('label')) : ''; if ($position !== '') { $unit['position'] = $position; } foreach (['label' => 'label', 'placement' => 'gamPlacement'] as $field => $key) { if ($get($field) !== '') { $unit[$key] = sanitize_text_field($get($field)); } } $units[] = $unit; } if (!$units) { return new WP_Error('parse_error', 'The CSV has a header but no ad units.'); } return $units; } public static function position_from($text) { $t = strtolower((string) $text); if ($t === '' || trim($t) === '') { return ''; } $w = function ($words) { return '/(? 'anchor', '/in[\s_-]*(article|content)|incontent/' => 'incontent', '/side[\s_-]*bar|skyscraper/' => 'sidebar', $w('rail') => 'sidebar', '/leaderboard|header/' => 'header', '/pre[\s_-]*footer|footer/' => 'footer', $w('top|atf') => 'header', $w('bottom|btf') => 'footer', ]; foreach ($rules as $pattern => $position) { if (preg_match($pattern, $t)) { return $position; } } return ''; } private static function header_map(array $row) { $map = []; foreach ($row as $index => $cell) { $cell = strtolower(trim(preg_replace('/^[#\s]+/', '', (string) $cell))); foreach (self::HEADERS as $field => $spellings) { if (!isset($map[$field]) && in_array($cell, $spellings, true)) { $map[$field] = $index; } } } return $map; } public static function sizes($raw) { $raw = strtolower((string) $raw); if ($raw !== '' && preg_match('/out[\s_-]*of[\s_-]*page/', $raw) && !preg_match('/\d+\s*x\s*\d+/', $raw)) { return 'out-of-page'; } preg_match_all('/(\d{1,4})\s*[x×]\s*(\d{1,4})|fluid/u', $raw, $m, PREG_SET_ORDER); $out = []; foreach ($m as $match) { $size = $match[0] === 'fluid' ? 'fluid' : $match[1] . 'x' . $match[2]; $out[$size] = true; } return implode(';', array_keys($out)); } private static function csv_rows($csv) { $rows = []; $row = []; $field = ''; $quoted = false; $length = strlen($csv); for ($i = 0; $i < $length; $i++) { $char = $csv[$i]; if ($quoted) { if ($char === '"') { if ($i + 1 < $length && $csv[$i + 1] === '"') { $field .= '"'; $i++; } else { $quoted = false; } } else { $field .= $char; } continue; } if ($char === '"') { $quoted = true; } elseif ($char === ',') { $row[] = $field; $field = ''; } elseif ($char === "\n" || $char === "\r") { if ($char === "\r" && $i + 1 < $length && $csv[$i + 1] === "\n") { $i++; } $row[] = $field; $rows[] = $row; $row = []; $field = ''; } else { $field .= $char; } } if ($field !== '' || $row) { $row[] = $field; $rows[] = $row; } return $rows; } }