'Algo não funciona', 'alteracao' => 'Pedido de alteração', 'sugestao' => 'Sugestão', 'duvida' => 'Dúvida', ]; } /** * A conta com sessão aberta, seja qual for o bootstrap do site. * * A frota guarda a sessão de três maneiras e nenhuma é conversora das outras: * * 1. `current_user()` — dance, fotografia, design * 2. `$_SESSION['user']` (array) — surf/silver, e os mesmos sem os helpers * carregados (o site público da fotografia) * 3. chaves soltas na sessão — quack: user_id, username, role * * A terceira não traz e-mail, e o balcão do hub exige um para poder responder; * por isso, e só nesse caso, vai-se buscá-lo à tabela `users` — uma vez, com o * resultado na própria sessão, para não custar uma query por página. * * Nunca abre sessão: as páginas públicas que a não têm aberta não a devem * passar a ter por causa de um include no rodapé. */ function platform_feedback_user(): ?array { if (function_exists('current_user')) { $user = current_user(); if (is_array($user) && $user) { return $user; } } if (session_status() !== PHP_SESSION_ACTIVE) { return null; } $user = $_SESSION['user'] ?? null; if (is_array($user) && $user) { return $user; } $id = (int) ($_SESSION['user_id'] ?? 0); if ($id <= 0) { return null; } $cache = $_SESSION['__ptfb_user'] ?? null; if (is_array($cache) && (int) ($cache['id'] ?? 0) === $id) { return $cache; } $user = [ 'id' => $id, 'name' => (string) ($_SESSION['username'] ?? ''), 'email' => '', 'role' => (string) ($_SESSION['role'] ?? ''), ]; // `SELECT *` e não uma lista de colunas: os nomes divergem entre sites // (name / full_name / display_name / username) e uma coluna inexistente // seria um erro de SQL em vez de um campo em falta. Da linha ficam dois // campos; o resto — hash da password incluído — morre nesta função. $pdo = hub_pdo(); if ($pdo instanceof PDO) { try { $st = $pdo->prepare('SELECT * FROM users WHERE id = ? LIMIT 1'); $st->execute([$id]); $row = $st->fetch(PDO::FETCH_ASSOC) ?: []; $user['email'] = trim((string) ($row['email'] ?? '')); $nome = platform_feedback_name($row); if ($nome !== '') { $user['name'] = $nome; } } catch (Throwable $e) { error_log('platform_feedback_user: ' . $e->getMessage()); } } $_SESSION['__ptfb_user'] = $user; return $user; } /** * O nome da pessoa, venha a coluna com o nome que vier. * * name (dance, fotografia), full_name (surf), display_name/username (quack). * A ordem é do mais human-readable para o mais técnico: um ticket assinado * "andre_m" diz menos do que um assinado "André Moreira". */ function platform_feedback_name(array $user): string { foreach (['name', 'full_name', 'display_name', 'nome', 'username'] as $campo) { $v = trim((string) ($user[$campo] ?? '')); if ($v !== '') { return $v; } } return ''; } /** Os papéis da conta — `roles` onde existe (dance), `role` nos restantes. */ function platform_feedback_roles(?array $user = null): array { $user ??= platform_feedback_user(); if (!$user) { return []; } $roles = $user['roles'] ?? []; if (!is_array($roles)) { $roles = []; } if (!$roles && !empty($user['role'])) { $roles = [(string) $user['role']]; } return array_values(array_unique(array_map('strval', $roles))); } function platform_feedback_is_admin(?array $user = null): bool { return (bool) array_intersect(['admin', 'sysadmin'], platform_feedback_roles($user)); } function platform_feedback_is_sysadmin(?array $user = null): bool { return in_array('sysadmin', platform_feedback_roles($user), true); } /** * O commit em que esta instalação está, lido do .git à mão. * * Sem shell e sem git: `git_local_state()` faz três chamadas ao binário, e num * alojamento cPanel não há binário nenhum. Aqui, ou o ficheiro está lá e * responde-se, ou devolve-se string vazia e o relato vai sem esta linha. */ function platform_feedback_commit(?string $root = null): string { $root = $root ?: dirname(__DIR__, 2); $head = @file_get_contents($root . '/.git/HEAD'); if (!is_string($head)) { return ''; } $head = trim($head); if (str_starts_with($head, 'ref: ')) { $ref = trim(substr($head, 5)); $hash = @file_get_contents($root . '/.git/' . $ref); if (!is_string($hash)) { // Repositório com as refs empacotadas (git gc) — não há ficheiro por ref. $packed = @file_get_contents($root . '/.git/packed-refs'); $hash = (is_string($packed) && preg_match('/^([0-9a-f]{40})\s+' . preg_quote($ref, '/') . '$/m', $packed, $m)) ? $m[1] : ''; } $head = trim((string) $hash); } return preg_match('/^[0-9a-f]{7,40}$/', $head) ? substr($head, 0, 7) : ''; } /** * O rodapé técnico que vai colado à descrição. * * Não é enfeite: sem a página não se sabe onde foi, sem a versão não se sabe * se já está corrigido, e sem o browser metade dos defeitos de interface não * se reproduzem. Pedir isto a quem reporta é garantir que não vem — mas vai * escrito à vista no formulário, porque o que segue junto com um relato tem de * ser sabido por quem o envia. */ function platform_feedback_context(array $user, string $pageUrl): string { $linhas = []; $nome = platform_feedback_name($user); $email = trim((string) ($user['email'] ?? '')); $papel = implode(', ', platform_feedback_roles($user)); $linhas[] = 'Conta: ' . ($nome !== '' ? $nome . ' <' . $email . '>' : $email) . ($papel !== '' ? ' · ' . $papel : ''); if ($pageUrl !== '') { $linhas[] = 'Página: ' . $pageUrl; } $plataforma = hub_app_name(); $host = (string) ($_SERVER['HTTP_HOST'] ?? ''); $linhas[] = 'Plataforma: ' . $plataforma . ($host !== '' ? ' (' . $host . ')' : ''); // O changelog não vem no bootstrap de nenhum site — carrega-se aqui. É um // ficheiro sem efeitos e a versão é o campo que mais depressa diz se um // defeito relatado já está corrigido. if (!function_exists('changelog_latest') && is_file(__DIR__ . '/functions_changelog.php')) { require_once __DIR__ . '/functions_changelog.php'; } $versao = function_exists('changelog_latest') ? (string) (changelog_latest()['version'] ?? '') : ''; $commit = platform_feedback_commit(); $partes = array_filter([$versao, $commit !== '' ? 'commit ' . $commit : '']); if ($partes) { $linhas[] = 'Versão: ' . implode(' · ', $partes); } $ua = trim((string) ($_SERVER['HTTP_USER_AGENT'] ?? '')); if ($ua !== '') { $linhas[] = 'Browser: ' . mb_substr($ua, 0, 300); } $linhas[] = 'Enviado em: ' . date('Y-m-d H:i'); return "\n\n---\n" . implode("\n", $linhas); } /** * Valida, monta e entrega. Devolve o mesmo formato do hub_ticket_create(): * ['ok'=>bool, 'error'=>string, 'ref'=>string, 'track_url'=>string]. * * $in: ['title', 'description', 'category', 'blocking', 'page_url'] */ function platform_feedback_submit(array $in, ?array $user = null): array { $falha = static fn(string $msg): array => ['ok' => false, 'error' => $msg, 'ref' => '', 'track_url' => '']; $user ??= platform_feedback_user(); if (!$user || !platform_feedback_is_admin($user)) { return $falha('Sessão sem permissão para reportar.'); } if (!hub_ticket_ready()) { return $falha('Esta plataforma ainda não está ligada ao balcão de apoio.'); } $email = trim((string) ($user['email'] ?? '')); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return $falha('A tua conta não tem um e-mail válido — sem ele não há como responder ao pedido.'); } $titulo = trim((string) ($in['title'] ?? '')); $texto = trim((string) ($in['description'] ?? '')); // As mesmas medidas do intake do hub, verificadas deste lado para a pessoa // saber o que falta antes de a mensagem atravessar a rede. if (mb_strlen($titulo) < 5) { return $falha('O assunto precisa de pelo menos 5 letras.'); } if (mb_strlen($texto) < 10) { return $falha('Descreve o que aconteceu — pelo menos 10 letras.'); } $titulo = mb_substr($titulo, 0, 190); $texto = mb_substr($texto, 0, 5000); $categorias = platform_feedback_categories(); $categoria = (string) ($in['category'] ?? 'problema'); if (!isset($categorias[$categoria])) { $categoria = 'problema'; } // Só o "está a bloquear" sobe a prioridade. Um formulário com as três à // escolha enche-se de urgentes; uma pergunta fechada não. $prioridade = !empty($in['blocking']) ? 1 : 2; // O URL da página vem do browser, e por isso não se acredita nele: só passa // se for do próprio site. Colado à descrição, seria uma ligação que quem // lê o ticket clicaria. $pageUrl = platform_feedback_safe_page_url((string) ($in['page_url'] ?? '')); return hub_ticket_create([ 'submitter_email' => $email, 'submitter_name' => mb_substr(platform_feedback_name($user), 0, 190), 'title' => $titulo, 'description' => $texto . platform_feedback_context($user, $pageUrl), 'category' => $categoria, 'priority' => $prioridade, ]); } /** * Aceita o URL da página só se apontar para este mesmo site. Um valor de fora * seria uma ligação para terceiros dentro de um ticket que alguém vai abrir. */ function platform_feedback_safe_page_url(string $url): string { $url = trim($url); if ($url === '' || mb_strlen($url) > 500) { return ''; } $host = parse_url($url, PHP_URL_HOST); $mine = (string) ($_SERVER['HTTP_HOST'] ?? ''); if (!is_string($host) || $host === '' || $mine === '' || strcasecmp($host, $mine) !== 0) { return ''; } return in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true) ? $url : ''; }