diff --git a/README.md b/README.md index 5f3a663..a518a20 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ feedback/ 1. 把整个 `feedback` 目录上传到站点根目录下,例如 `/www/wwwroot/你的域名/feedback/` (对应访问地址:`https://你的域名/feedback/`) -2. 确认该站点使用的 PHP 版本 **≥ 7.4**(推荐 8.0~8.3),并已启用扩展: +2. 确认该站点使用的 PHP 版本 **≥ 7.4**(推荐 8.1~8.5;**8.5 已逐项实测,无弃用告警**),并已启用扩展: **pdo_sqlite、sqlite3、openssl、mbstring**(宝塔默认均已有;在「软件商店 → PHP → 安装扩展」可查看)。 **curl 为可选**:有则优先用它和 OSS 通信,没有会自动退回 PHP 内置的流式请求,功能不受影响。 3. 给数据库目录写权限(宝塔一般已允许): diff --git a/install.php b/install.php index 3ff7e1d..584d95b 100644 --- a/install.php +++ b/install.php @@ -45,7 +45,7 @@ function fb_install_checks() { $ok = true; $verOk = version_compare(PHP_VERSION, '7.4.0', '>='); - $rows[] = ['PHP 版本', PHP_VERSION, $verOk, '需要 7.4 及以上(推荐 8.0~8.3)', $verOk ? '' : '当前版本过低,请切换站点的 PHP 版本']; + $rows[] = ['PHP 版本', PHP_VERSION, $verOk, '需要 7.4 及以上(推荐 8.1~8.5,已在 8.5 实测)', $verOk ? '' : '当前版本过低,请切换站点的 PHP 版本']; if (!$verOk) { $ok = false; } foreach ($need as $ext => $why) { diff --git a/lib/attach.php b/lib/attach.php index c06cdf2..ce6e2c1 100644 --- a/lib/attach.php +++ b/lib/attach.php @@ -362,7 +362,7 @@ function fb_attach_verify(array $list) { $ct = ''; if (function_exists('finfo_open')) { $f = @finfo_open(FILEINFO_MIME_TYPE); - if ($f) { $ct = strtolower(trim((string)@finfo_file($f, $p))); @finfo_close($f); } + if ($f) { $ct = strtolower(trim((string)@finfo_file($f, $p))); fb_finfo_close($f); } } elseif (function_exists('mime_content_type')) { $ct = strtolower(trim((string)@mime_content_type($p))); } diff --git a/lib/oss.php b/lib/oss.php index 0a3ea84..967293c 100644 --- a/lib/oss.php +++ b/lib/oss.php @@ -490,7 +490,7 @@ function fb_oss_put_file($key, $path, $mime = 'application/octet-stream') { $errno = curl_errno($ch); $errtxt = curl_error($ch); $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + fb_curl_close($ch); if ($errno) { @fclose($fp); return [false, '上传请求失败:' . $errtxt, $status]; } $errBody = (string)$body; } else { @@ -502,9 +502,15 @@ function fb_oss_put_file($key, $path, $mime = 'application/octet-stream') { 'timeout' => $timeout, 'ignore_errors' => true, ]]); + $hdrBefore = FB_HAS_HTTP_LAST_HDRS ? http_get_last_response_headers() : null; $body = @file_get_contents($url, false, $ctx); - if (isset($http_response_header) && is_array($http_response_header) - && preg_match('#^HTTP/\S+\s+(\d{3})#', $http_response_header[0], $m)) { + // PHP 8.5 起预定义局部变量 http_response_header 已弃用(连 @ 都压不住), + // 8.4 起改用 http_get_last_response_headers();老版本仍读局部变量。 + // 三元短路:新版本不执行第二支,老版本也不碰第一支。 + $hdrRaw = FB_HAS_HTTP_LAST_HDRS + ? fb_last_http_headers($hdrBefore) + : fb_local_http_headers(get_defined_vars()); + if ($hdrRaw && preg_match('#^HTTP/\S+\s+(\d{3})#', $hdrRaw[0], $m)) { $status = (int)$m[1]; } $errBody = ($body === false ? '' : (string)$body); @@ -588,7 +594,7 @@ function fb_oss_stream_get($key, $rangeHeader = '', array $subResource = [], $on $errno = curl_errno($ch); $errtxt = curl_error($ch); $st2 = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + fb_curl_close($ch); if ($st2 > 0) { $status = $st2; } if (!$statusFired && is_callable($onStatus)) { $onStatus($status); } @@ -685,7 +691,7 @@ function fb_http($method, $url, array $opt = []) { $bad = ($raw === false) || curl_errno($ch); $hsz = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE); $st = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + fb_curl_close($ch); if ($bad) { return [0, [], '']; } return [$st, fb_parse_headers(substr((string)$raw, 0, $hsz)), (string)substr((string)$raw, $hsz)]; } @@ -698,12 +704,16 @@ function fb_http($method, $url, array $opt = []) { ]; if ($noproxy) { $streamOpt['proxy'] = ''; $streamOpt['request_fulluri'] = false; } $ctx = stream_context_create(['http' => $streamOpt]); + $hdrBefore = FB_HAS_HTTP_LAST_HDRS ? http_get_last_response_headers() : null; $body = @file_get_contents($url, false, $ctx); $status = 0; $rawHead = ''; - if (isset($http_response_header) && is_array($http_response_header)) { + $hdrRaw = FB_HAS_HTTP_LAST_HDRS + ? fb_last_http_headers($hdrBefore) + : fb_local_http_headers(get_defined_vars()); + if ($hdrRaw) { $lines = []; - foreach ($http_response_header as $line) { + foreach ($hdrRaw as $line) { if (preg_match('#^HTTP/\S+\s+(\d{3})#i', $line, $m)) { $status = (int)$m[1]; $lines = []; } else { $lines[] = $line; } } diff --git a/lib/util.php b/lib/util.php index db1bac8..4d09d50 100644 --- a/lib/util.php +++ b/lib/util.php @@ -347,6 +347,65 @@ function fb_admin_set_password($pw) { return fb_set_setting('admin.password_hash', password_hash((string)$pw, PASSWORD_DEFAULT)); } +/** + * 关闭一个 curl 句柄(跨版本安全)。 + * -------------------------------------------------------------- + * PHP 8.0 起 CurlHandle 是对象,句柄随引用计数自动释放,curl_close() 变成空操作; + * PHP 8.5 起官方把这类"空操作函数"标为弃用,调用会写 E_DEPRECATED 日志。 + * 但 PHP 7.4 上它仍是真的释放动作(项目最低支持 7.4),所以只在新版本上跳过。 + */ +function fb_curl_close($ch) { + if (PHP_VERSION_ID < 80000 && $ch !== null) { @curl_close($ch); } +} + +/** 关闭一个 finfo 句柄(跨版本安全;理由同 fb_curl_close) */ +function fb_finfo_close($f) { + if (PHP_VERSION_ID < 80000 && $f !== null) { @finfo_close($f); } +} + +/** + * PHP 8.4+ 提供 http_get_last_response_headers();PHP 8.5 起,旧的预定义局部变量 + * http_response_header 已弃用(实测连 @ 抑制符都压不住,会照写 E_DEPRECATED)。 + * 这里探测一次存成常量,供各调用点做三元短路 —— 关键点:未取中的分支不会求值, + * 所以新版本上根本不会去读那个已被弃用的变量。 + */ +if (!defined('FB_HAS_HTTP_LAST_HDRS')) { + define('FB_HAS_HTTP_LAST_HDRS', function_exists('http_get_last_response_headers')); +} + +/** + * 取「上一次 http(s) 流请求」的响应头(仅 PHP 8.4+ 可调用本函数)。 + * ★ 新 API 是全局状态:若本次请求压根没发出去,它会返回 *上一次* 的残留头, + * 把旧状态当成新结果。所以要求传入请求前的基线 $before,未变化即视为「本次没有头」。 + * @param array|null $before 请求前 http_get_last_response_headers() 的快照 + * @return array + */ +function fb_last_http_headers($before = null) { + if (!function_exists('http_get_last_response_headers')) { return []; } + $h = http_get_last_response_headers(); + $h = is_array($h) ? $h : []; + if ($before !== null && $h === (array)$before) { return []; } + return $h; +} + +/** + * 从「当前作用域变量表」里取预定义变量 http_response_header(仅 PHP < 8.4 需要)。 + * -------------------------------------------------------------- + * 为什么不用直接读变量:PHP 8.5 对该变量的弃用是【编译期】判定的 —— 实测把它写在 + * 三元运算的「不该执行」那一支里照样报警,`@` 也压不住。也就是说,只要源码里出现 + * 这个标识符就会被记日志。唯一干净的做法是根本不去引用它,改用 get_defined_vars() + * 拿作用域变量表,再按键名取值。 + * ⚠️ 必须在发起请求的【同一作用域】调用($vars 由调用方传 get_defined_vars())。 + * @param array $vars get_defined_vars() 的结果 + * @return array + */ +function fb_local_http_headers($vars) { + if (!is_array($vars)) { return []; } + $k = 'http_response_header'; + if (!isset($vars[$k]) || !is_array($vars[$k])) { return []; } + return $vars[$k]; +} + /** 写一条操作日志 */ function fb_mod_log($action, $threadId = null, $detail = '') { try { diff --git a/media.php b/media.php index 51c60c6..40cb302 100644 --- a/media.php +++ b/media.php @@ -155,7 +155,7 @@ function fb_media_local_mime($path, $key) { $ct = ''; if (function_exists('finfo_open')) { $fi = @finfo_open(FILEINFO_MIME_TYPE); - if ($fi) { $ct = strtolower(trim((string)@finfo_file($fi, $path))); @finfo_close($fi); } + if ($fi) { $ct = strtolower(trim((string)@finfo_file($fi, $path))); fb_finfo_close($fi); } } if ($ct === '' || $ct === 'application/octet-stream') { $ct = fb_upload_mime_of_ext(fb_upload_key_ext($key));