<?php
declare(strict_types=1);
/**
 * 微信图片上传 · PHP + HTML 单文件
 *
 * 部署：把本文件放入已启用 PHP 的网站目录，再用浏览器打开。
 * 运行：PHP 7.4+（建议 PHP 8.x），启用 curl、fileinfo、session 扩展。
 * 本文件自带你提供的默认 Cookie，只保存在 PHP 代码中，不输出到网页。
 * Cookie 失效后替换下面 $defaultCookie；Referer 的 token 变动时同步更新。
 * 可用环境变量覆盖：WX_UPLOAD_COOKIE、WX_UPLOAD_REFERER、WX_UPLOAD_PASSWORD。
 * 如需限制访问，设置 $config['upload_password']，页面会出现上传口令输入框。
 *
 * 实际链路：用户浏览器选文件 → 本 PHP → 微信接口。服务器会接收并转发文件。
 * 普通网页 JavaScript 无法任意指定微信 Cookie / Origin；不是浏览器直传微信。
 * 临时文件由 PHP 接收，转发结束删除；程序不建立文件目录、不保存上传历史。
 * 页面默认最多 20 MiB；还受 PHP / Web 服务器 / 微信端实际限制约束。
 * 例如 php.ini：upload_max_filesize = 20M；post_max_size = 24M。
 * Nginx 可设置 client_max_body_size 24m；反向代理超时需大于 130 秒。
 * 页面须通过 PHP 服务访问，不要双击，也不要改成 .html。
 *
 * 必须交给 PHP 执行，勿以静态源码形式公开此文件；其中含登录 Cookie。
 * HTTP Content-Length、multipart boundary、Accept-Encoding 由 cURL 生成。
 */

// ==================== 服务端配置 ====================
$defaultCookie = '把你的下面这个域名的进去登录一下浏览器F12抓接口请求的包只要有比较长的cookie直接一股脑不带cookie这个标题把里面的内容复制过来把这段文字换掉即可';
$defaultReferer = 'https://mp.weixin.qq.com/wxamp/appmsgtopic/create_topic_page?topic=%E6%89%93%E5%8D%B0%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF~&topic_source=1&token=257553162&lang=zh_CN';
$config = [
    'cookie' => getenv('WX_UPLOAD_COOKIE') !== false ? (string) getenv('WX_UPLOAD_COOKIE') : $defaultCookie,
    'referer' => getenv('WX_UPLOAD_REFERER') !== false ? (string) getenv('WX_UPLOAD_REFERER') : $defaultReferer,
    'upload_password' => getenv('WX_UPLOAD_PASSWORD') !== false ? (string) getenv('WX_UPLOAD_PASSWORD') : '',
    'max_bytes' => 20 * 1024 * 1024,
    'connect_timeout' => 15,
    'timeout' => 120,
];
// ==================== 配置结束 ====================

ini_set('display_errors', '0');
header('Referrer-Policy: no-referrer');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');

function reply(array $body, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=UTF-8');
    echo json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
    exit;
}

function fail(string $message, int $status = 400, array $extra = []): void
{
    reply(array_merge(['ok' => false, 'message' => $message], $extra), $status);
}

function postText(string $key, string $fallback = ''): string
{
    return isset($_POST[$key]) && is_string($_POST[$key]) ? $_POST[$key] : $fallback;
}

function iniBytes(string $value): int
{
    $value = trim($value);
    if ($value === '' || (float) $value <= 0) {
        return 0;
    }
    $multipliers = ['k' => 1024, 'm' => 1048576, 'g' => 1073741824];
    return (int) ((float) $value * ($multipliers[strtolower(substr($value, -1))] ?? 1));
}

function validImageUrl(string $url): bool
{
    if (!filter_var($url, FILTER_VALIDATE_URL)) {
        return false;
    }
    $parts = parse_url($url);
    return is_array($parts)
        && in_array(strtolower($parts['scheme'] ?? ''), ['http', 'https'], true)
        && !isset($parts['user']) && !isset($parts['pass']);
}

