feat(rgpd): pedido de acesso/apagamento de dados + processo escrito (item 9.5)
- migration_20260925_rgpd_requests.sql: tabela rgpd_requests (email, tipo, estado, consentimento). - config/functions_rgpd.php: rgpd_find() (acesso, art.º 15) e rgpd_erase() (apagamento por anonimização, art.º 17) sobre membership_leads/trial_leads/ event_leads, + CRUD dos pedidos. - Política de Privacidade: formulário público (POST + CSRF + anti-spam) para pedir acesso ou apagamento, e o processo escrito (resposta em 30 dias). - Backoffice: página "Pedidos RGPD" (nav + index) para exportar os dados de um email (CSV, audit) ou anonimizá-los (POST + CSRF, audit). O email está desligado, por isso os pedidos ficam guardados (como as leads), não dependem de SMTP. Texto legal com placeholders — rever com jurista antes do go-live (como já constava na Política). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
116817e2de
commit
3f2c505c17
6 files changed
+248
-1
No files matched your search
@@ -43,6 +43,7 @@ require_once __DIR__ . '/functions_reviews.php'; // reviews Google (Places
|
|||||||
require_once __DIR__ . '/functions_trials.php'; // pedidos de aula experimental
|
require_once __DIR__ . '/functions_trials.php'; // pedidos de aula experimental
|
||||||
require_once __DIR__ . '/functions_privacy.php'; // mapa e fotos da Google só a pedido / servidos localmente
|
require_once __DIR__ . '/functions_privacy.php'; // mapa e fotos da Google só a pedido / servidos localmente
|
||||||
require_once __DIR__ . '/functions_forms.php'; // anti-spam + bloqueio de IP dos formulários públicos
|
require_once __DIR__ . '/functions_forms.php'; // anti-spam + bloqueio de IP dos formulários públicos
|
||||||
|
require_once __DIR__ . '/functions_rgpd.php'; // pedidos de acesso/apagamento (art.º 15/17)
|
||||||
|
|
||||||
// ─── DB-backed settings → legacy constants ───────────────────────────────────
|
// ─── DB-backed settings → legacy constants ───────────────────────────────────
|
||||||
// Resolved once from the settings cache so call sites read plain constants.
|
// Resolved once from the settings cache so call sites read plain constants.
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
// RGPD — pedidos de acesso (art.º 15.º) e apagamento (art.º 17.º).
|
||||||
|
//
|
||||||
|
// Os formulários públicos guardam PII em membership_leads, trial_leads e
|
||||||
|
// event_leads, sempre identificável pelo email. O "apagamento" aqui é
|
||||||
|
// anonimização: a linha fica (estatística/audit), mas o nome/email/telefone
|
||||||
|
// passam a placeholders — o dado pessoal deixa de existir sem quebrar nada.
|
||||||
|
|
||||||
|
function rgpd_pii_tables(): array {
|
||||||
|
return [
|
||||||
|
'membership_leads' => 'Pedidos de inscrição',
|
||||||
|
'trial_leads' => 'Aulas experimentais',
|
||||||
|
'event_leads' => 'Inscrições em eventos',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devolve os dados pessoais associados a um email, organizados por tabela.
|
||||||
|
function rgpd_find(PDO $pdo, string $email): array {
|
||||||
|
$out = [];
|
||||||
|
foreach (rgpd_pii_tables() as $table => $label) {
|
||||||
|
$st = $pdo->prepare(
|
||||||
|
"SELECT name, email, phone, message, created_at FROM `$table` WHERE email = ? ORDER BY created_at DESC"
|
||||||
|
);
|
||||||
|
$st->execute([$email]);
|
||||||
|
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if ($rows) $out[$table] = $rows;
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anonimiza os dados pessoais de um email em todas as tabelas públicas.
|
||||||
|
// Devolve o número de registos alterados.
|
||||||
|
function rgpd_erase(PDO $pdo, string $email): int {
|
||||||
|
$n = 0;
|
||||||
|
foreach (array_keys(rgpd_pii_tables()) as $table) {
|
||||||
|
$st = $pdo->prepare(
|
||||||
|
"UPDATE `$table` SET name = 'Apagado (RGPD)', email = '', phone = '', message = '' WHERE email = ?"
|
||||||
|
);
|
||||||
|
$st->execute([$email]);
|
||||||
|
$n += $st->rowCount();
|
||||||
|
}
|
||||||
|
return $n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_rgpd_request_create(PDO $pdo, array $d): int {
|
||||||
|
$st = $pdo->prepare(
|
||||||
|
"INSERT INTO rgpd_requests (gym_id, email, request_type, message, consent) VALUES (?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
$st->execute([
|
||||||
|
(int) ($d['gym_id'] ?? 0),
|
||||||
|
(string) $d['email'],
|
||||||
|
(string) $d['request_type'],
|
||||||
|
(string) ($d['message'] ?? ''),
|
||||||
|
(int) ($d['consent'] ?? 0),
|
||||||
|
]);
|
||||||
|
return (int) $pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_rgpd_request_get(PDO $pdo, int $id): ?array {
|
||||||
|
$st = $pdo->prepare("SELECT * FROM rgpd_requests WHERE id = ? LIMIT 1");
|
||||||
|
$st->execute([$id]);
|
||||||
|
return $st->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_rgpd_requests_list(PDO $pdo): array {
|
||||||
|
return $pdo->query(
|
||||||
|
"SELECT * FROM rgpd_requests WHERE deleted_at IS NULL ORDER BY created_at DESC"
|
||||||
|
)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_rgpd_request_mark(PDO $pdo, int $id, string $status, int $by): void {
|
||||||
|
$st = $pdo->prepare("UPDATE rgpd_requests SET status = ?, processed_at = NOW(), processed_by = ? WHERE id = ?");
|
||||||
|
$st->execute([$status, $by, $id]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- RGPD (art.º 15.º/17.º): pedidos de acesso e de apagamento de dados pessoais.
|
||||||
|
-- O visitante submete um pedido na Política de Privacidade; fica aqui (o email
|
||||||
|
-- está desligado, por isso o pedido é guardado, como as leads). O backoffice
|
||||||
|
-- processa: acesso = exportar os dados; apagamento = anonimizar.
|
||||||
|
CREATE TABLE IF NOT EXISTS `rgpd_requests` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`gym_id` INT NOT NULL DEFAULT 0,
|
||||||
|
`email` VARCHAR(190) NOT NULL,
|
||||||
|
`request_type` ENUM('acesso','apagamento') NOT NULL,
|
||||||
|
`message` TEXT NULL,
|
||||||
|
`status` ENUM('novo','concluido') NOT NULL DEFAULT 'novo',
|
||||||
|
`consent` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`processed_at` DATETIME NULL DEFAULT NULL,
|
||||||
|
`processed_by` INT UNSIGNED NULL DEFAULT NULL,
|
||||||
|
`deleted_at` DATETIME NULL DEFAULT NULL,
|
||||||
|
`deleted_by` INT UNSIGNED NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_rgpd_requests_status` (`status`, `created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -173,6 +173,10 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
|||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/trash/') !== false) ? ' active' : '' ?>"
|
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/trash/') !== false) ? ' active' : '' ?>"
|
||||||
href="<?= base_url('views/admin/trash/index.php') ?>"><i class="bi bi-trash3-fill"></i>Lixeira</a>
|
href="<?= base_url('views/admin/trash/index.php') ?>"><i class="bi bi-trash3-fill"></i>Lixeira</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/rgpd/') !== false) ? ' active' : '' ?>"
|
||||||
|
href="<?= base_url('views/admin/rgpd/index.php') ?>"><i class="bi bi-shield-lock-fill"></i>Pedidos RGPD</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|||||||
+62
-1
@@ -11,6 +11,36 @@ require __DIR__ . '/_open.php';
|
|||||||
$pp_name = htmlspecialchars($info['name'] ?? 'FitMe 3070');
|
$pp_name = htmlspecialchars($info['name'] ?? 'FitMe 3070');
|
||||||
$pp_email = htmlspecialchars($info['email'] ?? '');
|
$pp_email = htmlspecialchars($info['email'] ?? '');
|
||||||
$pp_addr = htmlspecialchars($info['adress'] ?? '');
|
$pp_addr = htmlspecialchars($info['adress'] ?? '');
|
||||||
|
|
||||||
|
// Pedido RGPD (acesso/apagamento). POST + CSRF + anti-spam; fica guardado
|
||||||
|
// (o email está desligado, não pode depender de SMTP) e é processado no
|
||||||
|
// backoffice (Sistema → Pedidos RGPD).
|
||||||
|
$rgpd_status = '';
|
||||||
|
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||||
|
csrf_check();
|
||||||
|
$rgpd_email = trim((string) ($_POST['rgpd_email'] ?? ''));
|
||||||
|
$rgpd_type = (string) ($_POST['rgpd_type'] ?? '') === 'apagamento' ? 'apagamento' : 'acesso';
|
||||||
|
$rgpd_msg = trim((string) ($_POST['rgpd_message'] ?? ''));
|
||||||
|
$rgpd_consent = !empty($_POST['rgpd_consent']);
|
||||||
|
if (!filter_var($rgpd_email, FILTER_VALIDATE_EMAIL) || !$rgpd_consent) {
|
||||||
|
$rgpd_status = 'erro';
|
||||||
|
} elseif (public_form_screen($pdo, 'rgpd', 'rgpd_requests', $rgpd_email, $rgpd_msg) !== null) {
|
||||||
|
$rgpd_status = 'ok'; // anti-spam: mesma resposta de sucesso
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
db_rgpd_request_create($pdo, [
|
||||||
|
'gym_id' => (int) $gym['id'],
|
||||||
|
'email' => $rgpd_email,
|
||||||
|
'request_type' => $rgpd_type,
|
||||||
|
'message' => $rgpd_msg,
|
||||||
|
'consent' => 1,
|
||||||
|
]);
|
||||||
|
$rgpd_status = 'ok';
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$rgpd_status = 'erro';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<section class="privacy-section" style="padding: 140px 0 60px;">
|
<section class="privacy-section" style="padding: 140px 0 60px;">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -59,10 +89,41 @@ $pp_addr = htmlspecialchars($info['adress'] ?? '');
|
|||||||
<p>O mapa da secção de contactos é do <b>Google Maps</b> e só é carregado se carregares em «Mostrar mapa»; nesse momento a Google recebe o teu endereço IP e pode guardar cookies, segundo a <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">política da Google</a>. As avaliações apresentadas vêm do perfil público do ginásio no Google; as fotos dos autores são guardadas neste site para o teu navegador não ter de as pedir à Google.</p>
|
<p>O mapa da secção de contactos é do <b>Google Maps</b> e só é carregado se carregares em «Mostrar mapa»; nesse momento a Google recebe o teu endereço IP e pode guardar cookies, segundo a <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">política da Google</a>. As avaliações apresentadas vêm do perfil público do ginásio no Google; as fotos dos autores são guardadas neste site para o teu navegador não ter de as pedir à Google.</p>
|
||||||
|
|
||||||
<h2 class="h4 mt-4">5. Os teus direitos</h2>
|
<h2 class="h4 mt-4">5. Os teus direitos</h2>
|
||||||
<p>Tens direito de acesso, retificação, apagamento, limitação, portabilidade e oposição, e de retirar o consentimento a qualquer momento (sem afetar o tratamento feito até aí). Basta escreveres para <a href="mailto:<?= $pp_email ?>"><?= $pp_email !== '' ? $pp_email : '[EMAIL]' ?></a>; respondemos no prazo de um mês.
|
<p>Tens direito de acesso, retificação, apagamento, limitação, portabilidade e oposição, e de retirar o consentimento a qualquer momento (sem afetar o tratamento feito até aí). Podes pedir a exportação dos teus dados ou o seu apagamento com o formulário abaixo; respondemos no prazo de um mês.
|
||||||
Se tiveres conta na área de cliente, podes corrigir os teus dados e preferências no teu perfil.
|
Se tiveres conta na área de cliente, podes corrigir os teus dados e preferências no teu perfil.
|
||||||
Tens ainda o direito de apresentar reclamação à Comissão Nacional de Proteção de Dados (<a href="https://www.cnpd.pt" target="_blank" rel="noopener">www.cnpd.pt</a>).</p>
|
Tens ainda o direito de apresentar reclamação à Comissão Nacional de Proteção de Dados (<a href="https://www.cnpd.pt" target="_blank" rel="noopener">www.cnpd.pt</a>).</p>
|
||||||
|
|
||||||
|
<h3 class="h6 mt-4 mb-2">Pedir acesso ou apagamento</h3>
|
||||||
|
<?php if ($rgpd_status === 'ok'): ?>
|
||||||
|
<div class="alert alert-success py-2">Pedido recebido. Vamos responder no prazo de um mês.</div>
|
||||||
|
<?php elseif ($rgpd_status === 'erro'): ?>
|
||||||
|
<div class="alert alert-danger py-2">Não foi possível enviar o pedido. Confirma o e-mail e tenta de novo.</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="POST" action="" class="mt-2 mb-3" style="max-width:520px;">
|
||||||
|
<?= public_form_trap('rgpd') /* honeypot + carimbo + prova de JS */ ?>
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label" for="rgpd_email">O teu e-mail</label>
|
||||||
|
<input type="email" class="form-control" id="rgpd_email" name="rgpd_email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label" for="rgpd_type">O que pretendes?</label>
|
||||||
|
<select class="form-select" id="rgpd_type" name="rgpd_type">
|
||||||
|
<option value="acesso">Aceder aos meus dados (exportação)</option>
|
||||||
|
<option value="apagamento">Apagar os meus dados</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label" for="rgpd_message">Mensagem (opcional)</label>
|
||||||
|
<textarea class="form-control" id="rgpd_message" name="rgpd_message" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="rgpd_consent" name="rgpd_consent" value="1" required>
|
||||||
|
<label class="form-check-label" for="rgpd_consent">Consinto no tratamento dos meus dados para responder a este pedido.</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Enviar pedido</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<h2 class="h4 mt-4">6. Cookies</h2>
|
<h2 class="h4 mt-4">6. Cookies</h2>
|
||||||
<p>Este site usa apenas um cookie técnico de sessão (<code>FIT_SESS</code>), necessário para os formulários e para a área reservada, que expira ao fechar o navegador. Não usamos cookies de publicidade nem de estatística. O Google Maps só coloca cookies se escolheres ver o mapa.</p>
|
<p>Este site usa apenas um cookie técnico de sessão (<code>FIT_SESS</code>), necessário para os formulários e para a área reservada, que expira ao fechar o navegador. Não usamos cookies de publicidade nem de estatística. O Google Maps só coloca cookies se escolheres ver o mapa.</p>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../../../config/db.php';
|
||||||
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
|
$user = require_admin();
|
||||||
|
|
||||||
|
// Apagar dados (POST + CSRF) — anonimiza e marca o pedido como concluído.
|
||||||
|
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||||
|
csrf_check();
|
||||||
|
$id = (int) ($_POST['id'] ?? 0);
|
||||||
|
if (isset($_POST['erase']) && $id) {
|
||||||
|
$req = db_rgpd_request_get($pdo, $id);
|
||||||
|
if ($req) {
|
||||||
|
$n = rgpd_erase($pdo, $req['email']);
|
||||||
|
db_rgpd_request_mark($pdo, $id, 'concluido', (int) $user['id']);
|
||||||
|
audit('rgpd.erase', [
|
||||||
|
'target_type' => 'rgpd_request', 'target_id' => $id,
|
||||||
|
'summary' => 'Apagamento RGPD de ' . audit_mask_email($req['email']) . ' — ' . $n . ' registos anonimizados',
|
||||||
|
]);
|
||||||
|
session()->setFlashdata('success', 'Dados de ' . htmlspecialchars($req['email']) . ' anonimizados (' . $n . ' registos).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header('Location: ' . base_url('views/admin/rgpd/index.php'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exportar dados (acesso, art.º 15.º) — CSV com tudo o que existe para o email.
|
||||||
|
if (isset($_GET['csv']) && is_numeric($_GET['csv'])) {
|
||||||
|
$req = db_rgpd_request_get($pdo, (int) $_GET['csv']);
|
||||||
|
if ($req) {
|
||||||
|
audit('rgpd.export', [
|
||||||
|
'target_type' => 'rgpd_request', 'target_id' => (int) $_GET['csv'],
|
||||||
|
'summary' => 'Exportação RGPD (acesso) de ' . audit_mask_email($req['email']),
|
||||||
|
]);
|
||||||
|
$rows = [];
|
||||||
|
foreach (rgpd_find($pdo, $req['email']) as $table => $items) {
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$rows[] = [rgpd_pii_tables()[$table] ?? $table, $it['name'], $it['email'], $it['phone'], $it['message'] ?? '', $it['created_at']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
csv_export('rgpd', 'dados-' . substr(md5($req['email']), 0, 8), 'Exportação RGPD (acesso) de ' . audit_mask_email($req['email']), ['Fonte', 'Nome', 'E-mail', 'Telefone', 'Mensagem', 'Data'], $rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$requests = db_rgpd_requests_list($pdo);
|
||||||
|
$page_title = 'Pedidos RGPD';
|
||||||
|
include __DIR__ . '/../../../includes/header.php';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h1 class="h3 mb-0">Pedidos RGPD</h1>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small">Pedidos de acesso (exportação) e apagamento feitos na Política de Privacidade. O apagamento anonimiza o nome/e-mail/telefone nas leads — a linha fica para estatística e audit.</p>
|
||||||
|
|
||||||
|
<div class="card shadow-sm"><div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light"><tr>
|
||||||
|
<th>Data</th><th>E-mail</th><th>Pedido</th><th>Mensagem</th><th>Estado</th><th style="width:230px">Ações</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (!$requests): ?>
|
||||||
|
<tr><td colspan="6" class="text-center text-muted py-4">Sem pedidos RGPD.</td></tr>
|
||||||
|
<?php else: foreach ($requests as $r): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap small"><?= htmlspecialchars(format_date_pt((string) $r['created_at'])) ?></td>
|
||||||
|
<td><?= htmlspecialchars($r['email']) ?></td>
|
||||||
|
<td><span class="badge <?= $r['request_type'] === 'apagamento' ? 'bg-danger' : 'bg-info text-dark' ?>"><?= $r['request_type'] === 'apagamento' ? 'Apagamento' : 'Acesso' ?></span></td>
|
||||||
|
<td class="small text-muted"><?= htmlspecialchars((string) $r['message']) ?></td>
|
||||||
|
<td><span class="badge <?= $r['status'] === 'novo' ? 'bg-warning text-dark' : 'bg-success' ?>"><?= $r['status'] === 'novo' ? 'Novo' : 'Concluído' ?></span></td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="?csv=<?= (int) $r['id'] ?>"><i class="bi bi-download me-1"></i>Exportar</a>
|
||||||
|
<?php if ($r['status'] === 'novo'): ?>
|
||||||
|
<form method="POST" class="d-inline" onsubmit="return confirm('Anonimizar todos os dados pessoais de <?= htmlspecialchars($r['email'], ENT_QUOTES) ?>? (irreversível)')">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="id" value="<?= (int) $r['id'] ?>">
|
||||||
|
<button class="btn btn-sm btn-outline-danger" name="erase" value="1"><i class="bi bi-eraser me-1"></i>Apagar</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
<?php include __DIR__ . '/../../../includes/footer.php';
|
||||||
Reference in new issue
Block a user