132 lines
3.8 KiB
PHP
132 lines
3.8 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 短链 HTTP 重定向预解析(与 puppeteer-api/url-resolve.js 行为对齐)
|
||
|
|
*
|
||
|
|
* 用于 antiBot.sessionKey 按落地业务域生成,工单 URL 仍可为 yyk.ink 等短链。
|
||
|
|
*/
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|