286 lines
7.7 KiB
PHP
286 lines
7.7 KiB
PHP
<?php
|
|
/**
|
|
* 图片代理与缓存系统
|
|
*
|
|
* 提供安全的图片代理和缓存服务,支持多种图片格式,具有完善的错误处理和日志记录功能
|
|
*/
|
|
class ImageProxy {
|
|
// 支持的图片类型
|
|
private const SUPPORTED_TYPES = [
|
|
'gif' => 'image/gif',
|
|
'jpeg' => 'image/jpeg',
|
|
'jpg' => 'image/jpeg',
|
|
'jpe' => 'image/jpeg',
|
|
'png' => 'image/png',
|
|
'webp' => 'image/webp',
|
|
'bmp' => 'image/bmp',
|
|
'svg' => 'image/svg+xml'
|
|
];
|
|
|
|
// 默认配置
|
|
private const DEFAULT_CONFIG = [
|
|
'cache_dir' => 'cache',
|
|
'timeout' => 30,
|
|
'connect_timeout' => 15,
|
|
'max_redirects' => 5,
|
|
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
|
];
|
|
|
|
private $config;
|
|
private $logger;
|
|
|
|
/**
|
|
* 构造函数
|
|
*
|
|
* @param array $config 配置参数
|
|
*/
|
|
public function __construct(array $config = []) {
|
|
$this->config = array_merge(self::DEFAULT_CONFIG, $config);
|
|
$this->initCacheDir();
|
|
$this->initLogger();
|
|
}
|
|
|
|
/**
|
|
* 初始化缓存目录
|
|
*/
|
|
private function initCacheDir() {
|
|
if (!file_exists($this->config['cache_dir'])) {
|
|
mkdir($this->config['cache_dir'], 0755, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 初始化日志记录器
|
|
*/
|
|
private function initLogger() {
|
|
$logDir = $this->config['cache_dir'] . '/logs';
|
|
if (!file_exists($logDir)) {
|
|
mkdir($logDir, 0755, true);
|
|
}
|
|
$this->logger = new Logger($logDir);
|
|
}
|
|
|
|
/**
|
|
* 处理图片请求
|
|
*
|
|
* @param string $url 图片URL
|
|
* @return void
|
|
*/
|
|
public function processRequest(string $url) {
|
|
try {
|
|
if (!$this->validateUrl($url)) {
|
|
throw new Exception('Invalid URL format');
|
|
}
|
|
|
|
$cachePath = $this->getCachePath($url);
|
|
|
|
// 检查缓存
|
|
if ($this->isCached($cachePath)) {
|
|
$this->serveFromCache($cachePath);
|
|
return;
|
|
}
|
|
|
|
// 获取远程图片
|
|
$imageData = $this->fetchRemoteImage($url);
|
|
|
|
// 验证图片数据
|
|
if (!$this->validateImageData($imageData)) {
|
|
throw new Exception('Invalid image data');
|
|
}
|
|
|
|
// 保存到缓存
|
|
$this->saveToCache($cachePath, $imageData);
|
|
|
|
// 输出图片
|
|
$this->outputImage($imageData, $this->getImageType($url));
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Image processing error: ' . $e->getMessage());
|
|
$this->outputError();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 验证URL格式
|
|
*
|
|
* @param string $url URL地址
|
|
* @return bool
|
|
*/
|
|
private function validateUrl(string $url): bool {
|
|
return filter_var($url, FILTER_VALIDATE_URL) !== false &&
|
|
preg_match('/^https?:\/\//i', $url);
|
|
}
|
|
|
|
/**
|
|
* 获取缓存路径
|
|
*
|
|
* @param string $url 图片URL
|
|
* @return string
|
|
*/
|
|
private function getCachePath(string $url): string {
|
|
$parsedUrl = parse_url($url);
|
|
$domain = $parsedUrl['host'];
|
|
$path = $parsedUrl['path'];
|
|
|
|
$cacheSubDir = $this->config['cache_dir'] . '/' . $domain;
|
|
if (!file_exists($cacheSubDir)) {
|
|
mkdir($cacheSubDir, 0755, true);
|
|
}
|
|
|
|
return $cacheSubDir . '/' . md5($url) . '.' . pathinfo($path, PATHINFO_EXTENSION);
|
|
}
|
|
|
|
/**
|
|
* 检查图片是否已缓存
|
|
*
|
|
* @param string $cachePath 缓存路径
|
|
* @return bool
|
|
*/
|
|
private function isCached(string $cachePath): bool {
|
|
return file_exists($cachePath) &&
|
|
(time() - filemtime($cachePath)) < 86400; // 缓存24小时
|
|
}
|
|
|
|
/**
|
|
* 从缓存输出图片
|
|
*
|
|
* @param string $cachePath 缓存路径
|
|
*/
|
|
private function serveFromCache(string $cachePath) {
|
|
$type = $this->getImageType($cachePath);
|
|
header("Content-type: " . $type);
|
|
readfile($cachePath);
|
|
}
|
|
|
|
/**
|
|
* 获取远程图片
|
|
*
|
|
* @param string $url 图片URL
|
|
* @return string
|
|
*/
|
|
private function fetchRemoteImage(string $url): string {
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_MAXREDIRS => $this->config['max_redirects'],
|
|
CURLOPT_CONNECTTIMEOUT => $this->config['connect_timeout'],
|
|
CURLOPT_TIMEOUT => $this->config['timeout'],
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
CURLOPT_SSL_VERIFYHOST => 2,
|
|
CURLOPT_USERAGENT => $this->config['user_agent'],
|
|
CURLOPT_REFERER => parse_url($url, PHP_URL_SCHEME) . '://' . parse_url($url, PHP_URL_HOST) . '/',
|
|
CURLOPT_HTTPHEADER => ['Accept: image/*']
|
|
]);
|
|
|
|
$data = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode !== 200 || empty($data)) {
|
|
throw new Exception('Failed to fetch image: HTTP ' . $httpCode);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* 验证图片数据
|
|
*
|
|
* @param string $data 图片数据
|
|
* @return bool
|
|
*/
|
|
private function validateImageData(string $data): bool {
|
|
if (empty($data)) {
|
|
return false;
|
|
}
|
|
|
|
// 检查文件头
|
|
$headers = [
|
|
'image/jpeg' => "\xFF\xD8\xFF",
|
|
'image/png' => "\x89PNG\r\n\x1a\n",
|
|
'image/gif' => "GIF",
|
|
'image/webp' => "RIFF....WEBP",
|
|
'image/bmp' => "BM",
|
|
'image/svg+xml' => '<?xml'
|
|
];
|
|
|
|
foreach ($headers as $type => $header) {
|
|
if (strpos($data, $header) === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 保存到缓存
|
|
*
|
|
* @param string $cachePath 缓存路径
|
|
* @param string $data 图片数据
|
|
*/
|
|
private function saveToCache(string $cachePath, string $data) {
|
|
file_put_contents($cachePath, $data);
|
|
}
|
|
|
|
/**
|
|
* 输出图片
|
|
*
|
|
* @param string $data 图片数据
|
|
* @param string $type 图片类型
|
|
*/
|
|
private function outputImage(string $data, string $type) {
|
|
header("Content-type: " . $type);
|
|
header("Cache-Control: public, max-age=86400");
|
|
echo $data;
|
|
}
|
|
|
|
/**
|
|
* 获取图片类型
|
|
*
|
|
* @param string $url 图片URL
|
|
* @return string
|
|
*/
|
|
private function getImageType(string $url): string {
|
|
$ext = strtolower(pathinfo($url, PATHINFO_EXTENSION));
|
|
return self::SUPPORTED_TYPES[$ext] ?? 'image/jpeg';
|
|
}
|
|
|
|
/**
|
|
* 输出错误信息
|
|
*/
|
|
private function outputError() {
|
|
header("HTTP/1.1 500 Internal Server Error");
|
|
header("Content-type: image/jpeg");
|
|
// 输出一个1x1的透明图片
|
|
echo base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 简单的日志记录类
|
|
*/
|
|
class Logger {
|
|
private $logDir;
|
|
|
|
public function __construct(string $logDir) {
|
|
$this->logDir = $logDir;
|
|
}
|
|
|
|
public function error(string $message) {
|
|
$this->log('ERROR', $message);
|
|
}
|
|
|
|
public function info(string $message) {
|
|
$this->log('INFO', $message);
|
|
}
|
|
|
|
private function log(string $level, string $message) {
|
|
$date = date('Y-m-d');
|
|
$time = date('Y-m-d H:i:s');
|
|
$logFile = $this->logDir . "/{$date}.log";
|
|
|
|
$logMessage = "[{$time}] [{$level}] {$message}\n";
|
|
file_put_contents($logFile, $logMessage, FILE_APPEND);
|
|
}
|
|
}
|