Files

137 lines
3.8 KiB
PHP
Raw Permalink Normal View History

2026-07-02 03:13:27 +08:00
<?php
declare(strict_types=1);
namespace app\common\service;
/**
* 短链 HTTP 重定向预解析(与 puppeteer-api/url-resolve.js 行为对齐)
*
* 用于 antiBot landingUrlHintCF 拦截时可能失败,浏览器仍会 follow。
*/
final class SplitPageUrlResolver
{
private const REDIRECT_CODES = [301, 302, 303, 307, 308];
/**
* 跟随 HTTP 重定向,返回最终 URL;失败时返回原始 URL
*/
public static function resolveFinalUrl(string $pageUrl, int $maxRedirects = 10, int $timeoutSec = 12): string
{
$current = trim($pageUrl);
if ($current === '') {
return '';
}
for ($i = 0; $i < $maxRedirects; $i++) {
$response = self::fetchHeadOrGet($current, $timeoutSec);
if ($response === null) {
return $current;
}
$code = $response['code'];
$location = $response['location'];
if (!in_array($code, self::REDIRECT_CODES, true) || $location === '') {
return $current;
}
$current = self::resolveRelativeUrl($current, $location);
}
return $current;
}
/**
* @return array{code: int, location: string}|null
*/
private static function fetchHeadOrGet(string $url, int $timeoutSec): ?array
{
$result = self::curlOnce($url, true, $timeoutSec);
if ($result !== null) {
return $result;
}
return self::curlOnce($url, false, $timeoutSec);
}
/**
* @return array{code: int, location: string}|null
*/
private static function curlOnce(string $url, bool $headOnly, int $timeoutSec): ?array
{
if (!function_exists('curl_init')) {
return null;
}
$ch = curl_init($url);
if ($ch === false) {
return null;
}
curl_setopt_array($ch, [
CURLOPT_NOBODY => $headOnly,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => $timeoutSec,
CURLOPT_HEADER => true,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; SplitPageUrlResolver/1.0)',
]);
$body = curl_exec($ch);
if ($body === false) {
curl_close($ch);
return null;
}
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$redirectUrl = (string) curl_getinfo($ch, CURLINFO_REDIRECT_URL);
curl_close($ch);
$location = $redirectUrl;
if ($location === '' && is_string($body)) {
$location = self::parseLocationHeader($body);
}
return ['code' => $code, 'location' => trim($location)];
}
private static function parseLocationHeader(string $rawHeaders): string
{
foreach (preg_split('/\r\n|\n|\r/', $rawHeaders) as $line) {
if (stripos($line, 'Location:') === 0) {
return trim(substr($line, 9));
}
}
return '';
}
private static function resolveRelativeUrl(string $baseUrl, string $location): string
{
if (preg_match('#^https?://#i', $location)) {
return $location;
}
$base = parse_url($baseUrl);
if (!is_array($base)) {
return $location;
}
$scheme = (string) ($base['scheme'] ?? 'https');
$host = (string) ($base['host'] ?? '');
if ($host === '') {
return $location;
}
if (strpos($location, '/') === 0) {
return $scheme . '://' . $host . $location;
}
$path = (string) ($base['path'] ?? '/');
$dir = rtrim(str_replace(basename($path), '', $path), '/');
return $scheme . '://' . $host . ($dir !== '' ? $dir . '/' : '/') . $location;
}
}