<?php
declare(strict_types=1);

$config = require __DIR__ . '/config.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    @ini_set('display_errors', '0');
    @ini_set('html_errors', '0');
    set_error_handler(function ($severity, $message, $file, $line) {
        if (!(error_reporting() & $severity)) return false;
        throw new ErrorException($message, 0, $severity, $file, $line);
    });
    ob_start();
    try {
        handle_upload($config);
    } catch (Throwable $e) {
        ob_end_clean();
        json_out(500, [
            'error' => $e->getMessage(),
            'where' => basename($e->getFile()) . ':' . $e->getLine(),
        ]);
    }
    if (ob_get_level() > 0) ob_end_flush();
    exit;
}

render_page();


function detect_mime(string $path, string $ext): string
{
    if (class_exists('finfo')) {
        $f = new finfo(FILEINFO_MIME_TYPE);
        $m = $f->file($path);
        if ($m) return $m;
    }
    if (function_exists('mime_content_type')) {
        $m = @mime_content_type($path);
        if ($m) return $m;
    }
    $info = @getimagesize($path);
    if ($info && !empty($info['mime'])) return $info['mime'];
    // 兜底：getimagesize 识别不了的（svg/avif/heic）按扩展名映射
    $map = [
        'svg'  => 'image/svg+xml',
        'avif' => 'image/avif',
        'heic' => 'image/heic',
        'heif' => 'image/heif',
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
    ];
    return $map[$ext] ?? '';
}

function normalize_mime(string $mime): string
{
    $mime = strtolower(trim(explode(';', $mime, 2)[0]));
    if ($mime === '') return '';

    $known = [
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'application/vnd.openxmlformats-officedocument.presentationml.presentation',
        'application/zip',
        'application/octet-stream',
        'image/svg+xml',
        'image/jpeg',
        'image/png',
        'image/gif',
        'image/webp',
        'image/avif',
        'image/bmp',
        'image/heic',
        'image/heif',
    ];
    foreach ($known as $type) {
        if (strpos($mime, $type) !== false) return $type;
    }

    return $mime;
}

function is_ooxml_zip(string $path, string $ext): bool
{
    // 各类 OOXML 文档在 zip 里必含的主部件
    $parts = ['docx' => 'word/document.xml', 'pptx' => 'ppt/presentation.xml'];
    $main = $parts[$ext] ?? null;
    if ($main === null) return false;

    if (class_exists('ZipArchive')) {
        $zip = new ZipArchive();
        if ($zip->open($path) === true) {
            $ok = $zip->locateName('[Content_Types].xml') !== false
                && $zip->locateName($main) !== false;
            $zip->close();
            return $ok;
        }
    }

    $fh = @fopen($path, 'rb');
    if (!$fh) return false;
    // zip 本地文件头魔数 PK\x03\x04
    if (fread($fh, 4) !== "PK\x03\x04") { fclose($fh); return false; }
    // OOXML/docx 必含这些条目；zip 里条目名以明文存储（不被压缩），
    // 故可直接在字节流里查找。规范建议它是首个条目，一般在文件头部；
    // 保险起见头部找不到再扫文件尾部的中央目录区。
    rewind($fh);
    $head = (string)fread($fh, 65536);
    $has_content_types = strpos($head, '[Content_Types].xml') !== false;
    $has_word_document = strpos($head, $main) !== false;
    if (@filesize($path) > 65536) {
        fseek($fh, -65536, SEEK_END);
        $tail = (string)fread($fh, 65536);
        $has_content_types = $has_content_types || strpos($tail, '[Content_Types].xml') !== false;
        $has_word_document = $has_word_document || strpos($tail, $main) !== false;
    }
    fclose($fh);
    return $has_content_types && $has_word_document;
}

function json_out(int $code, array $data): void
{
    http_response_code($code);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}

