'Method not allowed']); exit; } // Writes need the API key when NEWTON_API_KEY is set (see api-check.php) require_api_key(); $tournamentsDir = dirname(__DIR__) . '/tournaments/'; // Check if directory exists, create if it doesn't if (!is_dir($tournamentsDir)) { if (!mkdir($tournamentsDir, 0755, true)) { http_response_code(500); echo json_encode(['error' => 'Failed to create tournaments directory']); exit; } } // Check if directory is writable if (!is_writable($tournamentsDir)) { http_response_code(500); echo json_encode(['error' => 'Tournaments directory is not writable']); exit; } // Get POST data $input = file_get_contents('php://input'); // Cap payload size before decoding — a real tournament export is well under // this; anything larger is a disk-fill attempt or a mistake $maxUploadBytes = 10 * 1024 * 1024; if (strlen($input) > $maxUploadBytes) { http_response_code(413); echo json_encode(['error' => 'Payload too large (max 10 MB)']); exit; } $payload = json_decode($input, true); if ($payload === null) { http_response_code(400); echo json_encode(['error' => 'Invalid JSON payload']); exit; } // Validate required fields if (!isset($payload['filename']) || !isset($payload['data'])) { http_response_code(400); echo json_encode(['error' => 'Missing filename or data']); exit; } $filename = basename($payload['filename']); // Security: prevent directory traversal $data = $payload['data']; // Minimal shape check — the payload must look like a tournament export, // not arbitrary JSON hosted under /tournaments/ if (!is_array($data) || !isset($data['id'], $data['name']) || !isset($data['players']) || !is_array($data['players'])) { http_response_code(400); echo json_encode(['error' => 'Data does not look like a tournament export (missing id, name, or players)']); exit; } // Validate filename - allow unicode characters but prevent directory traversal if (!str_ends_with($filename, '.json')) { http_response_code(400); echo json_encode(['error' => 'Filename must end with .json']); exit; } // Prevent dangerous characters (path traversal, null bytes) if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false || strpos($filename, "\0") !== false) { http_response_code(400); echo json_encode(['error' => 'Invalid filename - contains dangerous characters']); exit; } // Save tournament data $filepath = $tournamentsDir . $filename; // Check if file already exists and overwrite not explicitly requested $overwriteRequested = isset($_GET['overwrite']) && $_GET['overwrite'] === 'true'; if (file_exists($filepath) && !$overwriteRequested) { http_response_code(409); // Conflict echo json_encode([ 'error' => 'Tournament file already exists', 'filename' => $filename, 'exists' => true, 'message' => 'A tournament with this filename already exists. Delete it first or use overwrite option.' ]); exit; } $jsonData = json_encode($data, JSON_PRETTY_PRINT); if ($jsonData === false) { http_response_code(500); echo json_encode(['error' => 'Failed to encode tournament data']); exit; } // Capture before the write — afterwards file_exists() is always true $wasOverwritten = file_exists($filepath); if (file_put_contents($filepath, $jsonData) === false) { http_response_code(500); echo json_encode(['error' => 'Failed to save tournament file']); exit; } // The club's player database travels with each upload (js/player-registry.js). Keep the // newest copy for Analytics, which reads it to show current names and follow renames and // merges. Outside the top level, so the tournament list doesn't pick it up. $registrySaved = false; if (isset($data['playerDatabase']['players']) && is_array($data['playerDatabase']['players'])) { $registryDir = $tournamentsDir . 'registry/'; $registryFile = $registryDir . 'player-database.json'; if (is_dir($registryDir) || @mkdir($registryDir, 0755, true)) { $incomingAt = isset($data['playerDatabase']['updatedAt']) ? (float)$data['playerDatabase']['updatedAt'] : 0; $current = is_file($registryFile) ? json_decode((string)@file_get_contents($registryFile), true) : null; $currentAt = is_array($current) && isset($current['updatedAt']) ? (float)$current['updatedAt'] : -1; if ($incomingAt >= $currentAt) { $registryJson = json_encode([ 'players' => array_values($data['playerDatabase']['players']), 'updatedAt' => $incomingAt ], JSON_PRETTY_PRINT); $registrySaved = $registryJson !== false && @file_put_contents($registryFile, $registryJson) !== false; } } } // Success echo json_encode([ 'success' => true, 'playerDatabaseSaved' => $registrySaved, 'filename' => $filename, 'path' => '/tournaments/' . $filename, 'message' => $wasOverwritten ? 'Tournament updated successfully' : 'Tournament uploaded successfully', 'overwritten' => $wasOverwritten ]);