- Rebuilt the CLI importer to use the official Facebook Graph API instead of HTML scraping.

Main changes:
Replaced 
[scripts/facebook-events-import.php](/Users/tyemeclifford/Documents/GH/MyKeyser/scripts/facebook-events-import.php) 
with a Graph API client.
It now supports --page-id, --pages-file, --event-id, --events-file, and 
optional area discovery via --discover-pages --center=LAT,LNG 
--query=....
Uses FACEBOOK_GRAPH_ACCESS_TOKEN or --access-token.
Pulls Graph fields for title, description, start/end time, 
place/location, ticket URI, and cover.source for the main photo.
Keeps the existing staged review/import workflow in 
/admin/event_imports.php.

- Implemented event detail pages and clickable event cards.
What changed:
Added [event.php](/Users/tyemeclifford/Documents/GH/MyKeyser/event.php) 
for individual event pages.
Made event cards clickable from 
[events.php](/Users/tyemeclifford/Documents/GH/MyKeyser/events.php), 
[index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/index.php), and 
legacy 
[2index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/2index.php).
Added large event photo display plus lightbox enlargement in 
[assets/css/style.css](/Users/tyemeclifford/Documents/GH/MyKeyser/assets/css/style.css) 
and 
[assets/js/app.js](/Users/tyemeclifford/Documents/GH/MyKeyser/assets/js/app.js).
Added eventUrl() and externalHref() helpers in 
[includes/functions.php](/Users/tyemeclifford/Documents/GH/MyKeyser/includes/functions.php).
Added copy-event-link support on event detail pages.
Updated README to list the new route.
This commit is contained in:
Ty Clifford
2026-06-19 18:07:45 -04:00
parent d83b62b79c
commit 354bb4255d
13 changed files with 1455 additions and 9 deletions
+53
View File
@@ -95,6 +95,34 @@ function _ensureAppUpgrades(PDO $db): void {
created_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_email_tokens_user ON email_verification_tokens(user_id, purpose, used_at);
CREATE TABLE IF NOT EXISTS event_imports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL DEFAULT 'facebook',
source_area TEXT NOT NULL DEFAULT '',
source_url TEXT NOT NULL DEFAULT '',
external_id TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
venue TEXT NOT NULL DEFAULT '',
address TEXT NOT NULL DEFAULT '',
event_date TEXT NOT NULL DEFAULT '',
event_time TEXT NOT NULL DEFAULT '',
end_date TEXT,
end_time TEXT NOT NULL DEFAULT '',
cost TEXT NOT NULL DEFAULT 'Free',
website TEXT NOT NULL DEFAULT '',
image_url TEXT NOT NULL DEFAULT '',
image_path TEXT NOT NULL DEFAULT '',
raw_json TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'staged',
status_note TEXT NOT NULL DEFAULT '',
imported_event_id INTEGER REFERENCES events(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_event_imports_status ON event_imports(status, created_at);
CREATE INDEX IF NOT EXISTS idx_event_imports_external ON event_imports(provider, external_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_event_imports_source ON event_imports(provider, source_url) WHERE source_url <> '';
CREATE TABLE IF NOT EXISTS forum_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
@@ -329,6 +357,31 @@ function _schema(PDO $db): void {
is_featured INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE TABLE IF NOT EXISTS event_imports(
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL DEFAULT 'facebook',
source_area TEXT NOT NULL DEFAULT '',
source_url TEXT NOT NULL DEFAULT '',
external_id TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
venue TEXT NOT NULL DEFAULT '',
address TEXT NOT NULL DEFAULT '',
event_date TEXT NOT NULL DEFAULT '',
event_time TEXT NOT NULL DEFAULT '',
end_date TEXT,
end_time TEXT NOT NULL DEFAULT '',
cost TEXT NOT NULL DEFAULT 'Free',
website TEXT NOT NULL DEFAULT '',
image_url TEXT NOT NULL DEFAULT '',
image_path TEXT NOT NULL DEFAULT '',
raw_json TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'staged',
status_note TEXT NOT NULL DEFAULT '',
imported_event_id INTEGER REFERENCES events(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE TABLE IF NOT EXISTS reports(
id INTEGER PRIMARY KEY AUTOINCREMENT,
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
+601
View File
@@ -0,0 +1,601 @@
<?php
if (!function_exists('qrun')) require_once __DIR__.'/boot.php';
function facebookEventImporterUserAgent(): string {
return 'MyKeyserEventImporter/1.0 (+https://mykeyser.local)';
}
function facebookEventText(mixed $value): string {
if (is_array($value)) $value = implode(' ', array_filter(array_map('facebookEventText', $value)));
$value = html_entity_decode(strip_tags((string)($value ?? '')), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
return trim(preg_replace('/\s+/', ' ', $value));
}
function facebookEventAllowedImageHost(string $url): bool {
$host = strtolower((string)(parse_url($url, PHP_URL_HOST) ?? ''));
if ($host === '') return false;
foreach (['facebook.com', 'fbcdn.net', 'fbsbx.com'] as $allowed) {
if ($host === $allowed || str_ends_with($host, '.'.$allowed)) return true;
}
return false;
}
function facebookEventNormalizeUrl(string $url, string $baseUrl = 'https://www.facebook.com'): string {
$url = trim(html_entity_decode($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
$url = str_replace(['\\/', '\\u002F', '\\u003A', '\\u0026'], ['/', '/', ':', '&'], $url);
if ($url === '') return '';
if (str_starts_with($url, '//')) $url = 'https:'.$url;
if (str_starts_with($url, '/')) {
$base = parse_url($baseUrl);
$scheme = $base['scheme'] ?? 'https';
$host = $base['host'] ?? 'www.facebook.com';
$url = $scheme.'://'.$host.$url;
}
$parts = parse_url($url);
if (!$parts || empty($parts['scheme']) || empty($parts['host'])) return '';
if (!in_array(strtolower($parts['scheme']), ['http', 'https'], true)) return '';
$host = strtolower($parts['host']);
$path = $parts['path'] ?? '';
if (($host === 'l.facebook.com' || str_ends_with($host, '.l.facebook.com')) && !empty($parts['query'])) {
parse_str($parts['query'], $query);
if (!empty($query['u'])) return facebookEventNormalizeUrl((string)$query['u'], $baseUrl);
}
$normalized = strtolower($parts['scheme']).'://'.$host.$path;
if (!empty($parts['query'])) $normalized .= '?'.$parts['query'];
return $normalized;
}
function facebookEventNormalizeEventUrl(string $url, string $baseUrl = 'https://www.facebook.com'): string {
$url = facebookEventNormalizeUrl($url, $baseUrl);
if ($url === '') return '';
$parts = parse_url($url);
$host = strtolower($parts['host'] ?? '');
if ($host !== 'facebook.com' && $host !== 'www.facebook.com' && !str_ends_with($host, '.facebook.com')) return '';
$path = preg_replace('~/+~', '/', (string)($parts['path'] ?? ''));
if (!preg_match('~^/(events|share)/~i', $path)) return '';
return 'https://www.facebook.com'.rtrim($path, '/');
}
function facebookEventExternalId(string $url): string {
$path = (string)(parse_url($url, PHP_URL_PATH) ?? '');
if (preg_match_all('~\d{6,}~', $path, $matches) && !empty($matches[0])) {
return end($matches[0]);
}
return '';
}
function facebookEventFetchUrl(string $url): array {
$url = facebookEventNormalizeUrl($url);
if ($url === '') return ['ok' => false, 'error' => 'Invalid URL.', 'body' => '', 'url' => ''];
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 4,
CURLOPT_TIMEOUT => 25,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_USERAGENT => facebookEventImporterUserAgent(),
CURLOPT_HTTPHEADER => ['Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'],
]);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$finalUrl = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
if (PHP_VERSION_ID < 80000) curl_close($ch);
if ($errno) return ['ok' => false, 'error' => $error ?: 'Request failed.', 'body' => '', 'url' => $url];
if ($status < 200 || $status >= 400) return ['ok' => false, 'error' => 'HTTP '.$status.' from Facebook.', 'body' => (string)$body, 'url' => $finalUrl ?: $url];
return ['ok' => true, 'error' => '', 'body' => (string)$body, 'url' => $finalUrl ?: $url];
}
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 25,
'ignore_errors' => true,
'header' => "User-Agent: ".facebookEventImporterUserAgent()."\r\nAccept: text/html\r\n",
],
]);
$body = @file_get_contents($url, false, $context);
$statusLine = $http_response_header[0] ?? '';
if ($body === false || ($statusLine && !preg_match('/\s[23]\d\d\s/', $statusLine))) {
return ['ok' => false, 'error' => $statusLine ?: 'Request failed.', 'body' => (string)$body, 'url' => $url];
}
return ['ok' => true, 'error' => '', 'body' => (string)$body, 'url' => $url];
}
function facebookEventDom(string $html): ?DOMDocument {
if (!class_exists('DOMDocument')) return null;
$dom = new DOMDocument();
$old = libxml_use_internal_errors(true);
$loaded = $dom->loadHTML('<?xml encoding="utf-8" ?>'.$html);
libxml_clear_errors();
libxml_use_internal_errors($old);
return $loaded ? $dom : null;
}
function facebookEventMeta(string $html): array {
$dom = facebookEventDom($html);
if (!$dom) return [];
$out = [];
foreach ($dom->getElementsByTagName('meta') as $meta) {
$key = $meta->getAttribute('property') ?: $meta->getAttribute('name');
$content = $meta->getAttribute('content');
if ($key !== '' && $content !== '') $out[strtolower($key)] = $content;
}
foreach ($dom->getElementsByTagName('link') as $link) {
if (strtolower($link->getAttribute('rel')) === 'canonical' && $link->getAttribute('href') !== '') {
$out['canonical'] = $link->getAttribute('href');
}
}
return $out;
}
function facebookEventFindJsonLdEvent(mixed $node): ?array {
if (!is_array($node)) return null;
$type = $node['@type'] ?? '';
$types = is_array($type) ? $type : [$type];
foreach ($types as $candidateType) {
if (strtolower((string)$candidateType) === 'event') return $node;
}
foreach ($node as $child) {
$found = facebookEventFindJsonLdEvent($child);
if ($found) return $found;
}
return null;
}
function facebookEventJsonLd(string $html): ?array {
$dom = facebookEventDom($html);
if (!$dom) return null;
foreach ($dom->getElementsByTagName('script') as $script) {
$type = strtolower($script->getAttribute('type'));
if ($type !== 'application/ld+json') continue;
$json = trim($script->textContent ?? '');
if ($json === '') continue;
$decoded = json_decode($json, true);
$found = facebookEventFindJsonLdEvent($decoded);
if ($found) return $found;
}
return null;
}
function facebookEventImageFromValue(mixed $value): string {
if (is_string($value)) return $value;
if (is_array($value)) {
if (isset($value['url'])) return facebookEventImageFromValue($value['url']);
foreach ($value as $item) {
$image = facebookEventImageFromValue($item);
if ($image !== '') return $image;
}
}
return '';
}
function facebookEventAddressFromLocation(mixed $location): array {
$venue = '';
$address = '';
if (is_string($location)) return [$location, ''];
if (!is_array($location)) return ['', ''];
$venue = facebookEventText($location['name'] ?? '');
$addr = $location['address'] ?? '';
if (is_string($addr)) {
$address = facebookEventText($addr);
} elseif (is_array($addr)) {
$parts = [];
foreach (['streetAddress', 'addressLocality', 'addressRegion', 'postalCode'] as $key) {
if (!empty($addr[$key])) $parts[] = facebookEventText($addr[$key]);
}
$address = implode(', ', array_filter($parts));
}
return [$venue, $address];
}
function facebookEventSplitDateTime(string $value): array {
$value = facebookEventText(str_replace([' at ', ' AT '], ' ', $value));
if ($value === '') return ['', ''];
$hasTime = (bool)preg_match('/(?:T|\b\d{1,2}:\d{2}\b|\b\d{1,2}\s*(?:am|pm)\b)/i', $value);
try {
$dt = new DateTimeImmutable($value);
} catch (Throwable) {
return ['', ''];
}
return [$dt->format('Y-m-d'), $hasTime ? $dt->format('g:i A') : ''];
}
function facebookEventDateFromText(string $text): array {
$months = 'Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?';
if (preg_match('/\b(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)?(?:day)?[,]?\s*(?:'.$months.')\s+\d{1,2}(?:,\s*\d{4})?(?:\s+(?:at|@)\s+\d{1,2}(?::\d{2})?\s*(?:am|pm)?)?/i', $text, $match)) {
return facebookEventSplitDateTime($match[0]);
}
if (preg_match('/\b\d{4}-\d{2}-\d{2}(?:[T\s]\d{1,2}:\d{2}(?::\d{2})?)?/i', $text, $match)) {
return facebookEventSplitDateTime($match[0]);
}
return ['', ''];
}
function facebookEventCleanTitle(string $title): string {
$title = facebookEventText($title);
$title = preg_replace('/\s*\|\s*Facebook\s*$/i', '', $title);
$title = preg_replace('/\s*-\s*Facebook\s*$/i', '', $title);
return trim($title);
}
function facebookEventWebsiteForStorage(string $url): string {
$url = trim($url);
if ($url === '') return '';
$url = preg_replace('~^https?://~i', '', $url);
return preg_replace('~/+$~', '', $url);
}
function facebookEventExtractEventUrls(string $html, string $sourceUrl = 'https://www.facebook.com'): array {
$links = [];
$dom = facebookEventDom($html);
if ($dom) {
foreach ($dom->getElementsByTagName('a') as $a) {
$href = $a->getAttribute('href');
$url = facebookEventNormalizeEventUrl($href, $sourceUrl);
if ($url !== '') $links[$url] = $url;
}
}
$decoded = html_entity_decode($html, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$decoded = str_replace(['\\/', '\\u002F', '\\u003A', '\\u0026'], ['/', '/', ':', '&'], $decoded);
if (preg_match_all('~https?://(?:www\.)?facebook\.com/(?:events|share)/[^\s"\'<>\\\\]+~i', $decoded, $matches)) {
foreach ($matches[0] as $raw) {
$url = facebookEventNormalizeEventUrl($raw, $sourceUrl);
if ($url !== '') $links[$url] = $url;
}
}
if (preg_match_all('~href=["\']([^"\']*(?:/events/|/share/)[^"\']*)["\']~i', $decoded, $matches)) {
foreach ($matches[1] as $raw) {
$url = facebookEventNormalizeEventUrl($raw, $sourceUrl);
if ($url !== '') $links[$url] = $url;
}
}
return array_values($links);
}
function facebookEventParseHtml(string $html, string $sourceUrl = '', string $area = ''): array {
$jsonLd = facebookEventJsonLd($html);
$meta = facebookEventMeta($html);
$sourceUrl = facebookEventNormalizeEventUrl($sourceUrl ?: ($meta['og:url'] ?? $meta['canonical'] ?? '')) ?: facebookEventNormalizeUrl($sourceUrl ?: ($meta['og:url'] ?? $meta['canonical'] ?? ''));
$candidate = [
'provider' => 'facebook',
'source_area' => $area,
'source_url' => $sourceUrl,
'external_id' => facebookEventExternalId($sourceUrl),
'title' => '',
'description' => '',
'venue' => '',
'address' => '',
'event_date' => '',
'event_time' => '',
'end_date' => null,
'end_time' => '',
'cost' => 'Free',
'website' => facebookEventWebsiteForStorage($sourceUrl),
'image_url' => '',
'image_path' => '',
'raw_json' => ['json_ld' => $jsonLd, 'meta' => $meta],
];
if ($jsonLd) {
$candidate['title'] = facebookEventCleanTitle((string)($jsonLd['name'] ?? ''));
$candidate['description'] = facebookEventText($jsonLd['description'] ?? '');
[$candidate['event_date'], $candidate['event_time']] = facebookEventSplitDateTime((string)($jsonLd['startDate'] ?? ''));
[$endDate, $endTime] = facebookEventSplitDateTime((string)($jsonLd['endDate'] ?? ''));
$candidate['end_date'] = $endDate ?: null;
$candidate['end_time'] = $endTime;
[$candidate['venue'], $candidate['address']] = facebookEventAddressFromLocation($jsonLd['location'] ?? null);
$candidate['image_url'] = facebookEventNormalizeUrl(facebookEventImageFromValue($jsonLd['image'] ?? ''), $sourceUrl ?: 'https://www.facebook.com');
if (!empty($jsonLd['url'])) {
$candidate['source_url'] = facebookEventNormalizeEventUrl((string)$jsonLd['url'], $sourceUrl ?: 'https://www.facebook.com') ?: $candidate['source_url'];
$candidate['website'] = facebookEventWebsiteForStorage($candidate['source_url']);
$candidate['external_id'] = facebookEventExternalId($candidate['source_url']);
}
if (!empty($jsonLd['offers']['price'])) $candidate['cost'] = facebookEventText($jsonLd['offers']['price']);
}
if ($candidate['title'] === '') $candidate['title'] = facebookEventCleanTitle($meta['og:title'] ?? $meta['twitter:title'] ?? '');
if ($candidate['description'] === '') $candidate['description'] = facebookEventText($meta['og:description'] ?? $meta['description'] ?? $meta['twitter:description'] ?? '');
if ($candidate['image_url'] === '') $candidate['image_url'] = facebookEventNormalizeUrl($meta['og:image'] ?? $meta['twitter:image'] ?? '', $sourceUrl ?: 'https://www.facebook.com');
if ($candidate['event_date'] === '') {
[$candidate['event_date'], $candidate['event_time']] = facebookEventDateFromText($candidate['title'].' '.$candidate['description']);
}
if ($candidate['source_url'] === '' && !empty($meta['og:url'])) {
$candidate['source_url'] = facebookEventNormalizeUrl($meta['og:url']);
$candidate['website'] = facebookEventWebsiteForStorage($candidate['source_url']);
$candidate['external_id'] = facebookEventExternalId($candidate['source_url']);
}
return $candidate;
}
function facebookGraphEventUrl(string $eventId): string {
$eventId = trim($eventId);
return $eventId !== '' ? 'https://www.facebook.com/events/'.rawurlencode($eventId) : '';
}
function facebookGraphPlaceToVenueAddress(mixed $place): array {
if (!is_array($place)) return ['', ''];
$venue = facebookEventText($place['name'] ?? '');
$location = $place['location'] ?? [];
if (!is_array($location)) return [$venue, ''];
$parts = [];
foreach (['street', 'city', 'state', 'zip', 'country'] as $key) {
if (!empty($location[$key])) $parts[] = facebookEventText($location[$key]);
}
return [$venue, implode(', ', array_filter($parts))];
}
function facebookGraphEventToCandidate(array $event, string $area = ''): array {
$eventId = trim((string)($event['id'] ?? ''));
$sourceUrl = facebookGraphEventUrl($eventId);
[$eventDate, $eventTime] = facebookEventSplitDateTime((string)($event['start_time'] ?? ''));
[$endDate, $endTime] = facebookEventSplitDateTime((string)($event['end_time'] ?? ''));
[$venue, $address] = facebookGraphPlaceToVenueAddress($event['place'] ?? null);
$ticketUrl = facebookEventNormalizeUrl((string)($event['ticket_uri'] ?? ''));
$website = $ticketUrl !== '' ? $ticketUrl : $sourceUrl;
$cover = $event['cover'] ?? [];
$imageUrl = is_array($cover) ? facebookEventNormalizeUrl((string)($cover['source'] ?? '')) : '';
return [
'provider' => 'facebook',
'source_area' => $area,
'source_url' => $sourceUrl,
'external_id' => $eventId,
'title' => facebookEventCleanTitle((string)($event['name'] ?? '')),
'description' => facebookEventText($event['description'] ?? ''),
'venue' => $venue,
'address' => $address,
'event_date' => $eventDate,
'event_time' => $eventTime,
'end_date' => $endDate ?: null,
'end_time' => $endTime,
'cost' => 'Free',
'website' => facebookEventWebsiteForStorage($website),
'image_url' => $imageUrl,
'image_path' => '',
'raw_json' => ['graph_event' => $event],
];
}
function facebookEventDownloadImage(string $imageUrl): array {
$imageUrl = facebookEventNormalizeUrl($imageUrl);
if ($imageUrl === '') return ['ok' => true, 'path' => '', 'error' => ''];
if (!facebookEventAllowedImageHost($imageUrl)) {
return ['ok' => false, 'path' => '', 'error' => 'Image host is not a Facebook image host.'];
}
$maxBytes = mbToBytes(setting('event_upload_max_mb', '5'), 5);
$tmp = tempnam(sys_get_temp_dir(), 'fb-event-img-');
if (!$tmp) return ['ok' => false, 'path' => '', 'error' => 'Could not create a temporary image file.'];
$mime = '';
$size = 0;
if (function_exists('curl_init')) {
$fh = fopen($tmp, 'wb');
if (!$fh) return ['ok' => false, 'path' => '', 'error' => 'Could not write a temporary image file.'];
$ch = curl_init($imageUrl);
curl_setopt_array($ch, [
CURLOPT_FILE => $fh,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 4,
CURLOPT_TIMEOUT => 25,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_USERAGENT => facebookEventImporterUserAgent(),
CURLOPT_WRITEFUNCTION => function ($ch, string $chunk) use ($fh, $maxBytes, &$size): int {
$size += strlen($chunk);
if ($size > $maxBytes) return 0;
$written = fwrite($fh, $chunk);
return $written === false ? 0 : $written;
},
]);
$ok = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$mime = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
if (PHP_VERSION_ID < 80000) curl_close($ch);
fclose($fh);
if (!$ok || $errno || $status < 200 || $status >= 400) {
@unlink($tmp);
$message = $size > $maxBytes ? 'Image exceeds the configured size limit.' : ($error ?: 'Image download failed.');
return ['ok' => false, 'path' => '', 'error' => $message];
}
} else {
$context = stream_context_create(['http' => ['timeout' => 25, 'ignore_errors' => true, 'header' => 'User-Agent: '.facebookEventImporterUserAgent()."\r\n"]]);
$body = @file_get_contents($imageUrl, false, $context);
if ($body === false || strlen($body) > $maxBytes) {
@unlink($tmp);
return ['ok' => false, 'path' => '', 'error' => $body === false ? 'Image download failed.' : 'Image exceeds the configured size limit.'];
}
file_put_contents($tmp, $body);
}
if (!function_exists('getimagesize') || !@getimagesize($tmp)) {
@unlink($tmp);
return ['ok' => false, 'path' => '', 'error' => 'Downloaded file is not a valid image.'];
}
if ($mime === '' && function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if ($finfo) {
$mime = (string)finfo_file($finfo, $tmp);
finfo_close($finfo);
}
}
$ext = match (strtolower(strtok($mime, ';') ?: '')) {
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
default => 'jpg',
};
$stored = storeUploadedFile(
['name' => 'facebook-event.'.$ext, 'tmp_name' => $tmp, 'error' => UPLOAD_ERR_OK, 'size' => filesize($tmp) ?: 0],
'events',
imageExts(),
$maxBytes,
[
'image_only' => true,
'resize_over_bytes' => mbToBytes(setting('forum_image_resize_threshold_mb', '1'), 1),
'image_max_width' => (int)setting('forum_image_max_width', '1600'),
'image_quality' => (int)setting('forum_image_quality', '82'),
]
);
if (!$stored['ok']) @unlink($tmp);
return $stored['ok']
? ['ok' => true, 'path' => (string)$stored['path'], 'error' => '']
: ['ok' => false, 'path' => '', 'error' => $stored['error'] ?? 'Image could not be saved.'];
}
function facebookEventCandidateIssues(array $candidate): array {
$issues = [];
if (trim((string)($candidate['title'] ?? '')) === '') $issues[] = 'Missing title';
$date = trim((string)($candidate['event_date'] ?? ''));
$dt = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
if ($date === '' || !$dt || $dt->format('Y-m-d') !== $date) $issues[] = 'Missing valid start date';
return $issues;
}
function facebookEventDuplicateId(array $candidate): int {
$title = trim((string)($candidate['title'] ?? ''));
$date = trim((string)($candidate['event_date'] ?? ''));
if ($title === '' || $date === '') return 0;
return (int)qval("SELECT id FROM events WHERE lower(title)=lower(?) AND event_date=? LIMIT 1", [$title, $date]);
}
function facebookEventStageCandidate(array $candidate, bool $downloadImage = true): array {
$provider = 'facebook';
$sourceUrl = facebookEventNormalizeEventUrl((string)($candidate['source_url'] ?? '')) ?: facebookEventNormalizeUrl((string)($candidate['source_url'] ?? ''));
$candidate['source_url'] = $sourceUrl;
$candidate['external_id'] = trim((string)($candidate['external_id'] ?? facebookEventExternalId($sourceUrl)));
$candidate['website'] = facebookEventWebsiteForStorage((string)($candidate['website'] ?? $sourceUrl));
$candidate['cost'] = trim((string)($candidate['cost'] ?? '')) ?: 'Free';
$candidate['end_date'] = ($candidate['end_date'] ?? '') ?: null;
$candidate['raw_json'] = $candidate['raw_json'] ?? [];
if ($downloadImage && empty($candidate['image_path']) && !empty($candidate['image_url'])) {
$image = facebookEventDownloadImage((string)$candidate['image_url']);
if ($image['ok'] && !empty($image['path'])) {
$candidate['image_path'] = $image['path'];
} elseif (!$image['ok']) {
$candidate['status_note'] = 'Image not downloaded: '.$image['error'];
}
}
$existing = null;
if ($sourceUrl !== '') $existing = qone("SELECT * FROM event_imports WHERE provider=? AND source_url=? LIMIT 1", [$provider, $sourceUrl]);
if (!$existing && $candidate['external_id'] !== '') {
$existing = qone("SELECT * FROM event_imports WHERE provider=? AND external_id=? LIMIT 1", [$provider, $candidate['external_id']]);
}
$fields = [
'provider' => $provider,
'source_area' => facebookEventText($candidate['source_area'] ?? ''),
'source_url' => $sourceUrl,
'external_id' => $candidate['external_id'],
'title' => facebookEventCleanTitle((string)($candidate['title'] ?? '')),
'description' => facebookEventText($candidate['description'] ?? ''),
'venue' => facebookEventText($candidate['venue'] ?? ''),
'address' => facebookEventText($candidate['address'] ?? ''),
'event_date' => trim((string)($candidate['event_date'] ?? '')),
'event_time' => facebookEventText($candidate['event_time'] ?? ''),
'end_date' => $candidate['end_date'],
'end_time' => facebookEventText($candidate['end_time'] ?? ''),
'cost' => facebookEventText($candidate['cost']),
'website' => facebookEventWebsiteForStorage((string)$candidate['website']),
'image_url' => facebookEventNormalizeUrl((string)($candidate['image_url'] ?? ''), $sourceUrl ?: 'https://www.facebook.com'),
'image_path' => trim((string)($candidate['image_path'] ?? '')),
'raw_json' => json_encode($candidate['raw_json'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '{}',
'status_note' => facebookEventText($candidate['status_note'] ?? ''),
];
if ($existing) {
if (($existing['status'] ?? '') === 'imported') {
return ['ok' => true, 'id' => (int)$existing['id'], 'action' => 'already_imported'];
}
qrun(
"UPDATE event_imports SET source_area=?,source_url=?,external_id=?,title=?,description=?,venue=?,address=?,event_date=?,event_time=?,end_date=?,end_time=?,cost=?,website=?,image_url=?,image_path=?,raw_json=?,status='staged',status_note=?,updated_at=datetime('now') WHERE id=?",
[$fields['source_area'], $fields['source_url'], $fields['external_id'], $fields['title'], $fields['description'], $fields['venue'], $fields['address'], $fields['event_date'], $fields['event_time'], $fields['end_date'], $fields['end_time'], $fields['cost'], $fields['website'], $fields['image_url'], $fields['image_path'], $fields['raw_json'], $fields['status_note'], (int)$existing['id']]
);
return ['ok' => true, 'id' => (int)$existing['id'], 'action' => 'updated'];
}
$id = qrun(
"INSERT INTO event_imports(provider,source_area,source_url,external_id,title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,image_url,image_path,raw_json,status,status_note)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'staged',?)",
[$fields['provider'], $fields['source_area'], $fields['source_url'], $fields['external_id'], $fields['title'], $fields['description'], $fields['venue'], $fields['address'], $fields['event_date'], $fields['event_time'], $fields['end_date'], $fields['end_time'], $fields['cost'], $fields['website'], $fields['image_url'], $fields['image_path'], $fields['raw_json'], $fields['status_note']]
);
return ['ok' => true, 'id' => $id, 'action' => 'created'];
}
function facebookEventSaveCandidateFields(int $id, array $row): void {
qrun(
"UPDATE event_imports SET title=?,description=?,venue=?,address=?,event_date=?,event_time=?,end_date=?,end_time=?,cost=?,website=?,image_path=?,status_note='',updated_at=datetime('now') WHERE id=? AND status!='imported'",
[
facebookEventCleanTitle((string)($row['title'] ?? '')),
facebookEventText($row['description'] ?? ''),
facebookEventText($row['venue'] ?? ''),
facebookEventText($row['address'] ?? ''),
trim((string)($row['event_date'] ?? '')),
facebookEventText($row['event_time'] ?? ''),
trim((string)($row['end_date'] ?? '')) ?: null,
facebookEventText($row['end_time'] ?? ''),
facebookEventText($row['cost'] ?? 'Free') ?: 'Free',
facebookEventWebsiteForStorage((string)($row['website'] ?? '')),
trim((string)($row['image_path'] ?? '')),
$id,
]
);
}
function facebookEventImportCandidate(int $id, int $userId, string $eventStatus = 'pending', bool $allowDuplicate = false): array {
$candidate = qone("SELECT * FROM event_imports WHERE id=?", [$id]);
if (!$candidate) return ['ok' => false, 'error' => 'Import candidate not found.'];
if (($candidate['status'] ?? '') === 'imported') return ['ok' => false, 'error' => 'This candidate has already been imported.'];
$issues = facebookEventCandidateIssues($candidate);
if ($issues) return ['ok' => false, 'error' => implode('; ', $issues).'.'];
$duplicateId = facebookEventDuplicateId($candidate);
if ($duplicateId && !$allowDuplicate) return ['ok' => false, 'error' => 'Possible duplicate of event #'.$duplicateId.'.'];
$eventStatus = in_array($eventStatus, ['pending', 'approved'], true) ? $eventStatus : 'pending';
$imagePath = (string)($candidate['image_path'] ?? '');
$note = '';
if ($imagePath === '' && !empty($candidate['image_url'])) {
$image = facebookEventDownloadImage((string)$candidate['image_url']);
if ($image['ok'] && !empty($image['path'])) {
$imagePath = (string)$image['path'];
} elseif (!$image['ok']) {
$note = 'Imported without image: '.$image['error'];
}
}
$eventId = qrun(
"INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,image_path,submitted_by,status,is_featured)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)",
[
$candidate['title'],
$candidate['description'],
$candidate['venue'],
$candidate['address'],
$candidate['event_date'],
$candidate['event_time'],
$candidate['end_date'] ?: null,
$candidate['end_time'],
$candidate['cost'] ?: 'Free',
facebookEventWebsiteForStorage((string)($candidate['website'] ?: $candidate['source_url'])),
'',
'',
'',
$imagePath,
$userId ?: null,
$eventStatus,
]
);
qrun("UPDATE event_imports SET status='imported',imported_event_id=?,image_path=?,status_note=?,updated_at=datetime('now') WHERE id=?", [$eventId, $imagePath, $note, $id]);
return ['ok' => true, 'event_id' => $eventId, 'warning' => $note];
}
+12 -1
View File
@@ -40,7 +40,7 @@ function g(string $k, mixed $d = ''): mixed { return $_GET[$k] ?? $d; }
function iget(string $k): int { return (int)($_POST[$k] ?? $_GET[$k] ?? 0); }
function ps(string $k): string { return trim((string)($_POST[$k] ?? '')); }
function gs(string $k): string { return trim((string)($_GET[$k] ?? '')); }
function isPost(): bool { return strtoupper($_SERVER['REQUEST_METHOD']) === 'POST'; }
function isPost(): bool { return strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST'; }
/* ── CSRF ─────────────────────────────────────────────── */
function csrf(): string {
@@ -325,6 +325,17 @@ function forumTopicUrl(array $topic): string {
return '/forum/'.rawurlencode((string)$topic['slug']).'/';
}
function eventUrl(array $event): string {
return '/event.php?id='.(int)($event['id'] ?? 0);
}
function externalHref(string $url): string {
$url = trim($url);
if ($url === '') return '';
if (preg_match('~^https?://~i', $url)) return $url;
return 'https://'.$url;
}
/* ── Email verification / Mailgun ─────────────────────── */
function requestBaseUrl(): string {
$configured = trim(setting('site_base_url', ''));