wpbridge/includes/Notification/NotificationManager.php
feibisi 7bb18002f7 fix: 修复 Codex v0.3.0 代码评审发现的问题
HIGH 级别修复:
- WebhookHandler: 添加 SSRF 防护 (Validator::is_valid_url)
- AIGateway: 自定义端点添加 SSRF 验证
- AIGateway: 白名单域名匹配改为大小写不敏感
- CommercialManager: 文件读取添加路径遍历防护 (realpath)
- GroupModel: from_array() 添加输入类型验证和清理
- GroupManager: add/remove_source_to_group 添加权限检查

MEDIUM 级别修复:
- NotificationManager: 添加 5 分钟速率限制防止通知轰炸
- NotificationManager: 允许第三方扩展通知处理器
- EmailHandler: 添加收件人邮箱格式验证
- EmailHandler: 移除自定义 From 头避免 SPF/DKIM 问题
- CommercialManager: lock/unlock_version 添加权限检查
- CommercialManager: 商业插件列表支持过滤器扩展
- GroupManager: toggle 方法改进原子性,先更新分组再更新源
- AbstractAdapter: 正则匹配添加错误处理

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:14:16 +08:00

231 lines
6.2 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* 通知管理器
*
* @package WPBridge
*/
namespace WPBridge\Notification;
use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 通知管理器类
*/
class NotificationManager {
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 通知处理器
*
* @var array
*/
private array $handlers = [];
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->init_handlers();
$this->init_hooks();
}
/**
* 初始化处理器
*/
private function init_handlers(): void {
$this->handlers = [
'email' => new EmailHandler( $this->settings ),
'webhook' => new WebhookHandler( $this->settings ),
];
// 允许第三方扩展通知处理器
$this->handlers = apply_filters(
'wpbridge_notification_handlers',
$this->handlers,
$this->settings
);
}
/**
* 初始化钩子
*/
private function init_hooks(): void {
add_action( 'wpbridge_update_available', [ $this, 'on_update_available' ], 10, 2 );
add_action( 'wpbridge_source_error', [ $this, 'on_source_error' ], 10, 2 );
add_action( 'wpbridge_source_recovered', [ $this, 'on_source_recovered' ], 10, 1 );
}
/**
* 发送通知
*
* @param string $type 通知类型
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
*/
public function send( string $type, string $subject, string $message, array $data = [] ): void {
// 速率限制检查:防止通知轰炸
$rate_limit_key = 'wpbridge_notification_' . md5( $type . $subject );
if ( get_transient( $rate_limit_key ) ) {
Logger::debug( '通知被速率限制', [ 'type' => $type, 'subject' => $subject ] );
return;
}
// 设置 5 分钟冷却时间
set_transient( $rate_limit_key, true, 5 * MINUTE_IN_SECONDS );
foreach ( $this->handlers as $name => $handler ) {
if ( $handler->is_enabled() && $handler->supports_type( $type ) ) {
try {
$handler->send( $subject, $message, $data );
Logger::debug( '通知发送成功', [
'handler' => $name,
'type' => $type,
] );
} catch ( \Exception $e ) {
Logger::error( '通知发送失败', [
'handler' => $name,
'error' => $e->getMessage(),
] );
}
}
}
}
/**
* 更新可用时触发
*
* @param string $slug 插件/主题 slug
* @param array $update 更新信息
*/
public function on_update_available( string $slug, array $update ): void {
$subject = sprintf(
/* translators: %s: plugin/theme name */
__( '[WPBridge] %s 有新版本可用', 'wpbridge' ),
$update['name'] ?? $slug
);
$message = sprintf(
/* translators: 1: name, 2: current version, 3: new version */
__( '%1$s 有新版本可用。当前版本: %2$s新版本: %3$s', 'wpbridge' ),
$update['name'] ?? $slug,
$update['current_version'] ?? 'unknown',
$update['new_version'] ?? 'unknown'
);
$this->send( 'update', $subject, $message, $update );
}
/**
* 源错误时触发
*
* @param string $source_id 源 ID
* @param string $error 错误信息
*/
public function on_source_error( string $source_id, string $error ): void {
$subject = sprintf(
/* translators: %s: source ID */
__( '[WPBridge] 更新源 %s 出现错误', 'wpbridge' ),
$source_id
);
$message = sprintf(
/* translators: 1: source ID, 2: error message */
__( '更新源 %1$s 出现错误: %2$s', 'wpbridge' ),
$source_id,
$error
);
$this->send( 'error', $subject, $message, [
'source_id' => $source_id,
'error' => $error,
] );
}
/**
* 源恢复时触发
*
* @param string $source_id 源 ID
*/
public function on_source_recovered( string $source_id ): void {
$subject = sprintf(
/* translators: %s: source ID */
__( '[WPBridge] 更新源 %s 已恢复', 'wpbridge' ),
$source_id
);
$message = sprintf(
/* translators: %s: source ID */
__( '更新源 %s 已恢复正常', 'wpbridge' ),
$source_id
);
$this->send( 'recovery', $subject, $message, [
'source_id' => $source_id,
] );
}
/**
* 获取处理器
*
* @param string $name 处理器名称
* @return HandlerInterface|null
*/
public function get_handler( string $name ): ?HandlerInterface {
return $this->handlers[ $name ] ?? null;
}
/**
* 获取所有处理器
*
* @return array
*/
public function get_handlers(): array {
return $this->handlers;
}
/**
* 测试通知
*
* @param string $handler_name 处理器名称
* @return bool
*/
public function test( string $handler_name ): bool {
$handler = $this->get_handler( $handler_name );
if ( null === $handler ) {
return false;
}
try {
$handler->send(
__( '[WPBridge] 测试通知', 'wpbridge' ),
__( '这是一条测试通知,如果您收到此消息,说明通知配置正确。', 'wpbridge' ),
[ 'test' => true ]
);
return true;
} catch ( \Exception $e ) {
Logger::error( '测试通知失败', [
'handler' => $handler_name,
'error' => $e->getMessage(),
] );
return false;
}
}
}