Atlas modal: pose toggle + wiki descriptions

avian/api/cutout.php: read ?pose=N parameter. pose=1 (default) serves
<slug>.png, pose=2 serves <slug>-2.png, etc. Pose >1 missing falls
back to pose-1 instead of skipping to the photo cutout so the flight
tab still shows something recognisable. Pose clamped to 1-99 to
block path-traversal payloads.

avian/api/wiki.php (new): Wikipedia REST summary proxy. Returns
{extract, thumbnail, title}. Thumbnail host pinned to wikimedia.org/
wikipedia.org to block SSRF. Cached for 24h.

avian/frontend/apt.js: fetch wiki descriptions from ./avian/api/wiki.php
(was hitting the worker's /api/wiki.json which 404'd in our overlay).
This commit is contained in:
Twarner491
2026-05-28 12:16:08 -07:00
parent 0105fb54f0
commit 35eed2a885
3 changed files with 74 additions and 3 deletions
+56
View File
@@ -0,0 +1,56 @@
<?php
// AvianVisitors - Wikipedia summary proxy.
//
// The detail modal calls /avian/api/wiki.php?sci=<name> for the species
// description. We hit Wikipedia's REST summary endpoint and return the
// extract + thumbnail. Cached at the browser + Caddy edge for 24 h
// because species descriptions don't really change.
//
// Pinned to wikimedia.org / wikipedia.org for the photo URL to block
// SSRF if Wikipedia ever returns a poisoned redirect target.
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: public, max-age=86400');
$sci = trim((string)($_GET['sci'] ?? ''));
if ($sci === '') {
http_response_code(400);
echo json_encode(['error' => 'sci required']);
exit;
}
if (!preg_match('/^[A-Za-z]{2,40}(?:[ ][a-z]{2,40}){1,3}$/', $sci)) {
http_response_code(400);
echo json_encode(['error' => 'invalid sci']);
exit;
}
$ua = getenv('AV_USER_AGENT') ?: 'AvianVisitors/1.0 (+https://github.com/Twarner491/AvianVisitors)';
$ctx = stream_context_create([
'http' => ['header' => "User-Agent: $ua\r\n", 'timeout' => 8],
]);
$url = 'https://en.wikipedia.org/api/rest_v1/page/summary/' . rawurlencode($sci);
$raw = @file_get_contents($url, false, $ctx);
if ($raw === false) {
echo json_encode(['extract' => null, 'thumbnail' => null]);
exit;
}
$j = json_decode($raw, true);
if (!is_array($j)) {
echo json_encode(['extract' => null, 'thumbnail' => null]);
exit;
}
$thumb = $j['thumbnail']['source'] ?? null;
if ($thumb) {
$host = parse_url((string)$thumb, PHP_URL_HOST) ?: '';
if (!preg_match('/(?:^|\.)(?:wikimedia\.org|wikipedia\.org)$/i', $host)) {
$thumb = null;
}
}
echo json_encode([
'extract' => $j['extract'] ?? null,
'thumbnail' => $thumb ? ['source' => $thumb] : null,
'title' => $j['title'] ?? null,
]);