0, 'reset' => $now + $windowSeconds]; if (file_exists($rateFile)) { $content = file_get_contents($rateFile); $data = json_decode($content, true) ?? $data; } // Reset if window expired if ($now > $data['reset']) { $data = ['count' => 0, 'reset' => $now + $windowSeconds]; } $data['count']++; file_put_contents($rateFile, json_encode($data)); return $data['count'] <= $maxRequests; } // Check rate limit for login endpoint (5 attempts per minute) if (preg_match('#^login$#', trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/')) && $_SERVER['REQUEST_METHOD'] === 'POST') { $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; if (!checkRateLimit('login_' . $ip, 5, 60)) { http_response_code(429); echo json_encode(['error' => 'Too many requests. Please try again later.']); exit; } } // Check general rate limit (500 requests per minute per IP - increased for testing) $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; if (!checkRateLimit('general_' . $ip, 500, 60)) { http_response_code(429); echo json_encode(['error' => 'Rate limit exceeded']); exit; } require_once __DIR__ . '/lib/Auth.php'; require_once __DIR__ . '/lib/ScoreScanner.php'; $auth = new Auth(); $scoresPath = getenv('SCORES_PATH'); if ($scoresPath === false || $scoresPath === '') { $scoresPath = __DIR__ . '/../legacy/Scores/'; } $scanner = new ScoreScanner($scoresPath); // Get Authorization header $token = null; $headers = getallheaders(); if (isset($headers['Authorization'])) { $authHeader = $headers['Authorization']; if (preg_match('/Bearer\s+(.+)/i', $authHeader, $matches)) { $token = $matches[1]; } } // Parse request $method = $_SERVER['REQUEST_METHOD']; $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $path = trim($uri, '/'); // Remove 'api/' prefix if present $path = preg_replace('#^api/#', '', $path); // Debug: log the path // file_put_contents('/tmp/api_debug.log', date('Y-m-d H:i:s') . " PATH: $path METHOD: $method\n", FILE_APPEND); // GET /download/:path - Download PDF (BEFORE auth check) if (preg_match('#^download/([^?]+)#', $path, $matches) && $method === 'GET') { $filePath = urldecode($matches[1]); // Security: Check for null bytes (must use double quotes for escape sequence) if (strpos($filePath, "\x00") !== false) { http_response_code(400); echo json_encode(['error' => 'Invalid file path']); exit; } // Security: Prevent directory traversal $filePath = str_replace(['../', '..\\', '..'], '', $filePath); // Check token from header or query parameter $downloadToken = $_GET['token'] ?? $token; $downloadToken = urldecode($downloadToken ?? ''); if ($downloadToken === null || $downloadToken === '') { http_response_code(401); echo json_encode(['error' => 'Authorization required']); exit; } $user = $auth->verifyToken($downloadToken); if ($user === null) { http_response_code(401); echo json_encode(['error' => 'Invalid token']); exit; } $basePath = getenv('SCORES_PATH') ?: __DIR__ . '/../legacy/Scores/'; $fullPath = $basePath . $filePath; // Security: Verify resolved path is within allowed directory $realBasePath = realpath($basePath); $realFilePath = realpath($fullPath); if ($realFilePath === false || strpos($realFilePath, $realBasePath) !== 0) { http_response_code(403); echo json_encode(['error' => 'Access denied']); exit; } // Security: Check file extension if (strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION)) !== 'pdf') { http_response_code(403); echo json_encode(['error' => 'Only PDF files are allowed']); exit; } if (!file_exists($realFilePath) || !is_file($realFilePath)) { http_response_code(404); echo json_encode(['error' => 'File not found']); exit; } header('Content-Type: application/pdf'); header('Content-Length: ' . filesize($realFilePath)); header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"'); readfile($realFilePath); exit; } // Route matching if (($path === 'login' || $path === 'api/login') && $method === 'POST') { $input = json_decode(file_get_contents('php://input'), true); $username = $input['username'] ?? ''; $password = $input['password'] ?? ''; $result = $auth->login($username, $password); if ($result['success']) { echo json_encode([ 'success' => true, 'token' => $result['token'], 'user' => $result['user'] ]); } else { http_response_code(401); echo json_encode([ 'success' => false, 'error' => $result['error'] ]); } exit; } // Protected routes - require auth if ($token === null) { http_response_code(401); echo json_encode(['error' => 'Authorization required']); exit; } $user = $auth->verifyToken($token); if ($user === null) { http_response_code(401); echo json_encode(['error' => 'Invalid token']); exit; } // GET /scores - List all scores if ($path === 'scores' && $method === 'GET') { $scores = $scanner->listScores(); if (isset($scores['error'])) { http_response_code(500); echo json_encode(['success' => false, 'error' => $scores['error']]); exit; } echo json_encode(['success' => true, 'scores' => $scores]); exit; } // GET /scores/:id - Get score details if (preg_match('#^scores/(\d+)$#', $path, $matches) && $method === 'GET') { $scoreId = $matches[1]; $score = $scanner->getScore($scoreId); if ($score === null) { http_response_code(404); echo json_encode(['error' => 'Score not found']); exit; } echo json_encode(['success' => true, 'score' => $score]); exit; } // GET /scores/:id/instruments - Get instruments for a score // GET /scores/:id/instruments?piece=1 - Get instruments for a specific piece if (preg_match('#^scores/(\d+)/instruments$#', $path, $matches) && $method === 'GET') { $scoreId = $matches[1]; $pieceId = $_GET['piece'] ?? null; $instruments = $scanner->getInstruments($scoreId, $pieceId); echo json_encode(['success' => true, 'instruments' => $instruments]); exit; } // GET /pieces/:scoreId - Get pieces for a score if (preg_match('#^pieces/(\d+)$#', $path, $matches) && $method === 'GET') { $scoreId = $matches[1]; $pieces = $scanner->getPieces($scoreId); echo json_encode(['success' => true, 'pieces' => $pieces]); exit; } // ADMIN ROUTES - require admin role if ($user['role'] !== 'admin') { http_response_code(403); echo json_encode(['error' => 'Admin access required']); exit; } // POST /admin/scores - Create new score if ($path === 'admin/scores' && $method === 'POST') { $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? null; $name = $input['name'] ?? ''; $compositor = $input['compositor'] ?? ''; $pieces = $input['pieces'] ?? []; // Auto-generate ID if not provided if (empty($id)) { $scores = $scanner->listScores(); $maxId = 0; foreach ($scores as $s) { $num = intval($s['id']); if ($num > $maxId) $maxId = $num; } // Skip existing IDs $scoresPath = getenv('SCORES_PATH') ?: __DIR__ . '/../legacy/Scores/'; $id = strval($maxId + 1); while (is_dir($scoresPath . $id)) { $id = strval(intval($id) + 1); } // Pad with zeros to 3 digits if needed if (strlen($id) < 3) { $id = str_pad($id, 3, '0', STR_PAD_LEFT); } } if (empty($name)) { http_response_code(400); echo json_encode(['error' => 'ID and name required']); exit; } $result = $scanner->createScore($id, $name, $compositor, $pieces); if ($result['success']) { echo json_encode(['success' => true, 'score' => $result['score']]); } else { http_response_code(400); echo json_encode(['error' => $result['error']]); } exit; } // PUT /admin/scores/:id - Update score if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'PUT') { $scoreId = $matches[1]; $input = json_decode(file_get_contents('php://input'), true); $name = $input['name'] ?? null; $compositor = $input['compositor'] ?? null; $ressource = $input['ressource'] ?? ''; $result = $scanner->updateScore($scoreId, $name, $compositor, $ressource); if ($result['success']) { echo json_encode(['success' => true]); } else { http_response_code(400); echo json_encode(['error' => $result['error']]); } exit; } // DELETE /admin/scores/:id - Delete score if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'DELETE') { $scoreId = $matches[1]; $result = $scanner->deleteScore($scoreId); if ($result['success']) { echo json_encode(['success' => true]); } else { if (strpos($result['error'], 'not found') !== false) { http_response_code(404); } else { http_response_code(400); } echo json_encode(['error' => $result['error']]); } exit; } // POST /admin/scores/:id/upload - Upload PDF if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') { $scoreId = $matches[1]; // Check for upload errors if (!isset($_FILES['file']) || $_FILES['file']['error'] === UPLOAD_ERR_NO_FILE) { // Check if post_max_size was exceeded $contentLength = $_SERVER['CONTENT_LENGTH'] ?? 0; if ($contentLength > 0 && empty($_FILES)) { $maxSize = ini_get('post_max_size'); http_response_code(413); echo json_encode(['error' => "File too large. Maximum size is $maxSize"]); exit; } http_response_code(400); echo json_encode(['error' => 'No file uploaded']); exit; } $file = $_FILES['file']; // Check for PHP upload errors if ($file['error'] !== UPLOAD_ERR_OK) { $errorMsg = 'Upload failed'; switch ($file['error']) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: $maxSize = ini_get('upload_max_filesize'); $errorMsg = "File too large. Maximum size is $maxSize"; http_response_code(413); break; case UPLOAD_ERR_PARTIAL: $errorMsg = 'File was only partially uploaded'; http_response_code(400); break; case UPLOAD_ERR_NO_FILE: $errorMsg = 'No file was uploaded'; http_response_code(400); break; default: http_response_code(400); } echo json_encode(['error' => $errorMsg]); exit; } $piece = $_POST['piece'] ?? '1'; $instrument = $_POST['instrument'] ?? ''; $version = $_POST['version'] ?? '1'; $key = $_POST['key'] ?? ''; $clef = $_POST['clef'] ?? ''; $variant = $_POST['variant'] ?? ''; $part = $_POST['part'] ?? '1'; $watermark = isset($_POST['watermark']) && $_POST['watermark'] === 'true'; $watermarkPosition = $_POST['watermarkPosition'] ?? 'left'; $result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part, $watermark, $watermarkPosition); if ($result['success']) { echo json_encode(['success' => true, 'path' => $result['path']]); } else { http_response_code(400); echo json_encode(['error' => $result['error']]); } exit; } // GET /admin/scores/:id/files - Get all files for a score if (preg_match('#^admin/scores/(\d+)/files$#', $path, $matches) && $method === 'GET') { $scoreId = $matches[1]; $result = $scanner->getScoreFiles($scoreId); if ($result['success']) { echo json_encode(['success' => true, 'files' => $result['files']]); } else { http_response_code(400); echo json_encode(['error' => $result['error']]); } exit; } // DELETE /admin/scores/:id/files - Delete a specific file if (preg_match('#^admin/scores/(\d+)/files$#', $path, $matches) && $method === 'DELETE') { $scoreId = $matches[1]; $filePath = $_GET['path'] ?? ''; if (empty($filePath)) { http_response_code(400); echo json_encode(['error' => 'File path required']); exit; } $result = $scanner->deleteScoreFile($scoreId, $filePath); if ($result['success']) { echo json_encode(['success' => true]); } else { http_response_code(400); echo json_encode(['error' => $result['error']]); } exit; } // 404 Not Found http_response_code(404); echo json_encode(['error' => 'Not found']);