`,短语必须有效(`Nav::ownerByPhrase`); * · **只返回这个主人自己的头像** —— 不接任何 id、不接任何别的键, * 所以拿别人的短语只能看到那个人自己的头像(那本来就公开在他的展示页上); * · 头像是**公开内容**(它天天挂在那个公开页上),不含隐私; * · 没有头像时 404(调用方 `onerror` 换成默认剪影,不在这里回退 —— * 那样"有没有设过头像"在服务端就分不清了)。 */ declare(strict_types=1); require __DIR__ . '/app/bootstrap.php'; use App\Nav; use App\Person; use App\Storage\Storage; function nav_av_fail(int $code, string $message): void { http_response_code($code); header('Content-Type: text/plain; charset=utf-8'); header('Cache-Control: no-store'); echo $message; exit; } $phrase = trim((string)($_GET['p'] ?? '')); if ($phrase === '') { nav_av_fail(404, '没有这个头像'); } $personId = Nav::ownerByPhrase($phrase); if ($personId <= 0) { nav_av_fail(404, '没有这个头像'); } $p = Person::find($personId); if (!$p) { nav_av_fail(404, '没有这个头像'); } $key = trim((string)($p['avatar'] ?? '')); if ($key === '') { nav_av_fail(404, '这个人没有头像'); // 前端 onerror 会换成默认剪影 } // 外部绝对地址(历史数据)→ 直接跳过去,不必代理 if (preg_match('#^https?://#i', $key)) { header('Location: ' . $key, true, 302); exit; } // 键净化:与 media.php 同一条判据(拒绝 `..`、绝对路径、点文件) $key = ltrim(str_replace('\\', '/', $key), '/'); if ($key === '' || strpos($key, "\0") !== false || strpos($key, '..') !== false || !preg_match('#^[A-Za-z0-9][A-Za-z0-9._/-]*$#', $key)) { nav_av_fail(404, '没有这个头像'); } foreach (explode('/', $key) as $seg) { if ($seg === '' || $seg === '.' || $seg[0] === '.') { nav_av_fail(404, '没有这个头像'); } } $familyId = (int)($p['family_id'] ?? 0); $res = Storage::read($familyId, $key); $status = (int)($res['status'] ?? 0); $body = (string)($res['body'] ?? ''); if (empty($res['ok']) || $status < 200 || $status >= 300) { nav_av_fail($status === 404 || $status === 0 ? 404 : 502, $status === 404 || $status === 0 ? '没有这个头像' : '头像暂时取不到'); } $mime = (new \finfo(FILEINFO_MIME_TYPE))->buffer($body) ?: ''; if ($mime === '' || $mime === 'application/octet-stream' || strpos($mime, 'text/html') !== false || strpos($mime, 'javascript') !== false) { $mime = 'image/jpeg'; // 头像只可能是位图;嗅探失败按图片给 } if (strpos($mime, 'svg') !== false) { $mime = 'image/svg+xml'; } $etag = '"' . md5($key . '|' . strlen($body) . '|' . $mime) . '"'; if (trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? '')) === $etag) { http_response_code(304); header('ETag: ' . $etag); header('Cache-Control: public, max-age=86400'); exit; } http_response_code(200); header('Content-Type: ' . $mime); header('Content-Length: ' . strlen($body)); // ⚠️ 只缓存 1 天(不是 immutable):头像可以被本人随时换掉, // 而这里的 URL 不带版本号(键就是那个短语,换头像时短语不变)。 header('Cache-Control: public, max-age=86400'); header('ETag: ' . $etag); header('X-Content-Type-Options: nosniff'); if ($mime === 'image/svg+xml') { header("Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox"); } echo $body; exit;