/** * ============================================================ * NovaPlay — API de sincronização (opcional, PHP + arquivos JSON) * ------------------------------------------------------------ * Serve para manter listas, favoritos e progresso iguais no * celular e no PC, sem banco de dados. Não é obrigatória: o * player funciona 100% local sem ela. * * COMO USAR * 1. Suba a pasta "api" junto com o site. * 2. Garanta permissão de escrita na pasta (chmod 755 ou 775). * 3. No site: "Personalizar" -> "Conta e sincronização" -> * informe "api/" e salve. * * Endpoints: * POST api/ { "action":"register", "user":"nome", "pass":"senha" } * POST api/ { "action":"pull", "user":"nome", "pass":"senha" } * POST api/ { "action":"push", "user":"nome", "pass":"senha", "data":{...} } * * AVISO: é uma solução simples para uso pessoal ou familiar. * Para muitos usuários, prefira um banco de dados real. * ============================================================ */ header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); header('X-Content-Type-Options: nosniff'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'Use POST.']); exit; } // ---- Proteções básicas -------------------------------------------- @ini_set('display_errors', '0'); $DATA_DIR = __DIR__ . '/data'; if (!is_dir($DATA_DIR)) { if (!@mkdir($DATA_DIR, 0755, true)) { echo json_encode(['ok' => false, 'error' => 'Sem permissão para criar a pasta api/data.']); exit; } } // Impede acesso direto aos arquivos de dados via web $guard = $DATA_DIR . '/.htaccess'; if (!file_exists($guard)) { @file_put_contents($guard, "Require all denied\nDeny from all\n"); } // ---- Entrada ------------------------------------------------------- $raw = file_get_contents('php://input'); $in = json_decode($raw, true); if (!is_array($in)) { $in = $_POST; } $action = isset($in['action']) ? strtolower(trim($in['action'])) : ''; $user = isset($in['user']) ? trim($in['user']) : ''; $pass = isset($in['pass']) ? (string)$in['pass'] : ''; // ---- Rate limit simples por IP (anti força bruta) ------------------ $rlFile = $DATA_DIR . '/.rl_' . md5($_SERVER['REMOTE_ADDR'] ?? 'x'); $now = time(); $rl = @json_decode(@file_get_contents($rlFile), true); if (!is_array($rl) || $now - ($rl['start'] ?? 0) > 300) $rl = ['start' => $now, 'n' => 0]; $rl['n']++; @file_put_contents($rlFile, json_encode($rl), LOCK_EX); if ($rl['n'] > 60) { http_response_code(429); echo json_encode(['ok' => false, 'error' => 'Muitas tentativas. Aguarde alguns minutos.']); exit; } // ---- Validação ----------------------------------------------------- if ($user === '' || !preg_match('/^[A-Za-z0-9_.-]{3,32}$/', $user)) { echo json_encode(['ok' => false, 'error' => 'Usuário inválido (3 a 32 caracteres: letras, números, _ . -).']); exit; } if (strlen($pass) < 4) { echo json_encode(['ok' => false, 'error' => 'Senha muito curta (mínimo 4 caracteres).']); exit; } $file = $DATA_DIR . '/' . strtolower($user) . '.json'; function load_user($file) { if (!file_exists($file)) return null; $d = json_decode((string)file_get_contents($file), true); return is_array($d) ? $d : null; } function save_user($file, $data) { $tmp = $file . '.tmp'; $ok = @file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX); if ($ok === false) return false; return @rename($tmp, $file); } function public_view($record) { return [ 'ok' => true, 'user' => $record['user'], 'data' => $record['data'] ?? new stdClass(), 'updatedAt' => $record['updatedAt'] ?? null ]; } // ---- Ações --------------------------------------------------------- switch ($action) { case 'register': if (file_exists($file)) { echo json_encode(['ok' => false, 'error' => 'Este usuário já existe.']); exit; } $record = [ 'user' => $user, 'hash' => password_hash($pass, PASSWORD_DEFAULT), 'data' => new stdClass(), 'createdAt' => gmdate('c'), 'updatedAt' => gmdate('c') ]; if (!save_user($file, $record)) { echo json_encode(['ok' => false, 'error' => 'Não foi possível gravar. Verifique as permissões da pasta api/data.']); exit; } echo json_encode(['ok' => true, 'message' => 'Conta criada.']); break; case 'pull': $record = load_user($file); if (!$record || !password_verify($pass, $record['hash'] ?? '')) { http_response_code(401); echo json_encode(['ok' => false, 'error' => 'Usuário ou senha incorretos.']); exit; } echo json_encode(public_view($record)); break; case 'push': $record = load_user($file); if (!$record || !password_verify($pass, $record['hash'] ?? '')) { http_response_code(401); echo json_encode(['ok' => false, 'error' => 'Usuário ou senha incorretos.']); exit; } if (!isset($in['data']) || !is_array($in['data'])) { echo json_encode(['ok' => false, 'error' => 'Envie "data" como objeto.']); exit; } // Limita o tamanho para proteger o disco $payload = json_encode($in['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (strlen($payload) > 2 * 1024 * 1024) { echo json_encode(['ok' => false, 'error' => 'Backup muito grande (limite de 2 MB).']); exit; } $record['data'] = $in['data']; $record['updatedAt'] = gmdate('c'); if (!save_user($file, $record)) { echo json_encode(['ok' => false, 'error' => 'Não foi possível gravar os dados.']); exit; } echo json_encode(['ok' => true, 'updatedAt' => $record['updatedAt']]); break; default: echo json_encode([ 'ok' => false, 'error' => 'Ação desconhecida. Use register, pull ou push.', 'endpoints' => [ 'POST {action:"register", user, pass}', 'POST {action:"pull", user, pass}', 'POST {action:"push", user, pass, data:{}}' ] ]); }