- Política de privacidade reescrita: tabela de dados/finalidades/fundamento (inclui os pedidos de aula, inscrição e eventos com IP), prazos concretos, subcontratantes, Google Maps e reviews, cookies. Termos e condições novos (/home/termos), no rodapé e no sitemap. Campos [ENTRE PARÊNTESES RETOS] e revisão por jurista pendentes antes de publicar no domínio real. - Google Maps só carrega ao carregar em «Mostrar mapa» (map_click_to_load); as fotos dos autores das reviews são guardadas pelo servidor em uploads/reviews/ e servidas localmente (review_avatar_url). - Retenção que a política declara, aplicada pelo cron (04:00): leads com mais de 12 meses → lixeira; blocked_submissions e outbox de segurança apagados aos 90 dias (trash_retention_sweep, auditado). - Backoffice e login com tema claro/escuro/automático: data-bs-theme aplicado no <head> antes do CSS (sem flash), escolha em localStorage, segue o sistema em tempo real, color-scheme e theme-color para os dois. Cores próprias passaram a variáveis do Bootstrap. lang="pt-PT". Testado numa cópia da BD de produção: 72/72 verificações. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62 lines
3.4 KiB
PHP
62 lines
3.4 KiB
PHP
<?php
|
|
// ─── Terceiros só depois de o visitante pedir ───────────────────────────────
|
|
// RGPD: nada da Google carrega sem o visitante o pedir. O iframe do Google Maps
|
|
// punha cookies e enviava o IP de cada visita; as fotos dos autores das
|
|
// reviews vinham de lh3.googleusercontent.com. Assim:
|
|
// - map_click_to_load(): o mapa fica num <template> e só entra na página
|
|
// quando se carrega no botão; o link «Abrir no Google Maps» funciona sem ele;
|
|
// - review_avatar_url(): o servidor guarda a foto uma vez em uploads/reviews/
|
|
// e o browser pede-a ao próprio site.
|
|
|
|
function map_click_to_load(string $iframeHtml, string $mapsLink = ''): string {
|
|
if (trim($iframeHtml) === '') return '';
|
|
static $n = 0;
|
|
$id = 'map-consent-' . (++$n);
|
|
$esc = fn(string $s): string => htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
|
$link = $mapsLink !== ''
|
|
? '<a href="' . $esc($mapsLink) . '" target="_blank" rel="noopener" class="map-consent-link">Abrir no Google Maps</a>'
|
|
: '';
|
|
return '<div class="map-consent" id="' . $id . '" style="position:absolute;inset:0;display:flex;flex-direction:column;'
|
|
. 'align-items:center;justify-content:center;gap:.6rem;padding:1rem;text-align:center;color:#f5f5f5;">'
|
|
. '<i class="fas fa-map-marker-alt" aria-hidden="true" style="font-size:1.8rem;color:#C2D500"></i>'
|
|
. '<button type="button" class="site-btn" style="min-height:44px" '
|
|
. 'onclick="var w=document.getElementById(\'' . $id . '\');var t=w.nextElementSibling;'
|
|
. 'w.insertAdjacentHTML(\'afterend\',t.innerHTML);w.remove();">Mostrar mapa</button>'
|
|
. '<small style="max-width:32em;opacity:.8">O mapa é do Google Maps: ao abri-lo, a Google recebe o seu endereço IP e pode guardar cookies.</small>'
|
|
. $link
|
|
. '</div><template>' . $iframeHtml . '</template>';
|
|
}
|
|
|
|
function review_avatar_url(array $r): string {
|
|
$src = (string) ($r['author_photo'] ?? '');
|
|
if ($src === '' || !preg_match('#^https://[a-z0-9.-]*googleusercontent\.com/#i', $src)) return '';
|
|
|
|
$base = substr(sha1($src), 0, 24);
|
|
$dir = dirname(__DIR__) . '/uploads/reviews/';
|
|
foreach (['jpg', 'png', 'webp'] as $ext) {
|
|
if (is_file($dir . $base . '.' . $ext)) return base_url('uploads/reviews/' . $base . '.' . $ext);
|
|
}
|
|
// Uma falha recente não se repete a cada visita (cada tentativa custa até 4 s).
|
|
$fail = $dir . '.' . $base . '.fail';
|
|
if (is_file($fail) && filemtime($fail) > time() - 86400) return '';
|
|
if (!function_exists('curl_init')) return '';
|
|
|
|
$ch = curl_init($src);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 2,
|
|
CURLOPT_TIMEOUT => 4, CURLOPT_CONNECTTIMEOUT => 3,
|
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
|
]);
|
|
$bin = curl_exec($ch);
|
|
curl_close($ch);
|
|
|
|
$info = (is_string($bin) && strlen($bin) > 0 && strlen($bin) < 512 * 1024) ? @getimagesizefromstring($bin) : false;
|
|
$ext = $info ? (['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'][$info['mime']] ?? null) : null;
|
|
if (!is_dir($dir)) @mkdir($dir, 0775, true);
|
|
if ($ext === null || !@file_put_contents($dir . $base . '.' . $ext, $bin)) {
|
|
@touch($fail);
|
|
return '';
|
|
}
|
|
return base_url('uploads/reviews/' . $base . '.' . $ext);
|
|
}
|