function forwardImage(array $config, string $temporary, string $filename, string $mime, int $size): array
{
    if (!extension_loaded('curl') || !class_exists('CURLFile')) {
        return ['ok' => false, 'message' => '服务器未启用 PHP cURL 扩展，请启用后重试。'];
    }
    $cookie = trim($config['cookie']);
    $referer = trim($config['referer']);
    if ($cookie === '') {
        return ['ok' => false, 'message' => '服务端尚未配置微信 Cookie。'];
    }
    if (preg_match('/[\r\n\x00]/', $cookie . $referer)) {
        return ['ok' => false, 'message' => '服务端 Cookie / Referer 中含换行或控制字符，请检查配置。'];
    }

    $lastModified = postText('lastModifiedDate');
    if ($lastModified === '' || strlen($lastModified) > 160 || preg_match('/[\r\n\x00]/', $lastModified)) {
        $lastModified = (new DateTimeImmutable('now', new DateTimeZone('Asia/Shanghai')))
            ->format('D M d Y H:i:s') . ' GMT+0800 (中国标准时间)';
    }

    // size / name / type 来自实际文件，不信任客户端同名文本字段。
    // CURLFile 会把临时文件的二进制写入 multipart 的 file 字段。
    $fields = [
        'id' => 'WU_FILE_0',
        'name' => $filename,
        'type' => $mime,
        'lastModifiedDate' => $lastModified,
        'size' => (string) $size,
        'file' => new CURLFile($temporary, $mime, $filename),
    ];
    $headers = [
        'Accept: */*',
        'Accept-Language: zh-CN,zh;q=0.8',
        'Cache-Control: no-cache',
        'Origin: https://mp.weixin.qq.com',
        'Pragma: no-cache',
        'Priority: u=1, i',
        'Sec-CH-UA: "Not)A;Brand";v="8", "Chromium";v="138", "Brave";v="138"',
        'Sec-CH-UA-Mobile: ?0',
        'Sec-CH-UA-Platform: "Windows"',
        'Sec-Fetch-Dest: empty',
        'Sec-Fetch-Mode: cors',
        'Sec-Fetch-Site: same-origin',
        'Sec-GPC: 1',
        'Traceparent: 00-' . bin2hex(random_bytes(16)) . '-' . bin2hex(random_bytes(8)) . '-01',
        'Tracestate: appid=wxa_otel',
        'Expect:',
    ];
    $body = '';
    $responseTooLarge = false;
    $handle = curl_init('https://mp.weixin.qq.com/cgi-bin/filetransfer?action=upload_cdn&f=json');
    if ($handle === false) {
        return ['ok' => false, 'message' => '无法初始化服务器上传连接。'];
    }
    try {
        curl_setopt_array($handle, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $fields,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_COOKIE => $cookie,
            CURLOPT_REFERER => $referer,
            CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
            // 空字符串只声明当前 libcurl 真正支持的压缩方式，并自动解压。
            CURLOPT_ENCODING => '',
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_CONNECTTIMEOUT => $config['connect_timeout'],
            CURLOPT_TIMEOUT => $config['timeout'],
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            // 固定 HTTPS 目的地址；不接受前端指定任意上游地址。
            CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
            CURLOPT_WRITEFUNCTION => static function ($ch, string $chunk) use (&$body, &$responseTooLarge): int {
                if (strlen($body) + strlen($chunk) > 1024 * 1024) {
                    $responseTooLarge = true;
                    return 0;
                }
                $body .= $chunk;
                return strlen($chunk);
            },
        ]);
        $executed = curl_exec($handle);
        $errorNumber = curl_errno($handle);
        $httpStatus = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE);
    } finally {
        curl_close($handle);
    }

    if ($responseTooLarge) {
        return ['ok' => false, 'message' => '微信接口返回内容异常过大，已停止读取。'];
    }
    if ($executed === false) {
        $messages = [
            6 => '服务器无法解析微信域名，请检查 DNS。',
            7 => '服务器无法连接微信接口，请检查网络。',
            28 => '微信接口连接或上传超时，请稍后重试。',
            35 => '服务器与微信接口的 TLS 连接失败。',
            60 => '服务器 CA 证书校验失败，请更新 CA 证书配置。',
        ];
        return ['ok' => false, 'message' => $messages[$errorNumber] ?? ('转发失败，cURL 错误码：' . $errorNumber), 'curl_errno' => $errorNumber];
    }

    // 保留上游业务 JSON；绝不把请求 Cookie / 请求头返回前端。
    $decoded = json_decode(ltrim($body, "\xEF\xBB\xBF \t\r\n"), true);
    $isJson = json_last_error() === JSON_ERROR_NONE && is_array($decoded);
    $extra = ['http_status' => $httpStatus];
    if ($isJson) {
        $extra['upstream'] = $decoded;
    } else {
        // 非 JSON 内容仅作文本展示，截断避免巨大登录页影响页面。
        $extra['upstream_text'] = substr($body, 0, 8000);
    }
    if ($httpStatus < 200 || $httpStatus >= 300) {
        return array_merge(['ok' => false, 'message' => '微信接口 HTTP ' . $httpStatus . '，请检查登录状态或服务端网络。'], $extra);
    }
    if (!$isJson) {
        return array_merge(['ok' => false, 'message' => '微信未返回有效 JSON，可能是登录过期、验证页面或接口响应变化。'], $extra);
    }
    $base = $decoded['base_resp'] ?? null;
    $hasRet = is_array($base) && array_key_exists('ret', $base);
    $ret = $hasRet ? $base['ret'] : null;
    if (!$hasRet || !in_array($ret, [0, '0'], true)) {
        $retLabel = is_scalar($ret) ? (string) $ret : '缺失';
        $err = is_array($base) && is_string($base['err_msg'] ?? null) ? $base['err_msg'] : '返回格式异常';
        return array_merge(['ok' => false, 'message' => '微信上传失败（ret=' . $retLabel . '）：' . $err . '。请检查 Cookie / 登录状态。'], $extra);
    }
    $url = isset($decoded['content']) && is_string($decoded['content']) ? $decoded['content'] : '';
    if (!validImageUrl($url)) {
        return array_merge(['ok' => false, 'message' => '接口 ret=0，但 content 中没有有效的 HTTP/HTTPS 图片链接。'], $extra);
    }
    return array_merge([
        'ok' => true,
        'message' => '上传成功',
        'url' => $url,
        // 保留原始链接；预览使用 HTTPS，避免 HTTPS 页面加载 HTTP 图片被拦截。
        'preview_url' => preg_replace('/^http:/i', 'https:', $url),
        'filename' => $filename,
        'size' => $size,
    ], $extra);
}

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if (!in_array($method, ['GET', 'HEAD', 'POST'], true)) {
    header('Allow: GET, HEAD, POST');
    fail('不支持该请求方法。', 405);
}
if (!function_exists('session_start')) {
    fail('服务器未启用 PHP session 扩展。', 500);
}
ini_set('session.use_strict_mode', '1');
session_name('wximg_upload_session');
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'secure' => !empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off',
    'httponly' => true,
    'samesite' => 'Strict',
]);
if (!session_start()) {
    fail('服务器会话无法启动，请检查 PHP session 存储权限。', 500);
}
if (!isset($_SESSION['wximg_csrf'])) {
    $_SESSION['wximg_csrf'] = bin2hex(random_bytes(32));
}
$csrf = (string) $_SESSION['wximg_csrf'];
session_write_close();

