S3upload/Plugin.php

365 lines
12 KiB
PHP
Raw Normal View History

2024-07-29 19:07:14 +08:00
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
// 包含 Composer 自动加载器
require_once __DIR__ . '/vendor/autoload.php';
use Aws\S3\S3Client;
use Aws\Exception\AwsException;
/**
* S3 协议上传插件
*
* @package S3Upload
* @author 老孙
* @version 1.0.4
* @link https://www.imsun.org
*/
class S3Upload_Plugin implements Typecho_Plugin_Interface
{
/**
* 激活插件方法,如果激活失败,直接抛出异常
*
* @access public
* @return void
* @throws Typecho_Plugin_Exception
*/
public static function activate()
{
Typecho_Plugin::factory('Widget_Upload')->uploadHandle = array('S3Upload_Plugin', 'uploadHandle');
Typecho_Plugin::factory('Widget_Upload')->modifyHandle = array('S3Upload_Plugin', 'modifyHandle');
Typecho_Plugin::factory('Widget_Upload')->deleteHandle = array('S3Upload_Plugin', 'deleteHandle');
Typecho_Plugin::factory('Widget_Upload')->attachmentHandle = array('S3Upload_Plugin', 'attachmentHandle');
Typecho_Plugin::factory('Widget_Upload')->attachmentDataHandle = array('S3Upload_Plugin', 'attachmentDataHandle');
Typecho_Plugin::factory('Widget_Archive')->beforeRender = array('S3Upload_Plugin', 'Widget_Archive_beforeRender');
return _t('插件已经激活,请设置 S3 配置信息');
}
/**
* 禁用插件方法,如果禁用失败,直接抛出异常
*
* @static
* @access public
* @return void
* @throws Typecho_Plugin_Exception
*/
public static function deactivate()
{
return _t('插件已被禁用');
}
/**
* 获取插件配置面板
*
* @access public
* @param Typecho_Widget_Helper_Form $form 配置面板
* @return void
*/
public static function config(Typecho_Widget_Helper_Form $form)
{
$endpoint = new Typecho_Widget_Helper_Form_Element_Text('endpoint', null, 's3.amazonaws.com', _t('S3 Endpoint'));
$form->addInput($endpoint->addRule('required', _t('必须填写 S3 Endpoint')));
$bucket = new Typecho_Widget_Helper_Form_Element_Text('bucket', null, '', _t('Bucket 名称'));
$form->addInput($bucket->addRule('required', _t('必须填写 Bucket 名称')));
$accessKey = new Typecho_Widget_Helper_Form_Element_Text('accessKey', null, '', _t('Access Key'));
$form->addInput($accessKey->addRule('required', _t('必须填写 Access Key')));
$secretKey = new Typecho_Widget_Helper_Form_Element_Text('secretKey', null, '', _t('Secret Key'));
$form->addInput($secretKey->addRule('required', _t('必须填写 Secret Key')));
$region = new Typecho_Widget_Helper_Form_Element_Text('region', null, 'us-east-1', _t('区域'));
$form->addInput($region->addRule('required', _t('必须填写区域')));
$customDomain = new Typecho_Widget_Helper_Form_Element_Text(
'customDomain',
null,
'',
_t('自定义域名 (可选)'),
_t('如果您使用自定义域名访问 S3 存储请在此填写。例如https://cdn.yourdomain.com')
);
$form->addInput($customDomain);
$sign = new Typecho_Widget_Helper_Form_Element_Radio(
'sign',
array('open' => _t('开启'), 'close' => _t('关闭')),
'open',
_t('签名开关'),
_t('是否对 URL 进行签名')
);
$form->addInput($sign);
}
/**
* 个人用户的配置面板
*
* @access public
* @param Typecho_Widget_Helper_Form $form
* @return void
*/
public static function personalConfig(Typecho_Widget_Helper_Form $form)
{
}
/**
* 上传文件处理函数
*
* @access public
* @param array $file 上传的文件
* @return mixed
*/
public static function uploadHandle($file)
{
if (empty($file['name'])) {
return false;
}
$ext = self::getSafeName($file['name']);
if (!Widget_Upload::checkFileType($ext)) {
return false;
}
$options = Helper::options()->plugin('S3Upload');
$date = new Typecho_Date($options->gmtTime);
$path = self::getUploadDir() . $date->year . '/' . $date->month;
$fileName = sprintf('%u', crc32(uniqid())) . '.' . $ext;
$uploadPath = $path . '/' . $fileName;
$s3Client = self::getS3Client();
try {
// 上传到S3
$s3Client->putObject([
'Bucket' => $options->bucket,
'Key' => $uploadPath,
'Body' => fopen($file['tmp_name'], 'rb'),
'ACL' => 'public-read',
]);
// 保存到本地
$uploadDir = __TYPECHO_ROOT_DIR__ . '/usr/uploads/';
$localPath = $uploadDir . $uploadPath;
$localDir = dirname($localPath);
if (!is_dir($localDir)) {
mkdir($localDir, 0755, true);
}
if (!copy($file['tmp_name'], $localPath)) {
error_log("Failed to save file locally: " . $localPath);
}
return [
'name' => $file['name'],
'path' => $uploadPath,
'size' => $file['size'],
'type' => $ext,
'mime' => Typecho_Common::mimeContentType($uploadPath)
];
} catch (AwsException $e) {
throw new Typecho_Plugin_Exception(_t('文件上传失败:' . $e->getMessage()));
}
}
/**
* 修改文件处理函数
*
* @access public
* @param array $content 老文件
* @param array $file 新上传的文件
* @return mixed
*/
public static function modifyHandle($content, $file)
{
if (empty($file['name'])) {
return false;
}
$fileInfo = self::uploadHandle($file);
if ($fileInfo) {
self::deleteHandle($content);
return $fileInfo;
}
return false;
}
/**
* 删除文件
*
* @access public
* @param array $content 文件相关信息
* @return boolean
*/
public static function deleteHandle(array $content)
{
$options = Helper::options()->plugin('S3Upload');
$s3Client = self::getS3Client();
try {
$s3Client->deleteObject([
'Bucket' => $options->bucket,
'Key' => $content['attachment']->path,
]);
// 删除本地文件
$localPath = __TYPECHO_ROOT_DIR__ . '/usr/uploads/' . $content['attachment']->path;
if (file_exists($localPath)) {
unlink($localPath);
}
return true;
} catch (AwsException $e) {
error_log("Failed to delete file: " . $e->getMessage());
return false;
}
}
/**
* 获取实际文件在 S3 上的绝对访问路径
*
* @access public
* @param array $content 文件相关信息
* @return string
*/
public static function attachmentHandle($content)
{
return self::attachmentDataHandle($content);
}
public static function attachmentDataHandle($content)
{
$opt = Helper::options()->plugin('S3Upload');
$s3Client = self::getS3Client();
$path = str_replace('/usr/uploads/', self::getUploadDir(), $content['attachment']->path);
if ($opt->sign == 'open') {
$command = $s3Client->getCommand('GetObject', [
'Bucket' => $opt->bucket,
'Key' => $path
]);
$request = $s3Client->createPresignedRequest($command, '+60 minutes');
$url = (string) $request->getUri();
} else {
$url = $s3Client->getObjectUrl($opt->bucket, $path);
}
return self::setDomain($url);
}
public static function Widget_Archive_beforeRender($archive)
{
$options = Helper::options()->plugin('S3Upload');
$s3Client = self::getS3Client();
if ($options->sign == 'open') {
if ($archive->is('single')) {
$archive->text = self::refresh_cdn_url($options, $s3Client, $archive->text);
}
}
}
private static function refresh_cdn_url($options, $s3Client, $text)
{
$domain = self::getDomain();
return preg_replace_callback(
'/<img.*?src=[\'\"](.*?)[\'\"].*?>/i',
function ($matches) use ($options, $s3Client, $domain) {
$url = $matches[1];
if (strpos($url, $domain) !== false) {
$path = str_replace($domain . '/', '', $url);
$path = explode('?', $path)[0]; // Remove query string
$command = $s3Client->getCommand('GetObject', [
'Bucket' => $options->bucket,
'Key' => $path
]);
$request = $s3Client->createPresignedRequest($command, '+10 minutes');
$newUrl = (string) $request->getUri();
$newUrl = self::setDomain($newUrl);
return str_replace($url, $newUrl, $matches[0]);
}
return $matches[0];
},
$text
);
}
private static function getS3Client()
{
$options = Helper::options()->plugin('S3Upload');
return new S3Client([
'version' => 'latest',
'region' => $options->region,
'endpoint' => 'https://' . $options->endpoint,
'use_path_style_endpoint' => false,
'credentials' => [
'key' => $options->accessKey,
'secret' => $options->secretKey,
],
]);
}
private static function getDomain()
{
$opt = Helper::options()->plugin('S3Upload');
$domain = $opt->customDomain;
if (empty($domain)) {
$domain = 'https://' . $opt->bucket . '.' . $opt->endpoint;
}
return rtrim($domain, '/'); // 移除末尾的 '/'
}
private static function setDomain($url)
{
$opt = Helper::options()->plugin('S3Upload');
$s3_url = 'https://' . $opt->bucket . '.' . $opt->endpoint;
$cdn_domain = $opt->customDomain;
error_log("setDomain - Original URL: " . $url);
error_log("setDomain - S3 URL base: " . $s3_url);
error_log("setDomain - CDN Domain: " . $cdn_domain);
if (!empty($cdn_domain)) {
// 使用 parse_url 来分析 URL
$parsed_url = parse_url($url);
$s3_host = $opt->bucket . '.' . $opt->endpoint;
if (isset($parsed_url['host']) && $parsed_url['host'] === $s3_host) {
$new_url = $cdn_domain . $parsed_url['path'];
if (isset($parsed_url['query'])) {
$new_url .= '?' . $parsed_url['query'];
}
error_log("setDomain - URL replaced with CDN domain: " . $new_url);
return $new_url;
} else {
error_log("setDomain - URL doesn't match expected S3 format. Using original URL.");
}
} else {
error_log("setDomain - No CDN domain set, using original S3 URL");
}
error_log("setDomain - Final URL: " . $url);
return $url;
}
private static function getUploadDir()
{
return 'usr/uploads/';
}
private static function getSafeName($name)
{
$name = str_replace(array('"', '<', '>'), '', $name);
$name = str_replace('\\', '/', $name);
$name = false === strpos($name, '/') ? ('a' . $name) : str_replace('/', '/a', $name);
$info = pathinfo($name);
$name = substr($info['basename'], 1);
return isset($info['extension']) ? strtolower($info['extension']) : '';
}
}