Compare commits

..

63 commits

Author SHA1 Message Date
4ced1bb61c chore: remove sensitive docs from tracking
Remove internal docs and CLAUDE.md from git tracking.
Files kept locally but excluded via .gitignore.
2026-02-18 16:21:31 +08:00
f2d004a520 feat(commercial): add BridgeServerHandler and encrypted API key storage
- Add BridgeServerHandler as new update source type for Bridge Server
- Register BRIDGE_SERVER type in SourceType enum
- Use encrypted storage for Bridge Server API key via Encryption class

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-15 09:51:39 +08:00
12647ae3b7 feat(commercial): add BridgeClient for Go server integration
- Add BridgeClient class for wpbridge-server communication
- Update BridgeManager to use Bridge Server for plugin list
- Add Bridge Server URL and API Key settings in admin
- Add AJAX handler for testing Bridge Server connection
- Include commercial bridge spec and review docs

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-15 09:28:51 +08:00
e9ee0328ac feat(commercial): add vendor system for third-party GPL plugin sources
- Add VendorInterface, AbstractVendor, VendorManager for vendor abstraction
- Add WooCommerceVendor for WooCommerce API Manager integration
- Add BridgeManager with hybrid mode (official + vendor + custom sources)
- Add GPLValidator for GPL compliance checking
- Add LicenseProxy for license key management
- Add VendorAdmin with AJAX handlers for vendor CRUD operations
- Add vendors.php admin tab with stats, vendor list, custom plugins
- Add vendor management CSS styles and JS module
- Update main.php to include vendors tab

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-15 08:39:37 +08:00
70f7a9c9f8 fix: complete wenpai-bridge integration across all config paths
- Update Settings.php preset: arkpress → json, api.wenpai.net → updates.wenpai.net
- Update SourceRegistry.php preset: TYPE_MIRROR → TYPE_JSON, same URL change
- Add JsonHandler::get_check_url() to extract base URL for health checks
- Fix {slug} template URL causing health check failures

Addresses Codex review findings:
- High: preset config not wired into active bootstrap path
- Medium: health checks failing on templated URLs

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-14 23:27:17 +08:00
9cccdd3e8f feat: integrate with wenpai-bridge service
- Update WENPAI_OPEN preset source to use updates.wenpai.net
- Change source type from ARKPRESS to JSON with {slug} template
- Add download_link support in UpdateInfo::from_array()

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-14 22:59:23 +08:00
cbafcab983 chore: bump version to 0.9.6 2026-02-14 22:27:06 +08:00
f3108c23d2 ci: add Forgejo release workflow
- Auto build ZIP on tag push
- Version consistency check
- PHP lint
- SHA-256 checksum
- Auto create/update release

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-14 22:21:04 +08:00
2118801fb7 Merge feicode initial commit 2026-02-14 22:19:42 +08:00
9433c41233 fix: 代码评审修复 (v0.9.5)
- 添加 AJAX 输入验证和清理(sanitize_text_field)
- refresh_batch 方法添加数组键存在性检查
- 移除 JS 中多余的参数传递

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 12:29:37 +08:00
e682553280 feat: 异步批量检测插件类型 (v0.9.4)
- 修复 WordPress.org 插件检测问题(启用 API 检查)
- 实现异步批量检测,每批 5 个插件
- 按钮显示进度(如 5/20),不再弹出多个通知
- 新增 AJAX 端点:prepare_refresh、refresh_batch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 12:25:51 +08:00
cc2d1cab88 ui: 将插件类型标签移到状态区域 (v0.9.2)
- 插件类型标签(免费/商业/私有/第三方)移到右侧状态区域
- 名称区域只保留"已激活"基本状态
- 新增 wpbridge-status-type-* 样式类
- 更新版本号到 0.9.2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 12:07:51 +08:00
f8a4757f05 ui: 统一已锁定标签样式
- 使用 wpbridge-status-badge 基础样式
- 新增 wpbridge-status-locked 变体
- 修复已禁用状态使用错误颜色(改为红色)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 12:02:27 +08:00
db4efe88f0 ui: 将已锁定标签移动到状态区域
减少插件名称区域的拥挤

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:59:06 +08:00
cece77eb82 chore: 更新版本号到 0.9.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:54:55 +08:00
f10729e055 fix: 代码审查修复
- 删除重复的 .wpbridge-modal-footer CSS 定义
- 同步 CLAUDE.md 版本号到 0.9.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:49:47 +08:00
92c2676160 fix: 补充缺失的 CSS 变量定义
- 添加 --wpbridge-white, --wpbridge-black
- 添加 --wpbridge-danger, --wpbridge-danger-light
- 添加 --wpbridge-warning-bg, --wpbridge-warning-text
- 添加 --wpbridge-radius-sm, --wpbridge-radius-md, --wpbridge-radius-lg
- 添加 --wpbridge-shadow-xl
- 整理变量定义顺序

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:41:56 +08:00
33c3aca16d chore: 更新版本号到 0.9.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:36:15 +08:00
1260e274e7 refactor: 替换浏览器原生对话框为自定义模态框
- 新增 Modal 模块,支持 confirm/prompt/alert 三种类型
- 替换所有 confirm() 调用为 Modal.confirm()
- 替换 prompt() 调用为 Modal.prompt()
- 替换 alert() 调用为 Modal.alert()
- API Key 生成后显示可复制的模态框
- 添加模态框相关 CSS 样式
- 添加新的 i18n 字符串

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:34:45 +08:00
93a4b85c5e feat: 更新日志聚合显示功能 (v0.9.0)
- 新增 ChangelogManager 类,支持从多种源获取更新日志
- 支持 WordPress.org、GitHub、Gitea(菲码源库)、自定义 JSON 源
- 添加模态框 UI 显示更新日志
- 在插件/主题列表添加查看更新日志按钮
- 简单的 Markdown 转 HTML 支持
- 缓存机制减少 API 请求

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:12:43 +08:00
6322087807 feat: WordPress Site Health 集成
- 添加更新源状态检查
- 添加配置完整性检查
- 在站点健康信息中显示 WPBridge 状态
- 提供问题修复建议

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:05:15 +08:00
2b9bfab1b1 feat: 备份回滚机制
- 新增 BackupManager 类管理备份
- 更新前自动创建 ZIP 备份
- 支持一键回滚到历史版本
- 自动清理旧备份(保留最近5个)
- 设置页面添加备份开关

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:03:51 +08:00
11538f73e2 feat: 版本锁定功能 (v0.9.0)
- 新增 VersionLock 类管理版本锁定
- 支持锁定当前版本,阻止自动更新
- 在插件列表显示锁定状态徽章
- 添加锁定/解锁 AJAX 处理
- 更新 ROADMAP 添加 v0.9.0 规划

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:01:17 +08:00
e79f080bb6 fix: 更新源解析与 Forgejo 兼容 2026-02-05 05:38:39 +08:00
9aa0d13758 docs: 添加用户文档 (用户指南、API文档、FAQ)
- 创建 docs/ 目录
- 添加 USER-GUIDE.md 用户指南
- 添加 API.md REST API 文档
- 添加 FAQ.md 常见问题
- 添加 README.md 文档索引
- 更新 ROADMAP.md 标记文档完成

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 05:04:49 +08:00
3e27104c09 docs: 更新 ROADMAP 标记 v0.8.0 完成
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 05:02:57 +08:00
561df0e4cf feat: 配置导入导出功能 + 文档更新 (v0.8.0)
- 新增 ConfigManager 类处理配置导入导出
- 设置页面添加导入导出 UI
- 支持合并或覆盖导入模式
- 敏感信息(API Key)可选导出
- 更新 ROADMAP.md 标记已完成任务
- 更新版本号到 0.8.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 05:00:48 +08:00
b730a637e6 fix: 修复处理器缺失与认证/同步问题 2026-02-05 04:51:21 +08:00
36920e2c5b fix: 修复 __PHP_Incomplete_Class 错误
- 在 main.php 中添加 is_array() 检查防止损坏的缓存数据
- 在 sources.php 中添加类型检查
- 在 diagnostics.php 中添加类型检查
- 在 overview.php 中添加类型检查
- 使用 wp_options 永久存储检测结果

问题原因:transient 中存储的对象在类定义变化后
反序列化失败,变成 __PHP_Incomplete_Class

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 04:21:17 +08:00
225c63362c feat: 商业插件检测缓存和手动刷新功能
- 添加 transient 缓存检测结果 (24小时TTL)
- 添加"刷新检测"按钮,支持手动触发重新检测
- 新增 refresh_all() 方法批量重新检测所有插件
- 新增 clear_cache() 方法清除检测缓存
- 新增 get_cache_stats() 方法获取缓存统计
- AJAX 处理 wpbridge_refresh_commercial_detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 04:06:34 +08:00
4a7e5a5a00 refactor: Cloud API 重命名为 Bridge API
- API 命名空间: wpbridge/v1 → bridge/v1
- UI 标签: Cloud API → Bridge API
- 端点路径: /wp-json/bridge/v1/*

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 03:58:19 +08:00
8dc2a7740d feat: 商业插件检测功能 - 远程 JSON 配置 (v0.7.5)
- 新增 CommercialDetector 商业插件智能检测器
- 新增 RemoteConfig 远程配置获取类 (wpcy.com/api/bridge/)
- 插件列表显示商业/免费类型徽章
- 支持 WordPress.org API 检测 + 已知商业插件列表
- 支持用户手动标记插件类型
- 支持深度扫描检测 (license 关键词、商业框架)
- 性能优化:默认跳过 API 检查,使用本地缓存

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 03:48:59 +08:00
8cee08b7ef feat: 添加概览仪表板和诊断工具页面 (v0.7.0)
新增功能:
- P3: 概览页面 - 状态仪表板、快速操作、系统信息
- P1: 诊断工具 - 更新源连通性测试、系统环境检查、配置检查、诊断报告导出

技术改进:
- 添加 ARIA 属性提升可访问性
- 使用 Clipboard API 替代废弃的 execCommand
- 完善 i18n 国际化支持
- 模态框焦点管理和 ESC 键关闭

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 02:05:50 +08:00
4e5c63df71 fix: 修复通知被遮挡问题
Codex 审查修复:
- 通知容器改为插入到 .wpbridge-content 内部顶部
- 添加 position: relative 和 z-index: 100
- 移除 header 的 margin-bottom(.wpbridge-wrap 已有 padding)
- 修复 CSS 语法错误(多余的闭括号)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 01:41:25 +08:00
5bdecaad3a refactor: 通知系统改用 WordPress 原生 notice 样式
参考 WPMind 实现:
- 使用 WordPress 原生 notice 类(notice-success/error/warning/info)
- 通知显示在 header 下方,而不是固定在右下角
- 支持关闭按钮和自动消失
- 使用 dashicons 图标

移除旧的 toast 容器和样式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 01:33:17 +08:00
62811a42bd chore: 更新版本号到 0.6.4
强制浏览器刷新 CSS/JS 缓存

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 01:27:00 +08:00
0bfee7f23b refactor: 简化配置面板,移除单选按钮改用操作按钮
UI 改进:
- 移除繁琐的单选按钮组
- 直接显示自定义源输入框
- 底部提供操作按钮:保存、重置为默认、禁用/启用更新
- 根据当前状态动态显示相应按钮

交互更直观:
- 输入 URL 点保存 → 设置自定义源
- 点禁用更新 → 禁用该项目的更新
- 点重置为默认 → 清除自定义配置

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 01:25:20 +08:00
dfb8dfb6f8 fix: 添加缺失的 CSS 变量,修复自定义源字段不显示
问题:选择"自定义源"时,更新地址和访问密码字段不显示
原因:CSS 变量未定义导致样式失效

修复:
- 添加缺失的 CSS 变量(border, bg, radius 等)
- 添加 .wpbridge-custom-source-row 样式定义

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 01:15:29 +08:00
dc2a0d521f refactor: 简化项目列表 UI,统一 header 样式
UI 改进:
- 移除繁琐的更新源下拉列表,改用状态徽章显示
- 配置面板添加单选按钮组(默认/自定义/禁用)
- 自定义源字段根据模式动态显示/隐藏
- 统一 header 位置(移到 .wrap 外部)
- 统一代码风格为 WordPress 标准

后端改进:
- 新增 ajax_save_item_config 统一接口
- 支持 default/custom/disabled 三种模式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 01:11:27 +08:00
abcda2dad0 fix: 修复版本号不一致和 HTML 结构错误
- 统一 WPBRIDGE_VERSION 常量为 0.6.3
- 删除多余的 </div> 闭合标签,修复布局错位
- 确保内联配置面板在 wpbridge-project-item 内部

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:49:53 +08:00
f45315d7c7 fix: 修复搜索框自动填充和折叠按钮位置
- 搜索框添加 autocomplete="off" 禁用浏览器自动填充
- 折叠按钮从末尾移到 checkbox 后面,避免拥挤
- 参考 WPMind 的折叠按钮布局设计

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:47:39 +08:00
57655acc13 style: 调整标题栏位置,更新版本号到 0.6.3
- 将标题栏移到 .wrap 容器外部
- 代码格式化优化

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:36:50 +08:00
618a86bd37 style: 优化内联配置面板样式
- 参考 WPMind 折叠卡片设计
- 展开按钮添加 hover 和 active 状态
- 展开时项目卡片边框高亮
- 配置面板样式优化:更大的间距和更清晰的层次
- 输入框添加 focus 状态样式
- 修复 JavaScript 面板切换逻辑

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:30:12 +08:00
983634caf5 feat: P0/P1 UX 改进 - 内联折叠配置面板
- 批量操作改为下拉选择(不再使用 prompt)
- 优先级语义化:首选源/备选源/最后选择
- 术语本地化:API URL→更新地址,认证令牌→访问密码
- URL 自动推断:从 URL 识别源类型(GitHub/GitLab/Gitee/JSON)
- 内联折叠配置面板(参考 WPMind 设计,替代弹窗)
- 安全修复:URL 协议验证、hostname 精确匹配
- 添加 is_inline 属性标记内联源

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:27:32 +08:00
ae5d77b5fc docs: 添加 UX 设计改进计划
附录 B: UX 设计改进计划
- B.1 问题:更新源配置流程过于复杂
- B.2 待排查的其他设计问题
- B.3 设计原则更新(简单优先、零配置可用)
- B.4 实施优先级
- B.5 详细问题清单
  - 源编辑器表单过于复杂
  - 优先级设计反直觉(数字越小越高)
  - 术语不友好(Slug、API URL)
  - 缺少智能推断
  - 批量操作使用 prompt()
  - 测试连接需要手动触发
- B.6 改进实施路线图

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:58:51 +08:00
73bacd08a0 chore: 更新版本号到 0.6.0
方案 B 里程碑完成:
- 项目优先架构
- FAIR 协议支持
- 管理界面重构

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:44:17 +08:00
b4ded8dabe fix: 修复 Codex 评审发现的问题
安全性修复:
- ajax_save_defaults() 添加 sanitize_text_field() 清理输入
- ajax_batch_set_source() 添加操作类型白名单验证
- projects.php 子 Tab 参数添加白名单验证

Bug 修复:
- 修复 CSS 多余闭合括号导致的样式解析错误
- 修复主题截图 URL 获取方式(使用完整 URL)

用户体验改进:
- 搜索功能添加 300ms 防抖,避免频繁 DOM 操作
- 批量操作添加确认对话框
- 单个项目源变更后即时更新 UI 状态(无需刷新)
- 源选择时显示加载状态

错误处理改进:
- FAIR 请求失败时记录错误日志(WP_DEBUG 模式)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:42:16 +08:00
50046d0e21 feat: 实现方案 B 管理界面和 FAIR 协议支持
任务 #43 - 重构管理界面为项目优先:
- 新增"项目"Tab,显示已安装插件/主题列表
- 子 Tab 结构:插件 / 主题 / 默认规则
- 支持为每个项目配置更新源(默认/禁用/自定义)
- 支持批量操作(设置源/重置/禁用)
- 支持搜索过滤
- 默认规则配置界面(全局/插件/主题)

任务 #44 - 添加 FAIR 源类型支持:
- 新增 FairProtocol 类:DID 解析、ED25519 签名验证
- 新增 FairSourceAdapter 类:FAIR 源更新检查和下载
- 支持 did:fair: 格式的 DID 标识符
- 支持 sodium 扩展和 paragonie/sodium_compat 回退
- 包签名验证和文件哈希校验

新增文件:
- includes/FAIR/FairProtocol.php
- includes/FAIR/FairSourceAdapter.php
- templates/admin/tabs/projects.php
- templates/admin/partials/project-list-plugins.php
- templates/admin/partials/project-list-themes.php
- templates/admin/partials/defaults-config.php

修改文件:
- includes/Admin/AdminPage.php - 添加项目配置 AJAX 处理
- templates/admin/main.php - 添加"项目"Tab
- assets/css/admin.css - 项目列表样式
- assets/js/admin.js - 项目管理 JavaScript

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:38:32 +08:00
6f39c7bbf9 refactor: 应用 Codex 评审的低优先级改进建议
1. migrate_item_configs() 添加空 source_id 的显式检查
   - 在使用前验证 $old_source_id 非空
   - 添加明确的警告日志

2. full_cleanup() 简化逻辑
   - 移除重复的 cleanup_old_data() 调用
   - 旧数据已在 migrate() 中清理,此方法只需清理备份

3. resolve_item_type() 添加 plugin: 前缀显式检查
   - 使代码逻辑更清晰
   - 添加向后兼容注释

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:30:16 +08:00
7c483e1a4d fix: 修复 Codex 评审发现的三个问题
1. [高] ItemSourceManager.get_effective_sources() null config 处理
   - 当项目未配置时 $config 为 null,访问 $config['item_type'] 会产生 PHP 警告
   - 新增 resolve_item_type() 方法,从 item_key 前缀推断类型
   - 使用 strpos() 替代 str_starts_with() 保持 PHP 7.4 兼容性

2. [中] MigrationManager 源 ID 映射问题
   - 迁移时使用旧的 source ID,但新系统可能生成不同的 key
   - 新增 $source_id_map 属性跟踪 旧ID → 新key 映射
   - 预置源也添加映射(wporg, wenpai-mirror, fair-aspirecloud)
   - migrate_item_configs() 和 setup_defaults() 使用映射后的 key
   - 验证源存在后再写入配置

3. [低] 旧数据清理
   - 新增 cleanup_old_data() 方法,迁移成功后删除旧选项
   - 新增 full_cleanup() 方法,完全清理包括备份
   - 备份数据保留以便回滚

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:28:12 +08:00
a92920111c feat: 实现方案 B 核心数据模型和迁移逻辑
新增数据模型类:
- SourceRegistry: 源注册表管理,支持 FAIR DID
- ItemSourceManager: 项目配置管理,绑定项目与源
- DefaultsManager: 默认规则管理,类型级全局策略
- MigrationManager: 从方案 A 迁移到方案 B

特性:
- 三层架构:源注册表 + 项目配置 + 默认规则
- 支持 FAIR DID 标识符
- 支持 ED25519 签名验证配置
- 预置源:WordPress.org、文派镜像、FAIR AspireCloud
- 自动迁移和回滚机制

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:20:48 +08:00
b727d461ee docs: 添加详细数据模型草案和迁移算法
- 添加 FAIR DID 标准参考
- 定义三个核心表结构:sources、item_sources、defaults
- 添加迁移算法伪代码
- 定义冲突处理规则和回滚机制
- 添加 FAIR 兼容性建议

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:13:59 +08:00
d25ddb42d2 docs: 记录方案 B 设计讨论和 Codex 评审结论
- 记录更新源配置方式重构讨论
- 添加 Codex 评审的关键发现和建议
- 确定三层数据结构:源注册表 + 项目配置表 + 默认规则
- 记录 UI 设计建议和迁移方案

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:06:51 +08:00
50589c9a28 fix: 修复 Codex 评审发现的 API Key 问题
- 修复 API Key 字段名与 RestController 不匹配的问题
  - 使用 key_hash 替代 hash
  - 使用 key_prefix 替代 prefix
  - 添加唯一 id 字段
  - 使用 MySQL 格式时间戳
- 修复 Key 撤销使用数组索引的问题,改用稳定的 key_id
- 修复速率限制单位不一致:UI 改为"次/分钟"与后端一致
- 添加 random_bytes 异常处理
- 显示 key_prefix 便于识别

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:56:54 +08:00
9edb3749c2 feat: 完善 UI 交互功能,菜单名称改为云桥
- 添加 AJAX 处理器:清除日志、生成/撤销 API Key
- 添加 API 设置保存功能
- 更新 source-editor.php 使用新设计系统
- 菜单名称从"文派云桥"简化为"云桥"
- 补充 CSS 返回链接样式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:49:04 +08:00
be3c89cab4 feat: WPBridge v0.5.0 - 古腾堡风格界面重构
基于 WPMind 设计系统重构管理界面:

- 新增 CSS 变量设计令牌系统
- 新增 Tab 导航布局(更新源/设置/Cloud API/日志)
- 新增卡片式更新源列表
- 新增 Toast 通知组件
- 新增统计面板
- 统一按钮、徽章、开关等组件样式
- 响应式布局支持
- 更丰富的 i18n 字符串

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 21:50:30 +08:00
ca8882c3f5 fix: 修复 RestController 中错误的方法调用
将 get_update_info() 改为 get_info(),与 HandlerInterface 接口定义保持一致。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:31:10 +08:00
63344444d7 fix: 修复 Codex v0.4.0 代码评审发现的问题
HIGH 级别修复:
- ApiKeyManager: API Key 改为哈希存储 (password_hash)
- ApiKeyManager: generate() 添加权限检查
- RestController: 修复 X-Forwarded-For 伪造问题
  - 添加 get_client_ip() 方法
  - 只在可信代理环境下信任 X-Forwarded-For
  - 添加 wpbridge_trusted_proxies 过滤器
- RestController: URL 参数传递 API Key 添加警告日志
- RestController: wenpai-git repo 参数添加严格验证

MEDIUM 级别修复:
- RestController: 路由正则限制为 owner/repo 格式
- RestController: API 使用记录改为缓存批量更新
  - 每 50 次写入数据库,减少 I/O
- RestController: /status 端点移除敏感配置信息

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:23:07 +08:00
40f3226132 feat: WPBridge v0.4.0 - Cloud API
REST API 端点:
- GET /wpbridge/v1/sources - 获取所有更新源
- GET /wpbridge/v1/sources/{id} - 获取单个更新源
- GET /wpbridge/v1/check/{source_id} - 检查更新源状态
- GET /wpbridge/v1/plugins/{slug}/info - 获取插件信息
- GET /wpbridge/v1/themes/{slug}/info - 获取主题信息
- GET /wpbridge/v1/wenpai-git/{repo}/releases - 菲码源库 Releases
- GET /wpbridge/v1/status - API 状态

API 认证:
- X-WPBridge-API-Key Header
- Authorization Bearer Token
- api_key 查询参数

API Key 管理:
- ApiKeyManager: Key 生成、验证、撤销
- 支持过期时间设置
- 使用统计记录

速率限制:
- 基于 IP 或 API Key 的限流
- 可配置每分钟请求数
- 429 响应包含 retry_after

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:18:51 +08:00
5969b2f131 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
0c10e306ac feat: WPBridge v0.3.0 - AI 桥接层 + 源分组 + 商业插件支持
源分组管理:
- GroupModel: 源分组数据模型
- GroupManager: 分组 CRUD、批量管理、共享认证

AI 桥接层:
- AIGateway: pre_http_request 拦截器
- 支持透传模式和 WPMind 集成
- 用户可配置白名单
- AdapterInterface: 适配器接口
- YoastAdapter: Yoast SEO Premium AI 适配
- RankMathAdapter: Rank Math Content AI 适配

商业插件支持:
- CommercialManager: 商业插件检测和管理
- 版本锁定功能
- EDD/WooCommerce Licensing 检测
- 更新源覆盖逻辑

通知系统:
- NotificationManager: 统一通知管理
- EmailHandler: 邮件通知(HTML 模板)
- WebhookHandler: Webhook 通知
  - 支持 Slack/Discord/Teams 格式
  - HMAC 签名验证

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:05:30 +08:00
19814805f7 fix: 修复 Codex 代码评审发现的问题
HIGH 级别修复:
- AdminPage.php: 移除重复的 Logger 导入
- AdminPage.php: 修复 auth_token 双重加密问题
- AdminPage.php: 添加 action 参数白名单验证
- CacheManager.php: get_stats() 使用 $wpdb->prepare()
- source-editor.php: 不显示加密后的 token,使用占位符
- Validator.php: DNS 解析失败时视为本地地址(SSRF 防护)

MEDIUM 级别修复:
- BridgeCommand.php: 使用 WordPress 文件系统 API
- SourceModel.php: 改进 auth_token 解密回退逻辑
- SourceModel.php: 添加 URL 协议验证 (http/https)
- Encryption.php: 使用随机生成的密钥替代站点 URL 哈希

LOW 级别修复:
- BackgroundUpdater.php: 移除 PHP 8.0 联合类型语法
- Plugin.php: 使用 wp_cache_flush_group() 如果可用
- Validator.php: 添加 IPv6 私有地址检查
- uninstall.php: 清理 wpbridge_encryption_key 选项

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 19:59:13 +08:00
6f85fd4dd5 feat: WPBridge v0.2.0 - 完整插件实现
v0.1.0 核心功能:
- 更新源管理 (SourceManager, SourceModel, SourceType)
- 插件/主题更新器 (PluginUpdater, ThemeUpdater)
- 缓存系统 (CacheManager, HealthChecker, FallbackStrategy)
- 安全模块 (Validator, Encryption)
- 管理界面 (AdminPage, templates, CSS, JS)
- 预设源支持 (ArkPress, AspireCloud)

v0.2.0 新增功能:
- 性能优化 (ParallelRequestManager, RequestDeduplicator)
- 条件请求 (ConditionalRequest - ETag/Last-Modified)
- 后台更新 (BackgroundUpdater - WP-Cron)
- Git 平台支持 (GitHub, GitLab, Gitee handlers)
- WP-CLI 命令 (wp bridge source/check/cache/diagnose/config)

安全修复:
- SQL 注入防护 ($wpdb->prepare)
- GET 参数清理 (sanitize_text_field)
- auth_token 加密存储 (AES-256-CBC)
- 缓存键哈希 (md5 + site_url)
- 卸载清理 (uninstall.php)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 19:52:27 +08:00
106 changed files with 17571 additions and 21622 deletions

View file

@ -1,119 +0,0 @@
name: Auto Label

on:
pull_request:
types: [opened, synchronize]

jobs:
auto-label:
if: github.repository != 'WenPai-org/ci-workflows'
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: 自动打标签
env:
FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
API_BASE="${GITHUB_SERVER_URL:-https://feicode.com}/api/v1"
OWNER="${GITHUB_REPOSITORY%/*}"
REPO="${GITHUB_REPOSITORY#*/}"
LABELS=""

# 获取 PR 变更文件
FILES=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/pulls/$PR_NUMBER/files" | \
python3 -c "import json,sys; [print(f['filename']) for f in json.load(sys.stdin)]" 2>/dev/null)

# 根据文件类型打标签
echo "$FILES" | grep -qE '\.php$' && LABELS="$LABELS,php"
echo "$FILES" | grep -qE '\.(js|ts|tsx|jsx)$' && LABELS="$LABELS,frontend"
echo "$FILES" | grep -qE '\.(css|scss|less)$' && LABELS="$LABELS,style"
echo "$FILES" | grep -qE '\.go$' && LABELS="$LABELS,go"
echo "$FILES" | grep -qE '\.(yml|yaml)$' && LABELS="$LABELS,ci/cd"
echo "$FILES" | grep -qE '(composer\.|package\.json|go\.mod)' && LABELS="$LABELS,dependencies"
echo "$FILES" | grep -qE '\.(md|txt|rst)$' && LABELS="$LABELS,documentation"
echo "$FILES" | grep -qE '(Dockerfile|docker-compose)' && LABELS="$LABELS,docker"

# 根据 PR 大小打标签
ADDITIONS=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/pulls/$PR_NUMBER" | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('additions',0)+d.get('deletions',0))" 2>/dev/null)

if [ "$ADDITIONS" -lt 10 ] 2>/dev/null; then
LABELS="$LABELS,size/S"
elif [ "$ADDITIONS" -lt 100 ] 2>/dev/null; then
LABELS="$LABELS,size/M"
elif [ "$ADDITIONS" -lt 500 ] 2>/dev/null; then
LABELS="$LABELS,size/L"
else
LABELS="$LABELS,size/XL"
fi

# 去掉开头逗号
LABELS="${LABELS#,}"

if [ -z "$LABELS" ]; then
echo "无需添加标签"
exit 0
fi

echo "添加标签: $LABELS"

# 确保标签存在,不存在则创建
IFS=',' read -ra LABEL_ARRAY <<< "$LABELS"
LABEL_IDS="["
for LABEL in "${LABEL_ARRAY[@]}"; do
# 查找标签
LABEL_ID=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/labels?limit=50" | \
python3 -c "
import json,sys
labels=json.load(sys.stdin)
for l in labels:
if l['name']=='$LABEL':
print(l['id'])
break
" 2>/dev/null)

# 标签不存在则创建
if [ -z "$LABEL_ID" ]; then
# 根据标签类型选颜色
case "$LABEL" in
php) COLOR="#4F5D95" ;;
frontend) COLOR="#f1e05a" ;;
go) COLOR="#00ADD8" ;;
ci/cd) COLOR="#0075ca" ;;
dependencies) COLOR="#0366d6" ;;
documentation) COLOR="#0075ca" ;;
docker) COLOR="#0db7ed" ;;
style) COLOR="#e34c26" ;;
size/S) COLOR="#009900" ;;
size/M) COLOR="#FFCC00" ;;
size/L) COLOR="#FF6600" ;;
size/XL) COLOR="#CC0000" ;;
*) COLOR="#ededed" ;;
esac
LABEL_ID=$(curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/labels" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$LABEL\",\"color\":\"$COLOR\"}" | \
python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
fi

[ -n "$LABEL_ID" ] && LABEL_IDS="$LABEL_IDS$LABEL_ID,"
done
LABEL_IDS="${LABEL_IDS%,}]"

# 添加标签到 PR
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$PR_NUMBER/labels" \
-H "Content-Type: application/json" \
-d "{\"labels\":$LABEL_IDS}" > /dev/null

echo "标签添加完成"

View file

@ -1,31 +0,0 @@
name: gitleaks 密钥泄露扫描

on:
push:
branches: ['*']
pull_request:
branches: ['*']

jobs:
gitleaks:
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Run gitleaks
run: |
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
gitleaks detect --source=. --log-opts="$GITHUB_SHA~1..$GITHUB_SHA" --verbose --exit-code 1 || {
echo "::error::gitleaks 发现了潜在的密钥泄露!请检查上方输出并移除敏感信息。"
exit 1
}
else
gitleaks detect --source=. --verbose --exit-code 1 || {
echo "::error::gitleaks 发现了潜在的密钥泄露!请检查上方输出并移除敏感信息。"
exit 1
}
fi
echo "gitleaks 扫描通过,未发现密钥泄露。"

View file

@ -1,128 +0,0 @@
name: 安全扫描

on:
push:
branches: ['main', 'master']
paths:
- 'Dockerfile*'
- 'docker-compose*.yml'
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
- 'yarn.lock'
- 'pnpm-lock.yaml'
- 'go.mod'
- 'go.sum'
pull_request:
branches: ['main', 'master']
schedule:
- cron: '0 3 * * 1' # 每周一凌晨 3 点

jobs:
security-scan:
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Hadolint Dockerfile 检查
run: |
# 检查是否存在 Dockerfile
DOCKERFILES=$(find . -name 'Dockerfile*' -not -path './.git/*' 2>/dev/null)
if [ -z "$DOCKERFILES" ]; then
echo "未找到 Dockerfile跳过"
exit 0
fi
# 如果未安装 hadolint 则跳过
if ! command -v hadolint &> /dev/null; then
echo "hadolint 未安装,跳过 Dockerfile 检查"
exit 0
fi
FAILED=0
for df in $DOCKERFILES; do
echo "--- 检查 $df ---"
hadolint "$df" || FAILED=1
done
if [ $FAILED -eq 1 ]; then
echo "::warning::Dockerfile 存在规范问题,请检查上方输出"
fi

- name: PHP Composer 安全审计
run: |
if [ ! -f composer.lock ]; then
echo "未找到 composer.lock跳过"
exit 0
fi
composer audit --format=table || {
echo "::error::Composer 依赖存在已知安全漏洞"
exit 1
}
echo "Composer 安全审计通过"

- name: npm 安全审计
run: |
if [ ! -f package-lock.json ] && [ ! -f yarn.lock ] && [ ! -f pnpm-lock.yaml ]; then
echo "未找到 JS 锁文件,跳过"
exit 0
fi
# npm audit 只报告 high 和 critical
if [ -f package-lock.json ]; then
npm audit --audit-level=high || {
echo "::error::npm 依赖存在高危安全漏洞"
exit 1
}
elif [ -f pnpm-lock.yaml ]; then
pnpm audit --audit-level=high || {
echo "::error::pnpm 依赖存在高危安全漏洞"
exit 1
}
elif [ -f yarn.lock ]; then
yarn npm audit --severity high || {
echo "::error::yarn 依赖存在高危安全漏洞"
exit 1
}
fi
echo "JS 依赖安全审计通过"

- name: Go 依赖漏洞检查
run: |
if [ ! -f go.mod ]; then
echo "未找到 go.mod跳过"
exit 0
fi
# govulncheck 检查已知漏洞
if ! command -v govulncheck &> /dev/null; then
go install golang.org/x/vuln/cmd/govulncheck@latest
export PATH="$(go env GOPATH)/bin:$PATH"
fi
govulncheck ./... || {
echo "::error::Go 依赖存在已知安全漏洞"
exit 1
}
echo "Go 依赖安全检查通过"

- name: Trivy 容器镜像扫描
run: |
DOCKERFILES=$(find . -name 'Dockerfile' -not -path './.git/*' 2>/dev/null)
if [ -z "$DOCKERFILES" ]; then
echo "未找到 Dockerfile跳过镜像扫描"
exit 0
fi
if ! command -v trivy &> /dev/null; then
echo "trivy 未安装,跳过容器镜像扫描"
exit 0
fi
# 构建并扫描镜像
IMAGE_NAME="ci-security-scan:$$"
podman build -t "$IMAGE_NAME" -f Dockerfile . || {
echo "::warning::容器构建失败,跳过镜像扫描"
exit 0
}
trivy image --severity HIGH,CRITICAL --exit-code 1 "$IMAGE_NAME" || {
echo "::error::容器镜像存在高危漏洞"
podman rmi "$IMAGE_NAME" 2>/dev/null
exit 1
}
podman rmi "$IMAGE_NAME" 2>/dev/null
echo "容器镜像安全扫描通过"

View file

@ -1,119 +0,0 @@
name: Stale Issue/PR 清理

on:
schedule:
- cron: '0 5 * * 1' # 每周一凌晨 5 点
workflow_dispatch:

jobs:
stale-cleanup:
if: github.repository != 'WenPai-org/ci-workflows'
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4

- name: 清理过期 Issue 和 PR
env:
FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
API_BASE="${GITHUB_SERVER_URL:-https://feicode.com}/api/v1"
OWNER="${GITHUB_REPOSITORY%/*}"
REPO="${GITHUB_REPOSITORY#*/}"
NOW=$(date +%s)

# 受保护的标签,带这些标签的不处理
PROTECTED_LABELS="pinned|help-wanted|bug|security|wontfix"

# Issue: 60 天无活动标记 stale再过 14 天关闭
STALE_DAYS=60
CLOSE_DAYS=74
# PR: 30 天无活动标记 stale再过 14 天关闭
PR_STALE_DAYS=30
PR_CLOSE_DAYS=44

process_items() {
local TYPE=$1 # issues 或 pulls
local STALE=$2
local CLOSE=$3

PAGE=1
while true; do
if [ "$TYPE" = "issues" ]; then
ITEMS=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues?state=open&type=issues&page=$PAGE&limit=50")
else
ITEMS=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues?state=open&type=pulls&page=$PAGE&limit=50")
fi

COUNT=$(echo "$ITEMS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null)
[ "$COUNT" = "0" ] || [ -z "$COUNT" ] && break

echo "$ITEMS" | python3 -c "
import json, sys, time

items = json.load(sys.stdin)
now = $NOW
stale_seconds = $STALE * 86400
close_seconds = $CLOSE * 86400
protected = set('$PROTECTED_LABELS'.split('|'))

for item in items:
number = item['number']
title = item['title']
labels = [l['name'] for l in item.get('labels', [])]

# 跳过受保护标签
if protected & set(labels):
continue

updated = item['updated_at']
# 解析 ISO 时间
from datetime import datetime, timezone
dt = datetime.fromisoformat(updated.replace('Z', '+00:00'))
updated_ts = int(dt.timestamp())
age = now - updated_ts

if age > close_seconds:
print(f'CLOSE|{number}|{title}')
elif age > stale_seconds and 'stale' not in labels:
print(f'STALE|{number}|{title}')
" | while IFS='|' read -r ACTION NUMBER TITLE; do
if [ "$ACTION" = "STALE" ]; then
echo "标记 #$NUMBER 为 stale: $TITLE"
# 添加 stale 标签
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER/labels" \
-H "Content-Type: application/json" \
-d '{"labels":["stale"]}' > /dev/null
# 添加评论
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER/comments" \
-H "Content-Type: application/json" \
-d "{\"body\":\"此 ${TYPE%s} 已超过 $STALE 天无活动,已标记为 stale。如果 14 天内无新活动将自动关闭。\"}" > /dev/null
elif [ "$ACTION" = "CLOSE" ]; then
echo "关闭 #$NUMBER: $TITLE"
curl -s -X PATCH -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' > /dev/null
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER/comments" \
-H "Content-Type: application/json" \
-d "{\"body\":\"此 ${TYPE%s} 因长期无活动已自动关闭。如需继续讨论请重新打开。\"}" > /dev/null
fi
done

PAGE=$((PAGE + 1))
done
}

echo "=== 处理 Issues (${STALE_DAYS}天stale / ${CLOSE_DAYS}天关闭) ==="
process_items "issues" $STALE_DAYS $CLOSE_DAYS

echo "=== 处理 PRs (${PR_STALE_DAYS}天stale / ${PR_CLOSE_DAYS}天关闭) ==="
process_items "pulls" $PR_STALE_DAYS $PR_CLOSE_DAYS

echo "清理完成"

View file

@ -1,34 +0,0 @@
name: Trivy 依赖漏洞扫描

on:
push:
branches: ['main', 'master']
paths:
- 'composer.lock'
- 'package-lock.json'
- 'yarn.lock'
- 'pnpm-lock.yaml'
pull_request:
branches: ['main', 'master']
# 每周一早上 8 点定时扫描
schedule:
- cron: '0 0 * * 1'

jobs:
trivy-scan:
runs-on: docker
container:
image: aquasec/trivy:latest
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4

- name: Run Trivy filesystem scan
run: |
trivy filesystem . --scanners vuln --severity HIGH,CRITICAL --exit-code 1 --format table --ignorefile .trivyignore 2>&1 || {
echo
echo ::warning::Trivy 发现了高危/严重漏洞,请检查上方输出。
echo 如需忽略特定 CVE请在仓库根目录创建 .trivyignore 文件。
exit 1
}
echo ✅ Trivy 扫描通过,未发现高危漏洞。

View file

@ -1,41 +0,0 @@
name: WordPress 插件 CI

on:
push:
branches: ['main', 'master']
pull_request:
branches: ['main', 'master']

jobs:
ci:
if: github.repository != 'WenPai-org/ci-workflows'
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4

- name: PHP Parallel Lint
run: |
parallel-lint --exclude vendor --exclude node_modules .

- name: PHPCS 代码规范检查
run: |
# 如果仓库有自定义 phpcs 配置则使用,否则用默认 WordPress 标准
if [ -f phpcs.xml ] || [ -f phpcs.xml.dist ] || [ -f .phpcs.xml ] || [ -f .phpcs.xml.dist ]; then
phpcs .
else
phpcs --standard=WordPress-Extra \
--extensions=php \
--ignore=vendor/*,node_modules/*,tests/*,lib/* \
--report=full \
-s .
fi

- name: Gitleaks 密钥泄露扫描
run: |
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
gitleaks detect --source=. --log-opts="$GITHUB_SHA~1..$GITHUB_SHA" --verbose --exit-code 1
else
gitleaks detect --source=. --verbose --exit-code 1
fi
echo "gitleaks 扫描通过"

View file

@ -1,99 +0,0 @@
name: WordPress 插件自动发布

on:
push:
tags:
- 'v*'
- '[0-9]+.[0-9]+*'

jobs:
release:
runs-on: docker
container:
image: php:8.2-cli-alpine
steps:
- name: Install system dependencies
run: |
apk add --no-cache git curl zip unzip rsync npm composer python3

- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4

- name: Install WP-CLI
run: |
curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp

- name: Install dependencies
run: |
if [ -f composer.json ]; then
composer install --no-dev --optimize-autoloader --no-interaction
fi
if [ -f package.json ]; then
npm ci --production 2>/dev/null || true
fi

- name: Run PHPCS (if configured)
run: |
if [ -f phpcs.xml ] || [ -f phpcs.xml.dist ] || [ -f .phpcs.xml ] || [ -f .phpcs.xml.dist ]; then
composer lint 2>/dev/null || vendor/bin/phpcs . || {
echo "::error::PHPCS 检查失败,请修复代码规范问题后重新打 tag。"
exit 1
}
else
echo "未找到 PHPCS 配置,跳过代码规范检查。"
fi

- name: Generate i18n files
run: |
SLUG=$(basename $GITHUB_REPOSITORY)
if [ -d languages ] || [ -d lang ]; then
LANG_DIR=$([ -d languages ] && echo languages || echo lang)
wp i18n make-pot . "$LANG_DIR/$SLUG.pot" --allow-root 2>/dev/null || true
wp i18n make-json "$LANG_DIR/" --no-purge --allow-root 2>/dev/null || true
fi

- name: Build release ZIP
run: |
PLUGIN_SLUG=$(basename $GITHUB_REPOSITORY)
TAG_NAME=${GITHUB_REF#refs/tags/}
mkdir -p /tmp/release
rsync -a --exclude='.git' --exclude='.forgejo' --exclude='.github' \
--exclude='node_modules' --exclude='.phpcs*' --exclude='phpstan*' \
--exclude='tests' --exclude='.editorconfig' --exclude='.gitignore' \
--exclude='phpunit*' --exclude='Gruntfile*' --exclude='webpack*' \
--exclude='package.json' --exclude='package-lock.json' \
--exclude='composer.lock' --exclude='.env*' \
. /tmp/release/$PLUGIN_SLUG/
cd /tmp/release
zip -r /tmp/$PLUGIN_SLUG-$TAG_NAME.zip $PLUGIN_SLUG/
echo "ZIP_PATH=/tmp/$PLUGIN_SLUG-$TAG_NAME.zip" >> $GITHUB_ENV
echo "PLUGIN_SLUG=$PLUGIN_SLUG" >> $GITHUB_ENV
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV

- name: Create Forgejo Release
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases" \
-d "{
\"tag_name\": \"$TAG_NAME\",
\"name\": \"$PLUGIN_SLUG $TAG_NAME\",
\"body\": \"自动发布 $TAG_NAME\",
\"draft\": false,
\"prerelease\": false
}")
RELEASE_ID=$(echo $RELEASE_RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
if [ -z "$RELEASE_ID" ]; then
echo "::warning::Release 创建失败或已存在。"
exit 0
fi
curl -s -X POST \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$ZIP_PATH" \
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=$PLUGIN_SLUG-$TAG_NAME.zip"
echo "Release $TAG_NAME 发布成功!"

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
vendor/
node_modules/
.env
*.log
.DS_Store
build/
dist/

# AI & internal docs (sensitive)
CLAUDE.md
.agent/
docs/

204
CLAUDE.md
View file

@ -1,204 +0,0 @@
# WPBridge - 文派云桥

WordPress 自定义源桥接插件,为开发者和高级用户提供灵活的更新源和 AI 服务桥接能力。

## 项目信息

| 项目 | 值 |
|------|-----|
| 插件名称 | WPBridge |
| 中文名称 | 文派云桥 |
| 版本 | 0.9.5 |
| 开发目录 | `~/Projects/wpbridge/` |

## 核心定位

> **自定义源桥接器 - 让用户完全控制 WordPress 的外部连接**

### 目标 (Goals)

```
WPBridge (文派云桥)
├── 自托管插件/主题更新服务器桥接
├── 商业插件自定义更新源管理
├── AI 服务请求桥接(可选)
└── 面向开发者/高级用户
```

### 非目标 (Non-Goals)

> **明确边界,避免与文派生态其他产品重叠**

```
WPBridge 不做:
├── ❌ 官方源镜像/加速(文派叶子的职责)
├── ❌ 商业插件破解或绕过授权
├── ❌ AI 模型本体WPMind 的职责)
├── ❌ WordPress 核心汉化LitePress 的职责)
└── ❌ 镜像源基础设施WPMirror 的职责)
```

### 使用动机优先级

| 优先级 | 场景 | 用户类型 |
|--------|------|----------|
| P0 | 企业内网部署,私有仓库 | 企业用户 |
| P1 | 商业插件更新源管理 | 商业插件用户 |
| P2 | 开发测试,自托管服务器 | 开发者 |
| P3 | AI 服务桥接 | AI 插件用户 |

```
文派叶子 (WPCY) - 官方源加速
├── WordPress.org → 文派镜像
├── 开箱即用
└── 面向普通用户

WPBridge (文派云桥) - 自定义源桥接
├── 第三方自托管更新服务器
├── 商业插件自定义更新源
├── AI 服务桥接(可选)
└── 面向开发者/高级用户
```

## 与文派生态的关系

```
文派生态 (WenPai.org)
├── 📦 WPMirror (wpmirror.com)
│ └── 镜像源 - 插件/主题下载
├── 🇨🇳 LitePress (litepress.cn)
│ └── WordPress 中国定制版
├── 🍃 文派叶子 WPCY (wpcy.com)
│ ├── 中国源加速(官方源 → 文派镜像)
│ ├── 插件/主题更新加速
│ ├── 翻译下载优化
│ └── 面向普通用户
├── 🤖 WPMind 文派心思
│ └── 纯 AI 应用(国内 AI 服务)
├── 🏛️ ArkPress文派开源自托管组件
│ ├── AspireCloud 中国分叉版本
│ ├── 针对中国网络环境优化
│ ├── 自托管更新服务器(服务端)
│ └── 与 WPBridge 配合使用
└── 🌉 WPBridge 文派云桥 ← 本项目
├── 自定义更新源桥接(客户端)
├── 商业插件更新源
├── AI 服务桥接(可选)
└── 面向开发者/高级用户
```

### ArkPress 与 WPBridge 的关系

```
ArkPress服务端 WPBridge客户端
│ │
│ 自托管更新服务器 │ 连接各种更新源
│ AspireCloud 中国分叉 │ 统一管理界面
│ 提供 API 服务 │ 性能优化
│ │
└────────── 配合使用 ──────────┘
完整的自托管更新解决方案
```

## 目标用户

| 用户类型 | 需求 | WPBridge 价值 |
|----------|------|---------------|
| **企业用户** | 内网部署、私有仓库 | 配置内网更新服务器 |
| **开发者** | 测试环境、自托管 | 灵活的更新源配置 |
| **商业插件用户** | 多个商业插件管理 | 统一管理更新源 |
| **AI 插件用户** | OpenAI 无法访问 | 透明切换国内服务 |

## 核心功能

```
WPBridge (文派云桥)
├── 📦 更新源桥接
│ ├── 自托管插件/主题更新服务器
│ ├── 商业插件自定义更新源
│ ├── 私有仓库支持 (GitHub/GitLab)
│ └── 更新源管理界面
├── 🤖 AI 桥接
│ ├── OpenAI API 兼容层
│ ├── 商业插件 AI 适配器 (Yoast/Rank Math)
│ └── 依赖 WPMind 提供 AI 能力(可选)
└── 🔧 高级配置
├── 自定义 HTTP 头
├── 认证方式配置 (API Key/OAuth/Basic)
└── 代理设置
```

## 使用场景

1. **企业内网部署**
- 配置内网更新服务器
- 私有插件/主题分发

2. **商业插件管理**
- 统一管理多个商业插件的更新源
- 解决授权验证超时问题

3. **开发测试**
- 配置测试服务器进行插件开发
- 版本控制和回滚

4. **AI 服务替换**
- 将 OpenAI 请求转发到国内服务
- 商业插件 AI 功能本地化

## 相关文档

- [ROADMAP.md](ROADMAP.md) - 开发路线图
- [DISCUSSION.md](DISCUSSION.md) - 讨论记录
- [DESIGN.md](DESIGN.md) - 技术设计文档
- [ARCHITECTURE.md](ARCHITECTURE.md) - 业务流程与架构设计
- [RESEARCH.md](RESEARCH.md) - 市场研究报告

## 技术栈

- PHP 7.4+
- WordPress 5.9+
- 可选依赖WPMindAI 桥接功能)

---

*创建日期: 2026-02-04*
*最后更新: 2026-02-05*

## 更新日志

### v0.9.5 (2026-02-05)
- 代码评审修复:添加 AJAX 输入验证和清理
- refresh_batch 方法添加数组键存在性检查
- 移除 JS 中多余的参数传递

### v0.9.4 (2026-02-05)
- 修复 WordPress.org 插件检测问题(启用 API 检查)
- 实现异步批量检测,每批 5 个插件
- 按钮显示进度(如 5/20不再弹出多个通知
- 新增 AJAX 端点prepare_refresh、refresh_batch

### v0.9.2 (2026-02-05)
- 将插件类型标签移到状态区域
- 统一状态标签样式

### v0.9.1 (2026-02-05)
- 代码评审修复:重复 CSS 定义、JS 语法错误

### v0.9.0 (2026-02-05)
- 自定义模态框替换浏览器原生对话框
- 添加 CSS 变量支持
- 版本锁定、备份回滚功能
- Site Health 集成
- 更新日志聚合

View file

@ -1,222 +0,0 @@
# WPBridge API 文档

> Bridge API - REST API 接口文档

## 概述

WPBridge 提供 REST API 供外部系统调用,用于获取插件状态、管理更新源等。

## 基础信息

- **基础 URL**: `/wp-json/bridge/v1/`
- **认证方式**: API Key
- **响应格式**: JSON

## 认证

### 获取 API Key

1. 登录 WordPress 后台
2. 进入「设置 > WPBridge > API」
3. 点击「生成 API Key」
4. 保存生成的 Key只显示一次

### 使用 API Key

在请求头中添加:

```http
X-WPBridge-Key: your_api_key_here
```

或使用查询参数:

```
?api_key=your_api_key_here
```

## API 端点

### 获取状态

获取 WPBridge 插件的运行状态。

```http
GET /wp-json/bridge/v1/status
```

#### 响应示例

```json
{
"success": true,
"data": {
"version": "0.8.0",
"sources_count": 5,
"enabled_sources": 4,
"last_check": "2026-02-05 10:30:00",
"cache_status": "healthy"
}
}
```

### 获取更新源列表

获取所有配置的更新源。

```http
GET /wp-json/bridge/v1/sources
```

#### 响应示例

```json
{
"success": true,
"data": {
"sources": [
{
"id": "source_abc123",
"name": "My Update Source",
"type": "json",
"enabled": true,
"priority": 10,
"last_check": "2026-02-05 10:00:00",
"status": "healthy"
}
]
}
}
```

### 检查更新源

检查指定更新源的连通性。

```http
POST /wp-json/bridge/v1/sources/{source_id}/check
```

#### 响应示例

```json
{
"success": true,
"data": {
"status": "healthy",
"response_time": 0.234,
"last_check": "2026-02-05 10:30:00"
}
}
```

### 获取插件信息

获取指定插件的更新信息。

```http
GET /wp-json/bridge/v1/plugins/{slug}/info
```

#### 参数

| 参数 | 类型 | 说明 |
|------|------|------|
| slug | string | 插件 slug |

#### 响应示例

```json
{
"success": true,
"data": {
"name": "Example Plugin",
"slug": "example-plugin",
"version": "1.2.3",
"requires": "5.9",
"tested": "6.4",
"download_url": "https://example.com/plugin.zip"
}
}
```

### 获取主题信息

获取指定主题的更新信息。

```http
GET /wp-json/bridge/v1/themes/{slug}/info
```

#### 参数

| 参数 | 类型 | 说明 |
|------|------|------|
| slug | string | 主题 slug |

## 错误响应

### 错误格式

```json
{
"success": false,
"data": {
"code": "error_code",
"message": "错误描述"
}
}
```

### 错误代码

| 代码 | HTTP 状态 | 说明 |
|------|-----------|------|
| `unauthorized` | 401 | 未提供或无效的 API Key |
| `forbidden` | 403 | 权限不足 |
| `not_found` | 404 | 资源不存在 |
| `invalid_request` | 400 | 请求参数无效 |
| `server_error` | 500 | 服务器内部错误 |

## 速率限制

- 默认限制60 请求/分钟
- 超出限制返回 429 状态码

## 示例代码

### cURL

```bash
curl -X GET \
'https://example.com/wp-json/bridge/v1/status' \
-H 'X-WPBridge-Key: your_api_key'
```

### PHP

```php
$response = wp_remote_get( 'https://example.com/wp-json/bridge/v1/status', [
'headers' => [
'X-WPBridge-Key' => 'your_api_key',
],
] );

$data = json_decode( wp_remote_retrieve_body( $response ), true );
```

### JavaScript

```javascript
fetch('https://example.com/wp-json/bridge/v1/status', {
headers: {
'X-WPBridge-Key': 'your_api_key'
}
})
.then(response => response.json())
.then(data => console.log(data));
```

---

*最后更新: 2026-02-05*

View file

@ -1,349 +0,0 @@
# 商业插件桥接方案技术评审报告

> 评审日期: 2026-02-15
> 评审对象: COMMERCIAL-BRIDGE-SPEC.md v1.0.0-draft

---

## 评审摘要

| 级别 | 数量 | 说明 |
|------|------|------|
| High | 5 | 必须修复才能上线 |
| Medium | 6 | 建议修复 |
| Low | 4 | 可选优化 |

---

## High 级别问题

### H1: 站点 URL 哈希可被伪造

**位置**: `LicenseProxy::proxy_request()`, `Service::hashSiteURL()`

**问题**: 使用 `home_url()` 作为站点标识,攻击者可以:
1. 在本地修改 `siteurl` 选项伪造任意站点
2. 多个站点使用同一 API Key 绕过站点限制

**建议**:
```php
// 使用多因素站点指纹
$site_fingerprint = hash('sha256', implode('|', [
home_url(),
DB_NAME,
AUTH_KEY, // wp-config.php 中的密钥
php_uname('n'), // 主机名
]));
```

---

### H2: API Key 明文传输到日志

**位置**: `LicenseProxy::proxy_request()`

**问题**:
```php
Logger::debug('License proxy intercepting', [
'vendor' => $vendor,
'plugin' => $plugin_slug,
'url' => $url, // 可能包含 license_key 参数
]);
```

原始 URL 可能包含用户的原厂 license_key会被记录到日志。

**建议**:
```php
// 过滤敏感参数
private function sanitize_url_for_log(string $url): string {
return preg_replace(
'/(license_key|license|key|password|secret)=[^&]+/i',
'$1=[REDACTED]',
$url
);
}
```

---

### H3: 缺少请求签名验证

**位置**: 服务端 `HandleProxy()`

**问题**: 仅依赖 API Key 验证,缺少请求完整性校验,可能被中间人篡改。

**建议**:
```go
// 添加 HMAC 签名
func (s *Service) verifyRequestSignature(apiKey string, req *ProxyRequest, signature string) bool {
mac := hmac.New(sha256.New, []byte(apiKey))
mac.Write([]byte(req.PluginSlug + req.SiteURL + req.Action))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
```

---

### H4: 响应格式硬编码不完整

**位置**: `format_edd_response()`, `format_freemius_response()`

**问题**:
1. EDD 响应缺少 `payment_id`, `customer_name`, `customer_email` 等字段
2. Freemius 响应缺少 `secret_key`, `public_key` 等字段
3. 某些插件会校验这些字段,导致授权失败

**建议**:
- 需要逆向分析每个插件的实际校验逻辑
- 建立插件响应格式数据库,动态匹配
- 添加响应格式版本控制

---

### H5: 缺少 GPL 合规验证机制

**位置**: 整体架构

**问题**:
1. 没有自动验证插件是否真的是 GPL 授权
2. 依赖人工维护 `gpl_compatible` 字段
3. 可能误桥接非 GPL 插件导致法律风险

**建议**:
```php
// 自动检测 GPL 兼容性
class GPLValidator {
public function validate(string $plugin_path): bool {
// 1. 检查 license.txt
// 2. 检查插件头部 License 字段
// 3. 检查 readme.txt
// 4. 查询 WordPress.org API
}
}
```

---

## Medium 级别问题

### M1: 缺少速率限制实现

**位置**: 服务端 API

**问题**: 文档提到"限流保护"但没有具体实现。

**建议**:
```go
// 使用令牌桶算法
type RateLimiter struct {
limits map[string]*rate.Limiter // 按 API Key
}

func (r *RateLimiter) Allow(apiKey string) bool {
limiter := r.getLimiter(apiKey)
return limiter.Allow()
}
```

---

### M2: 缺少重试和熔断机制

**位置**: `LicenseProxy::proxy_request()`

**问题**: 代理请求失败时直接返回 false没有重试逻辑。

**建议**:
```php
private function proxy_request_with_retry(...): array {
$max_retries = 3;
$backoff = 1;

for ($i = 0; $i < $max_retries; $i++) {
$response = $this->do_proxy_request(...);
if (!is_wp_error($response)) {
return $response;
}
sleep($backoff);
$backoff *= 2;
}

// 熔断:标记服务不可用
$this->mark_service_unavailable();
return false;
}
```

---

### M3: 数据库缺少软删除

**位置**: 数据库 Schema

**问题**: `site_activations` 使用 `ON DELETE CASCADE`,订阅删除时激活记录永久丢失。

**建议**:
```sql
ALTER TABLE site_activations ADD COLUMN deleted_at TIMESTAMP NULL;
ALTER TABLE subscriptions ADD COLUMN deleted_at TIMESTAMP NULL;
-- 使用软删除而非级联删除
```

---

### M4: 缺少审计日志

**位置**: 整体架构

**问题**: `license_requests` 表只记录基本信息,缺少:
- 请求 IP
- User-Agent
- 响应时间
- 完整请求/响应内容(用于调试)

**建议**: 扩展日志表结构,添加详细审计字段。

---

### M5: BridgeManager 缺少构造函数

**位置**: `BridgeManager` 类

**问题**:
```php
class BridgeManager {
private Settings $settings;
private RemoteConfig $remote_config;
// 缺少 __construct() 初始化这些属性
```

---

### M6: 缺少插件版本兼容性检查

**位置**: 更新流程

**问题**: 桥接的插件版本可能与用户 WordPress/PHP 版本不兼容。

**建议**:
```php
public function check_compatibility(string $plugin_slug, string $version): array {
$plugin = $this->registry->get($plugin_slug);
$issues = [];

if (version_compare(get_bloginfo('version'), $plugin->min_wp_version, '<')) {
$issues[] = 'WordPress 版本过低';
}
if (version_compare(PHP_VERSION, $plugin->min_php_version, '<')) {
$issues[] = 'PHP 版本过低';
}

return $issues;
}
```

---

## Low 级别问题

### L1: 硬编码代理 URL

**位置**: `LicenseProxy::proxy_request()`

**问题**:
```php
$proxy_url = 'https://updates.wenpai.net/api/v1/license/proxy';
```

**建议**: 使用配置项,支持自定义端点。

---

### L2: 缺少健康检查端点

**位置**: 服务端 API

**建议**: 添加 `GET /api/v1/health` 端点供客户端检测服务状态。

---

### L3: 响应缺少缓存控制

**位置**: 服务端响应

**建议**: 添加适当的 Cache-Control 头,减少重复请求。

---

### L4: 缺少国际化支持

**位置**: 错误消息

**问题**: Go 服务端错误消息是英文硬编码。

**建议**: 使用错误码,客户端根据错误码显示本地化消息。

---

## 商业风险评估

### 风险 1: 原厂法律行动 (高)

**分析**:
- Elementor、Yoast 等公司有法务团队
- 可能发送 DMCA 或律师函
- GPL 不保护商标,使用插件名称可能侵权

**缓解**:
1. 用户协议明确免责
2. 不使用原厂商标/Logo
3. 准备法律意见书

### 风险 2: 原厂技术对抗 (中)

**分析**:
- 原厂可能更新授权 API 格式
- 添加更复杂的校验机制
- 检测并封禁桥接请求

**缓解**:
1. 建立 API 变更监控
2. 快速响应机制
3. 多版本适配

### 风险 3: 用户信任问题 (中)

**分析**:
- 用户可能担心安全性
- 担心插件包被篡改
- 担心服务稳定性

**缓解**:
1. 透明的安全审计
2. 提供校验和验证
3. SLA 承诺

---

## 建议优先级

1. **立即修复**: H1, H2, H3 (安全相关)
2. **上线前修复**: H4, H5, M1, M2
3. **迭代优化**: M3-M6, L1-L4

---

## 结论

方案整体架构合理,但存在多个安全和实现细节问题需要解决。建议:

1. 先完成 High 级别问题修复
2. 从 1-2 个简单插件(如 ACF Pro开始试点
3. 收集反馈后再扩展支持范围

---

*评审人: Claude Code*
*评审日期: 2026-02-15*

View file

@ -1,814 +0,0 @@
# 商业插件桥接技术规范

> 创建日期: 2026-02-15
> 状态: 待评审
> 版本: v1.0.0-draft

---

## 1. 概述

### 1.1 目标

为购买了商业插件但授权过期/无法续费的用户提供替代更新源,实现:
- 商业插件自动检测
- 授权验证代理
- 更新包下载桥接
- 订阅管理

### 1.2 核心原则

- **GPL 合规**: 只桥接 GPL 授权的插件
- **透明代理**: 不修改插件代码,只代理网络请求
- **用户自主**: 用户明确选择启用桥接

---

## 2. 系统架构

### 2.1 整体架构

```
┌─────────────────────────────────────────────────────────────────┐
│ 用户 WordPress 站点 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ CommercialDetector│ │ LicenseProxy │ │ UpdateBridge │ │
│ │ (已有) │ │ (新增) │ │ (已有扩展) │ │
│ └────────┬─────────┘ └────────┬────────┘ └────────┬────────┘ │
└───────────┼─────────────────────┼─────────────────────┼──────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ 文派云桥服务端 (wenpai-bridge) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Plugin Registry │ │ License Service │ │ CDN / Storage │ │
│ │ 插件注册表 │ │ 授权服务 │ │ 下载存储 │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```

### 2.2 数据流

```
1. 检测流程:
用户站点 → CommercialDetector → 识别商业插件 → 显示桥接选项

2. 授权流程:
商业插件授权请求 → LicenseProxy 拦截 → 文派授权服务 → 返回有效授权

3. 更新流程:
WordPress 更新检查 → UpdateBridge → wenpai-bridge API → 返回更新信息

4. 下载流程:
用户点击更新 → 下载请求 → 文派 CDN → 返回插件包
```

---

## 3. 客户端组件设计

### 3.1 LicenseProxy (新增)

#### 3.1.1 职责
- 拦截商业插件的授权验证 HTTP 请求
- 识别授权系统类型 (EDD, Freemius, WC_AM 等)
- 转发到文派授权代理服务
- 转换响应格式以匹配原厂 API

#### 3.1.2 类设计

```php
<?php
declare(strict_types=1);

namespace WPBridge\Commercial;

use WPBridge\Core\Logger;
use WPBridge\Core\Settings;

class LicenseProxy {
/**
* 支持的授权系统配置
*/
private const VENDORS = [
'edd' => [
'name' => 'EDD Software Licensing',
'patterns' => [
'/edd-sl/',
'/edd-api/',
'action=activate_license',
'action=check_license',
'action=deactivate_license',
],
'response_format' => 'edd',
],
'freemius' => [
'name' => 'Freemius',
'patterns' => [
'api.freemius.com',
'wp-json/freemius',
],
'response_format' => 'freemius',
],
'wc_am' => [
'name' => 'WooCommerce API Manager',
'patterns' => [
'wc-api/wc-am-api',
'wc-api/am-software-api',
],
'response_format' => 'wc_am',
],
'envato' => [
'name' => 'Envato Market',
'patterns' => [
'api.envato.com',
],
'response_format' => 'envato',
],
];

private Settings $settings;
private array $bridged_plugins = [];

public function __construct(Settings $settings) {
$this->settings = $settings;
$this->bridged_plugins = $this->settings->get('bridged_plugins', []);
}

/**
* 初始化钩子
*/
public function init(): void {
if (!$this->is_enabled()) {
return;
}
add_filter('pre_http_request', [$this, 'intercept_request'], 5, 3);
}

/**
* 检查是否启用
*/
private function is_enabled(): bool {
return (bool) $this->settings->get('license_proxy_enabled', false);
}

/**
* 拦截 HTTP 请求
*/
public function intercept_request($preempt, array $args, string $url) {
// 1. 检测授权系统
$vendor = $this->detect_vendor($url);
if ($vendor === null) {
return $preempt;
}

// 2. 提取插件标识
$plugin_slug = $this->extract_plugin_slug($url, $args, $vendor);
if ($plugin_slug === null) {
return $preempt;
}

// 3. 检查是否在桥接列表
if (!$this->is_bridged($plugin_slug)) {
return $preempt;
}

Logger::debug('License proxy intercepting', [
'vendor' => $vendor,
'plugin' => $plugin_slug,
'url' => $url,
]);

// 4. 代理到文派服务
return $this->proxy_request($vendor, $plugin_slug, $url, $args);
}

/**
* 检测授权系统供应商
*/
private function detect_vendor(string $url): ?string {
foreach (self::VENDORS as $vendor_key => $config) {
foreach ($config['patterns'] as $pattern) {
if (stripos($url, $pattern) !== false) {
return $vendor_key;
}
}
}
return null;
}

/**
* 提取插件 slug
*/
private function extract_plugin_slug(string $url, array $args, string $vendor): ?string {
// 从 URL 参数提取
$parsed = wp_parse_url($url);
if (isset($parsed['query'])) {
parse_str($parsed['query'], $query);
// EDD 格式
if (isset($query['item_name'])) {
return sanitize_title($query['item_name']);
}
if (isset($query['item_id'])) {
return $this->resolve_item_id($query['item_id']);
}
}

// 从 POST body 提取
if (isset($args['body']) && is_array($args['body'])) {
if (isset($args['body']['item_name'])) {
return sanitize_title($args['body']['item_name']);
}
if (isset($args['body']['product_id'])) {
return $this->resolve_item_id($args['body']['product_id']);
}
}

// Freemius 格式: /v1/plugins/{id}/...
if ($vendor === 'freemius' && preg_match('#/plugins/(\d+)/#', $url, $matches)) {
return $this->resolve_freemius_id($matches[1]);
}

return null;
}

/**
* 检查插件是否在桥接列表
*/
private function is_bridged(string $plugin_slug): bool {
return in_array($plugin_slug, $this->bridged_plugins, true);
}

/**
* 代理请求到文派服务
*/
private function proxy_request(string $vendor, string $plugin_slug, string $original_url, array $args): array {
$proxy_url = 'https://updates.wenpai.net/api/v1/license/proxy';

$response = wp_remote_post($proxy_url, [
'timeout' => 15,
'headers' => [
'Content-Type' => 'application/json',
'X-WPBridge-Key' => $this->get_api_key(),
'X-WPBridge-Site' => home_url(),
],
'body' => wp_json_encode([
'vendor' => $vendor,
'plugin_slug' => $plugin_slug,
'original_url' => $original_url,
'action' => $this->extract_action($original_url, $args),
'site_url' => home_url(),
]),
]);

if (is_wp_error($response)) {
Logger::error('License proxy failed', [
'error' => $response->get_error_message(),
]);
// 失败时不拦截,让原始请求继续
return false;
}

// 转换响应格式
return $this->transform_response($vendor, $response);
}

/**
* 转换响应格式以匹配原厂 API
*/
private function transform_response(string $vendor, array $response): array {
$body = json_decode(wp_remote_retrieve_body($response), true);
if (!isset($body['success']) || !$body['success']) {
return false; // 让原始请求继续
}

$license = $body['license'] ?? [];
// 根据不同授权系统返回不同格式
switch ($vendor) {
case 'edd':
return $this->format_edd_response($license);
case 'freemius':
return $this->format_freemius_response($license);
case 'wc_am':
return $this->format_wc_am_response($license);
default:
return $this->format_generic_response($license);
}
}

/**
* 格式化 EDD 响应
*/
private function format_edd_response(array $license): array {
$body = wp_json_encode([
'success' => true,
'license' => $license['status'] ?? 'valid',
'item_name' => $license['item_name'] ?? '',
'expires' => $license['expires'] ?? 'lifetime',
'license_limit' => $license['license_limit'] ?? 0,
'site_count' => $license['site_count'] ?? 1,
'activations_left' => $license['activations_left'] ?? 'unlimited',
'checksum' => $license['checksum'] ?? '',
]);

return [
'response' => ['code' => 200, 'message' => 'OK'],
'body' => $body,
'headers' => ['content-type' => 'application/json'],
];
}

/**
* 格式化 Freemius 响应
*/
private function format_freemius_response(array $license): array {
$body = wp_json_encode([
'id' => $license['id'] ?? 0,
'plugin_id' => $license['plugin_id'] ?? 0,
'user_id' => $license['user_id'] ?? 0,
'plan_id' => $license['plan_id'] ?? 0,
'pricing_id' => $license['pricing_id'] ?? 0,
'quota' => $license['license_limit'] ?? null,
'activated' => $license['site_count'] ?? 1,
'activated_local' => 1,
'expiration' => $license['expires'] ?? null,
'is_free_localhost' => false,
'is_block_features' => false,
'is_cancelled' => false,
]);

return [
'response' => ['code' => 200, 'message' => 'OK'],
'body' => $body,
'headers' => ['content-type' => 'application/json'],
];
}

/**
* 格式化 WC API Manager 响应
*/
private function format_wc_am_response(array $license): array {
$body = wp_json_encode([
'success' => true,
'status_check' => 'active',
'activations' => (string) ($license['site_count'] ?? 1),
'activations_limit' => (string) ($license['license_limit'] ?? 'unlimited'),
]);

return [
'response' => ['code' => 200, 'message' => 'OK'],
'body' => $body,
'headers' => ['content-type' => 'application/json'],
];
}

/**
* 获取 API Key
*/
private function get_api_key(): string {
return $this->settings->get('wenpai_api_key', '');
}

/**
* 提取操作类型
*/
private function extract_action(string $url, array $args): string {
// 从 URL 提取
if (preg_match('/action=(\w+)/', $url, $matches)) {
return $matches[1];
}

// 从 body 提取
if (isset($args['body']['edd_action'])) {
return $args['body']['edd_action'];
}
if (isset($args['body']['wc-api'])) {
return $args['body']['request'] ?? 'status';
}

return 'check_license';
}
}
```

### 3.2 BridgeManager (新增)

#### 3.2.1 职责
- 管理桥接插件列表
- 提供桥接启用/禁用 UI
- 与服务端同步可桥接插件列表

```php
<?php
declare(strict_types=1);

namespace WPBridge\Commercial;

use WPBridge\Core\Settings;
use WPBridge\Core\RemoteConfig;

class BridgeManager {
private Settings $settings;
private RemoteConfig $remote_config;

/**
* 获取可桥接的商业插件列表(从服务端)
*/
public function get_available_plugins(): array {
return $this->remote_config->get('bridgeable_plugins', []);
}

/**
* 获取已启用桥接的插件
*/
public function get_bridged_plugins(): array {
return $this->settings->get('bridged_plugins', []);
}

/**
* 启用插件桥接
*/
public function enable_bridge(string $plugin_slug): bool {
// 检查是否在可桥接列表
$available = $this->get_available_plugins();
if (!isset($available[$plugin_slug])) {
return false;
}

// 检查订阅限制
if (!$this->check_subscription_limit()) {
return false;
}

$bridged = $this->get_bridged_plugins();
if (!in_array($plugin_slug, $bridged, true)) {
$bridged[] = $plugin_slug;
$this->settings->set('bridged_plugins', $bridged);
}

return true;
}

/**
* 禁用插件桥接
*/
public function disable_bridge(string $plugin_slug): bool {
$bridged = $this->get_bridged_plugins();
$bridged = array_diff($bridged, [$plugin_slug]);
return $this->settings->set('bridged_plugins', array_values($bridged));
}

/**
* 检查订阅限制
*/
private function check_subscription_limit(): bool {
$subscription = $this->get_subscription();
if ($subscription['plan'] === 'agency') {
return true; // 无限制
}

$current_count = count($this->get_bridged_plugins());
$limit = $subscription['plugins_limit'] ?? 5;

return $current_count < $limit;
}
}
```

---

## 4. 服务端组件设计

### 4.1 License Service (wenpai-bridge 扩展)

#### 4.1.1 API 端点

```
POST /api/v1/license/proxy
- 授权代理请求

GET /api/v1/license/status
- 查询授权状态

POST /api/v1/license/activate
- 激活站点

POST /api/v1/license/deactivate
- 停用站点
```

#### 4.1.2 Go 实现

```go
// internal/license/service.go
package license

import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"time"
)

var (
ErrInvalidAPIKey = errors.New("invalid api key")
ErrPluginNotBridged = errors.New("plugin not in bridge list")
ErrSubscriptionRequired = errors.New("subscription required")
ErrSiteLimitExceeded = errors.New("site activation limit exceeded")
)

type Service struct {
db *Database
subscriptions *SubscriptionService
registry *PluginRegistry
}

type ProxyRequest struct {
Vendor string `json:"vendor"`
PluginSlug string `json:"plugin_slug"`
OriginalURL string `json:"original_url"`
Action string `json:"action"`
SiteURL string `json:"site_url"`
}

type LicenseResponse struct {
Success bool `json:"success"`
License *LicenseInfo `json:"license,omitempty"`
Error string `json:"error,omitempty"`
}

type LicenseInfo struct {
Status string `json:"status"`
Expires string `json:"expires"`
LicenseLimit int `json:"license_limit"`
SiteCount int `json:"site_count"`
ActivationsLeft string `json:"activations_left"`
Features []string `json:"features"`
ItemName string `json:"item_name,omitempty"`
Checksum string `json:"checksum,omitempty"`
}

func (s *Service) HandleProxy(ctx context.Context, apiKey string, req *ProxyRequest) (*LicenseResponse, error) {
// 1. 验证 API Key
subscription, err := s.subscriptions.GetByAPIKey(ctx, apiKey)
if err != nil {
return nil, ErrInvalidAPIKey
}

// 2. 检查订阅状态
if !subscription.IsActive() {
return nil, ErrSubscriptionRequired
}

// 3. 检查插件是否可桥接
plugin, err := s.registry.GetPlugin(ctx, req.PluginSlug)
if err != nil || !plugin.BridgeEnabled {
return nil, ErrPluginNotBridged
}

// 4. 检查站点激活限制
siteHash := s.hashSiteURL(req.SiteURL)
if !s.checkSiteLimit(ctx, subscription, siteHash) {
return nil, ErrSiteLimitExceeded
}

// 5. 记录/更新站点激活
s.recordActivation(ctx, subscription.ID, req.SiteURL, siteHash)

// 6. 构建响应
license := &LicenseInfo{
Status: "valid",
Expires: subscription.ExpiresAt.Format("2006-01-02"),
LicenseLimit: subscription.SiteLimit,
SiteCount: s.getSiteCount(ctx, subscription.ID),
ActivationsLeft: s.getActivationsLeft(subscription),
Features: []string{"updates"},
ItemName: plugin.Name,
Checksum: s.generateChecksum(subscription, plugin),
}

return &LicenseResponse{
Success: true,
License: license,
}, nil
}

func (s *Service) hashSiteURL(url string) string {
h := sha256.New()
h.Write([]byte(url))
return hex.EncodeToString(h.Sum(nil))
}

func (s *Service) checkSiteLimit(ctx context.Context, sub *Subscription, siteHash string) bool {
// Agency 计划无限制
if sub.Plan == "agency" {
return true
}

// 检查是否已激活此站点
exists, _ := s.db.SiteActivationExists(ctx, sub.ID, siteHash)
if exists {
return true
}

// 检查是否超过限制
count := s.getSiteCount(ctx, sub.ID)
return count < sub.SiteLimit
}

func (s *Service) recordActivation(ctx context.Context, subID int64, siteURL, siteHash string) {
s.db.UpsertSiteActivation(ctx, &SiteActivation{
SubscriptionID: subID,
SiteURL: siteURL,
SiteHash: siteHash,
LastSeen: time.Now(),
})
}
```

### 4.2 数据库 Schema

```sql
-- 可桥接插件注册表
CREATE TABLE bridgeable_plugins (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
vendor VARCHAR(50) NOT NULL,
vendor_url VARCHAR(500),
gpl_compatible BOOLEAN DEFAULT TRUE,
bridge_enabled BOOLEAN DEFAULT TRUE,
download_url VARCHAR(500),
latest_version VARCHAR(50),
min_wp_version VARCHAR(20),
tested_wp_version VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_vendor (vendor),
INDEX idx_enabled (bridge_enabled)
);

-- 用户订阅
CREATE TABLE subscriptions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
api_key VARCHAR(64) NOT NULL UNIQUE,
plan ENUM('free', 'pro', 'agency') DEFAULT 'free',
site_limit INT DEFAULT 1,
plugins_limit INT DEFAULT 0,
status ENUM('active', 'expired', 'cancelled') DEFAULT 'active',
expires_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_api_key (api_key),
INDEX idx_status (status)
);

-- 站点激活记录
CREATE TABLE site_activations (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
subscription_id BIGINT UNSIGNED NOT NULL,
site_url VARCHAR(500) NOT NULL,
site_hash VARCHAR(64) NOT NULL,
activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_sub_site (subscription_id, site_hash),
INDEX idx_subscription (subscription_id),
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE
);

-- 授权请求日志
CREATE TABLE license_requests (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
subscription_id BIGINT UNSIGNED,
plugin_slug VARCHAR(255) NOT NULL,
vendor VARCHAR(50) NOT NULL,
action VARCHAR(50) NOT NULL,
site_url VARCHAR(500),
success BOOLEAN DEFAULT TRUE,
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_subscription (subscription_id),
INDEX idx_plugin (plugin_slug),
INDEX idx_created (created_at)
);
```

---

## 5. 支持的商业插件

### 5.1 第一批支持 (P0)

| 插件 | Slug | 授权系统 | GPL | 状态 |
|------|------|----------|-----|------|
| Elementor Pro | elementor-pro | EDD | Yes | 待添加 |
| Yoast SEO Premium | wordpress-seo-premium | EDD | Yes | 待添加 |
| ACF Pro | advanced-custom-fields-pro | EDD | Yes | 待添加 |
| Gravity Forms | gravityforms | EDD | Yes | 待添加 |
| WPForms Pro | wpforms | EDD | Yes | 待添加 |

### 5.2 第二批支持 (P1)

| 插件 | Slug | 授权系统 | GPL | 状态 |
|------|------|----------|-----|------|
| Rank Math Pro | seo-by-rank-math-pro | Freemius | Yes | 待添加 |
| WP Rocket | wp-rocket | Custom | Yes | 待添加 |
| Perfmatters | perfmatters | EDD | Yes | 待添加 |
| FlyingPress | flavor | EDD | Yes | 待添加 |

### 5.3 不支持的插件

以下插件因非 GPL 或其他原因不支持:
- Envato 独占插件(非 GPL
- 包含 SaaS 依赖的插件(如 Jetpack Premium

---

## 6. 安全考虑

### 6.1 API Key 安全
- API Key 使用 AES-256 加密存储
- 传输使用 HTTPS
- 支持 Key 轮换

### 6.2 请求验证
- 验证请求来源站点
- 防止重放攻击nonce
- 限流保护

### 6.3 下载安全
- 所有下载包经过病毒扫描
- 提供 SHA256 校验和
- 支持签名验证

---

## 7. 商业模式

### 7.1 定价

| 计划 | 价格 | 站点数 | 插件数 | 功能 |
|------|------|--------|--------|------|
| Free | ¥0 | 1 | 0 | 检测、开源更新 |
| Pro | ¥199/年 | 3 | 5 | 商业插件桥接 |
| Agency | ¥999/年 | 无限 | 无限 | 全功能 + 优先支持 |

### 7.2 收入预测

假设:
- 第一年 1000 付费用户
- Pro:Agency = 7:3
- 续费率 60%

年收入 = 700 × 199 + 300 × 999 = ¥439,000

---

## 8. 实施计划

### Phase 1: 基础设施 (2周)
- [ ] LicenseProxy 客户端组件
- [ ] License Service 服务端
- [ ] 数据库 Schema
- [ ] 基础 API

### Phase 2: 插件支持 (2周)
- [ ] EDD 授权系统适配
- [ ] 第一批 5 个插件接入
- [ ] 下载 CDN 配置

### Phase 3: 订阅系统 (1周)
- [ ] 订阅管理 UI
- [ ] 支付集成
- [ ] API Key 管理

### Phase 4: 测试发布 (1周)
- [ ] 集成测试
- [ ] Beta 测试
- [ ] 文档完善
- [ ] 正式发布

---

## 9. 风险与缓解

### 9.1 法律风险
- **风险**: 原厂法律诉讼
- **缓解**: 只支持 GPL 插件,明确用户协议

### 9.2 技术风险
- **风险**: 原厂 API 变更
- **缓解**: 模块化设计,快速适配

### 9.3 运营风险
- **风险**: 原厂封禁
- **缓解**: 自建 CDN多源备份

---

*最后更新: 2026-02-15*

View file

@ -1,202 +0,0 @@
# WPBridge 常见问题

> 常见问题解答 (FAQ)

## 安装与配置

### Q: WPBridge 的系统要求是什么?

- WordPress 5.9 或更高版本
- PHP 7.4 或更高版本
- 建议启用 cURL 扩展

### Q: 如何安装 WPBridge

1. 下载插件 ZIP 文件
2. 在 WordPress 后台进入「插件 > 安装插件 > 上传插件」
3. 上传并激活插件
4. 进入「设置 > WPBridge」配置

### Q: WPBridge 与文派叶子WPCY有什么区别

| 功能 | 文派叶子 (WPCY) | WPBridge |
|------|-----------------|----------|
| 目标用户 | 普通用户 | 开发者/高级用户 |
| 主要功能 | 官方源加速 | 自定义源桥接 |
| 配置复杂度 | 开箱即用 | 需要配置 |
| 商业插件支持 | 有限 | 完整支持 |

两者可以同时使用WPBridge 会自动检测 WPCY 并协同工作。

---

## 更新源

### Q: 支持哪些类型的更新源?

- JSON API标准格式
- GitHub Releases
- GitLab Releases
- Gitee Releases
- ArkPress文派自托管
- AspireCloud
- Plugin Update Checker (PUC) 格式
- FAIR Package Manager

### Q: 如何添加 GitHub 仓库作为更新源?

1. 进入「更新源」标签
2. 点击「添加更新源」
3. 选择类型为「GitHub」
4. 填写仓库 URL如 `https://github.com/owner/repo`
5. 如果是私有仓库,填写 Personal Access Token
6. 保存

### Q: 更新源不工作怎么办?

1. **检查 URL**:确保 URL 格式正确
2. **测试连通性**:在「诊断」页面点击「测试」按钮
3. **检查认证**:私有源需要正确的 Token
4. **查看日志**:启用调试模式查看详细日志
5. **检查防火墙**:确保服务器可以访问更新源

### Q: 多个更新源提供同一插件时如何处理?

WPBridge 会:
1. 按优先级排序(数字越小优先级越高)
2. 比较版本号,选择最高版本
3. 如果版本相同,使用优先级最高的源

---

## 商业插件

### Q: 如何管理商业插件的更新?

1. WPBridge 会自动检测商业插件
2. 您可以为商业插件配置专用更新源
3. 或者使用插件原有的更新机制

### Q: 商业插件检测不准确怎么办?

您可以手动标记插件类型:
1. 在「概览」页面找到插件
2. 点击类型标签
3. 选择正确的类型(免费/商业/第三方)

### Q: WPBridge 会绕过商业插件的授权验证吗?

**不会**。WPBridge 只是提供更新源桥接功能,不会绕过任何授权验证。您仍需要有效的授权才能使用商业插件。

---

## 性能与缓存

### Q: WPBridge 会影响网站性能吗?

WPBridge 设计时考虑了性能:
- 使用缓存减少请求次数
- 支持并行请求
- 后台预热机制
- 失败源冷却机制

### Q: 如何清除缓存?

**方法一:管理界面**
1. 进入「诊断」标签
2. 点击「清除缓存」按钮

**方法二WP-CLI**
```bash
wp bridge cache clear
```

### Q: 缓存时间可以调整吗?

可以。在「设置」标签中可以调整缓存时间:
- 1 小时
- 6 小时
- 12 小时(默认)
- 24 小时

---

## API 与集成

### Q: 如何使用 Bridge API

1. 在「API」标签生成 API Key
2. 在请求头中添加 `X-WPBridge-Key: your_key`
3. 调用 `/wp-json/bridge/v1/` 下的端点

详见 [API 文档](API.md)。

### Q: API Key 丢失了怎么办?

API Key 只在生成时显示一次。如果丢失:
1. 撤销旧的 Key
2. 生成新的 Key

### Q: 可以与其他系统集成吗?

可以。WPBridge 提供:
- REST API 供外部调用
- WP-CLI 命令供脚本使用
- Webhook 通知功能

---

## 故障排除

### Q: 插件激活后没有菜单?

1. 检查是否有 PHP 错误
2. 尝试停用其他插件排查冲突
3. 检查用户权限(需要管理员权限)

### Q: 更新检查失败?

1. 检查网络连接
2. 检查更新源 URL 是否可访问
3. 查看「诊断」页面的错误信息
4. 启用调试模式查看详细日志

### Q: 配置丢失了怎么恢复?

如果有备份:
1. 进入「设置」标签
2. 点击「导入」按钮
3. 选择备份的 JSON 文件

如果没有备份,需要重新配置。建议定期导出配置作为备份。

### Q: 如何获取调试信息?

1. 在「设置」中启用「调试模式」
2. 在「日志」标签查看日志
3. 或使用「诊断」页面的「导出报告」功能

---

## 其他问题

### Q: WPBridge 是免费的吗?

基础功能免费,高级功能(如多站点支持)可能需要付费。

### Q: 如何获取技术支持?

- **文档**https://wenpai.org/docs/wpbridge
- **问题反馈**https://github.com/ArkPress/wpbridge/issues
- **社区支持**https://wenpai.org/community

### Q: 如何参与开发?

WPBridge 是开源项目,欢迎贡献:
1. Fork 仓库
2. 创建功能分支
3. 提交 Pull Request

---

*最后更新: 2026-02-05*

View file

@ -1,36 +0,0 @@
# WPBridge 文档

> 文派云桥 - 自定义源桥接器

## 文档目录

### 用户文档

- [用户指南](USER-GUIDE.md) - 安装、配置和使用说明
- [常见问题](FAQ.md) - FAQ 和故障排除
- [API 文档](API.md) - REST API 接口文档

### 开发文档

- [CLAUDE.md](../CLAUDE.md) - 项目概述和 AI 协作指南
- [ROADMAP.md](../ROADMAP.md) - 开发路线图
- [ARCHITECTURE.md](../ARCHITECTURE.md) - 系统架构设计
- [DESIGN.md](../DESIGN.md) - 技术设计文档

### 其他文档

- [RESEARCH.md](../RESEARCH.md) - 市场研究报告
- [DISCUSSION.md](../DISCUSSION.md) - 讨论记录

---

## 快速链接

- **安装指南**: [USER-GUIDE.md#安装](USER-GUIDE.md#安装)
- **更新源配置**: [USER-GUIDE.md#更新源管理](USER-GUIDE.md#更新源管理)
- **WP-CLI 命令**: [USER-GUIDE.md#wp-cli-命令](USER-GUIDE.md#wp-cli-命令)
- **API 认证**: [API.md#认证](API.md#认证)

---

*最后更新: 2026-02-05*

View file

@ -1,756 +0,0 @@
# WPBridge 增值方案规划

> 创建日期: 2026-02-15
> 状态: 规划中

---

## 概述

本文档规划 WPBridge 的三个核心增值方向:
1. 商业插件桥接(付费核心)
2. 企业级更新管控
3. 多站点同步管理

---

## 方案一:商业插件桥接(付费核心)

### 1.1 定位

为购买了商业插件但授权过期/无法续费的用户提供替代更新源。

### 1.2 架构

```
用户站点 (wpbridge) 文派云桥服务端
┌─────────────────┐ ┌─────────────────────┐
│ CommercialDetector │ ──检测──→ │ plugin-registry │
│ (已有) │ │ (已有 wenpai-bridge)│
├─────────────────┤ ├─────────────────────┤
│ LicenseProxy │ ──验证──→ │ /license/verify │
│ (新增) │ │ (新增) │
├─────────────────┤ ├─────────────────────┤
│ UpdateBridge │ ──更新──→ │ /plugins/{slug}/info│
│ (已有 JsonHandler)│ │ (已有) │
└─────────────────┘ └─────────────────────┘
```

### 1.3 核心功能

| 功能 | 说明 | 实现难度 | 状态 |
|------|------|----------|------|
| 商业插件检测 | CommercialDetector支持 15+ 插件 | - | ✅ 已完成 |
| 授权代理 | 拦截原厂授权请求,转发到文派验证 | 中 | 待开发 |
| 更新桥接 | JsonHandler + wenpai-bridge | - | ✅ 已完成 |
| 下载代理 | 从文派 CDN 下载,避免原厂限制 | 低 | 待开发 |

### 1.4 新增组件LicenseProxy

```php
<?php
namespace WPBridge\Commercial;

class LicenseProxy {
/**
* 支持的授权系统
*/
private array $supported_vendors = [
'edd' => [
'name' => 'EDD Software Licensing',
'patterns' => [
'api.example.com/edd-sl',
'example.com/edd-api',
],
'actions' => ['activate_license', 'deactivate_license', 'check_license'],
],
'freemius' => [
'name' => 'Freemius',
'patterns' => [
'api.freemius.com',
],
'actions' => ['activate', 'deactivate', 'ping'],
],
'envato' => [
'name' => 'Envato Market',
'patterns' => [
'api.envato.com',
'envato.developer.com',
],
'actions' => ['verify-purchase'],
],
'wc_am' => [
'name' => 'WooCommerce API Manager',
'patterns' => [
'wc-api/wc-am-api',
'wc-api/am-software-api',
],
'actions' => ['activation', 'deactivation', 'status'],
],
];

/**
* 初始化钩子
*/
public function init(): void {
add_filter('pre_http_request', [$this, 'intercept_license_check'], 10, 3);
}

/**
* 拦截授权验证请求
*/
public function intercept_license_check($preempt, array $args, string $url) {
// 1. 检测是否是已知商业插件的授权 API
$vendor = $this->detect_vendor($url);
if (!$vendor) {
return $preempt;
}

// 2. 检查该插件是否在桥接列表中
$plugin_slug = $this->extract_plugin_slug($url, $args);
if (!$this->is_bridged_plugin($plugin_slug)) {
return $preempt;
}

// 3. 转发到文派授权代理
return $this->proxy_to_wenpai($vendor, $plugin_slug, $args);
}

/**
* 检测授权系统供应商
*/
private function detect_vendor(string $url): ?string {
foreach ($this->supported_vendors as $vendor_key => $vendor_config) {
foreach ($vendor_config['patterns'] as $pattern) {
if (strpos($url, $pattern) !== false) {
return $vendor_key;
}
}
}
return null;
}

/**
* 转发到文派授权代理
*/
private function proxy_to_wenpai(string $vendor, string $plugin_slug, array $args): array {
$wenpai_url = 'https://updates.wenpai.net/api/v1/license/proxy';

$response = wp_remote_post($wenpai_url, [
'timeout' => 15,
'headers' => [
'Content-Type' => 'application/json',
'X-WPBridge-Key' => $this->get_api_key(),
'X-WPBridge-Site' => home_url(),
],
'body' => wp_json_encode([
'vendor' => $vendor,
'plugin_slug' => $plugin_slug,
'action' => $this->extract_action($args),
'site_url' => home_url(),
]),
]);

if (is_wp_error($response)) {
return $response;
}

// 转换响应格式以匹配原厂 API
return $this->transform_response($vendor, $response);
}

/**
* 检查插件是否在桥接列表中
*/
private function is_bridged_plugin(string $plugin_slug): bool {
$bridged_plugins = get_option('wpbridge_bridged_plugins', []);
return in_array($plugin_slug, $bridged_plugins, true);
}
}
```

### 1.5 服务端扩展 (wenpai-bridge)

```go
// internal/license/proxy.go

package license

import (
"encoding/json"
"net/http"
)

type ProxyRequest struct {
Vendor string `json:"vendor"`
PluginSlug string `json:"plugin_slug"`
Action string `json:"action"`
SiteURL string `json:"site_url"`
}

type ProxyResponse struct {
Success bool `json:"success"`
License LicenseInfo `json:"license,omitempty"`
Error string `json:"error,omitempty"`
}

type LicenseInfo struct {
Status string `json:"status"` // valid, expired, disabled
Expires string `json:"expires"` // 2027-01-01
LicenseLimit int `json:"license_limit"` // 站点数限制
SiteCount int `json:"site_count"` // 已激活站点数
Features []string `json:"features"` // updates, support, addons
}

// HandleLicenseProxy 处理授权代理请求
func (h *Handler) HandleLicenseProxy(w http.ResponseWriter, r *http.Request) {
var req ProxyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, "invalid request", http.StatusBadRequest)
return
}

// 1. 验证 API Key
apiKey := r.Header.Get("X-WPBridge-Key")
if !h.validateAPIKey(apiKey) {
respondError(w, "invalid api key", http.StatusUnauthorized)
return
}

// 2. 检查插件是否在桥接列表
plugin, err := h.registry.GetPlugin(req.PluginSlug)
if err != nil || !plugin.BridgeEnabled {
respondError(w, "plugin not bridged", http.StatusNotFound)
return
}

// 3. 检查用户订阅状态
subscription, err := h.subscriptions.GetByAPIKey(apiKey)
if err != nil || !subscription.IsActive() {
respondError(w, "subscription required", http.StatusPaymentRequired)
return
}

// 4. 检查站点激活限制
if !h.checkSiteLimit(subscription, req.SiteURL) {
respondError(w, "site limit exceeded", http.StatusForbidden)
return
}

// 5. 返回授权信息
license := LicenseInfo{
Status: "valid",
Expires: subscription.ExpiresAt.Format("2006-01-02"),
LicenseLimit: subscription.SiteLimit,
SiteCount: h.getSiteCount(subscription.ID),
Features: []string{"updates"},
}

respondJSON(w, ProxyResponse{
Success: true,
License: license,
})
}
```

### 1.6 数据库设计

```sql
-- 桥接插件表
CREATE TABLE wpbridge_bridged_plugins (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
vendor VARCHAR(50) NOT NULL, -- edd, freemius, envato, wc_am
original_api_url VARCHAR(500),
bridge_enabled BOOLEAN DEFAULT TRUE,
download_source VARCHAR(500), -- 文派 CDN 地址
last_version VARCHAR(50),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_vendor (vendor),
INDEX idx_enabled (bridge_enabled)
);

-- 用户订阅表
CREATE TABLE wpbridge_subscriptions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
api_key VARCHAR(64) NOT NULL UNIQUE,
plan ENUM('free', 'pro', 'agency') DEFAULT 'free',
site_limit INT DEFAULT 1,
plugins_limit INT DEFAULT 0, -- 0 = unlimited for agency
status ENUM('active', 'expired', 'cancelled') DEFAULT 'active',
expires_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_status (status)
);

-- 站点激活记录表
CREATE TABLE wpbridge_site_activations (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
subscription_id BIGINT UNSIGNED NOT NULL,
site_url VARCHAR(500) NOT NULL,
site_hash VARCHAR(64) NOT NULL, -- SHA256(site_url)
activated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_seen DATETIME,
UNIQUE KEY uk_sub_site (subscription_id, site_hash),
INDEX idx_subscription (subscription_id)
);
```

### 1.7 商业模式

| 层级 | 价格 | 功能 |
|------|------|------|
| Free | 免费 | 开源插件更新、商业插件检测 |
| Pro | ¥199/年 | 商业插件更新桥接5个插件3个站点 |
| Agency | ¥999/年 | 无限插件 + 无限站点 + 授权代理 + 优先支持 |

### 1.8 支持的商业插件(初期)

| 插件 | 授权系统 | 优先级 |
|------|----------|--------|
| Elementor Pro | EDD | 高 |
| Yoast SEO Premium | EDD | 高 |
| ACF Pro | EDD | 高 |
| Gravity Forms | EDD | 高 |
| WP Rocket | Custom | 中 |
| Rank Math Pro | Freemius | 中 |
| WPForms Pro | EDD | 中 |

### 1.9 风险与合规

#### GPL 合规
- 只桥接 GPL 授权的插件更新
- 不破解非 GPL 插件(如 Envato 独占插件)
- 明确声明"替代更新源"而非"破解授权"

#### 法律风险
- 可能被原厂封禁 API
- 需要备用下载源(文派 CDN
- 用户协议明确免责条款

#### 技术风险
- 原厂 API 变更需要及时适配
- 需要持续维护插件兼容性
- 下载包需要安全扫描

---

## 方案二:企业级更新管控

### 2.1 定位

为企业/代理商提供 WordPress 更新的集中管控能力。

### 2.2 核心功能

| 功能 | 说明 | 优先级 | 状态 |
|------|------|--------|------|
| 版本锁定 | 锁定到指定版本,阻止自动更新 | 高 | 已有基础 |
| 更新审批 | 更新前需管理员审批 | 高 | 待开发 |
| 回滚机制 | 更新失败自动回滚 | 高 | 待开发 |
| 更新日志 | 聚合显示所有插件 changelog | 中 | 待开发 |
| 安全扫描 | 更新前检查 VirusTotal | 中 | 待开发 |

### 2.3 新增组件UpdateApproval

```php
<?php
namespace WPBridge\Enterprise;

class UpdateApproval {
const STATUS_PENDING = 'pending';
const STATUS_APPROVED = 'approved';
const STATUS_REJECTED = 'rejected';
const STATUS_AUTO = 'auto_approved';

/**
* 审批规则
*/
private array $rules = [];

/**
* 初始化
*/
public function init(): void {
add_filter('pre_set_site_transient_update_plugins', [$this, 'filter_updates'], 100);
add_filter('pre_set_site_transient_update_themes', [$this, 'filter_theme_updates'], 100);
add_action('admin_menu', [$this, 'add_approval_menu']);
add_action('wp_ajax_wpbridge_approve_update', [$this, 'ajax_approve']);
add_action('wp_ajax_wpbridge_reject_update', [$this, 'ajax_reject']);
}

/**
* 过滤更新,创建审批请求
*/
public function filter_updates($transient) {
if (empty($transient->response)) {
return $transient;
}

foreach ($transient->response as $file => $update) {
$slug = dirname($file);

// 检查是否需要审批
if ($this->requires_approval($slug, $update)) {
// 检查是否已审批
if (!$this->is_approved($slug, $update->new_version)) {
// 创建审批请求
$this->create_approval_request($file, $update);
// 从更新列表移除
unset($transient->response[$file]);
// 添加到待审批列表
$transient->no_update[$file] = $update;
}
}
}

return $transient;
}

/**
* 检查是否需要审批
*/
private function requires_approval(string $slug, object $update): bool {
// 规则 1: 主版本更新需要审批
if ($this->is_major_update($update)) {
return true;
}

// 规则 2: 指定插件需要审批
$require_approval_list = get_option('wpbridge_require_approval', []);
if (in_array($slug, $require_approval_list, true)) {
return true;
}

// 规则 3: 全局审批模式
$global_mode = get_option('wpbridge_approval_mode', 'none');
if ($global_mode === 'all') {
return true;
}

return false;
}

/**
* 创建审批请求
*/
private function create_approval_request(string $file, object $update): int {
global $wpdb;

$table = $wpdb->prefix . 'wpbridge_approvals';

// 检查是否已存在
$existing = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM $table WHERE plugin_file = %s AND new_version = %s AND status = %s",
$file,
$update->new_version,
self::STATUS_PENDING
));

if ($existing) {
return (int) $existing;
}

// 获取 changelog
$changelog = $this->fetch_changelog($update);

$wpdb->insert($table, [
'plugin_file' => $file,
'plugin_name' => $this->get_plugin_name($file),
'current_version' => $this->get_current_version($file),
'new_version' => $update->new_version,
'changelog' => $changelog,
'status' => self::STATUS_PENDING,
'created_at' => current_time('mysql'),
]);

// 发送通知
$this->notify_admins($file, $update);

return $wpdb->insert_id;
}

/**
* 审批更新
*/
public function approve(int $approval_id, int $user_id): bool {
global $wpdb;
$table = $wpdb->prefix . 'wpbridge_approvals';

return (bool) $wpdb->update(
$table,
[
'status' => self::STATUS_APPROVED,
'approved_by' => $user_id,
'approved_at' => current_time('mysql'),
],
['id' => $approval_id]
);
}

/**
* 拒绝更新
*/
public function reject(int $approval_id, int $user_id, string $reason = ''): bool {
global $wpdb;
$table = $wpdb->prefix . 'wpbridge_approvals';

return (bool) $wpdb->update(
$table,
[
'status' => self::STATUS_REJECTED,
'approved_by' => $user_id,
'approved_at' => current_time('mysql'),
'reject_reason' => $reason,
],
['id' => $approval_id]
);
}
}
```

### 2.4 新增组件BackupManager扩展

```php
<?php
namespace WPBridge\Enterprise;

class BackupManager {
const BACKUP_DIR = 'wpbridge-backups';
const MAX_BACKUPS = 3;

/**
* 更新前自动备份
*/
public function pre_update_backup(string $plugin_file): ?string {
$plugin_dir = WP_PLUGIN_DIR . '/' . dirname($plugin_file);

if (!is_dir($plugin_dir)) {
return null;
}

$backup_path = $this->create_backup($plugin_file);

if ($backup_path) {
$this->store_backup_meta($plugin_file, $backup_path);
$this->cleanup_old_backups($plugin_file);
}

return $backup_path;
}

/**
* 创建备份
*/
private function create_backup(string $plugin_file): ?string {
$plugin_slug = dirname($plugin_file);
$plugin_dir = WP_PLUGIN_DIR . '/' . $plugin_slug;
$version = $this->get_plugin_version($plugin_file);

$backup_dir = WP_CONTENT_DIR . '/' . self::BACKUP_DIR;
if (!is_dir($backup_dir)) {
wp_mkdir_p($backup_dir);
// 添加 .htaccess 保护
file_put_contents($backup_dir . '/.htaccess', 'deny from all');
}

$backup_filename = sprintf(
'%s-%s-%s.zip',
$plugin_slug,
$version,
date('Ymd-His')
);
$backup_path = $backup_dir . '/' . $backup_filename;

// 创建 ZIP
$zip = new \ZipArchive();
if ($zip->open($backup_path, \ZipArchive::CREATE) !== true) {
return null;
}

$this->add_dir_to_zip($zip, $plugin_dir, $plugin_slug);
$zip->close();

return $backup_path;
}

/**
* 一键回滚
*/
public function rollback(string $plugin_file, ?string $version = null): bool {
$backup = $this->get_backup($plugin_file, $version);

if (!$backup || !file_exists($backup['path'])) {
return false;
}

// 停用插件
deactivate_plugins($plugin_file);

// 删除当前版本
$plugin_dir = WP_PLUGIN_DIR . '/' . dirname($plugin_file);
$this->delete_directory($plugin_dir);

// 解压备份
$zip = new \ZipArchive();
if ($zip->open($backup['path']) !== true) {
return false;
}
$zip->extractTo(WP_PLUGIN_DIR);
$zip->close();

// 重新激活插件
activate_plugin($plugin_file);

// 记录回滚
$this->log_rollback($plugin_file, $backup['version']);

return true;
}

/**
* 获取备份列表
*/
public function get_backups(string $plugin_file): array {
$backups = get_option('wpbridge_backups', []);
$plugin_slug = dirname($plugin_file);

return $backups[$plugin_slug] ?? [];
}

/**
* 清理旧备份
*/
private function cleanup_old_backups(string $plugin_file): void {
$backups = $this->get_backups($plugin_file);

if (count($backups) <= self::MAX_BACKUPS) {
return;
}

// 按时间排序,删除最旧的
usort($backups, fn($a, $b) => $b['created_at'] <=> $a['created_at']);

$to_delete = array_slice($backups, self::MAX_BACKUPS);
foreach ($to_delete as $backup) {
if (file_exists($backup['path'])) {
unlink($backup['path']);
}
}

// 更新记录
$plugin_slug = dirname($plugin_file);
$all_backups = get_option('wpbridge_backups', []);
$all_backups[$plugin_slug] = array_slice($backups, 0, self::MAX_BACKUPS);
update_option('wpbridge_backups', $all_backups);
}
}
```

### 2.5 数据库表

```sql
-- 审批请求表
CREATE TABLE wp_wpbridge_approvals (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
plugin_file VARCHAR(255) NOT NULL,
plugin_name VARCHAR(255),
current_version VARCHAR(50),
new_version VARCHAR(50) NOT NULL,
changelog TEXT,
status ENUM('pending', 'approved', 'rejected', 'auto_approved') DEFAULT 'pending',
approved_by BIGINT UNSIGNED,
approved_at DATETIME,
reject_reason TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_plugin (plugin_file),
INDEX idx_created (created_at)
);
```

### 2.6 商业模式

| 层级 | 价格 | 功能 |
|------|------|------|
| Free | 免费 | 版本锁定3个插件 |
| Pro | ¥299/年 | 无限锁定 + 回滚 + 更新日志 |
| Enterprise | ¥1999/年 | 审批流程 + 安全扫描 + API + 多用户 |

---

## 方案三:多站点同步管理

### 3.1 定位

为管理多个 WordPress 站点的代理商/企业提供统一配置管理。

### 3.2 架构

```
┌─────────────────────────────────────────────────────────┐
│ WPBridge Hub (中心) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 配置管理 │ │ 站点监控 │ │ 批量操作 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 站点 A │ │ 站点 B │ │ 站点 C │
│ wpbridge │ │ wpbridge │ │ wpbridge │
│ (Agent) │ │ (Agent) │ │ (Agent) │
└─────────────┘ └─────────────┘ └─────────────┘
```

### 3.3 核心功能

| 功能 | 说明 | 优先级 |
|------|------|--------|
| 配置同步 | 中心配置自动同步到所有站点 | 高 |
| 站点监控 | 实时监控所有站点更新状态 | 高 |
| 批量更新 | 一键更新所有站点的指定插件 | 中 |
| 分组管理 | 按客户/项目分组管理站点 | 中 |
| 报告生成 | 生成更新状态报告 | 低 |

### 3.4 实现方式

推荐 SaaS 中心化方案,在 wenpai.net 上提供 Hub 服务。

### 3.5 商业模式

| 层级 | 价格 | 功能 |
|------|------|------|
| Free | 免费 | 单站点使用 |
| Pro | ¥499/年 | 最多 10 个站点同步 |
| Agency | ¥1999/年 | 最多 100 个站点 + 白标 |
| Enterprise | ¥9999/年 | 无限站点 + 自托管 Hub + API |

---

## 方案对比

| 维度 | 商业插件桥接 | 企业级管控 | 多站点同步 |
|------|-------------|-----------|-----------|
| 开发难度 | 中 | 中 | 高 |
| 市场需求 | 高(刚需) | 中 | 中 |
| 付费意愿 | 高 | 中 | 高(代理商) |
| 法律风险 | 中 | 低 | 低 |
| 竞品情况 | 少 | 多 | 少 |
| 与现有代码复用 | 高 | 高 | 中 |

---

## 建议优先级

1. **先完成 v0.9.0 路线图**(版本锁定 + 回滚)- 企业级管控的基础
2. **商业插件桥接** - 差异化竞争点,与 wenpai-bridge 协同
3. **多站点同步** - 作为 Pro/Agency 版本的高级功能

---

*最后更新: 2026-02-15*

View file

@ -1,268 +0,0 @@
# WPBridge 用户指南

> 自定义源桥接器 - 让用户完全控制 WordPress 的外部连接

## 目录

- [简介](#简介)
- [安装](#安装)
- [快速开始](#快速开始)
- [更新源管理](#更新源管理)
- [商业插件检测](#商业插件检测)
- [配置导入导出](#配置导入导出)
- [WP-CLI 命令](#wp-cli-命令)
- [Bridge API](#bridge-api)
- [常见问题](#常见问题)

---

## 简介

WPBridge文派云桥是一个 WordPress 插件,允许您配置自定义的插件和主题更新源。

### 主要功能

- **自定义更新源**:支持 JSON API、GitHub、GitLab、Gitee 等多种更新源
- **商业插件管理**:自动检测商业插件,支持自定义更新源
- **源分组**:批量管理多个更新源
- **Bridge API**:提供 REST API 供外部调用
- **WP-CLI 支持**:命令行管理更新源

### 适用场景

- 企业内网部署,需要私有更新服务器
- 商业插件用户,需要统一管理更新源
- 开发者测试环境
- 需要 AI 服务桥接的用户

---

## 安装

### 要求

- WordPress 5.9+
- PHP 7.4+

### 安装步骤

1. 下载插件 ZIP 文件
2. 在 WordPress 后台进入「插件 > 安装插件 > 上传插件」
3. 上传并激活插件
4. 进入「设置 > WPBridge」配置插件

---

## 快速开始

### 1. 访问设置页面

激活插件后,在 WordPress 后台菜单找到「设置 > WPBridge」。

### 2. 查看概览

概览页面显示:
- 已配置的更新源数量
- 源健康状态
- 最近的更新检查

### 3. 添加更新源

1. 点击「更新源」标签
2. 点击「添加更新源」按钮
3. 填写更新源信息:
- **名称**:更新源的显示名称
- **类型**JSON API / GitHub / GitLab / Gitee 等
- **URL**:更新源的 API 地址
- **项目类型**:插件或主题
4. 点击「保存」

---

## 更新源管理

### 支持的更新源类型

| 类型 | 说明 | URL 格式 |
|------|------|----------|
| JSON API | 标准 JSON 格式 | `https://example.com/updates.json` |
| GitHub | GitHub Releases | `https://github.com/owner/repo` |
| GitLab | GitLab Releases | `https://gitlab.com/owner/repo` |
| Gitee | Gitee Releases | `https://gitee.com/owner/repo` |
| ArkPress | 文派自托管方案 | `https://api.example.com/v1` |
| AspireCloud | AspireCloud 服务 | `https://api.aspirecloud.com` |
| PUC | Plugin Update Checker | `https://example.com/plugin-info.json` |

### 更新源优先级

当多个更新源提供同一插件的更新时WPBridge 会:
1. 按优先级排序(数字越小优先级越高)
2. 选择版本号最高的更新

### 认证配置

对于需要认证的更新源:
1. 在更新源设置中填写 API Token
2. 支持 API Key、Basic Auth、自定义 HTTP 头

---

## 商业插件检测

WPBridge 可以自动检测已安装的商业插件。

### 检测方式

1. **远程配置**:从云端获取已知商业插件列表
2. **WordPress.org 检查**:不在官方目录的插件标记为第三方
3. **手动标记**:用户可手动设置插件类型

### 插件类型

- **免费**WordPress.org 官方目录中的插件
- **商业**:已知的商业插件
- **第三方**:不在官方目录的其他插件

### 刷新检测

点击「刷新检测」按钮可重新检测所有插件类型。

---

## 配置导入导出

### 导出配置

1. 进入「设置」标签
2. 在「配置导入导出」区域点击「导出」
3. 可选择是否包含敏感信息API Key 等)
4. 下载 JSON 配置文件

### 导入配置

1. 点击「导入」按钮
2. 选择之前导出的 JSON 文件
3. 选择导入模式:
- **合并**:与现有配置合并
- **覆盖**:完全替换现有配置
4. 确认导入

---

## WP-CLI 命令

WPBridge 提供完整的 WP-CLI 支持。

### 更新源管理

```bash
# 列出所有更新源
wp bridge source list

# 添加更新源
wp bridge source add https://example.com/updates.json --name="My Source"

# 删除更新源
wp bridge source remove <source_id>

# 启用/禁用更新源
wp bridge source enable <source_id>
wp bridge source disable <source_id>
```

### 缓存管理

```bash
# 清除缓存
wp bridge cache clear

# 查看缓存状态
wp bridge cache status
```

### 诊断

```bash
# 检查所有更新源
wp bridge check

# 运行诊断
wp bridge diagnose
```

### 配置管理

```bash
# 导出配置
wp bridge config export /path/to/config.json

# 导入配置
wp bridge config import /path/to/config.json
```

---

## Bridge API

WPBridge 提供 REST API 供外部调用。

### 启用 API

1. 进入「API」标签
2. 点击「生成 API Key」
3. 保存生成的 Key只显示一次

### API 端点

```
GET /wp-json/bridge/v1/status
```

返回插件状态信息。

### 认证

在请求头中添加:
```
X-WPBridge-Key: your_api_key
```

---

## 常见问题

### Q: 更新源不工作怎么办?

1. 检查更新源 URL 是否正确
2. 在「诊断」页面测试源连通性
3. 检查是否需要认证信息
4. 查看调试日志(需启用调试模式)

### Q: 如何处理商业插件更新?

1. WPBridge 会自动检测商业插件
2. 您可以为商业插件配置自定义更新源
3. 或者手动标记插件类型

### Q: 配置丢失怎么恢复?

1. 如果有备份,使用「导入配置」功能恢复
2. 如果没有备份,需要重新配置

### Q: 如何与文派叶子配合使用?

WPBridge 会自动检测文派叶子WPCY的存在
- 官方源更新走 WPCY 加速
- 自定义源走 WPBridge 配置

---

## 获取帮助

- **文档**https://wenpai.org/docs/wpbridge
- **问题反馈**https://github.com/ArkPress/wpbridge/issues
- **社区支持**https://wenpai.org/community

---

*最后更新: 2026-02-05*

View file

@ -13,7 +13,7 @@ use WPBridge\Security\Validator;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -22,342 +22,333 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class AIGateway {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 已注册的适配器
*
* @var array
*/
private array $adapters = array();
/**
* 已注册的适配器
*
* @var array
*/
private array $adapters = [];

/**
* 白名单域名
*
* @var array
*/
private array $whitelist = array();
/**
* 白名单域名
*
* @var array
*/
private array $whitelist = [];

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->whitelist = $this->get_whitelist();
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->whitelist = $this->get_whitelist();

$this->init_hooks();
}
$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 只有启用 AI 桥接时才注册钩子
if ( ! $this->is_enabled() ) {
return;
}
/**
* 初始化钩子
*/
private function init_hooks(): void {
// 只有启用 AI 桥接时才注册钩子
if ( ! $this->is_enabled() ) {
return;
}

add_filter( 'pre_http_request', array( $this, 'intercept_request' ), 10, 3 );
}
add_filter( 'pre_http_request', [ $this, 'intercept_request' ], 10, 3 );
}

/**
* 是否启用 AI 桥接
*
* @return bool
*/
public function is_enabled(): bool {
$ai_settings = $this->settings->get( 'ai_bridge', array() );
return ! empty( $ai_settings['enabled'] );
}
/**
* 是否启用 AI 桥接
*
* @return bool
*/
public function is_enabled(): bool {
$ai_settings = $this->settings->get( 'ai_bridge', [] );
return ! empty( $ai_settings['enabled'] );
}

/**
* 获取桥接模式
*
* @return string disabled|passthrough|wpmind
*/
public function get_mode(): string {
$ai_settings = $this->settings->get( 'ai_bridge', array() );
return $ai_settings['mode'] ?? 'disabled';
}
/**
* 获取桥接模式
*
* @return string disabled|passthrough|wpmind
*/
public function get_mode(): string {
$ai_settings = $this->settings->get( 'ai_bridge', [] );
return $ai_settings['mode'] ?? 'disabled';
}

/**
* 获取白名单
*
* @return array
*/
private function get_whitelist(): array {
$ai_settings = $this->settings->get( 'ai_bridge', array() );
$whitelist = $ai_settings['whitelist'] ?? array();
/**
* 获取白名单
*
* @return array
*/
private function get_whitelist(): array {
$ai_settings = $this->settings->get( 'ai_bridge', [] );
$whitelist = $ai_settings['whitelist'] ?? [];

// 默认白名单
$default_whitelist = array(
'api.openai.com',
'api.anthropic.com',
'generativelanguage.googleapis.com',
);
// 默认白名单
$default_whitelist = [
'api.openai.com',
'api.anthropic.com',
'generativelanguage.googleapis.com',
];

return array_unique( array_merge( $default_whitelist, $whitelist ) );
}
return array_unique( array_merge( $default_whitelist, $whitelist ) );
}

/**
* 拦截 HTTP 请求
*
* @param false|array|\WP_Error $preempt 预处理结果
* @param array $args 请求参数
* @param string $url 请求 URL
* @return false|array|\WP_Error
*/
public function intercept_request( $preempt, array $args, string $url ) {
// 如果已经被其他过滤器处理,跳过
if ( false !== $preempt ) {
return $preempt;
}
/**
* 拦截 HTTP 请求
*
* @param false|array|\WP_Error $preempt 预处理结果
* @param array $args 请求参数
* @param string $url 请求 URL
* @return false|array|\WP_Error
*/
public function intercept_request( $preempt, array $args, string $url ) {
// 如果已经被其他过滤器处理,跳过
if ( false !== $preempt ) {
return $preempt;
}

// 检查是否是 AI API 请求
if ( ! $this->is_ai_request( $url ) ) {
return false;
}
// 检查是否是 AI API 请求
if ( ! $this->is_ai_request( $url ) ) {
return false;
}

Logger::debug( 'AI 请求拦截', array( 'url' => $url ) );
Logger::debug( 'AI 请求拦截', [ 'url' => $url ] );

// 根据模式处理
$mode = $this->get_mode();
// 根据模式处理
$mode = $this->get_mode();

switch ( $mode ) {
case 'passthrough':
return $this->handle_passthrough( $url, $args );
switch ( $mode ) {
case 'passthrough':
return $this->handle_passthrough( $url, $args );

case 'wpmind':
return $this->handle_wpmind( $url, $args );
case 'wpmind':
return $this->handle_wpmind( $url, $args );

default:
return false;
}
}
default:
return false;
}
}

/**
* 检查是否是 AI API 请求
*
* @param string $url URL
* @return bool
*/
private function is_ai_request( string $url ): bool {
$host = wp_parse_url( $url, PHP_URL_HOST );
/**
* 检查是否是 AI API 请求
*
* @param string $url URL
* @return bool
*/
private function is_ai_request( string $url ): bool {
$host = wp_parse_url( $url, PHP_URL_HOST );

if ( empty( $host ) ) {
return false;
}
if ( empty( $host ) ) {
return false;
}

$host = strtolower( $host );
$host = strtolower( $host );

foreach ( $this->whitelist as $allowed ) {
if ( strtolower( $allowed ) === $host ) {
return true;
}
}
foreach ( $this->whitelist as $allowed ) {
if ( strtolower( $allowed ) === $host ) {
return true;
}
}

return false;
}
return false;
}

/**
* 透传模式处理
*
* @param string $url 原始 URL
* @param array $args 请求参数
* @return array|\WP_Error
*/
private function handle_passthrough( string $url, array $args ) {
$ai_settings = $this->settings->get( 'ai_bridge', array() );
$custom_endpoint = $ai_settings['custom_endpoint'] ?? '';
/**
* 透传模式处理
*
* @param string $url 原始 URL
* @param array $args 请求参数
* @return array|\WP_Error
*/
private function handle_passthrough( string $url, array $args ) {
$ai_settings = $this->settings->get( 'ai_bridge', [] );
$custom_endpoint = $ai_settings['custom_endpoint'] ?? '';

if ( empty( $custom_endpoint ) ) {
Logger::warning( '透传模式未配置自定义端点' );
return false;
}
if ( empty( $custom_endpoint ) ) {
Logger::warning( '透传模式未配置自定义端点' );
return false;
}

// SSRF 防护:验证端点安全性
if ( ! Validator::is_valid_url( $custom_endpoint ) ) {
Logger::error( '自定义端点不安全', array( 'endpoint' => $custom_endpoint ) );
return false;
}
// SSRF 防护:验证端点安全性
if ( ! Validator::is_valid_url( $custom_endpoint ) ) {
Logger::error( '自定义端点不安全', [ 'endpoint' => $custom_endpoint ] );
return false;
}

// 替换 URL
$new_url = $this->replace_endpoint( $url, $custom_endpoint );
// 替换 URL
$new_url = $this->replace_endpoint( $url, $custom_endpoint );

Logger::debug(
'AI 请求透传',
array(
'original' => $url,
'new' => $new_url,
)
);
Logger::debug( 'AI 请求透传', [
'original' => $url,
'new' => $new_url,
] );

// 发送请求
return $this->forward_request( $new_url, $args );
}
// 发送请求
return $this->forward_request( $new_url, $args );
}

/**
* WPMind 模式处理
*
* @param string $url 原始 URL
* @param array $args 请求参数
* @return array|\WP_Error
*/
private function handle_wpmind( string $url, array $args ) {
// 检查 WPMind 是否可用
if ( ! $this->is_wpmind_available() ) {
Logger::warning( 'WPMind 不可用,回退到透传模式' );
return $this->handle_passthrough( $url, $args );
}
/**
* WPMind 模式处理
*
* @param string $url 原始 URL
* @param array $args 请求参数
* @return array|\WP_Error
*/
private function handle_wpmind( string $url, array $args ) {
// 检查 WPMind 是否可用
if ( ! $this->is_wpmind_available() ) {
Logger::warning( 'WPMind 不可用,回退到透传模式' );
return $this->handle_passthrough( $url, $args );
}

// 使用 WPMind API
$wpmind_endpoint = apply_filters( 'wpmind_api_endpoint', 'https://api.wpmind.cn/v1' );
// 使用 WPMind API
$wpmind_endpoint = apply_filters( 'wpmind_api_endpoint', 'https://api.wpmind.cn/v1' );

// 转换请求格式
$converted_args = $this->convert_to_wpmind_format( $url, $args );
// 转换请求格式
$converted_args = $this->convert_to_wpmind_format( $url, $args );

Logger::debug(
'AI 请求转发到 WPMind',
array(
'original' => $url,
'endpoint' => $wpmind_endpoint,
)
);
Logger::debug( 'AI 请求转发到 WPMind', [
'original' => $url,
'endpoint' => $wpmind_endpoint,
] );

return $this->forward_request( $wpmind_endpoint, $converted_args );
}
return $this->forward_request( $wpmind_endpoint, $converted_args );
}

/**
* 检查 WPMind 是否可用
*
* @return bool
*/
private function is_wpmind_available(): bool {
return class_exists( 'WPMind\\Core\\Plugin' ) ||
function_exists( 'wpmind_get_api_key' );
}
/**
* 检查 WPMind 是否可用
*
* @return bool
*/
private function is_wpmind_available(): bool {
return class_exists( 'WPMind\\Core\\Plugin' ) ||
function_exists( 'wpmind_get_api_key' );
}

/**
* 替换端点
*
* @param string $url 原始 URL
* @param string $endpoint 新端点
* @return string
*/
private function replace_endpoint( string $url, string $endpoint ): string {
$parsed = wp_parse_url( $url );
$path = $parsed['path'] ?? '';
$query = isset( $parsed['query'] ) ? '?' . $parsed['query'] : '';
/**
* 替换端点
*
* @param string $url 原始 URL
* @param string $endpoint 新端点
* @return string
*/
private function replace_endpoint( string $url, string $endpoint ): string {
$parsed = wp_parse_url( $url );
$path = $parsed['path'] ?? '';
$query = isset( $parsed['query'] ) ? '?' . $parsed['query'] : '';

// 移除端点末尾的斜杠
$endpoint = rtrim( $endpoint, '/' );
// 移除端点末尾的斜杠
$endpoint = rtrim( $endpoint, '/' );

return $endpoint . $path . $query;
}
return $endpoint . $path . $query;
}

/**
* 转换为 WPMind 格式
*
* @param string $url 原始 URL
* @param array $args 请求参数
* @return array
*/
private function convert_to_wpmind_format( string $url, array $args ): array {
// 获取 WPMind API Key
$api_key = '';
if ( function_exists( 'wpmind_get_api_key' ) ) {
$api_key = wpmind_get_api_key();
}
/**
* 转换为 WPMind 格式
*
* @param string $url 原始 URL
* @param array $args 请求参数
* @return array
*/
private function convert_to_wpmind_format( string $url, array $args ): array {
// 获取 WPMind API Key
$api_key = '';
if ( function_exists( 'wpmind_get_api_key' ) ) {
$api_key = wpmind_get_api_key();
}

// 更新认证头
if ( ! empty( $api_key ) ) {
$args['headers']['Authorization'] = 'Bearer ' . $api_key;
}
// 更新认证头
if ( ! empty( $api_key ) ) {
$args['headers']['Authorization'] = 'Bearer ' . $api_key;
}

// 添加来源标识
$args['headers']['X-WPBridge-Source'] = 'wpbridge';
// 添加来源标识
$args['headers']['X-WPBridge-Source'] = 'wpbridge';

return $args;
}
return $args;
}

/**
* 转发请求
*
* @param string $url URL
* @param array $args 请求参数
* @return array|\WP_Error
*/
private function forward_request( string $url, array $args ) {
// 移除 pre_http_request 过滤器避免递归
remove_filter( 'pre_http_request', array( $this, 'intercept_request' ), 10 );
/**
* 转发请求
*
* @param string $url URL
* @param array $args 请求参数
* @return array|\WP_Error
*/
private function forward_request( string $url, array $args ) {
// 移除 pre_http_request 过滤器避免递归
remove_filter( 'pre_http_request', [ $this, 'intercept_request' ], 10 );

$response = wp_remote_request( $url, $args );
$response = wp_remote_request( $url, $args );

// 重新添加过滤器
add_filter( 'pre_http_request', array( $this, 'intercept_request' ), 10, 3 );
// 重新添加过滤器
add_filter( 'pre_http_request', [ $this, 'intercept_request' ], 10, 3 );

if ( is_wp_error( $response ) ) {
Logger::error(
'AI 请求转发失败',
array(
'url' => $url,
'error' => $response->get_error_message(),
)
);
}
if ( is_wp_error( $response ) ) {
Logger::error( 'AI 请求转发失败', [
'url' => $url,
'error' => $response->get_error_message(),
] );
}

return $response;
}
return $response;
}

/**
* 注册适配器
*
* @param string $name 适配器名称
* @param Adapters\AdapterInterface $adapter 适配器实例
*/
public function register_adapter( string $name, Adapters\AdapterInterface $adapter ): void {
$this->adapters[ $name ] = $adapter;
Logger::debug( '注册 AI 适配器', array( 'name' => $name ) );
}
/**
* 注册适配器
*
* @param string $name 适配器名称
* @param Adapters\AdapterInterface $adapter 适配器实例
*/
public function register_adapter( string $name, Adapters\AdapterInterface $adapter ): void {
$this->adapters[ $name ] = $adapter;
Logger::debug( '注册 AI 适配器', [ 'name' => $name ] );
}

/**
* 获取适配器
*
* @param string $name 适配器名称
* @return Adapters\AdapterInterface|null
*/
public function get_adapter( string $name ): ?Adapters\AdapterInterface {
return $this->adapters[ $name ] ?? null;
}
/**
* 获取适配器
*
* @param string $name 适配器名称
* @return Adapters\AdapterInterface|null
*/
public function get_adapter( string $name ): ?Adapters\AdapterInterface {
return $this->adapters[ $name ] ?? null;
}

/**
* 获取所有适配器
*
* @return array
*/
public function get_adapters(): array {
return $this->adapters;
}
/**
* 获取所有适配器
*
* @return array
*/
public function get_adapters(): array {
return $this->adapters;
}

/**
* 获取状态信息
*
* @return array
*/
public function get_status(): array {
return array(
'enabled' => $this->is_enabled(),
'mode' => $this->get_mode(),
'wpmind_available' => $this->is_wpmind_available(),
'whitelist' => $this->whitelist,
'adapters' => array_keys( $this->adapters ),
);
}
/**
* 获取状态信息
*
* @return array
*/
public function get_status(): array {
return [
'enabled' => $this->is_enabled(),
'mode' => $this->get_mode(),
'wpmind_available' => $this->is_wpmind_available(),
'whitelist' => $this->whitelist,
'adapters' => array_keys( $this->adapters ),
];
}
}

View file

@ -12,7 +12,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -20,153 +20,153 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
abstract class AbstractAdapter implements AdapterInterface {

/**
* 设置实例
*
* @var Settings
*/
protected Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
protected Settings $settings;

/**
* 支持的插件 slug 列表
*
* @var array
*/
protected array $supported_plugins = array();
/**
* 支持的插件 slug 列表
*
* @var array
*/
protected array $supported_plugins = [];

/**
* 匹配的 URL 模式
*
* @var array
*/
protected array $url_patterns = array();
/**
* 匹配的 URL 模式
*
* @var array
*/
protected array $url_patterns = [];

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}

/**
* 检查是否支持该插件
*
* @param string $plugin_slug 插件 slug
* @return bool
*/
public function supports( string $plugin_slug ): bool {
return in_array( $plugin_slug, $this->supported_plugins, true );
}
/**
* 检查是否支持该插件
*
* @param string $plugin_slug 插件 slug
* @return bool
*/
public function supports( string $plugin_slug ): bool {
return in_array( $plugin_slug, $this->supported_plugins, true );
}

/**
* 检查请求是否匹配
*
* @param string $url 请求 URL
* @param array $args 请求参数
* @return bool
*/
public function matches( string $url, array $args ): bool {
foreach ( $this->url_patterns as $pattern ) {
$result = @preg_match( $pattern, $url );
if ( $result === false ) {
$this->log( '无效的 URL 匹配模式', array( 'pattern' => $pattern ) );
continue;
}
if ( $result === 1 ) {
return true;
}
}
return false;
}
/**
* 检查请求是否匹配
*
* @param string $url 请求 URL
* @param array $args 请求参数
* @return bool
*/
public function matches( string $url, array $args ): bool {
foreach ( $this->url_patterns as $pattern ) {
$result = @preg_match( $pattern, $url );
if ( $result === false ) {
$this->log( '无效的 URL 匹配模式', [ 'pattern' => $pattern ] );
continue;
}
if ( $result === 1 ) {
return true;
}
}
return false;
}

/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool {
$ai_settings = $this->settings->get( 'ai_bridge', array() );
$adapters = $ai_settings['adapters'] ?? array();
/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool {
$ai_settings = $this->settings->get( 'ai_bridge', [] );
$adapters = $ai_settings['adapters'] ?? [];

return in_array( $this->get_name(), $adapters, true );
}
return in_array( $this->get_name(), $adapters, true );
}

/**
* 记录日志
*
* @param string $message 消息
* @param array $context 上下文
*/
protected function log( string $message, array $context = array() ): void {
$context['adapter'] = $this->get_name();
Logger::debug( $message, $context );
}
/**
* 记录日志
*
* @param string $message 消息
* @param array $context 上下文
*/
protected function log( string $message, array $context = [] ): void {
$context['adapter'] = $this->get_name();
Logger::debug( $message, $context );
}

/**
* 获取请求体
*
* @param array $args 请求参数
* @return array|null
*/
protected function get_request_body( array $args ): ?array {
if ( empty( $args['body'] ) ) {
return null;
}
/**
* 获取请求体
*
* @param array $args 请求参数
* @return array|null
*/
protected function get_request_body( array $args ): ?array {
if ( empty( $args['body'] ) ) {
return null;
}

$body = $args['body'];
$body = $args['body'];

if ( is_string( $body ) ) {
$decoded = json_decode( $body, true );
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
}
if ( is_string( $body ) ) {
$decoded = json_decode( $body, true );
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
}

return is_array( $body ) ? $body : null;
}
return is_array( $body ) ? $body : null;
}

/**
* 设置请求体
*
* @param array $args 请求参数
* @param array $body 请求体
* @return array
*/
protected function set_request_body( array $args, array $body ): array {
$args['body'] = wp_json_encode( $body );
return $args;
}
/**
* 设置请求体
*
* @param array $args 请求参数
* @param array $body 请求体
* @return array
*/
protected function set_request_body( array $args, array $body ): array {
$args['body'] = wp_json_encode( $body );
return $args;
}

/**
* 获取响应体
*
* @param array|\WP_Error $response 响应
* @return array|null
*/
protected function get_response_body( $response ): ?array {
if ( is_wp_error( $response ) ) {
return null;
}
/**
* 获取响应体
*
* @param array|\WP_Error $response 响应
* @return array|null
*/
protected function get_response_body( $response ): ?array {
if ( is_wp_error( $response ) ) {
return null;
}

$body = wp_remote_retrieve_body( $response );
$body = wp_remote_retrieve_body( $response );

if ( empty( $body ) ) {
return null;
}
if ( empty( $body ) ) {
return null;
}

$decoded = json_decode( $body, true );
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
}
$decoded = json_decode( $body, true );
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
}

/**
* 设置响应体
*
* @param array $response 响应
* @param array $body 响应体
* @return array
*/
protected function set_response_body( array $response, array $body ): array {
$response['body'] = wp_json_encode( $body );
return $response;
}
/**
* 设置响应体
*
* @param array $response 响应
* @param array $body 响应体
* @return array
*/
protected function set_response_body( array $response, array $body ): array {
$response['body'] = wp_json_encode( $body );
return $response;
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\AIBridge\Adapters;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,58 +17,58 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
interface AdapterInterface {

/**
* 获取适配器名称
*
* @return string
*/
public function get_name(): string;
/**
* 获取适配器名称
*
* @return string
*/
public function get_name(): string;

/**
* 获取适配器描述
*
* @return string
*/
public function get_description(): string;
/**
* 获取适配器描述
*
* @return string
*/
public function get_description(): string;

/**
* 检查是否支持该插件
*
* @param string $plugin_slug 插件 slug
* @return bool
*/
public function supports( string $plugin_slug ): bool;
/**
* 检查是否支持该插件
*
* @param string $plugin_slug 插件 slug
* @return bool
*/
public function supports( string $plugin_slug ): bool;

/**
* 检查请求是否匹配
*
* @param string $url 请求 URL
* @param array $args 请求参数
* @return bool
*/
public function matches( string $url, array $args ): bool;
/**
* 检查请求是否匹配
*
* @param string $url 请求 URL
* @param array $args 请求参数
* @return bool
*/
public function matches( string $url, array $args ): bool;

/**
* 转换请求
*
* @param string $url 原始 URL
* @param array $args 原始参数
* @return array [url, args]
*/
public function transform_request( string $url, array $args ): array;
/**
* 转换请求
*
* @param string $url 原始 URL
* @param array $args 原始参数
* @return array [url, args]
*/
public function transform_request( string $url, array $args ): array;

/**
* 转换响应
*
* @param array|\WP_Error $response 原始响应
* @return array|\WP_Error
*/
public function transform_response( $response );
/**
* 转换响应
*
* @param array|\WP_Error $response 原始响应
* @return array|\WP_Error
*/
public function transform_response( $response );

/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool;
/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool;
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\AIBridge\Adapters;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -18,174 +18,168 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class RankMathAdapter extends AbstractAdapter {

/**
* 支持的插件
*
* @var array
*/
protected array $supported_plugins = array(
'seo-by-rank-math',
'seo-by-rank-math-pro',
);
/**
* 支持的插件
*
* @var array
*/
protected array $supported_plugins = [
'seo-by-rank-math',
'seo-by-rank-math-pro',
];

/**
* URL 匹配模式
*
* @var array
*/
protected array $url_patterns = array(
'#api\.openai\.com/v1/chat/completions#',
'#rankmath\.com.*api#i',
'#content-ai\.rankmath\.com#i',
);
/**
* URL 匹配模式
*
* @var array
*/
protected array $url_patterns = [
'#api\.openai\.com/v1/chat/completions#',
'#rankmath\.com.*api#i',
'#content-ai\.rankmath\.com#i',
];

/**
* 获取适配器名称
*
* @return string
*/
public function get_name(): string {
return 'rankmath';
}
/**
* 获取适配器名称
*
* @return string
*/
public function get_name(): string {
return 'rankmath';
}

/**
* 获取适配器描述
*
* @return string
*/
public function get_description(): string {
return __( 'Rank Math Content AI 功能适配', 'wpbridge' );
}
/**
* 获取适配器描述
*
* @return string
*/
public function get_description(): string {
return __( 'Rank Math Content AI 功能适配', 'wpbridge' );
}

/**
* 转换请求
*
* @param string $url 原始 URL
* @param array $args 原始参数
* @return array [url, args]
*/
public function transform_request( string $url, array $args ): array {
$body = $this->get_request_body( $args );
/**
* 转换请求
*
* @param string $url 原始 URL
* @param array $args 原始参数
* @return array [url, args]
*/
public function transform_request( string $url, array $args ): array {
$body = $this->get_request_body( $args );

if ( null === $body ) {
return array( $url, $args );
}
if ( null === $body ) {
return [ $url, $args ];
}

$this->log(
'Rank Math AI 请求转换',
array(
'model' => $body['model'] ?? 'unknown',
'type' => $this->detect_request_type( $body ),
)
);
$this->log( 'Rank Math AI 请求转换', [
'model' => $body['model'] ?? 'unknown',
'type' => $this->detect_request_type( $body ),
] );

// 转换模型名称
if ( isset( $body['model'] ) ) {
$body['model'] = $this->map_model( $body['model'] );
}
// 转换模型名称
if ( isset( $body['model'] ) ) {
$body['model'] = $this->map_model( $body['model'] );
}

// 根据请求类型优化
$request_type = $this->detect_request_type( $body );
$body = $this->optimize_for_type( $body, $request_type );
// 根据请求类型优化
$request_type = $this->detect_request_type( $body );
$body = $this->optimize_for_type( $body, $request_type );

$args = $this->set_request_body( $args, $body );
$args = $this->set_request_body( $args, $body );

return array( $url, $args );
}
return [ $url, $args ];
}

/**
* 转换响应
*
* @param array|\WP_Error $response 原始响应
* @return array|\WP_Error
*/
public function transform_response( $response ) {
if ( is_wp_error( $response ) ) {
return $response;
}
/**
* 转换响应
*
* @param array|\WP_Error $response 原始响应
* @return array|\WP_Error
*/
public function transform_response( $response ) {
if ( is_wp_error( $response ) ) {
return $response;
}

$body = $this->get_response_body( $response );
$body = $this->get_response_body( $response );

if ( null === $body ) {
return $response;
}
if ( null === $body ) {
return $response;
}

$this->log(
'Rank Math AI 响应转换',
array(
'has_choices' => isset( $body['choices'] ),
)
);
$this->log( 'Rank Math AI 响应转换', [
'has_choices' => isset( $body['choices'] ),
] );

return $response;
}
return $response;
}

/**
* 检测请求类型
*
* @param array $body 请求体
* @return string
*/
private function detect_request_type( array $body ): string {
if ( ! isset( $body['messages'] ) || ! is_array( $body['messages'] ) ) {
return 'unknown';
}
/**
* 检测请求类型
*
* @param array $body 请求体
* @return string
*/
private function detect_request_type( array $body ): string {
if ( ! isset( $body['messages'] ) || ! is_array( $body['messages'] ) ) {
return 'unknown';
}

$content = '';
foreach ( $body['messages'] as $message ) {
if ( isset( $message['content'] ) ) {
$content .= $message['content'] . ' ';
}
}
$content = '';
foreach ( $body['messages'] as $message ) {
if ( isset( $message['content'] ) ) {
$content .= $message['content'] . ' ';
}
}

$content = strtolower( $content );
$content = strtolower( $content );

if ( strpos( $content, 'title' ) !== false || strpos( $content, '标题' ) !== false ) {
return 'title';
}
if ( strpos( $content, 'title' ) !== false || strpos( $content, '标题' ) !== false ) {
return 'title';
}

if ( strpos( $content, 'description' ) !== false || strpos( $content, '描述' ) !== false ) {
return 'description';
}
if ( strpos( $content, 'description' ) !== false || strpos( $content, '描述' ) !== false ) {
return 'description';
}

if ( strpos( $content, 'keyword' ) !== false || strpos( $content, '关键词' ) !== false ) {
return 'keyword';
}
if ( strpos( $content, 'keyword' ) !== false || strpos( $content, '关键词' ) !== false ) {
return 'keyword';
}

if ( strpos( $content, 'content' ) !== false || strpos( $content, '内容' ) !== false ) {
return 'content';
}
if ( strpos( $content, 'content' ) !== false || strpos( $content, '内容' ) !== false ) {
return 'content';
}

return 'general';
}
return 'general';
}

/**
* 根据类型优化请求
*
* @param array $body 请求体
* @param string $type 请求类型
* @return array
*/
private function optimize_for_type( array $body, string $type ): array {
// 可以根据不同类型添加优化逻辑
// 例如:为中文内容生成添加特定提示
/**
* 根据类型优化请求
*
* @param array $body 请求体
* @param string $type 请求类型
* @return array
*/
private function optimize_for_type( array $body, string $type ): array {
// 可以根据不同类型添加优化逻辑
// 例如:为中文内容生成添加特定提示

return $body;
}
return $body;
}

/**
* 映射模型名称
*
* @param string $model 原始模型名称
* @return string
*/
private function map_model( string $model ): string {
$model_map = array(
'gpt-4' => 'gpt-4',
'gpt-4-turbo' => 'gpt-4-turbo',
'gpt-3.5-turbo' => 'gpt-3.5-turbo',
);
/**
* 映射模型名称
*
* @param string $model 原始模型名称
* @return string
*/
private function map_model( string $model ): string {
$model_map = [
'gpt-4' => 'gpt-4',
'gpt-4-turbo' => 'gpt-4-turbo',
'gpt-3.5-turbo' => 'gpt-3.5-turbo',
];

return $model_map[ $model ] ?? $model;
}
return $model_map[ $model ] ?? $model;
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\AIBridge\Adapters;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -18,134 +18,128 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class YoastAdapter extends AbstractAdapter {

/**
* 支持的插件
*
* @var array
*/
protected array $supported_plugins = array(
'wordpress-seo-premium',
'wordpress-seo',
);
/**
* 支持的插件
*
* @var array
*/
protected array $supported_plugins = [
'wordpress-seo-premium',
'wordpress-seo',
];

/**
* URL 匹配模式
*
* @var array
*/
protected array $url_patterns = array(
'#api\.openai\.com/v1/chat/completions#',
'#yoast\.com.*ai#i',
);
/**
* URL 匹配模式
*
* @var array
*/
protected array $url_patterns = [
'#api\.openai\.com/v1/chat/completions#',
'#yoast\.com.*ai#i',
];

/**
* 获取适配器名称
*
* @return string
*/
public function get_name(): string {
return 'yoast';
}
/**
* 获取适配器名称
*
* @return string
*/
public function get_name(): string {
return 'yoast';
}

/**
* 获取适配器描述
*
* @return string
*/
public function get_description(): string {
return __( 'Yoast SEO Premium AI 功能适配', 'wpbridge' );
}
/**
* 获取适配器描述
*
* @return string
*/
public function get_description(): string {
return __( 'Yoast SEO Premium AI 功能适配', 'wpbridge' );
}

/**
* 转换请求
*
* @param string $url 原始 URL
* @param array $args 原始参数
* @return array [url, args]
*/
public function transform_request( string $url, array $args ): array {
$body = $this->get_request_body( $args );
/**
* 转换请求
*
* @param string $url 原始 URL
* @param array $args 原始参数
* @return array [url, args]
*/
public function transform_request( string $url, array $args ): array {
$body = $this->get_request_body( $args );

if ( null === $body ) {
return array( $url, $args );
}
if ( null === $body ) {
return [ $url, $args ];
}

$this->log(
'Yoast AI 请求转换',
array(
'model' => $body['model'] ?? 'unknown',
)
);
$this->log( 'Yoast AI 请求转换', [
'model' => $body['model'] ?? 'unknown',
] );

// 转换模型名称(如果需要)
if ( isset( $body['model'] ) ) {
$body['model'] = $this->map_model( $body['model'] );
}
// 转换模型名称(如果需要)
if ( isset( $body['model'] ) ) {
$body['model'] = $this->map_model( $body['model'] );
}

// 添加系统提示优化
if ( isset( $body['messages'] ) && is_array( $body['messages'] ) ) {
$body['messages'] = $this->optimize_messages( $body['messages'] );
}
// 添加系统提示优化
if ( isset( $body['messages'] ) && is_array( $body['messages'] ) ) {
$body['messages'] = $this->optimize_messages( $body['messages'] );
}

$args = $this->set_request_body( $args, $body );
$args = $this->set_request_body( $args, $body );

return array( $url, $args );
}
return [ $url, $args ];
}

/**
* 转换响应
*
* @param array|\WP_Error $response 原始响应
* @return array|\WP_Error
*/
public function transform_response( $response ) {
if ( is_wp_error( $response ) ) {
return $response;
}
/**
* 转换响应
*
* @param array|\WP_Error $response 原始响应
* @return array|\WP_Error
*/
public function transform_response( $response ) {
if ( is_wp_error( $response ) ) {
return $response;
}

$body = $this->get_response_body( $response );
$body = $this->get_response_body( $response );

if ( null === $body ) {
return $response;
}
if ( null === $body ) {
return $response;
}

$this->log(
'Yoast AI 响应转换',
array(
'has_choices' => isset( $body['choices'] ),
)
);
$this->log( 'Yoast AI 响应转换', [
'has_choices' => isset( $body['choices'] ),
] );

// 响应格式通常兼容,无需转换
return $response;
}
// 响应格式通常兼容,无需转换
return $response;
}

/**
* 映射模型名称
*
* @param string $model 原始模型名称
* @return string
*/
private function map_model( string $model ): string {
$model_map = array(
'gpt-4' => 'gpt-4',
'gpt-4-turbo' => 'gpt-4-turbo',
'gpt-3.5-turbo' => 'gpt-3.5-turbo',
);
/**
* 映射模型名称
*
* @param string $model 原始模型名称
* @return string
*/
private function map_model( string $model ): string {
$model_map = [
'gpt-4' => 'gpt-4',
'gpt-4-turbo' => 'gpt-4-turbo',
'gpt-3.5-turbo' => 'gpt-3.5-turbo',
];

return $model_map[ $model ] ?? $model;
}
return $model_map[ $model ] ?? $model;
}

/**
* 优化消息
*
* @param array $messages 消息列表
* @return array
*/
private function optimize_messages( array $messages ): array {
// 可以在这里添加中文优化提示
// 例如:添加系统消息要求返回中文内容
/**
* 优化消息
*
* @param array $messages 消息列表
* @return array
*/
private function optimize_messages( array $messages ): array {
// 可以在这里添加中文优化提示
// 例如:添加系统消息要求返回中文内容

return $messages;
}
return $messages;
}
}

View file

@ -13,7 +13,7 @@ use WPBridge\Security\Encryption;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -21,276 +21,267 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class ApiKeyManager {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}

/**
* 生成新的 API Key
*
* @param string $name Key 名称
* @param string|null $expires_at 过期时间
* @param array $permissions 权限列表
* @return array
*/
public function generate( string $name, ?string $expires_at = null, array $permissions = array() ): array {
// 权限检查
if ( ! current_user_can( 'manage_options' ) ) {
throw new \Exception( __( '权限不足', 'wpbridge' ) );
}
/**
* 生成新的 API Key
*
* @param string $name Key 名称
* @param string|null $expires_at 过期时间
* @param array $permissions 权限列表
* @return array
*/
public function generate( string $name, ?string $expires_at = null, array $permissions = [] ): array {
// 权限检查
if ( ! current_user_can( 'manage_options' ) ) {
throw new \Exception( __( '权限不足', 'wpbridge' ) );
}

$api_key = Encryption::generate_token( 32 );
$key_id = 'key_' . wp_generate_uuid4();
$api_key = Encryption::generate_token( 32 );
$key_id = 'key_' . wp_generate_uuid4();

$key_data = array(
'id' => $key_id,
'name' => sanitize_text_field( $name ),
'key_hash' => password_hash( $api_key, PASSWORD_DEFAULT ), // 存储哈希而非明文
'key_prefix' => substr( $api_key, 0, 4 ) . '...' . substr( $api_key, -4 ), // 显示前4后4
'permissions' => $permissions,
'expires_at' => $expires_at,
'created_at' => current_time( 'mysql' ),
'created_by' => get_current_user_id(),
'last_used' => null,
'usage_count' => 0,
);
$key_data = [
'id' => $key_id,
'name' => sanitize_text_field( $name ),
'key_hash' => password_hash( $api_key, PASSWORD_DEFAULT ), // 存储哈希而非明文
'key_prefix' => substr( $api_key, 0, 4 ) . '...' . substr( $api_key, -4 ), // 显示前4后4
'permissions' => $permissions,
'expires_at' => $expires_at,
'created_at' => current_time( 'mysql' ),
'created_by' => get_current_user_id(),
'last_used' => null,
'usage_count' => 0,
];

// 保存到设置
$api_settings = $this->settings->get( 'api', array() );
$api_settings['keys'] = $api_settings['keys'] ?? array();
$api_settings['keys'][] = $key_data;
// 保存到设置
$api_settings = $this->settings->get( 'api', [] );
$api_settings['keys'] = $api_settings['keys'] ?? [];
$api_settings['keys'][] = $key_data;

$this->settings->set( 'api', $api_settings );
$this->settings->set( 'api', $api_settings );

Logger::info(
'API Key 已创建',
array(
'id' => $key_id,
'name' => $name,
)
);
Logger::info( 'API Key 已创建', [ 'id' => $key_id, 'name' => $name ] );

return array(
'id' => $key_id,
'name' => $name,
'key' => $api_key, // 只在创建时返回完整 key
'expires_at' => $expires_at,
'created_at' => $key_data['created_at'],
);
}
return [
'id' => $key_id,
'name' => $name,
'key' => $api_key, // 只在创建时返回完整 key
'expires_at' => $expires_at,
'created_at' => $key_data['created_at'],
];
}

/**
* 获取所有 API Keys不含完整 key
*
* @return array
*/
public function get_all(): array {
$api_settings = $this->settings->get( 'api', array() );
$keys = $api_settings['keys'] ?? array();
/**
* 获取所有 API Keys不含完整 key
*
* @return array
*/
public function get_all(): array {
$api_settings = $this->settings->get( 'api', [] );
$keys = $api_settings['keys'] ?? [];

return array_map(
function ( $key ) {
return array(
'id' => $key['id'],
'name' => $key['name'],
'key_prefix' => $key['key_prefix'],
'permissions' => $key['permissions'] ?? array(),
'expires_at' => $key['expires_at'],
'created_at' => $key['created_at'],
'last_used' => $key['last_used'],
'usage_count' => $key['usage_count'] ?? 0,
'is_expired' => $this->is_expired( $key ),
);
},
$keys
);
}
return array_map( function ( $key ) {
return [
'id' => $key['id'],
'name' => $key['name'],
'key_prefix' => $key['key_prefix'],
'permissions' => $key['permissions'] ?? [],
'expires_at' => $key['expires_at'],
'created_at' => $key['created_at'],
'last_used' => $key['last_used'],
'usage_count' => $key['usage_count'] ?? 0,
'is_expired' => $this->is_expired( $key ),
];
}, $keys );
}

/**
* 获取单个 API Key
*
* @param string $key_id Key ID
* @return array|null
*/
public function get( string $key_id ): ?array {
$api_settings = $this->settings->get( 'api', array() );
$keys = $api_settings['keys'] ?? array();
/**
* 获取单个 API Key
*
* @param string $key_id Key ID
* @return array|null
*/
public function get( string $key_id ): ?array {
$api_settings = $this->settings->get( 'api', [] );
$keys = $api_settings['keys'] ?? [];

foreach ( $keys as $key ) {
if ( $key['id'] === $key_id ) {
return array(
'id' => $key['id'],
'name' => $key['name'],
'key_prefix' => $key['key_prefix'],
'permissions' => $key['permissions'] ?? array(),
'expires_at' => $key['expires_at'],
'created_at' => $key['created_at'],
'last_used' => $key['last_used'],
'usage_count' => $key['usage_count'] ?? 0,
'is_expired' => $this->is_expired( $key ),
);
}
}
foreach ( $keys as $key ) {
if ( $key['id'] === $key_id ) {
return [
'id' => $key['id'],
'name' => $key['name'],
'key_prefix' => $key['key_prefix'],
'permissions' => $key['permissions'] ?? [],
'expires_at' => $key['expires_at'],
'created_at' => $key['created_at'],
'last_used' => $key['last_used'],
'usage_count' => $key['usage_count'] ?? 0,
'is_expired' => $this->is_expired( $key ),
];
}
}

return null;
}
return null;
}

/**
* 删除 API Key
*
* @param string $key_id Key ID
* @return bool
*/
public function delete( string $key_id ): bool {
$api_settings = $this->settings->get( 'api', array() );
$keys = $api_settings['keys'] ?? array();
$new_keys = array();
/**
* 删除 API Key
*
* @param string $key_id Key ID
* @return bool
*/
public function delete( string $key_id ): bool {
$api_settings = $this->settings->get( 'api', [] );
$keys = $api_settings['keys'] ?? [];
$new_keys = [];

foreach ( $keys as $key ) {
if ( $key['id'] !== $key_id ) {
$new_keys[] = $key;
}
}
foreach ( $keys as $key ) {
if ( $key['id'] !== $key_id ) {
$new_keys[] = $key;
}
}

if ( count( $new_keys ) === count( $keys ) ) {
return false;
}
if ( count( $new_keys ) === count( $keys ) ) {
return false;
}

$api_settings['keys'] = $new_keys;
$this->settings->set( 'api', $api_settings );
$api_settings['keys'] = $new_keys;
$this->settings->set( 'api', $api_settings );

Logger::info( 'API Key 已删除', array( 'id' => $key_id ) );
Logger::info( 'API Key 已删除', [ 'id' => $key_id ] );

return true;
}
return true;
}

/**
* 更新 API Key
*
* @param string $key_id Key ID
* @param array $data 更新数据
* @return bool
*/
public function update( string $key_id, array $data ): bool {
$api_settings = $this->settings->get( 'api', array() );
$keys = $api_settings['keys'] ?? array();
$found = false;
/**
* 更新 API Key
*
* @param string $key_id Key ID
* @param array $data 更新数据
* @return bool
*/
public function update( string $key_id, array $data ): bool {
$api_settings = $this->settings->get( 'api', [] );
$keys = $api_settings['keys'] ?? [];
$found = false;

foreach ( $keys as $index => $key ) {
if ( $key['id'] === $key_id ) {
if ( isset( $data['name'] ) ) {
$keys[ $index ]['name'] = sanitize_text_field( $data['name'] );
}
if ( isset( $data['expires_at'] ) ) {
$keys[ $index ]['expires_at'] = $data['expires_at'];
}
if ( isset( $data['permissions'] ) ) {
$keys[ $index ]['permissions'] = $data['permissions'];
}
$found = true;
break;
}
}
foreach ( $keys as $index => $key ) {
if ( $key['id'] === $key_id ) {
if ( isset( $data['name'] ) ) {
$keys[ $index ]['name'] = sanitize_text_field( $data['name'] );
}
if ( isset( $data['expires_at'] ) ) {
$keys[ $index ]['expires_at'] = $data['expires_at'];
}
if ( isset( $data['permissions'] ) ) {
$keys[ $index ]['permissions'] = $data['permissions'];
}
$found = true;
break;
}
}

if ( ! $found ) {
return false;
}
if ( ! $found ) {
return false;
}

$api_settings['keys'] = $keys;
$this->settings->set( 'api', $api_settings );
$api_settings['keys'] = $keys;
$this->settings->set( 'api', $api_settings );

Logger::info( 'API Key 已更新', array( 'id' => $key_id ) );
Logger::info( 'API Key 已更新', [ 'id' => $key_id ] );

return true;
}
return true;
}

/**
* 记录 API Key 使用
*
* @param string $api_key API Key
*/
public function record_usage( string $api_key ): void {
$api_settings = $this->settings->get( 'api', array() );
$keys = $api_settings['keys'] ?? array();
/**
* 记录 API Key 使用
*
* @param string $api_key API Key
*/
public function record_usage( string $api_key ): void {
$api_settings = $this->settings->get( 'api', [] );
$keys = $api_settings['keys'] ?? [];

foreach ( $keys as $index => $key ) {
if ( isset( $key['key_hash'] ) && password_verify( $api_key, $key['key_hash'] ) ) {
$keys[ $index ]['last_used'] = current_time( 'mysql' );
$keys[ $index ]['usage_count'] = ( $key['usage_count'] ?? 0 ) + 1;
break;
}
}
foreach ( $keys as $index => $key ) {
if ( isset( $key['key_hash'] ) && password_verify( $api_key, $key['key_hash'] ) ) {
$keys[ $index ]['last_used'] = current_time( 'mysql' );
$keys[ $index ]['usage_count'] = ( $key['usage_count'] ?? 0 ) + 1;
break;
}
}

$api_settings['keys'] = $keys;
$this->settings->set( 'api', $api_settings );
}
$api_settings['keys'] = $keys;
$this->settings->set( 'api', $api_settings );
}

/**
* 检查 Key 是否过期
*
* @param array $key Key 数据
* @return bool
*/
private function is_expired( array $key ): bool {
if ( empty( $key['expires_at'] ) ) {
return false;
}
/**
* 检查 Key 是否过期
*
* @param array $key Key 数据
* @return bool
*/
private function is_expired( array $key ): bool {
if ( empty( $key['expires_at'] ) ) {
return false;
}

return strtotime( $key['expires_at'] ) < time();
}
return strtotime( $key['expires_at'] ) < time();
}

/**
* 撤销所有 API Keys
*
* @return int 撤销的数量
*/
public function revoke_all(): int {
$api_settings = $this->settings->get( 'api', array() );
$count = count( $api_settings['keys'] ?? array() );
/**
* 撤销所有 API Keys
*
* @return int 撤销的数量
*/
public function revoke_all(): int {
$api_settings = $this->settings->get( 'api', [] );
$count = count( $api_settings['keys'] ?? [] );

$api_settings['keys'] = array();
$this->settings->set( 'api', $api_settings );
$api_settings['keys'] = [];
$this->settings->set( 'api', $api_settings );

Logger::info( '所有 API Keys 已撤销', array( 'count' => $count ) );
Logger::info( '所有 API Keys 已撤销', [ 'count' => $count ] );

return $count;
}
return $count;
}

/**
* 获取统计信息
*
* @return array
*/
public function get_stats(): array {
$keys = $this->get_all();
$total = count( $keys );
$active = 0;
$expired = 0;
/**
* 获取统计信息
*
* @return array
*/
public function get_stats(): array {
$keys = $this->get_all();
$total = count( $keys );
$active = 0;
$expired = 0;

foreach ( $keys as $key ) {
if ( $key['is_expired'] ) {
++$expired;
} else {
++$active;
}
}
foreach ( $keys as $key ) {
if ( $key['is_expired'] ) {
$expired++;
} else {
$active++;
}
}

return array(
'total' => $total,
'active' => $active,
'expired' => $expired,
);
}
return [
'total' => $total,
'active' => $active,
'expired' => $expired,
];
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -55,18 +55,18 @@ class VendorAdmin {
*/
private function init_hooks(): void {
// 供应商 AJAX 处理
add_action( 'wp_ajax_wpbridge_add_vendor', array( $this, 'ajax_add_vendor' ) );
add_action( 'wp_ajax_wpbridge_remove_vendor', array( $this, 'ajax_remove_vendor' ) );
add_action( 'wp_ajax_wpbridge_test_vendor', array( $this, 'ajax_test_vendor' ) );
add_action( 'wp_ajax_wpbridge_toggle_vendor', array( $this, 'ajax_toggle_vendor' ) );
add_action( 'wp_ajax_wpbridge_sync_vendor_plugins', array( $this, 'ajax_sync_vendor_plugins' ) );
add_action( 'wp_ajax_wpbridge_add_vendor', [ $this, 'ajax_add_vendor' ] );
add_action( 'wp_ajax_wpbridge_remove_vendor', [ $this, 'ajax_remove_vendor' ] );
add_action( 'wp_ajax_wpbridge_test_vendor', [ $this, 'ajax_test_vendor' ] );
add_action( 'wp_ajax_wpbridge_toggle_vendor', [ $this, 'ajax_toggle_vendor' ] );
add_action( 'wp_ajax_wpbridge_sync_vendor_plugins', [ $this, 'ajax_sync_vendor_plugins' ] );

// 自定义插件 AJAX 处理
add_action( 'wp_ajax_wpbridge_add_custom_plugin', array( $this, 'ajax_add_custom_plugin' ) );
add_action( 'wp_ajax_wpbridge_remove_custom_plugin', array( $this, 'ajax_remove_custom_plugin' ) );
add_action( 'wp_ajax_wpbridge_add_custom_plugin', [ $this, 'ajax_add_custom_plugin' ] );
add_action( 'wp_ajax_wpbridge_remove_custom_plugin', [ $this, 'ajax_remove_custom_plugin' ] );

// Bridge Server AJAX 处理
add_action( 'wp_ajax_wpbridge_test_bridge_server', array( $this, 'ajax_test_bridge_server' ) );
add_action( 'wp_ajax_wpbridge_test_bridge_server', [ $this, 'ajax_test_bridge_server' ] );
}

/**
@ -76,7 +76,7 @@ class VendorAdmin {
*/
private function get_bridge_manager(): BridgeManager {
if ( null === $this->bridge_manager ) {
$remote_config = new \WPBridge\Core\RemoteConfig( $this->settings );
$remote_config = new \WPBridge\Core\RemoteConfig( $this->settings );
$this->bridge_manager = new BridgeManager( $this->settings, $remote_config );
}
return $this->bridge_manager;
@ -91,7 +91,7 @@ class VendorAdmin {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
}

$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
@ -103,27 +103,27 @@ class VendorAdmin {

// 验证必填字段
if ( empty( $vendor_id ) ) {
wp_send_json_error( array( 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
}

if ( empty( $name ) ) {
wp_send_json_error( array( 'message' => __( '供应商名称不能为空', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '供应商名称不能为空', 'wpbridge' ) ] );
}

if ( empty( $api_url ) ) {
wp_send_json_error( array( 'message' => __( 'API 地址不能为空', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( 'API 地址不能为空', 'wpbridge' ) ] );
}

// 验证 URL 协议
$scheme = wp_parse_url( $api_url, PHP_URL_SCHEME );
if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
wp_send_json_error( array( 'message' => __( 'API 地址必须使用 http 或 https 协议', 'wpbridge' ) ) );
if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
wp_send_json_error( [ 'message' => __( 'API 地址必须使用 http 或 https 协议', 'wpbridge' ) ] );
}

// 验证供应商类型
$allowed_types = array( 'woocommerce' );
$allowed_types = [ 'woocommerce' ];
if ( ! in_array( $type, $allowed_types, true ) ) {
wp_send_json_error( array( 'message' => __( '不支持的供应商类型', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '不支持的供应商类型', 'wpbridge' ) ] );
}

$result = $this->get_bridge_manager()->add_vendor(
@ -151,13 +151,13 @@ class VendorAdmin {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
}

$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );

if ( empty( $vendor_id ) ) {
wp_send_json_error( array( 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
}

$result = $this->get_bridge_manager()->remove_vendor( $vendor_id );
@ -178,13 +178,13 @@ class VendorAdmin {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
}

$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );

if ( empty( $vendor_id ) ) {
wp_send_json_error( array( 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
}

$result = $this->get_bridge_manager()->test_vendor_connection( $vendor_id );
@ -205,40 +205,35 @@ class VendorAdmin {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
}

$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
$enabled = ! empty( $_POST['enabled'] );

if ( empty( $vendor_id ) ) {
wp_send_json_error( array( 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
}

$vendors = $this->settings->get( 'vendors', array() );
$vendors = $this->settings->get( 'vendors', [] );

if ( ! isset( $vendors[ $vendor_id ] ) ) {
wp_send_json_error( array( 'message' => __( '供应商不存在', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '供应商不存在', 'wpbridge' ) ] );
}

$vendors[ $vendor_id ]['enabled'] = $enabled;
$this->settings->set( 'vendors', $vendors );

Logger::info(
'Vendor toggled',
array(
'vendor_id' => $vendor_id,
'enabled' => $enabled,
)
);
Logger::info( 'Vendor toggled', [
'vendor_id' => $vendor_id,
'enabled' => $enabled,
] );

wp_send_json_success(
array(
'message' => $enabled
? __( '供应商已启用', 'wpbridge' )
: __( '供应商已禁用', 'wpbridge' ),
)
);
wp_send_json_success( [
'message' => $enabled
? __( '供应商已启用', 'wpbridge' )
: __( '供应商已禁用', 'wpbridge' ),
] );
}

/**
@ -250,7 +245,7 @@ class VendorAdmin {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
}

$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
@ -261,33 +256,29 @@ class VendorAdmin {
// 同步单个供应商
$vendor = $vendor_manager->get( $vendor_id );
if ( ! $vendor ) {
wp_send_json_error( array( 'message' => __( '供应商不存在', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '供应商不存在', 'wpbridge' ) ] );
}

$plugins = $vendor->get_plugins( true ); // 强制刷新
wp_send_json_success(
array(
'message' => sprintf(
wp_send_json_success( [
'message' => sprintf(
/* translators: %d: plugin count */
__( '已同步 %d 个插件', 'wpbridge' ),
count( $plugins )
),
'count' => count( $plugins ),
)
);
__( '已同步 %d 个插件', 'wpbridge' ),
count( $plugins )
),
'count' => count( $plugins ),
] );
} else {
// 同步所有供应商
$all_plugins = $vendor_manager->get_all_plugins( true );
wp_send_json_success(
array(
'message' => sprintf(
wp_send_json_success( [
'message' => sprintf(
/* translators: %d: plugin count */
__( '已同步 %d 个插件', 'wpbridge' ),
count( $all_plugins )
),
'count' => count( $all_plugins ),
)
);
__( '已同步 %d 个插件', 'wpbridge' ),
count( $all_plugins )
),
'count' => count( $all_plugins ),
] );
}
}

@ -300,7 +291,7 @@ class VendorAdmin {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
}

$plugin_slug = sanitize_key( $_POST['plugin_slug'] ?? '' );
@ -308,13 +299,13 @@ class VendorAdmin {
$update_url = esc_url_raw( $_POST['update_url'] ?? '' );

if ( empty( $plugin_slug ) ) {
wp_send_json_error( array( 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ] );
}

$info = array(
$info = [
'name' => $name ?: $plugin_slug,
'update_url' => $update_url,
);
];

$result = $this->get_bridge_manager()->add_custom_plugin( $plugin_slug, $info );

@ -334,13 +325,13 @@ class VendorAdmin {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
}

$plugin_slug = sanitize_key( $_POST['plugin_slug'] ?? '' );

if ( empty( $plugin_slug ) ) {
wp_send_json_error( array( 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ] );
}

$result = $this->get_bridge_manager()->remove_custom_plugin( $plugin_slug );
@ -361,19 +352,19 @@ class VendorAdmin {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
}

$bridge_client = $this->get_bridge_manager()->get_bridge_client();

if ( ! $bridge_client ) {
wp_send_json_error( array( 'message' => __( 'Bridge Server 未配置', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( 'Bridge Server 未配置', 'wpbridge' ) ] );
}

if ( $bridge_client->health_check() ) {
wp_send_json_success( array( 'message' => __( '连接成功', 'wpbridge' ) ) );
wp_send_json_success( [ 'message' => __( '连接成功', 'wpbridge' ) ] );
} else {
wp_send_json_error( array( 'message' => __( '连接失败', 'wpbridge' ) ) );
wp_send_json_error( [ 'message' => __( '连接失败', 'wpbridge' ) ] );
}
}

@ -383,10 +374,10 @@ class VendorAdmin {
* @return void
*/
public function render_vendor_settings(): void {
$vendors = $this->get_bridge_manager()->get_vendors();
$custom = $this->settings->get( 'custom_plugins', array() );
$all_plugins = $this->get_bridge_manager()->get_all_available_plugins();
$stats = $this->get_bridge_manager()->get_stats();
$vendors = $this->get_bridge_manager()->get_vendors();
$custom = $this->settings->get( 'custom_plugins', [] );
$all_plugins = $this->get_bridge_manager()->get_all_available_plugins();
$stats = $this->get_bridge_manager()->get_stats();

include WPBRIDGE_PATH . 'templates/admin/vendor-settings.php';
}
@ -397,14 +388,14 @@ class VendorAdmin {
* @return array
*/
public function get_vendor_data(): array {
return array(
'vendors' => $this->get_bridge_manager()->get_vendors(),
'custom' => $this->settings->get( 'custom_plugins', array() ),
'all_plugins' => $this->get_bridge_manager()->get_all_available_plugins(),
'stats' => $this->get_bridge_manager()->get_stats(),
'vendor_types' => array(
return [
'vendors' => $this->get_bridge_manager()->get_vendors(),
'custom' => $this->settings->get( 'custom_plugins', [] ),
'all_plugins' => $this->get_bridge_manager()->get_all_available_plugins(),
'stats' => $this->get_bridge_manager()->get_stats(),
'vendor_types' => [
'woocommerce' => __( 'WooCommerce 商店', 'wpbridge' ),
),
);
],
];
}
}

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ namespace WPBridge\Cache;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,182 +17,182 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class CacheManager {

/**
* 缓存组名
*
* @var string
*/
const CACHE_GROUP = 'wpbridge';
/**
* 缓存组名
*
* @var string
*/
const CACHE_GROUP = 'wpbridge';

/**
* 默认 TTL12 小时)
*
* @var int
*/
const DEFAULT_TTL = 43200;
/**
* 默认 TTL12 小时)
*
* @var int
*/
const DEFAULT_TTL = 43200;

/**
* 获取缓存
*
* @param string $key 缓存键
* @return mixed|false
*/
public function get( string $key ) {
// 优先使用对象缓存
if ( wp_using_ext_object_cache() ) {
$value = wp_cache_get( $key, self::CACHE_GROUP );
if ( false !== $value ) {
return $value;
}
}
/**
* 获取缓存
*
* @param string $key 缓存键
* @return mixed|false
*/
public function get( string $key ) {
// 优先使用对象缓存
if ( wp_using_ext_object_cache() ) {
$value = wp_cache_get( $key, self::CACHE_GROUP );
if ( false !== $value ) {
return $value;
}
}

// 降级到 transient
return get_transient( 'wpbridge_' . $key );
}
// 降级到 transient
return get_transient( 'wpbridge_' . $key );
}

/**
* 设置缓存
*
* @param string $key 缓存键
* @param mixed $value 缓存值
* @param int $ttl 过期时间(秒)
* @return bool
*/
public function set( string $key, $value, int $ttl = self::DEFAULT_TTL ): bool {
// 使用对象缓存
if ( wp_using_ext_object_cache() ) {
wp_cache_set( $key, $value, self::CACHE_GROUP, $ttl );
}
/**
* 设置缓存
*
* @param string $key 缓存键
* @param mixed $value 缓存值
* @param int $ttl 过期时间(秒)
* @return bool
*/
public function set( string $key, $value, int $ttl = self::DEFAULT_TTL ): bool {
// 使用对象缓存
if ( wp_using_ext_object_cache() ) {
wp_cache_set( $key, $value, self::CACHE_GROUP, $ttl );
}

// 同时存储到 transient
return set_transient( 'wpbridge_' . $key, $value, $ttl );
}
// 同时存储到 transient
return set_transient( 'wpbridge_' . $key, $value, $ttl );
}

/**
* 删除缓存
*
* @param string $key 缓存键
* @return bool
*/
public function delete( string $key ): bool {
if ( wp_using_ext_object_cache() ) {
wp_cache_delete( $key, self::CACHE_GROUP );
}
/**
* 删除缓存
*
* @param string $key 缓存键
* @return bool
*/
public function delete( string $key ): bool {
if ( wp_using_ext_object_cache() ) {
wp_cache_delete( $key, self::CACHE_GROUP );
}

return delete_transient( 'wpbridge_' . $key );
}
return delete_transient( 'wpbridge_' . $key );
}

/**
* 获取带过期缓存兜底的值
*
* @param string $key 缓存键
* @param callable $callback 获取新值的回调
* @param int $ttl 正常 TTL
* @param int $stale_ttl 过期缓存可用时间
* @return mixed
*/
public function get_with_fallback( string $key, callable $callback, int $ttl = self::DEFAULT_TTL, int $stale_ttl = 604800 ) {
// 尝试获取正常缓存
$value = $this->get( $key );
/**
* 获取带过期缓存兜底的值
*
* @param string $key 缓存键
* @param callable $callback 获取新值的回调
* @param int $ttl 正常 TTL
* @param int $stale_ttl 过期缓存可用时间
* @return mixed
*/
public function get_with_fallback( string $key, callable $callback, int $ttl = self::DEFAULT_TTL, int $stale_ttl = 604800 ) {
// 尝试获取正常缓存
$value = $this->get( $key );

if ( false !== $value ) {
return $value;
}
if ( false !== $value ) {
return $value;
}

// 尝试获取新值
try {
$new_value = $callback();
// 尝试获取新值
try {
$new_value = $callback();

if ( null !== $new_value && false !== $new_value ) {
$this->set( $key, $new_value, $ttl );
if ( null !== $new_value && false !== $new_value ) {
$this->set( $key, $new_value, $ttl );

// 同时存储一份过期缓存备份
$this->set( $key . '_stale', $new_value, $stale_ttl );
// 同时存储一份过期缓存备份
$this->set( $key . '_stale', $new_value, $stale_ttl );

return $new_value;
}
} catch ( \Exception $e ) {
// 获取新值失败,尝试使用过期缓存
}
return $new_value;
}
} catch ( \Exception $e ) {
// 获取新值失败,尝试使用过期缓存
}

// 尝试使用过期缓存
$stale_value = $this->get( $key . '_stale' );
// 尝试使用过期缓存
$stale_value = $this->get( $key . '_stale' );

if ( false !== $stale_value ) {
return $stale_value;
}
if ( false !== $stale_value ) {
return $stale_value;
}

return false;
}
return false;
}

/**
* 清除所有缓存
*/
public function flush(): void {
global $wpdb;
/**
* 清除所有缓存
*/
public function flush(): void {
global $wpdb;

// 清除 transient使用 prepare 防止 SQL 注入)
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_wpbridge_' ) . '%'
)
);
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_timeout_wpbridge_' ) . '%'
)
);
// 清除 transient使用 prepare 防止 SQL 注入)
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_wpbridge_' ) . '%'
)
);
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_timeout_wpbridge_' ) . '%'
)
);

// 清除对象缓存组(不使用 flush 避免影响其他插件)
if ( wp_using_ext_object_cache() ) {
wp_cache_delete( 'wpbridge', 'wpbridge' );
}
}
// 清除对象缓存组(不使用 flush 避免影响其他插件)
if ( wp_using_ext_object_cache() ) {
wp_cache_delete( 'wpbridge', 'wpbridge' );
}
}

/**
* 获取缓存统计
*
* @return array
*/
public function get_stats(): array {
global $wpdb;
/**
* 获取缓存统计
*
* @return array
*/
public function get_stats(): array {
global $wpdb;

$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_wpbridge_' ) . '%'
)
);
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_wpbridge_' ) . '%'
)
);

return array(
'transient_count' => (int) $count,
'object_cache' => wp_using_ext_object_cache(),
'object_cache_type' => $this->get_object_cache_type(),
);
}
return [
'transient_count' => (int) $count,
'object_cache' => wp_using_ext_object_cache(),
'object_cache_type' => $this->get_object_cache_type(),
];
}

/**
* 获取对象缓存类型
*
* @return string
*/
private function get_object_cache_type(): string {
if ( ! wp_using_ext_object_cache() ) {
return 'none';
}
/**
* 获取对象缓存类型
*
* @return string
*/
private function get_object_cache_type(): string {
if ( ! wp_using_ext_object_cache() ) {
return 'none';
}

global $wp_object_cache;
global $wp_object_cache;

if ( isset( $wp_object_cache->redis ) || class_exists( 'Redis' ) ) {
return 'redis';
}
if ( isset( $wp_object_cache->redis ) || class_exists( 'Redis' ) ) {
return 'redis';
}

if ( isset( $wp_object_cache->mc ) || class_exists( 'Memcached' ) ) {
return 'memcached';
}
if ( isset( $wp_object_cache->mc ) || class_exists( 'Memcached' ) ) {
return 'memcached';
}

return 'unknown';
}
return 'unknown';
}
}

View file

@ -14,7 +14,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -22,230 +22,222 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class FallbackStrategy {

/**
* 源不可用时的行为
*/
const ON_FAIL_SKIP = 'skip'; // 跳过,继续下一个源
const ON_FAIL_WARN = 'warn'; // 警告,但继续
const ON_FAIL_BLOCK = 'block'; // 阻止更新检查
/**
* 源不可用时的行为
*/
const ON_FAIL_SKIP = 'skip'; // 跳过,继续下一个源
const ON_FAIL_WARN = 'warn'; // 警告,但继续
const ON_FAIL_BLOCK = 'block'; // 阻止更新检查

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;
/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;

/**
* 健康检查器
*
* @var HealthChecker
*/
private HealthChecker $health_checker;
/**
* 健康检查器
*
* @var HealthChecker
*/
private HealthChecker $health_checker;

/**
* 最大重试次数
*
* @var int
*/
const MAX_RETRIES = 2;
/**
* 最大重试次数
*
* @var int
*/
const MAX_RETRIES = 2;

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->cache = new CacheManager();
$this->health_checker = new HealthChecker();
}
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->cache = new CacheManager();
$this->health_checker = new HealthChecker();
}

/**
* 获取可用的源列表(排除不可用的)
*
* @param SourceModel[] $sources 源列表
* @return SourceModel[]
*/
public function get_available_sources( array $sources ): array {
$available = array();
/**
* 获取可用的源列表(排除不可用的)
*
* @param SourceModel[] $sources 源列表
* @return SourceModel[]
*/
public function get_available_sources( array $sources ): array {
$available = [];

foreach ( $sources as $source ) {
// 检查是否在冷却期
if ( $this->health_checker->is_in_cooldown( $source->id ) ) {
Logger::debug( '源在冷却期,跳过', array( 'source' => $source->id ) );
continue;
}
foreach ( $sources as $source ) {
// 检查是否在冷却期
if ( $this->health_checker->is_in_cooldown( $source->id ) ) {
Logger::debug( '源在冷却期,跳过', [ 'source' => $source->id ] );
continue;
}

// 检查缓存的健康状态
$status = $this->health_checker->get_status( $source->id );
// 检查缓存的健康状态
$status = $this->health_checker->get_status( $source->id );

if ( null !== $status && ! $status->is_available() ) {
Logger::debug( '源不可用,跳过', array( 'source' => $source->id ) );
continue;
}
if ( null !== $status && ! $status->is_available() ) {
Logger::debug( '源不可用,跳过', [ 'source' => $source->id ] );
continue;
}

$available[] = $source;
}
$available[] = $source;
}

return $available;
}
return $available;
}

/**
* 执行带降级的操作
*
* @param SourceModel[] $sources 源列表
* @param callable $callback 操作回调,接收 SourceModel 参数
* @param string $cache_key 缓存键(用于过期缓存兜底)
* @return mixed
*/
public function execute_with_fallback( array $sources, callable $callback, string $cache_key = '' ) {
$available = $this->get_available_sources( $sources );
/**
* 执行带降级的操作
*
* @param SourceModel[] $sources 源列表
* @param callable $callback 操作回调,接收 SourceModel 参数
* @param string $cache_key 缓存键(用于过期缓存兜底)
* @return mixed
*/
public function execute_with_fallback( array $sources, callable $callback, string $cache_key = '' ) {
$available = $this->get_available_sources( $sources );

if ( empty( $available ) ) {
Logger::warning( '没有可用的更新源' );
if ( empty( $available ) ) {
Logger::warning( '没有可用的更新源' );

// 尝试使用过期缓存
if ( ! empty( $cache_key ) ) {
$stale = $this->cache->get( $cache_key . '_stale' );
if ( false !== $stale ) {
Logger::info( '使用过期缓存', array( 'key' => $cache_key ) );
return $stale;
}
}
// 尝试使用过期缓存
if ( ! empty( $cache_key ) ) {
$stale = $this->cache->get( $cache_key . '_stale' );
if ( false !== $stale ) {
Logger::info( '使用过期缓存', [ 'key' => $cache_key ] );
return $stale;
}
}

return null;
}
return null;
}

$last_error = null;
$last_error = null;

foreach ( $available as $source ) {
$retries = 0;
foreach ( $available as $source ) {
$retries = 0;

while ( $retries < self::MAX_RETRIES ) {
try {
$result = $callback( $source );
while ( $retries < self::MAX_RETRIES ) {
try {
$result = $callback( $source );

if ( null !== $result && false !== $result ) {
// 成功,缓存结果
if ( ! empty( $cache_key ) ) {
$this->cache->set( $cache_key, $result );
$this->cache->set( $cache_key . '_stale', $result, 604800 ); // 7 天
}
if ( null !== $result && false !== $result ) {
// 成功,缓存结果
if ( ! empty( $cache_key ) ) {
$this->cache->set( $cache_key, $result );
$this->cache->set( $cache_key . '_stale', $result, 604800 ); // 7 天
}

return $result;
}
return $result;
}

// 返回 null/false 但没有异常,不重试
break;
// 返回 null/false 但没有异常,不重试
break;

} catch ( \Exception $e ) {
$last_error = $e;
++$retries;
} catch ( \Exception $e ) {
$last_error = $e;
$retries++;

Logger::warning(
'操作失败,重试中',
array(
'source' => $source->id,
'retry' => $retries,
'error' => $e->getMessage(),
)
);
Logger::warning( '操作失败,重试中', [
'source' => $source->id,
'retry' => $retries,
'error' => $e->getMessage(),
] );

if ( $retries >= self::MAX_RETRIES ) {
// 标记源为失败
$this->health_checker->check( $source, true );
}
}
}
}
if ( $retries >= self::MAX_RETRIES ) {
// 标记源为失败
$this->health_checker->check( $source, true );
}
}
}
}

// 所有源都失败,尝试使用过期缓存
if ( ! empty( $cache_key ) ) {
$stale = $this->cache->get( $cache_key . '_stale' );
if ( false !== $stale ) {
Logger::info( '所有源失败,使用过期缓存', array( 'key' => $cache_key ) );
return $stale;
}
}
// 所有源都失败,尝试使用过期缓存
if ( ! empty( $cache_key ) ) {
$stale = $this->cache->get( $cache_key . '_stale' );
if ( false !== $stale ) {
Logger::info( '所有源失败,使用过期缓存', [ 'key' => $cache_key ] );
return $stale;
}
}

if ( null !== $last_error ) {
Logger::error( '所有源都失败', array( 'error' => $last_error->getMessage() ) );
}
if ( null !== $last_error ) {
Logger::error( '所有源都失败', [ 'error' => $last_error->getMessage() ] );
}

return null;
}
return null;
}

/**
* 处理源失败
*
* @param SourceModel $source 源模型
* @param string $error 错误信息
*/
public function handle_source_failure( SourceModel $source, string $error ): void {
$behavior = $this->settings->get( 'on_source_fail', self::ON_FAIL_SKIP );
/**
* 处理源失败
*
* @param SourceModel $source 源模型
* @param string $error 错误信息
*/
public function handle_source_failure( SourceModel $source, string $error ): void {
$behavior = $this->settings->get( 'on_source_fail', self::ON_FAIL_SKIP );

Logger::warning(
'源失败',
array(
'source' => $source->id,
'error' => $error,
'behavior' => $behavior,
)
);
Logger::warning( '源失败', [
'source' => $source->id,
'error' => $error,
'behavior' => $behavior,
] );

switch ( $behavior ) {
case self::ON_FAIL_WARN:
// 添加管理员通知
$this->add_admin_notice( $source, $error );
break;
switch ( $behavior ) {
case self::ON_FAIL_WARN:
// 添加管理员通知
$this->add_admin_notice( $source, $error );
break;

case self::ON_FAIL_BLOCK:
// 阻止更新检查(不推荐)
throw new \RuntimeException(
sprintf(
__( '更新源 %1$s 不可用: %2$s', 'wpbridge' ),
$source->name,
$error
)
);
case self::ON_FAIL_BLOCK:
// 阻止更新检查(不推荐)
throw new \RuntimeException( sprintf(
__( '更新源 %s 不可用: %s', 'wpbridge' ),
$source->name,
$error
) );

case self::ON_FAIL_SKIP:
default:
// 静默跳过
break;
}
}
case self::ON_FAIL_SKIP:
default:
// 静默跳过
break;
}
}

/**
* 添加管理员通知
*
* @param SourceModel $source 源模型
* @param string $error 错误信息
*/
private function add_admin_notice( SourceModel $source, string $error ): void {
$notices = get_option( 'wpbridge_admin_notices', array() );
/**
* 添加管理员通知
*
* @param SourceModel $source 源模型
* @param string $error 错误信息
*/
private function add_admin_notice( SourceModel $source, string $error ): void {
$notices = get_option( 'wpbridge_admin_notices', [] );

$notices[] = array(
'type' => 'warning',
'message' => sprintf(
__( '更新源 "%1$s" 暂时不可用: %2$s', 'wpbridge' ),
$source->name,
$error
),
'time' => time(),
);
$notices[] = [
'type' => 'warning',
'message' => sprintf(
__( '更新源 "%s" 暂时不可用: %s', 'wpbridge' ),
$source->name,
$error
),
'time' => time(),
];

// 只保留最近 10 条通知
$notices = array_slice( $notices, -10 );
// 只保留最近 10 条通知
$notices = array_slice( $notices, -10 );

update_option( 'wpbridge_admin_notices', $notices );
}
update_option( 'wpbridge_admin_notices', $notices );
}
}

View file

@ -13,7 +13,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -21,173 +21,167 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class HealthChecker {

/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;
/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;

/**
* 健康状态缓存 TTL1 小时)
*
* @var int
*/
const HEALTH_CACHE_TTL = 3600;
/**
* 健康状态缓存 TTL1 小时)
*
* @var int
*/
const HEALTH_CACHE_TTL = 3600;

/**
* 失败源冷却时间30 分钟)
*
* @var int
*/
const FAILED_COOLDOWN = 1800;
/**
* 失败源冷却时间30 分钟)
*
* @var int
*/
const FAILED_COOLDOWN = 1800;

/**
* 构造函数
*/
public function __construct() {
$this->cache = new CacheManager();
}
/**
* 构造函数
*/
public function __construct() {
$this->cache = new CacheManager();
}

/**
* 检查源健康状态
*
* @param SourceModel $source 源模型
* @param bool $force 是否强制检查
* @return HealthStatus
*/
public function check( SourceModel $source, bool $force = false ): HealthStatus {
$cache_key = 'health_' . $source->id;
/**
* 检查源健康状态
*
* @param SourceModel $source 源模型
* @param bool $force 是否强制检查
* @return HealthStatus
*/
public function check( SourceModel $source, bool $force = false ): HealthStatus {
$cache_key = 'health_' . $source->id;

// 检查缓存
if ( ! $force ) {
$cached = $this->cache->get( $cache_key );
if ( false !== $cached && $cached instanceof HealthStatus ) {
return $cached;
}
}
// 检查缓存
if ( ! $force ) {
$cached = $this->cache->get( $cache_key );
if ( false !== $cached && $cached instanceof HealthStatus ) {
return $cached;
}
}

// 检查是否在冷却期
if ( ! $force && $this->is_in_cooldown( $source->id ) ) {
return HealthStatus::failed( __( '源在冷却期内', 'wpbridge' ) );
}
// 检查是否在冷却期
if ( ! $force && $this->is_in_cooldown( $source->id ) ) {
return HealthStatus::failed( __( '源在冷却期内', 'wpbridge' ) );
}

// 执行健康检查
$handler = $source->get_handler();
// 执行健康检查
$handler = $source->get_handler();

if ( null === $handler ) {
$status = HealthStatus::failed( __( '无法获取处理器', 'wpbridge' ) );
} else {
$status = $handler->test_connection();
}
if ( null === $handler ) {
$status = HealthStatus::failed( __( '无法获取处理器', 'wpbridge' ) );
} else {
$status = $handler->test_connection();
}

// 缓存结果
$ttl = $status->is_healthy() ? self::HEALTH_CACHE_TTL : self::FAILED_COOLDOWN;
$this->cache->set( $cache_key, $status, $ttl );
// 缓存结果
$ttl = $status->is_healthy() ? self::HEALTH_CACHE_TTL : self::FAILED_COOLDOWN;
$this->cache->set( $cache_key, $status, $ttl );

// 如果失败,设置冷却
if ( ! $status->is_available() ) {
$this->set_cooldown( $source->id );
}
// 如果失败,设置冷却
if ( ! $status->is_available() ) {
$this->set_cooldown( $source->id );
}

Logger::debug(
'健康检查完成',
array(
'source' => $source->id,
'status' => $status->status,
'time' => $status->response_time,
)
);
Logger::debug( '健康检查完成', [
'source' => $source->id,
'status' => $status->status,
'time' => $status->response_time,
] );

return $status;
}
return $status;
}

/**
* 批量检查源健康状态
*
* @param SourceModel[] $sources 源列表
* @return array<string, HealthStatus>
*/
public function check_all( array $sources ): array {
$results = array();
/**
* 批量检查源健康状态
*
* @param SourceModel[] $sources 源列表
* @return array<string, HealthStatus>
*/
public function check_all( array $sources ): array {
$results = [];

foreach ( $sources as $source ) {
$results[ $source->id ] = $this->check( $source );
}
foreach ( $sources as $source ) {
$results[ $source->id ] = $this->check( $source );
}

return $results;
}
return $results;
}

/**
* 获取源健康状态(仅从缓存)
*
* @param string $source_id 源 ID
* @return HealthStatus|null
*/
public function get_status( string $source_id ): ?HealthStatus {
$cached = $this->cache->get( 'health_' . $source_id );
/**
* 获取源健康状态(仅从缓存)
*
* @param string $source_id 源 ID
* @return HealthStatus|null
*/
public function get_status( string $source_id ): ?HealthStatus {
$cached = $this->cache->get( 'health_' . $source_id );

if ( false !== $cached && $cached instanceof HealthStatus ) {
return $cached;
}
if ( false !== $cached && $cached instanceof HealthStatus ) {
return $cached;
}

return null;
}
return null;
}

/**
* 检查源是否在冷却期
*
* @param string $source_id 源 ID
* @return bool
*/
public function is_in_cooldown( string $source_id ): bool {
$cooldown = $this->cache->get( 'cooldown_' . $source_id );
return false !== $cooldown;
}
/**
* 检查源是否在冷却期
*
* @param string $source_id 源 ID
* @return bool
*/
public function is_in_cooldown( string $source_id ): bool {
$cooldown = $this->cache->get( 'cooldown_' . $source_id );
return false !== $cooldown;
}

/**
* 设置源冷却
*
* @param string $source_id 源 ID
*/
private function set_cooldown( string $source_id ): void {
$this->cache->set( 'cooldown_' . $source_id, time(), self::FAILED_COOLDOWN );
/**
* 设置源冷却
*
* @param string $source_id 源 ID
*/
private function set_cooldown( string $source_id ): void {
$this->cache->set( 'cooldown_' . $source_id, time(), self::FAILED_COOLDOWN );

Logger::info(
'源进入冷却期',
array(
'source' => $source_id,
'duration' => self::FAILED_COOLDOWN,
)
);
}
Logger::info( '源进入冷却期', [
'source' => $source_id,
'duration' => self::FAILED_COOLDOWN,
] );
}

/**
* 清除源冷却
*
* @param string $source_id 源 ID
*/
public function clear_cooldown( string $source_id ): void {
$this->cache->delete( 'cooldown_' . $source_id );
}
/**
* 清除源冷却
*
* @param string $source_id 源 ID
*/
public function clear_cooldown( string $source_id ): void {
$this->cache->delete( 'cooldown_' . $source_id );
}

/**
* 清除所有健康状态缓存
*/
public function clear_all(): void {
global $wpdb;
/**
* 清除所有健康状态缓存
*/
public function clear_all(): void {
global $wpdb;

$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_wpbridge_health_' ) . '%'
)
);
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_wpbridge_cooldown_' ) . '%'
)
);
}
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_wpbridge_health_' ) . '%'
)
);
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_wpbridge_cooldown_' ) . '%'
)
);
}
}

View file

@ -68,13 +68,10 @@ class BridgeClient {
$response = $this->request( 'GET', "/api/v1/plugin/{$slug}" );

if ( is_wp_error( $response ) ) {
Logger::error(
'Failed to get plugin info',
array(
'slug' => $slug,
'error' => $response->get_error_message(),
)
);
Logger::error( 'Failed to get plugin info', [
'slug' => $slug,
'error' => $response->get_error_message(),
] );
return null;
}

@ -98,19 +95,16 @@ class BridgeClient {
* @return array
*/
public function list_vendors(): array {
$response = $this->request( 'GET', '/api/v1/admin/vendors', array(), true );
$response = $this->request( 'GET', '/api/v1/admin/vendors', [], true );

if ( is_wp_error( $response ) ) {
Logger::error(
'Failed to list vendors',
array(
'error' => $response->get_error_message(),
)
);
return array();
Logger::error( 'Failed to list vendors', [
'error' => $response->get_error_message(),
] );
return [];
}

return $response ?? array();
return $response ?? [];
}

/**
@ -123,16 +117,16 @@ class BridgeClient {
$response = $this->request( 'POST', '/api/v1/admin/vendors', $data, true );

if ( is_wp_error( $response ) ) {
return array(
return [
'success' => false,
'message' => $response->get_error_message(),
);
];
}

return array(
return [
'success' => true,
'data' => $response,
);
];
}

/**
@ -146,16 +140,16 @@ class BridgeClient {
$response = $this->request( 'PUT', "/api/v1/admin/vendors/{$id}", $data, true );

if ( is_wp_error( $response ) ) {
return array(
return [
'success' => false,
'message' => $response->get_error_message(),
);
];
}

return array(
return [
'success' => true,
'data' => $response,
);
];
}

/**
@ -165,18 +159,18 @@ class BridgeClient {
* @return array
*/
public function delete_vendor( int $id ): array {
$response = $this->request( 'DELETE', "/api/v1/admin/vendors/{$id}", array(), true );
$response = $this->request( 'DELETE', "/api/v1/admin/vendors/{$id}", [], true );

if ( is_wp_error( $response ) ) {
return array(
return [
'success' => false,
'message' => $response->get_error_message(),
);
];
}

return array(
return [
'success' => true,
);
];
}

/**
@ -185,19 +179,16 @@ class BridgeClient {
* @return array
*/
public function list_plugins(): array {
$response = $this->request( 'GET', '/api/v1/admin/plugins', array(), true );
$response = $this->request( 'GET', '/api/v1/admin/plugins', [], true );

if ( is_wp_error( $response ) ) {
Logger::error(
'Failed to list plugins',
array(
'error' => $response->get_error_message(),
)
);
return array();
Logger::error( 'Failed to list plugins', [
'error' => $response->get_error_message(),
] );
return [];
}

return $response ?? array();
return $response ?? [];
}

/**
@ -210,16 +201,16 @@ class BridgeClient {
$response = $this->request( 'POST', '/api/v1/admin/plugins', $data, true );

if ( is_wp_error( $response ) ) {
return array(
return [
'success' => false,
'message' => $response->get_error_message(),
);
];
}

return array(
return [
'success' => true,
'data' => $response,
);
];
}

/**
@ -229,7 +220,7 @@ class BridgeClient {
* @return array|null
*/
public function get_plugin( string $slug ): ?array {
$response = $this->request( 'GET', "/api/v1/admin/plugins/{$slug}", array(), true );
$response = $this->request( 'GET', "/api/v1/admin/plugins/{$slug}", [], true );

if ( is_wp_error( $response ) ) {
return null;
@ -249,16 +240,16 @@ class BridgeClient {
$response = $this->request( 'PUT', "/api/v1/admin/plugins/{$slug}", $data, true );

if ( is_wp_error( $response ) ) {
return array(
return [
'success' => false,
'message' => $response->get_error_message(),
);
];
}

return array(
return [
'success' => true,
'data' => $response,
);
];
}

/**
@ -268,18 +259,18 @@ class BridgeClient {
* @return array
*/
public function delete_plugin( string $slug ): array {
$response = $this->request( 'DELETE', "/api/v1/admin/plugins/{$slug}", array(), true );
$response = $this->request( 'DELETE', "/api/v1/admin/plugins/{$slug}", [], true );

if ( is_wp_error( $response ) ) {
return array(
return [
'success' => false,
'message' => $response->get_error_message(),
);
];
}

return array(
return [
'success' => true,
);
];
}

/**
@ -306,17 +297,17 @@ class BridgeClient {
* @param bool $auth 是否需要认证
* @return array|\WP_Error
*/
private function request( string $method, string $endpoint, array $data = array(), bool $auth = false ) {
private function request( string $method, string $endpoint, array $data = [], bool $auth = false ) {
$url = $this->server_url . $endpoint;

$args = array(
$args = [
'method' => $method,
'timeout' => $this->timeout,
'headers' => array(
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
);
],
];

// 添加认证头
if ( $auth && ! empty( $this->api_key ) ) {
@ -324,7 +315,7 @@ class BridgeClient {
}

// 添加请求体
if ( ! empty( $data ) && in_array( $method, array( 'POST', 'PUT', 'PATCH' ), true ) ) {
if ( ! empty( $data ) && in_array( $method, [ 'POST', 'PUT', 'PATCH' ], true ) ) {
$args['body'] = wp_json_encode( $data );
}

@ -339,14 +330,14 @@ class BridgeClient {

// 处理 204 No Content
if ( $status_code === 204 ) {
return array();
return [];
}

// 处理错误状态码
if ( $status_code >= 400 ) {
$error_data = json_decode( $body, true );
$message = $error_data['message'] ?? $error_data['error'] ?? "HTTP {$status_code}";
return new \WP_Error( 'bridge_server_error', $message, array( 'status' => $status_code ) );
return new \WP_Error( 'bridge_server_error', $message, [ 'status' => $status_code ] );
}

// 解析 JSON 响应

View file

@ -118,10 +118,10 @@ class BridgeManager {
$client = new BridgeClient( $server_url, $api_key );

if ( ! $client->health_check() ) {
return array(
return [
'success' => false,
'message' => __( '无法连接到 Bridge Server', 'wpbridge' ),
);
];
}

// 保存配置URL 明文存储API Key 加密存储)
@ -130,12 +130,12 @@ class BridgeManager {

$this->bridge_client = $client;

Logger::info( 'Bridge server configured', array( 'url' => $server_url ) );
Logger::info( 'Bridge server configured', [ 'url' => $server_url ] );

return array(
return [
'success' => true,
'message' => __( 'Bridge Server 配置成功', 'wpbridge' ),
);
];
}

/**
@ -144,7 +144,7 @@ class BridgeManager {
* @return void
*/
private function init_vendors(): void {
$vendor_configs = $this->settings->get( 'vendors', array() );
$vendor_configs = $this->settings->get( 'vendors', [] );

foreach ( $vendor_configs as $vendor_id => $config ) {
if ( empty( $config['enabled'] ) ) {
@ -182,7 +182,7 @@ class BridgeManager {
$plugins = $this->bridge_client->list_plugins();
if ( ! empty( $plugins ) ) {
// 转换为 slug => info 格式
$result = array();
$result = [];
foreach ( $plugins as $plugin ) {
$result[ $plugin['slug'] ] = $plugin;
}
@ -191,7 +191,7 @@ class BridgeManager {
}

// 回退到 RemoteConfig
return $this->remote_config->get( 'bridgeable_plugins', array() );
return $this->remote_config->get( 'bridgeable_plugins', [] );
}

/**
@ -219,18 +219,15 @@ class BridgeManager {
* @return array
*/
public function get_all_available_plugins(): array {
$plugins = array();
$plugins = [];

// 1. 官方优化列表
$official = $this->get_available_plugins();
foreach ( $official as $slug => $info ) {
$plugins[ $slug ] = array_merge(
$info,
array(
'source' => 'official',
'vendor' => null,
)
);
$plugins[ $slug ] = array_merge( $info, [
'source' => 'official',
'vendor' => null,
] );
}

// 2. 供应商渠道插件
@ -245,16 +242,13 @@ class BridgeManager {
}

// 3. 用户自定义插件
$custom = $this->settings->get( 'custom_plugins', array() );
$custom = $this->settings->get( 'custom_plugins', [] );
foreach ( $custom as $slug => $info ) {
if ( ! isset( $plugins[ $slug ] ) ) {
$plugins[ $slug ] = array_merge(
$info,
array(
'source' => 'custom',
'vendor' => null,
)
);
$plugins[ $slug ] = array_merge( $info, [
'source' => 'custom',
'vendor' => null,
] );
}
}

@ -276,7 +270,7 @@ class BridgeManager {
* @return array
*/
public function get_bridged_plugins(): array {
return $this->settings->get( 'bridged_plugins', array() );
return $this->settings->get( 'bridged_plugins', [] );
}

/**
@ -290,11 +284,11 @@ class BridgeManager {
// 1. 检查是否在可桥接列表(混合模式)
$all_available = $this->get_all_available_plugins();
if ( ! isset( $all_available[ $plugin_slug ] ) ) {
return array(
return [
'success' => false,
'message' => __( '该插件不在可桥接列表中', 'wpbridge' ),
'code' => 'not_available',
);
];
}

$plugin_info = $all_available[ $plugin_slug ];
@ -302,40 +296,34 @@ class BridgeManager {
// 2. H5 修复: GPL 合规验证
$gpl_result = $this->gpl_validator->validate( $plugin_slug, $plugin_file );
if ( $gpl_result['is_gpl'] === false ) {
Logger::warning(
'GPL validation failed',
array(
'plugin' => $plugin_slug,
'result' => $gpl_result,
)
);
return array(
Logger::warning( 'GPL validation failed', [
'plugin' => $plugin_slug,
'result' => $gpl_result,
] );
return [
'success' => false,
'message' => __( '该插件不是 GPL 授权,无法桥接', 'wpbridge' ),
'code' => 'not_gpl',
'license' => $gpl_result['license'],
);
];
}

if ( $gpl_result['is_gpl'] === null && $gpl_result['confidence'] < 50 ) {
// 无法确定,但置信度低,警告用户
Logger::info(
'GPL validation uncertain',
array(
'plugin' => $plugin_slug,
'result' => $gpl_result,
)
);
Logger::info( 'GPL validation uncertain', [
'plugin' => $plugin_slug,
'result' => $gpl_result,
] );
}

// 3. 检查订阅限制
$limit_check = $this->check_subscription_limit();
if ( ! $limit_check['allowed'] ) {
return array(
return [
'success' => false,
'message' => $limit_check['message'],
'code' => 'limit_exceeded',
);
];
}

// 4. 添加到桥接列表
@ -344,23 +332,20 @@ class BridgeManager {
$bridged[] = $plugin_slug;
$this->settings->set( 'bridged_plugins', $bridged );

Logger::info(
'Plugin bridge enabled',
array(
'plugin' => $plugin_slug,
'gpl_result' => $gpl_result,
)
);
Logger::info( 'Plugin bridge enabled', [
'plugin' => $plugin_slug,
'gpl_result' => $gpl_result,
] );
}

return array(
'success' => true,
'message' => __( '桥接已启用', 'wpbridge' ),
'code' => 'enabled',
'gpl_result' => $gpl_result,
'source' => $plugin_info['source'] ?? 'official',
'vendor' => $plugin_info['vendor'] ?? null,
);
return [
'success' => true,
'message' => __( '桥接已启用', 'wpbridge' ),
'code' => 'enabled',
'gpl_result' => $gpl_result,
'source' => $plugin_info['source'] ?? 'official',
'vendor' => $plugin_info['vendor'] ?? null,
];
}

/**
@ -371,21 +356,21 @@ class BridgeManager {
*/
public function disable_bridge( string $plugin_slug ): array {
$bridged = $this->get_bridged_plugins();
$bridged = array_diff( $bridged, array( $plugin_slug ) );
$bridged = array_diff( $bridged, [ $plugin_slug ] );
$result = $this->settings->set( 'bridged_plugins', array_values( $bridged ) );

if ( $result ) {
Logger::info( 'Plugin bridge disabled', array( 'plugin' => $plugin_slug ) );
return array(
Logger::info( 'Plugin bridge disabled', [ 'plugin' => $plugin_slug ] );
return [
'success' => true,
'message' => __( '桥接已禁用', 'wpbridge' ),
);
];
}

return array(
return [
'success' => false,
'message' => __( '禁用失败', 'wpbridge' ),
);
];
}

/**
@ -408,24 +393,24 @@ class BridgeManager {

// Agency 计划无限制
if ( $subscription['plan'] === 'agency' ) {
return array( 'allowed' => true );
return [ 'allowed' => true ];
}

$current_count = count( $this->get_bridged_plugins() );
$limit = $subscription['plugins_limit'] ?? 5;

if ( $current_count >= $limit ) {
return array(
return [
'allowed' => false,
'message' => sprintf(
/* translators: %d: plugin limit */
__( '已达到插件数量限制 (%d),请升级订阅', 'wpbridge' ),
$limit
),
);
];
}

return array( 'allowed' => true );
return [ 'allowed' => true ];
}

/**
@ -434,15 +419,15 @@ class BridgeManager {
* @return array
*/
public function get_subscription(): array {
$default = array(
$default = [
'plan' => 'free',
'plugins_limit' => 0,
'site_limit' => 1,
'status' => 'active',
'expires_at' => null,
);
];

$subscription = $this->settings->get( 'subscription', array() );
$subscription = $this->settings->get( 'subscription', [] );
return array_merge( $default, $subscription );
}

@ -456,14 +441,14 @@ class BridgeManager {
$bridged = $this->get_bridged_plugins();
$available = $this->get_available_plugins();

return array(
return [
'bridged_count' => count( $bridged ),
'available_count' => count( $available ),
'plan' => $subscription['plan'],
'plugins_limit' => $subscription['plugins_limit'],
'plugins_used' => count( $bridged ),
'can_add_more' => $subscription['plan'] === 'agency' || count( $bridged ) < $subscription['plugins_limit'],
);
];
}

/**
@ -479,7 +464,7 @@ class BridgeManager {
$all_plugins = get_plugins();
$available = $this->get_available_plugins();
$bridged = $this->get_bridged_plugins();
$result = array();
$result = [];

foreach ( $all_plugins as $file => $data ) {
$slug = dirname( $file );
@ -490,14 +475,14 @@ class BridgeManager {
if ( isset( $available[ $slug ] ) ) {
$gpl_result = $this->gpl_validator->validate( $slug, $file );

$result[ $slug ] = array(
$result[ $slug ] = [
'file' => $file,
'name' => $data['Name'],
'version' => $data['Version'],
'is_bridged' => in_array( $slug, $bridged, true ),
'gpl_status' => $gpl_result,
'available' => $available[ $slug ],
);
];
}
}

@ -532,16 +517,16 @@ class BridgeManager {
string $consumer_key,
string $consumer_secret
): array {
$vendors = $this->settings->get( 'vendors', array() );
$vendors = $this->settings->get( 'vendors', [] );

if ( isset( $vendors[ $vendor_id ] ) ) {
return array(
return [
'success' => false,
'message' => __( '供应商 ID 已存在', 'wpbridge' ),
);
];
}

$vendors[ $vendor_id ] = array(
$vendors[ $vendor_id ] = [
'name' => $name,
'type' => $type,
'api_url' => $api_url,
@ -549,7 +534,7 @@ class BridgeManager {
'consumer_secret' => $consumer_secret,
'enabled' => true,
'created_at' => time(),
);
];

$this->settings->set( 'vendors', $vendors );

@ -565,18 +550,12 @@ class BridgeManager {
$this->vendor_manager->register( $vendor );
}

Logger::info(
'Vendor added',
array(
'vendor_id' => $vendor_id,
'type' => $type,
)
);
Logger::info( 'Vendor added', [ 'vendor_id' => $vendor_id, 'type' => $type ] );

return array(
return [
'success' => true,
'message' => __( '供应商已添加', 'wpbridge' ),
);
];
}

/**
@ -586,25 +565,25 @@ class BridgeManager {
* @return array
*/
public function remove_vendor( string $vendor_id ): array {
$vendors = $this->settings->get( 'vendors', array() );
$vendors = $this->settings->get( 'vendors', [] );

if ( ! isset( $vendors[ $vendor_id ] ) ) {
return array(
return [
'success' => false,
'message' => __( '供应商不存在', 'wpbridge' ),
);
];
}

unset( $vendors[ $vendor_id ] );
$this->settings->set( 'vendors', $vendors );
$this->vendor_manager->unregister( $vendor_id );

Logger::info( 'Vendor removed', array( 'vendor_id' => $vendor_id ) );
Logger::info( 'Vendor removed', [ 'vendor_id' => $vendor_id ] );

return array(
return [
'success' => true,
'message' => __( '供应商已移除', 'wpbridge' ),
);
];
}

/**
@ -613,7 +592,7 @@ class BridgeManager {
* @return array
*/
public function get_vendors(): array {
return $this->settings->get( 'vendors', array() );
return $this->settings->get( 'vendors', [] );
}

/**
@ -626,20 +605,20 @@ class BridgeManager {
$vendor = $this->vendor_manager->get( $vendor_id );

if ( ! $vendor ) {
return array(
return [
'success' => false,
'message' => __( '供应商不存在或未启用', 'wpbridge' ),
);
];
}

$result = $vendor->test_connection();

return array(
return [
'success' => $result,
'message' => $result
? __( '连接成功', 'wpbridge' )
: __( '连接失败', 'wpbridge' ),
);
];
}

/**
@ -650,23 +629,20 @@ class BridgeManager {
* @return array
*/
public function add_custom_plugin( string $plugin_slug, array $info ): array {
$custom = $this->settings->get( 'custom_plugins', array() );
$custom = $this->settings->get( 'custom_plugins', [] );

$custom[ $plugin_slug ] = array_merge(
$info,
array(
'added_at' => time(),
)
);
$custom[ $plugin_slug ] = array_merge( $info, [
'added_at' => time(),
] );

$this->settings->set( 'custom_plugins', $custom );

Logger::info( 'Custom plugin added', array( 'plugin' => $plugin_slug ) );
Logger::info( 'Custom plugin added', [ 'plugin' => $plugin_slug ] );

return array(
return [
'success' => true,
'message' => __( '自定义插件已添加', 'wpbridge' ),
);
];
}

/**
@ -676,21 +652,21 @@ class BridgeManager {
* @return array
*/
public function remove_custom_plugin( string $plugin_slug ): array {
$custom = $this->settings->get( 'custom_plugins', array() );
$custom = $this->settings->get( 'custom_plugins', [] );

if ( ! isset( $custom[ $plugin_slug ] ) ) {
return array(
return [
'success' => false,
'message' => __( '自定义插件不存在', 'wpbridge' ),
);
];
}

unset( $custom[ $plugin_slug ] );
$this->settings->set( 'custom_plugins', $custom );

return array(
return [
'success' => true,
'message' => __( '自定义插件已移除', 'wpbridge' ),
);
];
}
}

View file

@ -13,7 +13,7 @@ use WPBridge\Security\Validator;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -22,397 +22,370 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class CommercialManager {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 已注册的商业插件
*
* @var array
*/
private array $registered_plugins = array();
/**
* 已注册的商业插件
*
* @var array
*/
private array $registered_plugins = [];

/**
* 版本锁定列表
*
* @var array
*/
private array $version_locks = array();
/**
* 版本锁定列表
*
* @var array
*/
private array $version_locks = [];

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->version_locks = $this->settings->get( 'version_locks', array() );
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->version_locks = $this->settings->get( 'version_locks', [] );

$this->init_hooks();
}
$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'filter_updates' ), 100 );
add_action( 'upgrader_process_complete', array( $this, 'on_upgrade_complete' ), 10, 2 );
}
/**
* 初始化钩子
*/
private function init_hooks(): void {
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'filter_updates' ], 100 );
add_action( 'upgrader_process_complete', [ $this, 'on_upgrade_complete' ], 10, 2 );
}

/**
* 注册商业插件
*
* @param string $slug 插件 slug
* @param array $config 配置
*/
public function register( string $slug, array $config ): void {
$this->registered_plugins[ $slug ] = wp_parse_args(
$config,
array(
'name' => $slug,
'license_type' => 'unknown',
'update_source' => '',
'backup_enabled' => true,
)
);
/**
* 注册商业插件
*
* @param string $slug 插件 slug
* @param array $config 配置
*/
public function register( string $slug, array $config ): void {
$this->registered_plugins[ $slug ] = wp_parse_args( $config, [
'name' => $slug,
'license_type' => 'unknown',
'update_source' => '',
'backup_enabled' => true,
] );

Logger::debug( '注册商业插件', array( 'slug' => $slug ) );
}
Logger::debug( '注册商业插件', [ 'slug' => $slug ] );
}

/**
* 检测已安装的商业插件
*
* @return array
*/
public function detect_commercial_plugins(): array {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
/**
* 检测已安装的商业插件
*
* @return array
*/
public function detect_commercial_plugins(): array {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$all_plugins = get_plugins();
$commercial_plugins = array();
$all_plugins = get_plugins();
$commercial_plugins = [];

foreach ( $all_plugins as $file => $data ) {
$slug = dirname( $file );
foreach ( $all_plugins as $file => $data ) {
$slug = dirname( $file );

// 检测商业插件特征
if ( $this->is_commercial_plugin( $file, $data ) ) {
$commercial_plugins[ $slug ] = array(
'file' => $file,
'name' => $data['Name'],
'version' => $data['Version'],
'license_type' => $this->detect_license_type( $file, $data ),
'registered' => isset( $this->registered_plugins[ $slug ] ),
);
}
}
// 检测商业插件特征
if ( $this->is_commercial_plugin( $file, $data ) ) {
$commercial_plugins[ $slug ] = [
'file' => $file,
'name' => $data['Name'],
'version' => $data['Version'],
'license_type' => $this->detect_license_type( $file, $data ),
'registered' => isset( $this->registered_plugins[ $slug ] ),
];
}
}

return $commercial_plugins;
}
return $commercial_plugins;
}

/**
* 检查是否是商业插件
*
* @param string $file 插件文件
* @param array $data 插件数据
* @return bool
*/
private function is_commercial_plugin( string $file, array $data ): bool {
// 已知商业插件列表(允许通过过滤器扩展)
$known_commercial = apply_filters(
'wpbridge_known_commercial_plugins',
array(
'elementor-pro',
'wordpress-seo-premium',
'seo-by-rank-math-pro',
'advanced-custom-fields-pro',
'gravityforms',
'wpforms',
'ninja-forms',
'woocommerce-subscriptions',
'woocommerce-memberships',
'learndash',
'memberpress',
'wpml-sitepress-multilingual-cms',
'updraftplus-premium',
'wp-rocket',
'perfmatters',
)
);
/**
* 检查是否是商业插件
*
* @param string $file 插件文件
* @param array $data 插件数据
* @return bool
*/
private function is_commercial_plugin( string $file, array $data ): bool {
// 已知商业插件列表(允许通过过滤器扩展)
$known_commercial = apply_filters( 'wpbridge_known_commercial_plugins', [
'elementor-pro',
'wordpress-seo-premium',
'seo-by-rank-math-pro',
'advanced-custom-fields-pro',
'gravityforms',
'wpforms',
'ninja-forms',
'woocommerce-subscriptions',
'woocommerce-memberships',
'learndash',
'memberpress',
'wpml-sitepress-multilingual-cms',
'updraftplus-premium',
'wp-rocket',
'perfmatters',
] );

$slug = dirname( $file );
$slug = dirname( $file );

if ( in_array( $slug, $known_commercial, true ) ) {
return true;
}
if ( in_array( $slug, $known_commercial, true ) ) {
return true;
}

// 检查插件头信息
$plugin_path = WP_PLUGIN_DIR . '/' . $file;
// 检查插件头信息
$plugin_path = WP_PLUGIN_DIR . '/' . $file;

// 路径安全验证:防止路径遍历
$real_path = realpath( $plugin_path );
$plugin_dir_real = realpath( WP_PLUGIN_DIR );
// 路径安全验证:防止路径遍历
$real_path = realpath( $plugin_path );
$plugin_dir_real = realpath( WP_PLUGIN_DIR );

if ( ! $real_path || ! $plugin_dir_real || strpos( $real_path, $plugin_dir_real . DIRECTORY_SEPARATOR ) !== 0 ) {
return false; // 路径不安全,跳过
}
if ( ! $real_path || ! $plugin_dir_real || strpos( $real_path, $plugin_dir_real . DIRECTORY_SEPARATOR ) !== 0 ) {
return false; // 路径不安全,跳过
}

if ( file_exists( $real_path ) ) {
$content = file_get_contents( $real_path, false, null, 0, 8192 );
if ( file_exists( $real_path ) ) {
$content = file_get_contents( $real_path, false, null, 0, 8192 );

// 检查授权相关关键词
$license_keywords = array(
'license_key',
'license-key',
'activation_key',
'purchase_code',
'envato',
'codecanyon',
'themeforest',
);
// 检查授权相关关键词
$license_keywords = [
'license_key',
'license-key',
'activation_key',
'purchase_code',
'envato',
'codecanyon',
'themeforest',
];

foreach ( $license_keywords as $keyword ) {
if ( stripos( $content, $keyword ) !== false ) {
return true;
}
}
}
foreach ( $license_keywords as $keyword ) {
if ( stripos( $content, $keyword ) !== false ) {
return true;
}
}
}

return false;
}
return false;
}

/**
* 检测授权类型
*
* @param string $file 插件文件
* @param array $data 插件数据
* @return string
*/
private function detect_license_type( string $file, array $data ): string {
$plugin_path = WP_PLUGIN_DIR . '/' . $file;
/**
* 检测授权类型
*
* @param string $file 插件文件
* @param array $data 插件数据
* @return string
*/
private function detect_license_type( string $file, array $data ): string {
$plugin_path = WP_PLUGIN_DIR . '/' . $file;

if ( ! file_exists( $plugin_path ) ) {
return 'unknown';
}
if ( ! file_exists( $plugin_path ) ) {
return 'unknown';
}

$content = file_get_contents( $plugin_path, false, null, 0, 8192 );
$content = file_get_contents( $plugin_path, false, null, 0, 8192 );

// EDD Software Licensing
if ( stripos( $content, 'EDD_SL_Plugin_Updater' ) !== false ) {
return 'edd';
}
// EDD Software Licensing
if ( stripos( $content, 'EDD_SL_Plugin_Updater' ) !== false ) {
return 'edd';
}

// WooCommerce API Manager
if ( stripos( $content, 'WC_AM_Client' ) !== false ) {
return 'woocommerce';
}
// WooCommerce API Manager
if ( stripos( $content, 'WC_AM_Client' ) !== false ) {
return 'woocommerce';
}

// Envato
if ( stripos( $content, 'envato' ) !== false ) {
return 'envato';
}
// Envato
if ( stripos( $content, 'envato' ) !== false ) {
return 'envato';
}

// WPML
if ( stripos( $content, 'OTGS' ) !== false ) {
return 'otgs';
}
// WPML
if ( stripos( $content, 'OTGS' ) !== false ) {
return 'otgs';
}

return 'custom';
}
return 'custom';
}

/**
* 过滤更新
*
* @param object $transient 更新 transient
* @return object
*/
public function filter_updates( $transient ) {
if ( empty( $transient->response ) ) {
return $transient;
}
/**
* 过滤更新
*
* @param object $transient 更新 transient
* @return object
*/
public function filter_updates( $transient ) {
if ( empty( $transient->response ) ) {
return $transient;
}

foreach ( $transient->response as $file => $update ) {
$slug = dirname( $file );
foreach ( $transient->response as $file => $update ) {
$slug = dirname( $file );

// 检查版本锁定
if ( $this->is_version_locked( $slug ) ) {
$locked_version = $this->get_locked_version( $slug );
// 检查版本锁定
if ( $this->is_version_locked( $slug ) ) {
$locked_version = $this->get_locked_version( $slug );

if ( version_compare( $update->new_version, $locked_version, '>' ) ) {
Logger::info(
'版本锁定阻止更新',
array(
'slug' => $slug,
'locked_version' => $locked_version,
'new_version' => $update->new_version,
)
);
if ( version_compare( $update->new_version, $locked_version, '>' ) ) {
Logger::info( '版本锁定阻止更新', [
'slug' => $slug,
'locked_version' => $locked_version,
'new_version' => $update->new_version,
] );

unset( $transient->response[ $file ] );
}
}
}
unset( $transient->response[ $file ] );
}
}
}

return $transient;
}
return $transient;
}

/**
* 锁定版本
*
* @param string $slug 插件 slug
* @param string $version 版本号
* @return bool
*/
public function lock_version( string $slug, string $version ): bool {
// 权限检查
if ( ! current_user_can( 'update_plugins' ) ) {
Logger::warning(
'无权限锁定版本',
array(
'slug' => $slug,
'user' => get_current_user_id(),
)
);
return false;
}
/**
* 锁定版本
*
* @param string $slug 插件 slug
* @param string $version 版本号
* @return bool
*/
public function lock_version( string $slug, string $version ): bool {
// 权限检查
if ( ! current_user_can( 'update_plugins' ) ) {
Logger::warning( '无权限锁定版本', [ 'slug' => $slug, 'user' => get_current_user_id() ] );
return false;
}

// 验证版本号格式
if ( ! Validator::is_valid_version( $version ) ) {
return false;
}
// 验证版本号格式
if ( ! Validator::is_valid_version( $version ) ) {
return false;
}

$this->version_locks[ $slug ] = array(
'version' => sanitize_text_field( $version ),
'locked_at' => current_time( 'mysql' ),
'locked_by' => get_current_user_id(),
);
$this->version_locks[ $slug ] = [
'version' => sanitize_text_field( $version ),
'locked_at' => current_time( 'mysql' ),
'locked_by' => get_current_user_id(),
];

$result = $this->settings->set( 'version_locks', $this->version_locks );
$result = $this->settings->set( 'version_locks', $this->version_locks );

if ( $result ) {
Logger::info(
'版本已锁定',
array(
'slug' => $slug,
'version' => $version,
)
);
}
if ( $result ) {
Logger::info( '版本已锁定', [ 'slug' => $slug, 'version' => $version ] );
}

return $result;
}
return $result;
}

/**
* 解锁版本
*
* @param string $slug 插件 slug
* @return bool
*/
public function unlock_version( string $slug ): bool {
// 权限检查
if ( ! current_user_can( 'update_plugins' ) ) {
Logger::warning(
'无权限解锁版本',
array(
'slug' => $slug,
'user' => get_current_user_id(),
)
);
return false;
}
/**
* 解锁版本
*
* @param string $slug 插件 slug
* @return bool
*/
public function unlock_version( string $slug ): bool {
// 权限检查
if ( ! current_user_can( 'update_plugins' ) ) {
Logger::warning( '无权限解锁版本', [ 'slug' => $slug, 'user' => get_current_user_id() ] );
return false;
}

if ( ! isset( $this->version_locks[ $slug ] ) ) {
return false;
}
if ( ! isset( $this->version_locks[ $slug ] ) ) {
return false;
}

unset( $this->version_locks[ $slug ] );
unset( $this->version_locks[ $slug ] );

$result = $this->settings->set( 'version_locks', $this->version_locks );
$result = $this->settings->set( 'version_locks', $this->version_locks );

if ( $result ) {
Logger::info( '版本已解锁', array( 'slug' => $slug ) );
}
if ( $result ) {
Logger::info( '版本已解锁', [ 'slug' => $slug ] );
}

return $result;
}
return $result;
}

/**
* 检查版本是否锁定
*
* @param string $slug 插件 slug
* @return bool
*/
public function is_version_locked( string $slug ): bool {
return isset( $this->version_locks[ $slug ] );
}
/**
* 检查版本是否锁定
*
* @param string $slug 插件 slug
* @return bool
*/
public function is_version_locked( string $slug ): bool {
return isset( $this->version_locks[ $slug ] );
}

/**
* 获取锁定的版本
*
* @param string $slug 插件 slug
* @return string|null
*/
public function get_locked_version( string $slug ): ?string {
return $this->version_locks[ $slug ]['version'] ?? null;
}
/**
* 获取锁定的版本
*
* @param string $slug 插件 slug
* @return string|null
*/
public function get_locked_version( string $slug ): ?string {
return $this->version_locks[ $slug ]['version'] ?? null;
}

/**
* 获取所有版本锁定
*
* @return array
*/
public function get_version_locks(): array {
return $this->version_locks;
}
/**
* 获取所有版本锁定
*
* @return array
*/
public function get_version_locks(): array {
return $this->version_locks;
}

/**
* 更新完成时触发
*
* @param \WP_Upgrader $upgrader 升级器
* @param array $options 选项
*/
public function on_upgrade_complete( $upgrader, array $options ): void {
if ( $options['type'] !== 'plugin' || $options['action'] !== 'update' ) {
return;
}
/**
* 更新完成时触发
*
* @param \WP_Upgrader $upgrader 升级器
* @param array $options 选项
*/
public function on_upgrade_complete( $upgrader, array $options ): void {
if ( $options['type'] !== 'plugin' || $options['action'] !== 'update' ) {
return;
}

$plugins = $options['plugins'] ?? array();
$plugins = $options['plugins'] ?? [];

foreach ( $plugins as $file ) {
$slug = dirname( $file );
foreach ( $plugins as $file ) {
$slug = dirname( $file );

// 触发更新完成事件
do_action( 'wpbridge_plugin_updated', $slug, $file );
// 触发更新完成事件
do_action( 'wpbridge_plugin_updated', $slug, $file );

Logger::info( '插件更新完成', array( 'slug' => $slug ) );
}
}
Logger::info( '插件更新完成', [ 'slug' => $slug ] );
}
}

/**
* 获取已注册的商业插件
*
* @return array
*/
public function get_registered_plugins(): array {
return $this->registered_plugins;
}
/**
* 获取已注册的商业插件
*
* @return array
*/
public function get_registered_plugins(): array {
return $this->registered_plugins;
}

/**
* 获取统计信息
*
* @return array
*/
public function get_stats(): array {
$detected = $this->detect_commercial_plugins();
/**
* 获取统计信息
*
* @return array
*/
public function get_stats(): array {
$detected = $this->detect_commercial_plugins();

return array(
'detected_count' => count( $detected ),
'registered_count' => count( $this->registered_plugins ),
'locked_count' => count( $this->version_locks ),
);
}
return [
'detected_count' => count( $detected ),
'registered_count' => count( $this->registered_plugins ),
'locked_count' => count( $this->version_locks ),
];
}
}

View file

@ -27,7 +27,7 @@ class GPLValidator {
/**
* GPL 兼容的授权标识
*/
private const GPL_COMPATIBLE_LICENSES = array(
private const GPL_COMPATIBLE_LICENSES = [
'gpl',
'gpl-2.0',
'gpl-2.0+',
@ -45,12 +45,12 @@ class GPLValidator {
'mit',
'apache-2.0',
'bsd',
);
];

/**
* 已知的 GPL 商业插件列表
*/
private const KNOWN_GPL_PLUGINS = array(
private const KNOWN_GPL_PLUGINS = [
'elementor-pro',
'wordpress-seo-premium',
'advanced-custom-fields-pro',
@ -68,21 +68,21 @@ class GPLValidator {
'learndash',
'woocommerce-subscriptions',
'woocommerce-memberships',
);
];

/**
* 已知的非 GPL 插件列表(不应桥接)
*/
private const NON_GPL_PLUGINS = array(
private const NON_GPL_PLUGINS = [
// Envato 独占插件通常不是 GPL
);
];

/**
* 验证结果缓存
*
* @var array
*/
private array $cache = array();
private array $cache = [];

/**
* 验证插件是否 GPL 兼容
@ -115,32 +115,32 @@ class GPLValidator {
private function do_validate( string $plugin_slug, string $plugin_file ): array {
// 1. 检查已知列表
if ( in_array( $plugin_slug, self::KNOWN_GPL_PLUGINS, true ) ) {
return array(
return [
'is_gpl' => true,
'confidence' => 100,
'source' => 'known_list',
'license' => 'GPL-2.0+',
);
];
}

if ( in_array( $plugin_slug, self::NON_GPL_PLUGINS, true ) ) {
return array(
return [
'is_gpl' => false,
'confidence' => 100,
'source' => 'known_list',
'license' => 'proprietary',
);
];
}

// 2. 检查 WordPress.org如果存在则一定是 GPL
$wporg_result = $this->check_wordpress_org( $plugin_slug );
if ( $wporg_result !== null ) {
return array(
return [
'is_gpl' => true,
'confidence' => 100,
'source' => 'wordpress_org',
'license' => $wporg_result['license'] ?? 'GPL-2.0+',
);
];
}

// 3. 检查插件文件
@ -152,12 +152,12 @@ class GPLValidator {
}

// 4. 无法确定
return array(
return [
'is_gpl' => null,
'confidence' => 0,
'source' => 'unknown',
'license' => 'unknown',
);
];
}

/**
@ -175,7 +175,7 @@ class GPLValidator {
}

$url = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=' . urlencode( $plugin_slug );
$response = wp_remote_get( $url, array( 'timeout' => 5 ) );
$response = wp_remote_get( $url, [ 'timeout' => 5 ] );

if ( is_wp_error( $response ) ) {
return null;
@ -187,10 +187,10 @@ class GPLValidator {
if ( $code === 200 && ! empty( $body ) ) {
$data = json_decode( $body, true );
if ( isset( $data['slug'] ) ) {
$result = array(
$result = [
'license' => 'GPL-2.0+', // WordPress.org 要求 GPL
'name' => $data['name'] ?? '',
);
];
set_transient( $cache_key, $result, DAY_IN_SECONDS );
return $result;
}
@ -224,28 +224,28 @@ class GPLValidator {
if ( ! empty( $license ) ) {
$is_gpl = $this->is_gpl_compatible_license( $license );
if ( $is_gpl !== null ) {
return array(
return [
'is_gpl' => $is_gpl,
'confidence' => 90,
'source' => 'plugin_header',
'license' => $license,
);
];
}
}

// 检查 license.txt
$plugin_dir = dirname( $plugin_path );
$plugin_dir = dirname( $plugin_path );
$license_file = $plugin_dir . '/license.txt';

if ( file_exists( $license_file ) ) {
$license_content = file_get_contents( $license_file );
if ( $this->contains_gpl_text( $license_content ) ) {
return array(
return [
'is_gpl' => true,
'confidence' => 85,
'source' => 'license_file',
'license' => 'GPL (from license.txt)',
);
];
}
}

@ -257,12 +257,12 @@ class GPLValidator {
$license = trim( $matches[1] );
$is_gpl = $this->is_gpl_compatible_license( $license );
if ( $is_gpl !== null ) {
return array(
return [
'is_gpl' => $is_gpl,
'confidence' => 80,
'source' => 'readme_file',
'license' => $license,
);
];
}
}
}
@ -286,7 +286,7 @@ class GPLValidator {
}

// 检查明确的非 GPL 标识
$non_gpl_indicators = array( 'proprietary', 'commercial', 'all rights reserved', 'envato' );
$non_gpl_indicators = [ 'proprietary', 'commercial', 'all rights reserved', 'envato' ];
foreach ( $non_gpl_indicators as $indicator ) {
if ( strpos( $license_lower, $indicator ) !== false ) {
return false;
@ -303,7 +303,7 @@ class GPLValidator {
* @return bool
*/
private function contains_gpl_text( string $content ): bool {
$gpl_indicators = array(
$gpl_indicators = [
'GNU General Public License',
'GPL version 2',
'GPL version 3',
@ -311,7 +311,7 @@ class GPLValidator {
'GPLv3',
'free software',
'redistribute it and/or modify',
);
];

foreach ( $gpl_indicators as $indicator ) {
if ( stripos( $content, $indicator ) !== false ) {
@ -329,7 +329,7 @@ class GPLValidator {
* @return array
*/
public function validate_batch( array $plugins ): array {
$results = array();
$results = [];
foreach ( $plugins as $slug => $file ) {
$results[ $slug ] = $this->validate( $slug, $file );
}
@ -340,7 +340,7 @@ class GPLValidator {
* 清除缓存
*/
public function clear_cache(): void {
$this->cache = array();
$this->cache = [];
}

/**
@ -349,11 +349,11 @@ class GPLValidator {
* @param string $plugin_slug 插件 slug
*/
public function add_known_gpl( string $plugin_slug ): void {
$this->cache[ $plugin_slug ] = array(
$this->cache[ $plugin_slug ] = [
'is_gpl' => true,
'confidence' => 100,
'source' => 'manual',
'license' => 'GPL (manually verified)',
);
];
}
}

View file

@ -28,40 +28,40 @@ class LicenseProxy {
/**
* 支持的授权系统配置
*/
private const VENDORS = array(
'edd' => array(
private const VENDORS = [
'edd' => [
'name' => 'EDD Software Licensing',
'patterns' => array(
'patterns' => [
'/edd-sl/',
'/edd-api/',
'action=activate_license',
'action=check_license',
'action=deactivate_license',
),
],
'response_format' => 'edd',
),
'freemius' => array(
],
'freemius' => [
'name' => 'Freemius',
'patterns' => array(
'patterns' => [
'api.freemius.com',
'wp-json/freemius',
),
],
'response_format' => 'freemius',
),
'wc_am' => array(
],
'wc_am' => [
'name' => 'WooCommerce API Manager',
'patterns' => array(
'patterns' => [
'wc-api/wc-am-api',
'wc-api/am-software-api',
),
],
'response_format' => 'wc_am',
),
);
],
];

/**
* 敏感参数列表(用于日志过滤)
*/
private const SENSITIVE_PARAMS = array(
private const SENSITIVE_PARAMS = [
'license_key',
'license',
'key',
@ -70,7 +70,7 @@ class LicenseProxy {
'token',
'api_key',
'apikey',
);
];

/**
* 设置实例
@ -84,7 +84,7 @@ class LicenseProxy {
*
* @var array
*/
private array $bridged_plugins = array();
private array $bridged_plugins = [];

/**
* 构造函数
@ -93,7 +93,7 @@ class LicenseProxy {
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->bridged_plugins = $this->settings->get( 'bridged_plugins', array() );
$this->bridged_plugins = $this->settings->get( 'bridged_plugins', [] );
}

/**
@ -103,7 +103,7 @@ class LicenseProxy {
if ( ! $this->is_enabled() ) {
return;
}
add_filter( 'pre_http_request', array( $this, 'intercept_request' ), 5, 3 );
add_filter( 'pre_http_request', [ $this, 'intercept_request' ], 5, 3 );
}

/**
@ -142,14 +142,11 @@ class LicenseProxy {
}

// H2 修复: 过滤敏感信息后再记录日志
Logger::debug(
'License proxy intercepting',
array(
'vendor' => $vendor,
'plugin' => $plugin_slug,
'url' => $this->sanitize_url_for_log( $url ),
)
);
Logger::debug( 'License proxy intercepting', [
'vendor' => $vendor,
'plugin' => $plugin_slug,
'url' => $this->sanitize_url_for_log( $url ),
] );

// 4. 代理到文派服务
return $this->proxy_request( $vendor, $plugin_slug, $url, $args );
@ -173,7 +170,7 @@ class LicenseProxy {
* @return array 过滤后的请求体
*/
private function sanitize_body_for_log( array $body ): array {
$sanitized = array();
$sanitized = [];
foreach ( $body as $key => $value ) {
if ( in_array( strtolower( $key ), self::SENSITIVE_PARAMS, true ) ) {
$sanitized[ $key ] = '[REDACTED]';
@ -250,7 +247,7 @@ class LicenseProxy {
*/
private function resolve_item_id( string $item_id ): ?string {
// 从远程配置获取 ID 到 slug 的映射
$mapping = $this->settings->get( 'item_id_mapping', array() );
$mapping = $this->settings->get( 'item_id_mapping', [] );
return $mapping[ $item_id ] ?? null;
}

@ -261,7 +258,7 @@ class LicenseProxy {
* @return string|null
*/
private function resolve_freemius_id( string $freemius_id ): ?string {
$mapping = $this->settings->get( 'freemius_id_mapping', array() );
$mapping = $this->settings->get( 'freemius_id_mapping', [] );
return $mapping[ $freemius_id ] ?? null;
}

@ -283,13 +280,13 @@ class LicenseProxy {
* @return string
*/
private function generate_site_fingerprint(): string {
$factors = array(
$factors = [
home_url(),
defined( 'DB_NAME' ) ? DB_NAME : '',
defined( 'AUTH_KEY' ) ? AUTH_KEY : '',
defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '',
php_uname( 'n' ), // 主机名
);
];

return hash( 'sha256', implode( '|', $factors ) );
}
@ -304,15 +301,12 @@ class LicenseProxy {
* @return string
*/
private function generate_request_signature( string $api_key, string $plugin_slug, string $action, string $timestamp ): string {
$data = implode(
'|',
array(
$plugin_slug,
$this->generate_site_fingerprint(),
$action,
$timestamp,
)
);
$data = implode( '|', [
$plugin_slug,
$this->generate_site_fingerprint(),
$action,
$timestamp,
] );

return hash_hmac( 'sha256', $data, $api_key );
}
@ -340,36 +334,28 @@ class LicenseProxy {
$site_fingerprint = $this->generate_site_fingerprint();
$signature = $this->generate_request_signature( $api_key, $plugin_slug, $action, $timestamp );

$response = wp_remote_post(
$proxy_url,
array(
'timeout' => 15,
'headers' => array(
'Content-Type' => 'application/json',
'X-WPBridge-Key' => $api_key,
'X-WPBridge-Signature' => $signature,
'X-WPBridge-Timestamp' => $timestamp,
'X-WPBridge-Fingerprint' => $site_fingerprint,
),
'body' => wp_json_encode(
array(
'vendor' => $vendor,
'plugin_slug' => $plugin_slug,
'action' => $action,
'site_url' => home_url(),
'site_fingerprint' => $site_fingerprint,
)
),
)
);
$response = wp_remote_post( $proxy_url, [
'timeout' => 15,
'headers' => [
'Content-Type' => 'application/json',
'X-WPBridge-Key' => $api_key,
'X-WPBridge-Signature' => $signature,
'X-WPBridge-Timestamp' => $timestamp,
'X-WPBridge-Fingerprint' => $site_fingerprint,
],
'body' => wp_json_encode( [
'vendor' => $vendor,
'plugin_slug' => $plugin_slug,
'action' => $action,
'site_url' => home_url(),
'site_fingerprint' => $site_fingerprint,
] ),
] );

if ( is_wp_error( $response ) ) {
Logger::error(
'License proxy failed',
array(
'error' => $response->get_error_message(),
)
);
Logger::error( 'License proxy failed', [
'error' => $response->get_error_message(),
] );
// 失败时不拦截,让原始请求继续
return false;
}
@ -393,7 +379,7 @@ class LicenseProxy {
return false; // 让原始请求继续
}

$license = $body['license'] ?? array();
$license = $body['license'] ?? [];

// 根据不同授权系统返回不同格式
switch ( $vendor ) {
@ -419,35 +405,27 @@ class LicenseProxy {
// 获取插件特定的响应配置
$plugin_config = $this->get_plugin_response_config( $plugin_slug, 'edd' );

$body = wp_json_encode(
array_merge(
array(
'success' => true,
'license' => $license['status'] ?? 'valid',
'item_id' => $license['item_id'] ?? '',
'item_name' => $license['item_name'] ?? $plugin_config['item_name'] ?? '',
'license_limit' => $license['license_limit'] ?? 0,
'site_count' => $license['site_count'] ?? 1,
'expires' => $license['expires'] ?? 'lifetime',
'activations_left' => $license['activations_left'] ?? 'unlimited',
'checksum' => $license['checksum'] ?? $this->generate_checksum( $license ),
'payment_id' => $license['payment_id'] ?? 0,
'customer_name' => $license['customer_name'] ?? '',
'customer_email' => $license['customer_email'] ?? '',
'price_id' => $license['price_id'] ?? false,
),
$plugin_config['extra_fields'] ?? array()
)
);
$body = wp_json_encode( array_merge( [
'success' => true,
'license' => $license['status'] ?? 'valid',
'item_id' => $license['item_id'] ?? '',
'item_name' => $license['item_name'] ?? $plugin_config['item_name'] ?? '',
'license_limit' => $license['license_limit'] ?? 0,
'site_count' => $license['site_count'] ?? 1,
'expires' => $license['expires'] ?? 'lifetime',
'activations_left' => $license['activations_left'] ?? 'unlimited',
'checksum' => $license['checksum'] ?? $this->generate_checksum( $license ),
'payment_id' => $license['payment_id'] ?? 0,
'customer_name' => $license['customer_name'] ?? '',
'customer_email' => $license['customer_email'] ?? '',
'price_id' => $license['price_id'] ?? false,
], $plugin_config['extra_fields'] ?? [] ) );

return array(
'response' => array(
'code' => 200,
'message' => 'OK',
),
return [
'response' => [ 'code' => 200, 'message' => 'OK' ],
'body' => $body,
'headers' => array( 'content-type' => 'application/json' ),
);
'headers' => [ 'content-type' => 'application/json' ],
];
}

/**
@ -460,37 +438,29 @@ class LicenseProxy {
private function format_freemius_response( array $license, string $plugin_slug ): array {
$plugin_config = $this->get_plugin_response_config( $plugin_slug, 'freemius' );

$body = wp_json_encode(
array_merge(
array(
'id' => $license['id'] ?? 0,
'plugin_id' => $license['plugin_id'] ?? $plugin_config['plugin_id'] ?? 0,
'user_id' => $license['user_id'] ?? 0,
'plan_id' => $license['plan_id'] ?? $plugin_config['plan_id'] ?? 0,
'pricing_id' => $license['pricing_id'] ?? 0,
'quota' => $license['license_limit'] ?? null,
'activated' => $license['site_count'] ?? 1,
'activated_local' => 1,
'expiration' => $license['expires'] ?? null,
'secret_key' => $license['secret_key'] ?? $this->generate_secret_key(),
'public_key' => $license['public_key'] ?? $plugin_config['public_key'] ?? '',
'is_free_localhost' => false,
'is_block_features' => false,
'is_cancelled' => false,
'is_whitelabeled' => $license['is_whitelabeled'] ?? false,
),
$plugin_config['extra_fields'] ?? array()
)
);
$body = wp_json_encode( array_merge( [
'id' => $license['id'] ?? 0,
'plugin_id' => $license['plugin_id'] ?? $plugin_config['plugin_id'] ?? 0,
'user_id' => $license['user_id'] ?? 0,
'plan_id' => $license['plan_id'] ?? $plugin_config['plan_id'] ?? 0,
'pricing_id' => $license['pricing_id'] ?? 0,
'quota' => $license['license_limit'] ?? null,
'activated' => $license['site_count'] ?? 1,
'activated_local' => 1,
'expiration' => $license['expires'] ?? null,
'secret_key' => $license['secret_key'] ?? $this->generate_secret_key(),
'public_key' => $license['public_key'] ?? $plugin_config['public_key'] ?? '',
'is_free_localhost' => false,
'is_block_features' => false,
'is_cancelled' => false,
'is_whitelabeled' => $license['is_whitelabeled'] ?? false,
], $plugin_config['extra_fields'] ?? [] ) );

return array(
'response' => array(
'code' => 200,
'message' => 'OK',
),
return [
'response' => [ 'code' => 200, 'message' => 'OK' ],
'body' => $body,
'headers' => array( 'content-type' => 'application/json' ),
);
'headers' => [ 'content-type' => 'application/json' ],
];
}

/**
@ -500,25 +470,20 @@ class LicenseProxy {
* @return array
*/
private function format_wc_am_response( array $license ): array {
$body = wp_json_encode(
array(
'success' => true,
'status_check' => 'active',
'data' => 'active',
'activations' => (string) ( $license['site_count'] ?? 1 ),
'activations_limit' => (string) ( $license['license_limit'] ?? 'unlimited' ),
'activations_remaining' => (string) ( $license['activations_left'] ?? 'unlimited' ),
)
);
$body = wp_json_encode( [
'success' => true,
'status_check' => 'active',
'data' => 'active',
'activations' => (string) ( $license['site_count'] ?? 1 ),
'activations_limit' => (string) ( $license['license_limit'] ?? 'unlimited' ),
'activations_remaining'=> (string) ( $license['activations_left'] ?? 'unlimited' ),
] );

return array(
'response' => array(
'code' => 200,
'message' => 'OK',
),
return [
'response' => [ 'code' => 200, 'message' => 'OK' ],
'body' => $body,
'headers' => array( 'content-type' => 'application/json' ),
);
'headers' => [ 'content-type' => 'application/json' ],
];
}

/**
@ -528,22 +493,17 @@ class LicenseProxy {
* @return array
*/
private function format_generic_response( array $license ): array {
$body = wp_json_encode(
array(
'success' => true,
'license' => $license['status'] ?? 'valid',
'expires' => $license['expires'] ?? 'lifetime',
)
);
$body = wp_json_encode( [
'success' => true,
'license' => $license['status'] ?? 'valid',
'expires' => $license['expires'] ?? 'lifetime',
] );

return array(
'response' => array(
'code' => 200,
'message' => 'OK',
),
return [
'response' => [ 'code' => 200, 'message' => 'OK' ],
'body' => $body,
'headers' => array( 'content-type' => 'application/json' ),
);
'headers' => [ 'content-type' => 'application/json' ],
];
}

/**
@ -554,8 +514,8 @@ class LicenseProxy {
* @return array
*/
private function get_plugin_response_config( string $plugin_slug, string $vendor ): array {
$configs = $this->settings->get( 'plugin_response_configs', array() );
return $configs[ $plugin_slug ][ $vendor ] ?? array();
$configs = $this->settings->get( 'plugin_response_configs', [] );
return $configs[ $plugin_slug ][ $vendor ] ?? [];
}

/**

View file

@ -29,7 +29,7 @@ abstract class AbstractVendor implements VendorInterface {
*
* @var array
*/
protected array $config = array();
protected array $config = [];

/**
* 缓存前缀
@ -50,7 +50,7 @@ abstract class AbstractVendor implements VendorInterface {
*
* @param array $config 配置
*/
public function __construct( array $config = array() ) {
public function __construct( array $config = [] ) {
$this->config = array_merge( $this->get_default_config(), $config );
}

@ -60,13 +60,13 @@ abstract class AbstractVendor implements VendorInterface {
* @return array
*/
protected function get_default_config(): array {
return array(
return [
'api_url' => '',
'api_key' => '',
'api_secret' => '',
'timeout' => 15,
'enabled' => true,
);
];
}

/**
@ -94,13 +94,13 @@ abstract class AbstractVendor implements VendorInterface {
* @param string $method 方法 (GET/POST)
* @return array|null
*/
protected function api_request( string $endpoint, array $params = array(), string $method = 'GET' ): ?array {
protected function api_request( string $endpoint, array $params = [], string $method = 'GET' ): ?array {
$url = trailingslashit( $this->config['api_url'] ) . ltrim( $endpoint, '/' );

$args = array(
$args = [
'timeout' => $this->config['timeout'],
'headers' => $this->get_request_headers(),
);
];

if ( $method === 'GET' && ! empty( $params ) ) {
$url = add_query_arg( $params, $url );
@ -113,14 +113,11 @@ abstract class AbstractVendor implements VendorInterface {
: wp_remote_post( $url, $args );

if ( is_wp_error( $response ) ) {
Logger::error(
'Vendor API request failed',
array(
'vendor' => $this->get_id(),
'endpoint' => $endpoint,
'error' => $response->get_error_message(),
)
);
Logger::error( 'Vendor API request failed', [
'vendor' => $this->get_id(),
'endpoint' => $endpoint,
'error' => $response->get_error_message(),
] );
return null;
}

@ -128,27 +125,21 @@ abstract class AbstractVendor implements VendorInterface {
$body = wp_remote_retrieve_body( $response );

if ( $code !== 200 ) {
Logger::warning(
'Vendor API non-200 response',
array(
'vendor' => $this->get_id(),
'endpoint' => $endpoint,
'code' => $code,
)
);
Logger::warning( 'Vendor API non-200 response', [
'vendor' => $this->get_id(),
'endpoint' => $endpoint,
'code' => $code,
] );
return null;
}

$data = json_decode( $body, true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
Logger::error(
'Vendor API invalid JSON',
array(
'vendor' => $this->get_id(),
'endpoint' => $endpoint,
)
);
Logger::error( 'Vendor API invalid JSON', [
'vendor' => $this->get_id(),
'endpoint' => $endpoint,
] );
return null;
}

@ -161,10 +152,10 @@ abstract class AbstractVendor implements VendorInterface {
* @return array
*/
protected function get_request_headers(): array {
return array(
return [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
);
];
}

/**
@ -213,7 +204,7 @@ abstract class AbstractVendor implements VendorInterface {
public function search_plugins( string $keyword ): array {
$all_plugins = $this->get_plugins( 1, 1000 );
$keyword = strtolower( $keyword );
$results = array();
$results = [];

foreach ( $all_plugins['plugins'] as $plugin ) {
$name = strtolower( $plugin['name'] ?? '' );
@ -260,7 +251,7 @@ abstract class AbstractVendor implements VendorInterface {
* @return array
*/
protected function normalize_plugin( array $raw_plugin ): array {
return array(
return [
'slug' => $raw_plugin['slug'] ?? '',
'name' => $raw_plugin['name'] ?? $raw_plugin['title'] ?? '',
'version' => $raw_plugin['version'] ?? '',
@ -273,6 +264,6 @@ abstract class AbstractVendor implements VendorInterface {
'requires_php' => $raw_plugin['requires_php'] ?? '',
'last_updated' => $raw_plugin['last_updated'] ?? $raw_plugin['modified'] ?? '',
'vendor' => $this->get_id(),
);
];
}
}

View file

@ -37,7 +37,7 @@ class VendorManager {
*
* @var VendorInterface[]
*/
private array $vendors = array();
private array $vendors = [];

/**
* 单例实例
@ -73,7 +73,7 @@ class VendorManager {
* 加载已配置的供应商
*/
private function load_vendors(): void {
$vendor_configs = $this->settings->get( 'vendors', array() );
$vendor_configs = $this->settings->get( 'vendors', [] );

foreach ( $vendor_configs as $vendor_id => $config ) {
if ( empty( $config['enabled'] ) ) {
@ -111,13 +111,10 @@ class VendorManager {
// return new EDDVendor($vendor_id, $name, $config);

default:
Logger::warning(
'Unknown vendor type',
array(
'vendor_id' => $vendor_id,
'type' => $type,
)
);
Logger::warning( 'Unknown vendor type', [
'vendor_id' => $vendor_id,
'type' => $type,
] );
return null;
}
}
@ -164,11 +161,11 @@ class VendorManager {
* @return array
*/
public function get_vendors_info(): array {
$info = array();
$info = [];
foreach ( $this->vendors as $vendor ) {
$info[ $vendor->get_id() ] = array_merge(
$vendor->get_info(),
array( 'available' => $vendor->is_available() )
[ 'available' => $vendor->is_available() ]
);
}
return $info;
@ -182,10 +179,10 @@ class VendorManager {
* @return array
*/
public function search_plugins( string $keyword, string $vendor_id = '' ): array {
$results = array();
$results = [];

$vendors = ! empty( $vendor_id )
? array( $this->get_vendor( $vendor_id ) )
? [ $this->get_vendor( $vendor_id ) ]
: $this->get_vendors( true );

foreach ( $vendors as $vendor ) {
@ -201,13 +198,10 @@ class VendorManager {
$results[] = $plugin;
}
} catch ( \Exception $e ) {
Logger::error(
'Vendor search failed',
array(
'vendor' => $vendor->get_id(),
'error' => $e->getMessage(),
)
);
Logger::error( 'Vendor search failed', [
'vendor' => $vendor->get_id(),
'error' => $e->getMessage(),
] );
}
}

@ -286,7 +280,7 @@ class VendorManager {
* @return bool
*/
public function add_vendor_config( string $vendor_id, array $config ): bool {
$vendors = $this->settings->get( 'vendors', array() );
$vendors = $this->settings->get( 'vendors', [] );
$vendors[ $vendor_id ] = $config;

$result = $this->settings->set( 'vendors', $vendors );
@ -309,7 +303,7 @@ class VendorManager {
* @return bool
*/
public function remove_vendor_config( string $vendor_id ): bool {
$vendors = $this->settings->get( 'vendors', array() );
$vendors = $this->settings->get( 'vendors', [] );

if ( ! isset( $vendors[ $vendor_id ] ) ) {
return false;
@ -331,29 +325,29 @@ class VendorManager {
$vendor = $this->get_vendor( $vendor_id );

if ( $vendor === null ) {
return array(
return [
'success' => false,
'message' => __( '供应商不存在', 'wpbridge' ),
);
];
}

$available = $vendor->is_available();

if ( ! $available ) {
return array(
return [
'success' => false,
'message' => __( '供应商连接失败,请检查配置', 'wpbridge' ),
);
];
}

// 尝试获取插件列表
$plugins = $vendor->get_plugins( 1, 10 );

return array(
return [
'success' => true,
'message' => __( '连接成功', 'wpbridge' ),
'plugin_count' => $plugins['total'] ?? count( $plugins['plugins'] ),
);
];
}

/**
@ -362,19 +356,19 @@ class VendorManager {
* @return array
*/
public function get_stats(): array {
$total_vendors = count( $this->vendors );
$active_vendors = count( $this->get_vendors( true ) );
$total_plugins = 0;
$total_vendors = count( $this->vendors );
$active_vendors = count( $this->get_vendors( true ) );
$total_plugins = 0;

foreach ( $this->get_vendors( true ) as $vendor ) {
$plugins = $vendor->get_plugins( 1, 1 );
$plugins = $vendor->get_plugins( 1, 1 );
$total_plugins += $plugins['total'] ?? 0;
}

return array(
return [
'total_vendors' => $total_vendors,
'active_vendors' => $active_vendors,
'total_plugins' => $total_plugins,
);
];
}
}

View file

@ -51,7 +51,7 @@ class WooCommerceVendor extends AbstractVendor {
* @param string $vendor_name 供应商名称
* @param array $config 配置
*/
public function __construct( string $vendor_id, string $vendor_name, array $config = array() ) {
public function __construct( string $vendor_id, string $vendor_name, array $config = [] ) {
$this->vendor_id = $vendor_id;
$this->vendor_name = $vendor_name;
parent::__construct( $config );
@ -63,17 +63,14 @@ class WooCommerceVendor extends AbstractVendor {
* @return array
*/
protected function get_default_config(): array {
return array_merge(
parent::get_default_config(),
array(
'api_version' => 'v2', // API 版本
'product_id' => '', // 产品 ID某些商店需要
'instance' => '', // 实例标识
'use_rest_api' => true, // 是否使用 REST API
'products_endpoint' => '/wp-json/wc/v3/products', // 产品列表端点
'download_endpoint' => '/wp-json/wc-am/v2/download', // 下载端点
)
);
return array_merge( parent::get_default_config(), [
'api_version' => 'v2', // API 版本
'product_id' => '', // 产品 ID某些商店需要
'instance' => '', // 实例标识
'use_rest_api' => true, // 是否使用 REST API
'products_endpoint' => '/wp-json/wc/v3/products', // 产品列表端点
'download_endpoint' => '/wp-json/wc-am/v2/download', // 下载端点
] );
}

/**
@ -91,14 +88,14 @@ class WooCommerceVendor extends AbstractVendor {
* @return array
*/
public function get_info(): array {
return array(
return [
'id' => $this->vendor_id,
'name' => $this->vendor_name,
'url' => $this->config['api_url'],
'api_type' => 'wc_am',
'api_version' => $this->config['api_version'],
'requires_key' => true,
);
];
}

/**
@ -133,13 +130,10 @@ class WooCommerceVendor extends AbstractVendor {
}

// 尝试获取产品列表来验证
$response = $this->api_request(
$this->config['products_endpoint'],
array(
'per_page' => 1,
'status' => 'publish',
)
);
$response = $this->api_request( $this->config['products_endpoint'], [
'per_page' => 1,
'status' => 'publish',
] );

$valid = $response !== null;
$this->set_cache( $cache_key, $valid, 300 ); // 5分钟缓存
@ -162,26 +156,23 @@ class WooCommerceVendor extends AbstractVendor {
return $cached;
}

$response = $this->api_request(
$this->config['products_endpoint'],
array(
'page' => $page,
'per_page' => $limit,
'status' => 'publish',
'type' => 'simple', // 或 'variable' 取决于商店配置
'category' => $this->config['category'] ?? '', // 可选:按分类过滤
)
);
$response = $this->api_request( $this->config['products_endpoint'], [
'page' => $page,
'per_page' => $limit,
'status' => 'publish',
'type' => 'simple', // 或 'variable' 取决于商店配置
'category' => $this->config['category'] ?? '', // 可选:按分类过滤
] );

if ( $response === null ) {
return array(
'plugins' => array(),
return [
'plugins' => [],
'total' => 0,
'pages' => 0,
);
];
}

$plugins = array();
$plugins = [];
foreach ( $response as $product ) {
$plugin = $this->normalize_wc_product( $product );
if ( $plugin !== null ) {
@ -189,11 +180,11 @@ class WooCommerceVendor extends AbstractVendor {
}
}

$result = array(
$result = [
'plugins' => $plugins,
'total' => count( $plugins ), // WC API 返回 X-WP-Total header
'pages' => 1, // WC API 返回 X-WP-TotalPages header
);
];

$this->set_cache( $cache_key, $result );

@ -218,7 +209,7 @@ class WooCommerceVendor extends AbstractVendor {
return null;
}

return array(
return [
'slug' => $slug,
'name' => $product['name'] ?? '',
'version' => $this->extract_version( $product ),
@ -233,7 +224,7 @@ class WooCommerceVendor extends AbstractVendor {
'price' => $product['price'] ?? '0',
'product_id' => $product['id'] ?? 0,
'vendor' => $this->get_id(),
);
];
}

/**
@ -244,16 +235,16 @@ class WooCommerceVendor extends AbstractVendor {
*/
protected function is_plugin_product( array $product ): bool {
// 检查分类
$categories = $product['categories'] ?? array();
$categories = $product['categories'] ?? [];
foreach ( $categories as $cat ) {
$cat_slug = strtolower( $cat['slug'] ?? '' );
if ( in_array( $cat_slug, array( 'plugins', 'wordpress-plugins', 'wp-plugins' ), true ) ) {
if ( in_array( $cat_slug, [ 'plugins', 'wordpress-plugins', 'wp-plugins' ], true ) ) {
return true;
}
}

// 检查标签
$tags = $product['tags'] ?? array();
$tags = $product['tags'] ?? [];
foreach ( $tags as $tag ) {
$tag_slug = strtolower( $tag['slug'] ?? '' );
if ( strpos( $tag_slug, 'plugin' ) !== false ) {
@ -262,7 +253,7 @@ class WooCommerceVendor extends AbstractVendor {
}

// 检查是否有下载文件
$downloads = $product['downloads'] ?? array();
$downloads = $product['downloads'] ?? [];
foreach ( $downloads as $download ) {
$file = strtolower( $download['file'] ?? '' );
if ( strpos( $file, '.zip' ) !== false ) {
@ -323,7 +314,7 @@ class WooCommerceVendor extends AbstractVendor {
}

// 从下载文件名提取
$downloads = $product['downloads'] ?? array();
$downloads = $product['downloads'] ?? [];
foreach ( $downloads as $download ) {
$file = $download['file'] ?? '';
if ( preg_match( '/[\-_]v?(\d+\.\d+(?:\.\d+)?)/i', $file, $matches ) ) {
@ -358,7 +349,7 @@ class WooCommerceVendor extends AbstractVendor {
* @return string
*/
protected function extract_meta( array $product, string $meta_key ): string {
$meta_data = $product['meta_data'] ?? array();
$meta_data = $product['meta_data'] ?? [];
foreach ( $meta_data as $meta ) {
if ( ( $meta['key'] ?? '' ) === $meta_key ) {
return (string) ( $meta['value'] ?? '' );
@ -391,14 +382,14 @@ class WooCommerceVendor extends AbstractVendor {
return null; // 无更新
}

return array(
return [
'version' => $latest_version,
'download_url' => $this->get_download_url( $slug, $latest_version ),
'changelog' => $this->get_changelog( $slug ),
'tested' => $plugin['tested'] ?? '',
'requires' => $plugin['requires'] ?? '',
'requires_php' => $plugin['requires_php'] ?? '',
);
];
}

/**
@ -422,11 +413,11 @@ class WooCommerceVendor extends AbstractVendor {
}

// 构建 WC API Manager 下载链接
$params = array(
$params = [
'product_id' => $product_id,
'api_key' => $this->config['api_key'],
'instance' => $this->config['instance'] ?: $this->generate_instance_id(),
);
];

if ( ! empty( $version ) ) {
$params['version'] = $version;

View file

@ -9,7 +9,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,486 +17,486 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class BackupManager {

/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_backups';

/**
* 备份目录名
*/
const BACKUP_DIR = 'wpbridge-backups';

/**
* 最大保留备份数
*/
const MAX_BACKUPS = 5;

/**
* 单例实例
*
* @var BackupManager|null
*/
private static ?BackupManager $instance = null;

/**
* 备份记录缓存
*
* @var array|null
*/
private ?array $backups = null;

/**
* 获取单例实例
*
* @return BackupManager
*/
public static function get_instance(): BackupManager {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* 私有构造函数
*/
private function __construct() {
$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 在更新前创建备份
add_filter( 'upgrader_pre_install', array( $this, 'pre_install_backup' ), 10, 2 );
}

/**
* 获取备份目录路径
*
* @return string
*/
public function get_backup_dir(): string {
$upload_dir = wp_upload_dir();
return trailingslashit( $upload_dir['basedir'] ) . self::BACKUP_DIR;
}

/**
* 确保备份目录存在
*
* @return bool
*/
private function ensure_backup_dir(): bool {
$dir = $this->get_backup_dir();

if ( ! file_exists( $dir ) ) {
if ( ! wp_mkdir_p( $dir ) ) {
return false;
}

// 创建 .htaccess 防止直接访问
$htaccess = $dir . '/.htaccess';
if ( ! file_exists( $htaccess ) ) {
file_put_contents( $htaccess, "Deny from all\n" );
}

// 创建 index.php
$index = $dir . '/index.php';
if ( ! file_exists( $index ) ) {
file_put_contents( $index, "<?php\n// Silence is golden.\n" );
}
}

return true;
}

/**
* 获取所有备份记录
*
* @return array
*/
public function get_all(): array {
if ( null === $this->backups ) {
$this->backups = get_option( self::OPTION_NAME, array() );
if ( ! is_array( $this->backups ) ) {
$this->backups = array();
}
}
return $this->backups;
}

/**
* 获取项目的备份列表
*
* @param string $item_key 项目键
* @return array
*/
public function get_item_backups( string $item_key ): array {
$backups = $this->get_all();
return $backups[ $item_key ] ?? array();
}

/**
* 更新前创建备份
*
* @param bool|WP_Error $response 响应
* @param array $hook_extra 额外参数
* @return bool|WP_Error
*/
public function pre_install_backup( $response, $hook_extra ) {
// 检查是否启用了备份
$settings = new Settings();
if ( ! $settings->get( 'backup_enabled', true ) ) {
return $response;
}

// 确定项目类型和路径
if ( ! empty( $hook_extra['plugin'] ) ) {
$item_key = 'plugin:' . $hook_extra['plugin'];
$source_path = WP_PLUGIN_DIR . '/' . dirname( $hook_extra['plugin'] );

// 单文件插件
if ( dirname( $hook_extra['plugin'] ) === '.' ) {
$source_path = WP_PLUGIN_DIR . '/' . $hook_extra['plugin'];
}
} elseif ( ! empty( $hook_extra['theme'] ) ) {
$item_key = 'theme:' . $hook_extra['theme'];
$source_path = get_theme_root() . '/' . $hook_extra['theme'];
} else {
return $response;
}

// 创建备份
$this->create_backup( $item_key, $source_path );

return $response;
}

/**
* 创建备份
*
* @param string $item_key 项目键
* @param string $source_path 源路径
* @return array|false 备份信息或失败
*/
public function create_backup( string $item_key, string $source_path ) {
if ( ! file_exists( $source_path ) ) {
Logger::warning( "Backup failed: source not found - {$source_path}" );
return false;
}

if ( ! $this->ensure_backup_dir() ) {
Logger::error( 'Backup failed: cannot create backup directory' );
return false;
}

// 获取当前版本
$version = $this->get_item_version( $item_key, $source_path );

// 生成备份文件名
$backup_id = wp_generate_uuid4();
$backup_filename = sanitize_file_name( str_replace( ':', '-', $item_key ) ) . '-' . $version . '-' . gmdate( 'Ymd-His' ) . '.zip';
$backup_path = $this->get_backup_dir() . '/' . $backup_filename;

// 创建 ZIP 备份
if ( ! $this->create_zip( $source_path, $backup_path ) ) {
Logger::error( "Backup failed: cannot create zip - {$backup_path}" );
return false;
}

// 记录备份信息
$backup_info = array(
'id' => $backup_id,
'filename' => $backup_filename,
'version' => $version,
'size' => filesize( $backup_path ),
'created_at' => current_time( 'mysql' ),
);

$this->add_backup_record( $item_key, $backup_info );

// 清理旧备份
$this->cleanup_old_backups( $item_key );

Logger::info( "Backup created: {$item_key} v{$version}" );

return $backup_info;
}

/**
* 创建 ZIP 文件
*
* @param string $source_path 源路径
* @param string $zip_path ZIP 路径
* @return bool
*/
private function create_zip( string $source_path, string $zip_path ): bool {
if ( ! class_exists( 'ZipArchive' ) ) {
Logger::error( 'ZipArchive class not available' );
return false;
}

$zip = new \ZipArchive();

if ( $zip->open( $zip_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ) !== true ) {
return false;
}

if ( is_file( $source_path ) ) {
// 单文件
$zip->addFile( $source_path, basename( $source_path ) );
} else {
// 目录
$this->add_dir_to_zip( $zip, $source_path, basename( $source_path ) );
}

return $zip->close();
}

/**
* 递归添加目录到 ZIP
*
* @param \ZipArchive $zip ZIP 对象
* @param string $dir 目录路径
* @param string $zip_dir ZIP 内目录名
*/
private function add_dir_to_zip( \ZipArchive $zip, string $dir, string $zip_dir ): void {
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
\RecursiveIteratorIterator::SELF_FIRST
);

foreach ( $files as $file ) {
$file_path = $file->getRealPath();
$relative_path = $zip_dir . '/' . substr( $file_path, strlen( $dir ) + 1 );

if ( $file->isDir() ) {
$zip->addEmptyDir( $relative_path );
} else {
$zip->addFile( $file_path, $relative_path );
}
}
}

/**
* 获取项目版本
*
* @param string $item_key 项目键
* @param string $source_path 源路径
* @return string
*/
private function get_item_version( string $item_key, string $source_path ): string {
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
$plugin_file = substr( $item_key, 7 );
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$plugin_path = is_file( $source_path ) ? $source_path : $source_path . '/' . basename( $plugin_file );
if ( file_exists( $plugin_path ) ) {
$data = get_plugin_data( $plugin_path, false, false );
return $data['Version'] ?? '0.0.0';
}
} elseif ( strpos( $item_key, 'theme:' ) === 0 ) {
$theme_slug = substr( $item_key, 6 );
$theme = wp_get_theme( $theme_slug );
if ( $theme->exists() ) {
return $theme->get( 'Version' );
}
}

return '0.0.0';
}

/**
* 添加备份记录
*
* @param string $item_key 项目键
* @param array $backup_info 备份信息
*/
private function add_backup_record( string $item_key, array $backup_info ): void {
$backups = $this->get_all();

if ( ! isset( $backups[ $item_key ] ) ) {
$backups[ $item_key ] = array();
}

array_unshift( $backups[ $item_key ], $backup_info );

$this->backups = $backups;
update_option( self::OPTION_NAME, $backups );
}

/**
* 清理旧备份
*
* @param string $item_key 项目键
*/
private function cleanup_old_backups( string $item_key ): void {
$backups = $this->get_all();

if ( ! isset( $backups[ $item_key ] ) ) {
return;
}

$item_backups = $backups[ $item_key ];

while ( count( $item_backups ) > self::MAX_BACKUPS ) {
$old_backup = array_pop( $item_backups );

// 删除文件
$file_path = $this->get_backup_dir() . '/' . $old_backup['filename'];
if ( file_exists( $file_path ) ) {
unlink( $file_path );
}
}

$backups[ $item_key ] = $item_backups;
$this->backups = $backups;
update_option( self::OPTION_NAME, $backups );
}

/**
* 回滚到指定备份
*
* @param string $item_key 项目键
* @param string $backup_id 备份 ID
* @return bool|WP_Error
*/
public function rollback( string $item_key, string $backup_id ) {
$item_backups = $this->get_item_backups( $item_key );

// 查找备份
$backup = null;
foreach ( $item_backups as $b ) {
if ( $b['id'] === $backup_id ) {
$backup = $b;
break;
}
}

if ( ! $backup ) {
return new \WP_Error( 'backup_not_found', __( '备份不存在', 'wpbridge' ) );
}

$backup_path = $this->get_backup_dir() . '/' . $backup['filename'];

if ( ! file_exists( $backup_path ) ) {
return new \WP_Error( 'backup_file_missing', __( '备份文件不存在', 'wpbridge' ) );
}

// 确定目标路径
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
$plugin_file = substr( $item_key, 7 );
$target_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file );

if ( dirname( $plugin_file ) === '.' ) {
// 单文件插件
$target_dir = WP_PLUGIN_DIR;
}
} elseif ( strpos( $item_key, 'theme:' ) === 0 ) {
$theme_slug = substr( $item_key, 6 );
$target_dir = get_theme_root() . '/' . $theme_slug;
} else {
return new \WP_Error( 'invalid_item', __( '无效的项目', 'wpbridge' ) );
}

// 解压备份
$result = $this->extract_zip( $backup_path, dirname( $target_dir ) );

if ( is_wp_error( $result ) ) {
return $result;
}

Logger::info( "Rollback completed: {$item_key} to v{$backup['version']}" );

return true;
}

/**
* 解压 ZIP 文件
*
* @param string $zip_path ZIP 路径
* @param string $target_dir 目标目录
* @return bool|WP_Error
*/
private function extract_zip( string $zip_path, string $target_dir ) {
if ( ! class_exists( 'ZipArchive' ) ) {
return new \WP_Error( 'no_zip', __( 'ZipArchive 不可用', 'wpbridge' ) );
}

$zip = new \ZipArchive();

if ( $zip->open( $zip_path ) !== true ) {
return new \WP_Error( 'zip_open_failed', __( '无法打开备份文件', 'wpbridge' ) );
}

$zip->extractTo( $target_dir );
$zip->close();

return true;
}

/**
* 删除备份
*
* @param string $item_key 项目键
* @param string $backup_id 备份 ID
* @return bool
*/
public function delete_backup( string $item_key, string $backup_id ): bool {
$backups = $this->get_all();

if ( ! isset( $backups[ $item_key ] ) ) {
return false;
}

foreach ( $backups[ $item_key ] as $index => $backup ) {
if ( $backup['id'] === $backup_id ) {
// 删除文件
$file_path = $this->get_backup_dir() . '/' . $backup['filename'];
if ( file_exists( $file_path ) ) {
unlink( $file_path );
}

// 删除记录
unset( $backups[ $item_key ][ $index ] );
$backups[ $item_key ] = array_values( $backups[ $item_key ] );

$this->backups = $backups;
update_option( self::OPTION_NAME, $backups );

return true;
}
}

return false;
}

/**
* 获取备份总大小
*
* @return int 字节数
*/
public function get_total_size(): int {
$total = 0;
$backups = $this->get_all();

foreach ( $backups as $item_backups ) {
foreach ( $item_backups as $backup ) {
$total += $backup['size'] ?? 0;
}
}

return $total;
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->backups = null;
}
/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_backups';

/**
* 备份目录名
*/
const BACKUP_DIR = 'wpbridge-backups';

/**
* 最大保留备份数
*/
const MAX_BACKUPS = 5;

/**
* 单例实例
*
* @var BackupManager|null
*/
private static ?BackupManager $instance = null;

/**
* 备份记录缓存
*
* @var array|null
*/
private ?array $backups = null;

/**
* 获取单例实例
*
* @return BackupManager
*/
public static function get_instance(): BackupManager {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* 私有构造函数
*/
private function __construct() {
$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 在更新前创建备份
add_filter( 'upgrader_pre_install', [ $this, 'pre_install_backup' ], 10, 2 );
}

/**
* 获取备份目录路径
*
* @return string
*/
public function get_backup_dir(): string {
$upload_dir = wp_upload_dir();
return trailingslashit( $upload_dir['basedir'] ) . self::BACKUP_DIR;
}

/**
* 确保备份目录存在
*
* @return bool
*/
private function ensure_backup_dir(): bool {
$dir = $this->get_backup_dir();

if ( ! file_exists( $dir ) ) {
if ( ! wp_mkdir_p( $dir ) ) {
return false;
}

// 创建 .htaccess 防止直接访问
$htaccess = $dir . '/.htaccess';
if ( ! file_exists( $htaccess ) ) {
file_put_contents( $htaccess, "Deny from all\n" );
}

// 创建 index.php
$index = $dir . '/index.php';
if ( ! file_exists( $index ) ) {
file_put_contents( $index, "<?php\n// Silence is golden.\n" );
}
}

return true;
}

/**
* 获取所有备份记录
*
* @return array
*/
public function get_all(): array {
if ( null === $this->backups ) {
$this->backups = get_option( self::OPTION_NAME, [] );
if ( ! is_array( $this->backups ) ) {
$this->backups = [];
}
}
return $this->backups;
}

/**
* 获取项目的备份列表
*
* @param string $item_key 项目键
* @return array
*/
public function get_item_backups( string $item_key ): array {
$backups = $this->get_all();
return $backups[ $item_key ] ?? [];
}

/**
* 更新前创建备份
*
* @param bool|WP_Error $response 响应
* @param array $hook_extra 额外参数
* @return bool|WP_Error
*/
public function pre_install_backup( $response, $hook_extra ) {
// 检查是否启用了备份
$settings = new Settings();
if ( ! $settings->get( 'backup_enabled', true ) ) {
return $response;
}

// 确定项目类型和路径
if ( ! empty( $hook_extra['plugin'] ) ) {
$item_key = 'plugin:' . $hook_extra['plugin'];
$source_path = WP_PLUGIN_DIR . '/' . dirname( $hook_extra['plugin'] );

// 单文件插件
if ( dirname( $hook_extra['plugin'] ) === '.' ) {
$source_path = WP_PLUGIN_DIR . '/' . $hook_extra['plugin'];
}
} elseif ( ! empty( $hook_extra['theme'] ) ) {
$item_key = 'theme:' . $hook_extra['theme'];
$source_path = get_theme_root() . '/' . $hook_extra['theme'];
} else {
return $response;
}

// 创建备份
$this->create_backup( $item_key, $source_path );

return $response;
}

/**
* 创建备份
*
* @param string $item_key 项目键
* @param string $source_path 源路径
* @return array|false 备份信息或失败
*/
public function create_backup( string $item_key, string $source_path ) {
if ( ! file_exists( $source_path ) ) {
Logger::warning( "Backup failed: source not found - {$source_path}" );
return false;
}

if ( ! $this->ensure_backup_dir() ) {
Logger::error( 'Backup failed: cannot create backup directory' );
return false;
}

// 获取当前版本
$version = $this->get_item_version( $item_key, $source_path );

// 生成备份文件名
$backup_id = wp_generate_uuid4();
$backup_filename = sanitize_file_name( str_replace( ':', '-', $item_key ) ) . '-' . $version . '-' . gmdate( 'Ymd-His' ) . '.zip';
$backup_path = $this->get_backup_dir() . '/' . $backup_filename;

// 创建 ZIP 备份
if ( ! $this->create_zip( $source_path, $backup_path ) ) {
Logger::error( "Backup failed: cannot create zip - {$backup_path}" );
return false;
}

// 记录备份信息
$backup_info = [
'id' => $backup_id,
'filename' => $backup_filename,
'version' => $version,
'size' => filesize( $backup_path ),
'created_at' => current_time( 'mysql' ),
];

$this->add_backup_record( $item_key, $backup_info );

// 清理旧备份
$this->cleanup_old_backups( $item_key );

Logger::info( "Backup created: {$item_key} v{$version}" );

return $backup_info;
}

/**
* 创建 ZIP 文件
*
* @param string $source_path 源路径
* @param string $zip_path ZIP 路径
* @return bool
*/
private function create_zip( string $source_path, string $zip_path ): bool {
if ( ! class_exists( 'ZipArchive' ) ) {
Logger::error( 'ZipArchive class not available' );
return false;
}

$zip = new \ZipArchive();

if ( $zip->open( $zip_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ) !== true ) {
return false;
}

if ( is_file( $source_path ) ) {
// 单文件
$zip->addFile( $source_path, basename( $source_path ) );
} else {
// 目录
$this->add_dir_to_zip( $zip, $source_path, basename( $source_path ) );
}

return $zip->close();
}

/**
* 递归添加目录到 ZIP
*
* @param \ZipArchive $zip ZIP 对象
* @param string $dir 目录路径
* @param string $zip_dir ZIP 内目录名
*/
private function add_dir_to_zip( \ZipArchive $zip, string $dir, string $zip_dir ): void {
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
\RecursiveIteratorIterator::SELF_FIRST
);

foreach ( $files as $file ) {
$file_path = $file->getRealPath();
$relative_path = $zip_dir . '/' . substr( $file_path, strlen( $dir ) + 1 );

if ( $file->isDir() ) {
$zip->addEmptyDir( $relative_path );
} else {
$zip->addFile( $file_path, $relative_path );
}
}
}

/**
* 获取项目版本
*
* @param string $item_key 项目键
* @param string $source_path 源路径
* @return string
*/
private function get_item_version( string $item_key, string $source_path ): string {
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
$plugin_file = substr( $item_key, 7 );
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$plugin_path = is_file( $source_path ) ? $source_path : $source_path . '/' . basename( $plugin_file );
if ( file_exists( $plugin_path ) ) {
$data = get_plugin_data( $plugin_path, false, false );
return $data['Version'] ?? '0.0.0';
}
} elseif ( strpos( $item_key, 'theme:' ) === 0 ) {
$theme_slug = substr( $item_key, 6 );
$theme = wp_get_theme( $theme_slug );
if ( $theme->exists() ) {
return $theme->get( 'Version' );
}
}

return '0.0.0';
}

/**
* 添加备份记录
*
* @param string $item_key 项目键
* @param array $backup_info 备份信息
*/
private function add_backup_record( string $item_key, array $backup_info ): void {
$backups = $this->get_all();

if ( ! isset( $backups[ $item_key ] ) ) {
$backups[ $item_key ] = [];
}

array_unshift( $backups[ $item_key ], $backup_info );

$this->backups = $backups;
update_option( self::OPTION_NAME, $backups );
}

/**
* 清理旧备份
*
* @param string $item_key 项目键
*/
private function cleanup_old_backups( string $item_key ): void {
$backups = $this->get_all();

if ( ! isset( $backups[ $item_key ] ) ) {
return;
}

$item_backups = $backups[ $item_key ];

while ( count( $item_backups ) > self::MAX_BACKUPS ) {
$old_backup = array_pop( $item_backups );

// 删除文件
$file_path = $this->get_backup_dir() . '/' . $old_backup['filename'];
if ( file_exists( $file_path ) ) {
unlink( $file_path );
}
}

$backups[ $item_key ] = $item_backups;
$this->backups = $backups;
update_option( self::OPTION_NAME, $backups );
}

/**
* 回滚到指定备份
*
* @param string $item_key 项目键
* @param string $backup_id 备份 ID
* @return bool|WP_Error
*/
public function rollback( string $item_key, string $backup_id ) {
$item_backups = $this->get_item_backups( $item_key );

// 查找备份
$backup = null;
foreach ( $item_backups as $b ) {
if ( $b['id'] === $backup_id ) {
$backup = $b;
break;
}
}

if ( ! $backup ) {
return new \WP_Error( 'backup_not_found', __( '备份不存在', 'wpbridge' ) );
}

$backup_path = $this->get_backup_dir() . '/' . $backup['filename'];

if ( ! file_exists( $backup_path ) ) {
return new \WP_Error( 'backup_file_missing', __( '备份文件不存在', 'wpbridge' ) );
}

// 确定目标路径
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
$plugin_file = substr( $item_key, 7 );
$target_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file );

if ( dirname( $plugin_file ) === '.' ) {
// 单文件插件
$target_dir = WP_PLUGIN_DIR;
}
} elseif ( strpos( $item_key, 'theme:' ) === 0 ) {
$theme_slug = substr( $item_key, 6 );
$target_dir = get_theme_root() . '/' . $theme_slug;
} else {
return new \WP_Error( 'invalid_item', __( '无效的项目', 'wpbridge' ) );
}

// 解压备份
$result = $this->extract_zip( $backup_path, dirname( $target_dir ) );

if ( is_wp_error( $result ) ) {
return $result;
}

Logger::info( "Rollback completed: {$item_key} to v{$backup['version']}" );

return true;
}

/**
* 解压 ZIP 文件
*
* @param string $zip_path ZIP 路径
* @param string $target_dir 目标目录
* @return bool|WP_Error
*/
private function extract_zip( string $zip_path, string $target_dir ) {
if ( ! class_exists( 'ZipArchive' ) ) {
return new \WP_Error( 'no_zip', __( 'ZipArchive 不可用', 'wpbridge' ) );
}

$zip = new \ZipArchive();

if ( $zip->open( $zip_path ) !== true ) {
return new \WP_Error( 'zip_open_failed', __( '无法打开备份文件', 'wpbridge' ) );
}

$zip->extractTo( $target_dir );
$zip->close();

return true;
}

/**
* 删除备份
*
* @param string $item_key 项目键
* @param string $backup_id 备份 ID
* @return bool
*/
public function delete_backup( string $item_key, string $backup_id ): bool {
$backups = $this->get_all();

if ( ! isset( $backups[ $item_key ] ) ) {
return false;
}

foreach ( $backups[ $item_key ] as $index => $backup ) {
if ( $backup['id'] === $backup_id ) {
// 删除文件
$file_path = $this->get_backup_dir() . '/' . $backup['filename'];
if ( file_exists( $file_path ) ) {
unlink( $file_path );
}

// 删除记录
unset( $backups[ $item_key ][ $index ] );
$backups[ $item_key ] = array_values( $backups[ $item_key ] );

$this->backups = $backups;
update_option( self::OPTION_NAME, $backups );

return true;
}
}

return false;
}

/**
* 获取备份总大小
*
* @return int 字节数
*/
public function get_total_size(): int {
$total = 0;
$backups = $this->get_all();

foreach ( $backups as $item_backups ) {
foreach ( $item_backups as $backup ) {
$total += $backup['size'] ?? 0;
}
}

return $total;
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->backups = null;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,335 +17,335 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class ConfigManager {

/**
* 配置版本
*/
const CONFIG_VERSION = '1.0';
/**
* 配置版本
*/
const CONFIG_VERSION = '1.0';

/**
* 需要导出的选项
*
* @var array
*/
private array $export_options = array(
'wpbridge_sources',
'wpbridge_settings',
'wpbridge_ai_settings',
'wpbridge_source_groups',
'wpbridge_item_sources',
'wpbridge_defaults',
'wpbridge_source_registry',
'wpbridge_plugin_types',
);
/**
* 需要导出的选项
*
* @var array
*/
private array $export_options = [
'wpbridge_sources',
'wpbridge_settings',
'wpbridge_ai_settings',
'wpbridge_source_groups',
'wpbridge_item_sources',
'wpbridge_defaults',
'wpbridge_source_registry',
'wpbridge_plugin_types',
];

/**
* 导出配置
*
* @param bool $include_secrets 是否包含敏感信息API Key 等)
* @return array
*/
public function export( bool $include_secrets = false ): array {
$config = array(
'version' => self::CONFIG_VERSION,
'plugin' => WPBRIDGE_VERSION,
'site_url' => get_site_url(),
'exported' => current_time( 'mysql' ),
'options' => array(),
);
/**
* 导出配置
*
* @param bool $include_secrets 是否包含敏感信息API Key 等)
* @return array
*/
public function export( bool $include_secrets = false ): array {
$config = [
'version' => self::CONFIG_VERSION,
'plugin' => WPBRIDGE_VERSION,
'site_url' => get_site_url(),
'exported' => current_time( 'mysql' ),
'options' => [],
];

foreach ( $this->export_options as $option_name ) {
$value = get_option( $option_name, null );
foreach ( $this->export_options as $option_name ) {
$value = get_option( $option_name, null );

if ( null !== $value ) {
// 处理敏感信息
if ( ! $include_secrets ) {
$value = $this->sanitize_secrets( $option_name, $value );
}
$config['options'][ $option_name ] = $value;
}
}
if ( null !== $value ) {
// 处理敏感信息
if ( ! $include_secrets ) {
$value = $this->sanitize_secrets( $option_name, $value );
}
$config['options'][ $option_name ] = $value;
}
}

return $config;
}
return $config;
}

/**
* 导出为 JSON 字符串
*
* @param bool $include_secrets 是否包含敏感信息
* @return string
*/
public function export_json( bool $include_secrets = false ): string {
return wp_json_encode( $this->export( $include_secrets ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
}
/**
* 导出为 JSON 字符串
*
* @param bool $include_secrets 是否包含敏感信息
* @return string
*/
public function export_json( bool $include_secrets = false ): string {
return wp_json_encode( $this->export( $include_secrets ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
}

/**
* 导入配置
*
* @param array $config 配置数据
* @param bool $merge 是否合并true=合并false=覆盖)
* @return array 导入结果
*/
public function import( array $config, bool $merge = true ): array {
$result = array(
'success' => true,
'imported' => array(),
'skipped' => array(),
'errors' => array(),
);
/**
* 导入配置
*
* @param array $config 配置数据
* @param bool $merge 是否合并true=合并false=覆盖)
* @return array 导入结果
*/
public function import( array $config, bool $merge = true ): array {
$result = [
'success' => true,
'imported' => [],
'skipped' => [],
'errors' => [],
];

// 验证配置格式
$validation = $this->validate_config( $config );
if ( ! $validation['valid'] ) {
$result['success'] = false;
$result['errors'] = $validation['errors'];
return $result;
}
// 验证配置格式
$validation = $this->validate_config( $config );
if ( ! $validation['valid'] ) {
$result['success'] = false;
$result['errors'] = $validation['errors'];
return $result;
}

// 导入选项
foreach ( $config['options'] as $option_name => $value ) {
// 只导入允许的选项
if ( ! in_array( $option_name, $this->export_options, true ) ) {
$result['skipped'][] = $option_name;
continue;
}
// 导入选项
foreach ( $config['options'] as $option_name => $value ) {
// 只导入允许的选项
if ( ! in_array( $option_name, $this->export_options, true ) ) {
$result['skipped'][] = $option_name;
continue;
}

try {
if ( $merge ) {
$value = $this->merge_option( $option_name, $value );
}
try {
if ( $merge ) {
$value = $this->merge_option( $option_name, $value );
}

if ( update_option( $option_name, $value ) ) {
$result['imported'][] = $option_name;
} else {
// 值相同时 update_option 返回 false
$result['imported'][] = $option_name;
}
} catch ( \Exception $e ) {
$result['errors'][] = sprintf(
__( '导入 %1$s 失败: %2$s', 'wpbridge' ),
$option_name,
$e->getMessage()
);
}
}
if ( update_option( $option_name, $value ) ) {
$result['imported'][] = $option_name;
} else {
// 值相同时 update_option 返回 false
$result['imported'][] = $option_name;
}
} catch ( \Exception $e ) {
$result['errors'][] = sprintf(
__( '导入 %s 失败: %s', 'wpbridge' ),
$option_name,
$e->getMessage()
);
}
}

return $result;
}
return $result;
}

/**
* 从 JSON 字符串导入
*
* @param string $json JSON 字符串
* @param bool $merge 是否合并
* @return array 导入结果
*/
public function import_json( string $json, bool $merge = true ): array {
$config = json_decode( $json, true );
/**
* 从 JSON 字符串导入
*
* @param string $json JSON 字符串
* @param bool $merge 是否合并
* @return array 导入结果
*/
public function import_json( string $json, bool $merge = true ): array {
$config = json_decode( $json, true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
return array(
'success' => false,
'errors' => array( __( 'JSON 格式无效', 'wpbridge' ) ),
);
}
if ( json_last_error() !== JSON_ERROR_NONE ) {
return [
'success' => false,
'errors' => [ __( 'JSON 格式无效', 'wpbridge' ) ],
];
}

return $this->import( $config, $merge );
}
return $this->import( $config, $merge );
}

/**
* 验证配置格式
*
* @param array $config 配置数据
* @return array
*/
public function validate_config( array $config ): array {
$errors = array();
/**
* 验证配置格式
*
* @param array $config 配置数据
* @return array
*/
public function validate_config( array $config ): array {
$errors = [];

if ( empty( $config['version'] ) ) {
$errors[] = __( '缺少配置版本号', 'wpbridge' );
}
if ( empty( $config['version'] ) ) {
$errors[] = __( '缺少配置版本号', 'wpbridge' );
}

if ( empty( $config['options'] ) || ! is_array( $config['options'] ) ) {
$errors[] = __( '缺少配置选项', 'wpbridge' );
}
if ( empty( $config['options'] ) || ! is_array( $config['options'] ) ) {
$errors[] = __( '缺少配置选项', 'wpbridge' );
}

// 检查版本兼容性
if ( ! empty( $config['version'] ) && version_compare( $config['version'], self::CONFIG_VERSION, '>' ) ) {
$errors[] = sprintf(
__( '配置版本 %1$s 高于当前支持的版本 %2$s', 'wpbridge' ),
$config['version'],
self::CONFIG_VERSION
);
}
// 检查版本兼容性
if ( ! empty( $config['version'] ) && version_compare( $config['version'], self::CONFIG_VERSION, '>' ) ) {
$errors[] = sprintf(
__( '配置版本 %s 高于当前支持的版本 %s', 'wpbridge' ),
$config['version'],
self::CONFIG_VERSION
);
}

return array(
'valid' => empty( $errors ),
'errors' => $errors,
);
}
return [
'valid' => empty( $errors ),
'errors' => $errors,
];
}

/**
* 清理敏感信息
*
* @param string $option_name 选项名
* @param mixed $value 选项值
* @return mixed
*/
private function sanitize_secrets( string $option_name, $value ) {
if ( ! is_array( $value ) ) {
return $value;
}
/**
* 清理敏感信息
*
* @param string $option_name 选项名
* @param mixed $value 选项值
* @return mixed
*/
private function sanitize_secrets( string $option_name, $value ) {
if ( ! is_array( $value ) ) {
return $value;
}

// 更新源中的敏感字段
if ( 'wpbridge_sources' === $option_name ) {
foreach ( $value as &$source ) {
if ( isset( $source['auth_token'] ) && ! empty( $source['auth_token'] ) ) {
$source['auth_token'] = '***REDACTED***';
}
if ( isset( $source['api_key'] ) && ! empty( $source['api_key'] ) ) {
$source['api_key'] = '***REDACTED***';
}
}
}
// 更新源中的敏感字段
if ( 'wpbridge_sources' === $option_name ) {
foreach ( $value as &$source ) {
if ( isset( $source['auth_token'] ) && ! empty( $source['auth_token'] ) ) {
$source['auth_token'] = '***REDACTED***';
}
if ( isset( $source['api_key'] ) && ! empty( $source['api_key'] ) ) {
$source['api_key'] = '***REDACTED***';
}
}
}

// AI 设置中的敏感字段
if ( 'wpbridge_ai_settings' === $option_name ) {
if ( isset( $value['api_key'] ) && ! empty( $value['api_key'] ) ) {
$value['api_key'] = '***REDACTED***';
}
}
// AI 设置中的敏感字段
if ( 'wpbridge_ai_settings' === $option_name ) {
if ( isset( $value['api_key'] ) && ! empty( $value['api_key'] ) ) {
$value['api_key'] = '***REDACTED***';
}
}

return $value;
}
return $value;
}

/**
* 合并选项值
*
* @param string $option_name 选项名
* @param mixed $new_value 新值
* @return mixed
*/
private function merge_option( string $option_name, $new_value ) {
$current = get_option( $option_name, array() );
/**
* 合并选项值
*
* @param string $option_name 选项名
* @param mixed $new_value 新值
* @return mixed
*/
private function merge_option( string $option_name, $new_value ) {
$current = get_option( $option_name, [] );

// 如果当前值为空,直接使用新值
if ( empty( $current ) ) {
return $new_value;
}
// 如果当前值为空,直接使用新值
if ( empty( $current ) ) {
return $new_value;
}

// 如果不是数组,直接覆盖
if ( ! is_array( $current ) || ! is_array( $new_value ) ) {
return $new_value;
}
// 如果不是数组,直接覆盖
if ( ! is_array( $current ) || ! is_array( $new_value ) ) {
return $new_value;
}

// 更新源:按 ID 合并
if ( 'wpbridge_sources' === $option_name ) {
return $this->merge_sources( $current, $new_value );
}
// 更新源:按 ID 合并
if ( 'wpbridge_sources' === $option_name ) {
return $this->merge_sources( $current, $new_value );
}

// 源分组:按 ID 合并
if ( 'wpbridge_source_groups' === $option_name ) {
return $this->merge_by_id( $current, $new_value );
}
// 源分组:按 ID 合并
if ( 'wpbridge_source_groups' === $option_name ) {
return $this->merge_by_id( $current, $new_value );
}

// 其他数组:深度合并
return array_replace_recursive( $current, $new_value );
}
// 其他数组:深度合并
return array_replace_recursive( $current, $new_value );
}

/**
* 合并更新源
*
* @param array $current 当前源
* @param array $new 新源
* @return array
*/
private function merge_sources( array $current, array $new ): array {
$merged = $current;
$ids = array_column( $current, 'id' );
/**
* 合并更新源
*
* @param array $current 当前源
* @param array $new 新源
* @return array
*/
private function merge_sources( array $current, array $new ): array {
$merged = $current;
$ids = array_column( $current, 'id' );

foreach ( $new as $source ) {
if ( empty( $source['id'] ) ) {
continue;
}
foreach ( $new as $source ) {
if ( empty( $source['id'] ) ) {
continue;
}

$index = array_search( $source['id'], $ids, true );
$index = array_search( $source['id'], $ids, true );

if ( false !== $index ) {
// 更新现有源(保留敏感信息)
if ( isset( $source['auth_token'] ) && '***REDACTED***' === $source['auth_token'] ) {
$source['auth_token'] = $merged[ $index ]['auth_token'] ?? '';
}
$merged[ $index ] = array_merge( $merged[ $index ], $source );
} else {
// 添加新源
$merged[] = $source;
}
}
if ( false !== $index ) {
// 更新现有源(保留敏感信息)
if ( isset( $source['auth_token'] ) && '***REDACTED***' === $source['auth_token'] ) {
$source['auth_token'] = $merged[ $index ]['auth_token'] ?? '';
}
$merged[ $index ] = array_merge( $merged[ $index ], $source );
} else {
// 添加新源
$merged[] = $source;
}
}

return $merged;
}
return $merged;
}

/**
* 按 ID 合并数组
*
* @param array $current 当前数组
* @param array $new 新数组
* @return array
*/
private function merge_by_id( array $current, array $new ): array {
$merged = $current;
$ids = array_column( $current, 'id' );
/**
* 按 ID 合并数组
*
* @param array $current 当前数组
* @param array $new 新数组
* @return array
*/
private function merge_by_id( array $current, array $new ): array {
$merged = $current;
$ids = array_column( $current, 'id' );

foreach ( $new as $item ) {
if ( empty( $item['id'] ) ) {
continue;
}
foreach ( $new as $item ) {
if ( empty( $item['id'] ) ) {
continue;
}

$index = array_search( $item['id'], $ids, true );
$index = array_search( $item['id'], $ids, true );

if ( false !== $index ) {
$merged[ $index ] = array_merge( $merged[ $index ], $item );
} else {
$merged[] = $item;
}
}
if ( false !== $index ) {
$merged[ $index ] = array_merge( $merged[ $index ], $item );
} else {
$merged[] = $item;
}
}

return $merged;
}
return $merged;
}

/**
* 创建备份
*
* @return array 备份数据
*/
public function create_backup(): array {
return $this->export( true );
}
/**
* 创建备份
*
* @return array 备份数据
*/
public function create_backup(): array {
return $this->export( true );
}

/**
* 恢复备份
*
* @param array $backup 备份数据
* @return array 恢复结果
*/
public function restore_backup( array $backup ): array {
return $this->import( $backup, false );
}
/**
* 恢复备份
*
* @param array $backup 备份数据
* @return array 恢复结果
*/
public function restore_backup( array $backup ): array {
return $this->import( $backup, false );
}

/**
* 重置为默认配置
*
* @return bool
*/
public function reset_to_defaults(): bool {
foreach ( $this->export_options as $option_name ) {
delete_option( $option_name );
}
/**
* 重置为默认配置
*
* @return bool
*/
public function reset_to_defaults(): bool {
foreach ( $this->export_options as $option_name ) {
delete_option( $option_name );
}

// 重新初始化默认设置
$settings = new Settings();
$settings->init_defaults();
// 重新初始化默认设置
$settings = new Settings();
$settings->init_defaults();

return true;
}
return true;
}
}

View file

@ -12,7 +12,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -22,250 +22,250 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class DefaultsManager {

/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_defaults';
/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_defaults';

/**
* 作用范围
*/
const SCOPE_GLOBAL = 'global';
const SCOPE_PLUGIN = 'plugin';
const SCOPE_THEME = 'theme';
const SCOPE_CORE = 'core';
/**
* 作用范围
*/
const SCOPE_GLOBAL = 'global';
const SCOPE_PLUGIN = 'plugin';
const SCOPE_THEME = 'theme';
const SCOPE_CORE = 'core';

/**
* 缓存的默认规则
*
* @var array|null
*/
private ?array $cached_defaults = null;
/**
* 缓存的默认规则
*
* @var array|null
*/
private ?array $cached_defaults = null;

/**
* 获取所有默认规则
*
* @return array
*/
public function get_all(): array {
if ( null === $this->cached_defaults ) {
$this->cached_defaults = get_option( self::OPTION_NAME, array() );
$this->ensure_defaults();
}
return $this->cached_defaults;
}
/**
* 获取所有默认规则
*
* @return array
*/
public function get_all(): array {
if ( null === $this->cached_defaults ) {
$this->cached_defaults = get_option( self::OPTION_NAME, [] );
$this->ensure_defaults();
}
return $this->cached_defaults;
}

/**
* 获取指定范围的默认规则
*
* @param string $scope 作用范围
* @return array
*/
public function get( string $scope ): array {
$defaults = $this->get_all();
return $defaults[ $scope ] ?? $this->get_scope_defaults( $scope );
}
/**
* 获取指定范围的默认规则
*
* @param string $scope 作用范围
* @return array
*/
public function get( string $scope ): array {
$defaults = $this->get_all();
return $defaults[ $scope ] ?? $this->get_scope_defaults( $scope );
}

/**
* 设置指定范围的默认规则
*
* @param string $scope 作用范围
* @param array $rules 规则数据
* @return bool
*/
public function set( string $scope, array $rules ): bool {
$defaults = $this->get_all();
$defaults[ $scope ] = array_merge(
$this->get_scope_defaults( $scope ),
$rules
);
$defaults[ $scope ]['updated_at'] = current_time( 'mysql' );
/**
* 设置指定范围的默认规则
*
* @param string $scope 作用范围
* @param array $rules 规则数据
* @return bool
*/
public function set( string $scope, array $rules ): bool {
$defaults = $this->get_all();
$defaults[ $scope ] = array_merge(
$this->get_scope_defaults( $scope ),
$rules
);
$defaults[ $scope ]['updated_at'] = current_time( 'mysql' );

$this->cached_defaults = $defaults;
return update_option( self::OPTION_NAME, $defaults, false );
}
$this->cached_defaults = $defaults;
return update_option( self::OPTION_NAME, $defaults, false );
}

/**
* 获取默认源顺序
*
* @param string $scope 作用范围
* @return array 源键列表(按优先级排序)
*/
public function get_source_order( string $scope ): array {
$rules = $this->get( $scope );
return $rules['source_order'] ?? array( 'wenpai-mirror', 'wporg' );
}
/**
* 获取默认源顺序
*
* @param string $scope 作用范围
* @return array 源键列表(按优先级排序)
*/
public function get_source_order( string $scope ): array {
$rules = $this->get( $scope );
return $rules['source_order'] ?? [ 'wenpai-mirror', 'wporg' ];
}

/**
* 设置默认源顺序
*
* @param string $scope 作用范围
* @param array $source_order 源键列表
* @return bool
*/
public function set_source_order( string $scope, array $source_order ): bool {
return $this->set( $scope, array( 'source_order' => $source_order ) );
}
/**
* 设置默认源顺序
*
* @param string $scope 作用范围
* @param array $source_order 源键列表
* @return bool
*/
public function set_source_order( string $scope, array $source_order ): bool {
return $this->set( $scope, [ 'source_order' => $source_order ] );
}

/**
* 获取默认更新源列表
*
* @param string $scope 作用范围
* @param SourceRegistry $source_registry 源注册表
* @return array 源列表(按优先级排序)
*/
public function get_default_sources( string $scope, SourceRegistry $source_registry ): array {
$source_order = $this->get_source_order( $scope );
$sources = array();
$priority = 100;
/**
* 获取默认更新源列表
*
* @param string $scope 作用范围
* @param SourceRegistry $source_registry 源注册表
* @return array 源列表(按优先级排序)
*/
public function get_default_sources( string $scope, SourceRegistry $source_registry ): array {
$source_order = $this->get_source_order( $scope );
$sources = [];
$priority = 100;

foreach ( $source_order as $source_key ) {
$source = $source_registry->get( $source_key );
if ( $source && ! empty( $source['enabled'] ) ) {
$source['priority'] = $priority;
$sources[] = $source;
$priority -= 10;
}
}
foreach ( $source_order as $source_key ) {
$source = $source_registry->get( $source_key );
if ( $source && ! empty( $source['enabled'] ) ) {
$source['priority'] = $priority;
$sources[] = $source;
$priority -= 10;
}
}

// 如果没有配置的源可用,回退到 WordPress.org
if ( empty( $sources ) ) {
$rules = $this->get( $scope );
if ( ! empty( $rules['fallback_to_wporg'] ) ) {
$wporg = $source_registry->get( 'wporg' );
if ( $wporg && ! empty( $wporg['enabled'] ) ) {
$wporg['priority'] = 1;
$sources[] = $wporg;
}
}
}
// 如果没有配置的源可用,回退到 WordPress.org
if ( empty( $sources ) ) {
$rules = $this->get( $scope );
if ( ! empty( $rules['fallback_to_wporg'] ) ) {
$wporg = $source_registry->get( 'wporg' );
if ( $wporg && ! empty( $wporg['enabled'] ) ) {
$wporg['priority'] = 1;
$sources[] = $wporg;
}
}
}

return $sources;
}
return $sources;
}

/**
* 是否需要签名验证
*
* @param string $scope 作用范围
* @return bool
*/
public function is_signature_required( string $scope ): bool {
$rules = $this->get( $scope );
return ! empty( $rules['signature_required'] );
}
/**
* 是否需要签名验证
*
* @param string $scope 作用范围
* @return bool
*/
public function is_signature_required( string $scope ): bool {
$rules = $this->get( $scope );
return ! empty( $rules['signature_required'] );
}

/**
* 是否允许无签名包
*
* @param string $scope 作用范围
* @return bool
*/
public function is_unsigned_allowed( string $scope ): bool {
$rules = $this->get( $scope );
return ! empty( $rules['allow_unsigned'] );
}
/**
* 是否允许无签名包
*
* @param string $scope 作用范围
* @return bool
*/
public function is_unsigned_allowed( string $scope ): bool {
$rules = $this->get( $scope );
return ! empty( $rules['allow_unsigned'] );
}

/**
* 是否允许预发布版本
*
* @param string $scope 作用范围
* @return bool
*/
public function is_prerelease_allowed( string $scope ): bool {
$rules = $this->get( $scope );
return ! empty( $rules['allow_prerelease'] );
}
/**
* 是否允许预发布版本
*
* @param string $scope 作用范围
* @return bool
*/
public function is_prerelease_allowed( string $scope ): bool {
$rules = $this->get( $scope );
return ! empty( $rules['allow_prerelease'] );
}

/**
* 获取最低信任阈值
*
* @param string $scope 作用范围
* @return int
*/
public function get_trust_floor( string $scope ): int {
$rules = $this->get( $scope );
return (int) ( $rules['trust_floor'] ?? 0 );
}
/**
* 获取最低信任阈值
*
* @param string $scope 作用范围
* @return int
*/
public function get_trust_floor( string $scope ): int {
$rules = $this->get( $scope );
return (int) ( $rules['trust_floor'] ?? 0 );
}

/**
* 确保默认规则存在
*/
private function ensure_defaults(): void {
$needs_update = false;
$scopes = array( self::SCOPE_GLOBAL, self::SCOPE_PLUGIN, self::SCOPE_THEME, self::SCOPE_CORE );
/**
* 确保默认规则存在
*/
private function ensure_defaults(): void {
$needs_update = false;
$scopes = [ self::SCOPE_GLOBAL, self::SCOPE_PLUGIN, self::SCOPE_THEME, self::SCOPE_CORE ];

foreach ( $scopes as $scope ) {
if ( ! isset( $this->cached_defaults[ $scope ] ) ) {
$this->cached_defaults[ $scope ] = $this->get_scope_defaults( $scope );
$needs_update = true;
}
}
foreach ( $scopes as $scope ) {
if ( ! isset( $this->cached_defaults[ $scope ] ) ) {
$this->cached_defaults[ $scope ] = $this->get_scope_defaults( $scope );
$needs_update = true;
}
}

if ( $needs_update ) {
update_option( self::OPTION_NAME, $this->cached_defaults, false );
}
}
if ( $needs_update ) {
update_option( self::OPTION_NAME, $this->cached_defaults, false );
}
}

/**
* 获取范围的默认值
*
* @param string $scope 作用范围
* @return array
*/
private function get_scope_defaults( string $scope ): array {
$base = array(
'source_order' => array( 'wenpai-mirror', 'wporg' ),
'signature_required' => false,
'allow_unsigned' => true,
'allow_prerelease' => false,
'trust_floor' => 0,
'fallback_to_wporg' => true,
'policy' => array(),
'updated_at' => current_time( 'mysql' ),
);
/**
* 获取范围的默认值
*
* @param string $scope 作用范围
* @return array
*/
private function get_scope_defaults( string $scope ): array {
$base = [
'source_order' => [ 'wenpai-mirror', 'wporg' ],
'signature_required' => false,
'allow_unsigned' => true,
'allow_prerelease' => false,
'trust_floor' => 0,
'fallback_to_wporg' => true,
'policy' => [],
'updated_at' => current_time( 'mysql' ),
];

// 根据范围调整默认值
switch ( $scope ) {
case self::SCOPE_CORE:
$base['source_order'] = array( 'wporg' );
$base['trust_floor'] = 90;
break;
// 根据范围调整默认值
switch ( $scope ) {
case self::SCOPE_CORE:
$base['source_order'] = [ 'wporg' ];
$base['trust_floor'] = 90;
break;

case self::SCOPE_PLUGIN:
case self::SCOPE_THEME:
$base['source_order'] = array( 'wenpai-mirror', 'wporg' );
break;
case self::SCOPE_PLUGIN:
case self::SCOPE_THEME:
$base['source_order'] = [ 'wenpai-mirror', 'wporg' ];
break;

case self::SCOPE_GLOBAL:
default:
break;
}
case self::SCOPE_GLOBAL:
default:
break;
}

return $base;
}
return $base;
}

/**
* 重置为默认值
*
* @param string|null $scope 作用范围null 表示全部重置
* @return bool
*/
public function reset( ?string $scope = null ): bool {
if ( null === $scope ) {
$this->cached_defaults = null;
return delete_option( self::OPTION_NAME );
}
/**
* 重置为默认值
*
* @param string|null $scope 作用范围null 表示全部重置
* @return bool
*/
public function reset( ?string $scope = null ): bool {
if ( null === $scope ) {
$this->cached_defaults = null;
return delete_option( self::OPTION_NAME );
}

$defaults = $this->get_all();
$defaults[ $scope ] = $this->get_scope_defaults( $scope );
$this->cached_defaults = $defaults;
return update_option( self::OPTION_NAME, $defaults, false );
}
$defaults = $this->get_all();
$defaults[ $scope ] = $this->get_scope_defaults( $scope );
$this->cached_defaults = $defaults;
return update_option( self::OPTION_NAME, $defaults, false );
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->cached_defaults = null;
}
/**
* 清除缓存
*/
public function clear_cache(): void {
$this->cached_defaults = null;
}
}

View file

@ -12,7 +12,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -22,397 +22,385 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class ItemSourceManager {

/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_item_sources';
/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_item_sources';

/**
* 项目类型
*/
const TYPE_PLUGIN = 'plugin';
const TYPE_THEME = 'theme';
const TYPE_MUPLUGIN = 'mu-plugin';
const TYPE_DROPIN = 'dropin';
/**
* 项目类型
*/
const TYPE_PLUGIN = 'plugin';
const TYPE_THEME = 'theme';
const TYPE_MUPLUGIN = 'mu-plugin';
const TYPE_DROPIN = 'dropin';

/**
* 配置模式
*/
const MODE_DEFAULT = 'default';
const MODE_CUSTOM = 'custom';
const MODE_DISABLED = 'disabled';
/**
* 配置模式
*/
const MODE_DEFAULT = 'default';
const MODE_CUSTOM = 'custom';
const MODE_DISABLED = 'disabled';

/**
* 缓存的配置
*
* @var array|null
*/
private ?array $cached_configs = null;
/**
* 缓存的配置
*
* @var array|null
*/
private ?array $cached_configs = null;

/**
* 源注册表
*
* @var SourceRegistry
*/
private SourceRegistry $source_registry;
/**
* 源注册表
*
* @var SourceRegistry
*/
private SourceRegistry $source_registry;

/**
* 构造函数
*
* @param SourceRegistry $source_registry 源注册表
*/
public function __construct( SourceRegistry $source_registry ) {
$this->source_registry = $source_registry;
}
/**
* 构造函数
*
* @param SourceRegistry $source_registry 源注册表
*/
public function __construct( SourceRegistry $source_registry ) {
$this->source_registry = $source_registry;
}

/**
* 获取所有项目配置
*
* @return array
*/
public function get_all(): array {
if ( null === $this->cached_configs ) {
$this->cached_configs = get_option( self::OPTION_NAME, array() );
}
return $this->cached_configs;
}
/**
* 获取所有项目配置
*
* @return array
*/
public function get_all(): array {
if ( null === $this->cached_configs ) {
$this->cached_configs = get_option( self::OPTION_NAME, [] );
}
return $this->cached_configs;
}

/**
* 按类型获取项目配置
*
* @param string $type 项目类型
* @return array
*/
public function get_by_type( string $type ): array {
return array_filter( $this->get_all(), fn( $c ) => ( $c['item_type'] ?? '' ) === $type );
}
/**
* 按类型获取项目配置
*
* @param string $type 项目类型
* @return array
*/
public function get_by_type( string $type ): array {
return array_filter( $this->get_all(), fn( $c ) => ( $c['item_type'] ?? '' ) === $type );
}

/**
* 获取单个项目配置
*
* @param string $item_key 项目键plugin_basename 或主题目录名)
* @return array|null
*/
public function get( string $item_key ): ?array {
foreach ( $this->get_all() as $config ) {
if ( ( $config['item_key'] ?? '' ) === $item_key ) {
return $config;
}
}
return null;
}
/**
* 获取单个项目配置
*
* @param string $item_key 项目键plugin_basename 或主题目录名)
* @return array|null
*/
public function get( string $item_key ): ?array {
foreach ( $this->get_all() as $config ) {
if ( ( $config['item_key'] ?? '' ) === $item_key ) {
return $config;
}
}
return null;
}

/**
* 通过 DID 获取项目配置
*
* @param string $did 项目 DID
* @return array|null
*/
public function get_by_did( string $did ): ?array {
foreach ( $this->get_all() as $config ) {
if ( ( $config['item_did'] ?? '' ) === $did ) {
return $config;
}
}
return null;
}
/**
* 通过 DID 获取项目配置
*
* @param string $did 项目 DID
* @return array|null
*/
public function get_by_did( string $did ): ?array {
foreach ( $this->get_all() as $config ) {
if ( ( $config['item_did'] ?? '' ) === $did ) {
return $config;
}
}
return null;
}

/**
* 设置项目配置
*
* @param string $item_key 项目键
* @param array $config 配置数据
* @return bool
*/
public function set( string $item_key, array $config ): bool {
$configs = $this->get_all();
$found = false;
/**
* 设置项目配置
*
* @param string $item_key 项目键
* @param array $config 配置数据
* @return bool
*/
public function set( string $item_key, array $config ): bool {
$configs = $this->get_all();
$found = false;

foreach ( $configs as $index => $existing ) {
if ( ( $existing['item_key'] ?? '' ) === $item_key ) {
$configs[ $index ] = array_merge( $existing, $config );
$configs[ $index ]['item_key'] = $item_key;
$configs[ $index ]['updated_at'] = current_time( 'mysql' );
$found = true;
break;
}
}
foreach ( $configs as $index => $existing ) {
if ( ( $existing['item_key'] ?? '' ) === $item_key ) {
$configs[ $index ] = array_merge( $existing, $config );
$configs[ $index ]['item_key'] = $item_key;
$configs[ $index ]['updated_at'] = current_time( 'mysql' );
$found = true;
break;
}
}

if ( ! $found ) {
$config = $this->normalize_config( $config );
$config['item_key'] = $item_key;
$config['created_at'] = current_time( 'mysql' );
$config['updated_at'] = current_time( 'mysql' );
$configs[] = $config;
}
if ( ! $found ) {
$config = $this->normalize_config( $config );
$config['item_key'] = $item_key;
$config['created_at'] = current_time( 'mysql' );
$config['updated_at'] = current_time( 'mysql' );
$configs[] = $config;
}

$this->cached_configs = $configs;
return update_option( self::OPTION_NAME, $configs, false );
}
$this->cached_configs = $configs;
return update_option( self::OPTION_NAME, $configs, false );
}

/**
* 删除项目配置
*
* @param string $item_key 项目键
* @return bool
*/
public function delete( string $item_key ): bool {
$configs = $this->get_all();
/**
* 删除项目配置
*
* @param string $item_key 项目键
* @return bool
*/
public function delete( string $item_key ): bool {
$configs = $this->get_all();

foreach ( $configs as $index => $config ) {
if ( ( $config['item_key'] ?? '' ) === $item_key ) {
unset( $configs[ $index ] );
$configs = array_values( $configs );
$this->cached_configs = $configs;
return update_option( self::OPTION_NAME, $configs, false );
}
}
return false;
}
foreach ( $configs as $index => $config ) {
if ( ( $config['item_key'] ?? '' ) === $item_key ) {
unset( $configs[ $index ] );
$configs = array_values( $configs );
$this->cached_configs = $configs;
return update_option( self::OPTION_NAME, $configs, false );
}
}
return false;
}

/**
* 设置项目的更新源
*
* @param string $item_key 项目键
* @param string $source_key 源键
* @param int $priority 优先级
* @return bool
*/
public function set_source( string $item_key, string $source_key, int $priority = 50 ): bool {
$config = $this->get( $item_key ) ?? array();
$source_ids = $config['source_ids'] ?? array();
/**
* 设置项目的更新源
*
* @param string $item_key 项目键
* @param string $source_key 源键
* @param int $priority 优先级
* @return bool
*/
public function set_source( string $item_key, string $source_key, int $priority = 50 ): bool {
$config = $this->get( $item_key ) ?? [];
$source_ids = $config['source_ids'] ?? [];

// 检查源是否存在
if ( ! $this->source_registry->get( $source_key ) ) {
return false;
}
// 检查源是否存在
if ( ! $this->source_registry->get( $source_key ) ) {
return false;
}

// 添加或更新源
$source_ids[ $source_key ] = $priority;
arsort( $source_ids ); // 按优先级排序
// 添加或更新源
$source_ids[ $source_key ] = $priority;
arsort( $source_ids ); // 按优先级排序

return $this->set(
$item_key,
array(
'mode' => self::MODE_CUSTOM,
'source_ids' => $source_ids,
)
);
}
return $this->set( $item_key, [
'mode' => self::MODE_CUSTOM,
'source_ids' => $source_ids,
] );
}

/**
* 移除项目的更新源
*
* @param string $item_key 项目键
* @param string $source_key 源键
* @return bool
*/
public function remove_source( string $item_key, string $source_key ): bool {
$config = $this->get( $item_key );
if ( ! $config ) {
return false;
}
/**
* 移除项目的更新源
*
* @param string $item_key 项目键
* @param string $source_key 源键
* @return bool
*/
public function remove_source( string $item_key, string $source_key ): bool {
$config = $this->get( $item_key );
if ( ! $config ) {
return false;
}

$source_ids = $config['source_ids'] ?? array();
unset( $source_ids[ $source_key ] );
$source_ids = $config['source_ids'] ?? [];
unset( $source_ids[ $source_key ] );

// 如果没有自定义源了,切回默认模式
$mode = empty( $source_ids ) ? self::MODE_DEFAULT : self::MODE_CUSTOM;
// 如果没有自定义源了,切回默认模式
$mode = empty( $source_ids ) ? self::MODE_DEFAULT : self::MODE_CUSTOM;

return $this->set(
$item_key,
array(
'mode' => $mode,
'source_ids' => $source_ids,
)
);
}
return $this->set( $item_key, [
'mode' => $mode,
'source_ids' => $source_ids,
] );
}

/**
* 禁用项目更新
*
* @param string $item_key 项目键
* @return bool
*/
public function disable_updates( string $item_key ): bool {
return $this->set( $item_key, array( 'mode' => self::MODE_DISABLED ) );
}
/**
* 禁用项目更新
*
* @param string $item_key 项目键
* @return bool
*/
public function disable_updates( string $item_key ): bool {
return $this->set( $item_key, [ 'mode' => self::MODE_DISABLED ] );
}

/**
* 启用项目更新(切回默认)
*
* @param string $item_key 项目键
* @return bool
*/
public function enable_updates( string $item_key ): bool {
return $this->set( $item_key, array( 'mode' => self::MODE_DEFAULT ) );
}
/**
* 启用项目更新(切回默认)
*
* @param string $item_key 项目键
* @return bool
*/
public function enable_updates( string $item_key ): bool {
return $this->set( $item_key, [ 'mode' => self::MODE_DEFAULT ] );
}

/**
* 固定项目到特定源
*
* @param string $item_key 项目键
* @param string $source_key 源键
* @return bool
*/
public function pin_to_source( string $item_key, string $source_key ): bool {
return $this->set(
$item_key,
array(
'mode' => self::MODE_CUSTOM,
'source_ids' => array( $source_key => 100 ),
'pinned' => true,
)
);
}
/**
* 固定项目到特定源
*
* @param string $item_key 项目键
* @param string $source_key 源键
* @return bool
*/
public function pin_to_source( string $item_key, string $source_key ): bool {
return $this->set( $item_key, [
'mode' => self::MODE_CUSTOM,
'source_ids' => [ $source_key => 100 ],
'pinned' => true,
] );
}

/**
* 获取项目的有效更新源列表
*
* @param string $item_key 项目键
* @param DefaultsManager $defaults 默认规则管理器
* @return array 源列表(按优先级排序)
*/
public function get_effective_sources( string $item_key, DefaultsManager $defaults ): array {
$config = $this->get( $item_key );
/**
* 获取项目的有效更新源列表
*
* @param string $item_key 项目键
* @param DefaultsManager $defaults 默认规则管理器
* @return array 源列表(按优先级排序)
*/
public function get_effective_sources( string $item_key, DefaultsManager $defaults ): array {
$config = $this->get( $item_key );

// 如果禁用更新,返回空
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_DISABLED ) {
return array();
}
// 如果禁用更新,返回空
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_DISABLED ) {
return [];
}

// 如果有自定义配置,使用自定义源
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_CUSTOM ) {
$source_ids = $config['source_ids'] ?? array();
$sources = array();
// 如果有自定义配置,使用自定义源
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_CUSTOM ) {
$source_ids = $config['source_ids'] ?? [];
$sources = [];

foreach ( $source_ids as $source_key => $priority ) {
$source = $this->source_registry->get( $source_key );
if ( $source && ! empty( $source['enabled'] ) ) {
$source['priority'] = $priority;
$sources[] = $source;
}
}
foreach ( $source_ids as $source_key => $priority ) {
$source = $this->source_registry->get( $source_key );
if ( $source && ! empty( $source['enabled'] ) ) {
$source['priority'] = $priority;
$sources[] = $source;
}
}

// 按优先级排序
usort( $sources, fn( $a, $b ) => ( $b['priority'] ?? 0 ) - ( $a['priority'] ?? 0 ) );
return $sources;
}
// 按优先级排序
usort( $sources, fn( $a, $b ) => ( $b['priority'] ?? 0 ) - ( $a['priority'] ?? 0 ) );
return $sources;
}

// 使用默认源 - 从配置或 item_key 前缀推断类型
$item_type = $this->resolve_item_type( $item_key, $config );
return $defaults->get_default_sources( $item_type, $this->source_registry );
}
// 使用默认源 - 从配置或 item_key 前缀推断类型
$item_type = $this->resolve_item_type( $item_key, $config );
return $defaults->get_default_sources( $item_type, $this->source_registry );
}

/**
* 解析项目类型
*
* 优先从配置获取,否则从 item_key 前缀推断
*
* @param string $item_key 项目键
* @param array|null $config 项目配置(可能为 null
* @return string 项目类型
*/
private function resolve_item_type( string $item_key, ?array $config ): string {
// 优先使用配置中的类型
if ( $config && ! empty( $config['item_type'] ) ) {
return $config['item_type'];
}
/**
* 解析项目类型
*
* 优先从配置获取,否则从 item_key 前缀推断
*
* @param string $item_key 项目键
* @param array|null $config 项目配置(可能为 null
* @return string 项目类型
*/
private function resolve_item_type( string $item_key, ?array $config ): string {
// 优先使用配置中的类型
if ( $config && ! empty( $config['item_type'] ) ) {
return $config['item_type'];
}

// 从 item_key 前缀推断类型
// 格式: "type:identifier" 例如 "plugin:hello-dolly/hello.php" 或 "theme:flavor"
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
return self::TYPE_PLUGIN;
}
// 从 item_key 前缀推断类型
// 格式: "type:identifier" 例如 "plugin:hello-dolly/hello.php" 或 "theme:flavor"
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
return self::TYPE_PLUGIN;
}

if ( strpos( $item_key, 'theme:' ) === 0 ) {
return self::TYPE_THEME;
}
if ( strpos( $item_key, 'theme:' ) === 0 ) {
return self::TYPE_THEME;
}

if ( strpos( $item_key, 'mu-plugin:' ) === 0 ) {
return self::TYPE_MUPLUGIN;
}
if ( strpos( $item_key, 'mu-plugin:' ) === 0 ) {
return self::TYPE_MUPLUGIN;
}

if ( strpos( $item_key, 'dropin:' ) === 0 ) {
return self::TYPE_DROPIN;
}
if ( strpos( $item_key, 'dropin:' ) === 0 ) {
return self::TYPE_DROPIN;
}

// 无前缀时默认为插件类型(向后兼容)
return self::TYPE_PLUGIN;
}
// 无前缀时默认为插件类型(向后兼容)
return self::TYPE_PLUGIN;
}

/**
* 批量设置项目配置
*
* @param array $item_keys 项目键列表
* @param string $source_key 源键
* @param int $priority 优先级
* @return int 成功数量
*/
public function batch_set_source( array $item_keys, string $source_key, int $priority = 50 ): int {
$success = 0;
foreach ( $item_keys as $item_key ) {
if ( $this->set_source( $item_key, $source_key, $priority ) ) {
++$success;
}
}
return $success;
}
/**
* 批量设置项目配置
*
* @param array $item_keys 项目键列表
* @param string $source_key 源键
* @param int $priority 优先级
* @return int 成功数量
*/
public function batch_set_source( array $item_keys, string $source_key, int $priority = 50 ): int {
$success = 0;
foreach ( $item_keys as $item_key ) {
if ( $this->set_source( $item_key, $source_key, $priority ) ) {
$success++;
}
}
return $success;
}

/**
* 批量重置为默认
*
* @param array $item_keys 项目键列表
* @return int 成功数量
*/
public function batch_reset_to_default( array $item_keys ): int {
$success = 0;
foreach ( $item_keys as $item_key ) {
if ( $this->delete( $item_key ) ) {
++$success;
}
}
return $success;
}
/**
* 批量重置为默认
*
* @param array $item_keys 项目键列表
* @return int 成功数量
*/
public function batch_reset_to_default( array $item_keys ): int {
$success = 0;
foreach ( $item_keys as $item_key ) {
if ( $this->delete( $item_key ) ) {
$success++;
}
}
return $success;
}

/**
* 规范化配置数据
*
* @param array $config 配置数据
* @return array
*/
private function normalize_config( array $config ): array {
return wp_parse_args(
$config,
array(
'item_key' => '',
'item_type' => self::TYPE_PLUGIN,
'item_slug' => '',
'item_did' => '',
'label' => '',
'mode' => self::MODE_DEFAULT,
'source_ids' => array(),
'pinned' => false,
'signature_required' => false,
'allow_unsigned' => true,
'allow_prerelease' => false,
'min_version' => '',
'max_version' => '',
'last_good_version' => '',
'metadata' => array(
'preconfigured' => false,
'installed' => true,
),
'created_at' => current_time( 'mysql' ),
'updated_at' => current_time( 'mysql' ),
)
);
}
/**
* 规范化配置数据
*
* @param array $config 配置数据
* @return array
*/
private function normalize_config( array $config ): array {
return wp_parse_args( $config, [
'item_key' => '',
'item_type' => self::TYPE_PLUGIN,
'item_slug' => '',
'item_did' => '',
'label' => '',
'mode' => self::MODE_DEFAULT,
'source_ids' => [],
'pinned' => false,
'signature_required' => false,
'allow_unsigned' => true,
'allow_prerelease' => false,
'min_version' => '',
'max_version' => '',
'last_good_version' => '',
'metadata' => [
'preconfigured' => false,
'installed' => true,
],
'created_at' => current_time( 'mysql' ),
'updated_at' => current_time( 'mysql' ),
] );
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->cached_configs = null;
}
/**
* 清除缓存
*/
public function clear_cache(): void {
$this->cached_configs = null;
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,51 +17,51 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class Loader {

/**
* 命名空间前缀
*
* @var string
*/
private static string $namespace_prefix = 'WPBridge\\';
/**
* 命名空间前缀
*
* @var string
*/
private static string $namespace_prefix = 'WPBridge\\';

/**
* 基础目录
*
* @var string
*/
private static string $base_dir = '';
/**
* 基础目录
*
* @var string
*/
private static string $base_dir = '';

/**
* 注册自动加载器
*/
public static function register(): void {
self::$base_dir = WPBRIDGE_PATH . 'includes/';
spl_autoload_register( array( __CLASS__, 'autoload' ) );
}
/**
* 注册自动加载器
*/
public static function register(): void {
self::$base_dir = WPBRIDGE_PATH . 'includes/';
spl_autoload_register( [ __CLASS__, 'autoload' ] );
}

/**
* 自动加载类
*
* @param string $class 完整类名
*/
public static function autoload( string $class ): void {
// 检查是否是我们的命名空间
$len = strlen( self::$namespace_prefix );
if ( strncmp( self::$namespace_prefix, $class, $len ) !== 0 ) {
return;
}
/**
* 自动加载类
*
* @param string $class 完整类名
*/
public static function autoload( string $class ): void {
// 检查是否是我们的命名空间
$len = strlen( self::$namespace_prefix );
if ( strncmp( self::$namespace_prefix, $class, $len ) !== 0 ) {
return;
}

// 获取相对类名
$relative_class = substr( $class, $len );
// 获取相对类名
$relative_class = substr( $class, $len );

// 转换为文件路径
$file = self::$base_dir . str_replace( '\\', '/', $relative_class ) . '.php';
// 转换为文件路径
$file = self::$base_dir . str_replace( '\\', '/', $relative_class ) . '.php';

// 如果文件存在则加载
if ( file_exists( $file ) ) {
require_once $file;
}
}
// 如果文件存在则加载
if ( file_exists( $file ) ) {
require_once $file;
}
}
}

// 立即注册自动加载器

View file

@ -9,7 +9,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,162 +17,159 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class Logger {

/**
* 日志级别
*/
const LEVEL_DEBUG = 'debug';
const LEVEL_INFO = 'info';
const LEVEL_WARNING = 'warning';
const LEVEL_ERROR = 'error';
/**
* 日志级别
*/
const LEVEL_DEBUG = 'debug';
const LEVEL_INFO = 'info';
const LEVEL_WARNING = 'warning';
const LEVEL_ERROR = 'error';

/**
* 选项名称
*/
const OPTION_LOGS = 'wpbridge_logs';
/**
* 选项名称
*/
const OPTION_LOGS = 'wpbridge_logs';

/**
* 最大日志条数
*/
const MAX_LOGS = 100;
/**
* 最大日志条数
*/
const MAX_LOGS = 100;

/**
* 设置实例
*
* @var Settings|null
*/
private static ?Settings $settings = null;
/**
* 设置实例
*
* @var Settings|null
*/
private static ?Settings $settings = null;

/**
* 设置 Settings 实例
*
* @param Settings $settings
*/
public static function set_settings( Settings $settings ): void {
self::$settings = $settings;
}
/**
* 设置 Settings 实例
*
* @param Settings $settings
*/
public static function set_settings( Settings $settings ): void {
self::$settings = $settings;
}

/**
* 记录调试日志
*
* @param string $message 消息
* @param array $context 上下文
*/
public static function debug( string $message, array $context = array() ): void {
self::log( self::LEVEL_DEBUG, $message, $context );
}
/**
* 记录调试日志
*
* @param string $message 消息
* @param array $context 上下文
*/
public static function debug( string $message, array $context = [] ): void {
self::log( self::LEVEL_DEBUG, $message, $context );
}

/**
* 记录信息日志
*
* @param string $message 消息
* @param array $context 上下文
*/
public static function info( string $message, array $context = array() ): void {
self::log( self::LEVEL_INFO, $message, $context );
}
/**
* 记录信息日志
*
* @param string $message 消息
* @param array $context 上下文
*/
public static function info( string $message, array $context = [] ): void {
self::log( self::LEVEL_INFO, $message, $context );
}

/**
* 记录警告日志
*
* @param string $message 消息
* @param array $context 上下文
*/
public static function warning( string $message, array $context = array() ): void {
self::log( self::LEVEL_WARNING, $message, $context );
}
/**
* 记录警告日志
*
* @param string $message 消息
* @param array $context 上下文
*/
public static function warning( string $message, array $context = [] ): void {
self::log( self::LEVEL_WARNING, $message, $context );
}

/**
* 记录错误日志
*
* @param string $message 消息
* @param array $context 上下文
*/
public static function error( string $message, array $context = array() ): void {
self::log( self::LEVEL_ERROR, $message, $context );
}
/**
* 记录错误日志
*
* @param string $message 消息
* @param array $context 上下文
*/
public static function error( string $message, array $context = [] ): void {
self::log( self::LEVEL_ERROR, $message, $context );
}

/**
* 记录日志
*
* @param string $level 日志级别
* @param string $message 消息
* @param array $context 上下文
*/
public static function log( string $level, string $message, array $context = array() ): void {
// 检查是否启用调试模式(错误日志始终记录)
if ( $level !== self::LEVEL_ERROR ) {
if ( null === self::$settings ) {
self::$settings = new Settings();
}
if ( ! self::$settings->is_debug() ) {
return;
}
}
/**
* 记录日志
*
* @param string $level 日志级别
* @param string $message 消息
* @param array $context 上下文
*/
public static function log( string $level, string $message, array $context = [] ): void {
// 检查是否启用调试模式(错误日志始终记录)
if ( $level !== self::LEVEL_ERROR ) {
if ( null === self::$settings ) {
self::$settings = new Settings();
}
if ( ! self::$settings->is_debug() ) {
return;
}
}

$logs = get_option( self::OPTION_LOGS, array() );
$logs = get_option( self::OPTION_LOGS, [] );

// 添加新日志
$logs[] = array(
'time' => current_time( 'mysql' ),
'level' => $level,
'message' => $message,
'context' => self::sanitize_context( $context ),
);
// 添加新日志
$logs[] = [
'time' => current_time( 'mysql' ),
'level' => $level,
'message' => $message,
'context' => self::sanitize_context( $context ),
];

// 限制日志数量
if ( count( $logs ) > self::MAX_LOGS ) {
$logs = array_slice( $logs, -self::MAX_LOGS );
}
// 限制日志数量
if ( count( $logs ) > self::MAX_LOGS ) {
$logs = array_slice( $logs, -self::MAX_LOGS );
}

update_option( self::OPTION_LOGS, $logs, false );
}
update_option( self::OPTION_LOGS, $logs, false );
}

/**
* 清理上下文数据(脱敏)
*
* @param array $context 上下文
* @return array
*/
private static function sanitize_context( array $context ): array {
$sensitive_keys = array( 'auth_token', 'api_key', 'password', 'secret' );
/**
* 清理上下文数据(脱敏)
*
* @param array $context 上下文
* @return array
*/
private static function sanitize_context( array $context ): array {
$sensitive_keys = [ 'auth_token', 'api_key', 'password', 'secret' ];

foreach ( $context as $key => $value ) {
if ( in_array( strtolower( $key ), $sensitive_keys, true ) ) {
$context[ $key ] = '***REDACTED***';
} elseif ( is_array( $value ) ) {
$context[ $key ] = self::sanitize_context( $value );
}
}
foreach ( $context as $key => $value ) {
if ( in_array( strtolower( $key ), $sensitive_keys, true ) ) {
$context[ $key ] = '***REDACTED***';
} elseif ( is_array( $value ) ) {
$context[ $key ] = self::sanitize_context( $value );
}
}

return $context;
}
return $context;
}

/**
* 获取所有日志
*
* @param string|null $level 过滤级别
* @return array
*/
public static function get_logs( ?string $level = null ): array {
$logs = get_option( self::OPTION_LOGS, array() );
/**
* 获取所有日志
*
* @param string|null $level 过滤级别
* @return array
*/
public static function get_logs( ?string $level = null ): array {
$logs = get_option( self::OPTION_LOGS, [] );

if ( null !== $level ) {
$logs = array_filter(
$logs,
function ( $log ) use ( $level ) {
return $log['level'] === $level;
}
);
}
if ( null !== $level ) {
$logs = array_filter( $logs, function( $log ) use ( $level ) {
return $log['level'] === $level;
} );
}

// 按时间倒序
return array_reverse( $logs );
}
// 按时间倒序
return array_reverse( $logs );
}

/**
* 清除所有日志
*/
public static function clear(): void {
delete_option( self::OPTION_LOGS );
}
/**
* 清除所有日志
*/
public static function clear(): void {
delete_option( self::OPTION_LOGS );
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -23,290 +23,275 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class RemoteConfig {

/**
* 远程配置 URL
*/
const CONFIG_URL = 'https://wpcy.com/api/bridge/commercial-config.json';
/**
* 远程配置 URL
*/
const CONFIG_URL = 'https://wpcy.com/api/bridge/commercial-config.json';

/**
* 缓存键名
*/
const CACHE_KEY = 'wpbridge_remote_config';
/**
* 缓存键名
*/
const CACHE_KEY = 'wpbridge_remote_config';

/**
* 缓存时间(秒)- 默认 12 小时
*/
const CACHE_TTL = 43200;
/**
* 缓存时间(秒)- 默认 12 小时
*/
const CACHE_TTL = 43200;

/**
* 单例实例
*
* @var RemoteConfig|null
*/
private static $instance = null;
/**
* 单例实例
*
* @var RemoteConfig|null
*/
private static $instance = null;

/**
* 配置数据
*
* @var array|null
*/
private $config = null;
/**
* 配置数据
*
* @var array|null
*/
private $config = null;

/**
* 获取单例实例
*
* @return RemoteConfig
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 获取单例实例
*
* @return RemoteConfig
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* 构造函数
*/
private function __construct() {
$this->load_config();
}
/**
* 构造函数
*/
private function __construct() {
$this->load_config();
}

/**
* 加载配置(优先从缓存)
*/
private function load_config() {
// 尝试从缓存加载
$cached = get_transient( self::CACHE_KEY );
if ( $cached !== false ) {
$this->config = $cached;
return;
}
/**
* 加载配置(优先从缓存)
*/
private function load_config() {
// 尝试从缓存加载
$cached = get_transient( self::CACHE_KEY );
if ( $cached !== false ) {
$this->config = $cached;
return;
}

// 尝试从远程获取
$remote = $this->fetch_remote_config();
if ( $remote !== null ) {
$this->config = $remote;
set_transient( self::CACHE_KEY, $remote, self::CACHE_TTL );
return;
}
// 尝试从远程获取
$remote = $this->fetch_remote_config();
if ( $remote !== null ) {
$this->config = $remote;
set_transient( self::CACHE_KEY, $remote, self::CACHE_TTL );
return;
}

// 降级到内置配置
$this->config = $this->get_builtin_config();
}
// 降级到内置配置
$this->config = $this->get_builtin_config();
}

/**
* 从远程获取配置
*
* @return array|null
*/
private function fetch_remote_config() {
$response = wp_remote_get(
self::CONFIG_URL,
array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json',
),
)
);
/**
* 从远程获取配置
*
* @return array|null
*/
private function fetch_remote_config() {
$response = wp_remote_get( self::CONFIG_URL, array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json',
),
) );

if ( is_wp_error( $response ) ) {
Logger::warning(
'远程配置获取失败',
array(
'error' => $response->get_error_message(),
)
);
return null;
}
if ( is_wp_error( $response ) ) {
Logger::warning( '远程配置获取失败', array(
'error' => $response->get_error_message(),
) );
return null;
}

$code = wp_remote_retrieve_response_code( $response );
if ( $code !== 200 ) {
Logger::warning(
'远程配置响应异常',
array(
'code' => $code,
)
);
return null;
}
$code = wp_remote_retrieve_response_code( $response );
if ( $code !== 200 ) {
Logger::warning( '远程配置响应异常', array(
'code' => $code,
) );
return null;
}

$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
Logger::warning(
'远程配置 JSON 解析失败',
array(
'error' => json_last_error_msg(),
)
);
return null;
}
if ( json_last_error() !== JSON_ERROR_NONE ) {
Logger::warning( '远程配置 JSON 解析失败', array(
'error' => json_last_error_msg(),
) );
return null;
}

// 验证配置结构
if ( ! $this->validate_config( $data ) ) {
Logger::warning( '远程配置结构无效' );
return null;
}
// 验证配置结构
if ( ! $this->validate_config( $data ) ) {
Logger::warning( '远程配置结构无效' );
return null;
}

Logger::info(
'远程配置加载成功',
array(
'version' => $data['version'] ?? 'unknown',
)
);
Logger::info( '远程配置加载成功', array(
'version' => $data['version'] ?? 'unknown',
) );

return $data;
}
return $data;
}

/**
* 验证配置结构
*
* @param array $data 配置数据
* @return bool
*/
private function validate_config( $data ) {
if ( ! is_array( $data ) ) {
return false;
}
/**
* 验证配置结构
*
* @param array $data 配置数据
* @return bool
*/
private function validate_config( $data ) {
if ( ! is_array( $data ) ) {
return false;
}

// 必须包含版本号
if ( empty( $data['version'] ) ) {
return false;
}
// 必须包含版本号
if ( empty( $data['version'] ) ) {
return false;
}

return true;
}
return true;
}

/**
* 获取内置配置(降级方案)
*
* @return array
*/
private function get_builtin_config() {
return array(
'version' => '1.0.0-builtin',
'updated_at' => '2026-02-04',
'commercial_plugins' => array(
'elementor-pro',
'wordpress-seo-premium',
'gravityforms',
'advanced-custom-fields-pro',
'wp-rocket',
'wpforms-pro',
'memberpress',
'learndash',
),
'commercial_domains' => array(
'codecanyon.net',
'themeforest.net',
'elegantthemes.com',
),
'license_keywords' => array(
'license_key',
'license_status',
'activate_license',
'deactivate_license',
'check_license',
),
'commercial_frameworks' => array(
'EDD_SL_Plugin_Updater',
'Freemius',
'WC_AM_Client',
'Starter_Plugin_Updater',
),
);
}
/**
* 获取内置配置(降级方案)
*
* @return array
*/
private function get_builtin_config() {
return array(
'version' => '1.0.0-builtin',
'updated_at' => '2026-02-04',
'commercial_plugins' => array(
'elementor-pro',
'wordpress-seo-premium',
'gravityforms',
'advanced-custom-fields-pro',
'wp-rocket',
'wpforms-pro',
'memberpress',
'learndash',
),
'commercial_domains' => array(
'codecanyon.net',
'themeforest.net',
'elegantthemes.com',
),
'license_keywords' => array(
'license_key',
'license_status',
'activate_license',
'deactivate_license',
'check_license',
),
'commercial_frameworks' => array(
'EDD_SL_Plugin_Updater',
'Freemius',
'WC_AM_Client',
'Starter_Plugin_Updater',
),
);
}

/**
* 获取商业插件列表
*
* @return array
*/
public function get_commercial_plugins() {
return $this->config['commercial_plugins'] ?? array();
}
/**
* 获取商业插件列表
*
* @return array
*/
public function get_commercial_plugins() {
return $this->config['commercial_plugins'] ?? array();
}

/**
* 获取商业域名列表
*
* @return array
*/
public function get_commercial_domains() {
return $this->config['commercial_domains'] ?? array();
}
/**
* 获取商业域名列表
*
* @return array
*/
public function get_commercial_domains() {
return $this->config['commercial_domains'] ?? array();
}

/**
* 获取 License 关键词列表
*
* @return array
*/
public function get_license_keywords() {
return $this->config['license_keywords'] ?? array();
}
/**
* 获取 License 关键词列表
*
* @return array
*/
public function get_license_keywords() {
return $this->config['license_keywords'] ?? array();
}

/**
* 获取商业框架列表
*
* @return array
*/
public function get_commercial_frameworks() {
return $this->config['commercial_frameworks'] ?? array();
}
/**
* 获取商业框架列表
*
* @return array
*/
public function get_commercial_frameworks() {
return $this->config['commercial_frameworks'] ?? array();
}

/**
* 获取配置版本
*
* @return string
*/
public function get_version() {
return $this->config['version'] ?? 'unknown';
}
/**
* 获取配置版本
*
* @return string
*/
public function get_version() {
return $this->config['version'] ?? 'unknown';
}

/**
* 获取配置更新时间
*
* @return string
*/
public function get_updated_at() {
return $this->config['updated_at'] ?? 'unknown';
}
/**
* 获取配置更新时间
*
* @return string
*/
public function get_updated_at() {
return $this->config['updated_at'] ?? 'unknown';
}

/**
* 强制刷新配置
*
* @return bool
*/
public function refresh() {
delete_transient( self::CACHE_KEY );
/**
* 强制刷新配置
*
* @return bool
*/
public function refresh() {
delete_transient( self::CACHE_KEY );

$remote = $this->fetch_remote_config();
if ( $remote !== null ) {
$this->config = $remote;
set_transient( self::CACHE_KEY, $remote, self::CACHE_TTL );
return true;
}
$remote = $this->fetch_remote_config();
if ( $remote !== null ) {
$this->config = $remote;
set_transient( self::CACHE_KEY, $remote, self::CACHE_TTL );
return true;
}

// 刷新失败,保持当前配置
return false;
}
// 刷新失败,保持当前配置
return false;
}

/**
* 检查是否使用内置配置
*
* @return bool
*/
public function is_builtin() {
return strpos( $this->get_version(), 'builtin' ) !== false;
}
/**
* 检查是否使用内置配置
*
* @return bool
*/
public function is_builtin() {
return strpos( $this->get_version(), 'builtin' ) !== false;
}

/**
* 获取完整配置
*
* @return array
*/
public function get_all() {
return $this->config;
}
/**
* 获取完整配置
*
* @return array
*/
public function get_all() {
return $this->config;
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,346 +17,334 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class Settings {

/**
* 选项名称
*/
const OPTION_SOURCES = 'wpbridge_sources';
const OPTION_SETTINGS = 'wpbridge_settings';
const OPTION_AI_SETTINGS = 'wpbridge_ai_settings';
/**
* 选项名称
*/
const OPTION_SOURCES = 'wpbridge_sources';
const OPTION_SETTINGS = 'wpbridge_settings';
const OPTION_AI_SETTINGS = 'wpbridge_ai_settings';

/**
* 默认设置
*
* @var array
*/
private array $defaults = array(
'debug_mode' => false,
'cache_ttl' => 43200, // 12 小时
'request_timeout' => 10, // 秒
'fallback_enabled' => true,
);
/**
* 默认设置
*
* @var array
*/
private array $defaults = [
'debug_mode' => false,
'cache_ttl' => 43200, // 12 小时
'request_timeout' => 10, // 秒
'fallback_enabled' => true,
];

/**
* 缓存的设置
*
* @var array|null
*/
private ?array $cached_settings = null;
/**
* 缓存的设置
*
* @var array|null
*/
private ?array $cached_settings = null;

/**
* 缓存的更新源
*
* @var array|null
*/
private ?array $cached_sources = null;
/**
* 缓存的更新源
*
* @var array|null
*/
private ?array $cached_sources = null;

/**
* 初始化默认设置
*/
public function init_defaults(): void {
// 初始化基础设置
if ( false === get_option( self::OPTION_SETTINGS ) ) {
update_option( self::OPTION_SETTINGS, $this->defaults );
}
/**
* 初始化默认设置
*/
public function init_defaults(): void {
// 初始化基础设置
if ( false === get_option( self::OPTION_SETTINGS ) ) {
update_option( self::OPTION_SETTINGS, $this->defaults );
}

// 初始化更新源(包含预置源)
if ( false === get_option( self::OPTION_SOURCES ) ) {
update_option( self::OPTION_SOURCES, $this->get_preset_sources() );
}
// 初始化更新源(包含预置源)
if ( false === get_option( self::OPTION_SOURCES ) ) {
update_option( self::OPTION_SOURCES, $this->get_preset_sources() );
}

// 初始化 AI 设置
if ( false === get_option( self::OPTION_AI_SETTINGS ) ) {
update_option(
self::OPTION_AI_SETTINGS,
array(
'enabled' => false,
'mode' => 'disabled',
'whitelist' => array( 'api.openai.com', 'api.anthropic.com' ),
'custom_endpoint' => '',
)
);
}
}
// 初始化 AI 设置
if ( false === get_option( self::OPTION_AI_SETTINGS ) ) {
update_option( self::OPTION_AI_SETTINGS, [
'enabled' => false,
'mode' => 'disabled',
'whitelist' => [ 'api.openai.com', 'api.anthropic.com' ],
'custom_endpoint' => '',
] );
}
}

/**
* 获取预置更新源
*
* @return array
*/
private function get_preset_sources(): array {
return array(
array(
'id' => 'wenpai-open',
'name' => __( '文派开源更新源', 'wpbridge' ),
'type' => 'json',
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
'slug' => '',
'item_type' => 'plugin',
'auth_token' => '',
'enabled' => true,
'priority' => 10,
'is_preset' => true,
'metadata' => array(),
),
);
}
/**
* 获取预置更新源
*
* @return array
*/
private function get_preset_sources(): array {
return [
[
'id' => 'wenpai-open',
'name' => __( '文派开源更新源', 'wpbridge' ),
'type' => 'json',
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
'slug' => '',
'item_type' => 'plugin',
'auth_token' => '',
'enabled' => true,
'priority' => 10,
'is_preset' => true,
'metadata' => [],
],
];
}

/**
* 获取所有设置
*
* @return array
*/
public function get_all(): array {
if ( null === $this->cached_settings ) {
$this->cached_settings = wp_parse_args(
get_option( self::OPTION_SETTINGS, array() ),
$this->defaults
);
}
return $this->cached_settings;
}
/**
* 获取所有设置
*
* @return array
*/
public function get_all(): array {
if ( null === $this->cached_settings ) {
$this->cached_settings = wp_parse_args(
get_option( self::OPTION_SETTINGS, [] ),
$this->defaults
);
}
return $this->cached_settings;
}

/**
* 获取单个设置
*
* @param string $key 设置键
* @param mixed $default 默认值
* @return mixed
*/
public function get( string $key, $default = null ) {
$settings = $this->get_all();
return $settings[ $key ] ?? $default;
}
/**
* 获取单个设置
*
* @param string $key 设置键
* @param mixed $default 默认值
* @return mixed
*/
public function get( string $key, $default = null ) {
$settings = $this->get_all();
return $settings[ $key ] ?? $default;
}

/**
* 更新设置
*
* @param string $key 设置键
* @param mixed $value 设置值
* @return bool
*/
public function set( string $key, $value ): bool {
$settings = $this->get_all();
$settings[ $key ] = $value;
/**
* 更新设置
*
* @param string $key 设置键
* @param mixed $value 设置值
* @return bool
*/
public function set( string $key, $value ): bool {
$settings = $this->get_all();
$settings[ $key ] = $value;

$this->cached_settings = $settings;
return update_option( self::OPTION_SETTINGS, $settings );
}
$this->cached_settings = $settings;
return update_option( self::OPTION_SETTINGS, $settings );
}

/**
* 批量更新设置
*
* @param array $settings 设置数组
* @return bool
*/
public function update( array $settings ): bool {
$current = $this->get_all();
$merged = wp_parse_args( $settings, $current );
/**
* 批量更新设置
*
* @param array $settings 设置数组
* @return bool
*/
public function update( array $settings ): bool {
$current = $this->get_all();
$merged = wp_parse_args( $settings, $current );

$this->cached_settings = $merged;
return update_option( self::OPTION_SETTINGS, $merged );
}
$this->cached_settings = $merged;
return update_option( self::OPTION_SETTINGS, $merged );
}

/**
* 获取所有更新源
*
* @return array
*/
public function get_sources(): array {
if ( null === $this->cached_sources ) {
$this->cached_sources = get_option( self::OPTION_SOURCES, array() );
}
return $this->cached_sources;
}
/**
* 获取所有更新源
*
* @return array
*/
public function get_sources(): array {
if ( null === $this->cached_sources ) {
$this->cached_sources = get_option( self::OPTION_SOURCES, [] );
}
return $this->cached_sources;
}

/**
* 获取启用的更新源
*
* @return array
*/
public function get_enabled_sources(): array {
$sources = $this->get_sources();
return array_filter(
$sources,
function ( $source ) {
return ! empty( $source['enabled'] );
}
);
}
/**
* 获取启用的更新源
*
* @return array
*/
public function get_enabled_sources(): array {
$sources = $this->get_sources();
return array_filter( $sources, function( $source ) {
return ! empty( $source['enabled'] );
} );
}

/**
* 获取单个更新源
*
* @param string $id 源 ID
* @return array|null
*/
public function get_source( string $id ): ?array {
$sources = $this->get_sources();
foreach ( $sources as $source ) {
if ( $source['id'] === $id ) {
return $source;
}
}
return null;
}
/**
* 获取单个更新源
*
* @param string $id 源 ID
* @return array|null
*/
public function get_source( string $id ): ?array {
$sources = $this->get_sources();
foreach ( $sources as $source ) {
if ( $source['id'] === $id ) {
return $source;
}
}
return null;
}

/**
* 添加更新源
*
* @param array $source 源数据
* @return bool
*/
public function add_source( array $source ): bool {
$sources = $this->get_sources();
/**
* 添加更新源
*
* @param array $source 源数据
* @return bool
*/
public function add_source( array $source ): bool {
$sources = $this->get_sources();

// 生成唯一 ID
if ( empty( $source['id'] ) ) {
$source['id'] = 'source_' . wp_generate_uuid4();
}
// 生成唯一 ID
if ( empty( $source['id'] ) ) {
$source['id'] = 'source_' . wp_generate_uuid4();
}

// 设置默认值
$source = wp_parse_args(
$source,
array(
'name' => '',
'type' => 'json',
'api_url' => '',
'slug' => '',
'item_type' => 'plugin',
'auth_token' => '',
'enabled' => true,
'priority' => 50,
'is_preset' => false,
'metadata' => array(),
)
);
// 设置默认值
$source = wp_parse_args( $source, [
'name' => '',
'type' => 'json',
'api_url' => '',
'slug' => '',
'item_type' => 'plugin',
'auth_token' => '',
'enabled' => true,
'priority' => 50,
'is_preset' => false,
'metadata' => [],
] );

$sources[] = $source;
$sources[] = $source;

$this->cached_sources = $sources;
return update_option( self::OPTION_SOURCES, $sources );
}
$this->cached_sources = $sources;
return update_option( self::OPTION_SOURCES, $sources );
}

/**
* 更新更新源
*
* @param string $id 源 ID
* @param array $data 更新数据
* @return bool
*/
public function update_source( string $id, array $data ): bool {
$sources = $this->get_sources();
/**
* 更新更新源
*
* @param string $id 源 ID
* @param array $data 更新数据
* @return bool
*/
public function update_source( string $id, array $data ): bool {
$sources = $this->get_sources();

foreach ( $sources as $index => $source ) {
if ( $source['id'] === $id ) {
$sources[ $index ] = wp_parse_args( $data, $source );
$this->cached_sources = $sources;
return update_option( self::OPTION_SOURCES, $sources );
}
}
foreach ( $sources as $index => $source ) {
if ( $source['id'] === $id ) {
$sources[ $index ] = wp_parse_args( $data, $source );
$this->cached_sources = $sources;
return update_option( self::OPTION_SOURCES, $sources );
}
}

return false;
}
return false;
}

/**
* 删除更新源
*
* @param string $id 源 ID
* @return bool
*/
public function delete_source( string $id ): bool {
$sources = $this->get_sources();
/**
* 删除更新源
*
* @param string $id 源 ID
* @return bool
*/
public function delete_source( string $id ): bool {
$sources = $this->get_sources();

foreach ( $sources as $index => $source ) {
if ( $source['id'] === $id ) {
// 不允许删除预置源
if ( ! empty( $source['is_preset'] ) ) {
return false;
}
foreach ( $sources as $index => $source ) {
if ( $source['id'] === $id ) {
// 不允许删除预置源
if ( ! empty( $source['is_preset'] ) ) {
return false;
}

unset( $sources[ $index ] );
$sources = array_values( $sources ); // 重新索引
unset( $sources[ $index ] );
$sources = array_values( $sources ); // 重新索引

$this->cached_sources = $sources;
return update_option( self::OPTION_SOURCES, $sources );
}
}
$this->cached_sources = $sources;
return update_option( self::OPTION_SOURCES, $sources );
}
}

return false;
}
return false;
}

/**
* 启用/禁用更新源
*
* @param string $id 源 ID
* @param bool $enabled 是否启用
* @return bool
*/
public function toggle_source( string $id, bool $enabled ): bool {
return $this->update_source( $id, array( 'enabled' => $enabled ) );
}
/**
* 启用/禁用更新源
*
* @param string $id 源 ID
* @param bool $enabled 是否启用
* @return bool
*/
public function toggle_source( string $id, bool $enabled ): bool {
return $this->update_source( $id, [ 'enabled' => $enabled ] );
}

/**
* 获取 AI 设置
*
* @return array
*/
public function get_ai_settings(): array {
return get_option(
self::OPTION_AI_SETTINGS,
array(
'enabled' => false,
'mode' => 'disabled',
'whitelist' => array( 'api.openai.com', 'api.anthropic.com' ),
'custom_endpoint' => '',
)
);
}
/**
* 获取 AI 设置
*
* @return array
*/
public function get_ai_settings(): array {
return get_option( self::OPTION_AI_SETTINGS, [
'enabled' => false,
'mode' => 'disabled',
'whitelist' => [ 'api.openai.com', 'api.anthropic.com' ],
'custom_endpoint' => '',
] );
}

/**
* 更新 AI 设置
*
* @param array $settings AI 设置
* @return bool
*/
public function update_ai_settings( array $settings ): bool {
$current = $this->get_ai_settings();
$merged = wp_parse_args( $settings, $current );
return update_option( self::OPTION_AI_SETTINGS, $merged );
}
/**
* 更新 AI 设置
*
* @param array $settings AI 设置
* @return bool
*/
public function update_ai_settings( array $settings ): bool {
$current = $this->get_ai_settings();
$merged = wp_parse_args( $settings, $current );
return update_option( self::OPTION_AI_SETTINGS, $merged );
}

/**
* 是否启用调试模式
*
* @return bool
*/
public function is_debug(): bool {
return (bool) $this->get( 'debug_mode', false );
}
/**
* 是否启用调试模式
*
* @return bool
*/
public function is_debug(): bool {
return (bool) $this->get( 'debug_mode', false );
}

/**
* 获取缓存 TTL
*
* @return int
*/
public function get_cache_ttl(): int {
return (int) $this->get( 'cache_ttl', 43200 );
}
/**
* 获取缓存 TTL
*
* @return int
*/
public function get_cache_ttl(): int {
return (int) $this->get( 'cache_ttl', 43200 );
}

/**
* 获取请求超时时间
*
* @return int
*/
public function get_request_timeout(): int {
return (int) $this->get( 'request_timeout', 10 );
}
/**
* 获取请求超时时间
*
* @return int
*/
public function get_request_timeout(): int {
return (int) $this->get( 'request_timeout', 10 );
}

/**
* 清除设置缓存
*/
public function clear_cache(): void {
$this->cached_settings = null;
$this->cached_sources = null;
}
/**
* 清除设置缓存
*/
public function clear_cache(): void {
$this->cached_settings = null;
$this->cached_sources = null;
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\Cache\HealthChecker;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -19,288 +19,288 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class SiteHealth {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->init_hooks();
}
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 添加健康检查测试
add_filter( 'site_status_tests', array( $this, 'add_tests' ) );
/**
* 初始化钩子
*/
private function init_hooks(): void {
// 添加健康检查测试
add_filter( 'site_status_tests', [ $this, 'add_tests' ] );

// 添加调试信息
add_filter( 'debug_information', array( $this, 'add_debug_info' ) );
}
// 添加调试信息
add_filter( 'debug_information', [ $this, 'add_debug_info' ] );
}

/**
* 添加健康检查测试
*
* @param array $tests 测试列表
* @return array
*/
public function add_tests( array $tests ): array {
$tests['direct']['wpbridge_sources'] = array(
'label' => __( 'WPBridge 更新源状态', 'wpbridge' ),
'test' => array( $this, 'test_sources' ),
);
/**
* 添加健康检查测试
*
* @param array $tests 测试列表
* @return array
*/
public function add_tests( array $tests ): array {
$tests['direct']['wpbridge_sources'] = [
'label' => __( 'WPBridge 更新源状态', 'wpbridge' ),
'test' => [ $this, 'test_sources' ],
];

$tests['direct']['wpbridge_config'] = array(
'label' => __( 'WPBridge 配置检查', 'wpbridge' ),
'test' => array( $this, 'test_config' ),
);
$tests['direct']['wpbridge_config'] = [
'label' => __( 'WPBridge 配置检查', 'wpbridge' ),
'test' => [ $this, 'test_config' ],
];

return $tests;
}
return $tests;
}

/**
* 测试更新源状态
*
* @return array
*/
public function test_sources(): array {
$sources = $this->settings->get_enabled_sources();
$health_checker = new HealthChecker( $this->settings );
/**
* 测试更新源状态
*
* @return array
*/
public function test_sources(): array {
$sources = $this->settings->get_enabled_sources();
$health_checker = new HealthChecker( $this->settings );

$healthy = 0;
$degraded = 0;
$failed = 0;
$failed_sources = array();
$healthy = 0;
$degraded = 0;
$failed = 0;
$failed_sources = [];

foreach ( $sources as $source ) {
$status = $health_checker->check( $source );
foreach ( $sources as $source ) {
$status = $health_checker->check( $source );

if ( $status['status'] === 'healthy' ) {
++$healthy;
} elseif ( $status['status'] === 'degraded' ) {
++$degraded;
} else {
++$failed;
$failed_sources[] = $source['name'];
}
}
if ( $status['status'] === 'healthy' ) {
$healthy++;
} elseif ( $status['status'] === 'degraded' ) {
$degraded++;
} else {
$failed++;
$failed_sources[] = $source['name'];
}
}

$total = count( $sources );
$total = count( $sources );

if ( $total === 0 ) {
return array(
'label' => __( 'WPBridge: 未配置更新源', 'wpbridge' ),
'status' => 'recommended',
'badge' => array(
'label' => __( '推荐', 'wpbridge' ),
'color' => 'orange',
),
'description' => sprintf(
'<p>%s</p>',
__( '您尚未配置任何自定义更新源。如果您需要使用自定义更新源,请在 WPBridge 设置中添加。', 'wpbridge' )
),
'actions' => sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=wpbridge' ),
__( '配置更新源', 'wpbridge' )
),
'test' => 'wpbridge_sources',
);
}
if ( $total === 0 ) {
return [
'label' => __( 'WPBridge: 未配置更新源', 'wpbridge' ),
'status' => 'recommended',
'badge' => [
'label' => __( '推荐', 'wpbridge' ),
'color' => 'orange',
],
'description' => sprintf(
'<p>%s</p>',
__( '您尚未配置任何自定义更新源。如果您需要使用自定义更新源,请在 WPBridge 设置中添加。', 'wpbridge' )
),
'actions' => sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=wpbridge' ),
__( '配置更新源', 'wpbridge' )
),
'test' => 'wpbridge_sources',
];
}

if ( $failed > 0 ) {
return array(
'label' => sprintf(
__( 'WPBridge: %d 个更新源不可用', 'wpbridge' ),
$failed
),
'status' => 'critical',
'badge' => array(
'label' => __( '错误', 'wpbridge' ),
'color' => 'red',
),
'description' => sprintf(
'<p>%s</p><p>%s: %s</p>',
__( '部分更新源无法连接,这可能导致插件/主题无法正常更新。', 'wpbridge' ),
__( '不可用的更新源', 'wpbridge' ),
implode( ', ', $failed_sources )
),
'actions' => sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=wpbridge&tab=diagnostics' ),
__( '查看诊断', 'wpbridge' )
),
'test' => 'wpbridge_sources',
);
}
if ( $failed > 0 ) {
return [
'label' => sprintf(
__( 'WPBridge: %d 个更新源不可用', 'wpbridge' ),
$failed
),
'status' => 'critical',
'badge' => [
'label' => __( '错误', 'wpbridge' ),
'color' => 'red',
],
'description' => sprintf(
'<p>%s</p><p>%s: %s</p>',
__( '部分更新源无法连接,这可能导致插件/主题无法正常更新。', 'wpbridge' ),
__( '不可用的更新源', 'wpbridge' ),
implode( ', ', $failed_sources )
),
'actions' => sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=wpbridge&tab=diagnostics' ),
__( '查看诊断', 'wpbridge' )
),
'test' => 'wpbridge_sources',
];
}

if ( $degraded > 0 ) {
return array(
'label' => sprintf(
__( 'WPBridge: %d 个更新源响应较慢', 'wpbridge' ),
$degraded
),
'status' => 'recommended',
'badge' => array(
'label' => __( '警告', 'wpbridge' ),
'color' => 'orange',
),
'description' => sprintf(
'<p>%s</p>',
__( '部分更新源响应时间较长,可能影响更新检查速度。', 'wpbridge' )
),
'actions' => sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=wpbridge&tab=diagnostics' ),
__( '查看诊断', 'wpbridge' )
),
'test' => 'wpbridge_sources',
);
}
if ( $degraded > 0 ) {
return [
'label' => sprintf(
__( 'WPBridge: %d 个更新源响应较慢', 'wpbridge' ),
$degraded
),
'status' => 'recommended',
'badge' => [
'label' => __( '警告', 'wpbridge' ),
'color' => 'orange',
],
'description' => sprintf(
'<p>%s</p>',
__( '部分更新源响应时间较长,可能影响更新检查速度。', 'wpbridge' )
),
'actions' => sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=wpbridge&tab=diagnostics' ),
__( '查看诊断', 'wpbridge' )
),
'test' => 'wpbridge_sources',
];
}

return array(
'label' => sprintf(
__( 'WPBridge: 所有 %d 个更新源正常', 'wpbridge' ),
$healthy
),
'status' => 'good',
'badge' => array(
'label' => __( '正常', 'wpbridge' ),
'color' => 'green',
),
'description' => sprintf(
'<p>%s</p>',
__( '所有配置的更新源都可以正常连接。', 'wpbridge' )
),
'test' => 'wpbridge_sources',
);
}
return [
'label' => sprintf(
__( 'WPBridge: 所有 %d 个更新源正常', 'wpbridge' ),
$healthy
),
'status' => 'good',
'badge' => [
'label' => __( '正常', 'wpbridge' ),
'color' => 'green',
],
'description' => sprintf(
'<p>%s</p>',
__( '所有配置的更新源都可以正常连接。', 'wpbridge' )
),
'test' => 'wpbridge_sources',
];
}

/**
* 测试配置
*
* @return array
*/
public function test_config(): array {
$issues = array();
/**
* 测试配置
*
* @return array
*/
public function test_config(): array {
$issues = [];

// 检查调试模式
if ( $this->settings->is_debug() ) {
$issues[] = __( '调试模式已启用,建议在生产环境中关闭', 'wpbridge' );
}
// 检查调试模式
if ( $this->settings->is_debug() ) {
$issues[] = __( '调试模式已启用,建议在生产环境中关闭', 'wpbridge' );
}

// 检查缓存时间
$cache_ttl = $this->settings->get_cache_ttl();
if ( $cache_ttl < 3600 ) {
$issues[] = __( '缓存时间设置过短,可能导致频繁请求', 'wpbridge' );
}
// 检查缓存时间
$cache_ttl = $this->settings->get_cache_ttl();
if ( $cache_ttl < 3600 ) {
$issues[] = __( '缓存时间设置过短,可能导致频繁请求', 'wpbridge' );
}

// 检查备份功能
if ( ! $this->settings->get( 'backup_enabled', true ) ) {
$issues[] = __( '更新前备份已禁用,建议启用以便回滚', 'wpbridge' );
}
// 检查备份功能
if ( ! $this->settings->get( 'backup_enabled', true ) ) {
$issues[] = __( '更新前备份已禁用,建议启用以便回滚', 'wpbridge' );
}

// 检查 ZipArchive
if ( ! class_exists( 'ZipArchive' ) ) {
$issues[] = __( 'PHP ZipArchive 扩展未安装,备份功能将不可用', 'wpbridge' );
}
// 检查 ZipArchive
if ( ! class_exists( 'ZipArchive' ) ) {
$issues[] = __( 'PHP ZipArchive 扩展未安装,备份功能将不可用', 'wpbridge' );
}

if ( ! empty( $issues ) ) {
return array(
'label' => __( 'WPBridge: 配置需要优化', 'wpbridge' ),
'status' => 'recommended',
'badge' => array(
'label' => __( '建议', 'wpbridge' ),
'color' => 'orange',
),
'description' => sprintf(
'<p>%s</p><ul><li>%s</li></ul>',
__( '发现以下配置问题:', 'wpbridge' ),
implode( '</li><li>', $issues )
),
'actions' => sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=wpbridge&tab=settings' ),
__( '调整设置', 'wpbridge' )
),
'test' => 'wpbridge_config',
);
}
if ( ! empty( $issues ) ) {
return [
'label' => __( 'WPBridge: 配置需要优化', 'wpbridge' ),
'status' => 'recommended',
'badge' => [
'label' => __( '建议', 'wpbridge' ),
'color' => 'orange',
],
'description' => sprintf(
'<p>%s</p><ul><li>%s</li></ul>',
__( '发现以下配置问题:', 'wpbridge' ),
implode( '</li><li>', $issues )
),
'actions' => sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=wpbridge&tab=settings' ),
__( '调整设置', 'wpbridge' )
),
'test' => 'wpbridge_config',
];
}

return array(
'label' => __( 'WPBridge: 配置正常', 'wpbridge' ),
'status' => 'good',
'badge' => array(
'label' => __( '正常', 'wpbridge' ),
'color' => 'green',
),
'description' => sprintf(
'<p>%s</p>',
__( 'WPBridge 配置正确,所有功能正常运行。', 'wpbridge' )
),
'test' => 'wpbridge_config',
);
}
return [
'label' => __( 'WPBridge: 配置正常', 'wpbridge' ),
'status' => 'good',
'badge' => [
'label' => __( '正常', 'wpbridge' ),
'color' => 'green',
],
'description' => sprintf(
'<p>%s</p>',
__( 'WPBridge 配置正确,所有功能正常运行。', 'wpbridge' )
),
'test' => 'wpbridge_config',
];
}

/**
* 添加调试信息
*
* @param array $info 调试信息
* @return array
*/
public function add_debug_info( array $info ): array {
$sources = $this->settings->get_sources();
$enabled_sources = $this->settings->get_enabled_sources();
$version_lock = VersionLock::get_instance();
$backup_manager = BackupManager::get_instance();
/**
* 添加调试信息
*
* @param array $info 调试信息
* @return array
*/
public function add_debug_info( array $info ): array {
$sources = $this->settings->get_sources();
$enabled_sources = $this->settings->get_enabled_sources();
$version_lock = VersionLock::get_instance();
$backup_manager = BackupManager::get_instance();

$info['wpbridge'] = array(
'label' => 'WPBridge',
'fields' => array(
'version' => array(
'label' => __( '版本', 'wpbridge' ),
'value' => WPBRIDGE_VERSION,
),
'total_sources' => array(
'label' => __( '总更新源数', 'wpbridge' ),
'value' => count( $sources ),
),
'enabled_sources' => array(
'label' => __( '已启用更新源', 'wpbridge' ),
'value' => count( $enabled_sources ),
),
'locked_items' => array(
'label' => __( '已锁定项目数', 'wpbridge' ),
'value' => count( $version_lock->get_all() ),
),
'backup_size' => array(
'label' => __( '备份总大小', 'wpbridge' ),
'value' => size_format( $backup_manager->get_total_size() ),
),
'debug_mode' => array(
'label' => __( '调试模式', 'wpbridge' ),
'value' => $this->settings->is_debug() ? __( '已启用', 'wpbridge' ) : __( '已禁用', 'wpbridge' ),
),
'cache_ttl' => array(
'label' => __( '缓存时间', 'wpbridge' ),
'value' => human_time_diff( 0, $this->settings->get_cache_ttl() ),
),
'backup_enabled' => array(
'label' => __( '更新前备份', 'wpbridge' ),
'value' => $this->settings->get( 'backup_enabled', true ) ? __( '已启用', 'wpbridge' ) : __( '已禁用', 'wpbridge' ),
),
),
);
$info['wpbridge'] = [
'label' => 'WPBridge',
'fields' => [
'version' => [
'label' => __( '版本', 'wpbridge' ),
'value' => WPBRIDGE_VERSION,
],
'total_sources' => [
'label' => __( '总更新源数', 'wpbridge' ),
'value' => count( $sources ),
],
'enabled_sources' => [
'label' => __( '已启用更新源', 'wpbridge' ),
'value' => count( $enabled_sources ),
],
'locked_items' => [
'label' => __( '已锁定项目数', 'wpbridge' ),
'value' => count( $version_lock->get_all() ),
],
'backup_size' => [
'label' => __( '备份总大小', 'wpbridge' ),
'value' => size_format( $backup_manager->get_total_size() ),
],
'debug_mode' => [
'label' => __( '调试模式', 'wpbridge' ),
'value' => $this->settings->is_debug() ? __( '已启用', 'wpbridge' ) : __( '已禁用', 'wpbridge' ),
],
'cache_ttl' => [
'label' => __( '缓存时间', 'wpbridge' ),
'value' => human_time_diff( 0, $this->settings->get_cache_ttl() ),
],
'backup_enabled' => [
'label' => __( '更新前备份', 'wpbridge' ),
'value' => $this->settings->get( 'backup_enabled', true ) ? __( '已启用', 'wpbridge' ) : __( '已禁用', 'wpbridge' ),
],
],
];

return $info;
}
return $info;
}
}

View file

@ -12,7 +12,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -22,344 +22,341 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class SourceRegistry {

/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_source_registry';
/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_source_registry';

/**
* 源类型枚举
*/
const TYPE_WPORG = 'wporg';
const TYPE_FAIR = 'fair';
const TYPE_CUSTOM = 'custom';
const TYPE_GIT = 'git';
const TYPE_MIRROR = 'mirror';
const TYPE_JSON = 'json';
const TYPE_ARKPRESS = 'arkpress';
/**
* 源类型枚举
*/
const TYPE_WPORG = 'wporg';
const TYPE_FAIR = 'fair';
const TYPE_CUSTOM = 'custom';
const TYPE_GIT = 'git';
const TYPE_MIRROR = 'mirror';
const TYPE_JSON = 'json';
const TYPE_ARKPRESS = 'arkpress';

/**
* 签名方案
*/
const SIGNATURE_NONE = 'none';
const SIGNATURE_ED25519 = 'ed25519';
/**
* 签名方案
*/
const SIGNATURE_NONE = 'none';
const SIGNATURE_ED25519 = 'ed25519';

/**
* 认证类型
*/
const AUTH_NONE = 'none';
const AUTH_BASIC = 'basic';
const AUTH_BEARER = 'bearer';
const AUTH_TOKEN = 'token';
/**
* 认证类型
*/
const AUTH_NONE = 'none';
const AUTH_BASIC = 'basic';
const AUTH_BEARER = 'bearer';
const AUTH_TOKEN = 'token';

/**
* 缓存的源列表
*
* @var array|null
*/
private ?array $cached_sources = null;
/**
* 缓存的源列表
*
* @var array|null
*/
private ?array $cached_sources = null;

/**
* 获取所有源
*
* @return array
*/
public function get_all(): array {
if ( null === $this->cached_sources ) {
$this->cached_sources = get_option( self::OPTION_NAME, array() );
$this->ensure_preset_sources();
}
return $this->cached_sources;
}
/**
* 获取所有源
*
* @return array
*/
public function get_all(): array {
if ( null === $this->cached_sources ) {
$this->cached_sources = get_option( self::OPTION_NAME, [] );
$this->ensure_preset_sources();
}
return $this->cached_sources;
}

/**
* 获取启用的源
*
* @return array
*/
public function get_enabled(): array {
return array_filter( $this->get_all(), fn( $s ) => ! empty( $s['enabled'] ) );
}
/**
* 获取启用的源
*
* @return array
*/
public function get_enabled(): array {
return array_filter( $this->get_all(), fn( $s ) => ! empty( $s['enabled'] ) );
}

/**
* 按类型获取源
*
* @param string $type 源类型
* @return array
*/
public function get_by_type( string $type ): array {
return array_filter( $this->get_all(), fn( $s ) => ( $s['type'] ?? '' ) === $type );
}
/**
* 按类型获取源
*
* @param string $type 源类型
* @return array
*/
public function get_by_type( string $type ): array {
return array_filter( $this->get_all(), fn( $s ) => ( $s['type'] ?? '' ) === $type );
}

/**
* 获取单个源
*
* @param string $source_key 源唯一键
* @return array|null
*/
public function get( string $source_key ): ?array {
foreach ( $this->get_all() as $source ) {
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
return $source;
}
}
return null;
}
/**
* 获取单个源
*
* @param string $source_key 源唯一键
* @return array|null
*/
public function get( string $source_key ): ?array {
foreach ( $this->get_all() as $source ) {
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
return $source;
}
}
return null;
}

/**
* 通过 DID 获取源
*
* @param string $did 源 DID
* @return array|null
*/
public function get_by_did( string $did ): ?array {
foreach ( $this->get_all() as $source ) {
if ( ( $source['did'] ?? '' ) === $did ) {
return $source;
}
}
return null;
}
/**
* 通过 DID 获取源
*
* @param string $did 源 DID
* @return array|null
*/
public function get_by_did( string $did ): ?array {
foreach ( $this->get_all() as $source ) {
if ( ( $source['did'] ?? '' ) === $did ) {
return $source;
}
}
return null;
}

/**
* 添加源
*
* @param array $source 源数据
* @return string|false 成功返回 source_key失败返回 false
*/
public function add( array $source ) {
$sources = $this->get_all();
/**
* 添加源
*
* @param array $source 源数据
* @return string|false 成功返回 source_key失败返回 false
*/
public function add( array $source ) {
$sources = $this->get_all();

if ( empty( $source['source_key'] ) ) {
$source['source_key'] = 'src_' . wp_generate_uuid4();
}
if ( empty( $source['source_key'] ) ) {
$source['source_key'] = 'src_' . wp_generate_uuid4();
}

if ( $this->get( $source['source_key'] ) ) {
return false;
}
if ( $this->get( $source['source_key'] ) ) {
return false;
}

$source = $this->normalize_source( $source );
$sources[] = $source;
$this->cached_sources = $sources;
$source = $this->normalize_source( $source );
$sources[] = $source;
$this->cached_sources = $sources;

if ( update_option( self::OPTION_NAME, $sources, false ) ) {
return $source['source_key'];
}
return false;
}
if ( update_option( self::OPTION_NAME, $sources, false ) ) {
return $source['source_key'];
}
return false;
}

/**
* 更新源
*
* @param string $source_key 源唯一键
* @param array $data 更新数据
* @return bool
*/
public function update( string $source_key, array $data ): bool {
$sources = $this->get_all();
/**
* 更新源
*
* @param string $source_key 源唯一键
* @param array $data 更新数据
* @return bool
*/
public function update( string $source_key, array $data ): bool {
$sources = $this->get_all();

foreach ( $sources as $index => $source ) {
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
unset( $data['source_key'] );
$sources[ $index ] = array_merge( $source, $data );
$sources[ $index ]['updated_at'] = current_time( 'mysql' );
$this->cached_sources = $sources;
return update_option( self::OPTION_NAME, $sources, false );
}
}
return false;
}
foreach ( $sources as $index => $source ) {
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
unset( $data['source_key'] );
$sources[ $index ] = array_merge( $source, $data );
$sources[ $index ]['updated_at'] = current_time( 'mysql' );
$this->cached_sources = $sources;
return update_option( self::OPTION_NAME, $sources, false );
}
}
return false;
}

/**
* 删除源
*
* @param string $source_key 源唯一键
* @return bool
*/
public function delete( string $source_key ): bool {
$sources = $this->get_all();
/**
* 删除源
*
* @param string $source_key 源唯一键
* @return bool
*/
public function delete( string $source_key ): bool {
$sources = $this->get_all();

foreach ( $sources as $index => $source ) {
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
if ( ! empty( $source['is_preset'] ) ) {
return false;
}
unset( $sources[ $index ] );
$sources = array_values( $sources );
$this->cached_sources = $sources;
return update_option( self::OPTION_NAME, $sources, false );
}
}
return false;
}
foreach ( $sources as $index => $source ) {
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
if ( ! empty( $source['is_preset'] ) ) {
return false;
}
unset( $sources[ $index ] );
$sources = array_values( $sources );
$this->cached_sources = $sources;
return update_option( self::OPTION_NAME, $sources, false );
}
}
return false;
}

/**
* 启用/禁用源
*
* @param string $source_key 源唯一键
* @param bool $enabled 是否启用
* @return bool
*/
public function toggle( string $source_key, bool $enabled ): bool {
return $this->update( $source_key, array( 'enabled' => $enabled ) );
}
/**
* 启用/禁用源
*
* @param string $source_key 源唯一键
* @param bool $enabled 是否启用
* @return bool
*/
public function toggle( string $source_key, bool $enabled ): bool {
return $this->update( $source_key, [ 'enabled' => $enabled ] );
}

/**
* 规范化源数据
*
* @param array $source 源数据
* @return array
*/
private function normalize_source( array $source ): array {
return wp_parse_args(
$source,
array(
'source_key' => '',
'name' => '',
'type' => self::TYPE_CUSTOM,
'base_url' => '',
'api_url' => '',
'did' => '',
'public_key' => '',
'signature_scheme' => self::SIGNATURE_NONE,
'signature_required' => false,
'trust_level' => 50,
'enabled' => true,
'default_priority' => 50,
'auth_type' => self::AUTH_NONE,
'auth_secret_ref' => '',
'headers' => array(),
'capabilities' => array(),
'rate_limit' => array(),
'cache_ttl' => 43200,
'is_preset' => false,
'last_checked_at' => null,
'created_at' => current_time( 'mysql' ),
'updated_at' => current_time( 'mysql' ),
)
);
}
/**
* 规范化源数据
*
* @param array $source 源数据
* @return array
*/
private function normalize_source( array $source ): array {
return wp_parse_args( $source, [
'source_key' => '',
'name' => '',
'type' => self::TYPE_CUSTOM,
'base_url' => '',
'api_url' => '',
'did' => '',
'public_key' => '',
'signature_scheme' => self::SIGNATURE_NONE,
'signature_required' => false,
'trust_level' => 50,
'enabled' => true,
'default_priority' => 50,
'auth_type' => self::AUTH_NONE,
'auth_secret_ref' => '',
'headers' => [],
'capabilities' => [],
'rate_limit' => [],
'cache_ttl' => 43200,
'is_preset' => false,
'last_checked_at' => null,
'created_at' => current_time( 'mysql' ),
'updated_at' => current_time( 'mysql' ),
] );
}

/**
* 确保预置源存在
*/
private function ensure_preset_sources(): void {
$existing_keys = array_column( $this->cached_sources, 'source_key' );
$needs_update = false;
/**
* 确保预置源存在
*/
private function ensure_preset_sources(): void {
$existing_keys = array_column( $this->cached_sources, 'source_key' );
$needs_update = false;

foreach ( $this->get_preset_sources() as $preset ) {
if ( ! in_array( $preset['source_key'], $existing_keys, true ) ) {
$this->cached_sources[] = $preset;
$needs_update = true;
}
}
foreach ( $this->get_preset_sources() as $preset ) {
if ( ! in_array( $preset['source_key'], $existing_keys, true ) ) {
$this->cached_sources[] = $preset;
$needs_update = true;
}
}

if ( $needs_update ) {
update_option( self::OPTION_NAME, $this->cached_sources, false );
}
}
if ( $needs_update ) {
update_option( self::OPTION_NAME, $this->cached_sources, false );
}
}

/**
* 获取预置源列表
*
* @return array
*/
private function get_preset_sources(): array {
$now = current_time( 'mysql' );
return array(
array(
'source_key' => 'wporg',
'name' => 'WordPress.org',
'type' => self::TYPE_WPORG,
'base_url' => 'https://wordpress.org',
'api_url' => 'https://api.wordpress.org',
'did' => '',
'public_key' => '',
'signature_scheme' => self::SIGNATURE_NONE,
'signature_required' => false,
'trust_level' => 100,
'enabled' => true,
'default_priority' => 100,
'auth_type' => self::AUTH_NONE,
'auth_secret_ref' => '',
'headers' => array(),
'capabilities' => array( 'plugins', 'themes', 'core', 'translations' ),
'rate_limit' => array(),
'cache_ttl' => 43200,
'is_preset' => true,
'created_at' => $now,
'updated_at' => $now,
),
array(
'source_key' => 'wenpai-mirror',
'name' => '文派开源更新源',
'type' => self::TYPE_JSON,
'base_url' => 'https://wenpai.org',
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
'did' => '',
'public_key' => '',
'signature_scheme' => self::SIGNATURE_NONE,
'signature_required' => false,
'trust_level' => 90,
'enabled' => true,
'default_priority' => 10,
'auth_type' => self::AUTH_NONE,
'auth_secret_ref' => '',
'headers' => array(),
'capabilities' => array( 'plugins', 'themes' ),
'rate_limit' => array(),
'cache_ttl' => 43200,
'is_preset' => true,
'created_at' => $now,
'updated_at' => $now,
),
array(
'source_key' => 'fair-aspirecloud',
'name' => 'FAIR AspireCloud',
'type' => self::TYPE_FAIR,
'base_url' => 'https://aspirepress.org',
'api_url' => 'https://api.aspirecloud.io/v1',
'did' => '',
'public_key' => '',
'signature_scheme' => self::SIGNATURE_ED25519,
'signature_required' => false,
'trust_level' => 85,
'enabled' => false,
'default_priority' => 20,
'auth_type' => self::AUTH_NONE,
'auth_secret_ref' => '',
'headers' => array(),
'capabilities' => array( 'plugins', 'themes', 'fair_did' ),
'rate_limit' => array(),
'cache_ttl' => 43200,
'is_preset' => true,
'created_at' => $now,
'updated_at' => $now,
),
);
}
/**
* 获取预置源列表
*
* @return array
*/
private function get_preset_sources(): array {
$now = current_time( 'mysql' );
return [
[
'source_key' => 'wporg',
'name' => 'WordPress.org',
'type' => self::TYPE_WPORG,
'base_url' => 'https://wordpress.org',
'api_url' => 'https://api.wordpress.org',
'did' => '',
'public_key' => '',
'signature_scheme' => self::SIGNATURE_NONE,
'signature_required' => false,
'trust_level' => 100,
'enabled' => true,
'default_priority' => 100,
'auth_type' => self::AUTH_NONE,
'auth_secret_ref' => '',
'headers' => [],
'capabilities' => [ 'plugins', 'themes', 'core', 'translations' ],
'rate_limit' => [],
'cache_ttl' => 43200,
'is_preset' => true,
'created_at' => $now,
'updated_at' => $now,
],
[
'source_key' => 'wenpai-mirror',
'name' => '文派开源更新源',
'type' => self::TYPE_JSON,
'base_url' => 'https://wenpai.org',
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
'did' => '',
'public_key' => '',
'signature_scheme' => self::SIGNATURE_NONE,
'signature_required' => false,
'trust_level' => 90,
'enabled' => true,
'default_priority' => 10,
'auth_type' => self::AUTH_NONE,
'auth_secret_ref' => '',
'headers' => [],
'capabilities' => [ 'plugins', 'themes' ],
'rate_limit' => [],
'cache_ttl' => 43200,
'is_preset' => true,
'created_at' => $now,
'updated_at' => $now,
],
[
'source_key' => 'fair-aspirecloud',
'name' => 'FAIR AspireCloud',
'type' => self::TYPE_FAIR,
'base_url' => 'https://aspirepress.org',
'api_url' => 'https://api.aspirecloud.io/v1',
'did' => '',
'public_key' => '',
'signature_scheme' => self::SIGNATURE_ED25519,
'signature_required' => false,
'trust_level' => 85,
'enabled' => false,
'default_priority' => 20,
'auth_type' => self::AUTH_NONE,
'auth_secret_ref' => '',
'headers' => [],
'capabilities' => [ 'plugins', 'themes', 'fair_did' ],
'rate_limit' => [],
'cache_ttl' => 43200,
'is_preset' => true,
'created_at' => $now,
'updated_at' => $now,
],
];
}

/**
* 获取源类型标签
*
* @return array
*/
public static function get_type_labels(): array {
return array(
self::TYPE_WPORG => 'WordPress.org',
self::TYPE_FAIR => 'FAIR',
self::TYPE_CUSTOM => '自定义',
self::TYPE_GIT => 'Git 仓库',
self::TYPE_MIRROR => '镜像',
self::TYPE_JSON => 'JSON API',
self::TYPE_ARKPRESS => 'ArkPress',
);
}
/**
* 获取源类型标签
*
* @return array
*/
public static function get_type_labels(): array {
return [
self::TYPE_WPORG => 'WordPress.org',
self::TYPE_FAIR => 'FAIR',
self::TYPE_CUSTOM => '自定义',
self::TYPE_GIT => 'Git 仓库',
self::TYPE_MIRROR => '镜像',
self::TYPE_JSON => 'JSON API',
self::TYPE_ARKPRESS => 'ArkPress',
];
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->cached_sources = null;
}
/**
* 清除缓存
*/
public function clear_cache(): void {
$this->cached_sources = null;
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\Core;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,289 +17,285 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class VersionLock {

/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_version_locks';
/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_version_locks';

/**
* 锁定类型常量
*/
const LOCK_CURRENT = 'current'; // 锁定到当前版本
const LOCK_SPECIFIC = 'specific'; // 锁定到指定版本
const LOCK_IGNORE = 'ignore'; // 忽略特定版本
/**
* 锁定类型常量
*/
const LOCK_CURRENT = 'current'; // 锁定到当前版本
const LOCK_SPECIFIC = 'specific'; // 锁定到指定版本
const LOCK_IGNORE = 'ignore'; // 忽略特定版本

/**
* 单例实例
*
* @var VersionLock|null
*/
private static ?VersionLock $instance = null;
/**
* 单例实例
*
* @var VersionLock|null
*/
private static ?VersionLock $instance = null;

/**
* 锁定数据缓存
*
* @var array|null
*/
private ?array $locks = null;
/**
* 锁定数据缓存
*
* @var array|null
*/
private ?array $locks = null;

/**
* 获取单例实例
*
* @return VersionLock
*/
public static function get_instance(): VersionLock {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 获取单例实例
*
* @return VersionLock
*/
public static function get_instance(): VersionLock {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* 私有构造函数
*/
private function __construct() {
$this->init_hooks();
}
/**
* 私有构造函数
*/
private function __construct() {
$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 过滤插件更新
add_filter( 'site_transient_update_plugins', array( $this, 'filter_plugin_updates' ), 100 );
// 过滤主题更新
add_filter( 'site_transient_update_themes', array( $this, 'filter_theme_updates' ), 100 );
}
/**
* 初始化钩子
*/
private function init_hooks(): void {
// 过滤插件更新
add_filter( 'site_transient_update_plugins', [ $this, 'filter_plugin_updates' ], 100 );
// 过滤主题更新
add_filter( 'site_transient_update_themes', [ $this, 'filter_theme_updates' ], 100 );
}

/**
* 获取所有锁定
*
* @return array
*/
public function get_all(): array {
if ( null === $this->locks ) {
$this->locks = get_option( self::OPTION_NAME, array() );
if ( ! is_array( $this->locks ) ) {
$this->locks = array();
}
}
return $this->locks;
}
/**
* 获取所有锁定
*
* @return array
*/
public function get_all(): array {
if ( null === $this->locks ) {
$this->locks = get_option( self::OPTION_NAME, [] );
if ( ! is_array( $this->locks ) ) {
$this->locks = [];
}
}
return $this->locks;
}

/**
* 获取项目的锁定信息
*
* @param string $item_key 项目键(如 plugin:hello.php 或 theme:flavor
* @return array|null
*/
public function get( string $item_key ): ?array {
$locks = $this->get_all();
return $locks[ $item_key ] ?? null;
}
/**
* 获取项目的锁定信息
*
* @param string $item_key 项目键(如 plugin:hello.php 或 theme:flavor
* @return array|null
*/
public function get( string $item_key ): ?array {
$locks = $this->get_all();
return $locks[ $item_key ] ?? null;
}

/**
* 锁定项目版本
*
* @param string $item_key 项目键
* @param string $lock_type 锁定类型
* @param string $version 版本号(锁定到指定版本时使用)
* @param array $ignore_versions 忽略的版本列表
* @return bool
*/
public function lock( string $item_key, string $lock_type, string $version = '', array $ignore_versions = array() ): bool {
$locks = $this->get_all();
/**
* 锁定项目版本
*
* @param string $item_key 项目键
* @param string $lock_type 锁定类型
* @param string $version 版本号(锁定到指定版本时使用)
* @param array $ignore_versions 忽略的版本列表
* @return bool
*/
public function lock( string $item_key, string $lock_type, string $version = '', array $ignore_versions = [] ): bool {
$locks = $this->get_all();

$locks[ $item_key ] = array(
'type' => $lock_type,
'version' => $version,
'ignore_versions' => $ignore_versions,
'locked_at' => current_time( 'mysql' ),
);
$locks[ $item_key ] = [
'type' => $lock_type,
'version' => $version,
'ignore_versions' => $ignore_versions,
'locked_at' => current_time( 'mysql' ),
];

$this->locks = $locks;
return update_option( self::OPTION_NAME, $locks );
}
$this->locks = $locks;
return update_option( self::OPTION_NAME, $locks );
}

/**
* 解锁项目
*
* @param string $item_key 项目键
* @return bool
*/
public function unlock( string $item_key ): bool {
$locks = $this->get_all();
/**
* 解锁项目
*
* @param string $item_key 项目键
* @return bool
*/
public function unlock( string $item_key ): bool {
$locks = $this->get_all();

if ( ! isset( $locks[ $item_key ] ) ) {
return true;
}
if ( ! isset( $locks[ $item_key ] ) ) {
return true;
}

unset( $locks[ $item_key ] );
$this->locks = $locks;
return update_option( self::OPTION_NAME, $locks );
}
unset( $locks[ $item_key ] );
$this->locks = $locks;
return update_option( self::OPTION_NAME, $locks );
}

/**
* 检查项目是否被锁定
*
* @param string $item_key 项目键
* @return bool
*/
public function is_locked( string $item_key ): bool {
return null !== $this->get( $item_key );
}
/**
* 检查项目是否被锁定
*
* @param string $item_key 项目键
* @return bool
*/
public function is_locked( string $item_key ): bool {
return null !== $this->get( $item_key );
}

/**
* 检查是否应该阻止更新
*
* @param string $item_key 项目键
* @param string $current_version 当前版本
* @param string $new_version 新版本
* @return bool 返回 true 表示应该阻止更新
*/
public function should_block_update( string $item_key, string $current_version, string $new_version ): bool {
$lock = $this->get( $item_key );
/**
* 检查是否应该阻止更新
*
* @param string $item_key 项目键
* @param string $current_version 当前版本
* @param string $new_version 新版本
* @return bool 返回 true 表示应该阻止更新
*/
public function should_block_update( string $item_key, string $current_version, string $new_version ): bool {
$lock = $this->get( $item_key );

if ( null === $lock ) {
return false;
}
if ( null === $lock ) {
return false;
}

switch ( $lock['type'] ) {
case self::LOCK_CURRENT:
// 锁定到当前版本,阻止所有更新
return true;
switch ( $lock['type'] ) {
case self::LOCK_CURRENT:
// 锁定到当前版本,阻止所有更新
return true;

case self::LOCK_SPECIFIC:
// 锁定到指定版本,如果当前版本等于锁定版本则阻止更新
return version_compare( $current_version, $lock['version'], '==' );
case self::LOCK_SPECIFIC:
// 锁定到指定版本,如果当前版本等于锁定版本则阻止更新
return version_compare( $current_version, $lock['version'], '==' );

case self::LOCK_IGNORE:
// 忽略特定版本
return in_array( $new_version, $lock['ignore_versions'], true );
case self::LOCK_IGNORE:
// 忽略特定版本
return in_array( $new_version, $lock['ignore_versions'], true );

default:
return false;
}
}
default:
return false;
}
}

/**
* 过滤插件更新
*
* @param object $transient 更新 transient
* @return object
*/
public function filter_plugin_updates( $transient ) {
if ( empty( $transient ) || ! is_object( $transient ) ) {
return $transient;
}
/**
* 过滤插件更新
*
* @param object $transient 更新 transient
* @return object
*/
public function filter_plugin_updates( $transient ) {
if ( empty( $transient ) || ! is_object( $transient ) ) {
return $transient;
}

if ( empty( $transient->response ) ) {
return $transient;
}
if ( empty( $transient->response ) ) {
return $transient;
}

if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();

foreach ( $transient->response as $plugin_file => $update_info ) {
$item_key = 'plugin:' . $plugin_file;
$current_version = $plugins[ $plugin_file ]['Version'] ?? '0';
$new_version = is_object( $update_info ) ? $update_info->new_version : ( $update_info['new_version'] ?? '0' );
foreach ( $transient->response as $plugin_file => $update_info ) {
$item_key = 'plugin:' . $plugin_file;
$current_version = $plugins[ $plugin_file ]['Version'] ?? '0';
$new_version = is_object( $update_info ) ? $update_info->new_version : ( $update_info['new_version'] ?? '0' );

if ( $this->should_block_update( $item_key, $current_version, $new_version ) ) {
// 移动到 no_update
if ( ! isset( $transient->no_update ) ) {
$transient->no_update = array();
}
$transient->no_update[ $plugin_file ] = $update_info;
unset( $transient->response[ $plugin_file ] );
if ( $this->should_block_update( $item_key, $current_version, $new_version ) ) {
// 移动到 no_update
if ( ! isset( $transient->no_update ) ) {
$transient->no_update = [];
}
$transient->no_update[ $plugin_file ] = $update_info;
unset( $transient->response[ $plugin_file ] );

Logger::debug(
sprintf(
'Version lock: blocked update for %s (current: %s, new: %s)',
$plugin_file,
$current_version,
$new_version
)
);
}
}
Logger::debug( sprintf(
'Version lock: blocked update for %s (current: %s, new: %s)',
$plugin_file,
$current_version,
$new_version
) );
}
}

return $transient;
}
return $transient;
}

/**
* 过滤主题更新
*
* @param object $transient 更新 transient
* @return object
*/
public function filter_theme_updates( $transient ) {
if ( empty( $transient ) || ! is_object( $transient ) ) {
return $transient;
}
/**
* 过滤主题更新
*
* @param object $transient 更新 transient
* @return object
*/
public function filter_theme_updates( $transient ) {
if ( empty( $transient ) || ! is_object( $transient ) ) {
return $transient;
}

if ( empty( $transient->response ) ) {
return $transient;
}
if ( empty( $transient->response ) ) {
return $transient;
}

$themes = wp_get_themes();
$themes = wp_get_themes();

foreach ( $transient->response as $theme_slug => $update_info ) {
$item_key = 'theme:' . $theme_slug;
$current_version = isset( $themes[ $theme_slug ] ) ? $themes[ $theme_slug ]->get( 'Version' ) : '0';
$new_version = is_array( $update_info ) ? ( $update_info['new_version'] ?? '0' ) : '0';
foreach ( $transient->response as $theme_slug => $update_info ) {
$item_key = 'theme:' . $theme_slug;
$current_version = isset( $themes[ $theme_slug ] ) ? $themes[ $theme_slug ]->get( 'Version' ) : '0';
$new_version = is_array( $update_info ) ? ( $update_info['new_version'] ?? '0' ) : '0';

if ( $this->should_block_update( $item_key, $current_version, $new_version ) ) {
unset( $transient->response[ $theme_slug ] );
if ( $this->should_block_update( $item_key, $current_version, $new_version ) ) {
unset( $transient->response[ $theme_slug ] );

Logger::debug(
sprintf(
'Version lock: blocked update for theme %s (current: %s, new: %s)',
$theme_slug,
$current_version,
$new_version
)
);
}
}
Logger::debug( sprintf(
'Version lock: blocked update for theme %s (current: %s, new: %s)',
$theme_slug,
$current_version,
$new_version
) );
}
}

return $transient;
}
return $transient;
}

/**
* 获取锁定类型标签
*
* @param string $type 锁定类型
* @return string
*/
public static function get_type_label( string $type ): string {
$labels = array(
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
);
return $labels[ $type ] ?? $type;
}
/**
* 获取锁定类型标签
*
* @param string $type 锁定类型
* @return string
*/
public static function get_type_label( string $type ): string {
$labels = [
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
];
return $labels[ $type ] ?? $type;
}

/**
* 获取所有锁定类型
*
* @return array
*/
public static function get_lock_types(): array {
return array(
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
);
}
/**
* 获取所有锁定类型
*
* @return array
*/
public static function get_lock_types(): array {
return [
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
];
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->locks = null;
}
/**
* 清除缓存
*/
public function clear_cache(): void {
$this->locks = null;
}
}

View file

@ -14,7 +14,7 @@ namespace WPBridge\FAIR;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -22,301 +22,301 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class FairProtocol {

/**
* DID 方法前缀
*/
const DID_METHOD = 'did:fair:';
/**
* DID 方法前缀
*/
const DID_METHOD = 'did:fair:';

/**
* 支持的签名方案
*/
const SIGNATURE_ED25519 = 'ed25519';
/**
* 支持的签名方案
*/
const SIGNATURE_ED25519 = 'ed25519';

/**
* 解析 FAIR DID
*
* DID 格式: did:fair:<namespace>:<identifier>
* 例如: did:fair:wp:plugin:hello-dolly
*
* @param string $did DID 字符串
* @return array|null 解析结果
*/
public function parse_did( string $did ): ?array {
if ( strpos( $did, self::DID_METHOD ) !== 0 ) {
return null;
}
/**
* 解析 FAIR DID
*
* DID 格式: did:fair:<namespace>:<identifier>
* 例如: did:fair:wp:plugin:hello-dolly
*
* @param string $did DID 字符串
* @return array|null 解析结果
*/
public function parse_did( string $did ): ?array {
if ( strpos( $did, self::DID_METHOD ) !== 0 ) {
return null;
}

$parts = explode( ':', substr( $did, strlen( self::DID_METHOD ) ) );
$parts = explode( ':', substr( $did, strlen( self::DID_METHOD ) ) );

if ( count( $parts ) < 2 ) {
return null;
}
if ( count( $parts ) < 2 ) {
return null;
}

return array(
'method' => 'fair',
'namespace' => $parts[0] ?? '',
'type' => $parts[1] ?? '',
'identifier' => $parts[2] ?? '',
'version' => $parts[3] ?? null,
'raw' => $did,
);
}
return [
'method' => 'fair',
'namespace' => $parts[0] ?? '',
'type' => $parts[1] ?? '',
'identifier' => $parts[2] ?? '',
'version' => $parts[3] ?? null,
'raw' => $did,
];
}

/**
* 构建 FAIR DID
*
* @param string $namespace 命名空间 (如 'wp')
* @param string $type 类型 (如 'plugin', 'theme')
* @param string $identifier 标识符 (如 'hello-dolly')
* @param string|null $version 版本号 (可选)
* @return string
*/
public function build_did( string $namespace, string $type, string $identifier, ?string $version = null ): string {
$did = self::DID_METHOD . $namespace . ':' . $type . ':' . $identifier;
/**
* 构建 FAIR DID
*
* @param string $namespace 命名空间 (如 'wp')
* @param string $type 类型 (如 'plugin', 'theme')
* @param string $identifier 标识符 (如 'hello-dolly')
* @param string|null $version 版本号 (可选)
* @return string
*/
public function build_did( string $namespace, string $type, string $identifier, ?string $version = null ): string {
$did = self::DID_METHOD . $namespace . ':' . $type . ':' . $identifier;

if ( $version ) {
$did .= ':' . $version;
}
if ( $version ) {
$did .= ':' . $version;
}

return $did;
}
return $did;
}

/**
* 从 WordPress 项目生成 DID
*
* @param string $item_key 项目键 (如 'plugin:hello-dolly/hello.php')
* @param string $item_slug 项目 slug
* @return string
*/
public function generate_did_from_item( string $item_key, string $item_slug ): string {
$type = 'plugin';
/**
* 从 WordPress 项目生成 DID
*
* @param string $item_key 项目键 (如 'plugin:hello-dolly/hello.php')
* @param string $item_slug 项目 slug
* @return string
*/
public function generate_did_from_item( string $item_key, string $item_slug ): string {
$type = 'plugin';

if ( strpos( $item_key, 'theme:' ) === 0 ) {
$type = 'theme';
} elseif ( strpos( $item_key, 'mu-plugin:' ) === 0 ) {
$type = 'mu-plugin';
}
if ( strpos( $item_key, 'theme:' ) === 0 ) {
$type = 'theme';
} elseif ( strpos( $item_key, 'mu-plugin:' ) === 0 ) {
$type = 'mu-plugin';
}

return $this->build_did( 'wp', $type, $item_slug );
}
return $this->build_did( 'wp', $type, $item_slug );
}

/**
* 验证 ED25519 签名
*
* @param string $message 原始消息
* @param string $signature 签名 (base64 编码)
* @param string $public_key 公钥 (base64 编码)
* @return bool
*/
public function verify_ed25519_signature( string $message, string $signature, string $public_key ): bool {
// 检查 sodium 扩展
if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ) {
return $this->verify_ed25519_fallback( $message, $signature, $public_key );
}
/**
* 验证 ED25519 签名
*
* @param string $message 原始消息
* @param string $signature 签名 (base64 编码)
* @param string $public_key 公钥 (base64 编码)
* @return bool
*/
public function verify_ed25519_signature( string $message, string $signature, string $public_key ): bool {
// 检查 sodium 扩展
if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ) {
return $this->verify_ed25519_fallback( $message, $signature, $public_key );
}

try {
$signature_bin = base64_decode( $signature, true );
$public_key_bin = base64_decode( $public_key, true );
try {
$signature_bin = base64_decode( $signature, true );
$public_key_bin = base64_decode( $public_key, true );

if ( false === $signature_bin || false === $public_key_bin ) {
return false;
}
if ( false === $signature_bin || false === $public_key_bin ) {
return false;
}

// 验证签名长度
if ( strlen( $signature_bin ) !== SODIUM_CRYPTO_SIGN_BYTES ) {
return false;
}
// 验证签名长度
if ( strlen( $signature_bin ) !== SODIUM_CRYPTO_SIGN_BYTES ) {
return false;
}

// 验证公钥长度
if ( strlen( $public_key_bin ) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ) {
return false;
}
// 验证公钥长度
if ( strlen( $public_key_bin ) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ) {
return false;
}

return sodium_crypto_sign_verify_detached( $signature_bin, $message, $public_key_bin );
return sodium_crypto_sign_verify_detached( $signature_bin, $message, $public_key_bin );

} catch ( \Exception $e ) {
return false;
}
}
} catch ( \Exception $e ) {
return false;
}
}

/**
* ED25519 签名验证回退方案
*
* 当 sodium 扩展不可用时使用
*
* @param string $message 原始消息
* @param string $signature 签名
* @param string $public_key 公钥
* @return bool
*/
private function verify_ed25519_fallback( string $message, string $signature, string $public_key ): bool {
// 尝试使用 paragonie/sodium_compat
if ( class_exists( '\ParagonIE_Sodium_Compat' ) ) {
try {
$signature_bin = base64_decode( $signature, true );
$public_key_bin = base64_decode( $public_key, true );
/**
* ED25519 签名验证回退方案
*
* 当 sodium 扩展不可用时使用
*
* @param string $message 原始消息
* @param string $signature 签名
* @param string $public_key 公钥
* @return bool
*/
private function verify_ed25519_fallback( string $message, string $signature, string $public_key ): bool {
// 尝试使用 paragonie/sodium_compat
if ( class_exists( '\ParagonIE_Sodium_Compat' ) ) {
try {
$signature_bin = base64_decode( $signature, true );
$public_key_bin = base64_decode( $public_key, true );

if ( false === $signature_bin || false === $public_key_bin ) {
return false;
}
if ( false === $signature_bin || false === $public_key_bin ) {
return false;
}

return \ParagonIE_Sodium_Compat::crypto_sign_verify_detached(
$signature_bin,
$message,
$public_key_bin
);
} catch ( \Exception $e ) {
return false;
}
}
return \ParagonIE_Sodium_Compat::crypto_sign_verify_detached(
$signature_bin,
$message,
$public_key_bin
);
} catch ( \Exception $e ) {
return false;
}
}

// 无法验证签名
return false;
}
// 无法验证签名
return false;
}

/**
* 验证包签名
*
* @param array $package 包数据
* @return array 验证结果
*/
public function verify_package_signature( array $package ): array {
$result = array(
'valid' => false,
'signed' => false,
'signer' => null,
'algorithm' => null,
'error' => null,
);
/**
* 验证包签名
*
* @param array $package 包数据
* @return array 验证结果
*/
public function verify_package_signature( array $package ): array {
$result = [
'valid' => false,
'signed' => false,
'signer' => null,
'algorithm' => null,
'error' => null,
];

// 检查是否有签名
if ( empty( $package['signature'] ) ) {
$result['error'] = 'no_signature';
return $result;
}
// 检查是否有签名
if ( empty( $package['signature'] ) ) {
$result['error'] = 'no_signature';
return $result;
}

$result['signed'] = true;
$result['signed'] = true;

// 获取签名信息
$signature_data = $package['signature'];
$algorithm = $signature_data['algorithm'] ?? self::SIGNATURE_ED25519;
$signature = $signature_data['value'] ?? '';
$public_key = $signature_data['public_key'] ?? '';
$signer_did = $signature_data['signer'] ?? '';
// 获取签名信息
$signature_data = $package['signature'];
$algorithm = $signature_data['algorithm'] ?? self::SIGNATURE_ED25519;
$signature = $signature_data['value'] ?? '';
$public_key = $signature_data['public_key'] ?? '';
$signer_did = $signature_data['signer'] ?? '';

$result['algorithm'] = $algorithm;
$result['signer'] = $signer_did;
$result['algorithm'] = $algorithm;
$result['signer'] = $signer_did;

// 目前只支持 ED25519
if ( $algorithm !== self::SIGNATURE_ED25519 ) {
$result['error'] = 'unsupported_algorithm';
return $result;
}
// 目前只支持 ED25519
if ( $algorithm !== self::SIGNATURE_ED25519 ) {
$result['error'] = 'unsupported_algorithm';
return $result;
}

// 构建待验证消息
$message = $this->build_signature_message( $package );
// 构建待验证消息
$message = $this->build_signature_message( $package );

// 验证签名
if ( $this->verify_ed25519_signature( $message, $signature, $public_key ) ) {
$result['valid'] = true;
} else {
$result['error'] = 'invalid_signature';
}
// 验证签名
if ( $this->verify_ed25519_signature( $message, $signature, $public_key ) ) {
$result['valid'] = true;
} else {
$result['error'] = 'invalid_signature';
}

return $result;
}
return $result;
}

/**
* 构建签名消息
*
* @param array $package 包数据
* @return string
*/
private function build_signature_message( array $package ): string {
// 移除签名字段
$data = $package;
unset( $data['signature'] );
/**
* 构建签名消息
*
* @param array $package 包数据
* @return string
*/
private function build_signature_message( array $package ): string {
// 移除签名字段
$data = $package;
unset( $data['signature'] );

// 按键排序
ksort( $data );
// 按键排序
ksort( $data );

// JSON 编码
return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
}
// JSON 编码
return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
}

/**
* 解析 FAIR 仓库响应
*
* @param array $response API 响应
* @return array 解析后的包列表
*/
public function parse_repository_response( array $response ): array {
$packages = array();
/**
* 解析 FAIR 仓库响应
*
* @param array $response API 响应
* @return array 解析后的包列表
*/
public function parse_repository_response( array $response ): array {
$packages = [];

// FAIR 响应格式
if ( isset( $response['packages'] ) ) {
foreach ( $response['packages'] as $package ) {
$parsed = $this->parse_package( $package );
if ( $parsed ) {
$packages[] = $parsed;
}
}
}
// FAIR 响应格式
if ( isset( $response['packages'] ) ) {
foreach ( $response['packages'] as $package ) {
$parsed = $this->parse_package( $package );
if ( $parsed ) {
$packages[] = $parsed;
}
}
}

return $packages;
}
return $packages;
}

/**
* 解析单个包
*
* @param array $package 包数据
* @return array|null
*/
private function parse_package( array $package ): ?array {
if ( empty( $package['did'] ) && empty( $package['slug'] ) ) {
return null;
}
/**
* 解析单个包
*
* @param array $package 包数据
* @return array|null
*/
private function parse_package( array $package ): ?array {
if ( empty( $package['did'] ) && empty( $package['slug'] ) ) {
return null;
}

return array(
'did' => $package['did'] ?? '',
'slug' => $package['slug'] ?? '',
'name' => $package['name'] ?? '',
'version' => $package['version'] ?? '',
'download_url' => $package['download_url'] ?? '',
'homepage' => $package['homepage'] ?? '',
'description' => $package['description'] ?? '',
'author' => $package['author'] ?? '',
'requires' => $package['requires'] ?? '',
'requires_php' => $package['requires_php'] ?? '',
'tested' => $package['tested'] ?? '',
'signature' => $package['signature'] ?? null,
'signature_valid' => null,
'last_updated' => $package['last_updated'] ?? '',
);
}
return [
'did' => $package['did'] ?? '',
'slug' => $package['slug'] ?? '',
'name' => $package['name'] ?? '',
'version' => $package['version'] ?? '',
'download_url' => $package['download_url'] ?? '',
'homepage' => $package['homepage'] ?? '',
'description' => $package['description'] ?? '',
'author' => $package['author'] ?? '',
'requires' => $package['requires'] ?? '',
'requires_php' => $package['requires_php'] ?? '',
'tested' => $package['tested'] ?? '',
'signature' => $package['signature'] ?? null,
'signature_valid' => null,
'last_updated' => $package['last_updated'] ?? '',
];
}

/**
* 检查 sodium 扩展是否可用
*
* @return bool
*/
public function is_sodium_available(): bool {
return function_exists( 'sodium_crypto_sign_verify_detached' ) ||
class_exists( '\ParagonIE_Sodium_Compat' );
}
/**
* 检查 sodium 扩展是否可用
*
* @return bool
*/
public function is_sodium_available(): bool {
return function_exists( 'sodium_crypto_sign_verify_detached' ) ||
class_exists( '\ParagonIE_Sodium_Compat' );
}

/**
* 获取支持的签名算法
*
* @return array
*/
public function get_supported_algorithms(): array {
$algorithms = array();
/**
* 获取支持的签名算法
*
* @return array
*/
public function get_supported_algorithms(): array {
$algorithms = [];

if ( $this->is_sodium_available() ) {
$algorithms[] = self::SIGNATURE_ED25519;
}
if ( $this->is_sodium_available() ) {
$algorithms[] = self::SIGNATURE_ED25519;
}

return $algorithms;
}
return $algorithms;
}
}

View file

@ -12,7 +12,7 @@ namespace WPBridge\FAIR;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

use WPBridge\Core\SourceRegistry;
@ -22,335 +22,335 @@ use WPBridge\Core\SourceRegistry;
*/
class FairSourceAdapter {

/**
* FAIR 协议处理器
*
* @var FairProtocol
*/
private FairProtocol $protocol;
/**
* FAIR 协议处理器
*
* @var FairProtocol
*/
private FairProtocol $protocol;

/**
* 源配置
*
* @var array
*/
private array $source;
/**
* 源配置
*
* @var array
*/
private array $source;

/**
* 构造函数
*
* @param array $source 源配置
*/
public function __construct( array $source ) {
$this->source = $source;
$this->protocol = new FairProtocol();
}
/**
* 构造函数
*
* @param array $source 源配置
*/
public function __construct( array $source ) {
$this->source = $source;
$this->protocol = new FairProtocol();
}

/**
* 检查插件更新
*
* @param string $slug 插件 slug
* @param string $version 当前版本
* @return array|null 更新信息
*/
public function check_plugin_update( string $slug, string $version ): ?array {
return $this->check_update( 'plugin', $slug, $version );
}
/**
* 检查插件更新
*
* @param string $slug 插件 slug
* @param string $version 当前版本
* @return array|null 更新信息
*/
public function check_plugin_update( string $slug, string $version ): ?array {
return $this->check_update( 'plugin', $slug, $version );
}

/**
* 检查主题更新
*
* @param string $slug 主题 slug
* @param string $version 当前版本
* @return array|null 更新信息
*/
public function check_theme_update( string $slug, string $version ): ?array {
return $this->check_update( 'theme', $slug, $version );
}
/**
* 检查主题更新
*
* @param string $slug 主题 slug
* @param string $version 当前版本
* @return array|null 更新信息
*/
public function check_theme_update( string $slug, string $version ): ?array {
return $this->check_update( 'theme', $slug, $version );
}

/**
* 检查更新
*
* @param string $type 类型 (plugin/theme)
* @param string $slug slug
* @param string $version 当前版本
* @return array|null
*/
private function check_update( string $type, string $slug, string $version ): ?array {
$api_url = $this->source['api_url'] ?? '';
/**
* 检查更新
*
* @param string $type 类型 (plugin/theme)
* @param string $slug slug
* @param string $version 当前版本
* @return array|null
*/
private function check_update( string $type, string $slug, string $version ): ?array {
$api_url = $this->source['api_url'] ?? '';

if ( empty( $api_url ) ) {
return null;
}
if ( empty( $api_url ) ) {
return null;
}

// 构建 API 请求 URL
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug;
// 构建 API 请求 URL
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug;

// 发送请求
$response = $this->make_request( $endpoint );
// 发送请求
$response = $this->make_request( $endpoint );

if ( ! $response ) {
return null;
}
if ( ! $response ) {
return null;
}

// 解析响应
$package = $this->parse_response( $response );
// 解析响应
$package = $this->parse_response( $response );

if ( ! $package ) {
return null;
}
if ( ! $package ) {
return null;
}

// 检查版本
if ( version_compare( $package['version'], $version, '<=' ) ) {
return null; // 没有更新
}
// 检查版本
if ( version_compare( $package['version'], $version, '<=' ) ) {
return null; // 没有更新
}

// 验证签名(如果需要)
if ( ! empty( $this->source['signature_required'] ) ) {
$verification = $this->protocol->verify_package_signature( $response );
// 验证签名(如果需要)
if ( ! empty( $this->source['signature_required'] ) ) {
$verification = $this->protocol->verify_package_signature( $response );

if ( ! $verification['valid'] ) {
// 签名验证失败
return null;
}
if ( ! $verification['valid'] ) {
// 签名验证失败
return null;
}

$package['signature_valid'] = true;
}
$package['signature_valid'] = true;
}

return $package;
}
return $package;
}

/**
* 获取插件信息
*
* @param string $slug 插件 slug
* @return array|null
*/
public function get_plugin_info( string $slug ): ?array {
return $this->get_info( 'plugin', $slug );
}
/**
* 获取插件信息
*
* @param string $slug 插件 slug
* @return array|null
*/
public function get_plugin_info( string $slug ): ?array {
return $this->get_info( 'plugin', $slug );
}

/**
* 获取主题信息
*
* @param string $slug 主题 slug
* @return array|null
*/
public function get_theme_info( string $slug ): ?array {
return $this->get_info( 'theme', $slug );
}
/**
* 获取主题信息
*
* @param string $slug 主题 slug
* @return array|null
*/
public function get_theme_info( string $slug ): ?array {
return $this->get_info( 'theme', $slug );
}

/**
* 获取项目信息
*
* @param string $type 类型
* @param string $slug slug
* @return array|null
*/
private function get_info( string $type, string $slug ): ?array {
$api_url = $this->source['api_url'] ?? '';
/**
* 获取项目信息
*
* @param string $type 类型
* @param string $slug slug
* @return array|null
*/
private function get_info( string $type, string $slug ): ?array {
$api_url = $this->source['api_url'] ?? '';

if ( empty( $api_url ) ) {
return null;
}
if ( empty( $api_url ) ) {
return null;
}

$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug . '/info';
$response = $this->make_request( $endpoint );
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug . '/info';
$response = $this->make_request( $endpoint );

if ( ! $response ) {
return null;
}
if ( ! $response ) {
return null;
}

return $this->parse_response( $response );
}
return $this->parse_response( $response );
}

/**
* 通过 DID 查询
*
* @param string $did DID 字符串
* @return array|null
*/
public function query_by_did( string $did ): ?array {
$parsed = $this->protocol->parse_did( $did );
/**
* 通过 DID 查询
*
* @param string $did DID 字符串
* @return array|null
*/
public function query_by_did( string $did ): ?array {
$parsed = $this->protocol->parse_did( $did );

if ( ! $parsed ) {
return null;
}
if ( ! $parsed ) {
return null;
}

$api_url = $this->source['api_url'] ?? '';
$api_url = $this->source['api_url'] ?? '';

if ( empty( $api_url ) ) {
return null;
}
if ( empty( $api_url ) ) {
return null;
}

// FAIR API 支持 DID 查询
$endpoint = trailingslashit( $api_url ) . 'resolve?did=' . urlencode( $did );
$response = $this->make_request( $endpoint );
// FAIR API 支持 DID 查询
$endpoint = trailingslashit( $api_url ) . 'resolve?did=' . urlencode( $did );
$response = $this->make_request( $endpoint );

if ( ! $response ) {
return null;
}
if ( ! $response ) {
return null;
}

return $this->parse_response( $response );
}
return $this->parse_response( $response );
}

/**
* 发送 HTTP 请求
*
* @param string $url URL
* @return array|null
*/
private function make_request( string $url ): ?array {
$args = array(
'timeout' => 15,
'user-agent' => 'WPBridge/' . WPBRIDGE_VERSION . ' WordPress/' . get_bloginfo( 'version' ),
'headers' => array(
'Accept' => 'application/json',
),
);
/**
* 发送 HTTP 请求
*
* @param string $url URL
* @return array|null
*/
private function make_request( string $url ): ?array {
$args = [
'timeout' => 15,
'user-agent' => 'WPBridge/' . WPBRIDGE_VERSION . ' WordPress/' . get_bloginfo( 'version' ),
'headers' => [
'Accept' => 'application/json',
],
];

// 添加认证
if ( ! empty( $this->source['auth_type'] ) && $this->source['auth_type'] !== SourceRegistry::AUTH_NONE ) {
$auth_header = $this->get_auth_header();
if ( $auth_header ) {
$args['headers']['Authorization'] = $auth_header;
}
}
// 添加认证
if ( ! empty( $this->source['auth_type'] ) && $this->source['auth_type'] !== SourceRegistry::AUTH_NONE ) {
$auth_header = $this->get_auth_header();
if ( $auth_header ) {
$args['headers']['Authorization'] = $auth_header;
}
}

// 添加自定义头
if ( ! empty( $this->source['headers'] ) && is_array( $this->source['headers'] ) ) {
$args['headers'] = array_merge( $args['headers'], $this->source['headers'] );
}
// 添加自定义头
if ( ! empty( $this->source['headers'] ) && is_array( $this->source['headers'] ) ) {
$args['headers'] = array_merge( $args['headers'], $this->source['headers'] );
}

$response = wp_remote_get( $url, $args );
$response = wp_remote_get( $url, $args );

if ( is_wp_error( $response ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'WPBridge FAIR request failed: ' . $response->get_error_message() . ' URL: ' . $url );
}
return null;
}
if ( is_wp_error( $response ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'WPBridge FAIR request failed: ' . $response->get_error_message() . ' URL: ' . $url );
}
return null;
}

$code = wp_remote_retrieve_response_code( $response );
$code = wp_remote_retrieve_response_code( $response );

if ( $code !== 200 ) {
return null;
}
if ( $code !== 200 ) {
return null;
}

$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
return null;
}
if ( json_last_error() !== JSON_ERROR_NONE ) {
return null;
}

return $data;
}
return $data;
}

/**
* 获取认证头
*
* @return string|null
*/
private function get_auth_header(): ?string {
$auth_type = $this->source['auth_type'] ?? '';
$secret_ref = $this->source['auth_secret_ref'] ?? '';
/**
* 获取认证头
*
* @return string|null
*/
private function get_auth_header(): ?string {
$auth_type = $this->source['auth_type'] ?? '';
$secret_ref = $this->source['auth_secret_ref'] ?? '';

if ( empty( $secret_ref ) ) {
return null;
}
if ( empty( $secret_ref ) ) {
return null;
}

// 获取密钥
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );
// 获取密钥
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );

if ( empty( $secret ) ) {
return null;
}
if ( empty( $secret ) ) {
return null;
}

switch ( $auth_type ) {
case SourceRegistry::AUTH_BEARER:
return 'Bearer ' . $secret;
switch ( $auth_type ) {
case SourceRegistry::AUTH_BEARER:
return 'Bearer ' . $secret;

case SourceRegistry::AUTH_TOKEN:
return 'Token ' . $secret;
case SourceRegistry::AUTH_TOKEN:
return 'Token ' . $secret;

case SourceRegistry::AUTH_BASIC:
return 'Basic ' . base64_encode( $secret );
case SourceRegistry::AUTH_BASIC:
return 'Basic ' . base64_encode( $secret );

default:
return null;
}
}
default:
return null;
}
}

/**
* 解析响应
*
* @param array $response 响应数据
* @return array|null
*/
private function parse_response( array $response ): ?array {
// 标准 FAIR 响应格式
if ( isset( $response['data'] ) ) {
$response = $response['data'];
}
/**
* 解析响应
*
* @param array $response 响应数据
* @return array|null
*/
private function parse_response( array $response ): ?array {
// 标准 FAIR 响应格式
if ( isset( $response['data'] ) ) {
$response = $response['data'];
}

if ( empty( $response['slug'] ) && empty( $response['did'] ) ) {
return null;
}
if ( empty( $response['slug'] ) && empty( $response['did'] ) ) {
return null;
}

return array(
'did' => $response['did'] ?? '',
'slug' => $response['slug'] ?? '',
'name' => $response['name'] ?? '',
'version' => $response['version'] ?? '',
'new_version' => $response['version'] ?? '',
'download_url' => $response['download_url'] ?? $response['download_link'] ?? '',
'package' => $response['download_url'] ?? $response['download_link'] ?? '',
'homepage' => $response['homepage'] ?? '',
'url' => $response['homepage'] ?? '',
'description' => $response['description'] ?? '',
'author' => $response['author'] ?? '',
'requires' => $response['requires'] ?? '',
'requires_php' => $response['requires_php'] ?? '',
'tested' => $response['tested'] ?? '',
'last_updated' => $response['last_updated'] ?? '',
'signature' => $response['signature'] ?? null,
'signature_valid' => null,
'icons' => $response['icons'] ?? array(),
'banners' => $response['banners'] ?? array(),
);
}
return [
'did' => $response['did'] ?? '',
'slug' => $response['slug'] ?? '',
'name' => $response['name'] ?? '',
'version' => $response['version'] ?? '',
'new_version' => $response['version'] ?? '',
'download_url' => $response['download_url'] ?? $response['download_link'] ?? '',
'package' => $response['download_url'] ?? $response['download_link'] ?? '',
'homepage' => $response['homepage'] ?? '',
'url' => $response['homepage'] ?? '',
'description' => $response['description'] ?? '',
'author' => $response['author'] ?? '',
'requires' => $response['requires'] ?? '',
'requires_php' => $response['requires_php'] ?? '',
'tested' => $response['tested'] ?? '',
'last_updated' => $response['last_updated'] ?? '',
'signature' => $response['signature'] ?? null,
'signature_valid' => null,
'icons' => $response['icons'] ?? [],
'banners' => $response['banners'] ?? [],
];
}

/**
* 验证下载包
*
* @param string $file_path 文件路径
* @param array $package 包信息
* @return bool
*/
public function verify_download( string $file_path, array $package ): bool {
// 如果不需要签名验证
if ( empty( $this->source['signature_required'] ) ) {
return true;
}
/**
* 验证下载包
*
* @param string $file_path 文件路径
* @param array $package 包信息
* @return bool
*/
public function verify_download( string $file_path, array $package ): bool {
// 如果不需要签名验证
if ( empty( $this->source['signature_required'] ) ) {
return true;
}

// 检查包是否有签名
if ( empty( $package['signature'] ) ) {
return false;
}
// 检查包是否有签名
if ( empty( $package['signature'] ) ) {
return false;
}

// 计算文件哈希
$file_hash = hash_file( 'sha256', $file_path );
// 计算文件哈希
$file_hash = hash_file( 'sha256', $file_path );

// 验证哈希签名
$signature_data = $package['signature'];
// 验证哈希签名
$signature_data = $package['signature'];

if ( isset( $signature_data['file_hash'] ) ) {
if ( $signature_data['file_hash'] !== $file_hash ) {
return false;
}
}
if ( isset( $signature_data['file_hash'] ) ) {
if ( $signature_data['file_hash'] !== $file_hash ) {
return false;
}
}

return true;
}
return true;
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Settings;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -19,118 +19,115 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class EmailHandler implements HandlerInterface {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 支持的通知类型
*
* @var array
*/
private array $supported_types = array( 'update', 'error', 'recovery' );
/**
* 支持的通知类型
*
* @var array
*/
private array $supported_types = [ 'update', 'error', 'recovery' ];

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}

/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string {
return 'email';
}
/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string {
return 'email';
}

/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool {
$notification_settings = $this->settings->get( 'notifications', array() );
return ! empty( $notification_settings['email']['enabled'] );
}
/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool {
$notification_settings = $this->settings->get( 'notifications', [] );
return ! empty( $notification_settings['email']['enabled'] );
}

/**
* 是否支持该通知类型
*
* @param string $type 通知类型
* @return bool
*/
public function supports_type( string $type ): bool {
$notification_settings = $this->settings->get( 'notifications', array() );
$enabled_types = $notification_settings['email']['types'] ?? $this->supported_types;
/**
* 是否支持该通知类型
*
* @param string $type 通知类型
* @return bool
*/
public function supports_type( string $type ): bool {
$notification_settings = $this->settings->get( 'notifications', [] );
$enabled_types = $notification_settings['email']['types'] ?? $this->supported_types;

return in_array( $type, $enabled_types, true );
}
return in_array( $type, $enabled_types, true );
}

/**
* 发送通知
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @throws \Exception 发送失败时抛出异常
*/
public function send( string $subject, string $message, array $data = array() ): void {
$notification_settings = $this->settings->get( 'notifications', array() );
$recipients = $notification_settings['email']['recipients'] ?? array();
/**
* 发送通知
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @throws \Exception 发送失败时抛出异常
*/
public function send( string $subject, string $message, array $data = [] ): void {
$notification_settings = $this->settings->get( 'notifications', [] );
$recipients = $notification_settings['email']['recipients'] ?? [];

if ( empty( $recipients ) ) {
// 默认发送给管理员
$recipients = array( get_option( 'admin_email' ) );
}
if ( empty( $recipients ) ) {
// 默认发送给管理员
$recipients = [ get_option( 'admin_email' ) ];
}

// 验证收件人邮箱格式
$valid_recipients = array_filter(
$recipients,
function ( $email ) {
return is_email( $email );
}
);
// 验证收件人邮箱格式
$valid_recipients = array_filter( $recipients, function ( $email ) {
return is_email( $email );
} );

if ( empty( $valid_recipients ) ) {
throw new \Exception( __( '没有有效的收件人邮箱', 'wpbridge' ) );
}
if ( empty( $valid_recipients ) ) {
throw new \Exception( __( '没有有效的收件人邮箱', 'wpbridge' ) );
}

// 构建 HTML 邮件
$html_message = $this->build_html_message( $subject, $message, $data );
// 构建 HTML 邮件
$html_message = $this->build_html_message( $subject, $message, $data );

// 使用 WordPress 默认发件人,避免 SPF/DKIM 问题
$headers = array(
'Content-Type: text/html; charset=UTF-8',
);
// 使用 WordPress 默认发件人,避免 SPF/DKIM 问题
$headers = [
'Content-Type: text/html; charset=UTF-8',
];

$sent = wp_mail( $valid_recipients, $subject, $html_message, $headers );
$sent = wp_mail( $valid_recipients, $subject, $html_message, $headers );

if ( ! $sent ) {
throw new \Exception( __( '邮件发送失败', 'wpbridge' ) );
}
}
if ( ! $sent ) {
throw new \Exception( __( '邮件发送失败', 'wpbridge' ) );
}
}

/**
* 构建 HTML 邮件内容
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @return string
*/
private function build_html_message( string $subject, string $message, array $data ): string {
$site_name = get_bloginfo( 'name' );
$site_url = get_site_url();
/**
* 构建 HTML 邮件内容
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @return string
*/
private function build_html_message( string $subject, string $message, array $data ): string {
$site_name = get_bloginfo( 'name' );
$site_url = get_site_url();

$html = '<!DOCTYPE html>
$html = '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
@ -154,31 +151,31 @@ class EmailHandler implements HandlerInterface {
<div class="content">
<p>' . nl2br( esc_html( $message ) ) . '</p>';

// 添加数据表格
if ( ! empty( $data ) && ! isset( $data['test'] ) ) {
$html .= '<table class="data-table">';
foreach ( $data as $key => $value ) {
if ( is_scalar( $value ) ) {
$html .= '<tr><th>' . esc_html( $key ) . '</th><td>' . esc_html( $value ) . '</td></tr>';
}
}
$html .= '</table>';
}
// 添加数据表格
if ( ! empty( $data ) && ! isset( $data['test'] ) ) {
$html .= '<table class="data-table">';
foreach ( $data as $key => $value ) {
if ( is_scalar( $value ) ) {
$html .= '<tr><th>' . esc_html( $key ) . '</th><td>' . esc_html( $value ) . '</td></tr>';
}
}
$html .= '</table>';
}

$html .= '
$html .= '
</div>
<div class="footer">
<p>' . sprintf(
/* translators: %s: site name */
esc_html__( '此邮件由 %s 的 WPBridge 插件发送', 'wpbridge' ),
esc_html( $site_name )
) . '</p>
/* translators: %s: site name */
esc_html__( '此邮件由 %s 的 WPBridge 插件发送', 'wpbridge' ),
esc_html( $site_name )
) . '</p>
<p><a href="' . esc_url( $site_url ) . '">' . esc_html( $site_url ) . '</a></p>
</div>
</div>
</body>
</html>';

return $html;
}
return $html;
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\Notification;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,35 +17,35 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
interface HandlerInterface {

/**
* 发送通知
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @throws \Exception 发送失败时抛出异常
*/
public function send( string $subject, string $message, array $data = array() ): void;
/**
* 发送通知
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @throws \Exception 发送失败时抛出异常
*/
public function send( string $subject, string $message, array $data = [] ): void;

/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool;
/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool;

/**
* 是否支持该通知类型
*
* @param string $type 通知类型
* @return bool
*/
public function supports_type( string $type ): bool;
/**
* 是否支持该通知类型
*
* @param string $type 通知类型
* @return bool
*/
public function supports_type( string $type ): bool;

/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string;
/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string;
}

View file

@ -12,7 +12,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -20,237 +20,212 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class NotificationManager {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 通知处理器
*
* @var array
*/
private array $handlers = array();
/**
* 通知处理器
*
* @var array
*/
private array $handlers = [];

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->init_handlers();
$this->init_hooks();
}
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->init_handlers();
$this->init_hooks();
}

/**
* 初始化处理器
*/
private function init_handlers(): void {
$this->handlers = array(
'email' => new EmailHandler( $this->settings ),
'webhook' => new WebhookHandler( $this->settings ),
);
/**
* 初始化处理器
*/
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
);
}
// 允许第三方扩展通知处理器
$this->handlers = apply_filters(
'wpbridge_notification_handlers',
$this->handlers,
$this->settings
);
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
add_action( 'wpbridge_update_available', array( $this, 'on_update_available' ), 10, 2 );
add_action( 'wpbridge_source_error', array( $this, 'on_source_error' ), 10, 2 );
add_action( 'wpbridge_source_recovered', array( $this, 'on_source_recovered' ), 10, 1 );
}
/**
* 初始化钩子
*/
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 = array() ): void {
// 速率限制检查:防止通知轰炸
$rate_limit_key = 'wpbridge_notification_' . md5( $type . $subject );
if ( get_transient( $rate_limit_key ) ) {
Logger::debug(
'通知被速率限制',
array(
'type' => $type,
'subject' => $subject,
)
);
return;
}
/**
* 发送通知
*
* @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 );
// 设置 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(
'通知发送成功',
array(
'handler' => $name,
'type' => $type,
)
);
} catch ( \Exception $e ) {
Logger::error(
'通知发送失败',
array(
'handler' => $name,
'error' => $e->getMessage(),
)
);
}
}
}
}
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
);
/**
* 更新可用时触发
*
* @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'
);
$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 );
}
$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
);
/**
* 源错误时触发
*
* @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
);
$message = sprintf(
/* translators: 1: source ID, 2: error message */
__( '更新源 %1$s 出现错误: %2$s', 'wpbridge' ),
$source_id,
$error
);

$this->send(
'error',
$subject,
$message,
array(
'source_id' => $source_id,
'error' => $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
);
/**
* 源恢复时触发
*
* @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
);
$message = sprintf(
/* translators: %s: source ID */
__( '更新源 %s 已恢复正常', 'wpbridge' ),
$source_id
);

$this->send(
'recovery',
$subject,
$message,
array(
'source_id' => $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;
}
/**
* 获取处理器
*
* @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;
}
/**
* 获取所有处理器
*
* @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 );
/**
* 测试通知
*
* @param string $handler_name 处理器名称
* @return bool
*/
public function test( string $handler_name ): bool {
$handler = $this->get_handler( $handler_name );

if ( null === $handler ) {
return false;
}
if ( null === $handler ) {
return false;
}

try {
$handler->send(
__( '[WPBridge] 测试通知', 'wpbridge' ),
__( '这是一条测试通知,如果您收到此消息,说明通知配置正确。', 'wpbridge' ),
array( 'test' => true )
);
return true;
} catch ( \Exception $e ) {
Logger::error(
'测试通知失败',
array(
'handler' => $handler_name,
'error' => $e->getMessage(),
)
);
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;
}
}
}

View file

@ -13,7 +13,7 @@ use WPBridge\Security\Validator;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -21,306 +21,300 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class WebhookHandler implements HandlerInterface {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 支持的通知类型
*
* @var array
*/
private array $supported_types = array( 'update', 'error', 'recovery' );
/**
* 支持的通知类型
*
* @var array
*/
private array $supported_types = [ 'update', 'error', 'recovery' ];

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}

/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string {
return 'webhook';
}
/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string {
return 'webhook';
}

/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool {
$notification_settings = $this->settings->get( 'notifications', array() );
return ! empty( $notification_settings['webhook']['enabled'] ) &&
! empty( $notification_settings['webhook']['url'] );
}
/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool {
$notification_settings = $this->settings->get( 'notifications', [] );
return ! empty( $notification_settings['webhook']['enabled'] ) &&
! empty( $notification_settings['webhook']['url'] );
}

/**
* 是否支持该通知类型
*
* @param string $type 通知类型
* @return bool
*/
public function supports_type( string $type ): bool {
$notification_settings = $this->settings->get( 'notifications', array() );
$enabled_types = $notification_settings['webhook']['types'] ?? $this->supported_types;
/**
* 是否支持该通知类型
*
* @param string $type 通知类型
* @return bool
*/
public function supports_type( string $type ): bool {
$notification_settings = $this->settings->get( 'notifications', [] );
$enabled_types = $notification_settings['webhook']['types'] ?? $this->supported_types;

return in_array( $type, $enabled_types, true );
}
return in_array( $type, $enabled_types, true );
}

/**
* 发送通知
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @throws \Exception 发送失败时抛出异常
*/
public function send( string $subject, string $message, array $data = array() ): void {
$notification_settings = $this->settings->get( 'notifications', array() );
$webhook_url = $notification_settings['webhook']['url'] ?? '';
$webhook_secret = $notification_settings['webhook']['secret'] ?? '';
$webhook_format = $notification_settings['webhook']['format'] ?? 'json';
/**
* 发送通知
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @throws \Exception 发送失败时抛出异常
*/
public function send( string $subject, string $message, array $data = [] ): void {
$notification_settings = $this->settings->get( 'notifications', [] );
$webhook_url = $notification_settings['webhook']['url'] ?? '';
$webhook_secret = $notification_settings['webhook']['secret'] ?? '';
$webhook_format = $notification_settings['webhook']['format'] ?? 'json';

if ( empty( $webhook_url ) ) {
throw new \Exception( __( 'Webhook URL 未配置', 'wpbridge' ) );
}
if ( empty( $webhook_url ) ) {
throw new \Exception( __( 'Webhook URL 未配置', 'wpbridge' ) );
}

// SSRF 防护:验证 URL 安全性
if ( ! Validator::is_valid_url( $webhook_url ) ) {
throw new \Exception( __( 'Webhook URL 不安全,禁止访问内网地址', 'wpbridge' ) );
}
// SSRF 防护:验证 URL 安全性
if ( ! Validator::is_valid_url( $webhook_url ) ) {
throw new \Exception( __( 'Webhook URL 不安全,禁止访问内网地址', 'wpbridge' ) );
}

// 构建 payload
$payload = $this->build_payload( $subject, $message, $data, $webhook_format );
// 构建 payload
$payload = $this->build_payload( $subject, $message, $data, $webhook_format );

// 构建请求头
$headers = array(
'Content-Type' => 'application/json',
'User-Agent' => 'WPBridge/' . WPBRIDGE_VERSION,
);
// 构建请求头
$headers = [
'Content-Type' => 'application/json',
'User-Agent' => 'WPBridge/' . WPBRIDGE_VERSION,
];

// 添加签名
if ( ! empty( $webhook_secret ) ) {
$signature = $this->generate_signature( $payload, $webhook_secret );
$headers['X-WPBridge-Signature'] = $signature;
}
// 添加签名
if ( ! empty( $webhook_secret ) ) {
$signature = $this->generate_signature( $payload, $webhook_secret );
$headers['X-WPBridge-Signature'] = $signature;
}

// 发送请求
$response = wp_remote_post(
$webhook_url,
array(
'headers' => $headers,
'body' => $payload,
'timeout' => 10,
)
);
// 发送请求
$response = wp_remote_post( $webhook_url, [
'headers' => $headers,
'body' => $payload,
'timeout' => 10,
] );

if ( is_wp_error( $response ) ) {
throw new \Exception( $response->get_error_message() );
}
if ( is_wp_error( $response ) ) {
throw new \Exception( $response->get_error_message() );
}

$status_code = wp_remote_retrieve_response_code( $response );
$status_code = wp_remote_retrieve_response_code( $response );

if ( $status_code < 200 || $status_code >= 300 ) {
throw new \Exception(
sprintf(
/* translators: %d: HTTP status code */
__( 'Webhook 返回错误状态码: %d', 'wpbridge' ),
$status_code
)
);
}
if ( $status_code < 200 || $status_code >= 300 ) {
throw new \Exception(
sprintf(
/* translators: %d: HTTP status code */
__( 'Webhook 返回错误状态码: %d', 'wpbridge' ),
$status_code
)
);
}

Logger::debug(
'Webhook 发送成功',
array(
'url' => $webhook_url,
'status_code' => $status_code,
)
);
}
Logger::debug( 'Webhook 发送成功', [
'url' => $webhook_url,
'status_code' => $status_code,
] );
}

/**
* 构建 payload
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @param string $format 格式
* @return string
*/
private function build_payload( string $subject, string $message, array $data, string $format ): string {
$base_payload = array(
'event' => $data['type'] ?? 'notification',
'subject' => $subject,
'message' => $message,
'timestamp' => current_time( 'c' ),
'site' => array(
'name' => get_bloginfo( 'name' ),
'url' => get_site_url(),
),
'data' => $data,
);
/**
* 构建 payload
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @param string $format 格式
* @return string
*/
private function build_payload( string $subject, string $message, array $data, string $format ): string {
$base_payload = [
'event' => $data['type'] ?? 'notification',
'subject' => $subject,
'message' => $message,
'timestamp' => current_time( 'c' ),
'site' => [
'name' => get_bloginfo( 'name' ),
'url' => get_site_url(),
],
'data' => $data,
];

switch ( $format ) {
case 'slack':
return $this->format_slack( $subject, $message, $data );
switch ( $format ) {
case 'slack':
return $this->format_slack( $subject, $message, $data );

case 'discord':
return $this->format_discord( $subject, $message, $data );
case 'discord':
return $this->format_discord( $subject, $message, $data );

case 'teams':
return $this->format_teams( $subject, $message, $data );
case 'teams':
return $this->format_teams( $subject, $message, $data );

default:
return wp_json_encode( $base_payload );
}
}
default:
return wp_json_encode( $base_payload );
}
}

/**
* Slack 格式
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @return string
*/
private function format_slack( string $subject, string $message, array $data ): string {
$payload = array(
'text' => $subject,
'attachments' => array(
array(
'color' => $this->get_color_for_type( $data['type'] ?? 'info' ),
'text' => $message,
'footer' => 'WPBridge | ' . get_site_url(),
'ts' => time(),
),
),
);
/**
* Slack 格式
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @return string
*/
private function format_slack( string $subject, string $message, array $data ): string {
$payload = [
'text' => $subject,
'attachments' => [
[
'color' => $this->get_color_for_type( $data['type'] ?? 'info' ),
'text' => $message,
'footer' => 'WPBridge | ' . get_site_url(),
'ts' => time(),
],
],
];

return wp_json_encode( $payload );
}
return wp_json_encode( $payload );
}

/**
* Discord 格式
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @return string
*/
private function format_discord( string $subject, string $message, array $data ): string {
$payload = array(
'embeds' => array(
array(
'title' => $subject,
'description' => $message,
'color' => $this->get_color_int_for_type( $data['type'] ?? 'info' ),
'footer' => array(
'text' => 'WPBridge | ' . get_site_url(),
),
'timestamp' => gmdate( 'c' ),
),
),
);
/**
* Discord 格式
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @return string
*/
private function format_discord( string $subject, string $message, array $data ): string {
$payload = [
'embeds' => [
[
'title' => $subject,
'description' => $message,
'color' => $this->get_color_int_for_type( $data['type'] ?? 'info' ),
'footer' => [
'text' => 'WPBridge | ' . get_site_url(),
],
'timestamp' => gmdate( 'c' ),
],
],
];

return wp_json_encode( $payload );
}
return wp_json_encode( $payload );
}

/**
* Microsoft Teams 格式
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @return string
*/
private function format_teams( string $subject, string $message, array $data ): string {
$payload = array(
'@type' => 'MessageCard',
'@context' => 'http://schema.org/extensions',
'themeColor' => $this->get_color_hex_for_type( $data['type'] ?? 'info' ),
'summary' => $subject,
'sections' => array(
array(
'activityTitle' => $subject,
'text' => $message,
),
),
);
/**
* Microsoft Teams 格式
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @return string
*/
private function format_teams( string $subject, string $message, array $data ): string {
$payload = [
'@type' => 'MessageCard',
'@context' => 'http://schema.org/extensions',
'themeColor' => $this->get_color_hex_for_type( $data['type'] ?? 'info' ),
'summary' => $subject,
'sections' => [
[
'activityTitle' => $subject,
'text' => $message,
],
],
];

return wp_json_encode( $payload );
}
return wp_json_encode( $payload );
}

/**
* 生成签名
*
* @param string $payload 负载
* @param string $secret 密钥
* @return string
*/
private function generate_signature( string $payload, string $secret ): string {
return 'sha256=' . hash_hmac( 'sha256', $payload, $secret );
}
/**
* 生成签名
*
* @param string $payload 负载
* @param string $secret 密钥
* @return string
*/
private function generate_signature( string $payload, string $secret ): string {
return 'sha256=' . hash_hmac( 'sha256', $payload, $secret );
}

/**
* 获取类型对应的颜色Slack 格式)
*
* @param string $type 类型
* @return string
*/
private function get_color_for_type( string $type ): string {
$colors = array(
'update' => 'good',
'error' => 'danger',
'recovery' => 'good',
'warning' => 'warning',
);
/**
* 获取类型对应的颜色Slack 格式)
*
* @param string $type 类型
* @return string
*/
private function get_color_for_type( string $type ): string {
$colors = [
'update' => 'good',
'error' => 'danger',
'recovery' => 'good',
'warning' => 'warning',
];

return $colors[ $type ] ?? '#0073aa';
}
return $colors[ $type ] ?? '#0073aa';
}

/**
* 获取类型对应的颜色Discord 整数格式)
*
* @param string $type 类型
* @return int
*/
private function get_color_int_for_type( string $type ): int {
$colors = array(
'update' => 0x00aa00,
'error' => 0xaa0000,
'recovery' => 0x00aa00,
'warning' => 0xaaaa00,
);
/**
* 获取类型对应的颜色Discord 整数格式)
*
* @param string $type 类型
* @return int
*/
private function get_color_int_for_type( string $type ): int {
$colors = [
'update' => 0x00aa00,
'error' => 0xaa0000,
'recovery' => 0x00aa00,
'warning' => 0xaaaa00,
];

return $colors[ $type ] ?? 0x0073aa;
}
return $colors[ $type ] ?? 0x0073aa;
}

/**
* 获取类型对应的颜色(十六进制格式)
*
* @param string $type 类型
* @return string
*/
private function get_color_hex_for_type( string $type ): string {
$colors = array(
'update' => '00aa00',
'error' => 'aa0000',
'recovery' => '00aa00',
'warning' => 'aaaa00',
);
/**
* 获取类型对应的颜色(十六进制格式)
*
* @param string $type 类型
* @return string
*/
private function get_color_hex_for_type( string $type ): string {
$colors = [
'update' => '00aa00',
'error' => 'aa0000',
'recovery' => '00aa00',
'warning' => 'aaaa00',
];

return $colors[ $type ] ?? '0073aa';
}
return $colors[ $type ] ?? '0073aa';
}
}

View file

@ -14,7 +14,7 @@ use WPBridge\Cache\CacheManager;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -23,160 +23,157 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class BackgroundUpdater {

/**
* 定时任务钩子名称
*
* @var string
*/
const CRON_HOOK = 'wpbridge_update_sources';
/**
* 定时任务钩子名称
*
* @var string
*/
const CRON_HOOK = 'wpbridge_update_sources';

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 源管理器
*
* @var SourceManager
*/
private SourceManager $source_manager;
/**
* 源管理器
*
* @var SourceManager
*/
private SourceManager $source_manager;

/**
* 并行请求管理器
*
* @var ParallelRequestManager
*/
private ParallelRequestManager $parallel_manager;
/**
* 并行请求管理器
*
* @var ParallelRequestManager
*/
private ParallelRequestManager $parallel_manager;

/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;
/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->source_manager = new SourceManager( $settings );
$this->parallel_manager = new ParallelRequestManager( $settings->get_request_timeout() );
$this->cache = new CacheManager();
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->source_manager = new SourceManager( $settings );
$this->parallel_manager = new ParallelRequestManager( $settings->get_request_timeout() );
$this->cache = new CacheManager();

$this->init_hooks();
}
$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
add_action( self::CRON_HOOK, array( $this, 'run_update' ) );
}
/**
* 初始化钩子
*/
private function init_hooks(): void {
add_action( self::CRON_HOOK, [ $this, 'run_update' ] );
}

/**
* 调度更新任务
*/
public function schedule_update(): void {
if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
wp_schedule_event( time(), 'twicedaily', self::CRON_HOOK );
Logger::info( '已调度后台更新任务' );
}
}
/**
* 调度更新任务
*/
public function schedule_update(): void {
if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
wp_schedule_event( time(), 'twicedaily', self::CRON_HOOK );
Logger::info( '已调度后台更新任务' );
}
}

/**
* 取消调度
*/
public function unschedule(): void {
$timestamp = wp_next_scheduled( self::CRON_HOOK );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, self::CRON_HOOK );
Logger::info( '已取消后台更新任务' );
}
}
/**
* 取消调度
*/
public function unschedule(): void {
$timestamp = wp_next_scheduled( self::CRON_HOOK );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, self::CRON_HOOK );
Logger::info( '已取消后台更新任务' );
}
}

/**
* 执行更新
*/
public function run_update(): void {
Logger::info( '开始后台更新' );
/**
* 执行更新
*/
public function run_update(): void {
Logger::info( '开始后台更新' );

$sources = $this->source_manager->get_enabled_sorted();
$sources = $this->source_manager->get_enabled_sorted();

if ( empty( $sources ) ) {
Logger::debug( '没有启用的更新源' );
return;
}
if ( empty( $sources ) ) {
Logger::debug( '没有启用的更新源' );
return;
}

// 使用并行请求检查所有源
$results = $this->parallel_manager->check_multiple_sources( $sources );
// 使用并行请求检查所有源
$results = $this->parallel_manager->check_multiple_sources( $sources );

$success_count = 0;
$fail_count = 0;
$success_count = 0;
$fail_count = 0;

foreach ( $results as $source_id => $data ) {
if ( null !== $data ) {
// 缓存结果
$this->cache->set(
'source_data_' . $source_id,
$data,
$this->settings->get_cache_ttl()
);
++$success_count;
} else {
++$fail_count;
}
}
foreach ( $results as $source_id => $data ) {
if ( null !== $data ) {
// 缓存结果
$this->cache->set(
'source_data_' . $source_id,
$data,
$this->settings->get_cache_ttl()
);
$success_count++;
} else {
$fail_count++;
}
}

Logger::info(
'后台更新完成',
array(
'success' => $success_count,
'failed' => $fail_count,
)
);
}
Logger::info( '后台更新完成', [
'success' => $success_count,
'failed' => $fail_count,
] );
}

/**
* 手动触发更新
*
* @return array 更新结果
*/
public function trigger_update(): array {
$this->run_update();
/**
* 手动触发更新
*
* @return array 更新结果
*/
public function trigger_update(): array {
$this->run_update();

return array(
'status' => 'completed',
'time' => current_time( 'mysql' ),
);
}
return [
'status' => 'completed',
'time' => current_time( 'mysql' ),
];
}

/**
* 获取下次更新时间
*
* @return int|false 时间戳或 false
*/
public function get_next_scheduled() {
return wp_next_scheduled( self::CRON_HOOK );
}
/**
* 获取下次更新时间
*
* @return int|false 时间戳或 false
*/
public function get_next_scheduled() {
return wp_next_scheduled( self::CRON_HOOK );
}

/**
* 获取更新状态
*
* @return array
*/
public function get_status(): array {
$next = $this->get_next_scheduled();
/**
* 获取更新状态
*
* @return array
*/
public function get_status(): array {
$next = $this->get_next_scheduled();

return array(
'scheduled' => (bool) $next,
'next_run' => $next ? gmdate( 'Y-m-d H:i:s', $next ) : null,
'next_run_human' => $next ? human_time_diff( time(), $next ) : null,
);
}
return [
'scheduled' => (bool) $next,
'next_run' => $next ? gmdate( 'Y-m-d H:i:s', $next ) : null,
'next_run_human' => $next ? human_time_diff( time(), $next ) : null,
];
}
}

View file

@ -12,7 +12,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -21,140 +21,140 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class ConditionalRequest {

/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;
/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;

/**
* 缓存前缀
*
* @var string
*/
const CACHE_PREFIX = 'conditional_';
/**
* 缓存前缀
*
* @var string
*/
const CACHE_PREFIX = 'conditional_';

/**
* 构造函数
*/
public function __construct() {
$this->cache = new CacheManager();
}
/**
* 构造函数
*/
public function __construct() {
$this->cache = new CacheManager();
}

/**
* 构建条件请求头
*
* @param string $source_id 源 ID
* @return array
*/
public function build_headers( string $source_id ): array {
$cached = $this->get_cached_metadata( $source_id );
$headers = array();
/**
* 构建条件请求头
*
* @param string $source_id 源 ID
* @return array
*/
public function build_headers( string $source_id ): array {
$cached = $this->get_cached_metadata( $source_id );
$headers = [];

if ( ! empty( $cached['etag'] ) ) {
$headers['If-None-Match'] = $cached['etag'];
}
if ( ! empty( $cached['etag'] ) ) {
$headers['If-None-Match'] = $cached['etag'];
}

if ( ! empty( $cached['last_modified'] ) ) {
$headers['If-Modified-Since'] = $cached['last_modified'];
}
if ( ! empty( $cached['last_modified'] ) ) {
$headers['If-Modified-Since'] = $cached['last_modified'];
}

return $headers;
}
return $headers;
}

/**
* 处理响应
*
* @param string $source_id 源 ID
* @param array $response 响应数据
* @param array $headers 响应头
* @return array|null 处理后的数据304 时返回缓存数据
*/
public function process_response( string $source_id, ?array $response, array $headers ): ?array {
// 保存元数据
$metadata = array();
/**
* 处理响应
*
* @param string $source_id 源 ID
* @param array $response 响应数据
* @param array $headers 响应头
* @return array|null 处理后的数据304 时返回缓存数据
*/
public function process_response( string $source_id, ?array $response, array $headers ): ?array {
// 保存元数据
$metadata = [];

if ( ! empty( $headers['etag'] ) ) {
$metadata['etag'] = $headers['etag'];
}
if ( ! empty( $headers['etag'] ) ) {
$metadata['etag'] = $headers['etag'];
}

if ( ! empty( $headers['last-modified'] ) ) {
$metadata['last_modified'] = $headers['last-modified'];
}
if ( ! empty( $headers['last-modified'] ) ) {
$metadata['last_modified'] = $headers['last-modified'];
}

if ( ! empty( $metadata ) ) {
$this->save_metadata( $source_id, $metadata );
}
if ( ! empty( $metadata ) ) {
$this->save_metadata( $source_id, $metadata );
}

// 如果有新数据,缓存并返回
if ( null !== $response ) {
$this->save_cached_data( $source_id, $response );
return $response;
}
// 如果有新数据,缓存并返回
if ( null !== $response ) {
$this->save_cached_data( $source_id, $response );
return $response;
}

// 返回缓存数据
return $this->get_cached_data( $source_id );
}
// 返回缓存数据
return $this->get_cached_data( $source_id );
}

/**
* 处理 304 响应
*
* @param string $source_id 源 ID
* @return array|null 缓存的数据
*/
public function handle_not_modified( string $source_id ): ?array {
Logger::debug( '304 Not Modified', array( 'source' => $source_id ) );
return $this->get_cached_data( $source_id );
}
/**
* 处理 304 响应
*
* @param string $source_id 源 ID
* @return array|null 缓存的数据
*/
public function handle_not_modified( string $source_id ): ?array {
Logger::debug( '304 Not Modified', [ 'source' => $source_id ] );
return $this->get_cached_data( $source_id );
}

/**
* 获取缓存的元数据
*
* @param string $source_id 源 ID
* @return array
*/
private function get_cached_metadata( string $source_id ): array {
$cached = $this->cache->get( self::CACHE_PREFIX . 'meta_' . $source_id );
return is_array( $cached ) ? $cached : array();
}
/**
* 获取缓存的元数据
*
* @param string $source_id 源 ID
* @return array
*/
private function get_cached_metadata( string $source_id ): array {
$cached = $this->cache->get( self::CACHE_PREFIX . 'meta_' . $source_id );
return is_array( $cached ) ? $cached : [];
}

/**
* 保存元数据
*
* @param string $source_id 源 ID
* @param array $metadata 元数据
*/
private function save_metadata( string $source_id, array $metadata ): void {
$this->cache->set(
self::CACHE_PREFIX . 'meta_' . $source_id,
$metadata,
WEEK_IN_SECONDS
);
}
/**
* 保存元数据
*
* @param string $source_id 源 ID
* @param array $metadata 元数据
*/
private function save_metadata( string $source_id, array $metadata ): void {
$this->cache->set(
self::CACHE_PREFIX . 'meta_' . $source_id,
$metadata,
WEEK_IN_SECONDS
);
}

/**
* 获取缓存的数据
*
* @param string $source_id 源 ID
* @return array|null
*/
private function get_cached_data( string $source_id ): ?array {
$cached = $this->cache->get( self::CACHE_PREFIX . 'data_' . $source_id );
return is_array( $cached ) ? $cached : null;
}
/**
* 获取缓存的数据
*
* @param string $source_id 源 ID
* @return array|null
*/
private function get_cached_data( string $source_id ): ?array {
$cached = $this->cache->get( self::CACHE_PREFIX . 'data_' . $source_id );
return is_array( $cached ) ? $cached : null;
}

/**
* 保存缓存数据
*
* @param string $source_id 源 ID
* @param array $data 数据
*/
private function save_cached_data( string $source_id, array $data ): void {
$this->cache->set(
self::CACHE_PREFIX . 'data_' . $source_id,
$data,
DAY_IN_SECONDS
);
}
/**
* 保存缓存数据
*
* @param string $source_id 源 ID
* @param array $data 数据
*/
private function save_cached_data( string $source_id, array $data ): void {
$this->cache->set(
self::CACHE_PREFIX . 'data_' . $source_id,
$data,
DAY_IN_SECONDS
);
}
}

View file

@ -12,7 +12,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -21,155 +21,143 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class ParallelRequestManager {

/**
* 默认超时时间(秒)
*
* @var int
*/
private int $timeout = 10;
/**
* 默认超时时间(秒)
*
* @var int
*/
private int $timeout = 10;

/**
* 构造函数
*
* @param int $timeout 超时时间
*/
public function __construct( int $timeout = 10 ) {
$this->timeout = $timeout;
}
/**
* 构造函数
*
* @param int $timeout 超时时间
*/
public function __construct( int $timeout = 10 ) {
$this->timeout = $timeout;
}

/**
* 批量检查多个更新源
*
* @param SourceModel[] $sources 源列表
* @return array<string, array|null> 响应数据,键为源 ID
*/
public function check_multiple_sources( array $sources ): array {
if ( empty( $sources ) ) {
return array();
}
/**
* 批量检查多个更新源
*
* @param SourceModel[] $sources 源列表
* @return array<string, array|null> 响应数据,键为源 ID
*/
public function check_multiple_sources( array $sources ): array {
if ( empty( $sources ) ) {
return [];
}

$requests = array();
$requests = [];

foreach ( $sources as $source ) {
$requests[ $source->id ] = array(
'url' => $source->get_check_url(),
'type' => \WpOrg\Requests\Requests::GET,
'headers' => $source->get_headers(),
);
}
foreach ( $sources as $source ) {
$requests[ $source->id ] = [
'url' => $source->get_check_url(),
'type' => \WpOrg\Requests\Requests::GET,
'headers' => $source->get_headers(),
];
}

Logger::debug( '开始并行请求', array( 'count' => count( $requests ) ) );
Logger::debug( '开始并行请求', [ 'count' => count( $requests ) ] );

$start = microtime( true );
$start = microtime( true );

// 使用 WordPress Requests API 并行请求
$responses = \WpOrg\Requests\Requests::request_multiple(
$requests,
array(
'timeout' => $this->timeout,
'connect_timeout' => 5,
'follow_redirects' => true,
'redirects' => 3,
)
);
// 使用 WordPress Requests API 并行请求
$responses = \WpOrg\Requests\Requests::request_multiple(
$requests,
[
'timeout' => $this->timeout,
'connect_timeout' => 5,
'follow_redirects' => true,
'redirects' => 3,
]
);

$elapsed = round( ( microtime( true ) - $start ) * 1000 );
$elapsed = round( ( microtime( true ) - $start ) * 1000 );

Logger::debug(
'并行请求完成',
array(
'count' => count( $requests ),
'time_ms' => $elapsed,
)
);
Logger::debug( '并行请求完成', [
'count' => count( $requests ),
'time_ms' => $elapsed,
] );

return $this->process_responses( $responses );
}
return $this->process_responses( $responses );
}

/**
* 处理响应
*
* @param array $responses 响应数组
* @return array<string, array|null>
*/
private function process_responses( array $responses ): array {
$results = array();
/**
* 处理响应
*
* @param array $responses 响应数组
* @return array<string, array|null>
*/
private function process_responses( array $responses ): array {
$results = [];

foreach ( $responses as $source_id => $response ) {
if ( $response instanceof \WpOrg\Requests\Exception ) {
Logger::warning(
'请求失败',
array(
'source' => $source_id,
'error' => $response->getMessage(),
)
);
$results[ $source_id ] = null;
continue;
}
foreach ( $responses as $source_id => $response ) {
if ( $response instanceof \WpOrg\Requests\Exception ) {
Logger::warning( '请求失败', [
'source' => $source_id,
'error' => $response->getMessage(),
] );
$results[ $source_id ] = null;
continue;
}

if ( ! $response->success ) {
Logger::warning(
'请求返回非成功状态',
array(
'source' => $source_id,
'status' => $response->status_code,
)
);
$results[ $source_id ] = null;
continue;
}
if ( ! $response->success ) {
Logger::warning( '请求返回非成功状态', [
'source' => $source_id,
'status' => $response->status_code,
] );
$results[ $source_id ] = null;
continue;
}

$data = json_decode( $response->body, true );
$data = json_decode( $response->body, true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
Logger::warning(
'JSON 解析失败',
array(
'source' => $source_id,
'error' => json_last_error_msg(),
)
);
$results[ $source_id ] = null;
continue;
}
if ( json_last_error() !== JSON_ERROR_NONE ) {
Logger::warning( 'JSON 解析失败', [
'source' => $source_id,
'error' => json_last_error_msg(),
] );
$results[ $source_id ] = null;
continue;
}

$results[ $source_id ] = $data;
}
$results[ $source_id ] = $data;
}

return $results;
}
return $results;
}

/**
* 批量请求 URL
*
* @param array $urls URL 数组,键为标识符
* @param array $headers 公共请求头
* @return array<string, array|null>
*/
public function fetch_multiple( array $urls, array $headers = array() ): array {
if ( empty( $urls ) ) {
return array();
}
/**
* 批量请求 URL
*
* @param array $urls URL 数组,键为标识符
* @param array $headers 公共请求头
* @return array<string, array|null>
*/
public function fetch_multiple( array $urls, array $headers = [] ): array {
if ( empty( $urls ) ) {
return [];
}

$requests = array();
$requests = [];

foreach ( $urls as $key => $url ) {
$requests[ $key ] = array(
'url' => $url,
'type' => \WpOrg\Requests\Requests::GET,
'headers' => $headers,
);
}
foreach ( $urls as $key => $url ) {
$requests[ $key ] = [
'url' => $url,
'type' => \WpOrg\Requests\Requests::GET,
'headers' => $headers,
];
}

$responses = \WpOrg\Requests\Requests::request_multiple(
$requests,
array(
'timeout' => $this->timeout,
'connect_timeout' => 5,
)
);
$responses = \WpOrg\Requests\Requests::request_multiple(
$requests,
[
'timeout' => $this->timeout,
'connect_timeout' => 5,
]
);

return $this->process_responses( $responses );
}
return $this->process_responses( $responses );
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -20,99 +20,99 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class RequestDeduplicator {

/**
* 合并窗口时间(秒)
*
* @var int
*/
const MERGE_WINDOW = 5;
/**
* 合并窗口时间(秒)
*
* @var int
*/
const MERGE_WINDOW = 5;

/**
* 锁前缀
*
* @var string
*/
const LOCK_PREFIX = 'wpbridge_lock_';
/**
* 锁前缀
*
* @var string
*/
const LOCK_PREFIX = 'wpbridge_lock_';

/**
* 尝试获取锁
*
* @param string $source_id 源 ID
* @return bool 是否成功获取锁
*/
public function acquire_lock( string $source_id ): bool {
$lock_key = self::LOCK_PREFIX . $source_id;
/**
* 尝试获取锁
*
* @param string $source_id 源 ID
* @return bool 是否成功获取锁
*/
public function acquire_lock( string $source_id ): bool {
$lock_key = self::LOCK_PREFIX . $source_id;

// 检查是否已有锁
if ( get_transient( $lock_key ) ) {
Logger::debug( '请求被去重', array( 'source' => $source_id ) );
return false;
}
// 检查是否已有锁
if ( get_transient( $lock_key ) ) {
Logger::debug( '请求被去重', [ 'source' => $source_id ] );
return false;
}

// 设置锁
set_transient( $lock_key, time(), self::MERGE_WINDOW );
return true;
}
// 设置锁
set_transient( $lock_key, time(), self::MERGE_WINDOW );
return true;
}

/**
* 释放锁
*
* @param string $source_id 源 ID
*/
public function release_lock( string $source_id ): void {
delete_transient( self::LOCK_PREFIX . $source_id );
}
/**
* 释放锁
*
* @param string $source_id 源 ID
*/
public function release_lock( string $source_id ): void {
delete_transient( self::LOCK_PREFIX . $source_id );
}

/**
* 检查是否有锁
*
* @param string $source_id 源 ID
* @return bool
*/
public function has_lock( string $source_id ): bool {
return (bool) get_transient( self::LOCK_PREFIX . $source_id );
}
/**
* 检查是否有锁
*
* @param string $source_id 源 ID
* @return bool
*/
public function has_lock( string $source_id ): bool {
return (bool) get_transient( self::LOCK_PREFIX . $source_id );
}

/**
* 等待锁释放并获取结果
*
* @param string $source_id 源 ID
* @param callable $callback 获取结果的回调
* @param int $max_wait 最大等待时间(秒)
* @return mixed
*/
public function wait_and_get( string $source_id, callable $callback, int $max_wait = 10 ) {
$start = time();
/**
* 等待锁释放并获取结果
*
* @param string $source_id 源 ID
* @param callable $callback 获取结果的回调
* @param int $max_wait 最大等待时间(秒)
* @return mixed
*/
public function wait_and_get( string $source_id, callable $callback, int $max_wait = 10 ) {
$start = time();

while ( $this->has_lock( $source_id ) ) {
if ( ( time() - $start ) >= $max_wait ) {
Logger::warning( '等待锁超时', array( 'source' => $source_id ) );
break;
}
usleep( 100000 ); // 100ms
}
while ( $this->has_lock( $source_id ) ) {
if ( ( time() - $start ) >= $max_wait ) {
Logger::warning( '等待锁超时', [ 'source' => $source_id ] );
break;
}
usleep( 100000 ); // 100ms
}

return $callback();
}
return $callback();
}

/**
* 带锁执行操作
*
* @param string $source_id 源 ID
* @param callable $callback 操作回调
* @return mixed
*/
public function execute_with_lock( string $source_id, callable $callback ) {
if ( ! $this->acquire_lock( $source_id ) ) {
// 已有请求在进行中,等待结果
return $this->wait_and_get( $source_id, $callback );
}
/**
* 带锁执行操作
*
* @param string $source_id 源 ID
* @param callable $callback 操作回调
* @return mixed
*/
public function execute_with_lock( string $source_id, callable $callback ) {
if ( ! $this->acquire_lock( $source_id ) ) {
// 已有请求在进行中,等待结果
return $this->wait_and_get( $source_id, $callback );
}

try {
$result = $callback();
return $result;
} finally {
$this->release_lock( $source_id );
}
}
try {
$result = $callback();
return $result;
} finally {
$this->release_lock( $source_id );
}
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\Security;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,196 +17,196 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class Encryption {

/**
* 加密方法
*
* @var string
*/
const METHOD = 'aes-256-cbc';
/**
* 加密方法
*
* @var string
*/
const METHOD = 'aes-256-cbc';

/**
* 获取加密密钥
*
* @return string
*/
private static function get_key(): string {
// 优先使用自定义密钥
if ( defined( 'WPBRIDGE_ENCRYPTION_KEY' ) && WPBRIDGE_ENCRYPTION_KEY ) {
return WPBRIDGE_ENCRYPTION_KEY;
}
/**
* 获取加密密钥
*
* @return string
*/
private static function get_key(): string {
// 优先使用自定义密钥
if ( defined( 'WPBRIDGE_ENCRYPTION_KEY' ) && WPBRIDGE_ENCRYPTION_KEY ) {
return WPBRIDGE_ENCRYPTION_KEY;
}

// 使用 WordPress 的 AUTH_KEY
if ( defined( 'AUTH_KEY' ) && AUTH_KEY ) {
return AUTH_KEY;
}
// 使用 WordPress 的 AUTH_KEY
if ( defined( 'AUTH_KEY' ) && AUTH_KEY ) {
return AUTH_KEY;
}

// 最后使用 SECURE_AUTH_KEY
if ( defined( 'SECURE_AUTH_KEY' ) && SECURE_AUTH_KEY ) {
return SECURE_AUTH_KEY;
}
// 最后使用 SECURE_AUTH_KEY
if ( defined( 'SECURE_AUTH_KEY' ) && SECURE_AUTH_KEY ) {
return SECURE_AUTH_KEY;
}

// 如果都没有,生成并存储一个随机密钥
$key = get_option( 'wpbridge_encryption_key' );
if ( empty( $key ) ) {
$key = bin2hex( random_bytes( 32 ) );
update_option( 'wpbridge_encryption_key', $key, false );
}
return $key;
}
// 如果都没有,生成并存储一个随机密钥
$key = get_option( 'wpbridge_encryption_key' );
if ( empty( $key ) ) {
$key = bin2hex( random_bytes( 32 ) );
update_option( 'wpbridge_encryption_key', $key, false );
}
return $key;
}

/**
* 加密数据
*
* @param string $data 明文数据
* @return string 加密后的数据base64 编码)
*/
public static function encrypt( string $data ): string {
if ( empty( $data ) ) {
return '';
}
/**
* 加密数据
*
* @param string $data 明文数据
* @return string 加密后的数据base64 编码)
*/
public static function encrypt( string $data ): string {
if ( empty( $data ) ) {
return '';
}

$key = hash( 'sha256', self::get_key(), true );
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::METHOD ) );
$key = hash( 'sha256', self::get_key(), true );
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::METHOD ) );

$encrypted = openssl_encrypt( $data, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
$encrypted = openssl_encrypt( $data, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );

if ( false === $encrypted ) {
return '';
}
if ( false === $encrypted ) {
return '';
}

// 将 IV 和加密数据一起存储
return base64_encode( $iv . $encrypted );
}
// 将 IV 和加密数据一起存储
return base64_encode( $iv . $encrypted );
}

/**
* 解密数据
*
* @param string $data 加密数据base64 编码)
* @return string 解密后的明文
*/
public static function decrypt( string $data ): string {
if ( empty( $data ) ) {
return '';
}
/**
* 解密数据
*
* @param string $data 加密数据base64 编码)
* @return string 解密后的明文
*/
public static function decrypt( string $data ): string {
if ( empty( $data ) ) {
return '';
}

$data = base64_decode( $data );
$data = base64_decode( $data );

if ( false === $data ) {
return '';
}
if ( false === $data ) {
return '';
}

$key = hash( 'sha256', self::get_key(), true );
$iv_length = openssl_cipher_iv_length( self::METHOD );
$key = hash( 'sha256', self::get_key(), true );
$iv_length = openssl_cipher_iv_length( self::METHOD );

if ( strlen( $data ) < $iv_length ) {
return '';
}
if ( strlen( $data ) < $iv_length ) {
return '';
}

$iv = substr( $data, 0, $iv_length );
$encrypted = substr( $data, $iv_length );
$iv = substr( $data, 0, $iv_length );
$encrypted = substr( $data, $iv_length );

$decrypted = openssl_decrypt( $encrypted, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
$decrypted = openssl_decrypt( $encrypted, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );

if ( false === $decrypted ) {
return '';
}
if ( false === $decrypted ) {
return '';
}

return $decrypted;
}
return $decrypted;
}

/**
* 检查数据是否已加密
*
* @param string $data 数据
* @return bool
*/
public static function is_encrypted( string $data ): bool {
if ( empty( $data ) ) {
return false;
}
/**
* 检查数据是否已加密
*
* @param string $data 数据
* @return bool
*/
public static function is_encrypted( string $data ): bool {
if ( empty( $data ) ) {
return false;
}

// 检查是否是有效的 base64
$decoded = base64_decode( $data, true );
// 检查是否是有效的 base64
$decoded = base64_decode( $data, true );

if ( false === $decoded ) {
return false;
}
if ( false === $decoded ) {
return false;
}

// 检查长度是否足够包含 IV
$iv_length = openssl_cipher_iv_length( self::METHOD );
// 检查长度是否足够包含 IV
$iv_length = openssl_cipher_iv_length( self::METHOD );

return strlen( $decoded ) > $iv_length;
}
return strlen( $decoded ) > $iv_length;
}

/**
* 安全地存储敏感数据
*
* @param string $key 选项键
* @param string $value 敏感值
* @return bool
*/
public static function store_secure( string $key, string $value ): bool {
$encrypted = self::encrypt( $value );
return update_option( 'wpbridge_secure_' . $key, $encrypted );
}
/**
* 安全地存储敏感数据
*
* @param string $key 选项键
* @param string $value 敏感值
* @return bool
*/
public static function store_secure( string $key, string $value ): bool {
$encrypted = self::encrypt( $value );
return update_option( 'wpbridge_secure_' . $key, $encrypted );
}

/**
* 安全地获取敏感数据
*
* @param string $key 选项键
* @param string $default 默认值
* @return string
*/
public static function get_secure( string $key, string $default = '' ): string {
$encrypted = get_option( 'wpbridge_secure_' . $key, '' );
/**
* 安全地获取敏感数据
*
* @param string $key 选项键
* @param string $default 默认值
* @return string
*/
public static function get_secure( string $key, string $default = '' ): string {
$encrypted = get_option( 'wpbridge_secure_' . $key, '' );

if ( empty( $encrypted ) ) {
return $default;
}
if ( empty( $encrypted ) ) {
return $default;
}

$decrypted = self::decrypt( $encrypted );
$decrypted = self::decrypt( $encrypted );

return ! empty( $decrypted ) ? $decrypted : $default;
}
return ! empty( $decrypted ) ? $decrypted : $default;
}

/**
* 删除安全存储的数据
*
* @param string $key 选项键
* @return bool
*/
public static function delete_secure( string $key ): bool {
return delete_option( 'wpbridge_secure_' . $key );
}
/**
* 删除安全存储的数据
*
* @param string $key 选项键
* @return bool
*/
public static function delete_secure( string $key ): bool {
return delete_option( 'wpbridge_secure_' . $key );
}

/**
* 生成随机令牌
*
* @param int $length 长度
* @return string
*/
public static function generate_token( int $length = 32 ): string {
return bin2hex( random_bytes( $length / 2 ) );
}
/**
* 生成随机令牌
*
* @param int $length 长度
* @return string
*/
public static function generate_token( int $length = 32 ): string {
return bin2hex( random_bytes( $length / 2 ) );
}

/**
* 哈希密码/令牌(用于比较)
*
* @param string $data 数据
* @return string
*/
public static function hash( string $data ): string {
return hash( 'sha256', $data . self::get_key() );
}
/**
* 哈希密码/令牌(用于比较)
*
* @param string $data 数据
* @return string
*/
public static function hash( string $data ): string {
return hash( 'sha256', $data . self::get_key() );
}

/**
* 验证哈希
*
* @param string $data 原始数据
* @param string $hash 哈希值
* @return bool
*/
public static function verify_hash( string $data, string $hash ): bool {
return hash_equals( self::hash( $data ), $hash );
}
/**
* 验证哈希
*
* @param string $data 原始数据
* @param string $hash 哈希值
* @return bool
*/
public static function verify_hash( string $data, string $hash ): bool {
return hash_equals( self::hash( $data ), $hash );
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\Security;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,213 +17,213 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class Validator {

/**
* 校验 URL
*
* @param string $url URL
* @return bool
*/
public static function is_valid_url( string $url ): bool {
if ( empty( $url ) ) {
return false;
}
/**
* 校验 URL
*
* @param string $url URL
* @return bool
*/
public static function is_valid_url( string $url ): bool {
if ( empty( $url ) ) {
return false;
}

// 基本格式校验
if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
return false;
}
// 基本格式校验
if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
return false;
}

// 只允许 http 和 https
$scheme = parse_url( $url, PHP_URL_SCHEME );
if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
return false;
}
// 只允许 http 和 https
$scheme = parse_url( $url, PHP_URL_SCHEME );
if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
return false;
}

// 检查是否有主机名
$host = parse_url( $url, PHP_URL_HOST );
if ( empty( $host ) ) {
return false;
}
// 检查是否有主机名
$host = parse_url( $url, PHP_URL_HOST );
if ( empty( $host ) ) {
return false;
}

// 禁止本地地址(安全考虑)
if ( self::is_local_address( $host ) ) {
return false;
}
// 禁止本地地址(安全考虑)
if ( self::is_local_address( $host ) ) {
return false;
}

return true;
}
return true;
}

/**
* 检查是否是本地地址
*
* @param string $host 主机名
* @return bool
*/
private static function is_local_address( string $host ): bool {
// 本地主机名
$local_hosts = array( 'localhost', '127.0.0.1', '::1' );
/**
* 检查是否是本地地址
*
* @param string $host 主机名
* @return bool
*/
private static function is_local_address( string $host ): bool {
// 本地主机名
$local_hosts = [ 'localhost', '127.0.0.1', '::1' ];

if ( in_array( $host, $local_hosts, true ) ) {
return true;
}
if ( in_array( $host, $local_hosts, true ) ) {
return true;
}

// 私有 IP 范围
$ip = gethostbyname( $host );
// 私有 IP 范围
$ip = gethostbyname( $host );

if ( $ip === $host ) {
// 无法解析,为安全起见视为本地地址
return true;
}
if ( $ip === $host ) {
// 无法解析,为安全起见视为本地地址
return true;
}

// 检查私有 IP 范围IPv4
$private_ranges = array(
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'127.0.0.0/8',
);
// 检查私有 IP 范围IPv4
$private_ranges = [
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'127.0.0.0/8',
];

foreach ( $private_ranges as $range ) {
if ( self::ip_in_range( $ip, $range ) ) {
return true;
}
}
foreach ( $private_ranges as $range ) {
if ( self::ip_in_range( $ip, $range ) ) {
return true;
}
}

// 检查 IPv6 私有地址
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
// fc00::/7 (Unique Local Addresses)
// fe80::/10 (Link-Local Addresses)
if ( preg_match( '/^(fc|fd|fe80)/i', $ip ) ) {
return true;
}
}
// 检查 IPv6 私有地址
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
// fc00::/7 (Unique Local Addresses)
// fe80::/10 (Link-Local Addresses)
if ( preg_match( '/^(fc|fd|fe80)/i', $ip ) ) {
return true;
}
}

return false;
}
return false;
}

/**
* 检查 IP 是否在范围内
*
* @param string $ip IP 地址
* @param string $range CIDR 范围
* @return bool
*/
private static function ip_in_range( string $ip, string $range ): bool {
list( $subnet, $bits ) = explode( '/', $range );
/**
* 检查 IP 是否在范围内
*
* @param string $ip IP 地址
* @param string $range CIDR 范围
* @return bool
*/
private static function ip_in_range( string $ip, string $range ): bool {
list( $subnet, $bits ) = explode( '/', $range );

$ip_long = ip2long( $ip );
$subnet_long = ip2long( $subnet );
$mask = -1 << ( 32 - (int) $bits );
$ip_long = ip2long( $ip );
$subnet_long = ip2long( $subnet );
$mask = -1 << ( 32 - (int) $bits );

return ( $ip_long & $mask ) === ( $subnet_long & $mask );
}
return ( $ip_long & $mask ) === ( $subnet_long & $mask );
}

/**
* 校验版本号
*
* @param string $version 版本号
* @return bool
*/
public static function is_valid_version( string $version ): bool {
if ( empty( $version ) ) {
return false;
}
/**
* 校验版本号
*
* @param string $version 版本号
* @return bool
*/
public static function is_valid_version( string $version ): bool {
if ( empty( $version ) ) {
return false;
}

// 支持语义化版本和 WordPress 风格版本
// 例如: 1.0.0, 1.0, 1.0.0-beta, 1.0.0-rc.1
$pattern = '/^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$/';
// 支持语义化版本和 WordPress 风格版本
// 例如: 1.0.0, 1.0, 1.0.0-beta, 1.0.0-rc.1
$pattern = '/^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$/';

return (bool) preg_match( $pattern, $version );
}
return (bool) preg_match( $pattern, $version );
}

/**
* 校验 slug
*
* @param string $slug Slug
* @return bool
*/
public static function is_valid_slug( string $slug ): bool {
if ( empty( $slug ) ) {
return true; // 空 slug 是允许的(表示匹配所有)
}
/**
* 校验 slug
*
* @param string $slug Slug
* @return bool
*/
public static function is_valid_slug( string $slug ): bool {
if ( empty( $slug ) ) {
return true; // 空 slug 是允许的(表示匹配所有)
}

// 只允许小写字母、数字、连字符
$pattern = '/^[a-z0-9-]+$/';
// 只允许小写字母、数字、连字符
$pattern = '/^[a-z0-9-]+$/';

return (bool) preg_match( $pattern, $slug );
}
return (bool) preg_match( $pattern, $slug );
}

/**
* 校验 JSON 响应结构
*
* @param array $data 数据
* @param array $required 必需字段
* @return array 错误数组
*/
public static function validate_json_structure( array $data, array $required ): array {
$errors = array();
/**
* 校验 JSON 响应结构
*
* @param array $data 数据
* @param array $required 必需字段
* @return array 错误数组
*/
public static function validate_json_structure( array $data, array $required ): array {
$errors = [];

foreach ( $required as $field ) {
if ( ! isset( $data[ $field ] ) ) {
$errors[] = sprintf( __( '缺少必需字段: %s', 'wpbridge' ), $field );
}
}
foreach ( $required as $field ) {
if ( ! isset( $data[ $field ] ) ) {
$errors[] = sprintf( __( '缺少必需字段: %s', 'wpbridge' ), $field );
}
}

return $errors;
}
return $errors;
}

/**
* 校验更新信息 JSON
*
* @param array $data 数据
* @return array 错误数组
*/
public static function validate_update_info( array $data ): array {
$errors = array();
/**
* 校验更新信息 JSON
*
* @param array $data 数据
* @return array 错误数组
*/
public static function validate_update_info( array $data ): array {
$errors = [];

// 必需字段
if ( empty( $data['version'] ) ) {
$errors[] = __( '缺少版本号', 'wpbridge' );
} elseif ( ! self::is_valid_version( $data['version'] ) ) {
$errors[] = __( '无效的版本号格式', 'wpbridge' );
}
// 必需字段
if ( empty( $data['version'] ) ) {
$errors[] = __( '缺少版本号', 'wpbridge' );
} elseif ( ! self::is_valid_version( $data['version'] ) ) {
$errors[] = __( '无效的版本号格式', 'wpbridge' );
}

// 下载 URL
$download_url = $data['download_url'] ?? $data['package'] ?? '';
if ( ! empty( $download_url ) && ! self::is_valid_url( $download_url ) ) {
$errors[] = __( '无效的下载 URL', 'wpbridge' );
}
// 下载 URL
$download_url = $data['download_url'] ?? $data['package'] ?? '';
if ( ! empty( $download_url ) && ! self::is_valid_url( $download_url ) ) {
$errors[] = __( '无效的下载 URL', 'wpbridge' );
}

return $errors;
}
return $errors;
}

/**
* 清理 HTML
*
* @param string $html HTML 内容
* @return string
*/
public static function sanitize_html( string $html ): string {
return wp_kses_post( $html );
}
/**
* 清理 HTML
*
* @param string $html HTML 内容
* @return string
*/
public static function sanitize_html( string $html ): string {
return wp_kses_post( $html );
}

/**
* 清理文本
*
* @param string $text 文本
* @return string
*/
public static function sanitize_text( string $text ): string {
return sanitize_text_field( $text );
}
/**
* 清理文本
*
* @param string $text 文本
* @return string
*/
public static function sanitize_text( string $text ): string {
return sanitize_text_field( $text );
}

/**
* 清理 URL
*
* @param string $url URL
* @return string
*/
public static function sanitize_url( string $url ): string {
return esc_url_raw( $url );
}
/**
* 清理 URL
*
* @param string $url URL
* @return string
*/
public static function sanitize_url( string $url ): string {
return esc_url_raw( $url );
}
}

View file

@ -14,7 +14,7 @@ use WPBridge\UpdateSource\SourceManager;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -22,342 +22,333 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class GroupManager {

/**
* 选项名称
*
* @var string
*/
const OPTION_NAME = 'wpbridge_source_groups';
/**
* 选项名称
*
* @var string
*/
const OPTION_NAME = 'wpbridge_source_groups';

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 源管理器
*
* @var SourceManager
*/
private SourceManager $source_manager;
/**
* 源管理器
*
* @var SourceManager
*/
private SourceManager $source_manager;

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->source_manager = new SourceManager( $settings );
}
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->source_manager = new SourceManager( $settings );
}

/**
* 获取所有分组
*
* @return GroupModel[]
*/
public function get_all(): array {
$groups_data = get_option( self::OPTION_NAME, array() );
$groups = array();
/**
* 获取所有分组
*
* @return GroupModel[]
*/
public function get_all(): array {
$groups_data = get_option( self::OPTION_NAME, [] );
$groups = [];

foreach ( $groups_data as $data ) {
$groups[] = GroupModel::from_array( $data );
}
foreach ( $groups_data as $data ) {
$groups[] = GroupModel::from_array( $data );
}

return $groups;
}
return $groups;
}

/**
* 获取单个分组
*
* @param string $id 分组 ID
* @return GroupModel|null
*/
public function get( string $id ): ?GroupModel {
$groups = $this->get_all();
/**
* 获取单个分组
*
* @param string $id 分组 ID
* @return GroupModel|null
*/
public function get( string $id ): ?GroupModel {
$groups = $this->get_all();

foreach ( $groups as $group ) {
if ( $group->id === $id ) {
return $group;
}
}
foreach ( $groups as $group ) {
if ( $group->id === $id ) {
return $group;
}
}

return null;
}
return null;
}

/**
* 添加分组
*
* @param GroupModel $group 分组模型
* @return bool
*/
public function add( GroupModel $group ): bool {
if ( ! $group->is_valid() ) {
return false;
}
/**
* 添加分组
*
* @param GroupModel $group 分组模型
* @return bool
*/
public function add( GroupModel $group ): bool {
if ( ! $group->is_valid() ) {
return false;
}

$groups = $this->get_all();
$groups = $this->get_all();

// 生成 ID
if ( empty( $group->id ) ) {
$group->id = 'group_' . wp_generate_uuid4();
}
// 生成 ID
if ( empty( $group->id ) ) {
$group->id = 'group_' . wp_generate_uuid4();
}

$group->created_at = current_time( 'mysql' );
$group->updated_at = $group->created_at;
$group->created_at = current_time( 'mysql' );
$group->updated_at = $group->created_at;

// 加密共享认证令牌
if ( ! empty( $group->shared_auth_token ) ) {
$group->shared_auth_token = Encryption::encrypt( $group->shared_auth_token );
}
// 加密共享认证令牌
if ( ! empty( $group->shared_auth_token ) ) {
$group->shared_auth_token = Encryption::encrypt( $group->shared_auth_token );
}

$groups[] = $group;
$groups[] = $group;

Logger::info(
'添加源分组',
array(
'id' => $group->id,
'name' => $group->name,
)
);
Logger::info( '添加源分组', [ 'id' => $group->id, 'name' => $group->name ] );

return $this->save_groups( $groups );
}
return $this->save_groups( $groups );
}

/**
* 更新分组
*
* @param GroupModel $group 分组模型
* @return bool
*/
public function update( GroupModel $group ): bool {
if ( ! $group->is_valid() ) {
return false;
}
/**
* 更新分组
*
* @param GroupModel $group 分组模型
* @return bool
*/
public function update( GroupModel $group ): bool {
if ( ! $group->is_valid() ) {
return false;
}

$groups = $this->get_all();
$found = false;
$groups = $this->get_all();
$found = false;

foreach ( $groups as $index => $existing ) {
if ( $existing->id === $group->id ) {
$group->updated_at = current_time( 'mysql' );
$group->created_at = $existing->created_at;
foreach ( $groups as $index => $existing ) {
if ( $existing->id === $group->id ) {
$group->updated_at = current_time( 'mysql' );
$group->created_at = $existing->created_at;

// 处理共享认证令牌
if ( $group->shared_auth_token === '********' || empty( $group->shared_auth_token ) ) {
$group->shared_auth_token = $existing->shared_auth_token;
} else {
$group->shared_auth_token = Encryption::encrypt( $group->shared_auth_token );
}
// 处理共享认证令牌
if ( $group->shared_auth_token === '********' || empty( $group->shared_auth_token ) ) {
$group->shared_auth_token = $existing->shared_auth_token;
} else {
$group->shared_auth_token = Encryption::encrypt( $group->shared_auth_token );
}

$groups[ $index ] = $group;
$found = true;
break;
}
}
$groups[ $index ] = $group;
$found = true;
break;
}
}

if ( ! $found ) {
return false;
}
if ( ! $found ) {
return false;
}

Logger::info( '更新源分组', array( 'id' => $group->id ) );
Logger::info( '更新源分组', [ 'id' => $group->id ] );

return $this->save_groups( $groups );
}
return $this->save_groups( $groups );
}

/**
* 删除分组
*
* @param string $id 分组 ID
* @return bool
*/
public function delete( string $id ): bool {
$groups = $this->get_all();
$new_groups = array();
/**
* 删除分组
*
* @param string $id 分组 ID
* @return bool
*/
public function delete( string $id ): bool {
$groups = $this->get_all();
$new_groups = [];

foreach ( $groups as $group ) {
if ( $group->id !== $id ) {
$new_groups[] = $group;
}
}
foreach ( $groups as $group ) {
if ( $group->id !== $id ) {
$new_groups[] = $group;
}
}

if ( count( $new_groups ) === count( $groups ) ) {
return false;
}
if ( count( $new_groups ) === count( $groups ) ) {
return false;
}

Logger::info( '删除源分组', array( 'id' => $id ) );
Logger::info( '删除源分组', [ 'id' => $id ] );

return $this->save_groups( $new_groups );
}
return $this->save_groups( $new_groups );
}

/**
* 切换分组状态
*
* @param string $id 分组 ID
* @param bool $enabled 是否启用
* @return bool
*/
public function toggle( string $id, bool $enabled ): bool {
$group = $this->get( $id );
/**
* 切换分组状态
*
* @param string $id 分组 ID
* @param bool $enabled 是否启用
* @return bool
*/
public function toggle( string $id, bool $enabled ): bool {
$group = $this->get( $id );

if ( null === $group ) {
return false;
}
if ( null === $group ) {
return false;
}

// 先更新分组状态
$group->enabled = $enabled;
if ( ! $this->update( $group ) ) {
return false;
}
// 先更新分组状态
$group->enabled = $enabled;
if ( ! $this->update( $group ) ) {
return false;
}

// 然后批量更新源状态,记录失败的源
$failed_sources = array();
foreach ( $group->source_ids as $source_id ) {
if ( ! $this->source_manager->toggle( $source_id, $enabled ) ) {
$failed_sources[] = $source_id;
}
}
// 然后批量更新源状态,记录失败的源
$failed_sources = [];
foreach ( $group->source_ids as $source_id ) {
if ( ! $this->source_manager->toggle( $source_id, $enabled ) ) {
$failed_sources[] = $source_id;
}
}

if ( ! empty( $failed_sources ) ) {
Logger::warning(
'部分源状态切换失败',
array(
'group_id' => $id,
'failed' => $failed_sources,
)
);
}
if ( ! empty( $failed_sources ) ) {
Logger::warning( '部分源状态切换失败', [
'group_id' => $id,
'failed' => $failed_sources,
] );
}

return true;
}
return true;
}

/**
* 获取分组内的所有源
*
* @param string $group_id 分组 ID
* @return array
*/
public function get_group_sources( string $group_id ): array {
$group = $this->get( $group_id );
/**
* 获取分组内的所有源
*
* @param string $group_id 分组 ID
* @return array
*/
public function get_group_sources( string $group_id ): array {
$group = $this->get( $group_id );

if ( null === $group ) {
return array();
}
if ( null === $group ) {
return [];
}

$sources = array();
foreach ( $group->source_ids as $source_id ) {
$source = $this->source_manager->get( $source_id );
if ( null !== $source ) {
$sources[] = $source;
}
}
$sources = [];
foreach ( $group->source_ids as $source_id ) {
$source = $this->source_manager->get( $source_id );
if ( null !== $source ) {
$sources[] = $source;
}
}

return $sources;
}
return $sources;
}

/**
* 将源添加到分组
*
* @param string $group_id 分组 ID
* @param string $source_id 源 ID
* @return bool
*/
public function add_source_to_group( string $group_id, string $source_id ): bool {
// 权限检查
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
/**
* 将源添加到分组
*
* @param string $group_id 分组 ID
* @param string $source_id 源 ID
* @return bool
*/
public function add_source_to_group( string $group_id, string $source_id ): bool {
// 权限检查
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}

$group = $this->get( $group_id );
$group = $this->get( $group_id );

if ( null === $group ) {
return false;
}
if ( null === $group ) {
return false;
}

$group->add_source( $source_id );
$group->add_source( $source_id );

return $this->update( $group );
}
return $this->update( $group );
}

/**
* 从分组移除源
*
* @param string $group_id 分组 ID
* @param string $source_id 源 ID
* @return bool
*/
public function remove_source_from_group( string $group_id, string $source_id ): bool {
// 权限检查
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
/**
* 从分组移除源
*
* @param string $group_id 分组 ID
* @param string $source_id 源 ID
* @return bool
*/
public function remove_source_from_group( string $group_id, string $source_id ): bool {
// 权限检查
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}

$group = $this->get( $group_id );
$group = $this->get( $group_id );

if ( null === $group ) {
return false;
}
if ( null === $group ) {
return false;
}

$group->remove_source( $source_id );
$group->remove_source( $source_id );

return $this->update( $group );
}
return $this->update( $group );
}

/**
* 获取源所属的分组
*
* @param string $source_id 源 ID
* @return GroupModel[]
*/
public function get_source_groups( string $source_id ): array {
$groups = $this->get_all();
$source_groups = array();
/**
* 获取源所属的分组
*
* @param string $source_id 源 ID
* @return GroupModel[]
*/
public function get_source_groups( string $source_id ): array {
$groups = $this->get_all();
$source_groups = [];

foreach ( $groups as $group ) {
if ( $group->has_source( $source_id ) ) {
$source_groups[] = $group;
}
}
foreach ( $groups as $group ) {
if ( $group->has_source( $source_id ) ) {
$source_groups[] = $group;
}
}

return $source_groups;
}
return $source_groups;
}

/**
* 保存分组数据
*
* @param GroupModel[] $groups 分组列表
* @return bool
*/
private function save_groups( array $groups ): bool {
$data = array_map( fn( $g ) => $g->to_array(), $groups );
return update_option( self::OPTION_NAME, $data );
}
/**
* 保存分组数据
*
* @param GroupModel[] $groups 分组列表
* @return bool
*/
private function save_groups( array $groups ): bool {
$data = array_map( fn( $g ) => $g->to_array(), $groups );
return update_option( self::OPTION_NAME, $data );
}

/**
* 获取统计信息
*
* @return array
*/
public function get_stats(): array {
$groups = $this->get_all();
$total = count( $groups );
$enabled = 0;
$total_sources = 0;
/**
* 获取统计信息
*
* @return array
*/
public function get_stats(): array {
$groups = $this->get_all();
$total = count( $groups );
$enabled = 0;
$total_sources = 0;

foreach ( $groups as $group ) {
if ( $group->enabled ) {
++$enabled;
}
$total_sources += $group->get_source_count();
}
foreach ( $groups as $group ) {
if ( $group->enabled ) {
$enabled++;
}
$total_sources += $group->get_source_count();
}

return array(
'total' => $total,
'enabled' => $enabled,
'disabled' => $total - $enabled,
'total_sources' => $total_sources,
);
}
return [
'total' => $total,
'enabled' => $enabled,
'disabled' => $total - $enabled,
'total_sources' => $total_sources,
];
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\SourceGroup;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,185 +17,183 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class GroupModel {

/**
* 唯一标识
*
* @var string
*/
public string $id = '';
/**
* 唯一标识
*
* @var string
*/
public string $id = '';

/**
* 分组名称
*
* @var string
*/
public string $name = '';
/**
* 分组名称
*
* @var string
*/
public string $name = '';

/**
* 分组描述
*
* @var string
*/
public string $description = '';
/**
* 分组描述
*
* @var string
*/
public string $description = '';

/**
* 包含的源 ID 列表
*
* @var array
*/
public array $source_ids = array();
/**
* 包含的源 ID 列表
*
* @var array
*/
public array $source_ids = [];

/**
* 共享认证令牌
*
* @var string
*/
public string $shared_auth_token = '';
/**
* 共享认证令牌
*
* @var string
*/
public string $shared_auth_token = '';

/**
* 是否启用
*
* @var bool
*/
public bool $enabled = true;
/**
* 是否启用
*
* @var bool
*/
public bool $enabled = true;

/**
* 创建时间
*
* @var string
*/
public string $created_at = '';
/**
* 创建时间
*
* @var string
*/
public string $created_at = '';

/**
* 更新时间
*
* @var string
*/
public string $updated_at = '';
/**
* 更新时间
*
* @var string
*/
public string $updated_at = '';

/**
* 从数组创建实例
*
* @param array $data 数据数组
* @return self
*/
public static function from_array( array $data ): self {
$model = new self();
/**
* 从数组创建实例
*
* @param array $data 数据数组
* @return self
*/
public static function from_array( array $data ): self {
$model = new self();

$model->id = sanitize_text_field( $data['id'] ?? '' );
$model->name = sanitize_text_field( $data['name'] ?? '' );
$model->description = sanitize_textarea_field( $data['description'] ?? '' );
$model->id = sanitize_text_field( $data['id'] ?? '' );
$model->name = sanitize_text_field( $data['name'] ?? '' );
$model->description = sanitize_textarea_field( $data['description'] ?? '' );

// 验证 source_ids 为字符串数组
$source_ids = $data['source_ids'] ?? array();
if ( is_array( $source_ids ) ) {
$model->source_ids = array_values(
array_filter(
array_map( 'sanitize_text_field', $source_ids ),
function ( $id ) {
return ! empty( $id ) && is_string( $id );
}
)
);
}
// 验证 source_ids 为字符串数组
$source_ids = $data['source_ids'] ?? [];
if ( is_array( $source_ids ) ) {
$model->source_ids = array_values( array_filter(
array_map( 'sanitize_text_field', $source_ids ),
function ( $id ) {
return ! empty( $id ) && is_string( $id );
}
) );
}

$model->shared_auth_token = $data['shared_auth_token'] ?? '';
$model->enabled = (bool) ( $data['enabled'] ?? true );
$model->created_at = sanitize_text_field( $data['created_at'] ?? '' );
$model->updated_at = sanitize_text_field( $data['updated_at'] ?? '' );
$model->shared_auth_token = $data['shared_auth_token'] ?? '';
$model->enabled = (bool) ( $data['enabled'] ?? true );
$model->created_at = sanitize_text_field( $data['created_at'] ?? '' );
$model->updated_at = sanitize_text_field( $data['updated_at'] ?? '' );

return $model;
}
return $model;
}

/**
* 转换为数组
*
* @param bool $include_sensitive 是否包含敏感字段
* @return array
*/
public function to_array( bool $include_sensitive = true ): array {
$data = array(
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'source_ids' => $this->source_ids,
'enabled' => $this->enabled,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
);
/**
* 转换为数组
*
* @param bool $include_sensitive 是否包含敏感字段
* @return array
*/
public function to_array( bool $include_sensitive = true ): array {
$data = [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'source_ids' => $this->source_ids,
'enabled' => $this->enabled,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];

$data['shared_auth_token'] = $include_sensitive
? $this->shared_auth_token
: ( empty( $this->shared_auth_token ) ? '' : '********' );
$data['shared_auth_token'] = $include_sensitive
? $this->shared_auth_token
: ( empty( $this->shared_auth_token ) ? '' : '********' );

return $data;
}
return $data;
}

/**
* 验证模型
*
* @return array 错误数组
*/
public function validate(): array {
$errors = array();
/**
* 验证模型
*
* @return array 错误数组
*/
public function validate(): array {
$errors = [];

if ( empty( $this->name ) ) {
$errors['name'] = __( '分组名称不能为空', 'wpbridge' );
}
if ( empty( $this->name ) ) {
$errors['name'] = __( '分组名称不能为空', 'wpbridge' );
}

return $errors;
}
return $errors;
}

/**
* 是否有效
*
* @return bool
*/
public function is_valid(): bool {
return empty( $this->validate() );
}
/**
* 是否有效
*
* @return bool
*/
public function is_valid(): bool {
return empty( $this->validate() );
}

/**
* 添加源到分组
*
* @param string $source_id 源 ID
*/
public function add_source( string $source_id ): void {
if ( ! in_array( $source_id, $this->source_ids, true ) ) {
$this->source_ids[] = $source_id;
}
}
/**
* 添加源到分组
*
* @param string $source_id 源 ID
*/
public function add_source( string $source_id ): void {
if ( ! in_array( $source_id, $this->source_ids, true ) ) {
$this->source_ids[] = $source_id;
}
}

/**
* 从分组移除源
*
* @param string $source_id 源 ID
*/
public function remove_source( string $source_id ): void {
$this->source_ids = array_values(
array_filter(
$this->source_ids,
fn( $id ) => $id !== $source_id
)
);
}
/**
* 从分组移除源
*
* @param string $source_id 源 ID
*/
public function remove_source( string $source_id ): void {
$this->source_ids = array_values(
array_filter(
$this->source_ids,
fn( $id ) => $id !== $source_id
)
);
}

/**
* 检查源是否在分组中
*
* @param string $source_id 源 ID
* @return bool
*/
public function has_source( string $source_id ): bool {
return in_array( $source_id, $this->source_ids, true );
}
/**
* 检查源是否在分组中
*
* @param string $source_id 源 ID
* @return bool
*/
public function has_source( string $source_id ): bool {
return in_array( $source_id, $this->source_ids, true );
}

/**
* 获取源数量
*
* @return int
*/
public function get_source_count(): int {
return count( $this->source_ids );
}
/**
* 获取源数量
*
* @return int
*/
public function get_source_count(): int {
return count( $this->source_ids );
}
}

View file

@ -13,7 +13,7 @@ use WPBridge\Security\Encryption;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -21,231 +21,219 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
abstract class AbstractHandler implements HandlerInterface {

/**
* 源模型
*
* @var SourceModel
*/
protected SourceModel $source;
/**
* 源模型
*
* @var SourceModel
*/
protected SourceModel $source;

/**
* 请求超时时间(秒)
*
* @var int
*/
protected int $timeout = 10;
/**
* 请求超时时间(秒)
*
* @var int
*/
protected int $timeout = 10;

/**
* 构造函数
*
* @param SourceModel $source 源模型
*/
public function __construct( SourceModel $source ) {
$this->source = $source;
}
/**
* 构造函数
*
* @param SourceModel $source 源模型
*/
public function __construct( SourceModel $source ) {
$this->source = $source;
}

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'none',
'version' => 'json',
'download' => 'direct',
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'none',
'version' => 'json',
'download' => 'direct',
];
}

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
return $this->source->api_url;
}
/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
return $this->source->api_url;
}

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
return $this->source->get_headers();
}
/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
return $this->source->get_headers();
}

/**
* 验证认证信息
*
* @return bool
*/
public function validate_auth(): bool {
// 默认不需要认证
return true;
}
/**
* 验证认证信息
*
* @return bool
*/
public function validate_auth(): bool {
// 默认不需要认证
return true;
}

/**
* 测试连通性
*
* @return HealthStatus
*/
public function test_connection(): HealthStatus {
$start = microtime( true );
/**
* 测试连通性
*
* @return HealthStatus
*/
public function test_connection(): HealthStatus {
$start = microtime( true );

$response = wp_remote_get(
$this->get_check_url(),
array(
'timeout' => $this->timeout,
'headers' => $this->get_headers(),
)
);
$response = wp_remote_get( $this->get_check_url(), [
'timeout' => $this->timeout,
'headers' => $this->get_headers(),
] );

$elapsed = (int) ( ( microtime( true ) - $start ) * 1000 );
$elapsed = (int) ( ( microtime( true ) - $start ) * 1000 );

if ( is_wp_error( $response ) ) {
return HealthStatus::failed( $response->get_error_message() );
}
if ( is_wp_error( $response ) ) {
return HealthStatus::failed( $response->get_error_message() );
}

$code = wp_remote_retrieve_response_code( $response );
$code = wp_remote_retrieve_response_code( $response );

if ( $code >= 200 && $code < 300 ) {
return HealthStatus::healthy( $elapsed );
}
if ( $code >= 200 && $code < 300 ) {
return HealthStatus::healthy( $elapsed );
}

if ( $code >= 500 ) {
return HealthStatus::failed( sprintf( 'HTTP %d', $code ) );
}
if ( $code >= 500 ) {
return HealthStatus::failed( sprintf( 'HTTP %d', $code ) );
}

return HealthStatus::degraded( $elapsed, sprintf( 'HTTP %d', $code ) );
}
return HealthStatus::degraded( $elapsed, sprintf( 'HTTP %d', $code ) );
}

/**
* 发起 HTTP 请求
*
* @param string $url URL
* @param array $args 请求参数
* @return array|null
*/
protected function request( string $url, array $args = array() ): ?array {
$defaults = array(
'timeout' => $this->timeout,
'headers' => $this->get_headers(),
);
/**
* 发起 HTTP 请求
*
* @param string $url URL
* @param array $args 请求参数
* @return array|null
*/
protected function request( string $url, array $args = [] ): ?array {
$defaults = [
'timeout' => $this->timeout,
'headers' => $this->get_headers(),
];

$args = wp_parse_args( $args, $defaults );
$response = wp_remote_get( $url, $args );
$args = wp_parse_args( $args, $defaults );
$response = wp_remote_get( $url, $args );

if ( is_wp_error( $response ) ) {
Logger::error(
'请求失败',
array(
'url' => $this->redact_url( $url ),
'error' => $response->get_error_message(),
)
);
return null;
}
if ( is_wp_error( $response ) ) {
Logger::error( '请求失败', [
'url' => $this->redact_url( $url ),
'error' => $response->get_error_message(),
] );
return null;
}

$code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );

if ( $code < 200 || $code >= 300 ) {
Logger::warning(
'请求返回非 2xx 状态码',
array(
'url' => $this->redact_url( $url ),
'code' => $code,
)
);
return null;
}
if ( $code < 200 || $code >= 300 ) {
Logger::warning( '请求返回非 2xx 状态码', [
'url' => $this->redact_url( $url ),
'code' => $code,
] );
return null;
}

$data = json_decode( $body, true );
$data = json_decode( $body, true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
Logger::error(
'JSON 解析失败',
array(
'url' => $this->redact_url( $url ),
'error' => json_last_error_msg(),
)
);
return null;
}
if ( json_last_error() !== JSON_ERROR_NONE ) {
Logger::error( 'JSON 解析失败', [
'url' => $this->redact_url( $url ),
'error' => json_last_error_msg(),
] );
return null;
}

return $data;
}
return $data;
}

/**
* 比较版本号
*
* @param string $current 当前版本
* @param string $remote 远程版本
* @return bool 远程版本是否更新
*/
protected function is_newer_version( string $current, string $remote ): bool {
return version_compare( $remote, $current, '>' );
}
/**
* 比较版本号
*
* @param string $current 当前版本
* @param string $remote 远程版本
* @return bool 远程版本是否更新
*/
protected function is_newer_version( string $current, string $remote ): bool {
return version_compare( $remote, $current, '>' );
}

/**
* 获取解密后的认证令牌
*
* @return string
*/
protected function get_auth_token(): string {
if ( empty( $this->source->auth_token ) ) {
return '';
}
/**
* 获取解密后的认证令牌
*
* @return string
*/
protected function get_auth_token(): string {
if ( empty( $this->source->auth_token ) ) {
return '';
}

$token = Encryption::decrypt( $this->source->auth_token );
$token = Encryption::decrypt( $this->source->auth_token );

if ( empty( $token ) ) {
if ( Encryption::is_encrypted( $this->source->auth_token ) ) {
Logger::error( 'Token 解密失败', array( 'source' => $this->source->id ) );
return '';
}
if ( empty( $token ) ) {
if ( Encryption::is_encrypted( $this->source->auth_token ) ) {
Logger::error( 'Token 解密失败', [ 'source' => $this->source->id ] );
return '';
}

return $this->source->auth_token;
}
return $this->source->auth_token;
}

return $token;
}
return $token;
}

/**
* 脱敏 URL 中的敏感参数
*
* @param string $url 原始 URL
* @return string
*/
protected function redact_url( string $url ): string {
$parts = wp_parse_url( $url );
if ( empty( $parts ) || empty( $parts['query'] ) ) {
return $url;
}
/**
* 脱敏 URL 中的敏感参数
*
* @param string $url 原始 URL
* @return string
*/
protected function redact_url( string $url ): string {
$parts = wp_parse_url( $url );
if ( empty( $parts ) || empty( $parts['query'] ) ) {
return $url;
}

parse_str( $parts['query'], $query );
if ( empty( $query ) ) {
return $url;
}
parse_str( $parts['query'], $query );
if ( empty( $query ) ) {
return $url;
}

$sensitive_keys = array( 'access_token', 'api_key', 'token', 'key', 'auth', 'authorization' );
foreach ( $query as $key => $value ) {
if ( in_array( strtolower( (string) $key ), $sensitive_keys, true ) ) {
$query[ $key ] = '***';
}
}
$sensitive_keys = [ 'access_token', 'api_key', 'token', 'key', 'auth', 'authorization' ];
foreach ( $query as $key => $value ) {
if ( in_array( strtolower( (string) $key ), $sensitive_keys, true ) ) {
$query[ $key ] = '***';
}
}

$scheme = isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '';
$user = $parts['user'] ?? '';
$pass = isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
$auth = $user ? $user . $pass . '@' : '';
$host = $parts['host'] ?? '';
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
$path = $parts['path'] ?? '';
$querystr = http_build_query( $query );
$fragment = isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
$scheme = isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '';
$user = $parts['user'] ?? '';
$pass = isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
$auth = $user ? $user . $pass . '@' : '';
$host = $parts['host'] ?? '';
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
$path = $parts['path'] ?? '';
$querystr = http_build_query( $query );
$fragment = isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';

return $scheme . $auth . $host . $port . $path . ( $querystr ? '?' . $querystr : '' ) . $fragment;
}
return $scheme . $auth . $host . $port . $path . ( $querystr ? '?' . $querystr : '' ) . $fragment;
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -20,182 +20,159 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class ArkPressHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'token',
'version' => 'json',
'download' => 'direct',
'federation' => true,
'cdn' => true,
'mirror' => true,
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'json',
'download' => 'direct',
'federation' => true,
'cdn' => true,
'mirror' => true,
];
}

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
return rtrim( $this->source->api_url, '/' ) . '/plugins/update-check';
}
/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
return rtrim( $this->source->api_url, '/' ) . '/plugins/update-check';
}

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$url = $this->build_plugin_url( $slug );
$data = $this->request( $url );
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$url = $this->build_plugin_url( $slug );
$data = $this->request( $url );

if ( null === $data ) {
return null;
}
if ( null === $data ) {
return null;
}

// ArkPress API 响应格式
$remote_version = $data['version'] ?? $data['new_version'] ?? '';
// ArkPress API 响应格式
$remote_version = $data['version'] ?? $data['new_version'] ?? '';

if ( empty( $remote_version ) ) {
Logger::warning(
'ArkPress 响应缺少版本信息',
array(
'url' => $url,
'slug' => $slug,
)
);
return null;
}
if ( empty( $remote_version ) ) {
Logger::warning( 'ArkPress 响应缺少版本信息', [ 'url' => $url, 'slug' => $slug ] );
return null;
}

// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug(
'ArkPress: 无可用更新',
array(
'slug' => $slug,
'current' => $version,
'remote' => $remote_version,
)
);
return null;
}
// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug( 'ArkPress: 无可用更新', [
'slug' => $slug,
'current' => $version,
'remote' => $remote_version,
] );
return null;
}

// 构建更新信息
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $data['download_url'] ?? $data['package'] ?? '';
$info->details_url = $data['details_url'] ?? $data['url'] ?? '';
$info->requires = $data['requires'] ?? '';
$info->tested = $data['tested'] ?? '';
$info->requires_php = $data['requires_php'] ?? '';
$info->last_updated = $data['last_updated'] ?? '';
$info->icons = $data['icons'] ?? array();
$info->banners = $data['banners'] ?? array();
// 构建更新信息
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $data['download_url'] ?? $data['package'] ?? '';
$info->details_url = $data['details_url'] ?? $data['url'] ?? '';
$info->requires = $data['requires'] ?? '';
$info->tested = $data['tested'] ?? '';
$info->requires_php = $data['requires_php'] ?? '';
$info->last_updated = $data['last_updated'] ?? '';
$info->icons = $data['icons'] ?? [];
$info->banners = $data['banners'] ?? [];

if ( isset( $data['sections'] ) ) {
$info->changelog = $data['sections']['changelog'] ?? '';
$info->description = $data['sections']['description'] ?? '';
}
if ( isset( $data['sections'] ) ) {
$info->changelog = $data['sections']['changelog'] ?? '';
$info->description = $data['sections']['description'] ?? '';
}

Logger::info(
'ArkPress: 发现更新',
array(
'slug' => $slug,
'current' => $version,
'new' => $remote_version,
)
);
Logger::info( 'ArkPress: 发现更新', [
'slug' => $slug,
'current' => $version,
'new' => $remote_version,
] );

return $info;
}
return $info;
}

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$url = $this->build_plugin_url( $slug );
return $this->request( $url );
}
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$url = $this->build_plugin_url( $slug );
return $this->request( $url );
}

/**
* 批量检查更新
*
* @param array $plugins 插件列表 [ slug => version ]
* @return array<string, UpdateInfo>
*/
public function check_updates_batch( array $plugins ): array {
$url = rtrim( $this->source->api_url, '/' ) . '/plugins/update-check';
/**
* 批量检查更新
*
* @param array $plugins 插件列表 [ slug => version ]
* @return array<string, UpdateInfo>
*/
public function check_updates_batch( array $plugins ): array {
$url = rtrim( $this->source->api_url, '/' ) . '/plugins/update-check';

$response = wp_remote_post(
$url,
array(
'timeout' => $this->timeout,
'headers' => array_merge(
$this->get_headers(),
array(
'Content-Type' => 'application/json',
)
),
'body' => wp_json_encode(
array(
'plugins' => $plugins,
)
),
)
);
$response = wp_remote_post( $url, [
'timeout' => $this->timeout,
'headers' => array_merge( $this->get_headers(), [
'Content-Type' => 'application/json',
] ),
'body' => wp_json_encode( [
'plugins' => $plugins,
] ),
] );

if ( is_wp_error( $response ) ) {
Logger::error(
'ArkPress 批量检查失败',
array(
'error' => $response->get_error_message(),
)
);
return array();
}
if ( is_wp_error( $response ) ) {
Logger::error( 'ArkPress 批量检查失败', [
'error' => $response->get_error_message(),
] );
return [];
}

$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );

if ( ! is_array( $data ) || ! isset( $data['plugins'] ) ) {
return array();
}
if ( ! is_array( $data ) || ! isset( $data['plugins'] ) ) {
return [];
}

$updates = array();
foreach ( $data['plugins'] as $slug => $plugin_data ) {
if ( empty( $plugin_data['version'] ) ) {
continue;
}
$updates = [];
foreach ( $data['plugins'] as $slug => $plugin_data ) {
if ( empty( $plugin_data['version'] ) ) {
continue;
}

$current = $plugins[ $slug ] ?? '0.0.0';
if ( $this->is_newer_version( $current, $plugin_data['version'] ) ) {
$info = UpdateInfo::from_array( $plugin_data );
$info->slug = $slug;
$updates[ $slug ] = $info;
}
}
$current = $plugins[ $slug ] ?? '0.0.0';
if ( $this->is_newer_version( $current, $plugin_data['version'] ) ) {
$info = UpdateInfo::from_array( $plugin_data );
$info->slug = $slug;
$updates[ $slug ] = $info;
}
}

return $updates;
}
return $updates;
}

/**
* 构建插件 URL
*
* @param string $slug 插件 slug
* @return string
*/
protected function build_plugin_url( string $slug ): string {
return rtrim( $this->source->api_url, '/' ) . '/plugins/' . $slug;
}
/**
* 构建插件 URL
*
* @param string $slug 插件 slug
* @return string
*/
protected function build_plugin_url( string $slug ): string {
return rtrim( $this->source->api_url, '/' ) . '/plugins/' . $slug;
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -19,106 +19,94 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class AspireCloudHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'token',
'version' => 'json',
'download' => 'direct',
'federation' => true,
'cdn' => true,
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'json',
'download' => 'direct',
'federation' => true,
'cdn' => true,
];
}

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
return rtrim( $this->source->api_url, '/' ) . '/plugins/update-check/1.1';
}
/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
return rtrim( $this->source->api_url, '/' ) . '/plugins/update-check/1.1';
}

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$url = $this->build_plugin_url( $slug );
$data = $this->request( $url );
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$url = $this->build_plugin_url( $slug );
$data = $this->request( $url );

if ( null === $data ) {
return null;
}
if ( null === $data ) {
return null;
}

// AspireCloud API 响应格式
$remote_version = $data['version'] ?? $data['new_version'] ?? '';
// AspireCloud API 响应格式
$remote_version = $data['version'] ?? $data['new_version'] ?? '';

if ( empty( $remote_version ) ) {
Logger::warning(
'AspireCloud 响应缺少版本信息',
array(
'url' => $url,
'slug' => $slug,
)
);
return null;
}
if ( empty( $remote_version ) ) {
Logger::warning( 'AspireCloud 响应缺少版本信息', [ 'url' => $url, 'slug' => $slug ] );
return null;
}

// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug(
'AspireCloud: 无可用更新',
array(
'slug' => $slug,
'current' => $version,
'remote' => $remote_version,
)
);
return null;
}
// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug( 'AspireCloud: 无可用更新', [
'slug' => $slug,
'current' => $version,
'remote' => $remote_version,
] );
return null;
}

// 构建更新信息
$info = UpdateInfo::from_array( $data );
$info->slug = $slug;
// 构建更新信息
$info = UpdateInfo::from_array( $data );
$info->slug = $slug;

Logger::info(
'AspireCloud: 发现更新',
array(
'slug' => $slug,
'current' => $version,
'new' => $remote_version,
)
);
Logger::info( 'AspireCloud: 发现更新', [
'slug' => $slug,
'current' => $version,
'new' => $remote_version,
] );

return $info;
}
return $info;
}

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$url = $this->build_plugin_url( $slug );
return $this->request( $url );
}
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$url = $this->build_plugin_url( $slug );
return $this->request( $url );
}

/**
* 构建插件 URL
*
* @param string $slug 插件 slug
* @return string
*/
protected function build_plugin_url( string $slug ): string {
return rtrim( $this->source->api_url, '/' ) . '/plugins/info/1.2?slug=' . urlencode( $slug );
}
/**
* 构建插件 URL
*
* @param string $slug 插件 slug
* @return string
*/
protected function build_plugin_url( string $slug ): string {
return rtrim( $this->source->api_url, '/' ) . '/plugins/info/1.2?slug=' . urlencode( $slug );
}
}

View file

@ -56,12 +56,12 @@ class BridgeServerHandler extends AbstractHandler {
* @return array
*/
public function get_capabilities(): array {
return array(
return [
'auth' => 'api_key',
'version' => 'json',
'download' => 'signed_url',
'batch' => true,
);
];
}

/**
@ -82,7 +82,7 @@ class BridgeServerHandler extends AbstractHandler {
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
if ( ! $this->client || ! $this->client->is_configured() ) {
Logger::warning( 'Bridge Server 未配置', array( 'slug' => $slug ) );
Logger::warning( 'Bridge Server 未配置', [ 'slug' => $slug ] );
return null;
}

@ -101,26 +101,24 @@ class BridgeServerHandler extends AbstractHandler {
$download_url = $this->client->get_download_url( $slug );

if ( empty( $download_url ) ) {
Logger::warning( 'Bridge Server 无法获取下载 URL', array( 'slug' => $slug ) );
Logger::warning( 'Bridge Server 无法获取下载 URL', [ 'slug' => $slug ] );
return null;
}

return UpdateInfo::from_array(
array(
'slug' => $slug,
'version' => $info['version'],
'download_url' => $download_url,
'details_url' => $info['homepage'] ?? '',
'requires' => $info['requires'] ?? '',
'tested' => $info['tested'] ?? '',
'requires_php' => $info['requires_php'] ?? '',
'last_updated' => $info['updated_at'] ?? '',
'icons' => $info['icons'] ?? array(),
'banners' => $info['banners'] ?? array(),
'changelog' => $info['changelog'] ?? '',
'description' => $info['description'] ?? '',
)
);
return UpdateInfo::from_array( [
'slug' => $slug,
'version' => $info['version'],
'download_url' => $download_url,
'details_url' => $info['homepage'] ?? '',
'requires' => $info['requires'] ?? '',
'tested' => $info['tested'] ?? '',
'requires_php' => $info['requires_php'] ?? '',
'last_updated' => $info['updated_at'] ?? '',
'icons' => $info['icons'] ?? [],
'banners' => $info['banners'] ?? [],
'changelog' => $info['changelog'] ?? '',
'description' => $info['description'] ?? '',
] );
}

/**

View file

@ -12,7 +12,7 @@ use WPBridge\FAIR\FairSourceAdapter;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -20,95 +20,95 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class FairHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'token',
'version' => 'fair',
'download' => 'direct',
'signature' => 'ed25519',
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'fair',
'download' => 'direct',
'signature' => 'ed25519',
];
}

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$adapter = $this->get_adapter();
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$adapter = $this->get_adapter();

$data = $this->source->item_type === 'theme'
? $adapter->check_theme_update( $slug, $version )
: $adapter->check_plugin_update( $slug, $version );
$data = $this->source->item_type === 'theme'
? $adapter->check_theme_update( $slug, $version )
: $adapter->check_plugin_update( $slug, $version );

if ( null === $data ) {
return null;
}
if ( null === $data ) {
return null;
}

$info = UpdateInfo::from_array( $data );
$info->slug = $slug;
$info = UpdateInfo::from_array( $data );
$info->slug = $slug;

return $info;
}
return $info;
}

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$adapter = $this->get_adapter();
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$adapter = $this->get_adapter();

return $this->source->item_type === 'theme'
? $adapter->get_theme_info( $slug )
: $adapter->get_plugin_info( $slug );
}
return $this->source->item_type === 'theme'
? $adapter->get_theme_info( $slug )
: $adapter->get_plugin_info( $slug );
}

/**
* 获取 FAIR 适配器
*
* @return FairSourceAdapter
*/
private function get_adapter(): FairSourceAdapter {
return new FairSourceAdapter( $this->build_source_config() );
}
/**
* 获取 FAIR 适配器
*
* @return FairSourceAdapter
*/
private function get_adapter(): FairSourceAdapter {
return new FairSourceAdapter( $this->build_source_config() );
}

/**
* 构建 FAIR 源配置
*
* @return array
*/
private function build_source_config(): array {
$headers = array(
'Accept' => 'application/json',
);
/**
* 构建 FAIR 源配置
*
* @return array
*/
private function build_source_config(): array {
$headers = [
'Accept' => 'application/json',
];

$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
$scheme = $this->source->metadata['auth_scheme'] ?? '';
if ( $scheme === 'bearer' ) {
$headers['Authorization'] = 'Bearer ' . $token;
} elseif ( $scheme === 'basic' ) {
$headers['Authorization'] = 'Basic ' . base64_encode( $token );
} else {
$headers['Authorization'] = 'Token ' . $token;
}
}
$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
$scheme = $this->source->metadata['auth_scheme'] ?? '';
if ( $scheme === 'bearer' ) {
$headers['Authorization'] = 'Bearer ' . $token;
} elseif ( $scheme === 'basic' ) {
$headers['Authorization'] = 'Basic ' . base64_encode( $token );
} else {
$headers['Authorization'] = 'Token ' . $token;
}
}

return array(
'api_url' => $this->source->api_url,
'auth_type' => SourceRegistry::AUTH_NONE,
'auth_secret_ref' => '',
'signature_required' => (bool) ( $this->source->metadata['signature_required'] ?? false ),
'headers' => $headers,
);
}
return [
'api_url' => $this->source->api_url,
'auth_type' => SourceRegistry::AUTH_NONE,
'auth_secret_ref' => '',
'signature_required' => (bool) ( $this->source->metadata['signature_required'] ?? false ),
'headers' => $headers,
];
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -19,226 +19,220 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class GitHubHandler extends AbstractHandler {

/**
* GitHub API 基础 URL
*
* @var string
*/
const API_BASE = 'https://api.github.com';
/**
* GitHub API 基础 URL
*
* @var string
*/
const API_BASE = 'https://api.github.com';

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'token',
'version' => 'release',
'download' => 'release',
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'release',
'download' => 'release',
];
}

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = parent::get_headers();
$headers['Accept'] = 'application/vnd.github.v3+json';
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
return $headers;
}
/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = parent::get_headers();
$headers['Accept'] = 'application/vnd.github.v3+json';
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
return $headers;
}

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( empty( $repo ) ) {
return $this->source->api_url;
}
return self::API_BASE . '/repos/' . $repo . '/releases/latest';
}
/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( empty( $repo ) ) {
return $this->source->api_url;
}
return self::API_BASE . '/repos/' . $repo . '/releases/latest';
}

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$repo = $this->parse_repo_url( $this->source->api_url );
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$repo = $this->parse_repo_url( $this->source->api_url );

if ( empty( $repo ) ) {
Logger::warning( 'GitHub: 无效的仓库 URL', array( 'url' => $this->source->api_url ) );
return null;
}
if ( empty( $repo ) ) {
Logger::warning( 'GitHub: 无效的仓库 URL', [ 'url' => $this->source->api_url ] );
return null;
}

$url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
$data = $this->request( $url );
$url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
$data = $this->request( $url );

if ( null === $data ) {
return null;
}
if ( null === $data ) {
return null;
}

// 解析版本号(去除 v 前缀)
$remote_version = $data['tag_name'] ?? '';
$remote_version = ltrim( $remote_version, 'v' );
// 解析版本号(去除 v 前缀)
$remote_version = $data['tag_name'] ?? '';
$remote_version = ltrim( $remote_version, 'v' );

if ( empty( $remote_version ) ) {
Logger::warning( 'GitHub: 响应缺少版本信息', array( 'repo' => $repo ) );
return null;
}
if ( empty( $remote_version ) ) {
Logger::warning( 'GitHub: 响应缺少版本信息', [ 'repo' => $repo ] );
return null;
}

// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug(
'GitHub: 无可用更新',
array(
'repo' => $repo,
'current' => $version,
'remote' => $remote_version,
)
);
return null;
}
// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug( 'GitHub: 无可用更新', [
'repo' => $repo,
'current' => $version,
'remote' => $remote_version,
] );
return null;
}

// 查找下载 URL
$download_url = $this->find_download_url( $data, $slug );
// 查找下载 URL
$download_url = $this->find_download_url( $data, $slug );

if ( empty( $download_url ) ) {
Logger::warning( 'GitHub: 未找到下载 URL', array( 'repo' => $repo ) );
return null;
}
if ( empty( $download_url ) ) {
Logger::warning( 'GitHub: 未找到下载 URL', [ 'repo' => $repo ] );
return null;
}

// 构建更新信息
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $download_url;
$info->details_url = $data['html_url'] ?? '';
$info->last_updated = $data['published_at'] ?? '';
$info->changelog = $data['body'] ?? '';
// 构建更新信息
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $download_url;
$info->details_url = $data['html_url'] ?? '';
$info->last_updated = $data['published_at'] ?? '';
$info->changelog = $data['body'] ?? '';

Logger::info(
'GitHub: 发现更新',
array(
'repo' => $repo,
'current' => $version,
'new' => $remote_version,
)
);
Logger::info( 'GitHub: 发现更新', [
'repo' => $repo,
'current' => $version,
'new' => $remote_version,
] );

return $info;
}
return $info;
}

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$repo = $this->parse_repo_url( $this->source->api_url );
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$repo = $this->parse_repo_url( $this->source->api_url );

if ( empty( $repo ) ) {
return null;
}
if ( empty( $repo ) ) {
return null;
}

// 获取仓库信息
$repo_url = self::API_BASE . '/repos/' . $repo;
$repo_data = $this->request( $repo_url );
// 获取仓库信息
$repo_url = self::API_BASE . '/repos/' . $repo;
$repo_data = $this->request( $repo_url );

// 获取最新 Release
$release_url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
$release_data = $this->request( $release_url );
// 获取最新 Release
$release_url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
$release_data = $this->request( $release_url );

if ( null === $repo_data || null === $release_data ) {
return null;
}
if ( null === $repo_data || null === $release_data ) {
return null;
}

$version = ltrim( $release_data['tag_name'] ?? '', 'v' );
$version = ltrim( $release_data['tag_name'] ?? '', 'v' );

return array(
'name' => $repo_data['name'] ?? $slug,
'slug' => $slug,
'version' => $version,
'download_url' => $this->find_download_url( $release_data, $slug ),
'details_url' => $release_data['html_url'] ?? '',
'last_updated' => $release_data['published_at'] ?? '',
'sections' => array(
'description' => $repo_data['description'] ?? '',
'changelog' => $release_data['body'] ?? '',
),
);
}
return [
'name' => $repo_data['name'] ?? $slug,
'slug' => $slug,
'version' => $version,
'download_url' => $this->find_download_url( $release_data, $slug ),
'details_url' => $release_data['html_url'] ?? '',
'last_updated' => $release_data['published_at'] ?? '',
'sections' => [
'description' => $repo_data['description'] ?? '',
'changelog' => $release_data['body'] ?? '',
],
];
}

/**
* 解析仓库 URL
*
* @param string $url URL
* @return string|null owner/repo 格式
*/
private function parse_repo_url( string $url ): ?string {
// 支持多种格式
// https://github.com/owner/repo
// github.com/owner/repo
// owner/repo
/**
* 解析仓库 URL
*
* @param string $url URL
* @return string|null owner/repo 格式
*/
private function parse_repo_url( string $url ): ?string {
// 支持多种格式
// https://github.com/owner/repo
// github.com/owner/repo
// owner/repo

$url = trim( $url );
$url = trim( $url );

// 移除协议
$url = preg_replace( '#^https?://#', '', $url );
// 移除协议
$url = preg_replace( '#^https?://#', '', $url );

// 移除 github.com
$url = preg_replace( '#^github\.com/#', '', $url );
// 移除 github.com
$url = preg_replace( '#^github\.com/#', '', $url );

// 移除 .git 后缀
$url = preg_replace( '#\.git$#', '', $url );
// 移除 .git 后缀
$url = preg_replace( '#\.git$#', '', $url );

// 验证格式
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
return $url;
}
// 验证格式
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
return $url;
}

return null;
}
return null;
}

/**
* 查找下载 URL
*
* @param array $release Release 数据
* @param string $slug 插件 slug
* @return string|null
*/
private function find_download_url( array $release, string $slug ): ?string {
// 优先查找 assets 中的 zip 文件
if ( ! empty( $release['assets'] ) ) {
foreach ( $release['assets'] as $asset ) {
$name = $asset['name'] ?? '';
/**
* 查找下载 URL
*
* @param array $release Release 数据
* @param string $slug 插件 slug
* @return string|null
*/
private function find_download_url( array $release, string $slug ): ?string {
// 优先查找 assets 中的 zip 文件
if ( ! empty( $release['assets'] ) ) {
foreach ( $release['assets'] as $asset ) {
$name = $asset['name'] ?? '';

// 匹配 slug.zip 或 slug-version.zip
if ( preg_match( '/\.zip$/i', $name ) ) {
if ( stripos( $name, $slug ) !== false ) {
return $asset['browser_download_url'] ?? null;
}
}
}
// 匹配 slug.zip 或 slug-version.zip
if ( preg_match( '/\.zip$/i', $name ) ) {
if ( stripos( $name, $slug ) !== false ) {
return $asset['browser_download_url'] ?? null;
}
}
}

// 如果没有匹配 slug 的,返回第一个 zip
foreach ( $release['assets'] as $asset ) {
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
return $asset['browser_download_url'] ?? null;
}
}
}
// 如果没有匹配 slug 的,返回第一个 zip
foreach ( $release['assets'] as $asset ) {
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
return $asset['browser_download_url'] ?? null;
}
}
}

// 使用 zipball_url 作为后备
return $release['zipball_url'] ?? null;
}
// 使用 zipball_url 作为后备
return $release['zipball_url'] ?? null;
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -19,238 +19,232 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class GitLabHandler extends AbstractHandler {

/**
* GitLab API 基础 URL
*
* @var string
*/
const API_BASE = 'https://gitlab.com/api/v4';
/**
* GitLab API 基础 URL
*
* @var string
*/
const API_BASE = 'https://gitlab.com/api/v4';

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'token',
'version' => 'release',
'download' => 'release',
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'release',
'download' => 'release',
];
}

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = array();
/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = [];

$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
// GitLab 使用 PRIVATE-TOKEN 头
$headers['PRIVATE-TOKEN'] = $token;
}
$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
// GitLab 使用 PRIVATE-TOKEN 头
$headers['PRIVATE-TOKEN'] = $token;
}

return $headers;
}
return $headers;
}

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$project_id = $this->get_project_id();
if ( empty( $project_id ) ) {
return $this->source->api_url;
}
return self::API_BASE . '/projects/' . $project_id . '/releases';
}
/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$project_id = $this->get_project_id();
if ( empty( $project_id ) ) {
return $this->source->api_url;
}
return self::API_BASE . '/projects/' . $project_id . '/releases';
}

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$project_id = $this->get_project_id();
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$project_id = $this->get_project_id();

if ( empty( $project_id ) ) {
Logger::warning( 'GitLab: 无效的项目 URL', array( 'url' => $this->source->api_url ) );
return null;
}
if ( empty( $project_id ) ) {
Logger::warning( 'GitLab: 无效的项目 URL', [ 'url' => $this->source->api_url ] );
return null;
}

$url = self::API_BASE . '/projects/' . $project_id . '/releases';
$data = $this->request( $url );
$url = self::API_BASE . '/projects/' . $project_id . '/releases';
$data = $this->request( $url );

if ( null === $data || empty( $data ) ) {
return null;
}
if ( null === $data || empty( $data ) ) {
return null;
}

// 获取最新 Release第一个
$latest = $data[0] ?? null;
// 获取最新 Release第一个
$latest = $data[0] ?? null;

if ( null === $latest ) {
return null;
}
if ( null === $latest ) {
return null;
}

// 解析版本号
$remote_version = $latest['tag_name'] ?? '';
$remote_version = ltrim( $remote_version, 'v' );
// 解析版本号
$remote_version = $latest['tag_name'] ?? '';
$remote_version = ltrim( $remote_version, 'v' );

if ( empty( $remote_version ) ) {
Logger::warning( 'GitLab: 响应缺少版本信息', array( 'project' => $project_id ) );
return null;
}
if ( empty( $remote_version ) ) {
Logger::warning( 'GitLab: 响应缺少版本信息', [ 'project' => $project_id ] );
return null;
}

// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug(
'GitLab: 无可用更新',
array(
'project' => $project_id,
'current' => $version,
'remote' => $remote_version,
)
);
return null;
}
// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug( 'GitLab: 无可用更新', [
'project' => $project_id,
'current' => $version,
'remote' => $remote_version,
] );
return null;
}

// 查找下载 URL
$download_url = $this->find_download_url( $latest, $slug, $project_id );
// 查找下载 URL
$download_url = $this->find_download_url( $latest, $slug, $project_id );

if ( empty( $download_url ) ) {
Logger::warning( 'GitLab: 未找到下载 URL', array( 'project' => $project_id ) );
return null;
}
if ( empty( $download_url ) ) {
Logger::warning( 'GitLab: 未找到下载 URL', [ 'project' => $project_id ] );
return null;
}

// 构建更新信息
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $download_url;
$info->details_url = $latest['_links']['self'] ?? '';
$info->last_updated = $latest['released_at'] ?? '';
$info->changelog = $latest['description'] ?? '';
// 构建更新信息
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $download_url;
$info->details_url = $latest['_links']['self'] ?? '';
$info->last_updated = $latest['released_at'] ?? '';
$info->changelog = $latest['description'] ?? '';

Logger::info(
'GitLab: 发现更新',
array(
'project' => $project_id,
'current' => $version,
'new' => $remote_version,
)
);
Logger::info( 'GitLab: 发现更新', [
'project' => $project_id,
'current' => $version,
'new' => $remote_version,
] );

return $info;
}
return $info;
}

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$project_id = $this->get_project_id();
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$project_id = $this->get_project_id();

if ( empty( $project_id ) ) {
return null;
}
if ( empty( $project_id ) ) {
return null;
}

// 获取项目信息
$project_url = self::API_BASE . '/projects/' . $project_id;
$project_data = $this->request( $project_url );
// 获取项目信息
$project_url = self::API_BASE . '/projects/' . $project_id;
$project_data = $this->request( $project_url );

// 获取 Releases
$releases_url = self::API_BASE . '/projects/' . $project_id . '/releases';
$releases_data = $this->request( $releases_url );
// 获取 Releases
$releases_url = self::API_BASE . '/projects/' . $project_id . '/releases';
$releases_data = $this->request( $releases_url );

if ( null === $project_data ) {
return null;
}
if ( null === $project_data ) {
return null;
}

$latest = $releases_data[0] ?? array();
$version = ltrim( $latest['tag_name'] ?? '', 'v' );
$latest = $releases_data[0] ?? [];
$version = ltrim( $latest['tag_name'] ?? '', 'v' );

return array(
'name' => $project_data['name'] ?? $slug,
'slug' => $slug,
'version' => $version,
'download_url' => $this->find_download_url( $latest, $slug, $project_id ),
'details_url' => $project_data['web_url'] ?? '',
'last_updated' => $latest['released_at'] ?? '',
'sections' => array(
'description' => $project_data['description'] ?? '',
'changelog' => $latest['description'] ?? '',
),
);
}
return [
'name' => $project_data['name'] ?? $slug,
'slug' => $slug,
'version' => $version,
'download_url' => $this->find_download_url( $latest, $slug, $project_id ),
'details_url' => $project_data['web_url'] ?? '',
'last_updated' => $latest['released_at'] ?? '',
'sections' => [
'description' => $project_data['description'] ?? '',
'changelog' => $latest['description'] ?? '',
],
];
}

/**
* 获取项目 IDURL 编码的路径)
*
* @return string|null
*/
private function get_project_id(): ?string {
$url = trim( $this->source->api_url );
/**
* 获取项目 IDURL 编码的路径)
*
* @return string|null
*/
private function get_project_id(): ?string {
$url = trim( $this->source->api_url );

// 移除协议
$url = preg_replace( '#^https?://#', '', $url );
// 移除协议
$url = preg_replace( '#^https?://#', '', $url );

// 移除 gitlab.com
$url = preg_replace( '#^gitlab\.com/#', '', $url );
// 移除 gitlab.com
$url = preg_replace( '#^gitlab\.com/#', '', $url );

// 移除 .git 后缀
$url = preg_replace( '#\.git$#', '', $url );
// 移除 .git 后缀
$url = preg_replace( '#\.git$#', '', $url );

// URL 编码路径
if ( preg_match( '#^[\w.-]+/[\w.-]+(?:/[\w.-]+)*$#', $url ) ) {
return urlencode( $url );
}
// URL 编码路径
if ( preg_match( '#^[\w.-]+/[\w.-]+(?:/[\w.-]+)*$#', $url ) ) {
return urlencode( $url );
}

return null;
}
return null;
}

/**
* 查找下载 URL
*
* @param array $release Release 数据
* @param string $slug 插件 slug
* @param string $project_id 项目 ID
* @return string|null
*/
private function find_download_url( array $release, string $slug, string $project_id ): ?string {
// 查找 assets 中的链接
if ( ! empty( $release['assets']['links'] ) ) {
foreach ( $release['assets']['links'] as $link ) {
$name = $link['name'] ?? '';
/**
* 查找下载 URL
*
* @param array $release Release 数据
* @param string $slug 插件 slug
* @param string $project_id 项目 ID
* @return string|null
*/
private function find_download_url( array $release, string $slug, string $project_id ): ?string {
// 查找 assets 中的链接
if ( ! empty( $release['assets']['links'] ) ) {
foreach ( $release['assets']['links'] as $link ) {
$name = $link['name'] ?? '';

if ( preg_match( '/\.zip$/i', $name ) ) {
if ( stripos( $name, $slug ) !== false ) {
return $link['url'] ?? null;
}
}
}
if ( preg_match( '/\.zip$/i', $name ) ) {
if ( stripos( $name, $slug ) !== false ) {
return $link['url'] ?? null;
}
}
}

// 返回第一个 zip
foreach ( $release['assets']['links'] as $link ) {
if ( preg_match( '/\.zip$/i', $link['name'] ?? '' ) ) {
return $link['url'] ?? null;
}
}
}
// 返回第一个 zip
foreach ( $release['assets']['links'] as $link ) {
if ( preg_match( '/\.zip$/i', $link['name'] ?? '' ) ) {
return $link['url'] ?? null;
}
}
}

// 使用归档 URL 作为后备
$tag = $release['tag_name'] ?? '';
if ( ! empty( $tag ) ) {
return self::API_BASE . '/projects/' . $project_id . '/repository/archive.zip?sha=' . $tag;
}
// 使用归档 URL 作为后备
$tag = $release['tag_name'] ?? '';
if ( ! empty( $tag ) ) {
return self::API_BASE . '/projects/' . $project_id . '/repository/archive.zip?sha=' . $tag;
}

return null;
}
return null;
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -19,240 +19,234 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class GiteeHandler extends AbstractHandler {

/**
* Gitee API 基础 URL
*
* @var string
*/
const API_BASE = 'https://gitee.com/api/v5';
/**
* Gitee API 基础 URL
*
* @var string
*/
const API_BASE = 'https://gitee.com/api/v5';

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'token',
'version' => 'release',
'download' => 'release',
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'release',
'download' => 'release',
];
}

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = parent::get_headers();
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
return $headers;
}
/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = parent::get_headers();
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
return $headers;
}

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( empty( $repo ) ) {
return $this->source->api_url;
}
return self::API_BASE . '/repos/' . $repo . '/releases/latest';
}
/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( empty( $repo ) ) {
return $this->source->api_url;
}
return self::API_BASE . '/repos/' . $repo . '/releases/latest';
}

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$repo = $this->parse_repo_url( $this->source->api_url );
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$repo = $this->parse_repo_url( $this->source->api_url );

if ( empty( $repo ) ) {
Logger::warning( 'Gitee: 无效的仓库 URL', array( 'url' => $this->source->api_url ) );
return null;
}
if ( empty( $repo ) ) {
Logger::warning( 'Gitee: 无效的仓库 URL', [ 'url' => $this->source->api_url ] );
return null;
}

// 构建 URL带 access_token 参数)
$url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
// 构建 URL带 access_token 参数)
$url = self::API_BASE . '/repos/' . $repo . '/releases/latest';

$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
$url = add_query_arg( 'access_token', $token, $url );
}
$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
$url = add_query_arg( 'access_token', $token, $url );
}

$data = $this->request( $url );
$data = $this->request( $url );

if ( null === $data ) {
return null;
}
if ( null === $data ) {
return null;
}

// 解析版本号
$remote_version = $data['tag_name'] ?? '';
$remote_version = ltrim( $remote_version, 'v' );
// 解析版本号
$remote_version = $data['tag_name'] ?? '';
$remote_version = ltrim( $remote_version, 'v' );

if ( empty( $remote_version ) ) {
Logger::warning( 'Gitee: 响应缺少版本信息', array( 'repo' => $repo ) );
return null;
}
if ( empty( $remote_version ) ) {
Logger::warning( 'Gitee: 响应缺少版本信息', [ 'repo' => $repo ] );
return null;
}

// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug(
'Gitee: 无可用更新',
array(
'repo' => $repo,
'current' => $version,
'remote' => $remote_version,
)
);
return null;
}
// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug( 'Gitee: 无可用更新', [
'repo' => $repo,
'current' => $version,
'remote' => $remote_version,
] );
return null;
}

// 查找下载 URL
$download_url = $this->find_download_url( $data, $slug, $repo, $remote_version );
// 查找下载 URL
$download_url = $this->find_download_url( $data, $slug, $repo, $remote_version );

if ( empty( $download_url ) ) {
Logger::warning( 'Gitee: 未找到下载 URL', array( 'repo' => $repo ) );
return null;
}
if ( empty( $download_url ) ) {
Logger::warning( 'Gitee: 未找到下载 URL', [ 'repo' => $repo ] );
return null;
}

// 构建更新信息
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $download_url;
$info->details_url = 'https://gitee.com/' . $repo . '/releases/tag/' . $data['tag_name'];
$info->last_updated = $data['created_at'] ?? '';
$info->changelog = $data['body'] ?? '';
// 构建更新信息
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $download_url;
$info->details_url = 'https://gitee.com/' . $repo . '/releases/tag/' . $data['tag_name'];
$info->last_updated = $data['created_at'] ?? '';
$info->changelog = $data['body'] ?? '';

Logger::info(
'Gitee: 发现更新',
array(
'repo' => $repo,
'current' => $version,
'new' => $remote_version,
)
);
Logger::info( 'Gitee: 发现更新', [
'repo' => $repo,
'current' => $version,
'new' => $remote_version,
] );

return $info;
}
return $info;
}

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$repo = $this->parse_repo_url( $this->source->api_url );
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$repo = $this->parse_repo_url( $this->source->api_url );

if ( empty( $repo ) ) {
return null;
}
if ( empty( $repo ) ) {
return null;
}

// 获取仓库信息
$repo_url = self::API_BASE . '/repos/' . $repo;
$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
$repo_url = add_query_arg( 'access_token', $token, $repo_url );
}
$repo_data = $this->request( $repo_url );
// 获取仓库信息
$repo_url = self::API_BASE . '/repos/' . $repo;
$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
$repo_url = add_query_arg( 'access_token', $token, $repo_url );
}
$repo_data = $this->request( $repo_url );

// 获取最新 Release
$release_url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
if ( ! empty( $token ) ) {
$release_url = add_query_arg( 'access_token', $token, $release_url );
}
$release_data = $this->request( $release_url );
// 获取最新 Release
$release_url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
if ( ! empty( $token ) ) {
$release_url = add_query_arg( 'access_token', $token, $release_url );
}
$release_data = $this->request( $release_url );

if ( null === $repo_data ) {
return null;
}
if ( null === $repo_data ) {
return null;
}

$version = ltrim( $release_data['tag_name'] ?? '', 'v' );
$version = ltrim( $release_data['tag_name'] ?? '', 'v' );

return array(
'name' => $repo_data['name'] ?? $slug,
'slug' => $slug,
'version' => $version,
'download_url' => $this->find_download_url( $release_data ?? array(), $slug, $repo, $version ),
'details_url' => $repo_data['html_url'] ?? '',
'last_updated' => $release_data['created_at'] ?? '',
'sections' => array(
'description' => $repo_data['description'] ?? '',
'changelog' => $release_data['body'] ?? '',
),
);
}
return [
'name' => $repo_data['name'] ?? $slug,
'slug' => $slug,
'version' => $version,
'download_url' => $this->find_download_url( $release_data ?? [], $slug, $repo, $version ),
'details_url' => $repo_data['html_url'] ?? '',
'last_updated' => $release_data['created_at'] ?? '',
'sections' => [
'description' => $repo_data['description'] ?? '',
'changelog' => $release_data['body'] ?? '',
],
];
}

/**
* 解析仓库 URL
*
* @param string $url URL
* @return string|null owner/repo 格式
*/
private function parse_repo_url( string $url ): ?string {
$url = trim( $url );
/**
* 解析仓库 URL
*
* @param string $url URL
* @return string|null owner/repo 格式
*/
private function parse_repo_url( string $url ): ?string {
$url = trim( $url );

// 移除协议
$url = preg_replace( '#^https?://#', '', $url );
// 移除协议
$url = preg_replace( '#^https?://#', '', $url );

// 移除 gitee.com
$url = preg_replace( '#^gitee\.com/#', '', $url );
// 移除 gitee.com
$url = preg_replace( '#^gitee\.com/#', '', $url );

// 移除 .git 后缀
$url = preg_replace( '#\.git$#', '', $url );
// 移除 .git 后缀
$url = preg_replace( '#\.git$#', '', $url );

// 验证格式
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
return $url;
}
// 验证格式
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
return $url;
}

return null;
}
return null;
}

/**
* 查找下载 URL
*
* @param array $release Release 数据
* @param string $slug 插件 slug
* @param string $repo 仓库路径
* @param string $version 版本号
* @return string|null
*/
private function find_download_url( array $release, string $slug, string $repo, string $version ): ?string {
// 查找 assets 中的 zip 文件
if ( ! empty( $release['assets'] ) ) {
foreach ( $release['assets'] as $asset ) {
$name = $asset['name'] ?? '';
/**
* 查找下载 URL
*
* @param array $release Release 数据
* @param string $slug 插件 slug
* @param string $repo 仓库路径
* @param string $version 版本号
* @return string|null
*/
private function find_download_url( array $release, string $slug, string $repo, string $version ): ?string {
// 查找 assets 中的 zip 文件
if ( ! empty( $release['assets'] ) ) {
foreach ( $release['assets'] as $asset ) {
$name = $asset['name'] ?? '';

if ( preg_match( '/\.zip$/i', $name ) ) {
if ( stripos( $name, $slug ) !== false ) {
return $asset['browser_download_url'] ?? null;
}
}
}
if ( preg_match( '/\.zip$/i', $name ) ) {
if ( stripos( $name, $slug ) !== false ) {
return $asset['browser_download_url'] ?? null;
}
}
}

// 返回第一个 zip
foreach ( $release['assets'] as $asset ) {
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
return $asset['browser_download_url'] ?? null;
}
}
}
// 返回第一个 zip
foreach ( $release['assets'] as $asset ) {
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
return $asset['browser_download_url'] ?? null;
}
}
}

// 使用归档 URL 作为后备
$tag = $release['tag_name'] ?? '';
if ( ! empty( $tag ) ) {
return 'https://gitee.com/' . $repo . '/repository/archive/' . $tag . '.zip';
}
// 使用归档 URL 作为后备
$tag = $release['tag_name'] ?? '';
if ( ! empty( $tag ) ) {
return 'https://gitee.com/' . $repo . '/repository/archive/' . $tag . '.zip';
}

return null;
}
return null;
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\UpdateSource\SourceModel;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -19,64 +19,64 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
interface HandlerInterface {

/**
* 构造函数
*
* @param SourceModel $source 源模型
*/
public function __construct( SourceModel $source );
/**
* 构造函数
*
* @param SourceModel $source 源模型
*/
public function __construct( SourceModel $source );

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array;
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array;

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string;
/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string;

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array;
/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array;

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo;
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo;

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array;
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array;

/**
* 验证认证信息
*
* @return bool
*/
public function validate_auth(): bool;
/**
* 验证认证信息
*
* @return bool
*/
public function validate_auth(): bool;

/**
* 测试连通性
*
* @return HealthStatus
*/
public function test_connection(): HealthStatus;
/**
* 测试连通性
*
* @return HealthStatus
*/
public function test_connection(): HealthStatus;
}

/**
@ -84,158 +84,158 @@ interface HandlerInterface {
*/
class UpdateInfo {

/**
* 插件/主题 slug
*
* @var string
*/
public string $slug = '';
/**
* 插件/主题 slug
*
* @var string
*/
public string $slug = '';

/**
* 新版本号
*
* @var string
*/
public string $version = '';
/**
* 新版本号
*
* @var string
*/
public string $version = '';

/**
* 下载 URL
*
* @var string
*/
public string $download_url = '';
/**
* 下载 URL
*
* @var string
*/
public string $download_url = '';

/**
* 详情 URL
*
* @var string
*/
public string $details_url = '';
/**
* 详情 URL
*
* @var string
*/
public string $details_url = '';

/**
* 最低 WordPress 版本
*
* @var string
*/
public string $requires = '';
/**
* 最低 WordPress 版本
*
* @var string
*/
public string $requires = '';

/**
* 测试通过的 WordPress 版本
*
* @var string
*/
public string $tested = '';
/**
* 测试通过的 WordPress 版本
*
* @var string
*/
public string $tested = '';

/**
* 最低 PHP 版本
*
* @var string
*/
public string $requires_php = '';
/**
* 最低 PHP 版本
*
* @var string
*/
public string $requires_php = '';

/**
* 最后更新时间
*
* @var string
*/
public string $last_updated = '';
/**
* 最后更新时间
*
* @var string
*/
public string $last_updated = '';

/**
* 图标
*
* @var array
*/
public array $icons = array();
/**
* 图标
*
* @var array
*/
public array $icons = [];

/**
* 横幅
*
* @var array
*/
public array $banners = array();
/**
* 横幅
*
* @var array
*/
public array $banners = [];

/**
* 更新日志
*
* @var string
*/
public string $changelog = '';
/**
* 更新日志
*
* @var string
*/
public string $changelog = '';

/**
* 描述
*
* @var string
*/
public string $description = '';
/**
* 描述
*
* @var string
*/
public string $description = '';

/**
* 从数组创建
*
* @param array $data 数据
* @return self
*/
public static function from_array( array $data ): self {
$info = new self();
/**
* 从数组创建
*
* @param array $data 数据
* @return self
*/
public static function from_array( array $data ): self {
$info = new self();

$info->slug = $data['slug'] ?? '';
$info->version = $data['version'] ?? '';
$info->download_url = $data['download_url'] ?? $data['package'] ?? $data['download_link'] ?? '';
$info->details_url = $data['details_url'] ?? $data['url'] ?? '';
$info->requires = $data['requires'] ?? '';
$info->tested = $data['tested'] ?? '';
$info->requires_php = $data['requires_php'] ?? '';
$info->last_updated = $data['last_updated'] ?? '';
$info->icons = $data['icons'] ?? array();
$info->banners = $data['banners'] ?? array();
$info->changelog = $data['changelog'] ?? $data['sections']['changelog'] ?? '';
$info->description = $data['description'] ?? $data['sections']['description'] ?? '';
$info->slug = $data['slug'] ?? '';
$info->version = $data['version'] ?? '';
$info->download_url = $data['download_url'] ?? $data['package'] ?? $data['download_link'] ?? '';
$info->details_url = $data['details_url'] ?? $data['url'] ?? '';
$info->requires = $data['requires'] ?? '';
$info->tested = $data['tested'] ?? '';
$info->requires_php = $data['requires_php'] ?? '';
$info->last_updated = $data['last_updated'] ?? '';
$info->icons = $data['icons'] ?? [];
$info->banners = $data['banners'] ?? [];
$info->changelog = $data['changelog'] ?? $data['sections']['changelog'] ?? '';
$info->description = $data['description'] ?? $data['sections']['description'] ?? '';

return $info;
}
return $info;
}

/**
* 转换为 WordPress 更新对象格式
*
* @return object
*/
public function to_wp_update_object(): object {
return (object) array(
'slug' => $this->slug,
'new_version' => $this->version,
'package' => $this->download_url,
'url' => $this->details_url,
'requires' => $this->requires,
'tested' => $this->tested,
'requires_php' => $this->requires_php,
'icons' => $this->icons,
'banners' => $this->banners,
);
}
/**
* 转换为 WordPress 更新对象格式
*
* @return object
*/
public function to_wp_update_object(): object {
return (object) [
'slug' => $this->slug,
'new_version' => $this->version,
'package' => $this->download_url,
'url' => $this->details_url,
'requires' => $this->requires,
'tested' => $this->tested,
'requires_php' => $this->requires_php,
'icons' => $this->icons,
'banners' => $this->banners,
];
}

/**
* 转换为 plugins_api 响应格式
*
* @param string $name 插件名称
* @return object
*/
public function to_plugins_api_response( string $name = '' ): object {
return (object) array(
'name' => $name ?: $this->slug,
'slug' => $this->slug,
'version' => $this->version,
'download_link' => $this->download_url,
'requires' => $this->requires,
'tested' => $this->tested,
'requires_php' => $this->requires_php,
'last_updated' => $this->last_updated,
'sections' => array(
'description' => $this->description,
'changelog' => $this->changelog,
),
'icons' => $this->icons,
'banners' => $this->banners,
);
}
/**
* 转换为 plugins_api 响应格式
*
* @param string $name 插件名称
* @return object
*/
public function to_plugins_api_response( string $name = '' ): object {
return (object) [
'name' => $name ?: $this->slug,
'slug' => $this->slug,
'version' => $this->version,
'download_link' => $this->download_url,
'requires' => $this->requires,
'tested' => $this->tested,
'requires_php' => $this->requires_php,
'last_updated' => $this->last_updated,
'sections' => [
'description' => $this->description,
'changelog' => $this->changelog,
],
'icons' => $this->icons,
'banners' => $this->banners,
];
}
}

/**
@ -243,97 +243,97 @@ class UpdateInfo {
*/
class HealthStatus {

const STATUS_HEALTHY = 'healthy';
const STATUS_DEGRADED = 'degraded';
const STATUS_FAILED = 'failed';
const STATUS_HEALTHY = 'healthy';
const STATUS_DEGRADED = 'degraded';
const STATUS_FAILED = 'failed';

/**
* 状态
*
* @var string
*/
public string $status = self::STATUS_FAILED;
/**
* 状态
*
* @var string
*/
public string $status = self::STATUS_FAILED;

/**
* 响应时间(毫秒)
*
* @var int
*/
public int $response_time = 0;
/**
* 响应时间(毫秒)
*
* @var int
*/
public int $response_time = 0;

/**
* 错误信息
*
* @var string
*/
public string $error = '';
/**
* 错误信息
*
* @var string
*/
public string $error = '';

/**
* 检查时间
*
* @var int
*/
public int $checked_at = 0;
/**
* 检查时间
*
* @var int
*/
public int $checked_at = 0;

/**
* 创建健康状态
*
* @param int $response_time 响应时间
* @return self
*/
public static function healthy( int $response_time ): self {
$status = new self();
$status->status = self::STATUS_HEALTHY;
$status->response_time = $response_time;
$status->checked_at = time();
return $status;
}
/**
* 创建健康状态
*
* @param int $response_time 响应时间
* @return self
*/
public static function healthy( int $response_time ): self {
$status = new self();
$status->status = self::STATUS_HEALTHY;
$status->response_time = $response_time;
$status->checked_at = time();
return $status;
}

/**
* 创建降级状态
*
* @param int $response_time 响应时间
* @param string $reason 原因
* @return self
*/
public static function degraded( int $response_time, string $reason = '' ): self {
$status = new self();
$status->status = self::STATUS_DEGRADED;
$status->response_time = $response_time;
$status->error = $reason;
$status->checked_at = time();
return $status;
}
/**
* 创建降级状态
*
* @param int $response_time 响应时间
* @param string $reason 原因
* @return self
*/
public static function degraded( int $response_time, string $reason = '' ): self {
$status = new self();
$status->status = self::STATUS_DEGRADED;
$status->response_time = $response_time;
$status->error = $reason;
$status->checked_at = time();
return $status;
}

/**
* 创建失败状态
*
* @param string $error 错误信息
* @return self
*/
public static function failed( string $error ): self {
$status = new self();
$status->status = self::STATUS_FAILED;
$status->error = $error;
$status->checked_at = time();
return $status;
}
/**
* 创建失败状态
*
* @param string $error 错误信息
* @return self
*/
public static function failed( string $error ): self {
$status = new self();
$status->status = self::STATUS_FAILED;
$status->error = $error;
$status->checked_at = time();
return $status;
}

/**
* 是否健康
*
* @return bool
*/
public function is_healthy(): bool {
return $this->status === self::STATUS_HEALTHY;
}
/**
* 是否健康
*
* @return bool
*/
public function is_healthy(): bool {
return $this->status === self::STATUS_HEALTHY;
}

/**
* 是否可用(健康或降级)
*
* @return bool
*/
public function is_available(): bool {
return $this->status !== self::STATUS_FAILED;
}
/**
* 是否可用(健康或降级)
*
* @return bool
*/
public function is_available(): bool {
return $this->status !== self::STATUS_FAILED;
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource\Handlers;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

require_once __DIR__ . '/HandlerInterface.php';

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -20,128 +20,122 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class JsonHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'token',
'version' => 'json',
'download' => 'direct',
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'json',
'download' => 'direct',
];
}

/**
* 获取健康检查 URL
*
* 对于包含 {slug} 模板的 URL提取基础 URL 进行健康检查
*
* @return string
*/
public function get_check_url(): string {
$url = $this->source->api_url;
/**
* 获取健康检查 URL
*
* 对于包含 {slug} 模板的 URL提取基础 URL 进行健康检查
*
* @return string
*/
public function get_check_url(): string {
$url = $this->source->api_url;

// 如果 URL 包含 {slug} 模板,提取基础 URL
if ( strpos( $url, '{slug}' ) !== false ) {
// 从 https://updates.wenpai.net/api/v1/plugins/{slug}/info
// 提取 https://updates.wenpai.net/
$parsed = wp_parse_url( $url );
if ( $parsed && isset( $parsed['scheme'], $parsed['host'] ) ) {
$base_url = $parsed['scheme'] . '://' . $parsed['host'];
if ( isset( $parsed['port'] ) ) {
$base_url .= ':' . $parsed['port'];
}
return $base_url . '/';
}
}
// 如果 URL 包含 {slug} 模板,提取基础 URL
if ( strpos( $url, '{slug}' ) !== false ) {
// 从 https://updates.wenpai.net/api/v1/plugins/{slug}/info
// 提取 https://updates.wenpai.net/
$parsed = wp_parse_url( $url );
if ( $parsed && isset( $parsed['scheme'], $parsed['host'] ) ) {
$base_url = $parsed['scheme'] . '://' . $parsed['host'];
if ( isset( $parsed['port'] ) ) {
$base_url .= ':' . $parsed['port'];
}
return $base_url . '/';
}
}

return $url;
}
return $url;
}

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$url = $this->build_check_url( $slug );
$data = $this->request( $url );
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$url = $this->build_check_url( $slug );
$data = $this->request( $url );

if ( null === $data ) {
return null;
}
if ( null === $data ) {
return null;
}

// 处理 Plugin Update Checker 格式
$remote_version = $data['version'] ?? '';
// 处理 Plugin Update Checker 格式
$remote_version = $data['version'] ?? '';

if ( empty( $remote_version ) ) {
Logger::warning( 'JSON 响应缺少版本信息', array( 'url' => $url ) );
return null;
}
if ( empty( $remote_version ) ) {
Logger::warning( 'JSON 响应缺少版本信息', [ 'url' => $url ] );
return null;
}

// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug(
'无可用更新',
array(
'slug' => $slug,
'current' => $version,
'remote' => $remote_version,
)
);
return null;
}
// 检查是否有更新
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
Logger::debug( '无可用更新', [
'slug' => $slug,
'current' => $version,
'remote' => $remote_version,
] );
return null;
}

// 构建更新信息
$info = UpdateInfo::from_array( $data );
$info->slug = $slug;
// 构建更新信息
$info = UpdateInfo::from_array( $data );
$info->slug = $slug;

Logger::info(
'发现更新',
array(
'slug' => $slug,
'current' => $version,
'new' => $remote_version,
)
);
Logger::info( '发现更新', [
'slug' => $slug,
'current' => $version,
'new' => $remote_version,
] );

return $info;
}
return $info;
}

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$url = $this->build_check_url( $slug );
return $this->request( $url );
}
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$url = $this->build_check_url( $slug );
return $this->request( $url );
}

/**
* 构建检查 URL
*
* @param string $slug 插件/主题 slug
* @return string
*/
protected function build_check_url( string $slug ): string {
$url = $this->source->api_url;
/**
* 构建检查 URL
*
* @param string $slug 插件/主题 slug
* @return string
*/
protected function build_check_url( string $slug ): string {
$url = $this->source->api_url;

// 如果 URL 包含占位符,替换它
if ( strpos( $url, '{slug}' ) !== false ) {
return str_replace( '{slug}', $slug, $url );
}
// 如果 URL 包含占位符,替换它
if ( strpos( $url, '{slug}' ) !== false ) {
return str_replace( '{slug}', $slug, $url );
}

// 如果 URL 包含查询参数占位符
if ( strpos( $url, '?' ) !== false ) {
return add_query_arg( 'slug', $slug, $url );
}
// 如果 URL 包含查询参数占位符
if ( strpos( $url, '?' ) !== false ) {
return add_query_arg( 'slug', $slug, $url );
}

return $url;
}
return $url;
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource\Handlers;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**

View file

@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource\Handlers;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

require_once __DIR__ . '/HandlerInterface.php';

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -19,282 +19,282 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class WenPaiGitHandler extends AbstractHandler {

/**
* API 基础 URL
*
* @var string
*/
const API_BASE = 'https://git.wenpai.org/api/v1';
/**
* API 基础 URL
*
* @var string
*/
const API_BASE = 'https://git.wenpai.org/api/v1';

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'token',
'version' => 'release',
'download' => 'release',
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'release',
'download' => 'release',
];
}

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = parent::get_headers();
$headers['Accept'] = 'application/json';
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
return $headers;
}
/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = parent::get_headers();
$headers['Accept'] = 'application/json';
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
return $headers;
}

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( empty( $repo ) ) {
return $this->source->api_url;
}
return $this->get_api_base() . '/repos/' . $repo . '/releases';
}
/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( empty( $repo ) ) {
return $this->source->api_url;
}
return $this->get_api_base() . '/repos/' . $repo . '/releases';
}

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$repo = $this->parse_repo_url( $this->source->api_url );
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$repo = $this->parse_repo_url( $this->source->api_url );

if ( empty( $repo ) ) {
Logger::warning( '菲码源库: 无效的仓库 URL', array( 'url' => $this->source->api_url ) );
return null;
}
if ( empty( $repo ) ) {
Logger::warning( '菲码源库: 无效的仓库 URL', [ 'url' => $this->source->api_url ] );
return null;
}

$url = $this->get_api_base() . '/repos/' . $repo . '/releases';
$data = $this->request( $url );
$url = $this->get_api_base() . '/repos/' . $repo . '/releases';
$data = $this->request( $url );

if ( null === $data || empty( $data ) ) {
return null;
}
if ( null === $data || empty( $data ) ) {
return null;
}

$latest = $data[0] ?? null;
if ( null === $latest ) {
return null;
}
$latest = $data[0] ?? null;
if ( null === $latest ) {
return null;
}

$remote_version = ltrim( $latest['tag_name'] ?? '', 'v' );
if ( empty( $remote_version ) ) {
Logger::warning( '菲码源库: 响应缺少版本信息', array( 'repo' => $repo ) );
return null;
}
$remote_version = ltrim( $latest['tag_name'] ?? '', 'v' );
if ( empty( $remote_version ) ) {
Logger::warning( '菲码源库: 响应缺少版本信息', [ 'repo' => $repo ] );
return null;
}

if ( ! $this->is_newer_version( $version, $remote_version ) ) {
return null;
}
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
return null;
}

$download_url = $this->find_download_url( $latest, $slug, $repo );
if ( empty( $download_url ) ) {
Logger::warning( '菲码源库: 未找到下载 URL', array( 'repo' => $repo ) );
return null;
}
$download_url = $this->find_download_url( $latest, $slug, $repo );
if ( empty( $download_url ) ) {
Logger::warning( '菲码源库: 未找到下载 URL', [ 'repo' => $repo ] );
return null;
}

$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $download_url;
$info->details_url = $latest['html_url'] ?? '';
$info->last_updated = $latest['published_at'] ?? $latest['created_at'] ?? '';
$info->changelog = $latest['body'] ?? '';
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $download_url;
$info->details_url = $latest['html_url'] ?? '';
$info->last_updated = $latest['published_at'] ?? $latest['created_at'] ?? '';
$info->changelog = $latest['body'] ?? '';

return $info;
}
return $info;
}

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( empty( $repo ) ) {
return null;
}
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( empty( $repo ) ) {
return null;
}

$repo_url = $this->get_api_base() . '/repos/' . $repo;
$repo_data = $this->request( $repo_url );
$releases_url = $this->get_api_base() . '/repos/' . $repo . '/releases';
$release_data = $this->request( $releases_url );
$repo_url = $this->get_api_base() . '/repos/' . $repo;
$repo_data = $this->request( $repo_url );
$releases_url = $this->get_api_base() . '/repos/' . $repo . '/releases';
$release_data = $this->request( $releases_url );

if ( null === $repo_data ) {
return null;
}
if ( null === $repo_data ) {
return null;
}

$latest = $release_data[0] ?? array();
$version = ltrim( $latest['tag_name'] ?? '', 'v' );
$latest = $release_data[0] ?? [];
$version = ltrim( $latest['tag_name'] ?? '', 'v' );

return array(
'name' => $repo_data['name'] ?? $slug,
'slug' => $slug,
'version' => $version,
'download_url' => $this->find_download_url( $latest, $slug, $repo ),
'details_url' => $repo_data['html_url'] ?? '',
'last_updated' => $latest['published_at'] ?? $latest['created_at'] ?? '',
'sections' => array(
'description' => $repo_data['description'] ?? '',
'changelog' => $latest['body'] ?? '',
),
);
}
return [
'name' => $repo_data['name'] ?? $slug,
'slug' => $slug,
'version' => $version,
'download_url' => $this->find_download_url( $latest, $slug, $repo ),
'details_url' => $repo_data['html_url'] ?? '',
'last_updated' => $latest['published_at'] ?? $latest['created_at'] ?? '',
'sections' => [
'description' => $repo_data['description'] ?? '',
'changelog' => $latest['body'] ?? '',
],
];
}

/**
* 解析仓库 URL
*
* @param string $url URL
* @return string|null owner/repo 格式
*/
private function parse_repo_url( string $url ): ?string {
$url = trim( $url );
/**
* 解析仓库 URL
*
* @param string $url URL
* @return string|null owner/repo 格式
*/
private function parse_repo_url( string $url ): ?string {
$url = trim( $url );

$parts = wp_parse_url( $url );
if ( ! empty( $parts['host'] ) ) {
$path = trim( $parts['path'] ?? '', '/' );
$path = preg_replace( '#\.git$#', '', $path );
$parts = wp_parse_url( $url );
if ( ! empty( $parts['host'] ) ) {
$path = trim( $parts['path'] ?? '', '/' );
$path = preg_replace( '#\.git$#', '', $path );

if ( preg_match( '#^api/v1/repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
return $matches[1];
}
if ( preg_match( '#^api/v1/repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
return $matches[1];
}

if ( preg_match( '#^repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
return $matches[1];
}
if ( preg_match( '#^repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
return $matches[1];
}

if (
( preg_match( '#^api/v1(?:/|$)#', $path ) && ! preg_match( '#^api/v1/repos/[\w.-]+/[\w.-]+#', $path ) )
|| ( preg_match( '#^repos(?:/|$)#', $path ) && ! preg_match( '#^repos/[\w.-]+/[\w.-]+#', $path ) )
) {
return null;
}
if (
( preg_match( '#^api/v1(?:/|$)#', $path ) && ! preg_match( '#^api/v1/repos/[\w.-]+/[\w.-]+#', $path ) )
|| ( preg_match( '#^repos(?:/|$)#', $path ) && ! preg_match( '#^repos/[\w.-]+/[\w.-]+#', $path ) )
) {
return null;
}

$segments = array_values( array_filter( explode( '/', $path ) ) );
if ( count( $segments ) >= 2 ) {
$repo = $segments[0] . '/' . $segments[1];
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $repo ) ) {
return $repo;
}
}
$segments = array_values( array_filter( explode( '/', $path ) ) );
if ( count( $segments ) >= 2 ) {
$repo = $segments[0] . '/' . $segments[1];
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $repo ) ) {
return $repo;
}
}

return null;
}
return null;
}

$path = preg_replace( '#^https?://#', '', $url );
$path = preg_replace( '#^[^/]+/#', '', $path );
$path = trim( $path, '/' );
$path = preg_replace( '#\.git$#', '', $path );
$path = preg_replace( '#^https?://#', '', $url );
$path = preg_replace( '#^[^/]+/#', '', $path );
$path = trim( $path, '/' );
$path = preg_replace( '#\.git$#', '', $path );

if ( preg_match( '#^api/v1/repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
return $matches[1];
}
if ( preg_match( '#^api/v1/repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
return $matches[1];
}

if ( preg_match( '#^repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
return $matches[1];
}
if ( preg_match( '#^repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
return $matches[1];
}

if (
( preg_match( '#^api/v1(?:/|$)#', $path ) && ! preg_match( '#^api/v1/repos/[\w.-]+/[\w.-]+#', $path ) )
|| ( preg_match( '#^repos(?:/|$)#', $path ) && ! preg_match( '#^repos/[\w.-]+/[\w.-]+#', $path ) )
) {
return null;
}
if (
( preg_match( '#^api/v1(?:/|$)#', $path ) && ! preg_match( '#^api/v1/repos/[\w.-]+/[\w.-]+#', $path ) )
|| ( preg_match( '#^repos(?:/|$)#', $path ) && ! preg_match( '#^repos/[\w.-]+/[\w.-]+#', $path ) )
) {
return null;
}

$segments = array_values( array_filter( explode( '/', $path ) ) );
if ( count( $segments ) >= 2 ) {
$repo = $segments[0] . '/' . $segments[1];
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $repo ) ) {
return $repo;
}
}
$segments = array_values( array_filter( explode( '/', $path ) ) );
if ( count( $segments ) >= 2 ) {
$repo = $segments[0] . '/' . $segments[1];
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $repo ) ) {
return $repo;
}
}

return null;
}
return null;
}

/**
* 查找下载 URL
*
* @param array $release Release 数据
* @param string $slug 插件 slug
* @param string $repo 仓库路径
* @return string|null
*/
private function find_download_url( array $release, string $slug, string $repo ): ?string {
if ( ! empty( $release['assets'] ) ) {
foreach ( $release['assets'] as $asset ) {
$name = $asset['name'] ?? '';
/**
* 查找下载 URL
*
* @param array $release Release 数据
* @param string $slug 插件 slug
* @param string $repo 仓库路径
* @return string|null
*/
private function find_download_url( array $release, string $slug, string $repo ): ?string {
if ( ! empty( $release['assets'] ) ) {
foreach ( $release['assets'] as $asset ) {
$name = $asset['name'] ?? '';

if ( preg_match( '/\.zip$/i', $name ) ) {
if ( stripos( $name, $slug ) !== false ) {
return $asset['browser_download_url'] ?? null;
}
}
}
if ( preg_match( '/\.zip$/i', $name ) ) {
if ( stripos( $name, $slug ) !== false ) {
return $asset['browser_download_url'] ?? null;
}
}
}

foreach ( $release['assets'] as $asset ) {
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
return $asset['browser_download_url'] ?? null;
}
}
}
foreach ( $release['assets'] as $asset ) {
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
return $asset['browser_download_url'] ?? null;
}
}
}

if ( ! empty( $release['zipball_url'] ) ) {
return $release['zipball_url'];
}
if ( ! empty( $release['zipball_url'] ) ) {
return $release['zipball_url'];
}

$tag = $release['tag_name'] ?? '';
if ( ! empty( $tag ) ) {
return $this->get_api_base() . '/repos/' . $repo . '/archive/' . $tag . '.zip';
}
$tag = $release['tag_name'] ?? '';
if ( ! empty( $tag ) ) {
return $this->get_api_base() . '/repos/' . $repo . '/archive/' . $tag . '.zip';
}

return null;
}
return null;
}

/**
* 获取 API 基础地址
*
* @return string
*/
private function get_api_base(): string {
$parts = wp_parse_url( $this->source->api_url );
if ( ! empty( $parts['host'] ) ) {
$scheme = $parts['scheme'] ?? 'https';
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
$path = $parts['path'] ?? '';
$base_path = '';
/**
* 获取 API 基础地址
*
* @return string
*/
private function get_api_base(): string {
$parts = wp_parse_url( $this->source->api_url );
if ( ! empty( $parts['host'] ) ) {
$scheme = $parts['scheme'] ?? 'https';
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
$path = $parts['path'] ?? '';
$base_path = '';

if ( preg_match( '#^(.*?)/api/v1#', $path, $matches ) ) {
$base_path = rtrim( $matches[1], '/' );
} else {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( ! empty( $repo ) ) {
$repo_path = '/' . trim( $repo, '/' );
$pos = strpos( $path, $repo_path );
if ( false !== $pos ) {
$base_path = rtrim( substr( $path, 0, $pos ), '/' );
}
}
}
if ( preg_match( '#^(.*?)/api/v1#', $path, $matches ) ) {
$base_path = rtrim( $matches[1], '/' );
} else {
$repo = $this->parse_repo_url( $this->source->api_url );
if ( ! empty( $repo ) ) {
$repo_path = '/' . trim( $repo, '/' );
$pos = strpos( $path, $repo_path );
if ( false !== $pos ) {
$base_path = rtrim( substr( $path, 0, $pos ), '/' );
}
}
}

return $scheme . '://' . $parts['host'] . $port . $base_path . '/api/v1';
}
return $scheme . '://' . $parts['host'] . $port . $base_path . '/api/v1';
}

return self::API_BASE;
}
return self::API_BASE;
}
}

View file

@ -11,7 +11,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -21,96 +21,96 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class ZipHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return array(
'auth' => 'token',
'version' => 'zip',
'download' => 'direct',
);
}
/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'zip',
'download' => 'direct',
];
}

/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$remote_version = $this->resolve_version();
/**
* 检查更新
*
* @param string $slug 插件/主题 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
$remote_version = $this->resolve_version();

if ( empty( $remote_version ) ) {
Logger::debug( 'ZIP: 无法解析版本号', array( 'url' => $this->source->api_url ) );
return null;
}
if ( empty( $remote_version ) ) {
Logger::debug( 'ZIP: 无法解析版本号', [ 'url' => $this->source->api_url ] );
return null;
}

if ( ! $this->is_newer_version( $version, $remote_version ) ) {
return null;
}
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
return null;
}

$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $this->source->api_url;
$info->details_url = $this->source->api_url;
$info = new UpdateInfo();
$info->slug = $slug;
$info->version = $remote_version;
$info->download_url = $this->source->api_url;
$info->details_url = $this->source->api_url;

return $info;
}
return $info;
}

/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$remote_version = $this->resolve_version();
/**
* 获取项目信息
*
* @param string $slug 插件/主题 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
$remote_version = $this->resolve_version();

if ( empty( $remote_version ) ) {
return null;
}
if ( empty( $remote_version ) ) {
return null;
}

return array(
'name' => $this->source->name ?: $slug,
'slug' => $slug,
'version' => $remote_version,
'download_url' => $this->source->api_url,
'package' => $this->source->api_url,
'details_url' => $this->source->api_url,
);
}
return [
'name' => $this->source->name ?: $slug,
'slug' => $slug,
'version' => $remote_version,
'download_url' => $this->source->api_url,
'package' => $this->source->api_url,
'details_url' => $this->source->api_url,
];
}

/**
* 解析版本号
*
* @return string
*/
private function resolve_version(): string {
$metadata = $this->source->metadata ?? array();
/**
* 解析版本号
*
* @return string
*/
private function resolve_version(): string {
$metadata = $this->source->metadata ?? [];

if ( ! empty( $metadata['version'] ) ) {
return (string) $metadata['version'];
}
if ( ! empty( $metadata['version'] ) ) {
return (string) $metadata['version'];
}

if ( ! empty( $metadata['new_version'] ) ) {
return (string) $metadata['new_version'];
}
if ( ! empty( $metadata['new_version'] ) ) {
return (string) $metadata['new_version'];
}

$path = wp_parse_url( $this->source->api_url, PHP_URL_PATH );
if ( empty( $path ) ) {
return '';
}
$path = wp_parse_url( $this->source->api_url, PHP_URL_PATH );
if ( empty( $path ) ) {
return '';
}

$filename = basename( $path );
if ( preg_match( '/(\d+\.\d+\.\d+(?:[-+][\w\.]+)?)/', $filename, $matches ) ) {
return $matches[1];
}
$filename = basename( $path );
if ( preg_match( '/(\d+\.\d+\.\d+(?:[-+][\w\.]+)?)/', $filename, $matches ) ) {
return $matches[1];
}

return '';
}
return '';
}
}

View file

@ -15,7 +15,7 @@ use WPBridge\Cache\FallbackStrategy;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -23,353 +23,338 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class PluginUpdater {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 源解析器(方案 B
*
* @var SourceResolver
*/
private SourceResolver $source_resolver;
/**
* 源解析器(方案 B
*
* @var SourceResolver
*/
private SourceResolver $source_resolver;

/**
* 降级策略
*
* @var FallbackStrategy
*/
private FallbackStrategy $fallback_strategy;
/**
* 降级策略
*
* @var FallbackStrategy
*/
private FallbackStrategy $fallback_strategy;

/**
* 缓存键前缀
*
* @var string
*/
const CACHE_PREFIX = 'wpbridge_plugin_update_';
/**
* 缓存键前缀
*
* @var string
*/
const CACHE_PREFIX = 'wpbridge_plugin_update_';

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->source_resolver = new SourceResolver();
$this->fallback_strategy = new FallbackStrategy( $settings );
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->source_resolver = new SourceResolver();
$this->fallback_strategy = new FallbackStrategy( $settings );

$this->init_hooks();
}
$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 插件更新检查
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_updates' ), 10, 1 );
/**
* 初始化钩子
*/
private function init_hooks(): void {
// 插件更新检查
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_updates' ], 10, 1 );

// 插件信息
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 10, 3 );
// 插件信息
add_filter( 'plugins_api', [ $this, 'plugin_info' ], 10, 3 );

// 下载包过滤
add_filter( 'upgrader_pre_download', array( $this, 'filter_download' ), 10, 3 );
}
// 下载包过滤
add_filter( 'upgrader_pre_download', [ $this, 'filter_download' ], 10, 3 );
}

/**
* 检查插件更新
*
* @param object $transient 更新 transient
* @return object
*/
public function check_updates( $transient ) {
if ( empty( $transient ) || ! is_object( $transient ) ) {
$transient = new \stdClass();
}
/**
* 检查插件更新
*
* @param object $transient 更新 transient
* @return object
*/
public function check_updates( $transient ) {
if ( empty( $transient ) || ! is_object( $transient ) ) {
$transient = new \stdClass();
}

if ( ! isset( $transient->response ) ) {
$transient->response = array();
}
if ( ! isset( $transient->response ) ) {
$transient->response = [];
}

if ( ! isset( $transient->no_update ) ) {
$transient->no_update = array();
}
if ( ! isset( $transient->no_update ) ) {
$transient->no_update = [];
}

// 获取已安装的插件
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
// 获取已安装的插件
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();

foreach ( $plugins as $plugin_file => $plugin_data ) {
$slug = $this->get_plugin_slug( $plugin_file );
foreach ( $plugins as $plugin_file => $plugin_data ) {
$slug = $this->get_plugin_slug( $plugin_file );

$item_key = 'plugin:' . $plugin_file;
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'plugin' );
$mode = $resolved['mode'];
$matching_sources = $resolved['sources'];
$allow_wporg_fallback = ! empty( $resolved['has_wporg'] );
$item_key = 'plugin:' . $plugin_file;
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'plugin' );
$mode = $resolved['mode'];
$matching_sources = $resolved['sources'];
$allow_wporg_fallback = ! empty( $resolved['has_wporg'] );

if ( $mode === ItemSourceManager::MODE_DISABLED ) {
unset( $transient->response[ $plugin_file ] );
$transient->no_update[ $plugin_file ] = (object) array(
'slug' => $slug,
'plugin' => $plugin_file,
'new_version' => $plugin_data['Version'],
);
continue;
}
if ( $mode === ItemSourceManager::MODE_DISABLED ) {
unset( $transient->response[ $plugin_file ] );
$transient->no_update[ $plugin_file ] = (object) [
'slug' => $slug,
'plugin' => $plugin_file,
'new_version' => $plugin_data['Version'],
];
continue;
}

if ( empty( $matching_sources ) ) {
if ( $mode === ItemSourceManager::MODE_CUSTOM ) {
unset( $transient->response[ $plugin_file ] );
$transient->no_update[ $plugin_file ] = (object) array(
'slug' => $slug,
'plugin' => $plugin_file,
'new_version' => $plugin_data['Version'],
);
}
continue;
}
if ( empty( $matching_sources ) ) {
if ( $mode === ItemSourceManager::MODE_CUSTOM ) {
unset( $transient->response[ $plugin_file ] );
$transient->no_update[ $plugin_file ] = (object) [
'slug' => $slug,
'plugin' => $plugin_file,
'new_version' => $plugin_data['Version'],
];
}
continue;
}

$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;
$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;

if ( $take_over ) {
// 接管更新检查,清除默认响应
unset( $transient->response[ $plugin_file ] );
}
if ( $take_over ) {
// 接管更新检查,清除默认响应
unset( $transient->response[ $plugin_file ] );
}

// 尝试从缓存获取(使用 md5 哈希防止缓存污染)
$cache_key = self::CACHE_PREFIX . md5( $slug . get_site_url() );
$cached = get_transient( $cache_key );
// 尝试从缓存获取(使用 md5 哈希防止缓存污染)
$cache_key = self::CACHE_PREFIX . md5( $slug . get_site_url() );
$cached = get_transient( $cache_key );

if ( false !== $cached ) {
if ( ! empty( $cached['update'] ) ) {
$transient->response[ $plugin_file ] = (object) $cached['update'];
unset( $transient->no_update[ $plugin_file ] );
} elseif ( $take_over ) {
$transient->no_update[ $plugin_file ] = (object) array(
'slug' => $slug,
'plugin' => $plugin_file,
'new_version' => $plugin_data['Version'],
);
}
continue;
}
if ( false !== $cached ) {
if ( ! empty( $cached['update'] ) ) {
$transient->response[ $plugin_file ] = (object) $cached['update'];
unset( $transient->no_update[ $plugin_file ] );
} else {
if ( $take_over ) {
$transient->no_update[ $plugin_file ] = (object) [
'slug' => $slug,
'plugin' => $plugin_file,
'new_version' => $plugin_data['Version'],
];
}
}
continue;
}

// 检查更新
$update_info = $this->check_plugin_update( $slug, $plugin_data['Version'], $matching_sources );
// 检查更新
$update_info = $this->check_plugin_update( $slug, $plugin_data['Version'], $matching_sources );

if ( null !== $update_info ) {
$update_object = $update_info->to_wp_update_object();
$update_object->plugin = $plugin_file;
if ( null !== $update_info ) {
$update_object = $update_info->to_wp_update_object();
$update_object->plugin = $plugin_file;

$transient->response[ $plugin_file ] = $update_object;
unset( $transient->no_update[ $plugin_file ] );
$transient->response[ $plugin_file ] = $update_object;
unset( $transient->no_update[ $plugin_file ] );

// 缓存结果
set_transient(
$cache_key,
array(
'update' => (array) $update_object,
),
$this->settings->get_cache_ttl()
);
// 缓存结果
set_transient( $cache_key, [
'update' => (array) $update_object,
], $this->settings->get_cache_ttl() );

Logger::info(
'插件更新可用',
array(
'plugin' => $plugin_file,
'current' => $plugin_data['Version'],
'new' => $update_info->version,
)
);
} else {
if ( $take_over ) {
$transient->no_update[ $plugin_file ] = (object) array(
'slug' => $slug,
'plugin' => $plugin_file,
'new_version' => $plugin_data['Version'],
);
}
Logger::info( '插件更新可用', [
'plugin' => $plugin_file,
'current' => $plugin_data['Version'],
'new' => $update_info->version,
] );
} else {
if ( $take_over ) {
$transient->no_update[ $plugin_file ] = (object) [
'slug' => $slug,
'plugin' => $plugin_file,
'new_version' => $plugin_data['Version'],
];
}

// 缓存无更新结果
set_transient(
$cache_key,
array(
'update' => null,
),
$this->settings->get_cache_ttl()
);
}
}
// 缓存无更新结果
set_transient( $cache_key, [
'update' => null,
], $this->settings->get_cache_ttl() );
}
}

return $transient;
}
return $transient;
}

/**
* 检查单个插件更新
*
* @param string $slug 插件 slug
* @param string $version 当前版本
* @param SourceModel[] $sources 更新源列表
* @return UpdateInfo|null
*/
private function check_plugin_update( string $slug, string $version, array $sources ): ?UpdateInfo {
$cache_key = 'update_info_plugin_' . md5( $slug . get_site_url() );
/**
* 检查单个插件更新
*
* @param string $slug 插件 slug
* @param string $version 当前版本
* @param SourceModel[] $sources 更新源列表
* @return UpdateInfo|null
*/
private function check_plugin_update( string $slug, string $version, array $sources ): ?UpdateInfo {
$cache_key = 'update_info_plugin_' . md5( $slug . get_site_url() );

$result = $this->fallback_strategy->execute_with_fallback(
$sources,
function ( SourceModel $source ) use ( $slug, $version ) {
$handler = $source->get_handler();
$result = $this->fallback_strategy->execute_with_fallback(
$sources,
function( SourceModel $source ) use ( $slug, $version ) {
$handler = $source->get_handler();

if ( null === $handler ) {
Logger::warning(
'无法获取处理器',
array(
'source' => $source->id,
'type' => $source->type,
)
);
return null;
}
if ( null === $handler ) {
Logger::warning( '无法获取处理器', [
'source' => $source->id,
'type' => $source->type,
] );
return null;
}

try {
return $handler->check_update( $slug, $version );
} catch ( \Exception $e ) {
Logger::error(
'检查更新时发生错误',
array(
'source' => $source->id,
'slug' => $slug,
'error' => $e->getMessage(),
)
);
throw $e;
}
},
$cache_key
);
try {
return $handler->check_update( $slug, $version );
} catch ( \Exception $e ) {
Logger::error( '检查更新时发生错误', [
'source' => $source->id,
'slug' => $slug,
'error' => $e->getMessage(),
] );
throw $e;
}
},
$cache_key
);

return $result instanceof UpdateInfo ? $result : null;
}
return $result instanceof UpdateInfo ? $result : null;
}

/**
* 获取插件信息
*
* @param false|object|array $result 结果
* @param string $action 动作
* @param object $args 参数
* @return false|object|array
*/
public function plugin_info( $result, $action, $args ) {
if ( 'plugin_information' !== $action ) {
return $result;
}
/**
* 获取插件信息
*
* @param false|object|array $result 结果
* @param string $action 动作
* @param object $args 参数
* @return false|object|array
*/
public function plugin_info( $result, $action, $args ) {
if ( 'plugin_information' !== $action ) {
return $result;
}

$slug = $args->slug ?? '';
$slug = $args->slug ?? '';

if ( empty( $slug ) ) {
return $result;
}
if ( empty( $slug ) ) {
return $result;
}

$item_key = $this->get_item_key_from_slug( $slug );
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'plugin' );
$mode = $resolved['mode'];
$sources = $resolved['sources'];
$item_key = $this->get_item_key_from_slug( $slug );
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'plugin' );
$mode = $resolved['mode'];
$sources = $resolved['sources'];

if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
return $result;
}
if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
return $result;
}

// 获取第一个匹配的源
$source = reset( $sources );
$handler = $source->get_handler();
// 获取第一个匹配的源
$source = reset( $sources );
$handler = $source->get_handler();

if ( null === $handler ) {
return $result;
}
if ( null === $handler ) {
return $result;
}

$info = $handler->get_info( $slug );
$info = $handler->get_info( $slug );

if ( null === $info ) {
return $result;
}
if ( null === $info ) {
return $result;
}

// 转换为 plugins_api 响应格式
$update_info = Handlers\UpdateInfo::from_array( $info );
return $update_info->to_plugins_api_response( $info['name'] ?? $slug );
}
// 转换为 plugins_api 响应格式
$update_info = Handlers\UpdateInfo::from_array( $info );
return $update_info->to_plugins_api_response( $info['name'] ?? $slug );
}

/**
* 从 slug 推断项目键
*
* @param string $slug 插件 slug
* @return string
*/
private function get_item_key_from_slug( string $slug ): string {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
/**
* 从 slug 推断项目键
*
* @param string $slug 插件 slug
* @return string
*/
private function get_item_key_from_slug( string $slug ): string {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$plugins = get_plugins();
foreach ( $plugins as $plugin_file => $plugin_data ) {
$plugin_slug = $this->get_plugin_slug( $plugin_file );
if ( $plugin_slug === $slug ) {
return 'plugin:' . $plugin_file;
}
}
$plugins = get_plugins();
foreach ( $plugins as $plugin_file => $plugin_data ) {
$plugin_slug = $this->get_plugin_slug( $plugin_file );
if ( $plugin_slug === $slug ) {
return 'plugin:' . $plugin_file;
}
}

return 'plugin:' . $slug;
}
return 'plugin:' . $slug;
}

/**
* 过滤下载
*
* @param bool $reply 是否已处理
* @param string $package 下载包 URL
* @param object $upgrader 升级器
* @return bool
*/
public function filter_download( $reply, $package, $upgrader ) {
// 目前不做特殊处理,直接返回
return $reply;
}
/**
* 过滤下载
*
* @param bool $reply 是否已处理
* @param string $package 下载包 URL
* @param object $upgrader 升级器
* @return bool
*/
public function filter_download( $reply, $package, $upgrader ) {
// 目前不做特殊处理,直接返回
return $reply;
}

/**
* 获取插件 slug
*
* @param string $plugin_file 插件文件路径
* @return string
*/
private function get_plugin_slug( string $plugin_file ): string {
if ( strpos( $plugin_file, '/' ) !== false ) {
return dirname( $plugin_file );
}
return str_replace( '.php', '', $plugin_file );
}
/**
* 获取插件 slug
*
* @param string $plugin_file 插件文件路径
* @return string
*/
private function get_plugin_slug( string $plugin_file ): string {
if ( strpos( $plugin_file, '/' ) !== false ) {
return dirname( $plugin_file );
}
return str_replace( '.php', '', $plugin_file );
}

/**
* 清除插件更新缓存
*
* @param string|null $slug 插件 slug为空则清除所有
*/
public function clear_cache( ?string $slug = null ): void {
if ( null !== $slug ) {
delete_transient( self::CACHE_PREFIX . md5( $slug . get_site_url() ) );
} else {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'
)
);
}
/**
* 清除插件更新缓存
*
* @param string|null $slug 插件 slug为空则清除所有
*/
public function clear_cache( ?string $slug = null ): void {
if ( null !== $slug ) {
delete_transient( self::CACHE_PREFIX . md5( $slug . get_site_url() ) );
} else {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'
)
);
}

// 清除 WordPress 更新缓存
delete_site_transient( 'update_plugins' );
}
// 清除 WordPress 更新缓存
delete_site_transient( 'update_plugins' );
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -17,110 +17,110 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class PresetSources {

/**
* 文派开源更新源(默认预置)
*/
const WENPAI_OPEN = array(
'id' => 'wenpai-open',
'name' => '文派开源更新源',
'type' => SourceType::JSON,
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
'enabled' => true,
'priority' => 10,
'is_preset' => true,
);
/**
* 文派开源更新源(默认预置)
*/
const WENPAI_OPEN = [
'id' => 'wenpai-open',
'name' => '文派开源更新源',
'type' => SourceType::JSON,
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
'enabled' => true,
'priority' => 10,
'is_preset' => true,
];

/**
* ArkPress文派自托管方案
*/
const ARKPRESS = array(
'id' => 'arkpress',
'name' => 'ArkPress',
'type' => SourceType::ARKPRESS,
'api_url' => '', // 用户自定义
'enabled' => false,
'priority' => 20,
'is_preset' => true,
);
/**
* ArkPress文派自托管方案
*/
const ARKPRESS = [
'id' => 'arkpress',
'name' => 'ArkPress',
'type' => SourceType::ARKPRESS,
'api_url' => '', // 用户自定义
'enabled' => false,
'priority' => 20,
'is_preset' => true,
];

/**
* AspireCloud
*/
const ASPIRECLOUD = array(
'id' => 'aspirecloud',
'name' => 'AspireCloud',
'type' => SourceType::ASPIRECLOUD,
'api_url' => 'https://api.aspirepress.org',
'enabled' => false,
'priority' => 30,
'is_preset' => true,
);
/**
* AspireCloud
*/
const ASPIRECLOUD = [
'id' => 'aspirecloud',
'name' => 'AspireCloud',
'type' => SourceType::ASPIRECLOUD,
'api_url' => 'https://api.aspirepress.org',
'enabled' => false,
'priority' => 30,
'is_preset' => true,
];

/**
* FAIR Package Manager
*/
const FAIR = array(
'id' => 'fair',
'name' => 'FAIR Package Manager',
'type' => SourceType::FAIR,
'api_url' => 'https://api.fairpm.org',
'enabled' => false,
'priority' => 40,
'is_preset' => true,
);
/**
* FAIR Package Manager
*/
const FAIR = [
'id' => 'fair',
'name' => 'FAIR Package Manager',
'type' => SourceType::FAIR,
'api_url' => 'https://api.fairpm.org',
'enabled' => false,
'priority' => 40,
'is_preset' => true,
];

/**
* 获取所有预置源
*
* @return array
*/
public static function get_all(): array {
return array(
self::WENPAI_OPEN,
// 以下预置源默认不添加,用户可手动启用
// self::ARKPRESS,
// self::ASPIRECLOUD,
// self::FAIR,
);
}
/**
* 获取所有预置源
*
* @return array
*/
public static function get_all(): array {
return [
self::WENPAI_OPEN,
// 以下预置源默认不添加,用户可手动启用
// self::ARKPRESS,
// self::ASPIRECLOUD,
// self::FAIR,
];
}

/**
* 获取可用的预置源模板
*
* @return array
*/
public static function get_templates(): array {
return array(
'arkpress' => self::ARKPRESS,
'aspirecloud' => self::ASPIRECLOUD,
'fair' => self::FAIR,
);
}
/**
* 获取可用的预置源模板
*
* @return array
*/
public static function get_templates(): array {
return [
'arkpress' => self::ARKPRESS,
'aspirecloud' => self::ASPIRECLOUD,
'fair' => self::FAIR,
];
}

/**
* 根据 ID 获取预置源
*
* @param string $id 预置源 ID
* @return array|null
*/
public static function get_by_id( string $id ): ?array {
$all = array(
'wenpai-open' => self::WENPAI_OPEN,
'arkpress' => self::ARKPRESS,
'aspirecloud' => self::ASPIRECLOUD,
'fair' => self::FAIR,
);
/**
* 根据 ID 获取预置源
*
* @param string $id 预置源 ID
* @return array|null
*/
public static function get_by_id( string $id ): ?array {
$all = [
'wenpai-open' => self::WENPAI_OPEN,
'arkpress' => self::ARKPRESS,
'aspirecloud' => self::ASPIRECLOUD,
'fair' => self::FAIR,
];

return $all[ $id ] ?? null;
}
return $all[ $id ] ?? null;
}

/**
* 检查是否是预置源 ID
*
* @param string $id 源 ID
* @return bool
*/
public static function is_preset_id( string $id ): bool {
return in_array( $id, array( 'wenpai-open', 'arkpress', 'aspirecloud', 'fair' ), true );
}
/**
* 检查是否是预置源 ID
*
* @param string $id 源 ID
* @return bool
*/
public static function is_preset_id( string $id ): bool {
return in_array( $id, [ 'wenpai-open', 'arkpress', 'aspirecloud', 'fair' ], true );
}
}

View file

@ -12,7 +12,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -20,250 +20,235 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class SourceManager {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 缓存的源模型
*
* @var array<string, SourceModel>
*/
private array $source_models = array();
/**
* 缓存的源模型
*
* @var array<string, SourceModel>
*/
private array $source_models = [];

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}

/**
* 获取所有源
*
* @return SourceModel[]
*/
public function get_all(): array {
$sources = $this->settings->get_sources();
$models = array();
/**
* 获取所有源
*
* @return SourceModel[]
*/
public function get_all(): array {
$sources = $this->settings->get_sources();
$models = [];

foreach ( $sources as $source ) {
$model = SourceModel::from_array( $source );
$models[ $model->id ] = $model;
}
foreach ( $sources as $source ) {
$model = SourceModel::from_array( $source );
$models[ $model->id ] = $model;
}

$this->source_models = $models;
return $models;
}
$this->source_models = $models;
return $models;
}

/**
* 获取启用的源
*
* @return SourceModel[]
*/
public function get_enabled(): array {
$all = $this->get_all();
return array_filter(
$all,
function ( SourceModel $source ) {
return $source->enabled;
}
);
}
/**
* 获取启用的源
*
* @return SourceModel[]
*/
public function get_enabled(): array {
$all = $this->get_all();
return array_filter( $all, function( SourceModel $source ) {
return $source->enabled;
} );
}

/**
* 按优先级排序获取启用的源
*
* @return SourceModel[]
*/
public function get_enabled_sorted(): array {
$enabled = $this->get_enabled();
uasort(
$enabled,
function ( SourceModel $a, SourceModel $b ) {
return $a->priority <=> $b->priority;
}
);
return $enabled;
}
/**
* 按优先级排序获取启用的源
*
* @return SourceModel[]
*/
public function get_enabled_sorted(): array {
$enabled = $this->get_enabled();
uasort( $enabled, function( SourceModel $a, SourceModel $b ) {
return $a->priority <=> $b->priority;
} );
return $enabled;
}

/**
* 获取单个源
*
* @param string $id 源 ID
* @return SourceModel|null
*/
public function get( string $id ): ?SourceModel {
if ( isset( $this->source_models[ $id ] ) ) {
return $this->source_models[ $id ];
}
/**
* 获取单个源
*
* @param string $id 源 ID
* @return SourceModel|null
*/
public function get( string $id ): ?SourceModel {
if ( isset( $this->source_models[ $id ] ) ) {
return $this->source_models[ $id ];
}

$source = $this->settings->get_source( $id );
if ( null === $source ) {
return null;
}
$source = $this->settings->get_source( $id );
if ( null === $source ) {
return null;
}

$model = SourceModel::from_array( $source );
$this->source_models[ $id ] = $model;
return $model;
}
$model = SourceModel::from_array( $source );
$this->source_models[ $id ] = $model;
return $model;
}

/**
* 根据 slug 获取源
*
* @param string $slug 插件/主题 slug
* @param string $item_type 项目类型
* @return SourceModel[]
*/
public function get_by_slug( string $slug, string $item_type = 'plugin' ): array {
$all = $this->get_enabled_sorted();
return array_filter(
$all,
function ( SourceModel $source ) use ( $slug, $item_type ) {
// 空 slug 表示匹配所有
if ( empty( $source->slug ) ) {
return $source->item_type === $item_type;
}
return $source->slug === $slug && $source->item_type === $item_type;
}
);
}
/**
* 根据 slug 获取源
*
* @param string $slug 插件/主题 slug
* @param string $item_type 项目类型
* @return SourceModel[]
*/
public function get_by_slug( string $slug, string $item_type = 'plugin' ): array {
$all = $this->get_enabled_sorted();
return array_filter( $all, function( SourceModel $source ) use ( $slug, $item_type ) {
// 空 slug 表示匹配所有
if ( empty( $source->slug ) ) {
return $source->item_type === $item_type;
}
return $source->slug === $slug && $source->item_type === $item_type;
} );
}

/**
* 添加源
*
* @param SourceModel $source 源模型
* @return bool
*/
public function add( SourceModel $source ): bool {
// 验证
$errors = $source->validate();
if ( ! empty( $errors ) ) {
Logger::error( '添加源失败:验证错误', array( 'errors' => $errors ) );
return false;
}
/**
* 添加源
*
* @param SourceModel $source 源模型
* @return bool
*/
public function add( SourceModel $source ): bool {
// 验证
$errors = $source->validate();
if ( ! empty( $errors ) ) {
Logger::error( '添加源失败:验证错误', [ 'errors' => $errors ] );
return false;
}

// 生成 ID
if ( empty( $source->id ) ) {
$source->id = 'source_' . wp_generate_uuid4();
}
// 生成 ID
if ( empty( $source->id ) ) {
$source->id = 'source_' . wp_generate_uuid4();
}

$result = $this->settings->add_source( $source->to_array() );
$result = $this->settings->add_source( $source->to_array() );

if ( $result ) {
$this->source_models[ $source->id ] = $source;
Logger::info(
'添加源成功',
array(
'id' => $source->id,
'name' => $source->name,
)
);
}
if ( $result ) {
$this->source_models[ $source->id ] = $source;
Logger::info( '添加源成功', [ 'id' => $source->id, 'name' => $source->name ] );
}

return $result;
}
return $result;
}

/**
* 更新源
*
* @param SourceModel $source 源模型
* @return bool
*/
public function update( SourceModel $source ): bool {
// 验证
$errors = $source->validate();
if ( ! empty( $errors ) ) {
Logger::error( '更新源失败:验证错误', array( 'errors' => $errors ) );
return false;
}
/**
* 更新源
*
* @param SourceModel $source 源模型
* @return bool
*/
public function update( SourceModel $source ): bool {
// 验证
$errors = $source->validate();
if ( ! empty( $errors ) ) {
Logger::error( '更新源失败:验证错误', [ 'errors' => $errors ] );
return false;
}

$result = $this->settings->update_source( $source->id, $source->to_array() );
$result = $this->settings->update_source( $source->id, $source->to_array() );

if ( $result ) {
$this->source_models[ $source->id ] = $source;
Logger::info( '更新源成功', array( 'id' => $source->id ) );
}
if ( $result ) {
$this->source_models[ $source->id ] = $source;
Logger::info( '更新源成功', [ 'id' => $source->id ] );
}

return $result;
}
return $result;
}

/**
* 删除源
*
* @param string $id 源 ID
* @return bool
*/
public function delete( string $id ): bool {
// 不允许删除预置源
if ( PresetSources::is_preset_id( $id ) ) {
Logger::warning( '尝试删除预置源', array( 'id' => $id ) );
return false;
}
/**
* 删除源
*
* @param string $id 源 ID
* @return bool
*/
public function delete( string $id ): bool {
// 不允许删除预置源
if ( PresetSources::is_preset_id( $id ) ) {
Logger::warning( '尝试删除预置源', [ 'id' => $id ] );
return false;
}

$result = $this->settings->delete_source( $id );
$result = $this->settings->delete_source( $id );

if ( $result ) {
unset( $this->source_models[ $id ] );
Logger::info( '删除源成功', array( 'id' => $id ) );
}
if ( $result ) {
unset( $this->source_models[ $id ] );
Logger::info( '删除源成功', [ 'id' => $id ] );
}

return $result;
}
return $result;
}

/**
* 启用/禁用源
*
* @param string $id 源 ID
* @param bool $enabled 是否启用
* @return bool
*/
public function toggle( string $id, bool $enabled ): bool {
$result = $this->settings->toggle_source( $id, $enabled );
/**
* 启用/禁用源
*
* @param string $id 源 ID
* @param bool $enabled 是否启用
* @return bool
*/
public function toggle( string $id, bool $enabled ): bool {
$result = $this->settings->toggle_source( $id, $enabled );

if ( $result && isset( $this->source_models[ $id ] ) ) {
$this->source_models[ $id ]->enabled = $enabled;
Logger::info( $enabled ? '启用源' : '禁用源', array( 'id' => $id ) );
}
if ( $result && isset( $this->source_models[ $id ] ) ) {
$this->source_models[ $id ]->enabled = $enabled;
Logger::info( $enabled ? '启用源' : '禁用源', [ 'id' => $id ] );
}

return $result;
}
return $result;
}

/**
* 获取源统计
*
* @return array
*/
public function get_stats(): array {
$all = $this->get_all();
$enabled = $this->get_enabled();
/**
* 获取源统计
*
* @return array
*/
public function get_stats(): array {
$all = $this->get_all();
$enabled = $this->get_enabled();

$by_type = array();
foreach ( $all as $source ) {
$type = $source->type;
if ( ! isset( $by_type[ $type ] ) ) {
$by_type[ $type ] = 0;
}
++$by_type[ $type ];
}
$by_type = [];
foreach ( $all as $source ) {
$type = $source->type;
if ( ! isset( $by_type[ $type ] ) ) {
$by_type[ $type ] = 0;
}
$by_type[ $type ]++;
}

return array(
'total' => count( $all ),
'enabled' => count( $enabled ),
'by_type' => $by_type,
);
}
return [
'total' => count( $all ),
'enabled' => count( $enabled ),
'by_type' => $by_type,
];
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->source_models = array();
$this->settings->clear_cache();
}
/**
* 清除缓存
*/
public function clear_cache(): void {
$this->source_models = [];
$this->settings->clear_cache();
}
}

View file

@ -12,7 +12,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -20,252 +20,252 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class SourceModel {

/**
* 唯一标识
*
* @var string
*/
public string $id = '';
/**
* 唯一标识
*
* @var string
*/
public string $id = '';

/**
* 源名称
*
* @var string
*/
public string $name = '';
/**
* 源名称
*
* @var string
*/
public string $name = '';

/**
* 源类型(见 SourceType 枚举)
*
* @var string
*/
public string $type = '';
/**
* 源类型(见 SourceType 枚举)
*
* @var string
*/
public string $type = '';

/**
* API URL
*
* @var string
*/
public string $api_url = '';
/**
* API URL
*
* @var string
*/
public string $api_url = '';

/**
* 插件/主题 slug
*
* @var string
*/
public string $slug = '';
/**
* 插件/主题 slug
*
* @var string
*/
public string $slug = '';

/**
* 项目类型plugin 或 theme
*
* @var string
*/
public string $item_type = 'plugin';
/**
* 项目类型plugin 或 theme
*
* @var string
*/
public string $item_type = 'plugin';

/**
* 认证令牌
*
* @var string
*/
public string $auth_token = '';
/**
* 认证令牌
*
* @var string
*/
public string $auth_token = '';

/**
* Git 分支(可选)
*
* @var string
*/
public string $branch = '';
/**
* Git 分支(可选)
*
* @var string
*/
public string $branch = '';

/**
* 是否启用
*
* @var bool
*/
public bool $enabled = true;
/**
* 是否启用
*
* @var bool
*/
public bool $enabled = true;

/**
* 优先级(数字越小优先级越高)
*
* @var int
*/
public int $priority = 50;
/**
* 优先级(数字越小优先级越高)
*
* @var int
*/
public int $priority = 50;

/**
* 是否是预置源
*
* @var bool
*/
public bool $is_preset = false;
/**
* 是否是预置源
*
* @var bool
*/
public bool $is_preset = false;

/**
* 是否是内联源(项目专属,通过快速设置创建)
*
* @var bool
*/
public bool $is_inline = false;
/**
* 是否是内联源(项目专属,通过快速设置创建)
*
* @var bool
*/
public bool $is_inline = false;

/**
* 额外元数据
*
* @var array
*/
public array $metadata = array();
/**
* 额外元数据
*
* @var array
*/
public array $metadata = [];

/**
* 从数组创建实例
*
* @param array $data 数据数组
* @return self
*/
public static function from_array( array $data ): self {
$model = new self();
/**
* 从数组创建实例
*
* @param array $data 数据数组
* @return self
*/
public static function from_array( array $data ): self {
$model = new self();

$model->id = $data['id'] ?? '';
$model->name = $data['name'] ?? '';
$model->type = $data['type'] ?? SourceType::JSON;
$model->api_url = $data['api_url'] ?? '';
$model->slug = $data['slug'] ?? '';
$model->item_type = $data['item_type'] ?? 'plugin';
$model->auth_token = $data['auth_token'] ?? '';
$model->branch = $data['branch'] ?? '';
$model->enabled = (bool) ( $data['enabled'] ?? true );
$model->priority = (int) ( $data['priority'] ?? 50 );
$model->is_preset = (bool) ( $data['is_preset'] ?? false );
$model->is_inline = (bool) ( $data['is_inline'] ?? false );
$model->metadata = $data['metadata'] ?? array();
$model->id = $data['id'] ?? '';
$model->name = $data['name'] ?? '';
$model->type = $data['type'] ?? SourceType::JSON;
$model->api_url = $data['api_url'] ?? '';
$model->slug = $data['slug'] ?? '';
$model->item_type = $data['item_type'] ?? 'plugin';
$model->auth_token = $data['auth_token'] ?? '';
$model->branch = $data['branch'] ?? '';
$model->enabled = (bool) ( $data['enabled'] ?? true );
$model->priority = (int) ( $data['priority'] ?? 50 );
$model->is_preset = (bool) ( $data['is_preset'] ?? false );
$model->is_inline = (bool) ( $data['is_inline'] ?? false );
$model->metadata = $data['metadata'] ?? [];

return $model;
}
return $model;
}

/**
* 转换为数组
*
* @return array
*/
public function to_array(): array {
return array(
'id' => $this->id,
'name' => $this->name,
'type' => $this->type,
'api_url' => $this->api_url,
'slug' => $this->slug,
'item_type' => $this->item_type,
'auth_token' => $this->auth_token,
'branch' => $this->branch,
'enabled' => $this->enabled,
'priority' => $this->priority,
'is_preset' => $this->is_preset,
'is_inline' => $this->is_inline,
'metadata' => $this->metadata,
);
}
/**
* 转换为数组
*
* @return array
*/
public function to_array(): array {
return [
'id' => $this->id,
'name' => $this->name,
'type' => $this->type,
'api_url' => $this->api_url,
'slug' => $this->slug,
'item_type' => $this->item_type,
'auth_token' => $this->auth_token,
'branch' => $this->branch,
'enabled' => $this->enabled,
'priority' => $this->priority,
'is_preset' => $this->is_preset,
'is_inline' => $this->is_inline,
'metadata' => $this->metadata,
];
}

/**
* 验证模型
*
* @return array 错误数组,空数组表示验证通过
*/
public function validate(): array {
$errors = array();
/**
* 验证模型
*
* @return array 错误数组,空数组表示验证通过
*/
public function validate(): array {
$errors = [];

// 验证类型
if ( ! SourceType::is_valid( $this->type ) ) {
$errors['type'] = __( '无效的源类型', 'wpbridge' );
}
// 验证类型
if ( ! SourceType::is_valid( $this->type ) ) {
$errors['type'] = __( '无效的源类型', 'wpbridge' );
}

// 验证 API URL
if ( empty( $this->api_url ) ) {
$errors['api_url'] = __( 'API URL 不能为空', 'wpbridge' );
} elseif ( ! filter_var( $this->api_url, FILTER_VALIDATE_URL ) ) {
$errors['api_url'] = __( '无效的 URL 格式', 'wpbridge' );
} else {
// 检查协议是否为 http/https
$scheme = parse_url( $this->api_url, PHP_URL_SCHEME );
if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
$errors['api_url'] = __( 'URL 必须使用 http 或 https 协议', 'wpbridge' );
}
}
// 验证 API URL
if ( empty( $this->api_url ) ) {
$errors['api_url'] = __( 'API URL 不能为空', 'wpbridge' );
} elseif ( ! filter_var( $this->api_url, FILTER_VALIDATE_URL ) ) {
$errors['api_url'] = __( '无效的 URL 格式', 'wpbridge' );
} else {
// 检查协议是否为 http/https
$scheme = parse_url( $this->api_url, PHP_URL_SCHEME );
if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
$errors['api_url'] = __( 'URL 必须使用 http 或 https 协议', 'wpbridge' );
}
}

// 验证项目类型
if ( ! in_array( $this->item_type, array( 'plugin', 'theme' ), true ) ) {
$errors['item_type'] = __( '项目类型必须是 plugin 或 theme', 'wpbridge' );
}
// 验证项目类型
if ( ! in_array( $this->item_type, [ 'plugin', 'theme' ], true ) ) {
$errors['item_type'] = __( '项目类型必须是 plugin 或 theme', 'wpbridge' );
}

// 验证优先级
if ( $this->priority < 0 || $this->priority > 100 ) {
$errors['priority'] = __( '优先级必须在 0-100 之间', 'wpbridge' );
}
// 验证优先级
if ( $this->priority < 0 || $this->priority > 100 ) {
$errors['priority'] = __( '优先级必须在 0-100 之间', 'wpbridge' );
}

return $errors;
}
return $errors;
}

/**
* 是否有效
*
* @return bool
*/
public function is_valid(): bool {
return empty( $this->validate() );
}
/**
* 是否有效
*
* @return bool
*/
public function is_valid(): bool {
return empty( $this->validate() );
}

/**
* 获取处理器实例
*
* @return Handlers\HandlerInterface|null
*/
public function get_handler(): ?Handlers\HandlerInterface {
$handler_class = SourceType::get_handler_class( $this->type );
/**
* 获取处理器实例
*
* @return Handlers\HandlerInterface|null
*/
public function get_handler(): ?Handlers\HandlerInterface {
$handler_class = SourceType::get_handler_class( $this->type );

if ( null === $handler_class || ! class_exists( $handler_class ) ) {
return null;
}
if ( null === $handler_class || ! class_exists( $handler_class ) ) {
return null;
}

return new $handler_class( $this );
}
return new $handler_class( $this );
}

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$handler = $this->get_handler();
if ( null === $handler ) {
return $this->api_url;
}
return $handler->get_check_url();
}
/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
$handler = $this->get_handler();
if ( null === $handler ) {
return $this->api_url;
}
return $handler->get_check_url();
}

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = array();
/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = [];

if ( ! empty( $this->auth_token ) ) {
// 解密 auth_token
$decrypted_token = Encryption::decrypt( $this->auth_token );
if ( ! empty( $this->auth_token ) ) {
// 解密 auth_token
$decrypted_token = Encryption::decrypt( $this->auth_token );

// 如果解密失败且数据看起来是加密的,记录错误并返回空
if ( empty( $decrypted_token ) ) {
if ( Encryption::is_encrypted( $this->auth_token ) ) {
Logger::error( 'Token 解密失败', array( 'source' => $this->id ) );
return array();
}
// 可能是未加密的旧数据,直接使用
$decrypted_token = $this->auth_token;
}
// 如果解密失败且数据看起来是加密的,记录错误并返回空
if ( empty( $decrypted_token ) ) {
if ( Encryption::is_encrypted( $this->auth_token ) ) {
Logger::error( 'Token 解密失败', [ 'source' => $this->id ] );
return [];
}
// 可能是未加密的旧数据,直接使用
$decrypted_token = $this->auth_token;
}

// 根据类型设置不同的认证头
if ( SourceType::is_git_type( $this->type ) ) {
$headers['Authorization'] = 'token ' . $decrypted_token;
} else {
$headers['X-API-Key'] = $decrypted_token;
}
}
// 根据类型设置不同的认证头
if ( SourceType::is_git_type( $this->type ) ) {
$headers['Authorization'] = 'token ' . $decrypted_token;
} else {
$headers['X-API-Key'] = $decrypted_token;
}
}

return $headers;
}
return $headers;
}
}

View file

@ -17,7 +17,7 @@ use WPBridge\Core\Logger;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -25,213 +25,213 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class SourceResolver {

/**
* 源注册表
*
* @var SourceRegistry
*/
private SourceRegistry $source_registry;
/**
* 源注册表
*
* @var SourceRegistry
*/
private SourceRegistry $source_registry;

/**
* 项目配置管理器
*
* @var ItemSourceManager
*/
private ItemSourceManager $item_manager;
/**
* 项目配置管理器
*
* @var ItemSourceManager
*/
private ItemSourceManager $item_manager;

/**
* 默认规则管理器
*
* @var DefaultsManager
*/
private DefaultsManager $defaults_manager;
/**
* 默认规则管理器
*
* @var DefaultsManager
*/
private DefaultsManager $defaults_manager;

/**
* 构造函数
*/
public function __construct() {
$this->source_registry = new SourceRegistry();
$this->item_manager = new ItemSourceManager( $this->source_registry );
$this->defaults_manager = new DefaultsManager();
}
/**
* 构造函数
*/
public function __construct() {
$this->source_registry = new SourceRegistry();
$this->item_manager = new ItemSourceManager( $this->source_registry );
$this->defaults_manager = new DefaultsManager();
}

/**
* 解析指定项目的更新源
*
* @param string $item_key 项目键
* @param string $slug 插件/主题 slug
* @param string $item_type 项目类型
* @return array{mode:string,sources:SourceModel[],has_wporg:bool}
*/
public function resolve( string $item_key, string $slug, string $item_type ): array {
$config = $this->item_manager->get( $item_key );
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
/**
* 解析指定项目的更新源
*
* @param string $item_key 项目键
* @param string $slug 插件/主题 slug
* @param string $item_type 项目类型
* @return array{mode:string,sources:SourceModel[],has_wporg:bool}
*/
public function resolve( string $item_key, string $slug, string $item_type ): array {
$config = $this->item_manager->get( $item_key );
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;

if ( $mode === ItemSourceManager::MODE_DISABLED ) {
return array(
'mode' => $mode,
'sources' => array(),
'has_wporg' => false,
);
}
if ( $mode === ItemSourceManager::MODE_DISABLED ) {
return [
'mode' => $mode,
'sources' => [],
'has_wporg' => false,
];
}

$sources = $this->item_manager->get_effective_sources( $item_key, $this->defaults_manager );
$sources = $this->item_manager->get_effective_sources( $item_key, $this->defaults_manager );

if ( empty( $sources ) ) {
return array(
'mode' => $mode,
'sources' => array(),
'has_wporg' => false,
);
}
if ( empty( $sources ) ) {
return [
'mode' => $mode,
'sources' => [],
'has_wporg' => false,
];
}

$has_wporg = false;
$models = array();
foreach ( $sources as $source ) {
if ( ( $source['type'] ?? '' ) === SourceRegistry::TYPE_WPORG ) {
$has_wporg = true;
}
$has_wporg = false;
$models = [];
foreach ( $sources as $source ) {
if ( ( $source['type'] ?? '' ) === SourceRegistry::TYPE_WPORG ) {
$has_wporg = true;
}

$model = $this->convert_source( $source, $item_type, $slug, $mode === ItemSourceManager::MODE_CUSTOM );
if ( null !== $model ) {
$models[] = $model;
}
}
$model = $this->convert_source( $source, $item_type, $slug, $mode === ItemSourceManager::MODE_CUSTOM );
if ( null !== $model ) {
$models[] = $model;
}
}

return array(
'mode' => $mode,
'sources' => $models,
'has_wporg' => $has_wporg,
);
}
return [
'mode' => $mode,
'sources' => $models,
'has_wporg' => $has_wporg,
];
}

/**
* 将 SourceRegistry 记录转换为 SourceModel
*
* @param array $source 源配置
* @param string $item_type 项目类型
* @param string $slug 项目 slug
* @param bool $force_slug 是否强制绑定到 slug
* @return SourceModel|null
*/
private function convert_source( array $source, string $item_type, string $slug, bool $force_slug ): ?SourceModel {
$type = $this->map_type( $source );
if ( null === $type ) {
return null;
}
/**
* 将 SourceRegistry 记录转换为 SourceModel
*
* @param array $source 源配置
* @param string $item_type 项目类型
* @param string $slug 项目 slug
* @param bool $force_slug 是否强制绑定到 slug
* @return SourceModel|null
*/
private function convert_source( array $source, string $item_type, string $slug, bool $force_slug ): ?SourceModel {
$type = $this->map_type( $source );
if ( null === $type ) {
return null;
}

$api_url = $source['api_url'] ?? '';
if ( empty( $api_url ) ) {
Logger::warning( '源缺少 API URL', array( 'source' => $source['source_key'] ?? '' ) );
return null;
}
$api_url = $source['api_url'] ?? '';
if ( empty( $api_url ) ) {
Logger::warning( '源缺少 API URL', [ 'source' => $source['source_key'] ?? '' ] );
return null;
}

$id = $source['source_key'] ?? '';
if ( empty( $id ) ) {
return null;
}
$id = $source['source_key'] ?? '';
if ( empty( $id ) ) {
return null;
}

$model = new SourceModel();
$model->id = $id;
$model->name = $source['name'] ?? $id;
$model->type = $type;
$model->api_url = $api_url;
$model->item_type = $item_type;
$model->slug = $force_slug ? $slug : '';
$model->enabled = ! empty( $source['enabled'] );
$model->priority = (int) ( $source['priority'] ?? $source['default_priority'] ?? 50 );
$model->is_preset = ! empty( $source['is_preset'] );
$model->metadata = array(
'auth_scheme' => $source['auth_type'] ?? '',
'signature_required' => ! empty( $source['signature_required'] ),
);
$model = new SourceModel();
$model->id = $id;
$model->name = $source['name'] ?? $id;
$model->type = $type;
$model->api_url = $api_url;
$model->item_type = $item_type;
$model->slug = $force_slug ? $slug : '';
$model->enabled = ! empty( $source['enabled'] );
$model->priority = (int) ( $source['priority'] ?? $source['default_priority'] ?? 50 );
$model->is_preset = ! empty( $source['is_preset'] );
$model->metadata = [
'auth_scheme' => $source['auth_type'] ?? '',
'signature_required' => ! empty( $source['signature_required'] ),
];

$secret_ref = $source['auth_secret_ref'] ?? '';
if ( ! empty( $secret_ref ) ) {
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );
if ( ! empty( $secret ) ) {
$model->auth_token = Encryption::encrypt( $secret );
}
}
$secret_ref = $source['auth_secret_ref'] ?? '';
if ( ! empty( $secret_ref ) ) {
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );
if ( ! empty( $secret ) ) {
$model->auth_token = Encryption::encrypt( $secret );
}
}

return $model;
}
return $model;
}

/**
* 映射源类型
*
* @param array $source 源配置
* @return string|null
*/
private function map_type( array $source ): ?string {
$type = $source['type'] ?? '';
/**
* 映射源类型
*
* @param array $source 源配置
* @return string|null
*/
private function map_type( array $source ): ?string {
$type = $source['type'] ?? '';

switch ( $type ) {
case SourceRegistry::TYPE_WPORG:
return null;
switch ( $type ) {
case SourceRegistry::TYPE_WPORG:
return null;

case SourceRegistry::TYPE_MIRROR:
return SourceType::ARKPRESS;
case SourceRegistry::TYPE_MIRROR:
return SourceType::ARKPRESS;

case SourceRegistry::TYPE_FAIR:
return SourceType::FAIR;
case SourceRegistry::TYPE_FAIR:
return SourceType::FAIR;

case SourceRegistry::TYPE_JSON:
return SourceType::JSON;
case SourceRegistry::TYPE_JSON:
return SourceType::JSON;

case SourceRegistry::TYPE_ARKPRESS:
return SourceType::ARKPRESS;
case SourceRegistry::TYPE_ARKPRESS:
return SourceType::ARKPRESS;

case SourceRegistry::TYPE_GIT:
return $this->resolve_git_type( $source['api_url'] ?? '' );
case SourceRegistry::TYPE_GIT:
return $this->resolve_git_type( $source['api_url'] ?? '' );

case SourceRegistry::TYPE_CUSTOM:
return $this->guess_custom_type( $source['api_url'] ?? '' );
case SourceRegistry::TYPE_CUSTOM:
return $this->guess_custom_type( $source['api_url'] ?? '' );

default:
return null;
}
}
default:
return null;
}
}

/**
* 解析 Git 类型
*
* @param string $url 源 URL
* @return string
*/
private function resolve_git_type( string $url ): string {
$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
/**
* 解析 Git 类型
*
* @param string $url 源 URL
* @return string
*/
private function resolve_git_type( string $url ): string {
$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );

if ( strpos( $host, 'github.com' ) !== false ) {
return SourceType::GITHUB;
}
if ( strpos( $host, 'github.com' ) !== false ) {
return SourceType::GITHUB;
}

if ( strpos( $host, 'gitlab' ) !== false ) {
return SourceType::GITLAB;
}
if ( strpos( $host, 'gitlab' ) !== false ) {
return SourceType::GITLAB;
}

if ( strpos( $host, 'gitee.com' ) !== false ) {
return SourceType::GITEE;
}
if ( strpos( $host, 'gitee.com' ) !== false ) {
return SourceType::GITEE;
}

if ( strpos( $host, 'wenpai' ) !== false || strpos( $host, 'feicode' ) !== false ) {
return SourceType::WENPAI_GIT;
}
if ( strpos( $host, 'wenpai' ) !== false || strpos( $host, 'feicode' ) !== false ) {
return SourceType::WENPAI_GIT;
}

return SourceType::WENPAI_GIT;
}
return SourceType::WENPAI_GIT;
}

/**
* 推断自定义类型
*
* @param string $url 源 URL
* @return string
*/
private function guess_custom_type( string $url ): string {
if ( preg_match( '/\.zip$/i', $url ) ) {
return SourceType::ZIP;
}
/**
* 推断自定义类型
*
* @param string $url 源 URL
* @return string
*/
private function guess_custom_type( string $url ): string {
if ( preg_match( '/\.zip$/i', $url ) ) {
return SourceType::ZIP;
}

return SourceType::JSON;
}
return SourceType::JSON;
}
}

View file

@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -18,188 +18,188 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class SourceType {

// === 基础类型(用户自定义源)===
// === 基础类型(用户自定义源)===

/**
* 标准 JSON APIPlugin Update Checker 格式)
*/
const JSON = 'json';
/**
* 标准 JSON APIPlugin Update Checker 格式)
*/
const JSON = 'json';

/**
* GitHub Releases
*/
const GITHUB = 'github';
/**
* GitHub Releases
*/
const GITHUB = 'github';

/**
* GitLab Releases
*/
const GITLAB = 'gitlab';
/**
* GitLab Releases
*/
const GITLAB = 'gitlab';

/**
* Gitee Releases国内
*/
const GITEE = 'gitee';
/**
* Gitee Releases国内
*/
const GITEE = 'gitee';

/**
* 菲码源库
*/
const WENPAI_GIT = 'wenpai_git';
/**
* 菲码源库
*/
const WENPAI_GIT = 'wenpai_git';

/**
* 直接 ZIP URL
*/
const ZIP = 'zip';
/**
* 直接 ZIP URL
*/
const ZIP = 'zip';

// === 自托管服务器类型(预置源使用)===
// === 自托管服务器类型(预置源使用)===

/**
* ArkPress文派自托管AspireCloud 分叉)
*/
const ARKPRESS = 'arkpress';
/**
* ArkPress文派自托管AspireCloud 分叉)
*/
const ARKPRESS = 'arkpress';

/**
* AspireCloud
*/
const ASPIRECLOUD = 'aspirecloud';
/**
* AspireCloud
*/
const ASPIRECLOUD = 'aspirecloud';

/**
* FAIR Package Manager
*/
const FAIR = 'fair';
/**
* FAIR Package Manager
*/
const FAIR = 'fair';

/**
* Plugin Update Checker 服务器
*/
const PUC = 'puc';
/**
* Plugin Update Checker 服务器
*/
const PUC = 'puc';

/**
* WPBridge Server商业插件桥接服务
*/
const BRIDGE_SERVER = 'bridge_server';
/**
* WPBridge Server商业插件桥接服务
*/
const BRIDGE_SERVER = 'bridge_server';

// === 类型分组 ===
// === 类型分组 ===

/**
* Git 平台类型
*/
const GIT_TYPES = array(
self::GITHUB,
self::GITLAB,
self::GITEE,
self::WENPAI_GIT,
);
/**
* Git 平台类型
*/
const GIT_TYPES = [
self::GITHUB,
self::GITLAB,
self::GITEE,
self::WENPAI_GIT,
];

/**
* 自托管服务器类型
*/
const SERVER_TYPES = array(
self::ARKPRESS,
self::ASPIRECLOUD,
self::FAIR,
self::PUC,
self::BRIDGE_SERVER,
);
/**
* 自托管服务器类型
*/
const SERVER_TYPES = [
self::ARKPRESS,
self::ASPIRECLOUD,
self::FAIR,
self::PUC,
self::BRIDGE_SERVER,
];

/**
* 所有类型
*/
const ALL_TYPES = array(
self::JSON,
self::GITHUB,
self::GITLAB,
self::GITEE,
self::WENPAI_GIT,
self::ZIP,
self::ARKPRESS,
self::ASPIRECLOUD,
self::FAIR,
self::PUC,
self::BRIDGE_SERVER,
);
/**
* 所有类型
*/
const ALL_TYPES = [
self::JSON,
self::GITHUB,
self::GITLAB,
self::GITEE,
self::WENPAI_GIT,
self::ZIP,
self::ARKPRESS,
self::ASPIRECLOUD,
self::FAIR,
self::PUC,
self::BRIDGE_SERVER,
];

/**
* 类型标签映射
*
* @return array
*/
public static function get_labels(): array {
return array(
self::JSON => __( 'JSON API', 'wpbridge' ),
self::GITHUB => __( 'GitHub', 'wpbridge' ),
self::GITLAB => __( 'GitLab', 'wpbridge' ),
self::GITEE => __( 'Gitee', 'wpbridge' ),
self::WENPAI_GIT => __( '菲码源库', 'wpbridge' ),
self::ZIP => __( 'ZIP URL', 'wpbridge' ),
self::ARKPRESS => __( 'ArkPress', 'wpbridge' ),
self::ASPIRECLOUD => __( 'AspireCloud', 'wpbridge' ),
self::FAIR => __( 'FAIR', 'wpbridge' ),
self::PUC => __( 'PUC Server', 'wpbridge' ),
self::BRIDGE_SERVER => __( 'Bridge Server', 'wpbridge' ),
);
}
/**
* 类型标签映射
*
* @return array
*/
public static function get_labels(): array {
return [
self::JSON => __( 'JSON API', 'wpbridge' ),
self::GITHUB => __( 'GitHub', 'wpbridge' ),
self::GITLAB => __( 'GitLab', 'wpbridge' ),
self::GITEE => __( 'Gitee', 'wpbridge' ),
self::WENPAI_GIT => __( '菲码源库', 'wpbridge' ),
self::ZIP => __( 'ZIP URL', 'wpbridge' ),
self::ARKPRESS => __( 'ArkPress', 'wpbridge' ),
self::ASPIRECLOUD => __( 'AspireCloud', 'wpbridge' ),
self::FAIR => __( 'FAIR', 'wpbridge' ),
self::PUC => __( 'PUC Server', 'wpbridge' ),
self::BRIDGE_SERVER => __( 'Bridge Server', 'wpbridge' ),
];
}

/**
* 获取类型标签
*
* @param string $type 类型
* @return string
*/
public static function get_label( string $type ): string {
$labels = self::get_labels();
return $labels[ $type ] ?? $type;
}
/**
* 获取类型标签
*
* @param string $type 类型
* @return string
*/
public static function get_label( string $type ): string {
$labels = self::get_labels();
return $labels[ $type ] ?? $type;
}

/**
* 检查类型是否有效
*
* @param string $type 类型
* @return bool
*/
public static function is_valid( string $type ): bool {
return in_array( $type, self::ALL_TYPES, true );
}
/**
* 检查类型是否有效
*
* @param string $type 类型
* @return bool
*/
public static function is_valid( string $type ): bool {
return in_array( $type, self::ALL_TYPES, true );
}

/**
* 检查是否是 Git 类型
*
* @param string $type 类型
* @return bool
*/
public static function is_git_type( string $type ): bool {
return in_array( $type, self::GIT_TYPES, true );
}
/**
* 检查是否是 Git 类型
*
* @param string $type 类型
* @return bool
*/
public static function is_git_type( string $type ): bool {
return in_array( $type, self::GIT_TYPES, true );
}

/**
* 检查是否是服务器类型
*
* @param string $type 类型
* @return bool
*/
public static function is_server_type( string $type ): bool {
return in_array( $type, self::SERVER_TYPES, true );
}
/**
* 检查是否是服务器类型
*
* @param string $type 类型
* @return bool
*/
public static function is_server_type( string $type ): bool {
return in_array( $type, self::SERVER_TYPES, true );
}

/**
* 获取处理器类名
*
* @param string $type 类型
* @return string|null
*/
public static function get_handler_class( string $type ): ?string {
$handlers = array(
self::JSON => 'WPBridge\\UpdateSource\\Handlers\\JsonHandler',
self::GITHUB => 'WPBridge\\UpdateSource\\Handlers\\GitHubHandler',
self::GITLAB => 'WPBridge\\UpdateSource\\Handlers\\GitLabHandler',
self::GITEE => 'WPBridge\\UpdateSource\\Handlers\\GiteeHandler',
self::WENPAI_GIT => 'WPBridge\\UpdateSource\\Handlers\\WenPaiGitHandler',
self::ZIP => 'WPBridge\\UpdateSource\\Handlers\\ZipHandler',
self::ARKPRESS => 'WPBridge\\UpdateSource\\Handlers\\ArkPressHandler',
self::ASPIRECLOUD => 'WPBridge\\UpdateSource\\Handlers\\AspireCloudHandler',
self::FAIR => 'WPBridge\\UpdateSource\\Handlers\\FairHandler',
self::PUC => 'WPBridge\\UpdateSource\\Handlers\\PUCHandler',
self::BRIDGE_SERVER => 'WPBridge\\UpdateSource\\Handlers\\BridgeServerHandler',
);
/**
* 获取处理器类名
*
* @param string $type 类型
* @return string|null
*/
public static function get_handler_class( string $type ): ?string {
$handlers = [
self::JSON => 'WPBridge\\UpdateSource\\Handlers\\JsonHandler',
self::GITHUB => 'WPBridge\\UpdateSource\\Handlers\\GitHubHandler',
self::GITLAB => 'WPBridge\\UpdateSource\\Handlers\\GitLabHandler',
self::GITEE => 'WPBridge\\UpdateSource\\Handlers\\GiteeHandler',
self::WENPAI_GIT => 'WPBridge\\UpdateSource\\Handlers\\WenPaiGitHandler',
self::ZIP => 'WPBridge\\UpdateSource\\Handlers\\ZipHandler',
self::ARKPRESS => 'WPBridge\\UpdateSource\\Handlers\\ArkPressHandler',
self::ASPIRECLOUD => 'WPBridge\\UpdateSource\\Handlers\\AspireCloudHandler',
self::FAIR => 'WPBridge\\UpdateSource\\Handlers\\FairHandler',
self::PUC => 'WPBridge\\UpdateSource\\Handlers\\PUCHandler',
self::BRIDGE_SERVER => 'WPBridge\\UpdateSource\\Handlers\\BridgeServerHandler',
];

return $handlers[ $type ] ?? null;
}
return $handlers[ $type ] ?? null;
}
}

View file

@ -15,7 +15,7 @@ use WPBridge\Cache\FallbackStrategy;

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

/**
@ -23,310 +23,295 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class ThemeUpdater {

/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;

/**
* 源解析器(方案 B
*
* @var SourceResolver
*/
private SourceResolver $source_resolver;
/**
* 源解析器(方案 B
*
* @var SourceResolver
*/
private SourceResolver $source_resolver;

/**
* 降级策略
*
* @var FallbackStrategy
*/
private FallbackStrategy $fallback_strategy;
/**
* 降级策略
*
* @var FallbackStrategy
*/
private FallbackStrategy $fallback_strategy;

/**
* 缓存键前缀
*
* @var string
*/
const CACHE_PREFIX = 'wpbridge_theme_update_';
/**
* 缓存键前缀
*
* @var string
*/
const CACHE_PREFIX = 'wpbridge_theme_update_';

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->source_resolver = new SourceResolver();
$this->fallback_strategy = new FallbackStrategy( $settings );
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->source_resolver = new SourceResolver();
$this->fallback_strategy = new FallbackStrategy( $settings );

$this->init_hooks();
}
$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 主题更新检查
add_filter( 'pre_set_site_transient_update_themes', array( $this, 'check_updates' ), 10, 1 );
/**
* 初始化钩子
*/
private function init_hooks(): void {
// 主题更新检查
add_filter( 'pre_set_site_transient_update_themes', [ $this, 'check_updates' ], 10, 1 );

// 主题信息
add_filter( 'themes_api', array( $this, 'theme_info' ), 10, 3 );
}
// 主题信息
add_filter( 'themes_api', [ $this, 'theme_info' ], 10, 3 );
}

/**
* 检查主题更新
*
* @param object $transient 更新 transient
* @return object
*/
public function check_updates( $transient ) {
if ( empty( $transient ) || ! is_object( $transient ) ) {
$transient = new \stdClass();
}
/**
* 检查主题更新
*
* @param object $transient 更新 transient
* @return object
*/
public function check_updates( $transient ) {
if ( empty( $transient ) || ! is_object( $transient ) ) {
$transient = new \stdClass();
}

if ( ! isset( $transient->response ) ) {
$transient->response = array();
}
if ( ! isset( $transient->response ) ) {
$transient->response = [];
}

if ( ! isset( $transient->no_update ) ) {
$transient->no_update = array();
}
if ( ! isset( $transient->no_update ) ) {
$transient->no_update = [];
}

// 获取已安装的主题
$themes = wp_get_themes();
// 获取已安装的主题
$themes = wp_get_themes();

foreach ( $themes as $slug => $theme ) {
$item_key = 'theme:' . $slug;
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'theme' );
$mode = $resolved['mode'];
$matching_sources = $resolved['sources'];
$allow_wporg_fallback = ! empty( $resolved['has_wporg'] );
foreach ( $themes as $slug => $theme ) {
$item_key = 'theme:' . $slug;
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'theme' );
$mode = $resolved['mode'];
$matching_sources = $resolved['sources'];
$allow_wporg_fallback = ! empty( $resolved['has_wporg'] );

if ( $mode === ItemSourceManager::MODE_DISABLED ) {
unset( $transient->response[ $slug ] );
$transient->no_update[ $slug ] = array(
'theme' => $slug,
'new_version' => $theme->get( 'Version' ),
);
continue;
}
if ( $mode === ItemSourceManager::MODE_DISABLED ) {
unset( $transient->response[ $slug ] );
$transient->no_update[ $slug ] = [
'theme' => $slug,
'new_version' => $theme->get( 'Version' ),
];
continue;
}

if ( empty( $matching_sources ) ) {
if ( $mode === ItemSourceManager::MODE_CUSTOM ) {
unset( $transient->response[ $slug ] );
$transient->no_update[ $slug ] = array(
'theme' => $slug,
'new_version' => $theme->get( 'Version' ),
);
}
continue;
}
if ( empty( $matching_sources ) ) {
if ( $mode === ItemSourceManager::MODE_CUSTOM ) {
unset( $transient->response[ $slug ] );
$transient->no_update[ $slug ] = [
'theme' => $slug,
'new_version' => $theme->get( 'Version' ),
];
}
continue;
}

$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;
$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;

if ( $take_over ) {
// 接管更新检查,清除默认响应
unset( $transient->response[ $slug ] );
}
if ( $take_over ) {
// 接管更新检查,清除默认响应
unset( $transient->response[ $slug ] );
}

$version = $theme->get( 'Version' );
$version = $theme->get( 'Version' );

// 尝试从缓存获取(使用 md5 哈希防止缓存污染)
$cache_key = self::CACHE_PREFIX . md5( $slug . get_site_url() );
$cached = get_transient( $cache_key );
// 尝试从缓存获取(使用 md5 哈希防止缓存污染)
$cache_key = self::CACHE_PREFIX . md5( $slug . get_site_url() );
$cached = get_transient( $cache_key );

if ( false !== $cached ) {
if ( ! empty( $cached['update'] ) ) {
$transient->response[ $slug ] = $cached['update'];
unset( $transient->no_update[ $slug ] );
} elseif ( $take_over ) {
$transient->no_update[ $slug ] = array(
'theme' => $slug,
'new_version' => $version,
);
}
continue;
}
if ( false !== $cached ) {
if ( ! empty( $cached['update'] ) ) {
$transient->response[ $slug ] = $cached['update'];
unset( $transient->no_update[ $slug ] );
} else {
if ( $take_over ) {
$transient->no_update[ $slug ] = [
'theme' => $slug,
'new_version' => $version,
];
}
}
continue;
}

// 检查更新
$update_info = $this->check_theme_update( $slug, $version, $matching_sources );
// 检查更新
$update_info = $this->check_theme_update( $slug, $version, $matching_sources );

if ( null !== $update_info ) {
$update_data = array(
'theme' => $slug,
'new_version' => $update_info->version,
'url' => $update_info->details_url,
'package' => $update_info->download_url,
'requires' => $update_info->requires,
'requires_php' => $update_info->requires_php,
);
if ( null !== $update_info ) {
$update_data = [
'theme' => $slug,
'new_version' => $update_info->version,
'url' => $update_info->details_url,
'package' => $update_info->download_url,
'requires' => $update_info->requires,
'requires_php' => $update_info->requires_php,
];

$transient->response[ $slug ] = $update_data;
unset( $transient->no_update[ $slug ] );
$transient->response[ $slug ] = $update_data;
unset( $transient->no_update[ $slug ] );

// 缓存结果
set_transient(
$cache_key,
array(
'update' => $update_data,
),
$this->settings->get_cache_ttl()
);
// 缓存结果
set_transient( $cache_key, [
'update' => $update_data,
], $this->settings->get_cache_ttl() );

Logger::info(
'主题更新可用',
array(
'theme' => $slug,
'current' => $version,
'new' => $update_info->version,
)
);
} else {
if ( $take_over ) {
$transient->no_update[ $slug ] = array(
'theme' => $slug,
'new_version' => $version,
);
}
Logger::info( '主题更新可用', [
'theme' => $slug,
'current' => $version,
'new' => $update_info->version,
] );
} else {
if ( $take_over ) {
$transient->no_update[ $slug ] = [
'theme' => $slug,
'new_version' => $version,
];
}

// 缓存无更新结果
set_transient(
$cache_key,
array(
'update' => null,
),
$this->settings->get_cache_ttl()
);
}
}
// 缓存无更新结果
set_transient( $cache_key, [
'update' => null,
], $this->settings->get_cache_ttl() );
}
}

return $transient;
}
return $transient;
}

/**
* 检查单个主题更新
*
* @param string $slug 主题 slug
* @param string $version 当前版本
* @param SourceModel[] $sources 更新源列表
* @return UpdateInfo|null
*/
private function check_theme_update( string $slug, string $version, array $sources ): ?UpdateInfo {
$cache_key = 'update_info_theme_' . md5( $slug . get_site_url() );
/**
* 检查单个主题更新
*
* @param string $slug 主题 slug
* @param string $version 当前版本
* @param SourceModel[] $sources 更新源列表
* @return UpdateInfo|null
*/
private function check_theme_update( string $slug, string $version, array $sources ): ?UpdateInfo {
$cache_key = 'update_info_theme_' . md5( $slug . get_site_url() );

$result = $this->fallback_strategy->execute_with_fallback(
$sources,
function ( SourceModel $source ) use ( $slug, $version ) {
$handler = $source->get_handler();
$result = $this->fallback_strategy->execute_with_fallback(
$sources,
function( SourceModel $source ) use ( $slug, $version ) {
$handler = $source->get_handler();

if ( null === $handler ) {
Logger::warning(
'无法获取处理器',
array(
'source' => $source->id,
'type' => $source->type,
)
);
return null;
}
if ( null === $handler ) {
Logger::warning( '无法获取处理器', [
'source' => $source->id,
'type' => $source->type,
] );
return null;
}

try {
return $handler->check_update( $slug, $version );
} catch ( \Exception $e ) {
Logger::error(
'检查主题更新时发生错误',
array(
'source' => $source->id,
'slug' => $slug,
'error' => $e->getMessage(),
)
);
throw $e;
}
},
$cache_key
);
try {
return $handler->check_update( $slug, $version );
} catch ( \Exception $e ) {
Logger::error( '检查主题更新时发生错误', [
'source' => $source->id,
'slug' => $slug,
'error' => $e->getMessage(),
] );
throw $e;
}
},
$cache_key
);

return $result instanceof UpdateInfo ? $result : null;
}
return $result instanceof UpdateInfo ? $result : null;
}

/**
* 获取主题信息
*
* @param false|object|array $result 结果
* @param string $action 动作
* @param object $args 参数
* @return false|object|array
*/
public function theme_info( $result, $action, $args ) {
if ( 'theme_information' !== $action ) {
return $result;
}
/**
* 获取主题信息
*
* @param false|object|array $result 结果
* @param string $action 动作
* @param object $args 参数
* @return false|object|array
*/
public function theme_info( $result, $action, $args ) {
if ( 'theme_information' !== $action ) {
return $result;
}

$slug = $args->slug ?? '';
$slug = $args->slug ?? '';

if ( empty( $slug ) ) {
return $result;
}
if ( empty( $slug ) ) {
return $result;
}

$item_key = 'theme:' . $slug;
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'theme' );
$mode = $resolved['mode'];
$sources = $resolved['sources'];
$item_key = 'theme:' . $slug;
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'theme' );
$mode = $resolved['mode'];
$sources = $resolved['sources'];

if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
return $result;
}
if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
return $result;
}

// 获取第一个匹配的源
$source = reset( $sources );
$handler = $source->get_handler();
// 获取第一个匹配的源
$source = reset( $sources );
$handler = $source->get_handler();

if ( null === $handler ) {
return $result;
}
if ( null === $handler ) {
return $result;
}

$info = $handler->get_info( $slug );
$info = $handler->get_info( $slug );

if ( null === $info ) {
return $result;
}
if ( null === $info ) {
return $result;
}

// 转换为 themes_api 响应格式
return (object) array(
'name' => $info['name'] ?? $slug,
'slug' => $slug,
'version' => $info['version'] ?? '',
'download_link' => $info['download_url'] ?? $info['package'] ?? '',
'requires' => $info['requires'] ?? '',
'requires_php' => $info['requires_php'] ?? '',
'last_updated' => $info['last_updated'] ?? '',
'sections' => $info['sections'] ?? array(),
'screenshot_url' => $info['screenshot_url'] ?? '',
);
}
// 转换为 themes_api 响应格式
return (object) [
'name' => $info['name'] ?? $slug,
'slug' => $slug,
'version' => $info['version'] ?? '',
'download_link' => $info['download_url'] ?? $info['package'] ?? '',
'requires' => $info['requires'] ?? '',
'requires_php' => $info['requires_php'] ?? '',
'last_updated' => $info['last_updated'] ?? '',
'sections' => $info['sections'] ?? [],
'screenshot_url' => $info['screenshot_url'] ?? '',
];
}

/**
* 清除主题更新缓存
*
* @param string|null $slug 主题 slug为空则清除所有
*/
public function clear_cache( ?string $slug = null ): void {
if ( null !== $slug ) {
delete_transient( self::CACHE_PREFIX . md5( $slug . get_site_url() ) );
} else {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'
)
);
}
/**
* 清除主题更新缓存
*
* @param string|null $slug 主题 slug为空则清除所有
*/
public function clear_cache( ?string $slug = null ): void {
if ( null !== $slug ) {
delete_transient( self::CACHE_PREFIX . md5( $slug . get_site_url() ) );
} else {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'
)
);
}

// 清除 WordPress 更新缓存
delete_site_transient( 'update_themes' );
}
// 清除 WordPress 更新缓存
delete_site_transient( 'update_themes' );
}
}

View file

@ -1,77 +0,0 @@
<?xml version="1.0"?>
<ruleset name="WPBridge">
<description>文派云桥代码规范</description>

<file>.</file>
<arg name="extensions" value="php"/>
<arg name="warning-severity" value="0"/>
<exclude-pattern>vendor/*</exclude-pattern>
<exclude-pattern>node_modules/*</exclude-pattern>
<exclude-pattern>tests/*</exclude-pattern>
<exclude-pattern>lib/*</exclude-pattern>

<rule ref="WordPress-Extra">
<!-- 文件命名:项目使用 PSR-4 自动加载PascalCase 文件名 -->
<exclude name="WordPress.Files.FileName.NotHyphenatedLowercase"/>
<exclude name="WordPress.Files.FileName.InvalidClassFileName"/>

<!-- Yoda 条件:风格偏好,非安全问题 -->
<exclude name="WordPress.PHP.YodaConditions.NotYoda"/>

<!-- 短三元运算符:合法 PHP 语法 -->
<exclude name="Universal.Operators.DisallowShortTernary.Found"/>

<!-- 未使用函数参数WordPress 钩子回调常有未使用参数 -->
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter"/>

<!-- Nonce 验证REST API 端点由框架处理认证 -->
<exclude name="WordPress.Security.NonceVerification.Missing"/>
<exclude name="WordPress.Security.NonceVerification.Recommended"/>

<!-- 翻译注释:非关键问题 -->
<exclude name="WordPress.WP.I18n.MissingTranslatorsComment"/>

<!-- 保留字参数名PHP 参数命名 -->
<exclude name="Universal.NamingConventions.NoReservedKeywordParameterNames"/>

<!-- PHP 替代函数:项目中有合理使用场景 -->
<exclude name="WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents"/>
<exclude name="WordPress.WP.AlternativeFunctions.parse_url_parse_url"/>
<exclude name="WordPress.WP.AlternativeFunctions.file_system_operations_file_put_content"/>
<exclude name="WordPress.WP.AlternativeFunctions.unlink_unlink"/>
<exclude name="WordPress.WP.AlternativeFunctions.file_system_operations_is_writable"/>

<!-- 全局变量覆盖:模板文件中常见 -->
<exclude name="WordPress.WP.GlobalVariablesOverride.Prohibited"/>

<!-- base64/urlencode/serialize加密和 API 通信需要 -->
<exclude name="WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode"/>
<exclude name="WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode"/>
<exclude name="WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode"/>
<exclude name="WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize"/>

<!-- 错误抑制:特定场景需要 -->
<exclude name="WordPress.PHP.NoSilencedErrors.Discouraged"/>

<!-- wp_redirect合理使用 -->
<exclude name="WordPress.Security.SafeRedirect.wp_redirect_wp_redirect"/>

<!-- error_logLogger 类需要 -->
<exclude name="WordPress.PHP.DevelopmentFunctions.error_log_error_log"/>

<!-- 多对象/注释代码/空 catch/循环中 count/自增 -->
<exclude name="Generic.Files.OneObjectStructurePerFile.MultipleFound"/>
<exclude name="Squiz.PHP.CommentedOutCode.Found"/>
<exclude name="Generic.CodeAnalysis.EmptyStatement.DetectedCatch"/>
<exclude name="Squiz.Operators.IncrementDecrementUsage.Found"/>
<exclude name="Squiz.PHP.DisallowSizeFunctionsInLoops.Found"/>
</rule>

<!-- 安全规则:降级为 warning不排除保留审查能力 -->
<rule ref="WordPress.Security.EscapeOutput.ExceptionNotEscaped">
<type>warning</type>
</rule>
<rule ref="WordPress.Security.EscapeOutput.OutputNotEscaped">
<type>warning</type>
</rule>
</ruleset>

View file

@ -10,7 +10,7 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

use WPBridge\UpdateSource\SourceType;
@ -29,116 +29,116 @@ $logs = Logger::get_logs();

// 健康检查
$health_checker = new HealthChecker( $settings_obj );
$health_status = array();
$health_status = [];
foreach ( $sources as $source ) {
if ( $source->enabled ) {
$cached_status = get_transient( 'wpbridge_health_' . $source->id );
// 确保缓存的状态是数组,防止 __PHP_Incomplete_Class 错误
if ( $cached_status && is_array( $cached_status ) ) {
$health_status[ $source->id ] = $cached_status;
}
}
if ( $source->enabled ) {
$cached_status = get_transient( 'wpbridge_health_' . $source->id );
// 确保缓存的状态是数组,防止 __PHP_Incomplete_Class 错误
if ( $cached_status && is_array( $cached_status ) ) {
$health_status[ $source->id ] = $cached_status;
}
}
}
?>
<!-- 标题栏 -->
<header class="wpbridge-header">
<div class="wpbridge-header-left">
<span class="dashicons dashicons-networking wpbridge-logo"></span>
<h1 class="wpbridge-title">
<?php esc_html_e( '云桥', 'wpbridge' ); ?>
<span class="wpbridge-version">v<?php echo esc_html( WPBRIDGE_VERSION ); ?></span>
</h1>
</div>
<div class="wpbridge-header-left">
<span class="dashicons dashicons-networking wpbridge-logo"></span>
<h1 class="wpbridge-title">
<?php esc_html_e( '云桥', 'wpbridge' ); ?>
<span class="wpbridge-version">v<?php echo esc_html( WPBRIDGE_VERSION ); ?></span>
</h1>
</div>

<div class="wpbridge-header-right">
<a href="https://wenpai.org/plugins/wpbridge" target="_blank" class="wpbridge-header-link">
<span class="dashicons dashicons-book"></span>
<?php esc_html_e( '文档', 'wpbridge' ); ?>
</a>
<a href="https://github.com/WenPai-org/wpbridge" target="_blank" class="wpbridge-header-link">
<span class="dashicons dashicons-editor-code"></span>
<?php esc_html_e( 'GitHub', 'wpbridge' ); ?>
</a>
<a href="https://wenpai.org/support" target="_blank" class="wpbridge-header-link">
<span class="dashicons dashicons-sos"></span>
<?php esc_html_e( '支持', 'wpbridge' ); ?>
</a>
</div>
<div class="wpbridge-header-right">
<a href="https://wenpai.org/plugins/wpbridge" target="_blank" class="wpbridge-header-link">
<span class="dashicons dashicons-book"></span>
<?php esc_html_e( '文档', 'wpbridge' ); ?>
</a>
<a href="https://github.com/WenPai-org/wpbridge" target="_blank" class="wpbridge-header-link">
<span class="dashicons dashicons-editor-code"></span>
<?php esc_html_e( 'GitHub', 'wpbridge' ); ?>
</a>
<a href="https://wenpai.org/support" target="_blank" class="wpbridge-header-link">
<span class="dashicons dashicons-sos"></span>
<?php esc_html_e( '支持', 'wpbridge' ); ?>
</a>
</div>
</header>

<div class="wrap wpbridge-wrap">

<!-- 主内容区 -->
<div class="wpbridge-content">
<!-- Tab 卡片 -->
<div class="wpbridge-tabs-card">
<!-- Tab 导航 -->
<nav class="wpbridge-tab-list">
<a href="#overview" class="wpbridge-tab wpbridge-tab-active" data-tab="overview">
<?php esc_html_e( '概览', 'wpbridge' ); ?>
</a>
<a href="#projects" class="wpbridge-tab" data-tab="projects">
<?php esc_html_e( '项目', 'wpbridge' ); ?>
</a>
<a href="#sources" class="wpbridge-tab" data-tab="sources">
<?php esc_html_e( '更新源', 'wpbridge' ); ?>
</a>
<a href="#vendors" class="wpbridge-tab" data-tab="vendors">
<?php esc_html_e( '供应商', 'wpbridge' ); ?>
</a>
<a href="#diagnostics" class="wpbridge-tab" data-tab="diagnostics">
<?php esc_html_e( '诊断', 'wpbridge' ); ?>
</a>
<a href="#settings" class="wpbridge-tab" data-tab="settings">
<?php esc_html_e( '设置', 'wpbridge' ); ?>
</a>
<a href="#api" class="wpbridge-tab" data-tab="api">
<?php esc_html_e( 'Bridge API', 'wpbridge' ); ?>
</a>
<a href="#logs" class="wpbridge-tab" data-tab="logs">
<?php esc_html_e( '日志', 'wpbridge' ); ?>
</a>
</nav>
<!-- 主内容区 -->
<div class="wpbridge-content">
<!-- Tab 卡片 -->
<div class="wpbridge-tabs-card">
<!-- Tab 导航 -->
<nav class="wpbridge-tab-list">
<a href="#overview" class="wpbridge-tab wpbridge-tab-active" data-tab="overview">
<?php esc_html_e( '概览', 'wpbridge' ); ?>
</a>
<a href="#projects" class="wpbridge-tab" data-tab="projects">
<?php esc_html_e( '项目', 'wpbridge' ); ?>
</a>
<a href="#sources" class="wpbridge-tab" data-tab="sources">
<?php esc_html_e( '更新源', 'wpbridge' ); ?>
</a>
<a href="#vendors" class="wpbridge-tab" data-tab="vendors">
<?php esc_html_e( '供应商', 'wpbridge' ); ?>
</a>
<a href="#diagnostics" class="wpbridge-tab" data-tab="diagnostics">
<?php esc_html_e( '诊断', 'wpbridge' ); ?>
</a>
<a href="#settings" class="wpbridge-tab" data-tab="settings">
<?php esc_html_e( '设置', 'wpbridge' ); ?>
</a>
<a href="#api" class="wpbridge-tab" data-tab="api">
<?php esc_html_e( 'Bridge API', 'wpbridge' ); ?>
</a>
<a href="#logs" class="wpbridge-tab" data-tab="logs">
<?php esc_html_e( '日志', 'wpbridge' ); ?>
</a>
</nav>

<!-- Tab: 概览 -->
<div id="overview" class="wpbridge-tab-pane wpbridge-tab-pane-active">
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/overview.php'; ?>
</div>
<!-- Tab: 概览 -->
<div id="overview" class="wpbridge-tab-pane wpbridge-tab-pane-active">
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/overview.php'; ?>
</div>

<!-- Tab: 项目 -->
<div id="projects" class="wpbridge-tab-pane">
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/projects.php'; ?>
</div>
<!-- Tab: 项目 -->
<div id="projects" class="wpbridge-tab-pane">
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/projects.php'; ?>
</div>

<!-- Tab: 更新源 -->
<div id="sources" class="wpbridge-tab-pane">
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/sources.php'; ?>
</div>
<!-- Tab: 更新源 -->
<div id="sources" class="wpbridge-tab-pane">
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/sources.php'; ?>
</div>

<!-- Tab: 供应商 -->
<div id="vendors" class="wpbridge-tab-pane">
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/vendors.php'; ?>
</div>
<!-- Tab: 供应商 -->
<div id="vendors" class="wpbridge-tab-pane">
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/vendors.php'; ?>
</div>

<!-- Tab: 诊断 -->
<div id="diagnostics" class="wpbridge-tab-pane">
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/diagnostics.php'; ?>
</div>
<!-- Tab: 诊断 -->
<div id="diagnostics" class="wpbridge-tab-pane">
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/diagnostics.php'; ?>
</div>

<!-- Tab: 设置 -->
<div id="settings" class="wpbridge-tab-pane">
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/settings.php'; ?>
</div>
<!-- Tab: 设置 -->
<div id="settings" class="wpbridge-tab-pane">
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/settings.php'; ?>
</div>

<!-- Tab: Bridge API -->
<div id="api" class="wpbridge-tab-pane">
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/api.php'; ?>
</div>
<!-- Tab: Bridge API -->
<div id="api" class="wpbridge-tab-pane">
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/api.php'; ?>
</div>

<!-- Tab: 日志 -->
<div id="logs" class="wpbridge-tab-pane">
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/logs.php'; ?>
</div>
</div>
</div>
<!-- Tab: 日志 -->
<div id="logs" class="wpbridge-tab-pane">
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/logs.php'; ?>
</div>
</div>
</div>
</div>

View file

@ -11,131 +11,128 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

use WPBridge\Core\DefaultsManager;

// 获取当前默认规则
$defaults = $defaults_manager->get_all();
$global_sources = $defaults['global']['source_order'] ?? array();
$plugin_sources = $defaults['plugin']['source_order'] ?? array();
$theme_sources = $defaults['theme']['source_order'] ?? array();
$defaults = $defaults_manager->get_all();
$global_sources = $defaults['global']['source_order'] ?? [];
$plugin_sources = $defaults['plugin']['source_order'] ?? [];
$theme_sources = $defaults['theme']['source_order'] ?? [];
?>

<div class="wpbridge-defaults-config">
<div class="wpbridge-section">
<h3 class="wpbridge-section-title">
<span class="dashicons dashicons-admin-settings"></span>
<?php esc_html_e( '默认更新源配置', 'wpbridge' ); ?>
</h3>
<p class="wpbridge-section-desc">
<?php esc_html_e( '配置插件和主题的默认更新源顺序。当项目未单独配置时,将按此顺序查找更新。', 'wpbridge' ); ?>
</p>
</div>
<div class="wpbridge-section">
<h3 class="wpbridge-section-title">
<span class="dashicons dashicons-admin-settings"></span>
<?php esc_html_e( '默认更新源配置', 'wpbridge' ); ?>
</h3>
<p class="wpbridge-section-desc">
<?php esc_html_e( '配置插件和主题的默认更新源顺序。当项目未单独配置时,将按此顺序查找更新。', 'wpbridge' ); ?>
</p>
</div>

<form method="post" id="wpbridge-defaults-form">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="save_defaults">
<form method="post" id="wpbridge-defaults-form">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="save_defaults">

<!-- 全局默认 -->
<div class="wpbridge-config-card">
<div class="wpbridge-config-card-header">
<h4><?php esc_html_e( '全局默认', 'wpbridge' ); ?></h4>
<span class="wpbridge-config-card-desc"><?php esc_html_e( '适用于所有未单独配置的项目', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-config-card-body">
<div class="wpbridge-source-order" id="wpbridge-global-sources" data-scope="global">
<?php
foreach ( $all_sources as $source ) :
$priority = $global_sources[ $source['source_key'] ] ?? $source['default_priority'];
?>
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
<label class="wpbridge-toggle wpbridge-toggle-sm">
<input type="checkbox" name="global_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
value="1" <?php checked( isset( $global_sources[ $source['source_key'] ] ) || empty( $global_sources ) ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 全局默认 -->
<div class="wpbridge-config-card">
<div class="wpbridge-config-card-header">
<h4><?php esc_html_e( '全局默认', 'wpbridge' ); ?></h4>
<span class="wpbridge-config-card-desc"><?php esc_html_e( '适用于所有未单独配置的项目', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-config-card-body">
<div class="wpbridge-source-order" id="wpbridge-global-sources" data-scope="global">
<?php foreach ( $all_sources as $source ) :
$priority = $global_sources[ $source['source_key'] ] ?? $source['default_priority'];
?>
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
<label class="wpbridge-toggle wpbridge-toggle-sm">
<input type="checkbox" name="global_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
value="1" <?php checked( isset( $global_sources[ $source['source_key'] ] ) || empty( $global_sources ) ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>

<!-- 插件默认 -->
<div class="wpbridge-config-card">
<div class="wpbridge-config-card-header">
<h4><?php esc_html_e( '插件默认', 'wpbridge' ); ?></h4>
<span class="wpbridge-config-card-desc"><?php esc_html_e( '覆盖全局默认,仅适用于插件', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-config-card-body">
<label class="wpbridge-checkbox">
<input type="checkbox" name="plugin_override" id="plugin_override"
<?php checked( ! empty( $plugin_sources ) ); ?>>
<span><?php esc_html_e( '为插件使用单独的默认源配置', 'wpbridge' ); ?></span>
</label>
<div class="wpbridge-source-order wpbridge-source-order-override" id="wpbridge-plugin-sources" data-scope="plugin"
style="<?php echo empty( $plugin_sources ) ? 'display:none;' : ''; ?>">
<?php
foreach ( $all_sources as $source ) :
$priority = $plugin_sources[ $source['source_key'] ] ?? $source['default_priority'];
?>
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
<label class="wpbridge-toggle wpbridge-toggle-sm">
<input type="checkbox" name="plugin_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
value="1" <?php checked( isset( $plugin_sources[ $source['source_key'] ] ) ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 插件默认 -->
<div class="wpbridge-config-card">
<div class="wpbridge-config-card-header">
<h4><?php esc_html_e( '插件默认', 'wpbridge' ); ?></h4>
<span class="wpbridge-config-card-desc"><?php esc_html_e( '覆盖全局默认,仅适用于插件', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-config-card-body">
<label class="wpbridge-checkbox">
<input type="checkbox" name="plugin_override" id="plugin_override"
<?php checked( ! empty( $plugin_sources ) ); ?>>
<span><?php esc_html_e( '为插件使用单独的默认源配置', 'wpbridge' ); ?></span>
</label>
<div class="wpbridge-source-order wpbridge-source-order-override" id="wpbridge-plugin-sources" data-scope="plugin"
style="<?php echo empty( $plugin_sources ) ? 'display:none;' : ''; ?>">
<?php foreach ( $all_sources as $source ) :
$priority = $plugin_sources[ $source['source_key'] ] ?? $source['default_priority'];
?>
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
<label class="wpbridge-toggle wpbridge-toggle-sm">
<input type="checkbox" name="plugin_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
value="1" <?php checked( isset( $plugin_sources[ $source['source_key'] ] ) ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>

<!-- 主题默认 -->
<div class="wpbridge-config-card">
<div class="wpbridge-config-card-header">
<h4><?php esc_html_e( '主题默认', 'wpbridge' ); ?></h4>
<span class="wpbridge-config-card-desc"><?php esc_html_e( '覆盖全局默认,仅适用于主题', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-config-card-body">
<label class="wpbridge-checkbox">
<input type="checkbox" name="theme_override" id="theme_override"
<?php checked( ! empty( $theme_sources ) ); ?>>
<span><?php esc_html_e( '为主题使用单独的默认源配置', 'wpbridge' ); ?></span>
</label>
<div class="wpbridge-source-order wpbridge-source-order-override" id="wpbridge-theme-sources" data-scope="theme"
style="<?php echo empty( $theme_sources ) ? 'display:none;' : ''; ?>">
<?php
foreach ( $all_sources as $source ) :
$priority = $theme_sources[ $source['source_key'] ] ?? $source['default_priority'];
?>
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
<label class="wpbridge-toggle wpbridge-toggle-sm">
<input type="checkbox" name="theme_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
value="1" <?php checked( isset( $theme_sources[ $source['source_key'] ] ) ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 主题默认 -->
<div class="wpbridge-config-card">
<div class="wpbridge-config-card-header">
<h4><?php esc_html_e( '主题默认', 'wpbridge' ); ?></h4>
<span class="wpbridge-config-card-desc"><?php esc_html_e( '覆盖全局默认,仅适用于主题', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-config-card-body">
<label class="wpbridge-checkbox">
<input type="checkbox" name="theme_override" id="theme_override"
<?php checked( ! empty( $theme_sources ) ); ?>>
<span><?php esc_html_e( '为主题使用单独的默认源配置', 'wpbridge' ); ?></span>
</label>
<div class="wpbridge-source-order wpbridge-source-order-override" id="wpbridge-theme-sources" data-scope="theme"
style="<?php echo empty( $theme_sources ) ? 'display:none;' : ''; ?>">
<?php foreach ( $all_sources as $source ) :
$priority = $theme_sources[ $source['source_key'] ] ?? $source['default_priority'];
?>
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
<label class="wpbridge-toggle wpbridge-toggle-sm">
<input type="checkbox" name="theme_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
value="1" <?php checked( isset( $theme_sources[ $source['source_key'] ] ) ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>

<div class="wpbridge-form-actions">
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
<span class="dashicons dashicons-saved"></span>
<?php esc_html_e( '保存默认规则', 'wpbridge' ); ?>
</button>
</div>
</form>
<div class="wpbridge-form-actions">
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
<span class="dashicons dashicons-saved"></span>
<?php esc_html_e( '保存默认规则', 'wpbridge' ); ?>
</button>
</div>
</form>
</div>

View file

@ -12,7 +12,7 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

use WPBridge\Core\ItemSourceManager;
@ -27,259 +27,258 @@ $version_lock = VersionLock::get_instance();

<!-- 批量操作工具栏 -->
<div class="wpbridge-toolbar">
<div class="wpbridge-toolbar-left">
<label class="wpbridge-checkbox-all">
<input type="checkbox" id="wpbridge-select-all-plugins">
<span><?php esc_html_e( '全选', 'wpbridge' ); ?></span>
</label>
<select id="wpbridge-bulk-action-plugins" class="wpbridge-select">
<option value=""><?php esc_html_e( '批量操作', 'wpbridge' ); ?></option>
<option value="set_source"><?php esc_html_e( '设置更新源', 'wpbridge' ); ?></option>
<option value="reset_default"><?php esc_html_e( '重置为默认', 'wpbridge' ); ?></option>
<option value="disable"><?php esc_html_e( '禁用更新', 'wpbridge' ); ?></option>
</select>
<select id="wpbridge-bulk-source-plugins" class="wpbridge-select wpbridge-bulk-source-select" style="display: none;">
<option value=""><?php esc_html_e( '-- 选择更新源 --', 'wpbridge' ); ?></option>
<?php foreach ( $all_sources as $source ) : ?>
<option value="<?php echo esc_attr( $source['source_key'] ); ?>">
<?php echo esc_html( $source['name'] ); ?>
</option>
<?php endforeach; ?>
</select>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-apply-bulk-plugins">
<?php esc_html_e( '应用', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-toolbar-right">
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-refresh-detection" title="<?php esc_attr_e( '重新检测所有插件类型', 'wpbridge' ); ?>">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '刷新检测', 'wpbridge' ); ?>
</button>
<input type="search" class="wpbridge-search" id="wpbridge-search-plugins" placeholder="<?php esc_attr_e( '搜索插件...', 'wpbridge' ); ?>" autocomplete="off">
</div>
<div class="wpbridge-toolbar-left">
<label class="wpbridge-checkbox-all">
<input type="checkbox" id="wpbridge-select-all-plugins">
<span><?php esc_html_e( '全选', 'wpbridge' ); ?></span>
</label>
<select id="wpbridge-bulk-action-plugins" class="wpbridge-select">
<option value=""><?php esc_html_e( '批量操作', 'wpbridge' ); ?></option>
<option value="set_source"><?php esc_html_e( '设置更新源', 'wpbridge' ); ?></option>
<option value="reset_default"><?php esc_html_e( '重置为默认', 'wpbridge' ); ?></option>
<option value="disable"><?php esc_html_e( '禁用更新', 'wpbridge' ); ?></option>
</select>
<select id="wpbridge-bulk-source-plugins" class="wpbridge-select wpbridge-bulk-source-select" style="display: none;">
<option value=""><?php esc_html_e( '-- 选择更新源 --', 'wpbridge' ); ?></option>
<?php foreach ( $all_sources as $source ) : ?>
<option value="<?php echo esc_attr( $source['source_key'] ); ?>">
<?php echo esc_html( $source['name'] ); ?>
</option>
<?php endforeach; ?>
</select>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-apply-bulk-plugins">
<?php esc_html_e( '应用', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-toolbar-right">
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-refresh-detection" title="<?php esc_attr_e( '重新检测所有插件类型', 'wpbridge' ); ?>">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '刷新检测', 'wpbridge' ); ?>
</button>
<input type="search" class="wpbridge-search" id="wpbridge-search-plugins" placeholder="<?php esc_attr_e( '搜索插件...', 'wpbridge' ); ?>" autocomplete="off">
</div>
</div>

<!-- 插件列表 -->
<div class="wpbridge-project-list" id="wpbridge-plugins-list">
<?php if ( empty( $installed_plugins ) ) : ?>
<div class="wpbridge-empty">
<span class="dashicons dashicons-admin-plugins"></span>
<h3><?php esc_html_e( '暂无已安装插件', 'wpbridge' ); ?></h3>
</div>
<?php else : ?>
<?php
foreach ( $installed_plugins as $plugin_file => $plugin_data ) :
$item_key = 'plugin:' . $plugin_file;
$config = $item_manager->get( $item_key );
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
$effective_sources = $item_manager->get_effective_sources( $item_key, $defaults_manager );
<?php if ( empty( $installed_plugins ) ) : ?>
<div class="wpbridge-empty">
<span class="dashicons dashicons-admin-plugins"></span>
<h3><?php esc_html_e( '暂无已安装插件', 'wpbridge' ); ?></h3>
</div>
<?php else : ?>
<?php foreach ( $installed_plugins as $plugin_file => $plugin_data ) :
$item_key = 'plugin:' . $plugin_file;
$config = $item_manager->get( $item_key );
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
$effective_sources = $item_manager->get_effective_sources( $item_key, $defaults_manager );

// 获取插件 slug
$plugin_slug = dirname( $plugin_file );
if ( $plugin_slug === '.' ) {
$plugin_slug = basename( $plugin_file, '.php' );
}
// 获取插件 slug
$plugin_slug = dirname( $plugin_file );
if ( $plugin_slug === '.' ) {
$plugin_slug = basename( $plugin_file, '.php' );
}

// 判断是否激活
$is_active = is_plugin_active( $plugin_file );
// 判断是否激活
$is_active = is_plugin_active( $plugin_file );

// 获取版本锁定信息
$lock_info = $version_lock->get( $item_key );
$is_locked = null !== $lock_info;
// 获取版本锁定信息
$lock_info = $version_lock->get( $item_key );
$is_locked = null !== $lock_info;

// 检测插件类型
$type_info = $commercial_detector->detect( $plugin_slug, $plugin_file );
$type_label = CommercialDetector::get_type_label( $type_info['type'] );
?>
<div class="wpbridge-project-item" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-item-type="plugin">
<div class="wpbridge-project-checkbox">
<input type="checkbox" class="wpbridge-project-select" value="<?php echo esc_attr( $item_key ); ?>">
</div>
// 检测插件类型
$type_info = $commercial_detector->detect( $plugin_slug, $plugin_file );
$type_label = CommercialDetector::get_type_label( $type_info['type'] );
?>
<div class="wpbridge-project-item" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-item-type="plugin">
<div class="wpbridge-project-checkbox">
<input type="checkbox" class="wpbridge-project-select" value="<?php echo esc_attr( $item_key ); ?>">
</div>

<button type="button" class="wpbridge-btn wpbridge-btn-icon wpbridge-project-expand"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
title="<?php esc_attr_e( '展开配置', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-down-alt2"></span>
</button>
<button type="button" class="wpbridge-btn wpbridge-btn-icon wpbridge-project-expand"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
title="<?php esc_attr_e( '展开配置', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-down-alt2"></span>
</button>

<div class="wpbridge-project-info">
<div class="wpbridge-project-name">
<?php echo esc_html( $plugin_data['Name'] ); ?>
<?php if ( $is_active ) : ?>
<span class="wpbridge-badge wpbridge-badge-success"><?php esc_html_e( '已激活', 'wpbridge' ); ?></span>
<?php endif; ?>
</div>
<div class="wpbridge-project-meta">
<span class="wpbridge-project-version">v<?php echo esc_html( $plugin_data['Version'] ); ?></span>
<span class="wpbridge-project-slug"><?php echo esc_html( $plugin_slug ); ?></span>
<a href="#" class="wpbridge-view-changelog"
data-slug="<?php echo esc_attr( $plugin_slug ); ?>"
data-type="plugin"
data-source-type="wporg"
title="<?php esc_attr_e( '查看更新日志', 'wpbridge' ); ?>">
<span class="dashicons dashicons-list-view"></span>
</a>
<?php if ( ! empty( $plugin_data['Author'] ) ) : ?>
<span class="wpbridge-project-author"><?php echo esc_html( $plugin_data['Author'] ); ?></span>
<?php endif; ?>
</div>
</div>
<div class="wpbridge-project-info">
<div class="wpbridge-project-name">
<?php echo esc_html( $plugin_data['Name'] ); ?>
<?php if ( $is_active ) : ?>
<span class="wpbridge-badge wpbridge-badge-success"><?php esc_html_e( '已激活', 'wpbridge' ); ?></span>
<?php endif; ?>
</div>
<div class="wpbridge-project-meta">
<span class="wpbridge-project-version">v<?php echo esc_html( $plugin_data['Version'] ); ?></span>
<span class="wpbridge-project-slug"><?php echo esc_html( $plugin_slug ); ?></span>
<a href="#" class="wpbridge-view-changelog"
data-slug="<?php echo esc_attr( $plugin_slug ); ?>"
data-type="plugin"
data-source-type="wporg"
title="<?php esc_attr_e( '查看更新日志', 'wpbridge' ); ?>">
<span class="dashicons dashicons-list-view"></span>
</a>
<?php if ( ! empty( $plugin_data['Author'] ) ) : ?>
<span class="wpbridge-project-author"><?php echo esc_html( $plugin_data['Author'] ); ?></span>
<?php endif; ?>
</div>
</div>

<div class="wpbridge-project-status">
<!-- 插件类型徽章 -->
<span class="wpbridge-status-badge wpbridge-status-type-<?php echo esc_attr( $type_info['type'] ); ?>"
data-plugin-slug="<?php echo esc_attr( $plugin_slug ); ?>"
data-source="<?php echo esc_attr( $type_info['source'] ); ?>"
title="<?php echo esc_attr( $type_info['source'] === 'manual' ? __( '手动标记', 'wpbridge' ) : __( '自动检测', 'wpbridge' ) ); ?>">
<span class="dashicons <?php echo esc_attr( $type_label['icon'] ); ?>"></span>
<?php echo esc_html( $type_label['label'] ); ?>
</span>
<?php if ( $is_locked ) : ?>
<span class="wpbridge-status-badge wpbridge-status-locked wpbridge-version-lock-badge"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
title="<?php echo esc_attr( VersionLock::get_type_label( $lock_info['type'] ) ); ?>">
<span class="dashicons dashicons-lock"></span>
<?php esc_html_e( '已锁定', 'wpbridge' ); ?>
</span>
<?php endif; ?>
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
<span class="wpbridge-status-badge wpbridge-status-disabled">
<span class="dashicons dashicons-dismiss"></span>
<?php esc_html_e( '已禁用', 'wpbridge' ); ?>
</span>
<?php elseif ( $mode === ItemSourceManager::MODE_CUSTOM ) : ?>
<span class="wpbridge-status-badge wpbridge-status-custom">
<span class="dashicons dashicons-admin-links"></span>
<?php esc_html_e( '自定义', 'wpbridge' ); ?>
</span>
<?php else : ?>
<span class="wpbridge-status-badge wpbridge-status-default">
<span class="dashicons dashicons-yes-alt"></span>
<?php esc_html_e( '默认', 'wpbridge' ); ?>
</span>
<?php endif; ?>
</div>
<div class="wpbridge-project-status">
<!-- 插件类型徽章 -->
<span class="wpbridge-status-badge wpbridge-status-type-<?php echo esc_attr( $type_info['type'] ); ?>"
data-plugin-slug="<?php echo esc_attr( $plugin_slug ); ?>"
data-source="<?php echo esc_attr( $type_info['source'] ); ?>"
title="<?php echo esc_attr( $type_info['source'] === 'manual' ? __( '手动标记', 'wpbridge' ) : __( '自动检测', 'wpbridge' ) ); ?>">
<span class="dashicons <?php echo esc_attr( $type_label['icon'] ); ?>"></span>
<?php echo esc_html( $type_label['label'] ); ?>
</span>
<?php if ( $is_locked ) : ?>
<span class="wpbridge-status-badge wpbridge-status-locked wpbridge-version-lock-badge"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
title="<?php echo esc_attr( VersionLock::get_type_label( $lock_info['type'] ) ); ?>">
<span class="dashicons dashicons-lock"></span>
<?php esc_html_e( '已锁定', 'wpbridge' ); ?>
</span>
<?php endif; ?>
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
<span class="wpbridge-status-badge wpbridge-status-disabled">
<span class="dashicons dashicons-dismiss"></span>
<?php esc_html_e( '已禁用', 'wpbridge' ); ?>
</span>
<?php elseif ( $mode === ItemSourceManager::MODE_CUSTOM ) : ?>
<span class="wpbridge-status-badge wpbridge-status-custom">
<span class="dashicons dashicons-admin-links"></span>
<?php esc_html_e( '自定义', 'wpbridge' ); ?>
</span>
<?php else : ?>
<span class="wpbridge-status-badge wpbridge-status-default">
<span class="dashicons dashicons-yes-alt"></span>
<?php esc_html_e( '默认', 'wpbridge' ); ?>
</span>
<?php endif; ?>
</div>

<!-- 内联配置面板(默认折叠) -->
<div class="wpbridge-project-config-panel" data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
<!-- P2: 插件类型手动标记 -->
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '插件类型', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<select class="wpbridge-form-input wpbridge-plugin-type-select"
data-plugin-slug="<?php echo esc_attr( $plugin_slug ); ?>">
<option value="<?php echo esc_attr( CommercialDetector::TYPE_UNKNOWN ); ?>"
<?php selected( $type_info['type'], CommercialDetector::TYPE_UNKNOWN ); ?>>
<?php esc_html_e( '自动检测', 'wpbridge' ); ?>
</option>
<option value="<?php echo esc_attr( CommercialDetector::TYPE_FREE ); ?>"
<?php selected( $type_info['type'], CommercialDetector::TYPE_FREE ); ?>>
<?php esc_html_e( '免费插件', 'wpbridge' ); ?>
</option>
<option value="<?php echo esc_attr( CommercialDetector::TYPE_COMMERCIAL ); ?>"
<?php selected( $type_info['type'], CommercialDetector::TYPE_COMMERCIAL ); ?>>
<?php esc_html_e( '商业插件', 'wpbridge' ); ?>
</option>
<option value="<?php echo esc_attr( CommercialDetector::TYPE_PRIVATE ); ?>"
<?php selected( $type_info['type'], CommercialDetector::TYPE_PRIVATE ); ?>>
<?php esc_html_e( '私有插件', 'wpbridge' ); ?>
</option>
</select>
<p class="wpbridge-form-help">
<?php if ( $type_info['source'] === 'manual' ) : ?>
<?php esc_html_e( '当前为手动标记', 'wpbridge' ); ?>
<?php else : ?>
<?php
printf(
/* translators: %s: detection source */
esc_html__( '自动检测结果(来源:%s', 'wpbridge' ),
esc_html( $type_info['source'] )
);
?>
<?php endif; ?>
</p>
</div>
</div>
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<input type="url" class="wpbridge-form-input wpbridge-inline-url"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
placeholder="https://github.com/user/repo 或 https://example.com/update.json"
autocomplete="off">
<p class="wpbridge-form-help"><?php esc_html_e( '粘贴更新源地址,系统会自动识别类型', 'wpbridge' ); ?></p>
</div>
</div>
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<input type="password" class="wpbridge-form-input wpbridge-inline-token"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
placeholder="<?php esc_attr_e( '可选,用于私有仓库', 'wpbridge' ); ?>"
autocomplete="new-password">
</div>
</div>
<!-- 版本锁定 -->
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '版本锁定', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<div class="wpbridge-version-lock-controls" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-current-version="<?php echo esc_attr( $plugin_data['Version'] ); ?>">
<?php if ( $is_locked ) : ?>
<span class="wpbridge-lock-status">
<span class="dashicons dashicons-lock"></span>
<?php echo esc_html( VersionLock::get_type_label( $lock_info['type'] ) ); ?>
<?php if ( ! empty( $lock_info['version'] ) ) : ?>
(v<?php echo esc_html( $lock_info['version'] ); ?>)
<?php endif; ?>
</span>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-unlock-version"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-unlock"></span>
<?php esc_html_e( '解锁', 'wpbridge' ); ?>
</button>
<?php else : ?>
<select class="wpbridge-form-input wpbridge-lock-type-select" style="max-width: 150px;">
<option value=""><?php esc_html_e( '不锁定', 'wpbridge' ); ?></option>
<option value="current"><?php esc_html_e( '锁定当前版本', 'wpbridge' ); ?></option>
</select>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-lock-version"
data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
<span class="dashicons dashicons-lock"></span>
<?php esc_html_e( '锁定', 'wpbridge' ); ?>
</button>
<?php endif; ?>
</div>
<p class="wpbridge-form-help"><?php esc_html_e( '锁定后将阻止此插件的自动更新', 'wpbridge' ); ?></p>
</div>
</div>
<div class="wpbridge-config-actions">
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-btn-sm wpbridge-save-inline"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-saved"></span>
<?php esc_html_e( '保存', 'wpbridge' ); ?>
</button>
<?php if ( $mode !== ItemSourceManager::MODE_DEFAULT ) : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-reset-default"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<?php esc_html_e( '重置为默认', 'wpbridge' ); ?>
</button>
<?php endif; ?>
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-enable-update"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '启用更新', 'wpbridge' ); ?>
</button>
<?php else : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-disable-update"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-dismiss"></span>
<?php esc_html_e( '禁用更新', 'wpbridge' ); ?>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- 内联配置面板(默认折叠) -->
<div class="wpbridge-project-config-panel" data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
<!-- P2: 插件类型手动标记 -->
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '插件类型', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<select class="wpbridge-form-input wpbridge-plugin-type-select"
data-plugin-slug="<?php echo esc_attr( $plugin_slug ); ?>">
<option value="<?php echo esc_attr( CommercialDetector::TYPE_UNKNOWN ); ?>"
<?php selected( $type_info['type'], CommercialDetector::TYPE_UNKNOWN ); ?>>
<?php esc_html_e( '自动检测', 'wpbridge' ); ?>
</option>
<option value="<?php echo esc_attr( CommercialDetector::TYPE_FREE ); ?>"
<?php selected( $type_info['type'], CommercialDetector::TYPE_FREE ); ?>>
<?php esc_html_e( '免费插件', 'wpbridge' ); ?>
</option>
<option value="<?php echo esc_attr( CommercialDetector::TYPE_COMMERCIAL ); ?>"
<?php selected( $type_info['type'], CommercialDetector::TYPE_COMMERCIAL ); ?>>
<?php esc_html_e( '商业插件', 'wpbridge' ); ?>
</option>
<option value="<?php echo esc_attr( CommercialDetector::TYPE_PRIVATE ); ?>"
<?php selected( $type_info['type'], CommercialDetector::TYPE_PRIVATE ); ?>>
<?php esc_html_e( '私有插件', 'wpbridge' ); ?>
</option>
</select>
<p class="wpbridge-form-help">
<?php if ( $type_info['source'] === 'manual' ) : ?>
<?php esc_html_e( '当前为手动标记', 'wpbridge' ); ?>
<?php else : ?>
<?php
printf(
/* translators: %s: detection source */
esc_html__( '自动检测结果(来源:%s', 'wpbridge' ),
esc_html( $type_info['source'] )
);
?>
<?php endif; ?>
</p>
</div>
</div>
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<input type="url" class="wpbridge-form-input wpbridge-inline-url"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
placeholder="https://github.com/user/repo 或 https://example.com/update.json"
autocomplete="off">
<p class="wpbridge-form-help"><?php esc_html_e( '粘贴更新源地址,系统会自动识别类型', 'wpbridge' ); ?></p>
</div>
</div>
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<input type="password" class="wpbridge-form-input wpbridge-inline-token"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
placeholder="<?php esc_attr_e( '可选,用于私有仓库', 'wpbridge' ); ?>"
autocomplete="new-password">
</div>
</div>
<!-- 版本锁定 -->
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '版本锁定', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<div class="wpbridge-version-lock-controls" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-current-version="<?php echo esc_attr( $plugin_data['Version'] ); ?>">
<?php if ( $is_locked ) : ?>
<span class="wpbridge-lock-status">
<span class="dashicons dashicons-lock"></span>
<?php echo esc_html( VersionLock::get_type_label( $lock_info['type'] ) ); ?>
<?php if ( ! empty( $lock_info['version'] ) ) : ?>
(v<?php echo esc_html( $lock_info['version'] ); ?>)
<?php endif; ?>
</span>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-unlock-version"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-unlock"></span>
<?php esc_html_e( '解锁', 'wpbridge' ); ?>
</button>
<?php else : ?>
<select class="wpbridge-form-input wpbridge-lock-type-select" style="max-width: 150px;">
<option value=""><?php esc_html_e( '不锁定', 'wpbridge' ); ?></option>
<option value="current"><?php esc_html_e( '锁定当前版本', 'wpbridge' ); ?></option>
</select>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-lock-version"
data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
<span class="dashicons dashicons-lock"></span>
<?php esc_html_e( '锁定', 'wpbridge' ); ?>
</button>
<?php endif; ?>
</div>
<p class="wpbridge-form-help"><?php esc_html_e( '锁定后将阻止此插件的自动更新', 'wpbridge' ); ?></p>
</div>
</div>
<div class="wpbridge-config-actions">
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-btn-sm wpbridge-save-inline"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-saved"></span>
<?php esc_html_e( '保存', 'wpbridge' ); ?>
</button>
<?php if ( $mode !== ItemSourceManager::MODE_DEFAULT ) : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-reset-default"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<?php esc_html_e( '重置为默认', 'wpbridge' ); ?>
</button>
<?php endif; ?>
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-enable-update"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '启用更新', 'wpbridge' ); ?>
</button>
<?php else : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-disable-update"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-dismiss"></span>
<?php esc_html_e( '禁用更新', 'wpbridge' ); ?>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>

View file

@ -12,178 +12,177 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

use WPBridge\Core\ItemSourceManager;

// 获取当前主题
$current_theme = wp_get_theme();
$current_theme = wp_get_theme();
$current_theme_slug = $current_theme->get_stylesheet();
?>

<!-- 批量操作工具栏 -->
<div class="wpbridge-toolbar">
<div class="wpbridge-toolbar-left">
<label class="wpbridge-checkbox-all">
<input type="checkbox" id="wpbridge-select-all-themes">
<span><?php esc_html_e( '全选', 'wpbridge' ); ?></span>
</label>
<select id="wpbridge-bulk-action-themes" class="wpbridge-select">
<option value=""><?php esc_html_e( '批量操作', 'wpbridge' ); ?></option>
<option value="set_source"><?php esc_html_e( '设置更新源', 'wpbridge' ); ?></option>
<option value="reset_default"><?php esc_html_e( '重置为默认', 'wpbridge' ); ?></option>
<option value="disable"><?php esc_html_e( '禁用更新', 'wpbridge' ); ?></option>
</select>
<select id="wpbridge-bulk-source-themes" class="wpbridge-select wpbridge-bulk-source-select" style="display: none;">
<option value=""><?php esc_html_e( '-- 选择更新源 --', 'wpbridge' ); ?></option>
<?php foreach ( $all_sources as $source ) : ?>
<option value="<?php echo esc_attr( $source['source_key'] ); ?>">
<?php echo esc_html( $source['name'] ); ?>
</option>
<?php endforeach; ?>
</select>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-apply-bulk-themes">
<?php esc_html_e( '应用', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-toolbar-right">
<input type="search" class="wpbridge-search" id="wpbridge-search-themes" placeholder="<?php esc_attr_e( '搜索主题...', 'wpbridge' ); ?>" autocomplete="off">
</div>
<div class="wpbridge-toolbar-left">
<label class="wpbridge-checkbox-all">
<input type="checkbox" id="wpbridge-select-all-themes">
<span><?php esc_html_e( '全选', 'wpbridge' ); ?></span>
</label>
<select id="wpbridge-bulk-action-themes" class="wpbridge-select">
<option value=""><?php esc_html_e( '批量操作', 'wpbridge' ); ?></option>
<option value="set_source"><?php esc_html_e( '设置更新源', 'wpbridge' ); ?></option>
<option value="reset_default"><?php esc_html_e( '重置为默认', 'wpbridge' ); ?></option>
<option value="disable"><?php esc_html_e( '禁用更新', 'wpbridge' ); ?></option>
</select>
<select id="wpbridge-bulk-source-themes" class="wpbridge-select wpbridge-bulk-source-select" style="display: none;">
<option value=""><?php esc_html_e( '-- 选择更新源 --', 'wpbridge' ); ?></option>
<?php foreach ( $all_sources as $source ) : ?>
<option value="<?php echo esc_attr( $source['source_key'] ); ?>">
<?php echo esc_html( $source['name'] ); ?>
</option>
<?php endforeach; ?>
</select>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-apply-bulk-themes">
<?php esc_html_e( '应用', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-toolbar-right">
<input type="search" class="wpbridge-search" id="wpbridge-search-themes" placeholder="<?php esc_attr_e( '搜索主题...', 'wpbridge' ); ?>" autocomplete="off">
</div>
</div>

<!-- 主题列表 -->
<div class="wpbridge-project-list" id="wpbridge-themes-list">
<?php if ( empty( $installed_themes ) ) : ?>
<div class="wpbridge-empty">
<span class="dashicons dashicons-admin-appearance"></span>
<h3><?php esc_html_e( '暂无已安装主题', 'wpbridge' ); ?></h3>
</div>
<?php else : ?>
<?php
foreach ( $installed_themes as $theme_slug => $theme ) :
$item_key = 'theme:' . $theme_slug;
$config = $item_manager->get( $item_key );
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
<?php if ( empty( $installed_themes ) ) : ?>
<div class="wpbridge-empty">
<span class="dashicons dashicons-admin-appearance"></span>
<h3><?php esc_html_e( '暂无已安装主题', 'wpbridge' ); ?></h3>
</div>
<?php else : ?>
<?php foreach ( $installed_themes as $theme_slug => $theme ) :
$item_key = 'theme:' . $theme_slug;
$config = $item_manager->get( $item_key );
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;

// 判断是否当前主题
$is_active = $theme_slug === $current_theme_slug;
?>
<div class="wpbridge-project-item" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-item-type="theme">
<div class="wpbridge-project-checkbox">
<input type="checkbox" class="wpbridge-project-select" value="<?php echo esc_attr( $item_key ); ?>">
</div>
// 判断是否当前主题
$is_active = $theme_slug === $current_theme_slug;
?>
<div class="wpbridge-project-item" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-item-type="theme">
<div class="wpbridge-project-checkbox">
<input type="checkbox" class="wpbridge-project-select" value="<?php echo esc_attr( $item_key ); ?>">
</div>

<button type="button" class="wpbridge-btn wpbridge-btn-icon wpbridge-project-expand"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
title="<?php esc_attr_e( '展开配置', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-down-alt2"></span>
</button>
<button type="button" class="wpbridge-btn wpbridge-btn-icon wpbridge-project-expand"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
title="<?php esc_attr_e( '展开配置', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-down-alt2"></span>
</button>

<div class="wpbridge-project-thumbnail">
<?php
$screenshot = $theme->get_screenshot();
if ( $screenshot ) :
// get_screenshot() 返回相对路径,需要构建完整 URL
$screenshot_url = $theme->get_stylesheet_directory_uri() . '/' . basename( $screenshot );
?>
<img loading="lazy" src="<?php echo esc_url( $screenshot_url ); ?>" alt="<?php echo esc_attr( $theme->get( 'Name' ) ); ?>">
<?php else : ?>
<span class="dashicons dashicons-admin-appearance"></span>
<?php endif; ?>
</div>
<div class="wpbridge-project-thumbnail">
<?php
$screenshot = $theme->get_screenshot();
if ( $screenshot ) :
// get_screenshot() 返回相对路径,需要构建完整 URL
$screenshot_url = $theme->get_stylesheet_directory_uri() . '/' . basename( $screenshot );
?>
<img loading="lazy" src="<?php echo esc_url( $screenshot_url ); ?>" alt="<?php echo esc_attr( $theme->get( 'Name' ) ); ?>">
<?php else : ?>
<span class="dashicons dashicons-admin-appearance"></span>
<?php endif; ?>
</div>

<div class="wpbridge-project-info">
<div class="wpbridge-project-name">
<?php echo esc_html( $theme->get( 'Name' ) ); ?>
<?php if ( $is_active ) : ?>
<span class="wpbridge-badge wpbridge-badge-success"><?php esc_html_e( '当前主题', 'wpbridge' ); ?></span>
<?php endif; ?>
</div>
<div class="wpbridge-project-meta">
<span class="wpbridge-project-version">v<?php echo esc_html( $theme->get( 'Version' ) ); ?></span>
<span class="wpbridge-project-slug"><?php echo esc_html( $theme_slug ); ?></span>
<a href="#" class="wpbridge-view-changelog"
data-slug="<?php echo esc_attr( $theme_slug ); ?>"
data-type="theme"
data-source-type="wporg"
title="<?php esc_attr_e( '查看更新日志', 'wpbridge' ); ?>">
<span class="dashicons dashicons-list-view"></span>
</a>
<?php if ( $theme->get( 'Author' ) ) : ?>
<span class="wpbridge-project-author"><?php echo esc_html( $theme->get( 'Author' ) ); ?></span>
<?php endif; ?>
</div>
</div>
<div class="wpbridge-project-info">
<div class="wpbridge-project-name">
<?php echo esc_html( $theme->get( 'Name' ) ); ?>
<?php if ( $is_active ) : ?>
<span class="wpbridge-badge wpbridge-badge-success"><?php esc_html_e( '当前主题', 'wpbridge' ); ?></span>
<?php endif; ?>
</div>
<div class="wpbridge-project-meta">
<span class="wpbridge-project-version">v<?php echo esc_html( $theme->get( 'Version' ) ); ?></span>
<span class="wpbridge-project-slug"><?php echo esc_html( $theme_slug ); ?></span>
<a href="#" class="wpbridge-view-changelog"
data-slug="<?php echo esc_attr( $theme_slug ); ?>"
data-type="theme"
data-source-type="wporg"
title="<?php esc_attr_e( '查看更新日志', 'wpbridge' ); ?>">
<span class="dashicons dashicons-list-view"></span>
</a>
<?php if ( $theme->get( 'Author' ) ) : ?>
<span class="wpbridge-project-author"><?php echo esc_html( $theme->get( 'Author' ) ); ?></span>
<?php endif; ?>
</div>
</div>

<div class="wpbridge-project-status">
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
<span class="wpbridge-status-badge wpbridge-status-disabled">
<span class="dashicons dashicons-dismiss"></span>
<?php esc_html_e( '已禁用', 'wpbridge' ); ?>
</span>
<?php elseif ( $mode === ItemSourceManager::MODE_CUSTOM ) : ?>
<span class="wpbridge-status-badge wpbridge-status-custom">
<span class="dashicons dashicons-admin-links"></span>
<?php esc_html_e( '自定义', 'wpbridge' ); ?>
</span>
<?php else : ?>
<span class="wpbridge-status-badge wpbridge-status-default">
<span class="dashicons dashicons-yes-alt"></span>
<?php esc_html_e( '默认', 'wpbridge' ); ?>
</span>
<?php endif; ?>
</div>
<div class="wpbridge-project-status">
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
<span class="wpbridge-status-badge wpbridge-status-disabled">
<span class="dashicons dashicons-dismiss"></span>
<?php esc_html_e( '已禁用', 'wpbridge' ); ?>
</span>
<?php elseif ( $mode === ItemSourceManager::MODE_CUSTOM ) : ?>
<span class="wpbridge-status-badge wpbridge-status-custom">
<span class="dashicons dashicons-admin-links"></span>
<?php esc_html_e( '自定义', 'wpbridge' ); ?>
</span>
<?php else : ?>
<span class="wpbridge-status-badge wpbridge-status-default">
<span class="dashicons dashicons-yes-alt"></span>
<?php esc_html_e( '默认', 'wpbridge' ); ?>
</span>
<?php endif; ?>
</div>

<!-- 内联配置面板(默认折叠) -->
<div class="wpbridge-project-config-panel" data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<input type="url" class="wpbridge-form-input wpbridge-inline-url"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
placeholder="https://github.com/user/repo 或 https://example.com/update.json"
autocomplete="off">
<p class="wpbridge-form-help"><?php esc_html_e( '粘贴更新源地址,系统会自动识别类型', 'wpbridge' ); ?></p>
</div>
</div>
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<input type="password" class="wpbridge-form-input wpbridge-inline-token"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
placeholder="<?php esc_attr_e( '可选,用于私有仓库', 'wpbridge' ); ?>"
autocomplete="new-password">
</div>
</div>
<div class="wpbridge-config-actions">
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-btn-sm wpbridge-save-inline"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-saved"></span>
<?php esc_html_e( '保存', 'wpbridge' ); ?>
</button>
<?php if ( $mode !== ItemSourceManager::MODE_DEFAULT ) : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-reset-default"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<?php esc_html_e( '重置为默认', 'wpbridge' ); ?>
</button>
<?php endif; ?>
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-enable-update"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '启用更新', 'wpbridge' ); ?>
</button>
<?php else : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-disable-update"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-dismiss"></span>
<?php esc_html_e( '禁用更新', 'wpbridge' ); ?>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- 内联配置面板(默认折叠) -->
<div class="wpbridge-project-config-panel" data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<input type="url" class="wpbridge-form-input wpbridge-inline-url"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
placeholder="https://github.com/user/repo 或 https://example.com/update.json"
autocomplete="off">
<p class="wpbridge-form-help"><?php esc_html_e( '粘贴更新源地址,系统会自动识别类型', 'wpbridge' ); ?></p>
</div>
</div>
<div class="wpbridge-config-row">
<label class="wpbridge-config-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
<div class="wpbridge-config-field">
<input type="password" class="wpbridge-form-input wpbridge-inline-token"
data-item-key="<?php echo esc_attr( $item_key ); ?>"
placeholder="<?php esc_attr_e( '可选,用于私有仓库', 'wpbridge' ); ?>"
autocomplete="new-password">
</div>
</div>
<div class="wpbridge-config-actions">
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-btn-sm wpbridge-save-inline"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-saved"></span>
<?php esc_html_e( '保存', 'wpbridge' ); ?>
</button>
<?php if ( $mode !== ItemSourceManager::MODE_DEFAULT ) : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-reset-default"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<?php esc_html_e( '重置为默认', 'wpbridge' ); ?>
</button>
<?php endif; ?>
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-enable-update"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '启用更新', 'wpbridge' ); ?>
</button>
<?php else : ?>
<button type="button" class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-disable-update"
data-item-key="<?php echo esc_attr( $item_key ); ?>">
<span class="dashicons dashicons-dismiss"></span>
<?php esc_html_e( '禁用更新', 'wpbridge' ); ?>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>

View file

@ -9,137 +9,137 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}
?>

<div class="wrap wpbridge-wrap">
<h1><?php esc_html_e( 'WPBridge 设置', 'wpbridge' ); ?></h1>
<h1><?php esc_html_e( 'WPBridge 设置', 'wpbridge' ); ?></h1>

<?php settings_errors( 'wpbridge' ); ?>
<?php settings_errors( 'wpbridge' ); ?>

<form method="post">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="save_settings">
<form method="post">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="save_settings">

<h2><?php esc_html_e( '常规设置', 'wpbridge' ); ?></h2>
<h2><?php esc_html_e( '常规设置', 'wpbridge' ); ?></h2>

<table class="form-table">
<tr>
<th scope="row"><?php esc_html_e( '调试模式', 'wpbridge' ); ?></th>
<td>
<label>
<input type="checkbox"
name="debug_mode"
value="1"
<?php checked( $settings['debug_mode'] ?? false ); ?>>
<?php esc_html_e( '启用调试日志', 'wpbridge' ); ?>
</label>
<p class="description">
<?php esc_html_e( '启用后会记录详细的调试信息,仅在排查问题时启用。', 'wpbridge' ); ?>
</p>
</td>
</tr>
<table class="form-table">
<tr>
<th scope="row"><?php esc_html_e( '调试模式', 'wpbridge' ); ?></th>
<td>
<label>
<input type="checkbox"
name="debug_mode"
value="1"
<?php checked( $settings['debug_mode'] ?? false ); ?>>
<?php esc_html_e( '启用调试日志', 'wpbridge' ); ?>
</label>
<p class="description">
<?php esc_html_e( '启用后会记录详细的调试信息,仅在排查问题时启用。', 'wpbridge' ); ?>
</p>
</td>
</tr>

<tr>
<th scope="row">
<label for="wpbridge-cache-ttl"><?php esc_html_e( '缓存时间', 'wpbridge' ); ?></label>
</th>
<td>
<select id="wpbridge-cache-ttl" name="cache_ttl">
<option value="3600" <?php selected( $settings['cache_ttl'] ?? 43200, 3600 ); ?>>
<?php esc_html_e( '1 小时', 'wpbridge' ); ?>
</option>
<option value="21600" <?php selected( $settings['cache_ttl'] ?? 43200, 21600 ); ?>>
<?php esc_html_e( '6 小时', 'wpbridge' ); ?>
</option>
<option value="43200" <?php selected( $settings['cache_ttl'] ?? 43200, 43200 ); ?>>
<?php esc_html_e( '12 小时', 'wpbridge' ); ?>
</option>
<option value="86400" <?php selected( $settings['cache_ttl'] ?? 43200, 86400 ); ?>>
<?php esc_html_e( '24 小时', 'wpbridge' ); ?>
</option>
</select>
<p class="description">
<?php esc_html_e( '更新检查结果的缓存时间', 'wpbridge' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="wpbridge-cache-ttl"><?php esc_html_e( '缓存时间', 'wpbridge' ); ?></label>
</th>
<td>
<select id="wpbridge-cache-ttl" name="cache_ttl">
<option value="3600" <?php selected( $settings['cache_ttl'] ?? 43200, 3600 ); ?>>
<?php esc_html_e( '1 小时', 'wpbridge' ); ?>
</option>
<option value="21600" <?php selected( $settings['cache_ttl'] ?? 43200, 21600 ); ?>>
<?php esc_html_e( '6 小时', 'wpbridge' ); ?>
</option>
<option value="43200" <?php selected( $settings['cache_ttl'] ?? 43200, 43200 ); ?>>
<?php esc_html_e( '12 小时', 'wpbridge' ); ?>
</option>
<option value="86400" <?php selected( $settings['cache_ttl'] ?? 43200, 86400 ); ?>>
<?php esc_html_e( '24 小时', 'wpbridge' ); ?>
</option>
</select>
<p class="description">
<?php esc_html_e( '更新检查结果的缓存时间', 'wpbridge' ); ?>
</p>
</td>
</tr>

<tr>
<th scope="row">
<label for="wpbridge-timeout"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></label>
</th>
<td>
<input type="number"
id="wpbridge-timeout"
name="request_timeout"
value="<?php echo esc_attr( $settings['request_timeout'] ?? 10 ); ?>"
min="5"
max="60"
class="small-text">
<?php esc_html_e( '秒', 'wpbridge' ); ?>
<p class="description">
<?php esc_html_e( 'HTTP 请求的超时时间5-60 秒)', 'wpbridge' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="wpbridge-timeout"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></label>
</th>
<td>
<input type="number"
id="wpbridge-timeout"
name="request_timeout"
value="<?php echo esc_attr( $settings['request_timeout'] ?? 10 ); ?>"
min="5"
max="60"
class="small-text">
<?php esc_html_e( '秒', 'wpbridge' ); ?>
<p class="description">
<?php esc_html_e( 'HTTP 请求的超时时间5-60 秒)', 'wpbridge' ); ?>
</p>
</td>
</tr>

<tr>
<th scope="row"><?php esc_html_e( '降级策略', 'wpbridge' ); ?></th>
<td>
<label>
<input type="checkbox"
name="fallback_enabled"
value="1"
<?php checked( $settings['fallback_enabled'] ?? true ); ?>>
<?php esc_html_e( '启用过期缓存兜底', 'wpbridge' ); ?>
</label>
<p class="description">
<?php esc_html_e( '当更新源不可用时,使用过期的缓存数据。', 'wpbridge' ); ?>
</p>
</td>
</tr>
</table>
<tr>
<th scope="row"><?php esc_html_e( '降级策略', 'wpbridge' ); ?></th>
<td>
<label>
<input type="checkbox"
name="fallback_enabled"
value="1"
<?php checked( $settings['fallback_enabled'] ?? true ); ?>>
<?php esc_html_e( '启用过期缓存兜底', 'wpbridge' ); ?>
</label>
<p class="description">
<?php esc_html_e( '当更新源不可用时,使用过期的缓存数据。', 'wpbridge' ); ?>
</p>
</td>
</tr>
</table>

<p class="submit">
<input type="submit" class="button button-primary" value="<?php esc_attr_e( '保存设置', 'wpbridge' ); ?>">
</p>
</form>
<p class="submit">
<input type="submit" class="button button-primary" value="<?php esc_attr_e( '保存设置', 'wpbridge' ); ?>">
</p>
</form>

<hr>
<hr>

<h2><?php esc_html_e( '调试日志', 'wpbridge' ); ?></h2>
<h2><?php esc_html_e( '调试日志', 'wpbridge' ); ?></h2>

<?php if ( empty( $logs ) ) : ?>
<p><?php esc_html_e( '暂无日志', 'wpbridge' ); ?></p>
<?php else : ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width: 150px;"><?php esc_html_e( '时间', 'wpbridge' ); ?></th>
<th style="width: 80px;"><?php esc_html_e( '级别', 'wpbridge' ); ?></th>
<th><?php esc_html_e( '消息', 'wpbridge' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
<tr>
<td><?php echo esc_html( $log['time'] ); ?></td>
<td>
<span class="wpbridge-log-level wpbridge-log-<?php echo esc_attr( $log['level'] ); ?>">
<?php echo esc_html( strtoupper( $log['level'] ) ); ?>
</span>
</td>
<td>
<?php echo esc_html( $log['message'] ); ?>
<?php if ( ! empty( $log['context'] ) ) : ?>
<code><?php echo esc_html( wp_json_encode( $log['context'] ) ); ?></code>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php if ( empty( $logs ) ) : ?>
<p><?php esc_html_e( '暂无日志', 'wpbridge' ); ?></p>
<?php else : ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width: 150px;"><?php esc_html_e( '时间', 'wpbridge' ); ?></th>
<th style="width: 80px;"><?php esc_html_e( '级别', 'wpbridge' ); ?></th>
<th><?php esc_html_e( '消息', 'wpbridge' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
<tr>
<td><?php echo esc_html( $log['time'] ); ?></td>
<td>
<span class="wpbridge-log-level wpbridge-log-<?php echo esc_attr( $log['level'] ); ?>">
<?php echo esc_html( strtoupper( $log['level'] ) ); ?>
</span>
</td>
<td>
<?php echo esc_html( $log['message'] ); ?>
<?php if ( ! empty( $log['context'] ) ) : ?>
<code><?php echo esc_html( wp_json_encode( $log['context'] ) ); ?></code>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>

View file

@ -10,7 +10,7 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

$is_edit = null !== $source;
@ -19,195 +19,195 @@ $title = $is_edit ? __( '编辑更新源', 'wpbridge' ) : __( '添加更新源

<!-- 标题栏 -->
<header class="wpbridge-header">
<div class="wpbridge-header-left">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge' ) ); ?>" class="wpbridge-back-link">
<span class="dashicons dashicons-arrow-left-alt2"></span>
</a>
<h1 class="wpbridge-title"><?php echo esc_html( $title ); ?></h1>
</div>
<div class="wpbridge-header-left">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge' ) ); ?>" class="wpbridge-back-link">
<span class="dashicons dashicons-arrow-left-alt2"></span>
</a>
<h1 class="wpbridge-title"><?php echo esc_html( $title ); ?></h1>
</div>
</header>

<div class="wrap wpbridge-wrap">

<!-- 主内容区 -->
<div class="wpbridge-content">
<div class="wpbridge-tabs-card">
<div class="wpbridge-tab-pane wpbridge-tab-pane-active" style="padding: 24px;">
<?php settings_errors( 'wpbridge' ); ?>
<!-- 主内容区 -->
<div class="wpbridge-content">
<div class="wpbridge-tabs-card">
<div class="wpbridge-tab-pane wpbridge-tab-pane-active" style="padding: 24px;">
<?php settings_errors( 'wpbridge' ); ?>

<form method="post" class="wpbridge-editor-form">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="save_source">
<input type="hidden" name="source_id" value="<?php echo esc_attr( $source->id ?? '' ); ?>">
<form method="post" class="wpbridge-editor-form">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="save_source">
<input type="hidden" name="source_id" value="<?php echo esc_attr( $source->id ?? '' ); ?>">

<!-- 基本信息 -->
<div class="wpbridge-form-section">
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '基本信息', 'wpbridge' ); ?></h2>
<!-- 基本信息 -->
<div class="wpbridge-form-section">
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '基本信息', 'wpbridge' ); ?></h2>

<div class="wpbridge-form-row">
<label class="wpbridge-form-label">
<?php esc_html_e( '名称', 'wpbridge' ); ?>
<span class="required">*</span>
</label>
<div>
<input type="text"
name="name"
value="<?php echo esc_attr( $source->name ?? '' ); ?>"
class="wpbridge-form-input"
placeholder="<?php esc_attr_e( '例如:我的私有仓库', 'wpbridge' ); ?>"
required>
<p class="wpbridge-form-help"><?php esc_html_e( '更新源的显示名称', 'wpbridge' ); ?></p>
</div>
</div>
<div class="wpbridge-form-row">
<label class="wpbridge-form-label">
<?php esc_html_e( '名称', 'wpbridge' ); ?>
<span class="required">*</span>
</label>
<div>
<input type="text"
name="name"
value="<?php echo esc_attr( $source->name ?? '' ); ?>"
class="wpbridge-form-input"
placeholder="<?php esc_attr_e( '例如:我的私有仓库', 'wpbridge' ); ?>"
required>
<p class="wpbridge-form-help"><?php esc_html_e( '更新源的显示名称', 'wpbridge' ); ?></p>
</div>
</div>

<div class="wpbridge-form-row">
<label class="wpbridge-form-label">
<?php esc_html_e( '类型', 'wpbridge' ); ?>
<span class="required">*</span>
</label>
<div>
<select name="type" class="wpbridge-form-select">
<?php foreach ( $types as $type_value => $type_label ) : ?>
<option value="<?php echo esc_attr( $type_value ); ?>"
<?php selected( $source->type ?? 'json', $type_value ); ?>>
<?php echo esc_html( $type_label ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="wpbridge-form-help"><?php esc_html_e( '选择更新源的类型', 'wpbridge' ); ?></p>
</div>
</div>
<div class="wpbridge-form-row">
<label class="wpbridge-form-label">
<?php esc_html_e( '类型', 'wpbridge' ); ?>
<span class="required">*</span>
</label>
<div>
<select name="type" class="wpbridge-form-select">
<?php foreach ( $types as $type_value => $type_label ) : ?>
<option value="<?php echo esc_attr( $type_value ); ?>"
<?php selected( $source->type ?? 'json', $type_value ); ?>>
<?php echo esc_html( $type_label ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="wpbridge-form-help"><?php esc_html_e( '选择更新源的类型', 'wpbridge' ); ?></p>
</div>
</div>

<div class="wpbridge-form-row">
<label class="wpbridge-form-label">
<?php esc_html_e( '更新地址', 'wpbridge' ); ?>
<span class="required">*</span>
</label>
<div>
<input type="url"
name="api_url"
value="<?php echo esc_url( $source->api_url ?? '' ); ?>"
class="wpbridge-form-input"
style="max-width: 100%;"
placeholder="https://example.com/api/v1"
required>
<p class="wpbridge-form-help">
<?php esc_html_e( '更新源的地址。对于 JSON 类型,可以使用 {slug} 占位符。', 'wpbridge' ); ?>
</p>
</div>
</div>
</div>
<div class="wpbridge-form-row">
<label class="wpbridge-form-label">
<?php esc_html_e( '更新地址', 'wpbridge' ); ?>
<span class="required">*</span>
</label>
<div>
<input type="url"
name="api_url"
value="<?php echo esc_url( $source->api_url ?? '' ); ?>"
class="wpbridge-form-input"
style="max-width: 100%;"
placeholder="https://example.com/api/v1"
required>
<p class="wpbridge-form-help">
<?php esc_html_e( '更新源的地址。对于 JSON 类型,可以使用 {slug} 占位符。', 'wpbridge' ); ?>
</p>
</div>
</div>
</div>

<!-- 匹配规则 -->
<div class="wpbridge-form-section">
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '匹配规则', 'wpbridge' ); ?></h2>
<!-- 匹配规则 -->
<div class="wpbridge-form-section">
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '匹配规则', 'wpbridge' ); ?></h2>

<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '项目类型', 'wpbridge' ); ?></label>
<div>
<select name="item_type" class="wpbridge-form-select">
<option value="plugin" <?php selected( $source->item_type ?? 'plugin', 'plugin' ); ?>>
<?php esc_html_e( '插件', 'wpbridge' ); ?>
</option>
<option value="theme" <?php selected( $source->item_type ?? 'plugin', 'theme' ); ?>>
<?php esc_html_e( '主题', 'wpbridge' ); ?>
</option>
</select>
<p class="wpbridge-form-help"><?php esc_html_e( '此更新源用于插件还是主题', 'wpbridge' ); ?></p>
</div>
</div>
<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '项目类型', 'wpbridge' ); ?></label>
<div>
<select name="item_type" class="wpbridge-form-select">
<option value="plugin" <?php selected( $source->item_type ?? 'plugin', 'plugin' ); ?>>
<?php esc_html_e( '插件', 'wpbridge' ); ?>
</option>
<option value="theme" <?php selected( $source->item_type ?? 'plugin', 'theme' ); ?>>
<?php esc_html_e( '主题', 'wpbridge' ); ?>
</option>
</select>
<p class="wpbridge-form-help"><?php esc_html_e( '此更新源用于插件还是主题', 'wpbridge' ); ?></p>
</div>
</div>

<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '插件/主题标识', 'wpbridge' ); ?></label>
<div>
<input type="text"
name="slug"
value="<?php echo esc_attr( $source->slug ?? '' ); ?>"
class="wpbridge-form-input"
placeholder="<?php esc_attr_e( '留空匹配所有', 'wpbridge' ); ?>">
<p class="wpbridge-form-help">
<?php esc_html_e( '指定插件/主题的标识名称(通常是文件夹名)。留空表示匹配所有。', 'wpbridge' ); ?>
</p>
</div>
</div>
<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '插件/主题标识', 'wpbridge' ); ?></label>
<div>
<input type="text"
name="slug"
value="<?php echo esc_attr( $source->slug ?? '' ); ?>"
class="wpbridge-form-input"
placeholder="<?php esc_attr_e( '留空匹配所有', 'wpbridge' ); ?>">
<p class="wpbridge-form-help">
<?php esc_html_e( '指定插件/主题的标识名称(通常是文件夹名)。留空表示匹配所有。', 'wpbridge' ); ?>
</p>
</div>
</div>

<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '首选程度', 'wpbridge' ); ?></label>
<div>
<?php
$current_priority = $source->priority ?? 50;
// 将数字优先级映射到语义化选项
if ( $current_priority <= 20 ) {
$priority_level = 'primary';
} elseif ( $current_priority <= 60 ) {
$priority_level = 'secondary';
} else {
$priority_level = 'fallback';
}
?>
<select name="priority_level" class="wpbridge-form-select">
<option value="primary" <?php selected( $priority_level, 'primary' ); ?>>
<?php esc_html_e( '首选源 - 优先使用此源', 'wpbridge' ); ?>
</option>
<option value="secondary" <?php selected( $priority_level, 'secondary' ); ?>>
<?php esc_html_e( '备选源 - 首选不可用时使用', 'wpbridge' ); ?>
</option>
<option value="fallback" <?php selected( $priority_level, 'fallback' ); ?>>
<?php esc_html_e( '最后选择 - 其他源都不可用时使用', 'wpbridge' ); ?>
</option>
</select>
<p class="wpbridge-form-help">
<?php esc_html_e( '当多个源可用时,按此顺序尝试获取更新', 'wpbridge' ); ?>
</p>
</div>
</div>
</div>
<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '首选程度', 'wpbridge' ); ?></label>
<div>
<?php
$current_priority = $source->priority ?? 50;
// 将数字优先级映射到语义化选项
if ( $current_priority <= 20 ) {
$priority_level = 'primary';
} elseif ( $current_priority <= 60 ) {
$priority_level = 'secondary';
} else {
$priority_level = 'fallback';
}
?>
<select name="priority_level" class="wpbridge-form-select">
<option value="primary" <?php selected( $priority_level, 'primary' ); ?>>
<?php esc_html_e( '首选源 - 优先使用此源', 'wpbridge' ); ?>
</option>
<option value="secondary" <?php selected( $priority_level, 'secondary' ); ?>>
<?php esc_html_e( '备选源 - 首选不可用时使用', 'wpbridge' ); ?>
</option>
<option value="fallback" <?php selected( $priority_level, 'fallback' ); ?>>
<?php esc_html_e( '最后选择 - 其他源都不可用时使用', 'wpbridge' ); ?>
</option>
</select>
<p class="wpbridge-form-help">
<?php esc_html_e( '当多个源可用时,按此顺序尝试获取更新', 'wpbridge' ); ?>
</p>
</div>
</div>
</div>

<!-- 认证设置 -->
<div class="wpbridge-form-section">
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '认证设置', 'wpbridge' ); ?></h2>
<!-- 认证设置 -->
<div class="wpbridge-form-section">
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '认证设置', 'wpbridge' ); ?></h2>

<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
<div>
<input type="password"
name="auth_token"
value="<?php echo esc_attr( ! empty( $source->auth_token ) ? '********' : '' ); ?>"
class="wpbridge-form-input"
autocomplete="new-password"
placeholder="<?php echo esc_attr( ! empty( $source->auth_token ) ? __( '已设置(留空保持不变)', 'wpbridge' ) : __( '可选', 'wpbridge' ) ); ?>">
<p class="wpbridge-form-help">
<?php esc_html_e( '用于私有仓库或需要认证的更新源。留空表示无需认证。', 'wpbridge' ); ?>
</p>
</div>
</div>
<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
<div>
<input type="password"
name="auth_token"
value="<?php echo esc_attr( ! empty( $source->auth_token ) ? '********' : '' ); ?>"
class="wpbridge-form-input"
autocomplete="new-password"
placeholder="<?php echo esc_attr( ! empty( $source->auth_token ) ? __( '已设置(留空保持不变)', 'wpbridge' ) : __( '可选', 'wpbridge' ) ); ?>">
<p class="wpbridge-form-help">
<?php esc_html_e( '用于私有仓库或需要认证的更新源。留空表示无需认证。', 'wpbridge' ); ?>
</p>
</div>
</div>

<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '启用状态', 'wpbridge' ); ?></label>
<div>
<label class="wpbridge-toggle">
<input type="checkbox" name="enabled" value="1" <?php checked( $source->enabled ?? true ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
<p class="wpbridge-form-help" style="margin-top: 8px;">
<?php esc_html_e( '启用此更新源', 'wpbridge' ); ?>
</p>
</div>
</div>
</div>
<div class="wpbridge-form-row">
<label class="wpbridge-form-label"><?php esc_html_e( '启用状态', 'wpbridge' ); ?></label>
<div>
<label class="wpbridge-toggle">
<input type="checkbox" name="enabled" value="1" <?php checked( $source->enabled ?? true ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
<p class="wpbridge-form-help" style="margin-top: 8px;">
<?php esc_html_e( '启用此更新源', 'wpbridge' ); ?>
</p>
</div>
</div>
</div>

<!-- 操作按钮 -->
<div class="wpbridge-form-actions">
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
<span class="dashicons dashicons-saved"></span>
<?php echo esc_html( $is_edit ? __( '保存更改', 'wpbridge' ) : __( '添加更新源', 'wpbridge' ) ); ?>
</button>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge' ) ); ?>" class="wpbridge-btn wpbridge-btn-secondary">
<?php esc_html_e( '取消', 'wpbridge' ); ?>
</a>
</div>
</form>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="wpbridge-form-actions">
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
<span class="dashicons dashicons-saved"></span>
<?php echo esc_html( $is_edit ? __( '保存更改', 'wpbridge' ) : __( '添加更新源', 'wpbridge' ) ); ?>
</button>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge' ) ); ?>" class="wpbridge-btn wpbridge-btn-secondary">
<?php esc_html_e( '取消', 'wpbridge' ); ?>
</a>
</div>
</form>
</div>
</div>
</div>
</div>

View file

@ -9,121 +9,121 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

use WPBridge\UpdateSource\SourceType;
?>

<div class="wrap wpbridge-wrap">
<h1 class="wp-heading-inline"><?php esc_html_e( 'WPBridge 更新源', 'wpbridge' ); ?></h1>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="page-title-action">
<?php esc_html_e( '添加更新源', 'wpbridge' ); ?>
</a>
<hr class="wp-header-end">
<h1 class="wp-heading-inline"><?php esc_html_e( 'WPBridge 更新源', 'wpbridge' ); ?></h1>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="page-title-action">
<?php esc_html_e( '添加更新源', 'wpbridge' ); ?>
</a>
<hr class="wp-header-end">

<?php settings_errors( 'wpbridge' ); ?>
<?php settings_errors( 'wpbridge' ); ?>

<!-- 统计信息 -->
<div class="wpbridge-stats">
<div class="wpbridge-stat-item">
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['total'] ); ?></span>
<span class="wpbridge-stat-label"><?php esc_html_e( '总数', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-stat-item">
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['enabled'] ); ?></span>
<span class="wpbridge-stat-label"><?php esc_html_e( '已启用', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-stat-item">
<button type="button" class="button wpbridge-clear-cache">
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
</button>
</div>
</div>
<!-- 统计信息 -->
<div class="wpbridge-stats">
<div class="wpbridge-stat-item">
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['total'] ); ?></span>
<span class="wpbridge-stat-label"><?php esc_html_e( '总数', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-stat-item">
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['enabled'] ); ?></span>
<span class="wpbridge-stat-label"><?php esc_html_e( '已启用', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-stat-item">
<button type="button" class="button wpbridge-clear-cache">
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
</button>
</div>
</div>

<!-- 源列表 -->
<table class="wp-list-table widefat fixed striped wpbridge-sources-table">
<thead>
<tr>
<th class="column-status"><?php esc_html_e( '状态', 'wpbridge' ); ?></th>
<th class="column-name"><?php esc_html_e( '名称', 'wpbridge' ); ?></th>
<th class="column-type"><?php esc_html_e( '类型', 'wpbridge' ); ?></th>
<th class="column-slug"><?php esc_html_e( 'Slug', 'wpbridge' ); ?></th>
<th class="column-priority"><?php esc_html_e( '优先级', 'wpbridge' ); ?></th>
<th class="column-actions"><?php esc_html_e( '操作', 'wpbridge' ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $sources ) ) : ?>
<tr>
<td colspan="6" class="wpbridge-no-items">
<?php esc_html_e( '暂无更新源', 'wpbridge' ); ?>
</td>
</tr>
<?php else : ?>
<?php foreach ( $sources as $source ) : ?>
<tr data-source-id="<?php echo esc_attr( $source->id ); ?>">
<td class="column-status">
<label class="wpbridge-toggle">
<input type="checkbox"
class="wpbridge-toggle-source"
<?php checked( $source->enabled ); ?>
data-source-id="<?php echo esc_attr( $source->id ); ?>">
<span class="wpbridge-toggle-slider"></span>
</label>
</td>
<td class="column-name">
<strong>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>">
<?php echo esc_html( $source->name ?: $source->id ); ?>
</a>
</strong>
<?php if ( $source->is_preset ) : ?>
<span class="wpbridge-badge wpbridge-badge-preset"><?php esc_html_e( '预置', 'wpbridge' ); ?></span>
<?php endif; ?>
<div class="wpbridge-source-url">
<?php echo esc_html( $source->api_url ); ?>
</div>
</td>
<td class="column-type">
<span class="wpbridge-type-badge wpbridge-type-<?php echo esc_attr( $source->type ); ?>">
<?php echo esc_html( SourceType::get_label( $source->type ) ); ?>
</span>
</td>
<td class="column-slug">
<?php echo esc_html( $source->slug ?: __( '全部', 'wpbridge' ) ); ?>
</td>
<td class="column-priority">
<?php echo esc_html( $source->priority ); ?>
</td>
<td class="column-actions">
<button type="button"
class="button button-small wpbridge-test-source"
data-source-id="<?php echo esc_attr( $source->id ); ?>">
<?php esc_html_e( '测试', 'wpbridge' ); ?>
</button>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>"
class="button button-small">
<?php esc_html_e( '编辑', 'wpbridge' ); ?>
</a>
<?php if ( ! $source->is_preset ) : ?>
<button type="button"
class="button button-small button-link-delete wpbridge-delete-source"
data-source-id="<?php echo esc_attr( $source->id ); ?>">
<?php esc_html_e( '删除', 'wpbridge' ); ?>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<!-- 源列表 -->
<table class="wp-list-table widefat fixed striped wpbridge-sources-table">
<thead>
<tr>
<th class="column-status"><?php esc_html_e( '状态', 'wpbridge' ); ?></th>
<th class="column-name"><?php esc_html_e( '名称', 'wpbridge' ); ?></th>
<th class="column-type"><?php esc_html_e( '类型', 'wpbridge' ); ?></th>
<th class="column-slug"><?php esc_html_e( 'Slug', 'wpbridge' ); ?></th>
<th class="column-priority"><?php esc_html_e( '优先级', 'wpbridge' ); ?></th>
<th class="column-actions"><?php esc_html_e( '操作', 'wpbridge' ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $sources ) ) : ?>
<tr>
<td colspan="6" class="wpbridge-no-items">
<?php esc_html_e( '暂无更新源', 'wpbridge' ); ?>
</td>
</tr>
<?php else : ?>
<?php foreach ( $sources as $source ) : ?>
<tr data-source-id="<?php echo esc_attr( $source->id ); ?>">
<td class="column-status">
<label class="wpbridge-toggle">
<input type="checkbox"
class="wpbridge-toggle-source"
<?php checked( $source->enabled ); ?>
data-source-id="<?php echo esc_attr( $source->id ); ?>">
<span class="wpbridge-toggle-slider"></span>
</label>
</td>
<td class="column-name">
<strong>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>">
<?php echo esc_html( $source->name ?: $source->id ); ?>
</a>
</strong>
<?php if ( $source->is_preset ) : ?>
<span class="wpbridge-badge wpbridge-badge-preset"><?php esc_html_e( '预置', 'wpbridge' ); ?></span>
<?php endif; ?>
<div class="wpbridge-source-url">
<?php echo esc_html( $source->api_url ); ?>
</div>
</td>
<td class="column-type">
<span class="wpbridge-type-badge wpbridge-type-<?php echo esc_attr( $source->type ); ?>">
<?php echo esc_html( SourceType::get_label( $source->type ) ); ?>
</span>
</td>
<td class="column-slug">
<?php echo esc_html( $source->slug ?: __( '全部', 'wpbridge' ) ); ?>
</td>
<td class="column-priority">
<?php echo esc_html( $source->priority ); ?>
</td>
<td class="column-actions">
<button type="button"
class="button button-small wpbridge-test-source"
data-source-id="<?php echo esc_attr( $source->id ); ?>">
<?php esc_html_e( '测试', 'wpbridge' ); ?>
</button>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>"
class="button button-small">
<?php esc_html_e( '编辑', 'wpbridge' ); ?>
</a>
<?php if ( ! $source->is_preset ) : ?>
<button type="button"
class="button button-small button-link-delete wpbridge-delete-source"
data-source-id="<?php echo esc_attr( $source->id ); ?>">
<?php esc_html_e( '删除', 'wpbridge' ); ?>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>

<!-- 删除确认表单 -->
<form id="wpbridge-delete-form" method="post" style="display: none;">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="delete_source">
<input type="hidden" name="source_id" id="wpbridge-delete-source-id" value="">
</form>
<!-- 删除确认表单 -->
<form id="wpbridge-delete-form" method="post" style="display: none;">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="delete_source">
<input type="hidden" name="source_id" id="wpbridge-delete-source-id" value="">
</form>
</div>

View file

@ -9,182 +9,182 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

$api_settings = $settings['api'] ?? array();
$api_settings = $settings['api'] ?? [];
$api_enabled = ! empty( $api_settings['enabled'] );
$require_auth = ! empty( $api_settings['require_auth'] );
$rate_limit = $api_settings['rate_limit'] ?? 100;
$api_keys = $api_settings['keys'] ?? array();
$api_keys = $api_settings['keys'] ?? [];
?>

<form method="post" class="wpbridge-api-form">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="save_api_settings">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="save_api_settings">

<!-- API 状态面板 -->
<div class="wpbridge-stats-panel" style="margin-bottom: 24px;">
<div class="wpbridge-stat-card">
<div class="wpbridge-stat-card-header">
<span class="dashicons dashicons-rest-api"></span>
<?php esc_html_e( 'API 状态', 'wpbridge' ); ?>
</div>
<div class="wpbridge-stat-value <?php echo $api_enabled ? 'success' : ''; ?>">
<?php echo $api_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?>
</div>
</div>
<div class="wpbridge-stat-card">
<div class="wpbridge-stat-card-header">
<span class="dashicons dashicons-admin-network"></span>
<?php esc_html_e( 'API 端点', 'wpbridge' ); ?>
</div>
<div style="font-size: 12px; font-family: var(--wpbridge-font-mono); color: var(--wpbridge-gray-600); word-break: break-all;">
<?php echo esc_html( rest_url( 'bridge/v1/' ) ); ?>
</div>
</div>
<div class="wpbridge-stat-card">
<div class="wpbridge-stat-card-header">
<span class="dashicons dashicons-admin-users"></span>
<?php esc_html_e( 'API Keys', 'wpbridge' ); ?>
</div>
<div class="wpbridge-stat-value"><?php echo count( $api_keys ); ?></div>
</div>
</div>
<!-- API 状态面板 -->
<div class="wpbridge-stats-panel" style="margin-bottom: 24px;">
<div class="wpbridge-stat-card">
<div class="wpbridge-stat-card-header">
<span class="dashicons dashicons-rest-api"></span>
<?php esc_html_e( 'API 状态', 'wpbridge' ); ?>
</div>
<div class="wpbridge-stat-value <?php echo $api_enabled ? 'success' : ''; ?>">
<?php echo $api_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?>
</div>
</div>
<div class="wpbridge-stat-card">
<div class="wpbridge-stat-card-header">
<span class="dashicons dashicons-admin-network"></span>
<?php esc_html_e( 'API 端点', 'wpbridge' ); ?>
</div>
<div style="font-size: 12px; font-family: var(--wpbridge-font-mono); color: var(--wpbridge-gray-600); word-break: break-all;">
<?php echo esc_html( rest_url( 'bridge/v1/' ) ); ?>
</div>
</div>
<div class="wpbridge-stat-card">
<div class="wpbridge-stat-card-header">
<span class="dashicons dashicons-admin-users"></span>
<?php esc_html_e( 'API Keys', 'wpbridge' ); ?>
</div>
<div class="wpbridge-stat-value"><?php echo count( $api_keys ); ?></div>
</div>
</div>

<div class="wpbridge-settings-panel">
<!-- 启用 API -->
<div class="wpbridge-settings-row">
<div class="wpbridge-settings-info">
<h3 class="wpbridge-settings-title"><?php esc_html_e( '启用 Bridge API', 'wpbridge' ); ?></h3>
<p class="wpbridge-settings-desc"><?php esc_html_e( '允许通过 REST API 远程访问 WPBridge 功能。', 'wpbridge' ); ?></p>
</div>
<label class="wpbridge-toggle">
<input type="checkbox" name="api_enabled" value="1" <?php checked( $api_enabled ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>
<div class="wpbridge-settings-panel">
<!-- 启用 API -->
<div class="wpbridge-settings-row">
<div class="wpbridge-settings-info">
<h3 class="wpbridge-settings-title"><?php esc_html_e( '启用 Bridge API', 'wpbridge' ); ?></h3>
<p class="wpbridge-settings-desc"><?php esc_html_e( '允许通过 REST API 远程访问 WPBridge 功能。', 'wpbridge' ); ?></p>
</div>
<label class="wpbridge-toggle">
<input type="checkbox" name="api_enabled" value="1" <?php checked( $api_enabled ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>

<!-- 需要认证 -->
<div class="wpbridge-settings-row">
<div class="wpbridge-settings-info">
<h3 class="wpbridge-settings-title"><?php esc_html_e( '需要 API Key 认证', 'wpbridge' ); ?></h3>
<p class="wpbridge-settings-desc"><?php esc_html_e( '启用后,所有 API 请求都需要提供有效的 API Key。', 'wpbridge' ); ?></p>
</div>
<label class="wpbridge-toggle">
<input type="checkbox" name="require_auth" value="1" <?php checked( $require_auth ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>
<!-- 需要认证 -->
<div class="wpbridge-settings-row">
<div class="wpbridge-settings-info">
<h3 class="wpbridge-settings-title"><?php esc_html_e( '需要 API Key 认证', 'wpbridge' ); ?></h3>
<p class="wpbridge-settings-desc"><?php esc_html_e( '启用后,所有 API 请求都需要提供有效的 API Key。', 'wpbridge' ); ?></p>
</div>
<label class="wpbridge-toggle">
<input type="checkbox" name="require_auth" value="1" <?php checked( $require_auth ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>

<!-- 速率限制 -->
<div class="wpbridge-settings-row">
<div class="wpbridge-settings-info">
<h3 class="wpbridge-settings-title"><?php esc_html_e( '速率限制', 'wpbridge' ); ?></h3>
<p class="wpbridge-settings-desc"><?php esc_html_e( '每个 IP 每分钟允许的最大请求数。', 'wpbridge' ); ?></p>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<input type="number"
name="rate_limit"
value="<?php echo esc_attr( $rate_limit ); ?>"
min="10"
max="10000"
class="wpbridge-form-input"
style="max-width: 100px;">
<span style="color: var(--wpbridge-gray-500);"><?php esc_html_e( '次/分钟', 'wpbridge' ); ?></span>
</div>
</div>
</div>
<!-- 速率限制 -->
<div class="wpbridge-settings-row">
<div class="wpbridge-settings-info">
<h3 class="wpbridge-settings-title"><?php esc_html_e( '速率限制', 'wpbridge' ); ?></h3>
<p class="wpbridge-settings-desc"><?php esc_html_e( '每个 IP 每分钟允许的最大请求数。', 'wpbridge' ); ?></p>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<input type="number"
name="rate_limit"
value="<?php echo esc_attr( $rate_limit ); ?>"
min="10"
max="10000"
class="wpbridge-form-input"
style="max-width: 100px;">
<span style="color: var(--wpbridge-gray-500);"><?php esc_html_e( '次/分钟', 'wpbridge' ); ?></span>
</div>
</div>
</div>

<div class="wpbridge-form-actions" style="margin-top: 24px;">
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
<span class="dashicons dashicons-saved"></span>
<?php esc_html_e( '保存设置', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-form-actions" style="margin-top: 24px;">
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
<span class="dashicons dashicons-saved"></span>
<?php esc_html_e( '保存设置', 'wpbridge' ); ?>
</button>
</div>
</form>

<!-- API Keys 管理 -->
<div class="wpbridge-settings-panel" style="margin-top: 24px;">
<div class="wpbridge-sources-header" style="margin-bottom: 16px;">
<h2 class="wpbridge-sources-title"><?php esc_html_e( 'API Keys', 'wpbridge' ); ?></h2>
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-generate-api-key">
<span class="dashicons dashicons-plus-alt2"></span>
<?php esc_html_e( '生成新 Key', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-sources-header" style="margin-bottom: 16px;">
<h2 class="wpbridge-sources-title"><?php esc_html_e( 'API Keys', 'wpbridge' ); ?></h2>
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-generate-api-key">
<span class="dashicons dashicons-plus-alt2"></span>
<?php esc_html_e( '生成新 Key', 'wpbridge' ); ?>
</button>
</div>

<?php if ( empty( $api_keys ) ) : ?>
<div class="wpbridge-empty" style="padding: 32px;">
<span class="dashicons dashicons-admin-network"></span>
<h3 class="wpbridge-empty-title"><?php esc_html_e( '暂无 API Key', 'wpbridge' ); ?></h3>
<p class="wpbridge-empty-desc"><?php esc_html_e( '生成 API Key 以允许远程访问。', 'wpbridge' ); ?></p>
</div>
<?php else : ?>
<div class="wpbridge-api-keys-list">
<?php foreach ( $api_keys as $key_data ) : ?>
<div class="wpbridge-settings-row" data-key-id="<?php echo esc_attr( $key_data['id'] ?? '' ); ?>">
<div class="wpbridge-settings-info">
<h3 class="wpbridge-settings-title">
<?php echo esc_html( $key_data['name'] ?? __( '未命名', 'wpbridge' ) ); ?>
<?php if ( ! empty( $key_data['key_prefix'] ) ) : ?>
<code style="margin-left: 8px; font-size: 11px; color: var(--wpbridge-gray-500);">
<?php echo esc_html( $key_data['key_prefix'] ); ?>
</code>
<?php endif; ?>
</h3>
<p class="wpbridge-settings-desc">
<?php
$created_at = $key_data['created_at'] ?? '';
if ( ! empty( $created_at ) ) {
// 支持 MySQL 格式和 Unix 时间戳
$timestamp = is_numeric( $created_at ) ? $created_at : strtotime( $created_at );
/* translators: %s: date */
printf(
esc_html__( '创建于 %s', 'wpbridge' ),
esc_html( date_i18n( get_option( 'date_format' ), $timestamp ) )
);
}
$last_used = $key_data['last_used'] ?? null;
if ( ! empty( $last_used ) ) {
$last_timestamp = is_numeric( $last_used ) ? $last_used : strtotime( $last_used );
/* translators: %s: relative time */
printf(
' &middot; ' . esc_html__( '最后使用 %s', 'wpbridge' ),
esc_html( human_time_diff( $last_timestamp, time() ) . __( '前', 'wpbridge' ) )
);
}
?>
</p>
</div>
<button type="button"
class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-revoke-api-key"
data-key-id="<?php echo esc_attr( $key_data['id'] ?? '' ); ?>">
<span class="dashicons dashicons-no"></span>
<?php esc_html_e( '撤销', 'wpbridge' ); ?>
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ( empty( $api_keys ) ) : ?>
<div class="wpbridge-empty" style="padding: 32px;">
<span class="dashicons dashicons-admin-network"></span>
<h3 class="wpbridge-empty-title"><?php esc_html_e( '暂无 API Key', 'wpbridge' ); ?></h3>
<p class="wpbridge-empty-desc"><?php esc_html_e( '生成 API Key 以允许远程访问。', 'wpbridge' ); ?></p>
</div>
<?php else : ?>
<div class="wpbridge-api-keys-list">
<?php foreach ( $api_keys as $key_data ) : ?>
<div class="wpbridge-settings-row" data-key-id="<?php echo esc_attr( $key_data['id'] ?? '' ); ?>">
<div class="wpbridge-settings-info">
<h3 class="wpbridge-settings-title">
<?php echo esc_html( $key_data['name'] ?? __( '未命名', 'wpbridge' ) ); ?>
<?php if ( ! empty( $key_data['key_prefix'] ) ) : ?>
<code style="margin-left: 8px; font-size: 11px; color: var(--wpbridge-gray-500);">
<?php echo esc_html( $key_data['key_prefix'] ); ?>
</code>
<?php endif; ?>
</h3>
<p class="wpbridge-settings-desc">
<?php
$created_at = $key_data['created_at'] ?? '';
if ( ! empty( $created_at ) ) {
// 支持 MySQL 格式和 Unix 时间戳
$timestamp = is_numeric( $created_at ) ? $created_at : strtotime( $created_at );
/* translators: %s: date */
printf(
esc_html__( '创建于 %s', 'wpbridge' ),
esc_html( date_i18n( get_option( 'date_format' ), $timestamp ) )
);
}
$last_used = $key_data['last_used'] ?? null;
if ( ! empty( $last_used ) ) {
$last_timestamp = is_numeric( $last_used ) ? $last_used : strtotime( $last_used );
/* translators: %s: relative time */
printf(
' &middot; ' . esc_html__( '最后使用 %s', 'wpbridge' ),
esc_html( human_time_diff( $last_timestamp, time() ) . __( '前', 'wpbridge' ) )
);
}
?>
</p>
</div>
<button type="button"
class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-revoke-api-key"
data-key-id="<?php echo esc_attr( $key_data['id'] ?? '' ); ?>">
<span class="dashicons dashicons-no"></span>
<?php esc_html_e( '撤销', 'wpbridge' ); ?>
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>

<!-- API 文档 -->
<div class="wpbridge-settings-panel" style="margin-top: 24px;">
<h2 class="wpbridge-sources-title" style="margin-bottom: 16px;"><?php esc_html_e( 'API 文档', 'wpbridge' ); ?></h2>
<h2 class="wpbridge-sources-title" style="margin-bottom: 16px;"><?php esc_html_e( 'API 文档', 'wpbridge' ); ?></h2>

<div style="font-size: 13px; color: var(--wpbridge-gray-600);">
<p><strong><?php esc_html_e( '认证方式', 'wpbridge' ); ?></strong></p>
<p><?php esc_html_e( '在请求头中添加 API Key', 'wpbridge' ); ?></p>
<code style="display: block; padding: 12px; background: var(--wpbridge-gray-100); margin: 8px 0;">X-WPBridge-API-Key: your_api_key</code>
<div style="font-size: 13px; color: var(--wpbridge-gray-600);">
<p><strong><?php esc_html_e( '认证方式', 'wpbridge' ); ?></strong></p>
<p><?php esc_html_e( '在请求头中添加 API Key', 'wpbridge' ); ?></p>
<code style="display: block; padding: 12px; background: var(--wpbridge-gray-100); margin: 8px 0;">X-WPBridge-API-Key: your_api_key</code>

<p style="margin-top: 16px;"><strong><?php esc_html_e( '可用端点', 'wpbridge' ); ?></strong></p>
<ul style="margin: 8px 0; padding-left: 20px;">
<li><code>GET /wp-json/bridge/v1/status</code> - <?php esc_html_e( 'API 状态', 'wpbridge' ); ?></li>
<li><code>GET /wp-json/bridge/v1/sources</code> - <?php esc_html_e( '获取更新源列表', 'wpbridge' ); ?></li>
<li><code>GET /wp-json/bridge/v1/check/{source_id}</code> - <?php esc_html_e( '检查更新源状态', 'wpbridge' ); ?></li>
<li><code>GET /wp-json/bridge/v1/plugins/{slug}/info</code> - <?php esc_html_e( '获取插件信息', 'wpbridge' ); ?></li>
<li><code>GET /wp-json/bridge/v1/themes/{slug}/info</code> - <?php esc_html_e( '获取主题信息', 'wpbridge' ); ?></li>
</ul>
</div>
<p style="margin-top: 16px;"><strong><?php esc_html_e( '可用端点', 'wpbridge' ); ?></strong></p>
<ul style="margin: 8px 0; padding-left: 20px;">
<li><code>GET /wp-json/bridge/v1/status</code> - <?php esc_html_e( 'API 状态', 'wpbridge' ); ?></li>
<li><code>GET /wp-json/bridge/v1/sources</code> - <?php esc_html_e( '获取更新源列表', 'wpbridge' ); ?></li>
<li><code>GET /wp-json/bridge/v1/check/{source_id}</code> - <?php esc_html_e( '检查更新源状态', 'wpbridge' ); ?></li>
<li><code>GET /wp-json/bridge/v1/plugins/{slug}/info</code> - <?php esc_html_e( '获取插件信息', 'wpbridge' ); ?></li>
<li><code>GET /wp-json/bridge/v1/themes/{slug}/info</code> - <?php esc_html_e( '获取主题信息', 'wpbridge' ); ?></li>
</ul>
</div>
</div>

View file

@ -12,7 +12,7 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

use WPBridge\UpdateSource\SourceType;
@ -20,340 +20,340 @@ use WPBridge\UpdateSource\SourceType;

<!-- 诊断工具头部 -->
<div class="wpbridge-diagnostics-header">
<div class="wpbridge-diagnostics-info">
<h2><?php esc_html_e( '诊断工具', 'wpbridge' ); ?></h2>
<p><?php esc_html_e( '检查更新源连通性、系统环境和配置状态', 'wpbridge' ); ?></p>
</div>
<div class="wpbridge-diagnostics-actions">
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-run-all-diagnostics">
<span class="dashicons dashicons-controls-play"></span>
<?php esc_html_e( '运行全部诊断', 'wpbridge' ); ?>
</button>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-export-diagnostics">
<span class="dashicons dashicons-download"></span>
<?php esc_html_e( '导出报告', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-diagnostics-info">
<h2><?php esc_html_e( '诊断工具', 'wpbridge' ); ?></h2>
<p><?php esc_html_e( '检查更新源连通性、系统环境和配置状态', 'wpbridge' ); ?></p>
</div>
<div class="wpbridge-diagnostics-actions">
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-run-all-diagnostics">
<span class="dashicons dashicons-controls-play"></span>
<?php esc_html_e( '运行全部诊断', 'wpbridge' ); ?>
</button>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-export-diagnostics">
<span class="dashicons dashicons-download"></span>
<?php esc_html_e( '导出报告', 'wpbridge' ); ?>
</button>
</div>
</div>

<!-- 诊断结果概览 -->
<div class="wpbridge-diagnostics-summary" style="display: none;" aria-live="polite" aria-atomic="true">
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-passed">
<span class="dashicons dashicons-yes-alt"></span>
<span class="wpbridge-diagnostics-count">0</span>
<span><?php esc_html_e( '通过', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-warnings">
<span class="dashicons dashicons-warning"></span>
<span class="wpbridge-diagnostics-count">0</span>
<span><?php esc_html_e( '警告', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-failed">
<span class="dashicons dashicons-dismiss"></span>
<span class="wpbridge-diagnostics-count">0</span>
<span><?php esc_html_e( '失败', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-passed">
<span class="dashicons dashicons-yes-alt"></span>
<span class="wpbridge-diagnostics-count">0</span>
<span><?php esc_html_e( '通过', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-warnings">
<span class="dashicons dashicons-warning"></span>
<span class="wpbridge-diagnostics-count">0</span>
<span><?php esc_html_e( '警告', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-failed">
<span class="dashicons dashicons-dismiss"></span>
<span class="wpbridge-diagnostics-count">0</span>
<span><?php esc_html_e( '失败', 'wpbridge' ); ?></span>
</div>
</div>

<!-- 更新源连通性测试 -->
<div class="wpbridge-diagnostics-section">
<div class="wpbridge-diagnostics-section-header">
<h3>
<span class="dashicons dashicons-cloud"></span>
<?php esc_html_e( '更新源连通性', 'wpbridge' ); ?>
</h3>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-all-sources">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '测试全部', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-diagnostics-section-header">
<h3>
<span class="dashicons dashicons-cloud"></span>
<?php esc_html_e( '更新源连通性', 'wpbridge' ); ?>
</h3>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-all-sources">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '测试全部', 'wpbridge' ); ?>
</button>
</div>

<?php if ( empty( $sources ) ) : ?>
<div class="wpbridge-diagnostics-empty">
<span class="dashicons dashicons-info-outline"></span>
<p><?php esc_html_e( '暂无更新源,请先添加更新源', 'wpbridge' ); ?></p>
</div>
<?php else : ?>
<div class="wpbridge-source-tests">
<?php foreach ( $sources as $source ) : ?>
<div class="wpbridge-source-test-item" data-source-id="<?php echo esc_attr( $source->id ); ?>">
<div class="wpbridge-source-test-info">
<div class="wpbridge-source-test-name">
<?php echo esc_html( $source->name ?: $source->id ); ?>
<?php if ( $source->is_preset ) : ?>
<span class="wpbridge-badge wpbridge-badge-preset"><?php esc_html_e( '预置', 'wpbridge' ); ?></span>
<?php endif; ?>
</div>
<div class="wpbridge-source-test-url"><?php echo esc_html( $source->api_url ); ?></div>
</div>
<div class="wpbridge-source-test-meta">
<span class="wpbridge-badge wpbridge-badge-type <?php echo esc_attr( $source->type ); ?>">
<?php echo esc_html( SourceType::get_label( $source->type ) ); ?>
</span>
<span class="wpbridge-source-test-status">
<?php if ( ! $source->enabled ) : ?>
<span class="wpbridge-badge wpbridge-badge-disabled"><?php esc_html_e( '已禁用', 'wpbridge' ); ?></span>
<?php elseif ( isset( $health_status[ $source->id ] ) && is_array( $health_status[ $source->id ] ) ) : ?>
<?php
$status = $health_status[ $source->id ];
$status_class = $status['status'] ?? 'unknown';
$status_labels = array(
'healthy' => __( '正常', 'wpbridge' ),
'degraded' => __( '降级', 'wpbridge' ),
'failed' => __( '失败', 'wpbridge' ),
);
?>
<span class="wpbridge-badge wpbridge-badge-status <?php echo esc_attr( $status_class ); ?>">
<?php echo esc_html( $status_labels[ $status_class ] ?? $status_class ); ?>
</span>
<?php if ( ! empty( $status['response_time'] ) ) : ?>
<span class="wpbridge-source-test-time"><?php echo esc_html( $status['response_time'] ); ?>ms</span>
<?php endif; ?>
<?php else : ?>
<span class="wpbridge-badge wpbridge-badge-unknown"><?php esc_html_e( '未测试', 'wpbridge' ); ?></span>
<?php endif; ?>
</span>
</div>
<div class="wpbridge-source-test-actions">
<button type="button"
class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-single-source"
data-source-id="<?php echo esc_attr( $source->id ); ?>"
<?php disabled( ! $source->enabled ); ?>>
<span class="dashicons dashicons-admin-site-alt3"></span>
<?php esc_html_e( '测试', 'wpbridge' ); ?>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ( empty( $sources ) ) : ?>
<div class="wpbridge-diagnostics-empty">
<span class="dashicons dashicons-info-outline"></span>
<p><?php esc_html_e( '暂无更新源,请先添加更新源', 'wpbridge' ); ?></p>
</div>
<?php else : ?>
<div class="wpbridge-source-tests">
<?php foreach ( $sources as $source ) : ?>
<div class="wpbridge-source-test-item" data-source-id="<?php echo esc_attr( $source->id ); ?>">
<div class="wpbridge-source-test-info">
<div class="wpbridge-source-test-name">
<?php echo esc_html( $source->name ?: $source->id ); ?>
<?php if ( $source->is_preset ) : ?>
<span class="wpbridge-badge wpbridge-badge-preset"><?php esc_html_e( '预置', 'wpbridge' ); ?></span>
<?php endif; ?>
</div>
<div class="wpbridge-source-test-url"><?php echo esc_html( $source->api_url ); ?></div>
</div>
<div class="wpbridge-source-test-meta">
<span class="wpbridge-badge wpbridge-badge-type <?php echo esc_attr( $source->type ); ?>">
<?php echo esc_html( SourceType::get_label( $source->type ) ); ?>
</span>
<span class="wpbridge-source-test-status">
<?php if ( ! $source->enabled ) : ?>
<span class="wpbridge-badge wpbridge-badge-disabled"><?php esc_html_e( '已禁用', 'wpbridge' ); ?></span>
<?php elseif ( isset( $health_status[ $source->id ] ) && is_array( $health_status[ $source->id ] ) ) : ?>
<?php
$status = $health_status[ $source->id ];
$status_class = $status['status'] ?? 'unknown';
$status_labels = array(
'healthy' => __( '正常', 'wpbridge' ),
'degraded' => __( '降级', 'wpbridge' ),
'failed' => __( '失败', 'wpbridge' ),
);
?>
<span class="wpbridge-badge wpbridge-badge-status <?php echo esc_attr( $status_class ); ?>">
<?php echo esc_html( $status_labels[ $status_class ] ?? $status_class ); ?>
</span>
<?php if ( ! empty( $status['response_time'] ) ) : ?>
<span class="wpbridge-source-test-time"><?php echo esc_html( $status['response_time'] ); ?>ms</span>
<?php endif; ?>
<?php else : ?>
<span class="wpbridge-badge wpbridge-badge-unknown"><?php esc_html_e( '未测试', 'wpbridge' ); ?></span>
<?php endif; ?>
</span>
</div>
<div class="wpbridge-source-test-actions">
<button type="button"
class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-single-source"
data-source-id="<?php echo esc_attr( $source->id ); ?>"
<?php disabled( ! $source->enabled ); ?>>
<span class="dashicons dashicons-admin-site-alt3"></span>
<?php esc_html_e( '测试', 'wpbridge' ); ?>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>

<!-- 系统环境检查 -->
<div class="wpbridge-diagnostics-section">
<div class="wpbridge-diagnostics-section-header">
<h3>
<span class="dashicons dashicons-admin-tools"></span>
<?php esc_html_e( '系统环境', 'wpbridge' ); ?>
</h3>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-check-environment">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '检查', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-diagnostics-section-header">
<h3>
<span class="dashicons dashicons-admin-tools"></span>
<?php esc_html_e( '系统环境', 'wpbridge' ); ?>
</h3>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-check-environment">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '检查', 'wpbridge' ); ?>
</button>
</div>

<div class="wpbridge-environment-checks">
<?php
// PHP 版本检查
$php_version = PHP_VERSION;
$php_ok = version_compare( $php_version, '7.4', '>=' );
?>
<div class="wpbridge-check-item <?php echo $php_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $php_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'PHP 版本', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $php_version ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '要求 >= 7.4', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-environment-checks">
<?php
// PHP 版本检查
$php_version = PHP_VERSION;
$php_ok = version_compare( $php_version, '7.4', '>=' );
?>
<div class="wpbridge-check-item <?php echo $php_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $php_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'PHP 版本', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $php_version ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '要求 >= 7.4', 'wpbridge' ); ?></span>
</div>

<?php
// WordPress 版本检查
$wp_version = get_bloginfo( 'version' );
$wp_ok = version_compare( $wp_version, '5.9', '>=' );
?>
<div class="wpbridge-check-item <?php echo $wp_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $wp_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'WordPress 版本', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $wp_version ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '要求 >= 5.9', 'wpbridge' ); ?></span>
</div>
<?php
// WordPress 版本检查
$wp_version = get_bloginfo( 'version' );
$wp_ok = version_compare( $wp_version, '5.9', '>=' );
?>
<div class="wpbridge-check-item <?php echo $wp_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $wp_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'WordPress 版本', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $wp_version ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '要求 >= 5.9', 'wpbridge' ); ?></span>
</div>

<?php
// cURL 扩展检查
$curl_ok = function_exists( 'curl_version' );
$curl_version = $curl_ok ? curl_version()['version'] : __( '未安装', 'wpbridge' );
?>
<div class="wpbridge-check-item <?php echo $curl_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $curl_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'cURL 扩展', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $curl_version ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
</div>
<?php
// cURL 扩展检查
$curl_ok = function_exists( 'curl_version' );
$curl_version = $curl_ok ? curl_version()['version'] : __( '未安装', 'wpbridge' );
?>
<div class="wpbridge-check-item <?php echo $curl_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $curl_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'cURL 扩展', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $curl_version ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
</div>

<?php
// OpenSSL 扩展检查
$openssl_ok = extension_loaded( 'openssl' );
$openssl_version = $openssl_ok ? OPENSSL_VERSION_TEXT : __( '未安装', 'wpbridge' );
?>
<div class="wpbridge-check-item <?php echo $openssl_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $openssl_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'OpenSSL 扩展', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $openssl_version ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
</div>
<?php
// OpenSSL 扩展检查
$openssl_ok = extension_loaded( 'openssl' );
$openssl_version = $openssl_ok ? OPENSSL_VERSION_TEXT : __( '未安装', 'wpbridge' );
?>
<div class="wpbridge-check-item <?php echo $openssl_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $openssl_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'OpenSSL 扩展', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $openssl_version ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
</div>

<?php
// JSON 扩展检查
$json_ok = function_exists( 'json_encode' );
?>
<div class="wpbridge-check-item <?php echo $json_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $json_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'JSON 扩展', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo $json_ok ? esc_html__( '已安装', 'wpbridge' ) : esc_html__( '未安装', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
</div>
<?php
// JSON 扩展检查
$json_ok = function_exists( 'json_encode' );
?>
<div class="wpbridge-check-item <?php echo $json_ok ? 'passed' : 'failed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $json_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( 'JSON 扩展', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo $json_ok ? esc_html__( '已安装', 'wpbridge' ) : esc_html__( '未安装', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
</div>

<?php
// 外部 HTTP 请求检查
$http_ok = ! defined( 'WP_HTTP_BLOCK_EXTERNAL' ) || ! WP_HTTP_BLOCK_EXTERNAL;
?>
<div class="wpbridge-check-item <?php echo $http_ok ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $http_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '外部 HTTP 请求', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo $http_ok ? esc_html__( '允许', 'wpbridge' ) : esc_html__( '已阻止', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐允许', 'wpbridge' ); ?></span>
</div>
<?php
// 外部 HTTP 请求检查
$http_ok = ! defined( 'WP_HTTP_BLOCK_EXTERNAL' ) || ! WP_HTTP_BLOCK_EXTERNAL;
?>
<div class="wpbridge-check-item <?php echo $http_ok ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $http_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '外部 HTTP 请求', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo $http_ok ? esc_html__( '允许', 'wpbridge' ) : esc_html__( '已阻止', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐允许', 'wpbridge' ); ?></span>
</div>

<?php
// 内存限制检查
$memory_limit = ini_get( 'memory_limit' );
$memory_bytes = wp_convert_hr_to_bytes( $memory_limit );
$memory_ok = $memory_bytes >= 67108864; // 64MB
?>
<div class="wpbridge-check-item <?php echo $memory_ok ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $memory_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '内存限制', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $memory_limit ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐 >= 64M', 'wpbridge' ); ?></span>
</div>
<?php
// 内存限制检查
$memory_limit = ini_get( 'memory_limit' );
$memory_bytes = wp_convert_hr_to_bytes( $memory_limit );
$memory_ok = $memory_bytes >= 67108864; // 64MB
?>
<div class="wpbridge-check-item <?php echo $memory_ok ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $memory_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '内存限制', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $memory_limit ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐 >= 64M', 'wpbridge' ); ?></span>
</div>

<?php
// 执行时间限制检查
$max_execution = ini_get( 'max_execution_time' );
$execution_ok = 0 === (int) $max_execution || $max_execution >= 30;
?>
<div class="wpbridge-check-item <?php echo $execution_ok ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $execution_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '最大执行时间', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $max_execution ); ?>s</span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐 >= 30s', 'wpbridge' ); ?></span>
</div>
</div>
<?php
// 执行时间限制检查
$max_execution = ini_get( 'max_execution_time' );
$execution_ok = 0 === (int) $max_execution || $max_execution >= 30;
?>
<div class="wpbridge-check-item <?php echo $execution_ok ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $execution_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '最大执行时间', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $max_execution ); ?>s</span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐 >= 30s', 'wpbridge' ); ?></span>
</div>
</div>
</div>

<!-- 配置检查 -->
<div class="wpbridge-diagnostics-section">
<div class="wpbridge-diagnostics-section-header">
<h3>
<span class="dashicons dashicons-admin-settings"></span>
<?php esc_html_e( '配置检查', 'wpbridge' ); ?>
</h3>
</div>
<div class="wpbridge-diagnostics-section-header">
<h3>
<span class="dashicons dashicons-admin-settings"></span>
<?php esc_html_e( '配置检查', 'wpbridge' ); ?>
</h3>
</div>

<div class="wpbridge-config-checks">
<?php
// 调试模式检查
$debug_mode = ! empty( $settings['debug_mode'] );
?>
<div class="wpbridge-check-item <?php echo $debug_mode ? 'warning' : 'passed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $debug_mode ? 'dashicons-warning' : 'dashicons-yes-alt'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '调试模式', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo $debug_mode ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '生产环境建议禁用', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-config-checks">
<?php
// 调试模式检查
$debug_mode = ! empty( $settings['debug_mode'] );
?>
<div class="wpbridge-check-item <?php echo $debug_mode ? 'warning' : 'passed'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $debug_mode ? 'dashicons-warning' : 'dashicons-yes-alt'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '调试模式', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo $debug_mode ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '生产环境建议禁用', 'wpbridge' ); ?></span>
</div>

<?php
// 缓存降级检查
$fallback_enabled = ! empty( $settings['fallback_enabled'] );
?>
<div class="wpbridge-check-item <?php echo $fallback_enabled ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $fallback_enabled ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '缓存降级', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo $fallback_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '建议启用', 'wpbridge' ); ?></span>
</div>
<?php
// 缓存降级检查
$fallback_enabled = ! empty( $settings['fallback_enabled'] );
?>
<div class="wpbridge-check-item <?php echo $fallback_enabled ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $fallback_enabled ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '缓存降级', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo $fallback_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '建议启用', 'wpbridge' ); ?></span>
</div>

<?php
// 请求超时检查
$request_timeout = $settings['request_timeout'] ?? 10;
$timeout_ok = $request_timeout >= 5 && $request_timeout <= 30;
?>
<div class="wpbridge-check-item <?php echo $timeout_ok ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $timeout_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $request_timeout ); ?>s</span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '建议 5-30 秒', 'wpbridge' ); ?></span>
</div>
<?php
// 请求超时检查
$request_timeout = $settings['request_timeout'] ?? 10;
$timeout_ok = $request_timeout >= 5 && $request_timeout <= 30;
?>
<div class="wpbridge-check-item <?php echo $timeout_ok ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $timeout_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $request_timeout ); ?>s</span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '建议 5-30 秒', 'wpbridge' ); ?></span>
</div>

<?php
// 缓存 TTL 检查
$cache_ttl = $settings['cache_ttl'] ?? 43200;
$cache_hours = $cache_ttl / 3600;
?>
<div class="wpbridge-check-item passed">
<span class="wpbridge-check-icon dashicons dashicons-yes-alt"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '缓存有效期', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $cache_hours ); ?> <?php esc_html_e( '小时', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '当前设置', 'wpbridge' ); ?></span>
</div>
<?php
// 缓存 TTL 检查
$cache_ttl = $settings['cache_ttl'] ?? 43200;
$cache_hours = $cache_ttl / 3600;
?>
<div class="wpbridge-check-item passed">
<span class="wpbridge-check-icon dashicons dashicons-yes-alt"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '缓存有效期', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $cache_hours ); ?> <?php esc_html_e( '小时', 'wpbridge' ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '当前设置', 'wpbridge' ); ?></span>
</div>

<?php
// 启用的更新源数量检查
$enabled_sources = $stats['enabled'] ?? 0;
?>
<div class="wpbridge-check-item <?php echo $enabled_sources > 0 ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $enabled_sources > 0 ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '启用的更新源', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $enabled_sources ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '至少需要 1 个', 'wpbridge' ); ?></span>
</div>
</div>
<?php
// 启用的更新源数量检查
$enabled_sources = $stats['enabled'] ?? 0;
?>
<div class="wpbridge-check-item <?php echo $enabled_sources > 0 ? 'passed' : 'warning'; ?>">
<span class="wpbridge-check-icon dashicons <?php echo $enabled_sources > 0 ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
<div class="wpbridge-check-info">
<span class="wpbridge-check-label"><?php esc_html_e( '启用的更新源', 'wpbridge' ); ?></span>
<span class="wpbridge-check-value"><?php echo esc_html( $enabled_sources ); ?></span>
</div>
<span class="wpbridge-check-requirement"><?php esc_html_e( '至少需要 1 个', 'wpbridge' ); ?></span>
</div>
</div>
</div>

<!-- 诊断报告导出模态框 -->
<div id="wpbridge-export-modal" class="wpbridge-modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="wpbridge-export-modal-title">
<div class="wpbridge-modal-content">
<div class="wpbridge-modal-header">
<h3 id="wpbridge-export-modal-title"><?php esc_html_e( '诊断报告', 'wpbridge' ); ?></h3>
<button type="button" class="wpbridge-modal-close" aria-label="<?php esc_attr_e( '关闭', 'wpbridge' ); ?>">&times;</button>
</div>
<div class="wpbridge-modal-body">
<textarea id="wpbridge-diagnostics-report" class="wpbridge-diagnostics-textarea" readonly></textarea>
</div>
<div class="wpbridge-modal-footer">
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-copy-report">
<span class="dashicons dashicons-admin-page"></span>
<?php esc_html_e( '复制到剪贴板', 'wpbridge' ); ?>
</button>
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-download-report">
<span class="dashicons dashicons-download"></span>
<?php esc_html_e( '下载报告', 'wpbridge' ); ?>
</button>
</div>
</div>
<div class="wpbridge-modal-content">
<div class="wpbridge-modal-header">
<h3 id="wpbridge-export-modal-title"><?php esc_html_e( '诊断报告', 'wpbridge' ); ?></h3>
<button type="button" class="wpbridge-modal-close" aria-label="<?php esc_attr_e( '关闭', 'wpbridge' ); ?>">&times;</button>
</div>
<div class="wpbridge-modal-body">
<textarea id="wpbridge-diagnostics-report" class="wpbridge-diagnostics-textarea" readonly></textarea>
</div>
<div class="wpbridge-modal-footer">
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-copy-report">
<span class="dashicons dashicons-admin-page"></span>
<?php esc_html_e( '复制到剪贴板', 'wpbridge' ); ?>
</button>
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-download-report">
<span class="dashicons dashicons-download"></span>
<?php esc_html_e( '下载报告', 'wpbridge' ); ?>
</button>
</div>
</div>
</div>

View file

@ -9,52 +9,52 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}
?>

<div class="wpbridge-logs-panel">
<div class="wpbridge-logs-header">
<h2 class="wpbridge-logs-title"><?php esc_html_e( '调试日志', 'wpbridge' ); ?></h2>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-clear-logs">
<span class="dashicons dashicons-trash"></span>
<?php esc_html_e( '清除日志', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-logs-header">
<h2 class="wpbridge-logs-title"><?php esc_html_e( '调试日志', 'wpbridge' ); ?></h2>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-clear-logs">
<span class="dashicons dashicons-trash"></span>
<?php esc_html_e( '清除日志', 'wpbridge' ); ?>
</button>
</div>

<?php if ( empty( $logs ) ) : ?>
<div class="wpbridge-logs-empty">
<span class="dashicons dashicons-media-text" style="font-size: 32px; width: 32px; height: 32px; color: var(--wpbridge-gray-300);"></span>
<p><?php esc_html_e( '暂无日志记录', 'wpbridge' ); ?></p>
<p style="font-size: 12px; color: var(--wpbridge-gray-400);">
<?php esc_html_e( '启用调试模式后,日志将显示在这里。', 'wpbridge' ); ?>
</p>
</div>
<?php else : ?>
<div class="wpbridge-logs-list">
<?php foreach ( array_slice( $logs, 0, 100 ) as $log ) : ?>
<div class="wpbridge-log-item">
<span class="wpbridge-log-level <?php echo esc_attr( $log['level'] ); ?>">
<?php echo esc_html( strtoupper( $log['level'] ) ); ?>
</span>
<span class="wpbridge-log-time">
<?php echo esc_html( $log['time'] ); ?>
</span>
<span class="wpbridge-log-message">
<?php echo esc_html( $log['message'] ); ?>
<?php if ( ! empty( $log['context'] ) ) : ?>
<code style="display: block; margin-top: 4px; font-size: 11px; color: var(--wpbridge-gray-500);">
<?php echo esc_html( wp_json_encode( $log['context'], JSON_UNESCAPED_UNICODE ) ); ?>
</code>
<?php endif; ?>
</span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ( empty( $logs ) ) : ?>
<div class="wpbridge-logs-empty">
<span class="dashicons dashicons-media-text" style="font-size: 32px; width: 32px; height: 32px; color: var(--wpbridge-gray-300);"></span>
<p><?php esc_html_e( '暂无日志记录', 'wpbridge' ); ?></p>
<p style="font-size: 12px; color: var(--wpbridge-gray-400);">
<?php esc_html_e( '启用调试模式后,日志将显示在这里。', 'wpbridge' ); ?>
</p>
</div>
<?php else : ?>
<div class="wpbridge-logs-list">
<?php foreach ( array_slice( $logs, 0, 100 ) as $log ) : ?>
<div class="wpbridge-log-item">
<span class="wpbridge-log-level <?php echo esc_attr( $log['level'] ); ?>">
<?php echo esc_html( strtoupper( $log['level'] ) ); ?>
</span>
<span class="wpbridge-log-time">
<?php echo esc_html( $log['time'] ); ?>
</span>
<span class="wpbridge-log-message">
<?php echo esc_html( $log['message'] ); ?>
<?php if ( ! empty( $log['context'] ) ) : ?>
<code style="display: block; margin-top: 4px; font-size: 11px; color: var(--wpbridge-gray-500);">
<?php echo esc_html( wp_json_encode( $log['context'], JSON_UNESCAPED_UNICODE ) ); ?>
</code>
<?php endif; ?>
</span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>

<div style="margin-top: 16px; padding: 16px; background: var(--wpbridge-info-light); font-size: 13px;">
<strong><?php esc_html_e( '提示', 'wpbridge' ); ?>:</strong>
<?php esc_html_e( '日志仅在启用调试模式时记录。生产环境建议关闭调试模式以提高性能。', 'wpbridge' ); ?>
<strong><?php esc_html_e( '提示', 'wpbridge' ); ?>:</strong>
<?php esc_html_e( '日志仅在启用调试模式时记录。生产环境建议关闭调试模式以提高性能。', 'wpbridge' ); ?>
</div>

View file

@ -12,7 +12,7 @@

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit;
}

// 计算统计数据
@ -24,13 +24,13 @@ $item_configs = get_option( 'wpbridge_item_sources', array() );
$custom_count = 0;
$disabled_count = 0;
foreach ( $item_configs as $config ) {
if ( isset( $config['mode'] ) ) {
if ( $config['mode'] === 'custom' ) {
++$custom_count;
} elseif ( $config['mode'] === 'disabled' ) {
++$disabled_count;
}
}
if ( isset( $config['mode'] ) ) {
if ( $config['mode'] === 'custom' ) {
$custom_count++;
} elseif ( $config['mode'] === 'disabled' ) {
$disabled_count++;
}
}
}

// 健康源统计
@ -38,20 +38,20 @@ $healthy_count = 0;
$degraded_count = 0;
$failed_count = 0;
foreach ( $health_status as $status ) {
// 确保 $status 是数组
if ( is_array( $status ) && isset( $status['status'] ) ) {
switch ( $status['status'] ) {
case 'healthy':
++$healthy_count;
break;
case 'degraded':
++$degraded_count;
break;
case 'failed':
++$failed_count;
break;
}
}
// 确保 $status 是数组
if ( is_array( $status ) && isset( $status['status'] ) ) {
switch ( $status['status'] ) {
case 'healthy':
$healthy_count++;
break;
case 'degraded':
$degraded_count++;
break;
case 'failed':
$failed_count++;
break;
}
}
}

// 缓存状态
@ -59,214 +59,211 @@ $cache_enabled = ! empty( $settings['fallback_enabled'] );
$debug_mode = ! empty( $settings['debug_mode'] );

// 计算健康百分比
$total_checked = count( $health_status );
$health_percent = $total_checked > 0 ? round( ( $healthy_count / $total_checked ) * 100 ) : 0;
$total_checked = count( $health_status );
$health_percent = $total_checked > 0 ? round( ( $healthy_count / $total_checked ) * 100 ) : 0;
$health_status_class = $failed_count > 0 ? 'error' : ( $degraded_count > 0 ? 'warning' : 'success' );
?>

<!-- 状态摘要栏 -->
<div class="wpbridge-status-bar">
<div class="wpbridge-status-bar-item">
<span class="wpbridge-status-indicator <?php echo $stats['enabled'] > 0 ? 'active' : 'inactive'; ?>"></span>
<span class="wpbridge-status-text">
<?php
printf(
/* translators: %d: number of enabled sources */
esc_html__( '%d 个更新源已启用', 'wpbridge' ),
$stats['enabled']
);
?>
</span>
</div>
<?php if ( $debug_mode ) : ?>
<div class="wpbridge-status-bar-item wpbridge-status-bar-warning">
<span class="dashicons dashicons-warning"></span>
<span class="wpbridge-status-text"><?php esc_html_e( '调试模式已开启', 'wpbridge' ); ?></span>
</div>
<?php endif; ?>
<div class="wpbridge-status-bar-actions">
<button type="button" class="wpbridge-btn wpbridge-btn-sm wpbridge-btn-secondary wpbridge-clear-cache">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
</button>
</div>
<div class="wpbridge-status-bar-item">
<span class="wpbridge-status-indicator <?php echo $stats['enabled'] > 0 ? 'active' : 'inactive'; ?>"></span>
<span class="wpbridge-status-text">
<?php
printf(
/* translators: %d: number of enabled sources */
esc_html__( '%d 个更新源已启用', 'wpbridge' ),
$stats['enabled']
);
?>
</span>
</div>
<?php if ( $debug_mode ) : ?>
<div class="wpbridge-status-bar-item wpbridge-status-bar-warning">
<span class="dashicons dashicons-warning"></span>
<span class="wpbridge-status-text"><?php esc_html_e( '调试模式已开启', 'wpbridge' ); ?></span>
</div>
<?php endif; ?>
<div class="wpbridge-status-bar-actions">
<button type="button" class="wpbridge-btn wpbridge-btn-sm wpbridge-btn-secondary wpbridge-clear-cache">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
</button>
</div>
</div>

<!-- 核心指标 -->
<div class="wpbridge-metrics">
<!-- 更新源 -->
<div class="wpbridge-metric-card">
<div class="wpbridge-metric-icon">
<span class="dashicons dashicons-cloud"></span>
</div>
<div class="wpbridge-metric-content">
<div class="wpbridge-metric-value"><?php echo esc_html( $stats['total'] ); ?></div>
<div class="wpbridge-metric-label"><?php esc_html_e( '更新源', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub">
<span class="wpbridge-metric-highlight"><?php echo esc_html( $stats['enabled'] ); ?></span> <?php esc_html_e( '已启用', 'wpbridge' ); ?>
</div>
</div>
<a href="#sources" class="wpbridge-metric-link" data-tab-link="sources" aria-label="<?php esc_attr_e( '管理更新源', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-right-alt2"></span>
</a>
</div>
<!-- 更新源 -->
<div class="wpbridge-metric-card">
<div class="wpbridge-metric-icon">
<span class="dashicons dashicons-cloud"></span>
</div>
<div class="wpbridge-metric-content">
<div class="wpbridge-metric-value"><?php echo esc_html( $stats['total'] ); ?></div>
<div class="wpbridge-metric-label"><?php esc_html_e( '更新源', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub">
<span class="wpbridge-metric-highlight"><?php echo esc_html( $stats['enabled'] ); ?></span> <?php esc_html_e( '已启用', 'wpbridge' ); ?>
</div>
</div>
<a href="#sources" class="wpbridge-metric-link" data-tab-link="sources" aria-label="<?php esc_attr_e( '管理更新源', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-right-alt2"></span>
</a>
</div>

<!-- 项目 -->
<div class="wpbridge-metric-card">
<div class="wpbridge-metric-icon">
<span class="dashicons dashicons-admin-plugins"></span>
</div>
<div class="wpbridge-metric-content">
<div class="wpbridge-metric-value"><?php echo esc_html( $plugins_count + $themes_count ); ?></div>
<div class="wpbridge-metric-label"><?php esc_html_e( '项目', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub">
<?php echo esc_html( $plugins_count ); ?> <?php esc_html_e( '插件', 'wpbridge' ); ?> / <?php echo esc_html( $themes_count ); ?> <?php esc_html_e( '主题', 'wpbridge' ); ?>
</div>
</div>
<a href="#projects" class="wpbridge-metric-link" data-tab-link="projects" aria-label="<?php esc_attr_e( '管理项目', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-right-alt2"></span>
</a>
</div>
<!-- 项目 -->
<div class="wpbridge-metric-card">
<div class="wpbridge-metric-icon">
<span class="dashicons dashicons-admin-plugins"></span>
</div>
<div class="wpbridge-metric-content">
<div class="wpbridge-metric-value"><?php echo esc_html( $plugins_count + $themes_count ); ?></div>
<div class="wpbridge-metric-label"><?php esc_html_e( '项目', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub">
<?php echo esc_html( $plugins_count ); ?> <?php esc_html_e( '插件', 'wpbridge' ); ?> / <?php echo esc_html( $themes_count ); ?> <?php esc_html_e( '主题', 'wpbridge' ); ?>
</div>
</div>
<a href="#projects" class="wpbridge-metric-link" data-tab-link="projects" aria-label="<?php esc_attr_e( '管理项目', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-right-alt2"></span>
</a>
</div>

<!-- 自定义配置 -->
<div class="wpbridge-metric-card">
<div class="wpbridge-metric-icon">
<span class="dashicons dashicons-admin-settings"></span>
</div>
<div class="wpbridge-metric-content">
<div class="wpbridge-metric-value"><?php echo esc_html( $custom_count + $disabled_count ); ?></div>
<div class="wpbridge-metric-label"><?php esc_html_e( '自定义配置', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub">
<?php if ( $custom_count > 0 || $disabled_count > 0 ) : ?>
<?php if ( $custom_count > 0 ) : ?>
<span class="wpbridge-metric-highlight"><?php echo esc_html( $custom_count ); ?></span> <?php esc_html_e( '自定义', 'wpbridge' ); ?>
<?php endif; ?>
<?php if ( $disabled_count > 0 ) : ?>
<?php
if ( $custom_count > 0 ) :
?>
/ <?php endif; ?>
<span class="wpbridge-metric-muted"><?php echo esc_html( $disabled_count ); ?></span> <?php esc_html_e( '禁用', 'wpbridge' ); ?>
<?php endif; ?>
<?php else : ?>
<?php esc_html_e( '全部使用默认', 'wpbridge' ); ?>
<?php endif; ?>
</div>
</div>
<a href="#projects" class="wpbridge-metric-link" data-tab-link="projects" aria-label="<?php esc_attr_e( '查看配置', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-right-alt2"></span>
</a>
</div>
<!-- 自定义配置 -->
<div class="wpbridge-metric-card">
<div class="wpbridge-metric-icon">
<span class="dashicons dashicons-admin-settings"></span>
</div>
<div class="wpbridge-metric-content">
<div class="wpbridge-metric-value"><?php echo esc_html( $custom_count + $disabled_count ); ?></div>
<div class="wpbridge-metric-label"><?php esc_html_e( '自定义配置', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub">
<?php if ( $custom_count > 0 || $disabled_count > 0 ) : ?>
<?php if ( $custom_count > 0 ) : ?>
<span class="wpbridge-metric-highlight"><?php echo esc_html( $custom_count ); ?></span> <?php esc_html_e( '自定义', 'wpbridge' ); ?>
<?php endif; ?>
<?php if ( $disabled_count > 0 ) : ?>
<?php if ( $custom_count > 0 ) : ?> / <?php endif; ?>
<span class="wpbridge-metric-muted"><?php echo esc_html( $disabled_count ); ?></span> <?php esc_html_e( '禁用', 'wpbridge' ); ?>
<?php endif; ?>
<?php else : ?>
<?php esc_html_e( '全部使用默认', 'wpbridge' ); ?>
<?php endif; ?>
</div>
</div>
<a href="#projects" class="wpbridge-metric-link" data-tab-link="projects" aria-label="<?php esc_attr_e( '查看配置', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-right-alt2"></span>
</a>
</div>

<!-- 健康状态 -->
<div class="wpbridge-metric-card wpbridge-metric-card-health">
<div class="wpbridge-metric-icon wpbridge-metric-icon-<?php echo esc_attr( $health_status_class ); ?>">
<span class="dashicons dashicons-heart"></span>
</div>
<div class="wpbridge-metric-content">
<?php if ( $total_checked > 0 ) : ?>
<div class="wpbridge-metric-value wpbridge-metric-value-<?php echo esc_attr( $health_status_class ); ?>">
<?php echo esc_html( $health_percent ); ?>%
</div>
<div class="wpbridge-metric-label"><?php esc_html_e( '健康度', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub">
<?php echo esc_html( $healthy_count ); ?>/<?php echo esc_html( $total_checked ); ?> <?php esc_html_e( '源正常', 'wpbridge' ); ?>
</div>
<?php else : ?>
<div class="wpbridge-metric-value wpbridge-metric-value-muted">--</div>
<div class="wpbridge-metric-label"><?php esc_html_e( '健康度', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub"><?php esc_html_e( '暂无检查数据', 'wpbridge' ); ?></div>
<?php endif; ?>
</div>
<a href="#diagnostics" class="wpbridge-metric-link" data-tab-link="diagnostics" aria-label="<?php esc_attr_e( '运行诊断', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-right-alt2"></span>
</a>
</div>
<!-- 健康状态 -->
<div class="wpbridge-metric-card wpbridge-metric-card-health">
<div class="wpbridge-metric-icon wpbridge-metric-icon-<?php echo esc_attr( $health_status_class ); ?>">
<span class="dashicons dashicons-heart"></span>
</div>
<div class="wpbridge-metric-content">
<?php if ( $total_checked > 0 ) : ?>
<div class="wpbridge-metric-value wpbridge-metric-value-<?php echo esc_attr( $health_status_class ); ?>">
<?php echo esc_html( $health_percent ); ?>%
</div>
<div class="wpbridge-metric-label"><?php esc_html_e( '健康度', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub">
<?php echo esc_html( $healthy_count ); ?>/<?php echo esc_html( $total_checked ); ?> <?php esc_html_e( '源正常', 'wpbridge' ); ?>
</div>
<?php else : ?>
<div class="wpbridge-metric-value wpbridge-metric-value-muted">--</div>
<div class="wpbridge-metric-label"><?php esc_html_e( '健康度', 'wpbridge' ); ?></div>
<div class="wpbridge-metric-sub"><?php esc_html_e( '暂无检查数据', 'wpbridge' ); ?></div>
<?php endif; ?>
</div>
<a href="#diagnostics" class="wpbridge-metric-link" data-tab-link="diagnostics" aria-label="<?php esc_attr_e( '运行诊断', 'wpbridge' ); ?>">
<span class="dashicons dashicons-arrow-right-alt2"></span>
</a>
</div>
</div>

<!-- 快速入口 + 系统信息 -->
<div class="wpbridge-overview-panels">
<!-- 快速入口 -->
<div class="wpbridge-panel wpbridge-panel-actions">
<div class="wpbridge-panel-header">
<h3><?php esc_html_e( '快速入口', 'wpbridge' ); ?></h3>
</div>
<div class="wpbridge-panel-body">
<div class="wpbridge-action-list">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="wpbridge-action-item">
<span class="wpbridge-action-icon">
<span class="dashicons dashicons-plus-alt2"></span>
</span>
<span class="wpbridge-action-text">
<span class="wpbridge-action-title"><?php esc_html_e( '添加更新源', 'wpbridge' ); ?></span>
<span class="wpbridge-action-desc"><?php esc_html_e( '配置新的自定义更新源', 'wpbridge' ); ?></span>
</span>
</a>
<a href="#diagnostics" class="wpbridge-action-item" data-tab-link="diagnostics">
<span class="wpbridge-action-icon">
<span class="dashicons dashicons-admin-tools"></span>
</span>
<span class="wpbridge-action-text">
<span class="wpbridge-action-title"><?php esc_html_e( '诊断工具', 'wpbridge' ); ?></span>
<span class="wpbridge-action-desc"><?php esc_html_e( '检查源连通性和系统环境', 'wpbridge' ); ?></span>
</span>
</a>
<a href="#api" class="wpbridge-action-item" data-tab-link="api">
<span class="wpbridge-action-icon">
<span class="dashicons dashicons-rest-api"></span>
</span>
<span class="wpbridge-action-text">
<span class="wpbridge-action-title"><?php esc_html_e( 'Bridge API', 'wpbridge' ); ?></span>
<span class="wpbridge-action-desc"><?php esc_html_e( '管理 API 密钥和访问控制', 'wpbridge' ); ?></span>
</span>
</a>
<a href="#settings" class="wpbridge-action-item" data-tab-link="settings">
<span class="wpbridge-action-icon">
<span class="dashicons dashicons-admin-generic"></span>
</span>
<span class="wpbridge-action-text">
<span class="wpbridge-action-title"><?php esc_html_e( '设置', 'wpbridge' ); ?></span>
<span class="wpbridge-action-desc"><?php esc_html_e( '配置缓存、超时和调试选项', 'wpbridge' ); ?></span>
</span>
</a>
</div>
</div>
</div>
<!-- 快速入口 -->
<div class="wpbridge-panel wpbridge-panel-actions">
<div class="wpbridge-panel-header">
<h3><?php esc_html_e( '快速入口', 'wpbridge' ); ?></h3>
</div>
<div class="wpbridge-panel-body">
<div class="wpbridge-action-list">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="wpbridge-action-item">
<span class="wpbridge-action-icon">
<span class="dashicons dashicons-plus-alt2"></span>
</span>
<span class="wpbridge-action-text">
<span class="wpbridge-action-title"><?php esc_html_e( '添加更新源', 'wpbridge' ); ?></span>
<span class="wpbridge-action-desc"><?php esc_html_e( '配置新的自定义更新源', 'wpbridge' ); ?></span>
</span>
</a>
<a href="#diagnostics" class="wpbridge-action-item" data-tab-link="diagnostics">
<span class="wpbridge-action-icon">
<span class="dashicons dashicons-admin-tools"></span>
</span>
<span class="wpbridge-action-text">
<span class="wpbridge-action-title"><?php esc_html_e( '诊断工具', 'wpbridge' ); ?></span>
<span class="wpbridge-action-desc"><?php esc_html_e( '检查源连通性和系统环境', 'wpbridge' ); ?></span>
</span>
</a>
<a href="#api" class="wpbridge-action-item" data-tab-link="api">
<span class="wpbridge-action-icon">
<span class="dashicons dashicons-rest-api"></span>
</span>
<span class="wpbridge-action-text">
<span class="wpbridge-action-title"><?php esc_html_e( 'Bridge API', 'wpbridge' ); ?></span>
<span class="wpbridge-action-desc"><?php esc_html_e( '管理 API 密钥和访问控制', 'wpbridge' ); ?></span>
</span>
</a>
<a href="#settings" class="wpbridge-action-item" data-tab-link="settings">
<span class="wpbridge-action-icon">
<span class="dashicons dashicons-admin-generic"></span>
</span>
<span class="wpbridge-action-text">
<span class="wpbridge-action-title"><?php esc_html_e( '设置', 'wpbridge' ); ?></span>
<span class="wpbridge-action-desc"><?php esc_html_e( '配置缓存、超时和调试选项', 'wpbridge' ); ?></span>
</span>
</a>
</div>
</div>
</div>

<!-- 系统信息 -->
<div class="wpbridge-panel wpbridge-panel-info">
<div class="wpbridge-panel-header">
<h3><?php esc_html_e( '系统信息', 'wpbridge' ); ?></h3>
</div>
<div class="wpbridge-panel-body">
<div class="wpbridge-info-list">
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( 'WordPress', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( get_bloginfo( 'version' ) ); ?></span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( 'PHP', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( PHP_VERSION ); ?></span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( '插件版本', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( WPBRIDGE_VERSION ); ?></span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( '缓存 TTL', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( ( $settings['cache_ttl'] ?? 43200 ) / 3600 ); ?>h</span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( $settings['request_timeout'] ?? 10 ); ?>s</span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( '缓存降级', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value wpbridge-info-value-<?php echo $cache_enabled ? 'success' : 'muted'; ?>">
<?php echo $cache_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?>
</span>
</div>
</div>
</div>
</div>
<!-- 系统信息 -->
<div class="wpbridge-panel wpbridge-panel-info">
<div class="wpbridge-panel-header">
<h3><?php esc_html_e( '系统信息', 'wpbridge' ); ?></h3>
</div>
<div class="wpbridge-panel-body">
<div class="wpbridge-info-list">
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( 'WordPress', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( get_bloginfo( 'version' ) ); ?></span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( 'PHP', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( PHP_VERSION ); ?></span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( '插件版本', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( WPBRIDGE_VERSION ); ?></span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( '缓存 TTL', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( ( $settings['cache_ttl'] ?? 43200 ) / 3600 ); ?>h</span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value"><?php echo esc_html( $settings['request_timeout'] ?? 10 ); ?>s</span>
</div>
<div class="wpbridge-info-item">
<span class="wpbridge-info-label"><?php esc_html_e( '缓存降级', 'wpbridge' ); ?></span>
<span class="wpbridge-info-value wpbridge-info-value-<?php echo $cache_enabled ? 'success' : 'muted'; ?>">
<?php echo $cache_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?>
</span>
</div>
</div>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show more