\n Require all denied\n\n" . "\n Order allow,deny\n Deny from all\n\n\n" . "\n" . " \n Require all denied\n \n" . " \n Order allow,deny\n Deny from all\n \n" . "\n"; } /** 确保目录存在并补齐防护文件(规则版本见上方 FB_LOCAL_GUARD_VER) */ function fb_local_ensure_dir() { $dir = fb_local_dir(); if (!is_dir($dir)) { @mkdir($dir, 0755, true); if (!is_dir($dir)) { return false; } @chmod($dir, 0755); } $ht = $dir . '/.htaccess'; if (!is_file($ht) || fb_local_guard_stale($ht)) { @file_put_contents($ht, fb_local_guard_rule()); @chmod($ht, 0644); } $idx = $dir . '/index.html'; if (!is_file($idx)) { @file_put_contents($idx, ''); @chmod($idx, 0644); } return true; } /** * 现存的 .htaccess 是不是旧版(需要升级)。 * 判据:拿不到 `fb-local-guard: vN` 标记,或标记里的 N < FB_LOCAL_GUARD_VER * → 都算过期,整份重写(旧文件没有标记,正是最需要升级的那种)。 * 只在目录可写时才重写,避免不可写环境下每次都白试一遍。 */ function fb_local_guard_stale($ht) { $body = @file_get_contents($ht, false, null, 0, 4096); if ($body === false) { return false; } if (strpos($body, '# fb-local-guard: v' . FB_LOCAL_GUARD_VER) !== false) { return false; } return true; } /** 本机附件是否可写 */ function fb_local_writable() { fb_local_ensure_dir(); $dir = fb_local_dir(); return is_dir($dir) && is_writable($dir); } /** * 本机附件目录的诊断信息(供后台「存储体检」显示)。 * 真实踩过的坑:用 SFTP/部署账号手动建 uploads/ 时,目录属主是部署账号, * PHP 进程(www)写不进去 —— 表现就是「文件已存在但不可写」。 * 此时最省事的办法是删掉该目录,让 PHP 自己重建(属主即 www)。 * @return array{ok:bool,dir:string,tip:string} */ function fb_local_diag() { $dir = fb_local_dir(); fb_local_ensure_dir(); // 顺带补齐 .htaccess / index.html $exist = is_dir($dir); if (!$exist) { return ['ok' => false, 'dir' => $dir, 'tip' => '目录不存在且无法创建:上级目录 feedback/ 对 PHP 进程不可写,请检查目录权限。']; } if (!is_writable($dir)) { $owner = function_exists('posix_getpwuid') ? @posix_getpwuid(@fileowner($dir)) : null; $oname = is_array($owner) && isset($owner['name']) ? $owner['name'] : ('uid ' . @fileowner($dir)); $me = function_exists('posix_geteuid') ? @posix_getpwuid(@posix_geteuid()) : null; $mname = is_array($me) && isset($me['name']) ? $me['name'] : ('uid ' . @getmyuid()); return ['ok' => false, 'dir' => $dir, 'tip' => '目录属主是 ' . $oname . ',而 PHP 以 ' . $mname . ' 运行 → 不可写。' . '处理办法:删掉整个 uploads/ 目录(包括里面的 .htaccess),再刷新本页,' . 'PHP 会自动重建并把属主设为自己;或在宝塔面板里把 uploads/ 属主改为站点运行用户。']; } return ['ok' => true, 'dir' => $dir, 'tip' => '']; } /** 当前存储是否可用(决定要不要显示上传入口) */ function fb_store_enabled() { $m = fb_store_mode(); if ($m === 'local') { return fb_local_writable(); } if ($m === 'oss') { return fb_oss_enabled(); } return false; } /** 生成「本机存储」的对象键 */ function fb_local_make_key($ext) { $ext = strtolower(preg_replace('/[^A-Za-z0-9]/', '', (string)$ext)); return 'local/' . date('Y/m') . '/' . bin2hex(random_bytes(8)) . '.' . $ext; } /** 该附件是不是存在本机 */ function fb_attach_is_local($key) { return strpos((string)$key, 'local/') === 0; } /* ================================================================== * 软件图标(管理员上传的图片) * ------------------------------------------------------------------ * 🔴 存储位置【跟随后台配置】,与附件完全同一套规则: * attachment.storage = local → local/icons/YYYY/MM/. 写 uploads/ * attachment.storage = oss → /icons/YYYY/MM/. 写私有桶 * 管理员在后台配的哪种,图标就存哪种;前台地址两种模式完全一致, * 都是 media.php?k=... 代理,换存储方式不会让已有图标失效。 * * 早期实现把图标键固定写成 `icons/...`(不带 local/ 前缀), * 结果 fb_attach_is_local() 永远返回 false —— 本机模式下文件明明写进了 * uploads/icons/,media.php 却当成 OSS 对象去读,直接 503。 * 这是「本地存储时图标显示不出来」的根因,键必须带存储前缀。 * * 为什么不复用普通附件: * 普通附件必须挂在某条 thread 下才允许被 media.php 读出(否则本站就成了 * 「能读整个桶」的开放代理)。软件图标不属于任何帖子,用它那套鉴权会被拒。 * 于是键里多一段 `icons/`,media.php 据此识别并按公开资源处理(写入仍受控)。 * ================================================================== */ function fb_icon_key_prefix() { return 'icons/'; } /** * 图标允许的扩展名(比附件白名单窄:图标不需要 gif 动图)。 * ⚠️ 刻意不收 svg:SVG 能内嵌