- 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
+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];
}