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( '//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']) : ''; } }