pdo = $pdo; $this->config = $config; $this->logger = new Logger('api'); $this->analytics = new Analytics($pdo); $this->paste = new Paste($pdo); } public function handle(string $method, string $action, ?string $id): void { $this->sendCorsHeaders(); if ($method === 'OPTIONS') { http_response_code(204); exit; } if ($action === 'create' && $method === 'POST') { $this->create(); return; } if ($action === 'get' && $method === 'GET' && $id !== null) { $this->get($id); return; } if ($action === 'unlock' && $method === 'POST' && $id !== null) { $this->unlock($id); return; } $this->jsonResponse(['success' => false, 'message' => 'Not found.'], 404); } private function create(): void { $body = $this->readBody(); $text = trim((string)($body['text'] ?? '')); $password = (string)($body['password'] ?? ''); $expire = isset($body['expire']) ? (int)$body['expire'] : 0; if ($text === '') { $this->analytics->record('paste_validation_failed', null, ['reason' => 'empty_text', 'source' => 'api']); $this->jsonResponse(['success' => false, 'message' => 'Text cannot be empty.'], 422); } $id = generateId(); $enc = encryptText($text, $this->config['app']['master_key']); $password_hash = $password !== '' ? password_hash($password, PASSWORD_DEFAULT) : null; $expire_time = $expire > 0 ? time() + $expire : null; $saved = $this->paste->save($id, $enc['cipher'], $enc['iv'], $expire_time, $password_hash); if (!$saved) { $this->logger->error('API failed to save paste', ['id' => $id]); $this->analytics->record('paste_save_failed', $id, ['source' => 'api']); $this->jsonResponse(['success' => false, 'message' => 'Failed to save paste. Please try again.'], 500); } $charCount = mb_strlen($text, 'UTF-8'); $this->logger->info('Paste created via API', [ 'id' => $id, 'char_count' => $charCount, 'has_password' => $password_hash !== null, 'expires_in' => $expire > 0 ? "{$expire}s" : 'never', ]); $this->analytics->record('paste_created', $id, [ 'char_count' => $charCount, 'has_password' => $password_hash !== null, 'expire_secs' => $expire > 0 ? $expire : null, 'storage' => $expire_time !== null ? 'redis' : 'mysql', 'source' => 'api', ]); $base = rtrim( $this->config['app']['base_url'] ?: ('http' . (isset($_SERVER['HTTPS']) ? 's' : '') . '://' . $_SERVER['HTTP_HOST']), '/' ); $this->jsonResponse([ 'success' => true, 'id' => $id, 'url' => $base . '/view/' . $id, 'has_password' => $password_hash !== null, 'expires_at' => $expire_time, ], 201); } private function get(string $id): void { $id = preg_replace('/[^a-f0-9]/i', '', $id); $data = $id !== '' ? $this->paste->get($id) : false; if (!$data) { $this->analytics->record('paste_not_found', $id ?: null, ['source' => 'api']); $this->jsonResponse(['success' => false, 'message' => 'Paste not found.'], 404); } if ($data['expire_time'] !== null && time() > (int)$data['expire_time']) { $this->analytics->record('paste_expired', $id, ['expired_at' => $data['expire_time'], 'source' => 'api']); $this->jsonResponse(['success' => false, 'message' => 'This paste has expired.'], 410); } if ($data['password_hash']) { $this->analytics->record('paste_password_prompt', $id, ['source' => 'api']); $this->jsonResponse([ 'success' => true, 'needs_password' => true, 'expires_at' => $data['expire_time'] !== null ? (int)$data['expire_time'] : null, ]); } $decrypted = decryptText($data['encrypted_text'], $data['iv'], $this->config['app']['master_key']); if ($decrypted === false) { $this->logger->error('API decryption failed for paste', ['id' => $id]); $this->analytics->record('paste_decrypt_error', $id, ['source' => 'api']); $this->jsonResponse(['success' => false, 'message' => 'Decryption failed. The paste may be corrupted.'], 500); } $this->analytics->record('paste_viewed', $id, [ 'char_count' => mb_strlen($decrypted, 'UTF-8'), 'has_expiry' => $data['expire_time'] !== null, 'source' => 'api', ]); $this->jsonResponse([ 'success' => true, 'needs_password' => false, 'text' => $decrypted, 'expires_at' => $data['expire_time'] !== null ? (int)$data['expire_time'] : null, ]); } private function unlock(string $id): void { $id = preg_replace('/[^a-f0-9]/i', '', $id); $body = $this->readBody(); $data = $id !== '' ? $this->paste->get($id) : false; if (!$data) { $this->jsonResponse(['success' => false, 'message' => 'Paste not found.'], 404); } if ($data['expire_time'] !== null && time() > (int)$data['expire_time']) { $this->jsonResponse(['success' => false, 'message' => 'This paste has expired.'], 410); } if (!$data['password_hash']) { $this->jsonResponse(['success' => false, 'message' => 'This paste is not password protected.'], 400); } $submitted = (string)($body['password'] ?? ''); if (!password_verify($submitted, $data['password_hash'])) { $this->analytics->record('paste_failed_password', $id, ['source' => 'api']); $this->jsonResponse(['success' => false, 'message' => 'Incorrect password.'], 401); } $decrypted = decryptText($data['encrypted_text'], $data['iv'], $this->config['app']['master_key']); if ($decrypted === false) { $this->logger->error('API decryption failed for paste', ['id' => $id]); $this->analytics->record('paste_decrypt_error', $id, ['source' => 'api']); $this->jsonResponse(['success' => false, 'message' => 'Decryption failed. The paste may be corrupted.'], 500); } $this->analytics->record('paste_unlocked', $id, ['source' => 'api']); $this->jsonResponse([ 'success' => true, 'text' => $decrypted, 'expires_at' => $data['expire_time'] !== null ? (int)$data['expire_time'] : null, ]); } private function readBody(): array { $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; if (stripos($contentType, 'application/json') !== false) { $raw = file_get_contents('php://input'); $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : []; } return $_POST; } private function sendCorsHeaders(): void { $allowedOrigin = $this->config['app']['cors_origin'] ?? '*'; header("Access-Control-Allow-Origin: {$allowedOrigin}"); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); header('Access-Control-Max-Age: 86400'); } private function jsonResponse(array $data, int $status = 200): void { http_response_code($status); header('Content-Type: application/json'); echo json_encode($data); exit; } }