365 lines
12 KiB
PHP
365 lines
12 KiB
PHP
<?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']) : '';
|
||
}
|
||
}
|