208 lines
8.4 KiB
PHP
208 lines
8.4 KiB
PHP
<?php
|
||||
|
|
/**
|
|||
|
|
* 附件上传接口
|
|||
|
|
* ------------------------------------------------------------------
|
|||
|
|
* 两种存储方式由后台设置决定(attachment.storage):
|
|||
|
|
*
|
|||
|
|
* ① oss(阿里云 OSS 直传,原行为)
|
|||
|
|
* POST upload.php kind=&ext=&size=&mime=&csrf=
|
|||
|
|
* → 返回 host / key / policy / signature…,浏览器把文件直接 POST 到 OSS,
|
|||
|
|
* 文件不经过本服务器。凭证来自 ECS 实例 RAM 角色的临时凭证(或静态 AK)。
|
|||
|
|
*
|
|||
|
|
* ② local(存本机磁盘)
|
|||
|
|
* 第一步同上,只是返回的 host 指向本文件的 ?action=put;
|
|||
|
|
* 第二步浏览器把文件 multipart POST 到 upload.php?action=put,
|
|||
|
|
* 服务端校验后落到 uploads/YYYY/MM/ 下。
|
|||
|
|
*
|
|||
|
|
* 两种方式共用同一套校验:扩展名 / MIME 白名单、单文件大小、每 IP 频率、全站总量上限。
|
|||
|
|
*/
|
|||
|
|
require __DIR__ . '/lib/bootstrap.php';
|
|||
|
|
|
|||
|
|
/** 统一 JSON 输出 */
|
|||
|
|
function fb_up_json($data, $code = 200) {
|
|||
|
|
http_response_code($code);
|
|||
|
|
header('Content-Type: application/json; charset=utf-8');
|
|||
|
|
header('Cache-Control: no-store');
|
|||
|
|
header('X-Content-Type-Options: nosniff');
|
|||
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|||
|
|
exit;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (strtoupper(isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET') !== 'POST') {
|
|||
|
|
fb_up_json(['ok' => 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,
|
|||
|
|
]);
|