Files
MovieLog_Server/lib/storage/http.php
T
laoyang 3a3c5890cc 安装向导 + PHP 8.5 兼容修复
新增 install.php:全新部署时自动接管,用户只需上传文件、打开站点即进入
安装向导。安装页自己做三件事:
  - 建 db/ 与 uploads/{posters,tickets,photos,avatars}/,权限调到 775
  - 逐目录真写临时文件实测可写,不靠 is_writable 猜
  - 建库、建管理员、写 db/.installed 锁、自删本文件

配套改动:
  - config.php 把安装检查前置于建库之前,未安装一律 302 到 install.php。
    原实现无条件调用 initDB(),导致「打开首页」就等于建库并种下 admin/123456。
  - lib/db.php 的 initDB(\$seedDefaultAdmin = true);安装向导传 false,
    管理员由用户自己设,绝不种弱口令。新增 isInstalled()。
  - login.php / nav.php 接上 siteName(),站点名称不再只存不用。
  - lib/settings.php 新增 siteName(),缺省回退「电影日志」。

PHP 8.5 兼容:
  - 删掉 fetch_movie.php 与 lib/storage/http.php 的 curl_close()。
    8.5 起该函数 deprecated,调用会往输出插一行警告,把 JSON 响应污染成
    「警告 + JSON」,前端 JSON.parse 失败 → 电影名联想功能完全失灵。
  - config.php 的 display_errors 由 1 改为 0(生产环境不应向浏览器吐错误),
    错误仍写日志;需要调试时设 MOVIELOG_DEBUG=1 打开。

线上验证(movie.yangyq.net,PHP 8.5.12):
  未安装时 / 、/login.php 均 302 → install.php;
  安装页 200、体检 10/10 通过、自动建好 5 个目录;
  安装后 install.php 404、管理员登录成功、首页 200 无报错;
  联想接口带会话请求「流浪地球/星际穿越/霸王别姬」均返回纯 JSON(首字符 {)。
2026-10-31 21:35:41 +08:00

147 lines
5.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// ============================================
// 文件: lib/storage/http.php
// 说明: 对象存储驱动共用的 HTTP 客户端与签名工具函数
// ============================================
// 该路径是否允许当前请求访问。
//
// 宝塔等环境会给站点加 .user.ini:open_basedir=/站点目录/:/tmp/。
// 此时对 /etc/ssl/... 这类"站外路径"调用 is_file() 会抛
// Warning: is_file(): open_basedir restriction in effect
// 而站点开着 display_errors,警告就会被打进响应体——对返回 JSON 的接口
// 是致命的(前端 JSON.parse 直接失败)。所以探测前先自己判断一遍。
function storagePathAllowed($path) {
$path = str_replace('\\', '/', trim((string)$path));
if ($path === '') return false;
$open = (string)ini_get('open_basedir');
if ($open !== '') {
$allowed = false;
foreach (explode(PATH_SEPARATOR, $open) as $dir) {
$dir = rtrim(str_replace('\\', '/', trim($dir)), '/');
if ($dir === '') continue;
if ($path === $dir || strpos($path, $dir . '/') === 0) {
$allowed = true;
break;
}
}
if (!$allowed) return false;
}
// 仍在允许目录内,也压掉可能的名权限/符号链接警告
return @is_file($path) && @is_readable($path);
}
// 定位系统 CA 证书,供 SSL 校验使用(找不到时返回 null,交由 curl 自行处理)
function storageCaBundle() {
static $ca = false;
if ($ca !== false) return $ca;
$candidates = [
ini_get('curl.cainfo'),
ini_get('openssl.cafile'),
'/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/cert.pem',
];
foreach ($candidates as $path) {
if (storagePathAllowed($path)) {
$ca = (string)$path;
return $ca;
}
}
$ca = null;
return $ca;
}
/**
* 发起 HTTP 请求。
*
* @param string $method GET / PUT / POST / DELETE / HEAD
* @param string $url 完整 URL
* @param array $headers 形如 ['Content-Type: image/jpeg', ...]
* @param string|null $body 请求体
* @param int $timeout 总超时(秒)
* @param bool $insecure 是否跳过 SSL 证书校验(默认否)
* @param int|null $connectTimeout 连接阶段超时(秒)。默认由总超时推算;
* 想"快速失败"的地方(比如探测内网 endpoint)显式给一个很小的值。
*
* @return array ['ok'=>bool,'status'=>int,'headers'=>array,'body'=>string,'error'=>string]
*/
function storageHttp($method, $url, array $headers = [], $body = null, $timeout = 30, $insecure = false, $connectTimeout = null) {
$respHeaders = [];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => strtoupper($method),
CURLOPT_HTTPHEADER => $headers,
CURLOPT_CONNECTTIMEOUT => $connectTimeout === null
? min(10, max(3, $timeout))
: max(1, (int)$connectTimeout),
CURLOPT_TIMEOUT => $timeout,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => !$insecure,
CURLOPT_SSL_VERIFYHOST => $insecure ? 0 : 2,
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$respHeaders) {
$pos = strpos($line, ':');
if ($pos !== false) {
$name = strtolower(trim(substr($line, 0, $pos)));
$respHeaders[$name] = trim(substr($line, $pos + 1));
}
return strlen($line);
},
]);
$ca = storageCaBundle();
if ($ca && !$insecure) {
curl_setopt($ch, CURLOPT_CAINFO, $ca);
}
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$out = curl_exec($ch);
$err = curl_error($ch);
$errno = curl_errno($ch);
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
// 不调用 curl_close():PHP 8.0 起句柄由 GC 回收;8.5 起该函数被标记 deprecated,
// 调用会在输出里插入警告,污染本模块返回给前端的 JSON。
if ($out === false) {
return [
'ok' => false, 'status' => 0, 'headers' => [],
'body' => '',
'error' => $errno === CURLE_OPERATION_TIMEDOUT
? "请求超时({$timeout}s)"
: "网络错误({$errno}): {$err}",
];
}
return [
'ok' => $status >= 200 && $status < 300,
'status' => $status,
'headers' => $respHeaders,
'body' => $out,
'error' => $status >= 200 && $status < 300 ? '' : "HTTP {$status}",
];
}
// 生成对象的访问 URL(路径部分逐段编码,保留斜杠)
function storageEncodeKey($key) {
$key = ltrim((string)$key, '/');
$segments = explode('/', $key);
return implode('/', array_map('rawurlencode', $segments));
}
// 解析 host 形式:bucket.endpoint 或自定义域名
function storageHostOf($domain, $useHttps = true) {
$domain = preg_replace('#^https?://#i', '', trim((string)$domain));
return ($useHttps ? 'https://' : 'http://') . rtrim($domain, '/');
}
// 统一把失败信息压缩成一行,便于写入设置页展示
function storageShortError($text, $max = 200) {
$text = preg_replace('/\s+/', ' ', trim((string)$text));
return mb_strlen($text, 'UTF-8') > $max ? mb_substr($text, 0, $max, 'UTF-8') . '…' : $text;
}