idk
This commit is contained in:
1 parent
e68d357177
commit
3e3382df57
65 files changed
+1836
-42
No files matched your search
+10
-2
@@ -49,8 +49,16 @@ $pass = env('DB_PASS');
|
||||
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4";
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, $user, $pass);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$pdo = new PDO($dsn, $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
// Real server-side prepares (not driver emulation): a tighter defence
|
||||
// against injection than emulated string-interpolation. Safe here — the
|
||||
// codebase uses positional ? params (no reused :named, no bound LIMIT).
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
// Keep values coming back as strings, as the emulated driver did, so the
|
||||
// type change from native prepares doesn't ripple through existing code.
|
||||
PDO::ATTR_STRINGIFY_FETCHES => true,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
_bootstrap_fail('PDO connect failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ require_once __DIR__ . '/functions_migrations.php';// platform_update.php migrat
|
||||
require_once __DIR__ . '/functions_notify_client.php'; // notify_remote_send()
|
||||
require_once __DIR__ . '/functions_queue.php'; // enqueue_*/flush_message façade → relay
|
||||
require_once __DIR__ . '/functions_csrf.php';
|
||||
require_once __DIR__ . '/functions_html.php'; // allowlist HTML sanitiser (clean_richtext/clean_map_embed)
|
||||
require_once __DIR__ . '/ci_compat.php'; // CI4 view-helper shim (script_tag/link_tag/view/esc/session)
|
||||
require_once __DIR__ . '/functions_site.php'; // public-site data (site_info/offer/team/modalidades/horario)
|
||||
require_once __DIR__ . '/functions_images.php'; // upload resize/compress (gallery/team/modalidades caps)
|
||||
@@ -29,6 +30,7 @@ require_once __DIR__ . '/functions_modalidades.php'; // modalidades CRUD
|
||||
require_once __DIR__ . '/functions_galerias.php'; // galleries + images CRUD
|
||||
require_once __DIR__ . '/functions_work_hours.php'; // work hours CRUD
|
||||
require_once __DIR__ . '/functions_horario.php'; // class schedule CRUD
|
||||
require_once __DIR__ . '/functions_events.php'; // time-limited events + landing pages + leads
|
||||
|
||||
// ─── DB-backed settings → legacy constants ───────────────────────────────────
|
||||
// Resolved once from the settings cache so call sites read plain constants.
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
// ─── Events CRUD helpers ───────────────────────────────────────────────────────
|
||||
// Time-limited events that drive a homepage banner + a branded landing page at
|
||||
// /evento/<slug>. Soft-delete via deleted_at (same shape as modalidades). An
|
||||
// event is publicly visible only while is_active = 1 and NOW() is inside the
|
||||
// [publish_from, publish_until] window. Each landing page can capture leads into
|
||||
// the event_leads table. See database/migration_20260612_events.sql.
|
||||
|
||||
// All columns the create/update statements bind, with safe defaults. Keeps the
|
||||
// view code and the two write paths in sync from one place.
|
||||
function events_defaults(): array {
|
||||
return [
|
||||
'slug' => '', 'title' => '', 'subtitle' => '', 'summary' => '',
|
||||
'description' => '', 'hero_image' => '', 'cor' => '#C2D500',
|
||||
'location' => '', 'event_starts_at' => null, 'event_ends_at' => null,
|
||||
'publish_from' => '', 'publish_until' => '', 'is_active' => 1,
|
||||
'cta_label' => '', 'cta_url' => '', 'show_lead_form' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Reads ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function db_events_list(PDO $pdo, bool $include_deleted = false): array {
|
||||
$sql = "SELECT * FROM events";
|
||||
if (!$include_deleted) $sql .= " WHERE deleted_at IS NULL";
|
||||
$sql .= " ORDER BY publish_from DESC, e_id DESC";
|
||||
return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function db_events_get(PDO $pdo, int $id): ?array {
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE e_id = ? LIMIT 1");
|
||||
$stmt->execute([$id]);
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
|
||||
function db_events_get_by_slug(PDO $pdo, string $slug): ?array {
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE slug = ? AND deleted_at IS NULL LIMIT 1");
|
||||
$stmt->execute([$slug]);
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
|
||||
// Publicly visible events, in show order (soonest-ending first so the most
|
||||
// urgent banner leads). Defensive: returns [] if the table doesn't exist yet.
|
||||
function db_events_active(PDO $pdo): array {
|
||||
try {
|
||||
$stmt = $pdo->query(
|
||||
"SELECT * FROM events
|
||||
WHERE deleted_at IS NULL AND is_active = 1
|
||||
AND NOW() BETWEEN publish_from AND publish_until
|
||||
ORDER BY publish_until ASC, publish_from ASC"
|
||||
);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle label for the backoffice badge.
|
||||
function events_status(array $row): string {
|
||||
if (!empty($row['deleted_at'])) return 'deleted';
|
||||
if ((int) ($row['is_active'] ?? 0) !== 1) return 'inactive';
|
||||
$now = time();
|
||||
$from = strtotime((string) ($row['publish_from'] ?? '')) ?: 0;
|
||||
$until = strtotime((string) ($row['publish_until'] ?? '')) ?: 0;
|
||||
if ($from && $now < $from) return 'scheduled';
|
||||
if ($until && $now > $until) return 'expired';
|
||||
return 'active';
|
||||
}
|
||||
|
||||
// Is the event live for the public right now?
|
||||
function events_is_live(array $row): bool {
|
||||
return events_status($row) === 'active';
|
||||
}
|
||||
|
||||
// ─── Slug ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Build a URL-safe, unique slug from a title. When editing, pass $ignoreId so a
|
||||
// row doesn't collide with itself.
|
||||
function events_make_slug(PDO $pdo, string $title, ?int $ignoreId = null): string {
|
||||
$base = strtolower(trim($title));
|
||||
// Best-effort transliteration of accented PT characters.
|
||||
if (function_exists('iconv')) {
|
||||
$t = @iconv('UTF-8', 'ASCII//TRANSLIT', $base);
|
||||
if ($t !== false) $base = $t;
|
||||
}
|
||||
$base = preg_replace('/[^a-z0-9]+/', '-', strtolower($base));
|
||||
$base = trim((string) $base, '-');
|
||||
if ($base === '') $base = 'evento';
|
||||
$base = substr($base, 0, 150);
|
||||
|
||||
$slug = $base;
|
||||
$i = 2;
|
||||
while (true) {
|
||||
if ($ignoreId !== null) {
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM events WHERE slug = ? AND e_id <> ? LIMIT 1");
|
||||
$stmt->execute([$slug, $ignoreId]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM events WHERE slug = ? LIMIT 1");
|
||||
$stmt->execute([$slug]);
|
||||
}
|
||||
if (!$stmt->fetchColumn()) return $slug;
|
||||
$slug = $base . '-' . $i++;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Writes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Coerce an empty datetime-local string to NULL (the optional event_* columns).
|
||||
function events_nullable_dt($value): ?string {
|
||||
$value = trim((string) $value);
|
||||
return $value === '' ? null : str_replace('T', ' ', $value);
|
||||
}
|
||||
|
||||
function db_events_create(PDO $pdo, array $data): int {
|
||||
$d = array_merge(events_defaults(), $data);
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO events
|
||||
(slug, title, subtitle, summary, description, hero_image, cor, location,
|
||||
event_starts_at, event_ends_at, publish_from, publish_until,
|
||||
is_active, cta_label, cta_url, show_lead_form)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$d['slug'], $d['title'], $d['subtitle'], $d['summary'], $d['description'],
|
||||
$d['hero_image'], $d['cor'], $d['location'],
|
||||
events_nullable_dt($d['event_starts_at']), events_nullable_dt($d['event_ends_at']),
|
||||
str_replace('T', ' ', (string) $d['publish_from']),
|
||||
str_replace('T', ' ', (string) $d['publish_until']),
|
||||
(int) $d['is_active'], $d['cta_label'], $d['cta_url'], (int) $d['show_lead_form'],
|
||||
]);
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function db_events_update(PDO $pdo, int $id, array $data): bool {
|
||||
$d = array_merge(events_defaults(), $data);
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE events SET
|
||||
slug = ?, title = ?, subtitle = ?, summary = ?, description = ?,
|
||||
hero_image = ?, cor = ?, location = ?,
|
||||
event_starts_at = ?, event_ends_at = ?, publish_from = ?, publish_until = ?,
|
||||
is_active = ?, cta_label = ?, cta_url = ?, show_lead_form = ?
|
||||
WHERE e_id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$d['slug'], $d['title'], $d['subtitle'], $d['summary'], $d['description'],
|
||||
$d['hero_image'], $d['cor'], $d['location'],
|
||||
events_nullable_dt($d['event_starts_at']), events_nullable_dt($d['event_ends_at']),
|
||||
str_replace('T', ' ', (string) $d['publish_from']),
|
||||
str_replace('T', ' ', (string) $d['publish_until']),
|
||||
(int) $d['is_active'], $d['cta_label'], $d['cta_url'], (int) $d['show_lead_form'],
|
||||
$id,
|
||||
]);
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
function db_events_delete(PDO $pdo, int $id): bool {
|
||||
$stmt = $pdo->prepare("UPDATE events SET deleted_at = NOW() WHERE e_id = ? AND deleted_at IS NULL");
|
||||
$stmt->execute([$id]);
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
function db_events_restore(PDO $pdo, int $id): bool {
|
||||
$stmt = $pdo->prepare("UPDATE events SET deleted_at = NULL WHERE e_id = ? AND deleted_at IS NOT NULL");
|
||||
$stmt->execute([$id]);
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
// ─── Hero image upload / removal ───────────────────────────────────────────────
|
||||
// Stored in uploads/events/ as `upl_<slug>_<rand>.<ext>`. Mirrors the modalidades
|
||||
// icon upload, but optimises to the larger IMG_MAX_EVENT cap (it's a full-bleed
|
||||
// hero). events_image_remove() only ever unlinks files it created (`upl_` prefix).
|
||||
|
||||
function events_image_dir(): string {
|
||||
return __DIR__ . '/../uploads/events/';
|
||||
}
|
||||
|
||||
function events_image_upload(array $file, ?string &$error = null): ?string {
|
||||
$error = null;
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
|
||||
return null; // nothing uploaded — leave the existing image alone
|
||||
}
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
$error = 'Falha no upload da imagem (código ' . (int) $file['error'] . ').';
|
||||
return null;
|
||||
}
|
||||
if (($file['size'] ?? 0) > 6 * 1024 * 1024) {
|
||||
$error = 'A imagem excede o limite de 6 MB.';
|
||||
return null;
|
||||
}
|
||||
if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
|
||||
$error = 'Upload inválido.';
|
||||
return null;
|
||||
}
|
||||
$allowed = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
|
||||
$ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowed, true)) {
|
||||
$error = 'Formato inválido. Permitidos: ' . implode(', ', $allowed) . '.';
|
||||
return null;
|
||||
}
|
||||
|
||||
$dir = events_image_dir();
|
||||
if (!is_dir($dir)) @mkdir($dir, 0775, true);
|
||||
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', strtolower(pathinfo($file['name'], PATHINFO_FILENAME)));
|
||||
$slug = substr(trim((string) $slug, '-'), 0, 40);
|
||||
if ($slug === '') $slug = 'hero';
|
||||
$filename = 'upl_' . $slug . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $dir . $filename)) {
|
||||
$error = 'Não foi possível guardar a imagem.';
|
||||
return null;
|
||||
}
|
||||
if (defined('IMG_MAX_EVENT') && function_exists('img_optimize')) {
|
||||
img_optimize($dir . $filename, IMG_MAX_EVENT);
|
||||
}
|
||||
return $filename;
|
||||
}
|
||||
|
||||
function events_image_remove(?string $filename): void {
|
||||
$filename = basename((string) $filename);
|
||||
if ($filename === '' || strncmp($filename, 'upl_', 4) !== 0) return;
|
||||
$path = events_image_dir() . $filename;
|
||||
if (is_file($path)) @unlink($path);
|
||||
}
|
||||
|
||||
// ─── Leads ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function db_event_leads_list(PDO $pdo, int $eventId): array {
|
||||
$stmt = $pdo->prepare("SELECT * FROM event_leads WHERE e_id = ? ORDER BY created_at DESC, lead_id DESC");
|
||||
$stmt->execute([$eventId]);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function db_event_leads_count(PDO $pdo, int $eventId): int {
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM event_leads WHERE e_id = ?");
|
||||
$stmt->execute([$eventId]);
|
||||
return (int) $stmt->fetchColumn();
|
||||
} catch (Throwable $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function db_event_lead_create(PDO $pdo, int $eventId, array $data): int {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO event_leads (e_id, name, email, phone, message, consent, consent_text, ip, user_agent)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$eventId,
|
||||
mb_substr(trim((string) ($data['name'] ?? '')), 0, 150),
|
||||
mb_substr(trim((string) ($data['email'] ?? '')), 0, 190),
|
||||
mb_substr(trim((string) ($data['phone'] ?? '')), 0, 40),
|
||||
mb_substr(trim((string) ($data['message'] ?? '')), 0, 1000),
|
||||
!empty($data['consent']) ? 1 : 0,
|
||||
mb_substr(trim((string) ($data['consent_text'] ?? '')), 0, 255),
|
||||
mb_substr(trim((string) ($data['ip'] ?? '')), 0, 45),
|
||||
mb_substr(trim((string) ($data['user_agent'] ?? '')), 0, 255),
|
||||
]);
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function db_event_lead_delete(PDO $pdo, int $leadId, int $eventId): bool {
|
||||
$stmt = $pdo->prepare("DELETE FROM event_leads WHERE lead_id = ? AND e_id = ?");
|
||||
$stmt->execute([$leadId, $eventId]);
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
// ─── HTML sanitisation ───────────────────────────────────────────────────────
|
||||
// Admin-authored rich text (Summernote) and the Google-Maps embed are rendered
|
||||
// raw on the public site. To keep that from being a stored-XSS vector if an
|
||||
// admin/staff account is ever misused, every such value is passed through this
|
||||
// allowlist sanitiser at OUTPUT time (covers legacy rows too, not just new saves).
|
||||
//
|
||||
// Dependency-free: uses ext-dom (libxml). Falls back to a strip_tags allowlist
|
||||
// if DOM is unavailable. Disallowed elements are dropped wholesale; on every
|
||||
// surviving element only whitelisted attributes remain, and href/src schemes
|
||||
// are validated (no javascript:/data:/vbscript:).
|
||||
//
|
||||
// clean_richtext($html) — modalidades.descricao, events.description, …
|
||||
// clean_map_embed($html) — fitme3070_info.google_maps (single Google iframe)
|
||||
|
||||
// Tags allowed in general rich text (no attributes unless listed).
|
||||
const HTML_RICHTEXT_TAGS = [
|
||||
'p' => [], 'br' => [], 'hr' => [], 'div' => [], 'span' => [],
|
||||
'strong' => [], 'b' => [], 'em' => [], 'i' => [], 'u' => [], 's' => [],
|
||||
'ul' => [], 'ol' => [], 'li' => [],
|
||||
'h2' => [], 'h3' => [], 'h4' => [], 'h5' => [],
|
||||
'blockquote' => [], 'a' => ['href', 'target', 'rel'],
|
||||
];
|
||||
|
||||
// Elements removed entirely (content and all) when they appear anywhere.
|
||||
const HTML_DANGEROUS_TAGS = [
|
||||
'script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button',
|
||||
'textarea', 'select', 'option', 'link', 'meta', 'base', 'svg', 'math',
|
||||
'audio', 'video', 'source', 'frame', 'frameset', 'applet',
|
||||
];
|
||||
|
||||
function clean_richtext(?string $html): string {
|
||||
return html_sanitize((string) $html, HTML_RICHTEXT_TAGS);
|
||||
}
|
||||
|
||||
// The maps field holds a single Google Maps <iframe> embed. Allow exactly that.
|
||||
function clean_map_embed(?string $html): string {
|
||||
$allowed = [
|
||||
'iframe' => ['src', 'width', 'height', 'style', 'loading',
|
||||
'allowfullscreen', 'referrerpolicy', 'title'],
|
||||
];
|
||||
return html_sanitize((string) $html, $allowed, true);
|
||||
}
|
||||
|
||||
// Core sanitiser. $allowed maps tag => list of permitted attributes.
|
||||
// $allowMapsIframe gates iframe src to Google-Maps embed URLs only.
|
||||
function html_sanitize(string $html, array $allowed, bool $allowMapsIframe = false): string {
|
||||
$html = trim($html);
|
||||
if ($html === '') return '';
|
||||
|
||||
if (!class_exists('DOMDocument')) {
|
||||
// Conservative fallback: keep a safe inline tag set, drop ALL attributes
|
||||
// (strip_tags removes attributes with the tags it keeps's contents intact).
|
||||
return strip_tags($html, '<p><br><strong><b><em><i><u><ul><ol><li><h2><h3><h4><blockquote><span><a>');
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$prev = libxml_use_internal_errors(true);
|
||||
// The XML encoding hint forces UTF-8; NOIMPLIED/NODEFDTD keep it a fragment
|
||||
// (no <html>/<body>/doctype wrappers added).
|
||||
$doc->loadHTML(
|
||||
'<?xml encoding="utf-8"?><div id="__root__">' . $html . '</div>',
|
||||
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NONET
|
||||
);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($prev);
|
||||
|
||||
$root = $doc->getElementById('__root__');
|
||||
if (!$root) {
|
||||
// getElementById can miss without a DTD; fall back to first div.
|
||||
$root = $doc->getElementsByTagName('div')->item(0);
|
||||
}
|
||||
if (!$root) return '';
|
||||
|
||||
_html_sanitize_children($root, $allowed, $allowMapsIframe);
|
||||
|
||||
$out = '';
|
||||
foreach (iterator_to_array($root->childNodes) as $child) {
|
||||
$out .= $doc->saveHTML($child);
|
||||
}
|
||||
return trim($out);
|
||||
}
|
||||
|
||||
// Recursively scrub a node's children in place.
|
||||
function _html_sanitize_children(DOMNode $node, array $allowed, bool $allowMapsIframe): void {
|
||||
foreach (iterator_to_array($node->childNodes) as $child) {
|
||||
if ($child->nodeType === XML_COMMENT_NODE) {
|
||||
$node->removeChild($child);
|
||||
continue;
|
||||
}
|
||||
if ($child->nodeType !== XML_ELEMENT_NODE) {
|
||||
continue; // plain text — safe (serialised escaped)
|
||||
}
|
||||
|
||||
$tag = strtolower($child->nodeName);
|
||||
|
||||
// Disallowed → remove the element and everything inside it.
|
||||
if (!isset($allowed[$tag])) {
|
||||
$node->removeChild($child);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip every attribute that isn't explicitly permitted for this tag,
|
||||
// and validate URL-bearing ones.
|
||||
if ($child->hasAttributes()) {
|
||||
foreach (iterator_to_array($child->attributes) as $attr) {
|
||||
$name = strtolower($attr->name);
|
||||
if (!in_array($name, $allowed[$tag], true)) {
|
||||
$child->removeAttribute($attr->name); // drops on*, style (rich text), etc.
|
||||
continue;
|
||||
}
|
||||
if (($name === 'href' || $name === 'src')
|
||||
&& !_html_url_is_safe($attr->value, $tag === 'iframe' && $allowMapsIframe)) {
|
||||
$child->removeAttribute($attr->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Harden links that open in a new tab.
|
||||
if ($tag === 'a' && strtolower($child->getAttribute('target')) === '_blank') {
|
||||
$child->setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
_html_sanitize_children($child, $allowed, $allowMapsIframe);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow http(s)/mailto/tel/anchors/relative; for iframes, only Google-Maps
|
||||
// embeds. Always reject javascript:/data:/vbscript: and other schemes.
|
||||
function _html_url_is_safe(string $url, bool $isMapsIframe): bool {
|
||||
$url = trim($url);
|
||||
if ($url === '') return false;
|
||||
if (preg_match('/^\s*(javascript|data|vbscript|file):/i', $url)) return false;
|
||||
|
||||
if ($isMapsIframe) {
|
||||
// Any Google-owned Maps URL (www.google.com/maps/embed, maps.google.pt/…).
|
||||
return (bool) preg_match('#^https://([a-z0-9-]+\.)*google\.[a-z.]+/maps#i', $url);
|
||||
}
|
||||
if (preg_match('#^(https?:|mailto:|tel:|/|\#|\?|\.)#i', $url)) return true;
|
||||
// No scheme at all → relative path (e.g. "home/contactos") → safe.
|
||||
return !preg_match('#^[a-z][a-z0-9+.\-]*:#i', $url);
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
const IMG_MAX_GALLERY = 1920;
|
||||
const IMG_MAX_TEAM = 512;
|
||||
const IMG_MAX_MODALIDADE = 256;
|
||||
const IMG_MAX_EVENT = 1600;
|
||||
|
||||
const IMG_JPEG_QUALITY = 82;
|
||||
const IMG_WEBP_QUALITY = 80;
|
||||
|
||||
@@ -62,6 +62,13 @@ function site_modalidades(PDO $pdo): array {
|
||||
} catch (Throwable $e) { return []; }
|
||||
}
|
||||
|
||||
// Currently-visible time-limited events for the homepage banner. Thin wrapper
|
||||
// over db_events_active() (functions_events.php) so the site code reads in the
|
||||
// same site_*() style as everything else here.
|
||||
function site_active_events(PDO $pdo): array {
|
||||
return function_exists('db_events_active') ? db_events_active($pdo) : [];
|
||||
}
|
||||
|
||||
// ─── Image-path resolver ──────────────────────────────────────────────────────
|
||||
// DB stores either a bare filename ("1.jpg") or a full relative path
|
||||
// ("uploads/team/foto/1.jpg"). This normalises both to a working URL.
|
||||
|
||||
Reference in new issue
Block a user