function handle_upload(array $cfg): void
{
    $token = $_SERVER['HTTP_X_TOKEN'] ?? $_POST['token'] ?? '';
    if (!hash_equals((string)$cfg['token'], (string)$token)) {
        json_out(401, ['error' => '密钥错误']);
        return;
    }

    if (!isset($_FILES['file']) || !is_array($_FILES['file'])) {
        json_out(400, ['error' => '未收到文件']);
        return;
    }

    $f = $_FILES['file'];
    if ($f['error'] !== UPLOAD_ERR_OK) {
        $map = [
            UPLOAD_ERR_INI_SIZE   => '文件超过 php.ini 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 扩展中止',
        ];
        json_out(400, ['error' => $map[$f['error']] ?? ('上传错误: ' . $f['error'])]);
        return;
    }

    if ($f['size'] > $cfg['max_size']) {
        json_out(413, ['error' => '文件过大，最大 ' . round($cfg['max_size'] / 1024 / 1024) . ' MB']);
        return;
    }

    $ext = strtolower(pathinfo((string)$f['name'], PATHINFO_EXTENSION));
    if ($ext === 'jpeg') $ext = 'jpg';
    if (!in_array($ext, $cfg['allowed_ext'], true)) {
        json_out(415, ['error' => '不支持的扩展名: ' . $ext]);
        return;
    }

    $mime = normalize_mime(detect_mime($f['tmp_name'], $ext));
    if ($ext === 'docx' || $ext === 'pptx') {
        // docx/pptx 是 zip 容器，libmagic 在不同版本/不同文件上可能识别为正式 OOXML
        // MIME，也可能只识别成 application/zip 甚至 application/octet-stream。
        // 因此放宽 MIME 判断，改用 zip 结构特征确认它确实是 Word/PowerPoint 文档，
        // 避免任意 zip 改名冒充。
        $docx_mimes = [
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            'application/zip', 'application/octet-stream', '',
        ];
        if (!in_array($mime, $docx_mimes, true) || !is_ooxml_zip($f['tmp_name'], $ext)) {
            json_out(415, ['error' => '不是有效的 ' . $ext . ' 文件: ' . ($mime ?: '未知')]);
            return;
        }
    } elseif (!in_array($mime, $cfg['allowed_mime'], true)) {
        json_out(415, ['error' => '不支持的文件类型: ' . ($mime ?: '未知')]);
        return;
    }

    if ($ext === 'svg') {
        $content = file_get_contents($f['tmp_name']);
        if ($content === false || preg_match('/<script\b|on\w+\s*=|javascript:|<!ENTITY/i', $content)) {
            json_out(400, ['error' => 'SVG 内含脚本或外部实体，已拒绝']);
            return;
        }
    }

    $hash = hash_file('sha256', $f['tmp_name']);
    $shortcode = substr($hash, 0, 10);
    $year = date('Y');
    $month = date('m');
    $rel_dir = "uploads/$year/$month";
    $abs_dir = __DIR__ . "/$rel_dir";

    if (!is_dir($abs_dir) && !mkdir($abs_dir, 0755, true) && !is_dir($abs_dir)) {
        json_out(500, ['error' => '目录创建失败']);
        return;
    }

    $rel_path = "$rel_dir/$shortcode.$ext";
    $abs_path = __DIR__ . "/$rel_path";

    if (!file_exists($abs_path)) {
        if (!move_uploaded_file($f['tmp_name'], $abs_path)) {
            json_out(500, ['error' => '保存失败']);
            return;
        }
        @chmod($abs_path, 0644);
    }

    $base = rtrim((string)$cfg['base_url'], '/');
    if ($base === '') {
        $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $base = $scheme . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
    }
    $url = $base . '/' . $rel_path;

    // 图片给图片语法，其它（如 docx）给普通链接语法
    $is_image = strncmp($mime, 'image/', 6) === 0;
    if ($is_image) {
        $markdown = "![]($url)";
        $html     = '<img src="' . $url . '" alt="" />';
    } else {
        $safe_name = htmlspecialchars((string)$f['name'], ENT_QUOTES);
        $markdown  = '[' . $f['name'] . '](' . $url . ')';
        $html      = '<a href="' . $url . '">' . $safe_name . '</a>';
    }

    json_out(200, [
        'url'      => $url,
        'markdown' => $markdown,
        'html'     => $html,
        'name'     => $f['name'],
        'size'     => $f['size'],
        'is_image' => $is_image,
    ]);
}

