/ * * Quem chama este arquivo é o SERVIDOR do FiveM (não o jogador), sempre com o token * secreto abaixo. Sem o token certo, nada é listado, enviado ou apagado. * * LEIA o LEIA-ME.md antes de usar. */ // ====================== CONFIGURAÇÃO (edite aqui) ====================== // TROQUE por uma senha grande e aleatória. TEM QUE ser IGUAL a Config.ImageManager.token // no config/config.lua do et_admin. const UPLOAD_TOKEN = 'ETSTORELUA202310'; // Pasta (dentro do htdocs) onde as imagens ficam. Não coloque "/" no início nem no fim. const IMAGES_DIR = 'img'; // (Opcional, recomendado) IPs que podem chamar este arquivo. Coloque o IP do seu servidor // FiveM. Deixe vazio [] pra aceitar qualquer IP (só o token protege). const ALLOWED_IPS = []; // Tamanho máximo por imagem (em bytes). Padrão: 5 MB. const MAX_BYTES = 5 * 1024 * 1024; // Extensões aceitas. const ALLOWED_EXT = ['png', 'jpg', 'jpeg', 'webp', 'gif']; // ====================== NÃO PRECISA EDITAR DAQUI PRA BAIXO ====================== header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); function out(int $status, array $payload): void { http_response_code($status); echo json_encode($payload, JSON_UNESCAPED_UNICODE); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { out(405, ['ok' => false, 'error' => 'Use POST.']); } if (UPLOAD_TOKEN === 'TROQUE_ESSE_TOKEN' || strlen(UPLOAD_TOKEN) < 16) { out(500, ['ok' => false, 'error' => 'Token não configurado no upload.php (troque UPLOAD_TOKEN por algo com 16+ caracteres).']); } if (count(ALLOWED_IPS) > 0 && !in_array($_SERVER['REMOTE_ADDR'] ?? '', ALLOWED_IPS, true)) { out(403, ['ok' => false, 'error' => 'IP não autorizado.']); } $sent = $_SERVER['HTTP_X_UPLOAD_TOKEN'] ?? ''; if (!hash_equals(UPLOAD_TOKEN, $sent)) { out(403, ['ok' => false, 'error' => 'Token inválido.']); } $body = json_decode(file_get_contents('php://input'), true); if (!is_array($body)) { out(400, ['ok' => false, 'error' => 'Corpo inválido.']); } $root = realpath(__DIR__); $imagesRoot = $root . DIRECTORY_SEPARATOR . IMAGES_DIR; if (!is_dir($imagesRoot) && !@mkdir($imagesRoot, 0755, true)) { out(500, ['ok' => false, 'error' => 'Não consegui criar a pasta ' . IMAGES_DIR . ' (permissão de escrita?).']); } $imagesRoot = realpath($imagesRoot); // "carros/suv" -> segmentos seguros. Bloqueia "..", caracteres estranhos e caminho absoluto. function safe_folder(string $folder): ?string { $folder = trim(str_replace('\\', '/', $folder), '/'); if ($folder === '') return ''; $parts = explode('/', $folder); foreach ($parts as $p) { if ($p === '' || $p === '.' || $p === '..' || !preg_match('/^[A-Za-z0-9_\- ]{1,64}$/', $p)) { return null; } } return implode('/', $parts); } function safe_filename(string $name): ?string { $name = basename(str_replace('\\', '/', $name)); if (!preg_match('/^[A-Za-z0-9_\-. ]{1,120}$/', $name) || $name[0] === '.') return null; $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); return in_array($ext, ALLOWED_EXT, true) ? $name : null; } $action = (string)($body['action'] ?? ''); $folder = safe_folder((string)($body['folder'] ?? '')); if ($folder === null) { out(400, ['ok' => false, 'error' => 'Nome de pasta inválido (use letras, números, espaço, _ e -).']); } $dir = $folder === '' ? $imagesRoot : $imagesRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folder); // Garante que o caminho final continua DENTRO da pasta de imagens (defesa extra). function inside_root(string $path, string $imagesRoot): bool { $real = realpath($path); return $real !== false && strncmp($real, $imagesRoot, strlen($imagesRoot)) === 0; } switch ($action) { case 'ping': out(200, ['ok' => true, 'imagesDir' => IMAGES_DIR, 'maxBytes' => MAX_BYTES]); case 'list': if (!is_dir($dir) || !inside_root($dir, $imagesRoot)) { out(200, ['ok' => true, 'folders' => [], 'files' => []]); } $folders = []; $files = []; foreach (scandir($dir) ?: [] as $entry) { if ($entry === '.' || $entry === '..') continue; $full = $dir . DIRECTORY_SEPARATOR . $entry; if (is_dir($full)) { $folders[] = $entry; } elseif (is_file($full) && in_array(strtolower(pathinfo($entry, PATHINFO_EXTENSION)), ALLOWED_EXT, true)) { $files[] = ['name' => $entry, 'size' => filesize($full), 'mtime' => filemtime($full)]; } } sort($folders, SORT_NATURAL | SORT_FLAG_CASE); usort($files, fn($a, $b) => $b['mtime'] <=> $a['mtime']); out(200, ['ok' => true, 'folders' => $folders, 'files' => $files]); case 'mkdir': $name = safe_folder((string)($body['name'] ?? '')); if ($name === null || $name === '' || strpos($name, '/') !== false) { out(400, ['ok' => false, 'error' => 'Nome de pasta inválido.']); } if (!is_dir($dir) || !inside_root($dir, $imagesRoot)) { out(404, ['ok' => false, 'error' => 'Pasta pai não existe.']); } $new = $dir . DIRECTORY_SEPARATOR . $name; if (is_dir($new)) out(409, ['ok' => false, 'error' => 'Essa pasta já existe.']); if (!@mkdir($new, 0755)) out(500, ['ok' => false, 'error' => 'Não consegui criar a pasta (permissão?).']); out(200, ['ok' => true]); case 'rmdir': // Só apaga pasta VAZIA e nunca a raiz de imagens. if ($folder === '' || !is_dir($dir) || !inside_root($dir, $imagesRoot)) { out(400, ['ok' => false, 'error' => 'Pasta inválida.']); } $left = array_diff(scandir($dir) ?: [], ['.', '..']); if (count($left) > 0) out(409, ['ok' => false, 'error' => 'A pasta não está vazia.']); if (!@rmdir($dir)) out(500, ['ok' => false, 'error' => 'Não consegui apagar a pasta.']); out(200, ['ok' => true]); case 'upload': $name = safe_filename((string)($body['name'] ?? '')); if ($name === null) { out(400, ['ok' => false, 'error' => 'Nome de arquivo inválido. Extensões aceitas: ' . implode(', ', ALLOWED_EXT) . '.']); } $raw = base64_decode((string)($body['data'] ?? ''), true); if ($raw === false || $raw === '') out(400, ['ok' => false, 'error' => 'Dados da imagem inválidos.']); if (strlen($raw) > MAX_BYTES) out(413, ['ok' => false, 'error' => 'Imagem maior que o limite do servidor.']); // Confere o CONTEÚDO (não só a extensão) -- impede subir .php disfarçado de .png. $info = @getimagesizefromstring($raw); $okTypes = [IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_WEBP]; if ($info === false || !in_array($info[2], $okTypes, true)) { out(400, ['ok' => false, 'error' => 'O arquivo não é uma imagem válida.']); } if (!is_dir($dir) && !@mkdir($dir, 0755, true)) { out(500, ['ok' => false, 'error' => 'Não consegui criar a pasta de destino.']); } if (!inside_root($dir, $imagesRoot)) out(400, ['ok' => false, 'error' => 'Pasta inválida.']); $target = $dir . DIRECTORY_SEPARATOR . $name; if (file_exists($target) && empty($body['overwrite'])) { out(409, ['ok' => false, 'exists' => true, 'error' => 'Já existe uma imagem com esse nome.']); } if (file_put_contents($target, $raw, LOCK_EX) === false) { out(500, ['ok' => false, 'error' => 'Falha ao gravar (permissão de escrita na pasta?).']); } out(200, ['ok' => true, 'name' => $name, 'size' => strlen($raw)]); case 'delete': $name = safe_filename((string)($body['name'] ?? '')); if ($name === null) out(400, ['ok' => false, 'error' => 'Nome de arquivo inválido.']); $target = $dir . DIRECTORY_SEPARATOR . $name; if (!is_file($target) || !inside_root($target, $imagesRoot)) { out(404, ['ok' => false, 'error' => 'Arquivo não encontrado.']); } if (!@unlink($target)) out(500, ['ok' => false, 'error' => 'Não consegui apagar o arquivo.']); out(200, ['ok' => true]); default: out(400, ['ok' => false, 'error' => 'Ação desconhecida.']); }