no message
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
<?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 [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user