function render_page(): void
{
    header('Content-Type: text/html; charset=utf-8');
    ?><!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>图床</title>
<style>
  :root {
    --bg: #0f1115; --panel: #181b22; --line: #262a33;
    --fg: #e6e8ee; --muted: #8b93a7; --accent: #4f8cff; --ok: #3ecf8e; --err: #ff5d5d;
  }
  * { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg);
    font: 14px/1.6 -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
  .wrap { max-width: 760px; margin: 0 auto; padding: 48px 20px 80px; }
  h1 { font-size: 22px; font-weight: 600; margin: 0 0 4px; }
  .sub { color: var(--muted); font-size: 13px; margin-bottom: 28px; }
  .drop {
    display: block;
    border: 2px dashed var(--line); border-radius: 12px;
    padding: 56px 20px; text-align: center; color: var(--muted);
    transition: .15s; cursor: pointer; background: var(--panel);
  }
  .drop:hover, .drop.over { border-color: var(--accent); color: var(--fg); }
  .drop strong { color: var(--fg); display: block; font-size: 15px; margin-bottom: 6px; }
  .drop span { display: block; }
  .drop kbd { background: #20242d; border: 1px solid var(--line); border-radius: 4px;
    padding: 1px 6px; font-size: 12px; color: var(--fg); }
  input[type=file] { display: none; }
  .list { margin-top: 24px; display: flex; flex-direction: column; gap: 12px; }
  .card {
    background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
    padding: 14px; display: grid; grid-template-columns: 72px 1fr; gap: 14px;
  }
  .card.err { border-color: var(--err); }
  .thumb {
    width: 72px; height: 72px; border-radius: 6px; object-fit: cover;
    background: #0a0c10; border: 1px solid var(--line);
  }
  .thumb-doc { display: flex; align-items: center; justify-content: center;
    font-size: 12px; font-weight: 600; color: var(--accent); letter-spacing: .5px; }
  .meta { min-width: 0; display: flex; flex-direction: column; gap: 6px; }
  .meta .name { font-size: 13px; color: var(--muted); white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; }
  .row { display: flex; gap: 6px; align-items: center; min-width: 0; }
  .row code {
    flex: 1 1 0; min-width: 0; background: #0a0c10; border: 1px solid var(--line);
    padding: 6px 8px; border-radius: 5px; font-size: 12px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #cfd4e0;
    font-family: "SF Mono", Consolas, Menlo, monospace;
  }
  .btn {
    background: #20242d; border: 1px solid var(--line); color: var(--fg);
    padding: 6px 10px; border-radius: 5px; font-size: 12px; cursor: pointer;
    white-space: nowrap; transition: .15s;
  }
  .btn:hover { border-color: var(--accent); color: var(--accent); }
  .btn.copied { border-color: var(--ok); color: var(--ok); }
  .err-msg { color: var(--err); font-size: 13px; }
  .top-bar { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 28px; }
  .top-bar > div { min-width: 0; }
  .token-btn { background: none; border: none; color: var(--muted); font-size: 12px;
    cursor: pointer; padding: 4px 8px; }
  .token-btn:hover { color: var(--fg); }
</style>
</head>
<body>
<div class="wrap">
  <div class="top-bar">
    <div>
      <h1>图床</h1>
      <div class="sub">拖拽 / 粘贴 / 点击上传，自动生成直链</div>
    </div>
    <button class="token-btn" id="tokenBtn">修改密钥</button>
  </div>

  <label class="drop" id="drop">
    <strong>把图片、.docx 或 .pptx 拖到这里</strong>
    <span>或点击选择，或按 <kbd>Ctrl</kbd> + <kbd>V</kbd> 粘贴</span>
    <input type="file" id="file" accept="image/*,.docx,.pptx" multiple>
  </label>

  <div class="list" id="list"></div>
</div>

<script>
const TOKEN_KEY = 'pic-upload-token';

function getToken() {
  let t = localStorage.getItem(TOKEN_KEY);
  if (!t) {
    t = prompt('请输入上传密钥（仅首次需要，将保存在浏览器）：');
    if (t) localStorage.setItem(TOKEN_KEY, t);
  }
  return t;
}

document.getElementById('tokenBtn').onclick = () => {
  const cur = localStorage.getItem(TOKEN_KEY) || '';
  const t = prompt('修改上传密钥：', cur);
  if (t !== null) localStorage.setItem(TOKEN_KEY, t);
};

const drop = document.getElementById('drop');
const fileInput = document.getElementById('file');
const list = document.getElementById('list');

fileInput.addEventListener('change', () => {
  [...fileInput.files].forEach(upload);
  fileInput.value = '';
});

['dragenter', 'dragover'].forEach(ev =>
  drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.add('over'); })
);
['dragleave', 'drop'].forEach(ev =>
  drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('over'); })
);
drop.addEventListener('drop', e => {
  [...e.dataTransfer.files].forEach(upload);
});

