242 lines
8.0 KiB
PHP
242 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace app\common\service\article;
|
|
|
|
use think\Response;
|
|
|
|
class ArticleImageProxyService
|
|
{
|
|
private const SOURCE_HOSTS = [
|
|
'hkjc_racingnews' => [
|
|
'res.hkjc.com',
|
|
'racingnews.hkjc.com',
|
|
],
|
|
'taiwan_lottery' => [
|
|
'cdn.taiwanlottery.com.tw',
|
|
'www.taiwanlottery.com',
|
|
'taiwanlottery.com',
|
|
],
|
|
];
|
|
|
|
private const SOURCE_REFERERS = [
|
|
'hkjc_racingnews' => 'https://racingnews.hkjc.com/',
|
|
'taiwan_lottery' => 'https://www.taiwanlottery.com/',
|
|
];
|
|
|
|
public static function rewriteArticlePayload(array $article): array
|
|
{
|
|
$ext = self::decodeExt($article['ext'] ?? []);
|
|
if (!self::shouldRewrite($ext)) {
|
|
return $article;
|
|
}
|
|
|
|
if (!empty($article['image'])) {
|
|
$article['image'] = self::buildProxyUrl((string) $article['image'], $ext);
|
|
}
|
|
|
|
if (!empty($article['content'])) {
|
|
$article['content'] = preg_replace_callback(
|
|
'/(<img\b[^>]*\bsrc=["\'])([^"\']+)(["\'][^>]*>)/i',
|
|
static function (array $matches) use ($ext) {
|
|
$proxyUrl = self::buildProxyUrl($matches[2] ?? '', $ext);
|
|
if ($proxyUrl === '' || $proxyUrl === ($matches[2] ?? '')) {
|
|
return $matches[0];
|
|
}
|
|
return $matches[1]
|
|
. htmlspecialchars($proxyUrl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
|
. $matches[3];
|
|
},
|
|
(string) $article['content']
|
|
);
|
|
}
|
|
|
|
return $article;
|
|
}
|
|
|
|
public static function proxyRemoteImage(string $url): Response
|
|
{
|
|
$normalizedUrl = self::normalizeUrl($url);
|
|
if ($normalizedUrl === '' || !self::isAllowedRemoteUrl($normalizedUrl)) {
|
|
return response('image not found', 404)->header([
|
|
'Content-Type' => 'text/plain; charset=utf-8',
|
|
'Cache-Control' => 'public, max-age=300',
|
|
]);
|
|
}
|
|
|
|
$source = self::detectSourceByUrl($normalizedUrl);
|
|
$headers = [
|
|
'Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
|
'User-Agent: Mozilla/5.0 (compatible; SportEra/1.0)',
|
|
];
|
|
$referer = self::SOURCE_REFERERS[$source] ?? '';
|
|
if ($referer !== '') {
|
|
$headers[] = 'Referer: ' . $referer;
|
|
}
|
|
|
|
$ch = curl_init($normalizedUrl);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_MAXREDIRS => 3,
|
|
CURLOPT_CONNECTTIMEOUT => 10,
|
|
CURLOPT_TIMEOUT => 20,
|
|
CURLOPT_ENCODING => '',
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
]);
|
|
|
|
$body = curl_exec($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
$contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
|
$effectiveUrl = (string) curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
|
|
$curlError = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($body === false || $curlError !== '' || $httpCode < 200 || $httpCode >= 300) {
|
|
return response('image not found', 404)->header([
|
|
'Content-Type' => 'text/plain; charset=utf-8',
|
|
'Cache-Control' => 'public, max-age=300',
|
|
]);
|
|
}
|
|
|
|
$effectiveUrl = $effectiveUrl !== '' ? $effectiveUrl : $normalizedUrl;
|
|
if (!self::isAllowedRemoteUrl($effectiveUrl)) {
|
|
return response('image not found', 404)->header([
|
|
'Content-Type' => 'text/plain; charset=utf-8',
|
|
'Cache-Control' => 'public, max-age=300',
|
|
]);
|
|
}
|
|
|
|
$contentType = self::normalizeContentType($contentType, $effectiveUrl);
|
|
if (!str_starts_with($contentType, 'image/')) {
|
|
return response('image not found', 404)->header([
|
|
'Content-Type' => 'text/plain; charset=utf-8',
|
|
'Cache-Control' => 'public, max-age=300',
|
|
]);
|
|
}
|
|
|
|
return response($body, 200)->header([
|
|
'Content-Type' => $contentType,
|
|
'Cache-Control' => 'public, max-age=86400',
|
|
'Access-Control-Allow-Origin' => '*',
|
|
]);
|
|
}
|
|
|
|
private static function shouldRewrite(array $ext): bool
|
|
{
|
|
$source = (string) ($ext['source'] ?? '');
|
|
return isset(self::SOURCE_HOSTS[$source]);
|
|
}
|
|
|
|
private static function buildProxyUrl(string $url, array $ext = []): string
|
|
{
|
|
$normalizedUrl = self::normalizeUrl($url);
|
|
if ($normalizedUrl === '' || self::isLocalProxyUrl($normalizedUrl)) {
|
|
return $url;
|
|
}
|
|
|
|
if (!self::isAllowedRemoteUrl($normalizedUrl, $ext)) {
|
|
return $url;
|
|
}
|
|
|
|
$path = '/api/article/imageProxy?url=' . rawurlencode($normalizedUrl);
|
|
$domain = rtrim((string) request()->domain(), '/');
|
|
return $domain !== '' ? $domain . $path : $path;
|
|
}
|
|
|
|
private static function normalizeUrl(string $url): string
|
|
{
|
|
$url = trim(html_entity_decode($url, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
|
if ($url === '') {
|
|
return '';
|
|
}
|
|
if (str_starts_with($url, '//')) {
|
|
return 'https:' . $url;
|
|
}
|
|
return $url;
|
|
}
|
|
|
|
private static function isAllowedRemoteUrl(string $url, array $ext = []): bool
|
|
{
|
|
$parts = parse_url($url);
|
|
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
|
|
$host = strtolower((string) ($parts['host'] ?? ''));
|
|
if ($host === '' || !in_array($scheme, ['http', 'https'], true)) {
|
|
return false;
|
|
}
|
|
|
|
$allowedHosts = self::getAllowedHosts($ext);
|
|
foreach ($allowedHosts as $allowedHost) {
|
|
$allowedHost = strtolower($allowedHost);
|
|
if ($host === $allowedHost || str_ends_with($host, '.' . $allowedHost)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static function getAllowedHosts(array $ext = []): array
|
|
{
|
|
$source = (string) ($ext['source'] ?? '');
|
|
if ($source !== '' && isset(self::SOURCE_HOSTS[$source])) {
|
|
return self::SOURCE_HOSTS[$source];
|
|
}
|
|
|
|
$allHosts = [];
|
|
foreach (self::SOURCE_HOSTS as $hosts) {
|
|
$allHosts = array_merge($allHosts, $hosts);
|
|
}
|
|
return array_values(array_unique($allHosts));
|
|
}
|
|
|
|
private static function detectSourceByUrl(string $url): string
|
|
{
|
|
$parts = parse_url($url);
|
|
$host = strtolower((string) ($parts['host'] ?? ''));
|
|
foreach (self::SOURCE_HOSTS as $source => $hosts) {
|
|
foreach ($hosts as $allowedHost) {
|
|
$allowedHost = strtolower($allowedHost);
|
|
if ($host === $allowedHost || str_ends_with($host, '.' . $allowedHost)) {
|
|
return $source;
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
private static function isLocalProxyUrl(string $url): bool
|
|
{
|
|
return str_contains($url, '/api/article/imageProxy?');
|
|
}
|
|
|
|
private static function normalizeContentType(string $contentType, string $url): string
|
|
{
|
|
$contentType = strtolower(trim(explode(';', $contentType)[0] ?? ''));
|
|
if (str_starts_with($contentType, 'image/')) {
|
|
return $contentType;
|
|
}
|
|
|
|
$path = strtolower((string) parse_url($url, PHP_URL_PATH));
|
|
return match (pathinfo($path, PATHINFO_EXTENSION)) {
|
|
'jpg', 'jpeg' => 'image/jpeg',
|
|
'png' => 'image/png',
|
|
'gif' => 'image/gif',
|
|
'webp' => 'image/webp',
|
|
'svg' => 'image/svg+xml',
|
|
default => 'application/octet-stream',
|
|
};
|
|
}
|
|
|
|
private static function decodeExt($ext): array
|
|
{
|
|
if (is_array($ext)) {
|
|
return $ext;
|
|
}
|
|
if (is_string($ext) && $ext !== '') {
|
|
$decoded = json_decode($ext, true);
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
return [];
|
|
}
|
|
}
|