<?php // admin/games/externalimport.php – Import games from external JSON export declare(strict_types=1); require __DIR__ . '/../inc/admin_boot.php'; $pdo = Database::pdo(); $err = ''; $msg = ''; $summary = null; // --------------------------------------------------------------------- // Stable CSRF for this page only (independent from global csrf_check()) // --------------------------------------------------------------------- if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } if (!isset($_SESSION['import_csrf'])) { $_SESSION['import_csrf'] = bin2hex(random_bytes(32)); } $importCsrf = (string)$_SESSION['import_csrf']; function import_csrf_is_valid(): bool { if (!isset($_POST['csrf'], $_SESSION['import_csrf'])) { return false; } $posted = (string)$_POST['csrf']; $session = (string)$_SESSION['import_csrf']; return hash_equals($session, $posted); } // --------------------------------------------------------------------- // Handle POST: upload + import // --------------------------------------------------------------------- if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Our own CSRF check (do NOT call global csrf_check() here) if (!import_csrf_is_valid()) { $err = 'Security token mismatch. Please reload the page and try again.'; } else { if (!isset($_FILES['file']) || !is_uploaded_file($_FILES['file']['tmp_name'])) { $err = 'Please choose a JSON export file.'; } elseif ($_FILES['file']['error'] !== UPLOAD_ERR_OK) { $err = 'Upload failed with error code: ' . $_FILES['file']['error']; } else { // Allow larger imports @set_time_limit(300); $raw = file_get_contents($_FILES['file']['tmp_name']); if ($raw === false || trim($raw) === '') { $err = 'The uploaded file appears to be empty.'; } else { $data = json_decode($raw, true); if (!is_array($data)) { $err = 'Invalid JSON format.'; } elseif (!isset($data['games']) || !is_array($data['games'])) { $err = 'Invalid games export JSON format (missing "games" array).'; } elseif (isset($data['export_type']) && $data['export_type'] !== 'games') { $err = 'This export file is not a games export (export_type != "games").'; } else { try { // Discover games table columns (excluding id and category_id) $colStmt = $pdo->query("SHOW COLUMNS FROM games"); $gameCols = []; while ($c = $colStmt->fetch(PDO::FETCH_ASSOC)) { $name = (string)$c['Field']; if ($name === 'id' || $name === 'category_id') { continue; } $gameCols[] = $name; } // Discover categories table columns $catStmt = $pdo->query("SHOW COLUMNS FROM categories"); $catCols = []; while ($c = $catStmt->fetch(PDO::FETCH_ASSOC)) { $catCols[] = (string)$c['Field']; } $hasCatSlug = in_array('slug', $catCols, true); $inserted = 0; $skipped = 0; $failed = 0; $errors = []; $categoryCache = []; // Optional: expected vs actual count $expectedCount = isset($data['games_count']) ? (int)$data['games_count'] : null; $actualCount = count($data['games']); // if ($expectedCount !== null && $expectedCount !== $actualCount) { /* log mismatch if needed */ } foreach ($data['games'] as $idx => $g) { if (!is_array($g)) { $failed++; continue; } $fields = (isset($g['fields']) && is_array($g['fields'])) ? $g['fields'] : []; $categoryName = trim((string)($g['category_name'] ?? '')); // Duplicate detection: $slug = isset($fields['slug']) ? (string)$fields['slug'] : ''; $externalId = isset($fields['external_id']) ? (string)$fields['external_id'] : ''; $provider = isset($fields['provider']) ? (string)$fields['provider'] : ''; $dupId = null; if ($externalId !== '' && $provider !== '') { $st = $pdo->prepare("SELECT id FROM games WHERE external_id = :eid AND provider = :prov LIMIT 1"); $st->execute([':eid' => $externalId, ':prov' => $provider]); $dupId = $st->fetchColumn() ?: null; } if (!$dupId && $slug !== '') { $st = $pdo->prepare("SELECT id FROM games WHERE slug = :slug LIMIT 1"); $st->execute([':slug' => $slug]); $dupId = $st->fetchColumn() ?: null; } if ($dupId) { $skipped++; continue; } // Resolve / create category by name $categoryId = null; if ($categoryName !== '') { if (isset($categoryCache[$categoryName])) { $categoryId = $categoryCache[$categoryName]; } else { $st = $pdo->prepare("SELECT id FROM categories WHERE name = :n LIMIT 1"); $st->execute([':n' => $categoryName]); $categoryId = $st->fetchColumn(); if (!$categoryId) { if ($hasCatSlug) { // generate unique slug from name $base = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $categoryName), '-')); if ($base === '') { $base = 'category'; } $slugCandidate = $base; $suffix = 2; while (true) { $st2 = $pdo->prepare("SELECT id FROM categories WHERE slug = :s LIMIT 1"); $st2->execute([':s' => $slugCandidate]); if (!$st2->fetchColumn()) { break; } $slugCandidate = $base . '-' . $suffix; $suffix++; } $ins = $pdo->prepare("INSERT INTO categories (name, slug) VALUES (:n, :s)"); $ins->execute([':n' => $categoryName, ':s' => $slugCandidate]); } else { $ins = $pdo->prepare("INSERT INTO categories (name) VALUES (:n)"); $ins->execute([':n' => $categoryName]); } $categoryId = (int)$pdo->lastInsertId(); } $categoryCache[$categoryName] = (int)$categoryId; } } if (!$fields) { $failed++; continue; } // Build insert columns intersection $insertCols = []; $params = []; foreach ($gameCols as $col) { if (array_key_exists($col, $fields)) { $insertCols[] = $col; $params[':' . $col] = $fields[$col]; } } if ($categoryId) { $insertCols[] = 'category_id'; $params[':category_id'] = $categoryId; } if (!$insertCols) { $failed++; continue; } $colSql = implode(',', $insertCols); $paramSql = implode(',', array_keys($params)); $sql = "INSERT INTO games ($colSql) VALUES ($paramSql)"; try { $st = $pdo->prepare($sql); $st->execute($params); $inserted++; } catch (Throwable $e) { $failed++; if ($failed <= 5) { $errors[] = 'Game #' . ($idx + 1) . ': ' . $e->getMessage(); } } } $summary = [ 'inserted' => $inserted, 'skipped' => $skipped, 'failed' => $failed, 'errors' => $errors, ]; if ($inserted > 0) { $msg = "Imported {$inserted} external game(s). Skipped {$skipped}, failed {$failed}."; } else { $err = "Nothing imported. Skipped {$skipped}, failed {$failed}."; } } catch (Throwable $e) { $err = 'Import failed: ' . $e->getMessage(); } } } } } } require __DIR__ . '/../inc/header.php'; ?> <section class="section"> <div class="container"> <div class="level"> <div class="level-left"> <h1 class="title">External Games Import</h1> </div> <div class="level-right"> <a class="button is-light" href="/admin/games/manage.php">Back to games</a> </div> </div> <?php if ($msg): ?> <div class="notification is-success"><?= h($msg) ?></div> <?php endif; ?> <?php if ($err): ?> <div class="notification is-danger"><?= h($err) ?></div> <?php endif; ?> <?php if ($summary): ?> <article class="message is-info"> <div class="message-header"> <p>Import summary</p> </div> <div class="message-body"> <ul> <li><strong>Inserted:</strong> <?= (int)$summary['inserted'] ?></li> <li><strong>Skipped (duplicates):</strong> <?= (int)$summary['skipped'] ?></li> <li><strong>Failed:</strong> <?= (int)$summary['failed'] ?></li> </ul> <?php if (!empty($summary['errors'])): ?> <hr> <p><strong>Sample errors:</strong></p> <ul> <?php foreach ($summary['errors'] as $e): ?> <li><?= h($e) ?></li> <?php endforeach; ?> </ul> <?php endif; ?> </div> </article> <?php endif; ?> <div class="box" style="max-width: 640px;"> <h2 class="subtitle">Upload external export JSON</h2> <p class="mb-3"> Export games from another installation using the <strong>“Export as JSON”</strong> bulk action or the <strong>“Export Games”</strong> box, then upload the JSON file here to import. </p> <form method="post" enctype="multipart/form-data"> <!-- our own CSRF token (stable for this page) --> <input type="hidden" name="csrf" value="<?= h($importCsrf) ?>"> <div class="field"> <label class="label">Export file (.json)</label> <div class="control"> <input class="input" type="file" name="file" accept=".json,application/json" required> </div> <p class="help">The file should be generated by the games export feature.</p> </div> <div class="field"> <button class="button is-primary"> <span class="icon"><i class="fas fa-file-import"></i></span> <span>Import external games</span> </button> </div> </form> </div> </div> </section> <?php require __DIR__ . '/../inc/footer.php'; ?>