document.addEventListener('paste', e => {
  const items = e.clipboardData?.items || [];
  for (const it of items) {
    if (it.kind === 'file') {
      const f = it.getAsFile();
      if (f) upload(f);
    }
  }
});

const DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
const PPTX_MIME = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';

async function upload(file) {
  const isDocx = /\.(docx|pptx)$/i.test(file.name) || file.type === DOCX_MIME || file.type === PPTX_MIME;
  if (!file.type.startsWith('image/') && !isDocx) {
    addCard({ error: '只支持图片、.docx 或 .pptx：' + file.name });
    return;
  }
  const token = getToken();
  if (!token) { addCard({ error: '未填写密钥' }); return; }

  const card = addCard({ uploading: true, name: file.name });

  const fd = new FormData();
  fd.append('file', file);

  try {
    const r = await fetch('', {
      method: 'POST',
      headers: { 'X-Token': token },
      body: fd,
    });
    const data = await r.json();
    if (!r.ok) {
      card.replaceWith(buildCard({ error: data.error || ('HTTP ' + r.status), name: file.name }));
      return;
    }
    card.replaceWith(buildCard(data));
  } catch (err) {
    card.replaceWith(buildCard({ error: String(err), name: file.name }));
  }
}

function addCard(state) {
  const el = buildCard(state);
  list.prepend(el);
  return el;
}

function buildCard(s) {
  const el = document.createElement('div');
  el.className = 'card' + (s.error ? ' err' : '');

  if (s.error) {
    el.innerHTML = `
      <div class="thumb"></div>
      <div class="meta">
        <div class="name">${escapeHtml(s.name || '')}</div>
        <div class="err-msg">${escapeHtml(s.error)}</div>
      </div>`;
    return el;
  }

  if (s.uploading) {
    el.innerHTML = `
      <div class="thumb"></div>
      <div class="meta">
        <div class="name">${escapeHtml(s.name)}</div>
        <div class="err-msg" style="color:var(--muted)">上传中…</div>
      </div>`;
    return el;
  }

  const thumb = s.is_image === false
    ? `<div class="thumb thumb-doc">${/\.pptx$/i.test(s.name) ? 'PPTX' : 'DOCX'}</div>`
    : `<img class="thumb" src="${escapeAttr(s.url)}" alt="">`;
  el.innerHTML = `
    ${thumb}
    <div class="meta">
      <div class="name">${escapeHtml(s.name)} · ${formatSize(s.size)}</div>
      <div class="row"><code>${escapeHtml(s.url)}</code><button class="btn" data-copy="${escapeAttr(s.url)}">URL</button></div>
      <div class="row"><code>${escapeHtml(s.markdown)}</code><button class="btn" data-copy="${escapeAttr(s.markdown)}">MD</button></div>
      <div class="row"><code>${escapeHtml(s.html)}</code><button class="btn" data-copy="${escapeAttr(s.html)}">HTML</button></div>
    </div>`;

  el.querySelectorAll('button[data-copy]').forEach(btn => {
    btn.onclick = async () => {
      try {
        await navigator.clipboard.writeText(btn.dataset.copy);
        const orig = btn.textContent;
        btn.textContent = '已复制';
        btn.classList.add('copied');
        setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); }, 1200);
      } catch {
        alert('复制失败，请手动选中');
      }
    };
  });

  return el;
}

function escapeHtml(s) {
  return String(s ?? '').replace(/[&<>"']/g, c =>
    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
function escapeAttr(s) { return escapeHtml(s); }
function formatSize(n) {
  if (n < 1024) return n + ' B';
  if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
  return (n / 1024 / 1024).toFixed(2) + ' MB';
}
</script>
</body>
</html>
<?php
}
