false, 'error' => 'method not allowed'], 405); } $mode = fb_store_mode(); $putAt = (isset($_GET['action']) && (string)$_GET['action'] === 'put'); if ($mode === 'off') { fb_up_json(['ok' => false, 'error' => '附件上传未启用。'], 403); } if ($mode === 'oss' && !fb_oss_enabled()) { fb_up_json(['ok' => false, 'error' => '对象存储未配置完整,请到后台「站点设置」补齐桶信息。'], 403); } if ($mode === 'local' && !fb_local_writable()) { $ld = fb_local_diag(); fb_up_json(['ok' => false, 'error' => '本机附件目录不可写(uploads/)。' . ($ld['tip'] !== '' ? ' ' . $ld['tip'] : '')], 403); } if (!fb_csrf_ok(isset($_POST['csrf']) ? $_POST['csrf'] : '')) { fb_up_json(['ok' => false, 'error' => '会话已过期,请刷新页面后重试。'], 400); } /* ================================================================== * 本机存储:接收文件本体 * ================================================================== */ if ($putAt) { if ($mode !== 'local') { fb_up_json(['ok' => false, 'error' => '当前存储方式不支持本机上传。'], 400); } $kind = strtolower(trim(isset($_POST['kind']) ? (string)$_POST['kind'] : '')); $key = trim(isset($_POST['key']) ? (string)$_POST['key'] : ''); if ($kind !== 'image' && $kind !== 'video') { fb_up_json(['ok' => false, 'error' => '不支持的附件类型。'], 400); } if (!fb_upload_kind_on($kind)) { fb_up_json(['ok' => false, 'error' => '该类型附件未开放上传。'], 403); } if (!fb_attach_is_local($key) || !fb_upload_key_ok($key)) { fb_up_json(['ok' => false, 'error' => '附件地址不合法,请重新上传。'], 400); } $ext = fb_upload_key_ext($key); if (!fb_upload_ext_ok($kind, $ext)) { fb_up_json(['ok' => false, 'error' => '不支持的文件格式。'], 400); } if (empty($_FILES['file']) || !is_array($_FILES['file'])) { fb_up_json(['ok' => false, 'error' => '没有收到文件。'], 400); } $f = $_FILES['file']; if (!isset($f['error']) || (int)$f['error'] !== UPLOAD_ERR_OK) { fb_up_json(['ok' => false, 'error' => '文件上传失败(错误码 ' . (int)(isset($f['error']) ? $f['error'] : -1) . ')。'], 400); } if (!is_uploaded_file((string)$f['tmp_name'])) { fb_up_json(['ok' => false, 'error' => '文件上传失败。'], 400); } $size = (int)@filesize($f['tmp_name']); $maxB = (int)fb_upload_cfg($kind . '_max_bytes', 0); if ($size <= 0) { fb_up_json(['ok' => false, 'error' => '文件为空。'], 400); } if ($maxB > 0 && $size > $maxB) { fb_up_json(['ok' => false, 'error' => '文件超过大小限制(' . fb_size_h($maxB) . ')。'], 400); } $mime = strtolower(trim(isset($_POST['mime']) ? (string)$_POST['mime'] : '')); if ($mime === '') { $mime = fb_upload_mime_of_ext($ext); } if (!fb_upload_mime_ok($kind, $mime)) { fb_up_json(['ok' => false, 'error' => '不支持的文件类型。'], 400); } // 二次确认总量上限(防止并发绕过) $pdo = fb_db(); $cap = (int)fb_upload_cfg('total_cap_bytes', 0); if ($cap > 0 && (fb_attach_total_bytes($pdo) + $size) > $cap) { fb_up_json(['ok' => false, 'error' => '附件存储空间已满,暂时无法上传。'], 507); } $path = fb_local_path_of($key); $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } if (!is_dir($dir) || !@move_uploaded_file((string)$f['tmp_name'], $path)) { fb_up_json(['ok' => false, 'error' => '保存文件失败,请稍后重试。'], 500); } @chmod($path, 0644); fb_up_json([ 'ok' => true, 'key' => $key, 'url' => fb_attach_url($key), 'size' => $size, 'mime' => $mime, ]); } /* ================================================================== * 领取上传凭证 * ================================================================== */ $kind = strtolower(trim(isset($_POST['kind']) ? (string)$_POST['kind'] : '')); if ($kind !== 'image' && $kind !== 'video') { fb_up_json(['ok' => false, 'error' => '不支持的附件类型。'], 400); } if (!fb_upload_kind_on($kind)) { fb_up_json(['ok' => false, 'error' => '该类型附件未开放上传。'], 403); } $ext = fb_upload_norm_ext(isset($_POST['ext']) ? (string)$_POST['ext'] : ''); $size = (int)(isset($_POST['size']) ? $_POST['size'] : 0); $mime = strtolower(trim(isset($_POST['mime']) ? (string)$_POST['mime'] : '')); if ($mime === '') { $mime = fb_upload_mime_of_ext($ext); } $maxBytes = (int)fb_upload_cfg($kind . '_max_bytes', 0); if ($ext === '' || !fb_upload_ext_ok($kind, $ext)) { fb_up_json(['ok' => false, 'error' => '不支持的文件格式。'], 400); } if (!fb_upload_mime_ok($kind, $mime)) { fb_up_json(['ok' => false, 'error' => '不支持的文件类型。'], 400); } if ($size <= 0) { fb_up_json(['ok' => false, 'error' => '文件为空。'], 400); } if ($maxBytes > 0 && $size > $maxBytes) { fb_up_json(['ok' => false, 'error' => '文件超过大小限制(' . fb_size_h($maxBytes) . ')。'], 400); } $limit = (int)fb_upload_cfg('upload_per_hour', 60); if (!fb_rate_check('upload', $limit)) { fb_up_json(['ok' => false, 'error' => '上传过于频繁,请稍后再试。'], 429); } // 全站总量上限:达到后不再发放凭证 $pdo = fb_db(); $cap = (int)fb_upload_cfg('total_cap_bytes', 0); if ($cap > 0 && (fb_attach_total_bytes($pdo) + $size) > $cap) { fb_up_json(['ok' => false, 'error' => '附件存储空间已满,暂时无法上传,请稍后再试。'], 507); } /* ---------- 本机存储:返回一个指向本文件的上传地址 ---------- */ if ($mode === 'local') { $key = fb_local_make_key($ext); fb_rate_hit('upload'); fb_up_json([ 'ok' => true, 'credential' => [ 'host' => 'upload.php?action=put', 'key' => $key, 'url' => fb_attach_url($key), ], 'fields' => [ 'csrf' => fb_csrf_token(), 'key' => $key, 'kind' => $kind, 'mime' => $mime, ], 'mime' => $mime, 'kind' => $kind, ]); } /* ---------- OSS 直传 ---------- */ $key = fb_oss_make_key($ext); $cred = fb_oss_upload_credential($key, $maxBytes, $kind . '/'); fb_rate_hit('upload'); // extra_fields 是除固定项外需要浏览器一并提交的表单域:使用实例 RAM 角色的 // 临时凭证时,就是 x-oss-security-token(OSS 靠它识别这份临时身份)。 $fields = [ 'key' => $cred['key'], 'policy' => $cred['policy'], 'OSSAccessKeyId' => $cred['access_key_id'], 'signature' => $cred['signature'], 'success_action_status' => '200', 'Content-Type' => $mime, ]; if (!empty($cred['extra_fields']) && is_array($cred['extra_fields'])) { foreach ($cred['extra_fields'] as $k => $v) { $fields[$k] = $v; } } fb_up_json([ 'ok' => true, 'credential' => $cred, 'fields' => $fields, 'mime' => $mime, 'kind' => $kind, ]);