$postLimit = iniBytes((string) ini_get('post_max_size'));
$phpFileLimit = iniBytes((string) ini_get('upload_max_filesize'));
$effectiveLimit = (int) $config['max_bytes'];
if ($phpFileLimit > 0) {
    $effectiveLimit = min($effectiveLimit, $phpFileLimit);
}
if ($postLimit > 0) {
    $effectiveLimit = min($effectiveLimit, max(0, $postLimit - 65536));
}
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'];

if ($method === 'POST') {
    if ($postLimit > 0 && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > $postLimit) {
        fail('文件超过服务器 post_max_size，请缩小图片或调高 PHP 限制。', 413);
    }
    if (postText('action') !== 'upload') {
        fail('无效操作。');
    }
    if (!hash_equals($csrf, postText('csrf'))) {
        fail('页面会话已过期，请刷新后重新上传。', 403);
    }
    if ($config['upload_password'] !== '' && !hash_equals($config['upload_password'], postText('upload_password'))) {
        fail('上传口令不正确。', 403);
    }
    $uploaded = $_FILES['file'] ?? null;
    if (!is_array($uploaded) || !isset($uploaded['error']) || !is_int($uploaded['error'])) {
        fail('请选择一张图片，file 字段必须为单个二进制文件。');
    }
    $uploadErrors = [
        UPLOAD_ERR_INI_SIZE => '文件超过 PHP upload_max_filesize 限制。',
        UPLOAD_ERR_FORM_SIZE => '文件超过页面允许的大小。',
        UPLOAD_ERR_PARTIAL => '文件只上传了一部分，请重试。',
        UPLOAD_ERR_NO_FILE => '没有选择文件。',
        UPLOAD_ERR_NO_TMP_DIR => '服务器缺少临时上传目录。',
        UPLOAD_ERR_CANT_WRITE => '服务器无法写入临时文件，请检查权限和磁盘空间。',
        UPLOAD_ERR_EXTENSION => 'PHP 扩展中断了上传。',
    ];
    if ($uploaded['error'] !== UPLOAD_ERR_OK) {
        $oversize = in_array($uploaded['error'], [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true);
        fail($uploadErrors[$uploaded['error']] ?? '文件接收失败。', $oversize ? 413 : 400);
    }
    $temporary = $uploaded['tmp_name'] ?? '';
    if (!is_string($temporary) || !is_uploaded_file($temporary)) {
        fail('文件临时路径无效。');
    }
    try {
        $size = (int) filesize($temporary);
        if ($size < 1 || $size > $effectiveLimit) {
            $result = ['ok' => false, 'message' => '图片为空或超过当前站点大小限制。'];
            $status = 413;
        } elseif (!extension_loaded('fileinfo')) {
            $result = ['ok' => false, 'message' => '服务器未启用 PHP fileinfo 扩展，请启用后重试。'];
            $status = 500;
        } else {
            $mime = (string) (new finfo(FILEINFO_MIME_TYPE))->file($temporary);
            if ($mime === 'image/x-ms-bmp') {
                $mime = 'image/bmp';
            }
            if (!in_array($mime, $allowedTypes, true)) {
                $result = ['ok' => false, 'message' => '请选择 JPG、PNG、GIF、WebP 或 BMP 图片，微信端支持情况以接口返回为准。'];
                $status = 415;
            } else {
                $originalName = isset($uploaded['name']) && is_string($uploaded['name']) ? $uploaded['name'] : 'image';
                $filename = basename(str_replace('\\', '/', $originalName));
                $filename = preg_replace('/[\x00-\x1F\x7F"\\\\]/', '_', $filename) ?? 'image';
                if ($filename === '' || $filename === '.' || $filename === '..') {
                    $filename = 'image';
                }
                @set_time_limit($config['timeout'] + 10);
                $result = forwardImage($config, $temporary, $filename, $mime, $size);
                $status = $result['ok'] ? 200 : 502;
            }
        }
    } catch (Throwable $error) {
        // 不向浏览器输出堆栈、Cookie 或服务端本地路径。
        $result = ['ok' => false, 'message' => '服务器处理异常，请检查 PHP 扩展和配置后重试。'];
        $status = 500;
    } finally {
        // 无永久存储；请求结束时 PHP 也会清理未移动的上传临时文件。
        if (is_file($temporary)) {
            @unlink($temporary);
        }
    }
    reply($result, $status);
}

header('Content-Type: text/html; charset=UTF-8');
$pageConfig = json_encode([
    'csrf' => $csrf,
    'maxBytes' => $effectiveLimit,
    'timeoutMs' => ($config['timeout'] + 180) * 1000,
    'allowedTypes' => $allowedTypes,
], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE);
?>
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="referrer" content="no-referrer">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>图片上传</title>
    <style>
        :root{color-scheme:light;--green:#087f5b;--ink:#16332b;--muted:#6d7e78;--line:#e2eae6;--soft:#f5f9f6}
        *{box-sizing:border-box}body{margin:0;font:15px/1.65 system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;color:var(--ink);background:#f3f6f3}
        button,input{font:inherit}button,a,input{outline-offset:4px}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.48}button:focus-visible,a:focus-visible,input:focus-visible{outline:3px solid #8ccbb3}
        [hidden]{display:none!important}.shell{max-width:980px;margin:0 auto;padding:48px 24px 32px}.topline{display:flex;align-items:center;gap:10px;color:var(--green);font-weight:750;font-size:14px;letter-spacing:1px}.mark{display:grid;place-items:center;width:32px;height:32px;border-radius:10px;background:var(--green);color:white}.mark svg{width:20px;height:20px}
        header{margin-bottom:27px}h1{font-size:34px;line-height:1.2;letter-spacing:-1px;margin:23px 0 10px}header p{margin:0;color:var(--muted)}.layout{display:grid;grid-template-columns:1.06fr 1fr;gap:20px;align-items:start}.card{background:white;border:1px solid var(--line);border-radius:20px;padding:24px;box-shadow:0 8px 32px #25483406}.cardhead{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px}.cardhead h2{font-size:16px;margin:0}.badge{font-size:11px;font-weight:600;color:#769085;background:var(--soft);padding:3px 9px;border-radius:20px;letter-spacing:1px}
        .drop{width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;border:1.5px dashed #b6cfc2;border-radius:14px;min-height:235px;padding:24px 16px;background:#f7faf8;color:var(--ink);transition:.15s}.drop:hover,.drop.dragging{background:#eaf5ee;border-color:var(--green)}.upload-icon{width:53px;height:53px;background:white;border:1px solid #dfebe3;border-radius:16px;display:grid;place-items:center;color:var(--green);margin-bottom:5px}.upload-icon svg{width:25px;height:25px}.drop strong{font-size:16px}.drop small{color:var(--muted);font-size:12px}.localpreview{width:100%;height:134px;object-fit:contain}.filename{font-size:13px;max-width:100%;overflow-wrap:anywhere;font-weight:650}.fileinfo{font-size:12px;color:var(--muted)}
        .primary{background:var(--green);color:white;border:0;border-radius:10px;padding:12px 18px;font-weight:650;width:100%;margin-top:18px}.secondary{border:1px solid var(--line);background:white;color:var(--ink);padding:8px 12px;border-radius:8px;font-size:13px;text-decoration:none;text-align:center}.secondary:hover{background:var(--soft)}.help{color:var(--muted);font-size:12px;margin:12px 0 0}.password{margin-top:16px}.password label{display:block;font-size:13px;margin-bottom:6px}.password input{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:8px}.progresswrap{margin-top:18px}.progressmeta{display:flex;justify-content:space-between;gap:12px;font-size:12px;color:var(--muted)}progress{width:100%;height:7px;appearance:none;border:0;border-radius:8px;background:#e8eee9;overflow:hidden}progress::-webkit-progress-bar{background:#e8eee9}progress::-webkit-progress-value{background:var(--green);border-radius:8px}progress::-moz-progress-bar{background:var(--green)}
        .status{margin-top:16px;padding:10px 12px;border-radius:9px;font-size:13px;background:#edf6f0;color:var(--green);overflow-wrap:anywhere}.status.error{background:#fff0ed;color:#b23e2e}.status.info{background:#eef3f6;color:#466279}.empty{height:316px;display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;color:var(--muted);gap:8px}.empty svg{width:47px;height:47px;color:#b8cbc0}.empty strong{color:#83968d;font-size:14px}.empty p{font-size:12px;margin:0}.resultimage{width:100%;height:202px;object-fit:contain;background:var(--soft);border-radius:11px;border:1px solid var(--line)}.imageerror{background:#fff6e9;padding:14px;border-radius:8px;font-size:12px;color:#846436}.label{font-size:12px;display:block;color:var(--muted);margin:13px 0 6px}.url{width:100%;border:1px solid var(--line);background:#fafcfb;border-radius:8px;padding:10px;color:#3b5c4e;font-size:12px}.actions{display:flex;gap:8px;margin-top:10px}.actions>*{flex:1}details{margin-top:18px;border-top:1px solid var(--line);padding-top:14px}summary{cursor:pointer;font-size:12px;color:var(--muted)}pre{font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere;background:#f6f8f6;border-radius:10px;padding:13px;max-height:250px;overflow:auto;color:#41544b}.footer{text-align:center;color:#91a198;font-size:11px;margin-top:23px}noscript{display:block;padding:14px;background:#fff0ed;color:#b23e2e}
        @media(max-width:700px){.shell{padding:26px 16px}.layout{grid-template-columns:1fr}h1{font-size:29px}.card{padding:20px}.empty{height:130px}.drop{min-height:215px}.cardhead{margin-bottom:15px}}
    </style>
</head>
<body>
<main class="shell">
    <header>
        <div class="topline"><span class="mark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="m4 16 5-5 4 4 3-3 4 4"/><circle cx="16.5" cy="8.5" r="1"/></svg></span> 图片上传</div>
        <h1>选一张图片，获取链接。</h1>
        <p>选择或拖入图片，上传后即可预览和复制图片地址。</p>
    </header>
    <noscript>请启用浏览器 JavaScript 后使用上传功能。</noscript>
    <div class="layout">
        <section class="card" aria-labelledby="uploadTitle">
            <div class="cardhead"><h2 id="uploadTitle">选择图片</h2><span class="badge">01 / UPLOAD</span></div>
            <form id="uploadForm" enctype="multipart/form-data" method="post">
                <input type="file" id="fileInput" name="file" accept="image/jpeg,image/png,image/gif,image/webp,image/bmp,.jpg,.jpeg,.png,.gif,.webp,.bmp" hidden>
                <button type="button" class="drop" id="dropZone" aria-label="选择或拖入一张图片">
                    <span class="upload-icon" id="uploadIcon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V3m-5 5 5-5 5 5M4 15v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4"/></svg></span>
                    <img id="localPreview" class="localpreview" alt="已选图片预览" hidden>
                    <strong id="dropTitle">点击选择，或拖拽到这里</strong>
                    <small id="dropHint">JPG · PNG · GIF · WebP · BMP</small>
                    <span id="fileName" class="filename" hidden></span>
                    <span id="fileInfo" class="fileinfo">正在读取上传限制…</span>
                </button>
                <?php if ($config['upload_password'] !== ''): ?>
                <div class="password"><label for="uploadPassword">上传口令</label><input type="password" id="uploadPassword" autocomplete="off" required placeholder="请输入上传口令"></div>
                <?php endif; ?>
                <button class="primary" type="submit" id="uploadButton" disabled>开始上传</button>
                <div class="progresswrap" id="progressWrap" hidden>
                    <div class="progressmeta"><span id="progressLabel">正在上传</span><span id="progressText">0%</span></div>
                    <progress id="progress" max="100" value="0" aria-label="上传进度"></progress>
                </div>
                <div class="status" id="status" role="status" aria-live="polite" hidden></div>
                <p class="help" id="limitHint"></p>
            </form>
        </section>
        <section class="card" aria-labelledby="resultTitle">
            <div class="cardhead"><h2 id="resultTitle">上传结果</h2><span class="badge">02 / RESULT</span></div>
            <div class="empty" id="emptyState"><svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.3" aria-hidden="true"><rect x="7" y="6" width="34" height="36" rx="6"/><path d="m8 32 10-10 8 8 5-5 10 10"/><circle cx="31" cy="16" r="3"/></svg><strong>图片链接会显示在这里</strong><p>上传完成后，一键复制并使用。</p></div>
            <div id="successResult" hidden>
                <img class="resultimage" id="resultImage" alt="上传成功的图片" referrerpolicy="no-referrer">
                <div class="imageerror" id="imageError" hidden>已取得链接，但浏览器暂时无法显示远程图片。你仍可以复制链接或打开查看。</div>
                <label class="label" for="resultUrl">图片地址</label>
                <input class="url" id="resultUrl" readonly spellcheck="false" aria-label="上传后的图片地址">
                <div class="actions"><button type="button" class="secondary" id="copyButton">复制链接</button><a class="secondary" id="openLink" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">打开原图</a></div>
            </div>
            <details id="responseDetails" hidden><summary>查看完整接口返回</summary><pre id="rawResponse"></pre></details>
        </section>
    </div>
    <p class="footer">WECHAT IMAGE UPLOAD</p>
</main>
<script>
'use strict';
const CONFIG = <?= $pageConfig ?>;
const $ = id => document.getElementById(id);
let selectedFile = null;
let previewObjectUrl = null;
let isUploading = false;
let uploadCounter = 0;
let copyTimer = null;
const fileInput = $('fileInput');
const dropZone = $('dropZone');
const formatBytes = bytes => bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)} MB` : `${(bytes / 1024).toFixed(1)} KB`;
$('fileInfo').textContent = `单张最多 ${formatBytes(CONFIG.maxBytes)}`;
$('limitHint').textContent = `当前站点上限 ${formatBytes(CONFIG.maxBytes)}，上传结果以接口返回为准。`;

function showStatus(message, kind = 'info') {
    $('status').textContent = message;
    $('status').className = `status ${kind}`;
    $('status').hidden = false;
}

function clearResult() {
    $('emptyState').hidden = false;
    $('successResult').hidden = true;
    $('responseDetails').hidden = true;
    $('responseDetails').open = false;
    $('rawResponse').textContent = '';
    $('resultUrl').value = '';
    $('resultImage').removeAttribute('src');
    $('openLink').removeAttribute('href');
    $('imageError').hidden = true;
    clearTimeout(copyTimer);
    $('copyButton').textContent = '复制链接';
}

function selectFile(file) {
    if (!file || isUploading) return;
    selectedFile = null;
    $('uploadButton').disabled = true;
    clearResult();
    if (previewObjectUrl) { URL.revokeObjectURL(previewObjectUrl); previewObjectUrl = null; }
    $('localPreview').removeAttribute('src');
    $('localPreview').hidden = true;
    $('uploadIcon').hidden = false;
    $('dropTitle').hidden = false;
    $('dropHint').hidden = false;
    $('fileName').hidden = true;
    $('fileInfo').textContent = `单张最多 ${formatBytes(CONFIG.maxBytes)}`;
    if (file.size < 1 || file.size > CONFIG.maxBytes) {
        showStatus(`请选择非空且不超过 ${formatBytes(CONFIG.maxBytes)} 的图片。`, 'error');
        return;
    }
    const extensionOk = /\.(jpe?g|png|gif|webp|bmp)$/i.test(file.name);
    if (!(CONFIG.allowedTypes.includes(file.type) || file.type === 'image/x-ms-bmp' || (!file.type && extensionOk))) {
        showStatus('请选择 JPG、PNG、GIF、WebP 或 BMP 图片。', 'error');
        return;
    }
    selectedFile = file;
    $('status').hidden = true;
    $('progressWrap').hidden = true;
    previewObjectUrl = URL.createObjectURL(file);
    $('localPreview').src = previewObjectUrl;
    $('localPreview').hidden = false;
    $('uploadIcon').hidden = true;
    $('dropTitle').hidden = true;
    $('dropHint').hidden = true;
    $('fileName').textContent = file.name;
    $('fileName').hidden = false;
    $('fileInfo').textContent = `${formatBytes(file.size)} · 点击更换图片`;
    $('uploadButton').disabled = false;
    $('uploadButton').textContent = '开始上传';
}

dropZone.addEventListener('click', () => {
    if (isUploading) return;
    fileInput.value = '';
    fileInput.click();
});
fileInput.addEventListener('change', () => selectFile(fileInput.files[0]));
['dragenter', 'dragover'].forEach(name => dropZone.addEventListener(name, event => {
    event.preventDefault();
    if (!isUploading) dropZone.classList.add('dragging');
}));
['dragleave', 'drop'].forEach(name => dropZone.addEventListener(name, event => {
    event.preventDefault();
    dropZone.classList.remove('dragging');
}));
dropZone.addEventListener('drop', event => {
    if (isUploading) return;
    if (event.dataTransfer.files.length > 1) {
        showStatus('一次请选择一张图片。', 'error');
        return;
    }
    selectFile(event.dataTransfer.files[0]);
});
document.addEventListener('dragover', event => event.preventDefault());
document.addEventListener('drop', event => event.preventDefault());
$('localPreview').addEventListener('error', () => { $('localPreview').hidden = true; });

function safeUrl(value) {
    try {
        const url = new URL(value);
        return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password ? url.href : null;
    } catch { return null; }
}

function showResponse(data, httpStatus) {
    $('responseDetails').hidden = false;
    $('rawResponse').textContent = data.upstream !== undefined
        ? JSON.stringify(data.upstream, null, 2)
        : data.upstream_text || JSON.stringify(data, null, 2);
    if (httpStatus < 200 || httpStatus >= 300 || data.ok !== true || !safeUrl(data.url)) {
        showStatus(data.message || '上传失败，请展开接口返回查看详情。', 'error');
        $('responseDetails').open = true;
        return;
    }
    $('emptyState').hidden = true;
    $('successResult').hidden = false;
    $('resultUrl').value = data.url;
    $('resultImage').hidden = false;
    $('resultImage').src = safeUrl(data.preview_url) || safeUrl(data.url);
    $('openLink').href = safeUrl(data.preview_url) || safeUrl(data.url);
    showStatus('上传成功，图片链接已生成。', 'success');
    $('uploadButton').textContent = '再次上传';
}

$('resultImage').addEventListener('error', () => {
    if (!$('resultImage').hasAttribute('src')) return;
    $('resultImage').hidden = true;
    $('imageError').hidden = false;
});
$('resultImage').addEventListener('load', () => {
    $('resultImage').hidden = false;
    $('imageError').hidden = true;
});

$('uploadForm').addEventListener('submit', event => {
    event.preventDefault();
    if (!selectedFile || isUploading) return;
    isUploading = true;
    clearResult();
    const generation = ++uploadCounter;
    const button = $('uploadButton');
    button.disabled = true;
    button.textContent = '正在上传…';
    dropZone.disabled = true;
    fileInput.disabled = true;
    $('progressWrap').hidden = false;
    $('progress').value = 0;
    $('progressLabel').textContent = '正在上传图片';
    $('progressText').textContent = '0%';
    showStatus('正在上传，请保持页面打开。');

    const form = new FormData();
    form.append('action', 'upload');
    form.append('csrf', CONFIG.csrf);
    form.append('id', 'WU_FILE_0');
    form.append('name', selectedFile.name);
    form.append('type', selectedFile.type || 'application/octet-stream');
    form.append('lastModifiedDate', new Date(selectedFile.lastModified || Date.now()).toString());
    form.append('size', String(selectedFile.size));
    form.append('file', selectedFile, selectedFile.name);
    if ($('uploadPassword')) form.append('upload_password', $('uploadPassword').value);

    const xhr = new XMLHttpRequest();
    const endpoint = new URL(window.location.href);
    endpoint.hash = '';
    xhr.open('POST', endpoint.href, true);
    xhr.timeout = CONFIG.timeoutMs;
    xhr.setRequestHeader('Accept', 'application/json');
    // 不设置 Content-Type / boundary / Content-Length / Cookie / Origin。
    // FormData 让浏览器正确生成 multipart；微信请求头由 PHP 服务端设置。
    xhr.upload.addEventListener('progress', event => {
        if (event.lengthComputable) {
            const percent = Math.min(100, Math.round(event.loaded / event.total * 100));
            $('progress').value = percent;
            $('progressText').textContent = `${percent}%`;
        }
    });
    xhr.upload.addEventListener('load', () => {
        $('progress').value = 100;
        $('progressLabel').textContent = '文件已接收，等待接口返回';
        $('progressText').textContent = '处理中…';
        showStatus('文件已上传，正在等待图片链接。');
    });
    xhr.addEventListener('load', () => {
        try {
            const data = JSON.parse(xhr.responseText);
            if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('invalid response');
            showResponse(data, xhr.status);
        } catch {
            const message = xhr.status === 413
                ? '文件被服务器拒绝：请调高 Nginx / Apache 和 PHP 的上传大小限制。'
                : `服务器没有返回有效 JSON（HTTP ${xhr.status}），请检查 PHP 运行环境。`;
            showStatus(message, 'error');
            $('responseDetails').hidden = false;
            $('responseDetails').open = true;
            $('rawResponse').textContent = xhr.responseText.slice(0, 8000) || '响应内容为空。';
        }
    });
    xhr.addEventListener('error', () => showStatus('连接服务器失败，请检查网络后重试。', 'error'));
    xhr.addEventListener('timeout', () => showStatus('请求超时，上游可能仍在处理，请稍后检查再决定是否重试。', 'error'));
    xhr.addEventListener('loadend', () => {
        if (generation !== uploadCounter) return;
        isUploading = false;
        button.disabled = !selectedFile;
        if ($('successResult').hidden) button.textContent = '重新上传';
        dropZone.disabled = false;
        fileInput.disabled = false;
        $('progressWrap').hidden = true;
    });
    xhr.send(form);
});

$('copyButton').addEventListener('click', async () => {
    const url = $('resultUrl').value;
    if (!url) return;
    let copied = false;
    if (navigator.clipboard && window.isSecureContext) {
        try { await navigator.clipboard.writeText(url); copied = true; } catch {}
    }
    if (!copied) {
        $('resultUrl').focus();
        $('resultUrl').select();
        $('resultUrl').setSelectionRange(0, url.length);
        try { copied = document.execCommand('copy'); } catch {}
    }
    $('copyButton').textContent = copied ? '已复制 ✓' : '请手动复制';
    if (!copied) showStatus('链接已选中，请按 Ctrl+C 或长按复制。');
    clearTimeout(copyTimer);
    copyTimer = setTimeout(() => { $('copyButton').textContent = '复制链接'; }, 1800);
});
window.addEventListener('beforeunload', () => {
    if (previewObjectUrl) URL.revokeObjectURL(previewObjectUrl);
});
</script>
</body>
</html>
