137 lines
3.8 KiB
PHP
Executable File
137 lines
3.8 KiB
PHP
Executable File
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\service;
|
||
|
||
/**
|
||
* 短链 HTTP 重定向预解析(与 puppeteer-api/url-resolve.js 行为对齐)
|
||
*
|
||
* 用于 antiBot landingUrlHint;CF 拦截时可能失败,浏览器仍会 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;
|
||
}
|
||
}
|