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
102 changed files with 34761 additions and 0 deletions

View file

@ -0,0 +1,244 @@
# 文派统一插件发布 CI Workflow
# 触发push tag v*
# 运行环境forgejo-ci-php:latest (Alpine + php-cli + git + rsync + zip + node)
name: Release Plugin

on:
push:
tags:
- "v*"

env:
PLUGIN_SLUG: ${{ github.event.repository.name }}

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Verify tools
shell: bash
run: |
set -euo pipefail
php -v | head -1
git --version
rsync --version | head -1
zip --version | head -2
jq --version
curl --version | head -1

- name: Extract version from tag
id: version
shell: bash
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Tag: $TAG, Version: $VERSION"

- name: Detect main plugin file
id: detect
shell: bash
run: |
set -euo pipefail
MAIN_FILE=$(grep -rl "Plugin Name:" *.php 2>/dev/null | head -1)
if [ -z "$MAIN_FILE" ]; then
echo "::error::No main plugin file found"
exit 1
fi
echo "main_file=$MAIN_FILE" >> "$GITHUB_OUTPUT"
echo "Main file: $MAIN_FILE"

- name: Validate version consistency
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
MAIN_FILE="${{ steps.detect.outputs.main_file }}"
HEADER_VERSION=$(grep -i "Version:" "$MAIN_FILE" | grep -v "Requires" | grep -v "Tested" | head -1 | sed "s/.*Version:[[:space:]]*//" | sed "s/[[:space:]]*$//" | tr -d "\r")
echo "Extracted header version: [$HEADER_VERSION]"
if [ "$HEADER_VERSION" != "$VERSION" ]; then
echo "::error::Version mismatch: tag=$VERSION, header=$HEADER_VERSION"
exit 1
fi
if [ -f "readme.txt" ]; then
STABLE_TAG=$(grep -i "^Stable tag:" readme.txt | head -1 | sed "s/.*Stable tag:[[:space:]]*//" | sed "s/[[:space:]]*$//" | tr -d "\r")
if [ -n "$STABLE_TAG" ] && [ "$STABLE_TAG" != "$VERSION" ]; then
echo "::error::Stable tag mismatch: tag=$VERSION, readme=$STABLE_TAG"
exit 1
fi
fi
echo "Version consistency check passed: $VERSION"

- name: PHP Lint
shell: bash
run: |
set -euo pipefail
ERRORS=0
while IFS= read -r file; do
if ! php -l "$file" > /dev/null 2>&1; then
php -l "$file"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*")
if [ "$ERRORS" -gt 0 ]; then
echo "::error::PHP lint found $ERRORS error(s)"
exit 1
fi
echo "PHP lint passed"

- name: Build ZIP
id: build
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
SLUG="${{ env.PLUGIN_SLUG }}"
ZIP_NAME="${SLUG}-${VERSION}.zip"
BUILD_DIR="/tmp/build/${SLUG}"
RELEASE_DIR="/tmp/release"

rm -rf "$BUILD_DIR" "$RELEASE_DIR"
mkdir -p "$BUILD_DIR" "$RELEASE_DIR"

rsync -a \
--exclude=".git" \
--exclude=".github" \
--exclude=".forgejo" \
--exclude=".gitignore" \
--exclude=".gitattributes" \
--exclude=".editorconfig" \
--exclude=".env*" \
--exclude="node_modules" \
--exclude="tests" \
--exclude="phpunit.xml*" \
--exclude="phpcs.xml*" \
--exclude="phpstan.neon*" \
--exclude="composer.json" \
--exclude="composer.lock" \
--exclude="package.json" \
--exclude="package-lock.json" \
--exclude="Gruntfile.js" \
--exclude="webpack.config.js" \
--exclude="*.md" \
--exclude="LICENSE" \
--exclude="Makefile" \
./ "$BUILD_DIR/"

(
cd /tmp/build
zip -qr "${RELEASE_DIR}/${ZIP_NAME}" "${SLUG}/"
)

echo "zip_path=${RELEASE_DIR}/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "release_dir=${RELEASE_DIR}" >> "$GITHUB_OUTPUT"
echo "Built: ${ZIP_NAME} ($(du -h "${RELEASE_DIR}/${ZIP_NAME}" | cut -f1))"

- name: Calculate SHA-256
id: checksum
shell: bash
run: |
set -euo pipefail
ZIP_PATH="${{ steps.build.outputs.zip_path }}"
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
RELEASE_DIR="${{ steps.build.outputs.release_dir }}"
SHA256=$(sha256sum "$ZIP_PATH" | cut -d" " -f1)
echo "$SHA256 $ZIP_NAME" > "${RELEASE_DIR}/${ZIP_NAME}.sha256"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
echo "SHA-256: $SHA256"
ls -la "$RELEASE_DIR"

- name: Create or Update Release & Upload Assets
shell: bash
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -euo pipefail

AUTH_TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN:-}}"
if [ -z "$AUTH_TOKEN" ]; then
echo "::error::Missing auth token: set RELEASE_TOKEN or use default GITHUB_TOKEN"
exit 1
fi

TAG="${{ steps.version.outputs.tag }}"
SLUG="${{ env.PLUGIN_SLUG }}"
RELEASE_DIR="${{ steps.build.outputs.release_dir }}"
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
SHA256="${{ steps.checksum.outputs.sha256 }}"
API_URL="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
AUTH_HEADER="Authorization: token ${AUTH_TOKEN}"
printf -v RELEASE_NOTES '## %s %s\n\n### Checksums\n\n| File | SHA-256 |\n|------|---------|\\n| %s | %s |\n' "$SLUG" "$TAG" "$ZIP_NAME" "$SHA256"

echo ">>> Resolving release ${TAG}"
STATUS=$(curl -sS -o /tmp/release.json -w "%{http_code}" \
-H "$AUTH_HEADER" \
"${API_URL}/releases/tags/${TAG}")

if [ "$STATUS" = "200" ]; then
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
echo "Release exists (id=${RELEASE_ID}), patching metadata"
curl -sS -f -X PATCH \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg name "$TAG" --arg body "$RELEASE_NOTES" '{name: $name, body: $body, draft: false, prerelease: false}')" \
"${API_URL}/releases/${RELEASE_ID}" > /tmp/release.json
elif [ "$STATUS" = "404" ]; then
echo "Release not found, creating"
curl -sS -f -X POST \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg tag "$TAG" --arg name "$TAG" --arg body "$RELEASE_NOTES" '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')" \
"${API_URL}/releases" > /tmp/release.json
else
echo "::error::Failed to query release (HTTP ${STATUS})"
cat /tmp/release.json
exit 1
fi

RELEASE_ID=$(jq -r '.id' /tmp/release.json)
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "::error::Failed to resolve release id"
cat /tmp/release.json
exit 1
fi

echo ">>> Uploading assets to release ${RELEASE_ID}"
for FILE in "${RELEASE_DIR}"/*; do
FILENAME=$(basename "$FILE")
EXISTING_ASSET_ID=$(jq -r --arg n "$FILENAME" '.assets[]? | select(.name == $n) | .id' /tmp/release.json | head -1)

if [ -n "$EXISTING_ASSET_ID" ] && [ "$EXISTING_ASSET_ID" != "null" ]; then
echo " deleting old asset: ${FILENAME} (id=${EXISTING_ASSET_ID})"
curl -sS -f -X DELETE \
-H "$AUTH_HEADER" \
"${API_URL}/releases/${RELEASE_ID}/assets/${EXISTING_ASSET_ID}" > /dev/null
fi

ENCODED_NAME=$(printf "%s" "$FILENAME" | jq -sRr @uri)
echo " uploading: ${FILENAME}"
curl -sS -f --retry 3 --retry-delay 2 --retry-all-errors \
-X POST \
-H "$AUTH_HEADER" \
-F "attachment=@${FILE}" \
"${API_URL}/releases/${RELEASE_ID}/assets?name=${ENCODED_NAME}" > /dev/null
done

echo ">>> Verifying uploaded assets"
curl -sS -f -H "$AUTH_HEADER" "${API_URL}/releases/${RELEASE_ID}" > /tmp/release-final.json
for FILE in "${RELEASE_DIR}"/*; do
FILENAME=$(basename "$FILE")
if ! jq -e --arg n "$FILENAME" '.assets[]? | select(.name == $n)' /tmp/release-final.json > /dev/null; then
echo "::error::Missing uploaded asset: ${FILENAME}"
jq -r '.assets[]?.name' /tmp/release-final.json
exit 1
fi
done

echo ">>> Release ${TAG} published successfully"
echo "Release URL: ${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"

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/

498
ARCHITECTURE.md Normal file
View file

@ -0,0 +1,498 @@
# WPBridge 业务流程与架构设计

> 详细的业务流程图和系统架构

*创建日期: 2026-02-04*

---

## 1. 核心业务流程

### 1.1 更新源桥接流程

```
┌─────────────────────────────────────────────────────────────────┐
│ WordPress 更新检查流程 │
└─────────────────────────────────────────────────────────────────┘

WordPress 核心 WPBridge 外部源
│ │ │
│ 1. 触发更新检查 │ │
│ (wp_update_plugins) │ │
│──────────────────────────────>│ │
│ │ │
│ │ 2. 检查是否有自定义源 │
│ │ (查询 wpbridge_sources) │
│ │ │
│ │ 3. 遍历匹配的源 │
│ │──────────────────────────────>
│ │ │
│ │ 4. 获取版本信息 │
│ │<──────────────────────────────
│ │ │
│ │ 5. 缓存结果 │
│ │ (transient) │
│ │ │
│ 6. 返回更新信息 │ │
│<──────────────────────────────│ │
│ │ │
│ 7. 显示更新通知 │ │
│ │ │
```

### 1.2 更新下载流程

```
┌─────────────────────────────────────────────────────────────────┐
│ 插件/主题下载流程 │
└─────────────────────────────────────────────────────────────────┘

用户点击更新 WPBridge 外部源
│ │ │
│ 1. 触发下载 │ │
│ (upgrader_pre_download) │ │
│──────────────────────────────>│ │
│ │ │
│ │ 2. 检查是否需要桥接 │
│ │ (匹配 slug) │
│ │ │
│ │ 3. 获取下载 URL │
│ │ (可能需要认证) │
│ │──────────────────────────────>
│ │ │
│ │ 4. 下载 ZIP 包 │
│ │<──────────────────────────────
│ │ │
│ │ 5. 安全检查 │
│ │ (哈希/大小/结构) │
│ │ │
│ 6. 返回本地文件路径 │ │
│<──────────────────────────────│ │
│ │ │
│ 7. WordPress 安装更新 │ │
│ │ │
```

### 1.3 AI 桥接流程

```
┌─────────────────────────────────────────────────────────────────┐
│ AI 请求桥接流程 │
└─────────────────────────────────────────────────────────────────┘

第三方插件 WPBridge AI 服务
(如 AI Engine) │ │
│ │ │
│ 1. 发起 HTTP 请求 │ │
│ (api.openai.com) │ │
│──────────────────────────────>│ │
│ │ │
│ │ 2. 白名单检查 │
│ │ (是否在拦截列表) │
│ │ │
│ │ 3. 模式判断 │
│ │ ┌─────────────────────────┐│
│ │ │ MODE_DISABLED → 放行 ││
│ │ │ MODE_PASSTHROUGH → 转发 ││
│ │ │ MODE_WPMIND → WPMind ││
│ │ └─────────────────────────┘│
│ │ │
│ │ 4a. 透传模式 │
│ │──────────────────────────────>
│ │ (用户指定端点) │
│ │ │
│ │ 4b. WPMind 模式 │
│ │──────> WPMind ──────────────>
│ │ (国内 AI 服务) │
│ │ │
│ 5. 返回响应 │ │
│<──────────────────────────────│ │
│ │ │
```

---

## 2. 系统架构

### 2.1 整体架构图

```
┌─────────────────────────────────────────────────────────────────┐
│ WPBridge 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Admin Layer │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ AdminPage │ │ SourceEditor│ │ AISettings │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Core Layer │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Plugin │ │ Settings │ │ Logger │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┴──────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
│ │ │ UpdateSource │ │ AIBridge │ │ │
│ │ │ Module │ │ Module │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │
│ │ │ │SourceManager │ │ │ │ Interceptor │ │ │ │
│ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │
│ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │
│ │ │ │PluginUpdater │ │ │ │ WPMindBridge │ │ │ │
│ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │
│ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │
│ │ │ │ ThemeUpdater │ │ │ │ Passthrough │ │ │ │
│ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ └─────────────────────┘ └─────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Handler Layer │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ JSON │ │ GitHub │ │ GitLab │ │ WenPai │ │ │
│ │ │ Handler │ │ Handler │ │ Handler │ │ Handler │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```

### 2.2 目录结构

```
wpbridge/
├── wpbridge.php # 主文件
├── uninstall.php # 卸载脚本
├── CHANGELOG.md # 更新日志
├── includes/
│ ├── Core/
│ │ ├── Plugin.php # 插件主类
│ │ ├── Loader.php # 自动加载
│ │ ├── Settings.php # 设置管理
│ │ ├── Logger.php # 日志系统
│ │ └── Encryption.php # 加密工具
│ │
│ ├── UpdateSource/
│ │ ├── SourceManager.php # 更新源管理
│ │ ├── SourceModel.php # 数据模型
│ │ ├── PluginUpdater.php # 插件更新器
│ │ ├── ThemeUpdater.php # 主题更新器
│ │ ├── CacheManager.php # 缓存管理
│ │ └── Handlers/
│ │ ├── HandlerInterface.php # 统一接口
│ │ ├── JsonHandler.php # JSON API
│ │ ├── GitHubHandler.php # GitHub
│ │ ├── GitLabHandler.php # GitLab
│ │ └── WenPaiGitHandler.php # 菲码源库
│ │
│ ├── AIBridge/
│ │ ├── AIGateway.php # AI 网关
│ │ ├── Interceptor.php # 请求拦截
│ │ ├── WPMindBridge.php # WPMind 桥接
│ │ ├── Passthrough.php # 透传模式
│ │ └── Adapters/
│ │ ├── AdapterInterface.php
│ │ ├── YoastAdapter.php
│ │ └── RankMathAdapter.php
│ │
│ └── Admin/
│ ├── AdminPage.php # 管理页面
│ ├── SourceEditor.php # 更新源编辑器
│ └── AISettings.php # AI 设置
├── templates/
│ └── admin/
│ ├── settings.php
│ ├── source-list.php
│ ├── source-editor.php
│ └── ai-settings.php
├── assets/
│ ├── css/
│ │ └── admin.css
│ └── js/
│ └── admin.js
└── languages/
└── wpbridge.pot
```

---

## 3. 数据模型

### 3.1 更新源数据结构

```php
// wp_options: wpbridge_sources
[
[
'id' => 'src_abc123', // 唯一标识
'name' => 'My Plugin Source', // 显示名称
'type' => 'json', // json|github|gitlab|wenpai|zip
'slug' => 'my-plugin', // 插件/主题 slug
'item_type' => 'plugin', // plugin|theme
'source_url' => 'https://...', // 更新源地址
'auth_type' => 'token', // none|token|basic|oauth
'auth_token' => 'encrypted:...', // 加密存储
'branch' => 'main', // Git 分支(可选)
'enabled' => true, // 是否启用
'priority' => 10, // 优先级
'created_at' => '2026-02-04 10:00:00',
'updated_at' => '2026-02-04 10:00:00',
],
// ...
]
```

### 3.2 缓存数据结构

```php
// transient: wpbridge_update_cache_{slug}
[
'version' => '2.0.0',
'download_url' => 'https://...',
'tested' => '6.4',
'requires' => '5.9',
'requires_php' => '7.4',
'last_checked' => 1707012000,
'source_id' => 'src_abc123',
]

// transient: wpbridge_source_health_{source_id}
[
'status' => 'healthy', // healthy|degraded|failed
'last_check' => 1707012000,
'error_count' => 0,
'last_error' => null,
]
```

### 3.3 AI 桥接配置

```php
// wp_options: wpbridge_ai_settings
[
'enabled' => true,
'mode' => 'wpmind', // disabled|passthrough|wpmind
'custom_endpoint' => '', // 透传模式的目标端点
'whitelist' => [ // 拦截白名单
'api.openai.com',
'api.anthropic.com',
],
'adapters' => [ // 启用的适配器
'yoast' => true,
'rankmath' => false,
],
]
```

---

## 4. 核心类设计

### 4.1 SourceHandlerInterface

```php
<?php
namespace WPBridge\UpdateSource\Handlers;

interface SourceHandlerInterface {
/**
* 获取处理器能力
* @return array ['auth' => [], 'version' => [], 'download' => []]
*/
public function getCapabilities(): array;

/**
* 获取最新版本信息
* @param string $identifier 源标识URL/仓库地址)
* @param array $options 选项(认证信息等)
* @return VersionInfo|null
*/
public function getLatestVersion(string $identifier, array $options = []): ?VersionInfo;

/**
* 获取下载 URL
* @param string $identifier 源标识
* @param string $version 版本号
* @param array $options 选项
* @return string|null
*/
public function getDownloadUrl(string $identifier, string $version, array $options = []): ?string;

/**
* 验证源可用性
* @param string $identifier 源标识
* @param array $options 选项
* @return HealthStatus
*/
public function checkHealth(string $identifier, array $options = []): HealthStatus;
}
```

### 4.2 VersionInfo 值对象

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

class VersionInfo {
public string $version;
public string $downloadUrl;
public ?string $tested = null;
public ?string $requires = null;
public ?string $requiresPhp = null;
public ?string $changelog = null;
public ?string $hash = null;
public ?int $fileSize = null;
public int $checkedAt;

public function __construct(string $version, string $downloadUrl) {
$this->version = $version;
$this->downloadUrl = $downloadUrl;
$this->checkedAt = time();
}

public function toArray(): array {
return [
'version' => $this->version,
'download_url' => $this->downloadUrl,
'tested' => $this->tested,
'requires' => $this->requires,
'requires_php' => $this->requiresPhp,
'changelog' => $this->changelog,
'hash' => $this->hash,
'file_size' => $this->fileSize,
'checked_at' => $this->checkedAt,
];
}
}
```

### 4.3 HealthStatus 值对象

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

class HealthStatus {
const STATUS_HEALTHY = 'healthy';
const STATUS_DEGRADED = 'degraded';
const STATUS_FAILED = 'failed';

public string $status;
public int $responseTime; // ms
public ?string $error = null;
public int $checkedAt;

public static function healthy(int $responseTime): self {
$status = new self();
$status->status = self::STATUS_HEALTHY;
$status->responseTime = $responseTime;
$status->checkedAt = time();
return $status;
}

public static function failed(string $error): self {
$status = new self();
$status->status = self::STATUS_FAILED;
$status->responseTime = 0;
$status->error = $error;
$status->checkedAt = time();
return $status;
}
}
```

---

## 5. WordPress 钩子集成

### 5.1 更新检查钩子

```php
// 插件更新检查
add_filter('pre_set_site_transient_update_plugins', [$this, 'checkPluginUpdates'], 10, 1);

// 主题更新检查
add_filter('pre_set_site_transient_update_themes', [$this, 'checkThemeUpdates'], 10, 1);

// 插件信息 API
add_filter('plugins_api', [$this, 'pluginInfo'], 20, 3);

// 主题信息 API
add_filter('themes_api', [$this, 'themeInfo'], 20, 3);

// 下载包过滤
add_filter('upgrader_pre_download', [$this, 'filterDownload'], 10, 3);
```

### 5.2 AI 拦截钩子

```php
// HTTP 请求拦截(优先级 1最早执行
add_filter('pre_http_request', [$this, 'interceptAIRequest'], 1, 3);
```

---

## 6. 错误处理与日志

### 6.1 错误码定义

```php
class ErrorCodes {
// 源相关错误 (1xxx)
const SOURCE_NOT_FOUND = 1001;
const SOURCE_UNREACHABLE = 1002;
const SOURCE_INVALID_RESPONSE = 1003;
const SOURCE_AUTH_FAILED = 1004;

// 下载相关错误 (2xxx)
const DOWNLOAD_FAILED = 2001;
const DOWNLOAD_HASH_MISMATCH = 2002;
const DOWNLOAD_SIZE_EXCEEDED = 2003;
const DOWNLOAD_INVALID_ZIP = 2004;

// AI 桥接错误 (3xxx)
const AI_WPMIND_UNAVAILABLE = 3001;
const AI_ENDPOINT_UNREACHABLE = 3002;
const AI_RESPONSE_INVALID = 3003;

// 配置错误 (4xxx)
const CONFIG_INVALID = 4001;
const CONFIG_ENCRYPTION_FAILED = 4002;
}
```

### 6.2 日志级别

```php
class Logger {
const LEVEL_DEBUG = 'debug';
const LEVEL_INFO = 'info';
const LEVEL_WARNING = 'warning';
const LEVEL_ERROR = 'error';

public function log(string $level, string $message, array $context = []): void;
public function debug(string $message, array $context = []): void;
public function info(string $message, array $context = []): void;
public function warning(string $message, array $context = []): void;
public function error(string $message, array $context = []): void;
}
```

---

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

1509
DESIGN.md Normal file

File diff suppressed because it is too large Load diff

417
DEVELOPMENT-PLAN.md Normal file
View file

@ -0,0 +1,417 @@
# WPBridge 开发计划

> 完整的开发计划和任务分解

*创建日期: 2026-02-04*

---

## 一、项目概述

### 1.1 项目定位

**WPBridge文派云桥** - 自定义源桥接器,让用户完全控制 WordPress 的外部连接。

### 1.2 核心价值

1. **自定义更新源桥接** - 支持自托管更新服务器、商业插件更新源
2. **性能优化** - 并行请求、智能缓存、减少后台加载时间
3. **AI 服务桥接** - OpenAI API 兼容层,可选依赖 WPMind

### 1.3 非目标

- 不替代 WordPress.org 官方源(由文派叶子 WPCY 负责)
- 不提供镜像/CDN 服务
- 不破解/绕过商业插件授权

---

## 二、版本规划

```
v0.1.0 MVP2-3 周)
v0.2.0 性能优化 + Git 支持2-3 周)
v0.3.0 AI 桥接 + 商业插件2-3 周)
v0.4.0 Cloud API可选1-2 周)
v1.0.0 正式发布1-2 周)
```

---

## 三、v0.1.0 MVP 详细计划

### 3.1 目标

实现最小可用的更新源桥接功能,确保基础稳定性。

### 3.2 范围

| 包含 | 不包含(移至后续版本)|
|------|----------------------|
| JSON API 桥接 | Git 仓库支持 |
| 预置源文派开源、ArkPress、AspireCloud| WP-CLI |
| 基础缓存和降级 | 诊断工具 |
| 简单管理界面 | 配置导入导出 |
| 安全基础 | FAIR 支持 |

### 3.3 任务分解

#### 阶段 1插件骨架Day 1-2

```
任务 1.1: 创建插件主文件
- wpbridge.php插件头信息、激活/停用钩子)
- 预计2 小时

任务 1.2: 自动加载器
- includes/Core/Loader.php
- PSR-4 风格自动加载
- 预计1 小时

任务 1.3: 插件主类
- includes/Core/Plugin.php
- 单例模式,初始化各模块
- 预计2 小时

任务 1.4: 设置管理
- includes/Core/Settings.php
- wp_options 读写封装
- 预计2 小时
```

#### 阶段 2数据模型Day 3-4

```
任务 2.1: 源类型枚举
- includes/UpdateSource/SourceType.php
- 统一定义所有源类型
- 预计1 小时

任务 2.2: 更新源模型
- includes/UpdateSource/SourceModel.php
- 数据结构、验证、序列化
- 预计2 小时

任务 2.3: 源管理器
- includes/UpdateSource/SourceManager.php
- CRUD 操作、预置源加载
- 预计3 小时

任务 2.4: 预置源配置
- includes/UpdateSource/PresetSources.php
- 文派开源、ArkPress、AspireCloud
- 预计2 小时
```

#### 阶段 3核心桥接Day 5-8

```
任务 3.1: 处理器接口
- includes/UpdateSource/Handlers/HandlerInterface.php
- 统一接口定义
- 预计1 小时

任务 3.2: JSON 处理器
- includes/UpdateSource/Handlers/JsonHandler.php
- Plugin Update Checker 格式兼容
- 预计3 小时

任务 3.3: ArkPress 处理器
- includes/UpdateSource/Handlers/ArkPressHandler.php
- AspireCloud API 兼容
- 预计3 小时

任务 3.4: AspireCloud 处理器
- includes/UpdateSource/Handlers/AspireCloudHandler.php
- 预计2 小时

任务 3.5: 插件更新器
- includes/UpdateSource/PluginUpdater.php
- pre_set_site_transient_update_plugins 钩子
- plugins_api 钩子
- 预计4 小时

任务 3.6: 主题更新器
- includes/UpdateSource/ThemeUpdater.php
- pre_set_site_transient_update_themes 钩子
- themes_api 钩子
- 预计3 小时
```

#### 阶段 4缓存与降级Day 9-10

```
任务 4.1: 缓存管理器
- includes/Cache/CacheManager.php
- Transient 缓存封装
- 预计2 小时

任务 4.2: 源健康检查
- includes/Cache/HealthChecker.php
- 连通性测试、状态缓存
- 预计2 小时

任务 4.3: 降级策略
- includes/Cache/FallbackStrategy.php
- 过期缓存兜底、失败冷却
- 预计2 小时
```

#### 阶段 5安全与日志Day 11-12

```
任务 5.1: 输入校验
- includes/Security/Validator.php
- URL 格式、版本号、JSON 结构
- 预计2 小时

任务 5.2: 密钥加密
- includes/Security/Encryption.php
- API Key 加密存储
- 预计2 小时

任务 5.3: 日志系统
- includes/Core/Logger.php
- 调试日志、错误日志
- 预计2 小时
```

#### 阶段 6管理界面Day 13-15

```
任务 6.1: 管理页面
- includes/Admin/AdminPage.php
- 设置页面注册
- 预计2 小时

任务 6.2: 源列表界面
- templates/admin/source-list.php
- WP_List_Table 实现
- 预计3 小时

任务 6.3: 源编辑表单
- templates/admin/source-editor.php
- 添加/编辑更新源
- 预计3 小时

任务 6.4: 样式和脚本
- assets/css/admin.css
- assets/js/admin.js
- 预计2 小时
```

### 3.4 文件结构

```
wpbridge/
├── wpbridge.php # 主文件
├── includes/
│ ├── Core/
│ │ ├── Plugin.php # 插件主类
│ │ ├── Loader.php # 自动加载
│ │ ├── Settings.php # 设置管理
│ │ └── Logger.php # 日志系统
│ │
│ ├── UpdateSource/
│ │ ├── SourceType.php # 源类型枚举
│ │ ├── SourceModel.php # 数据模型
│ │ ├── SourceManager.php # 源管理器
│ │ ├── PresetSources.php # 预置源配置
│ │ ├── PluginUpdater.php # 插件更新器
│ │ ├── ThemeUpdater.php # 主题更新器
│ │ └── Handlers/
│ │ ├── HandlerInterface.php
│ │ ├── JsonHandler.php
│ │ ├── ArkPressHandler.php
│ │ └── AspireCloudHandler.php
│ │
│ ├── Cache/
│ │ ├── CacheManager.php # 缓存管理
│ │ ├── HealthChecker.php # 健康检查
│ │ └── FallbackStrategy.php # 降级策略
│ │
│ ├── Security/
│ │ ├── Validator.php # 输入校验
│ │ └── Encryption.php # 密钥加密
│ │
│ └── Admin/
│ └── AdminPage.php # 管理页面
├── templates/
│ └── admin/
│ ├── source-list.php
│ └── source-editor.php
└── assets/
├── css/
│ └── admin.css
└── js/
└── admin.js
```

### 3.5 验收标准

1. **功能验收**
- [ ] 可添加自定义 JSON 更新源
- [ ] 预置源(文派开源)可正常检查更新
- [ ] 更新信息正确显示在 WordPress 后台
- [ ] 可下载并安装更新

2. **稳定性验收**
- [ ] 源不可用时不阻塞后台
- [ ] 缓存正常工作
- [ ] 错误信息用户友好

3. **安全验收**
- [ ] URL 格式校验有效
- [ ] API Key 加密存储
- [ ] 无 XSS/SQL 注入风险

---

## 四、v0.2.0 性能优化 + Git 支持

### 4.1 目标

实现性能优化核心功能,支持 Git 仓库作为更新源。

### 4.2 关键任务

#### 性能优化
- [ ] 并行请求管理器ParallelRequestManager
- [ ] 请求去重器RequestDeduplicator
- [ ] 条件请求ConditionalRequest
- [ ] 缓存分层(对象缓存 + DB
- [ ] WP-Cron 后台预热BackgroundUpdater

#### Git 仓库支持
- [ ] GitHub 处理器GitHubHandler
- [ ] GitLab 处理器GitLabHandler
- [ ] Gitee 处理器GiteeHandler
- [ ] 菲码源库处理器WenPaiGitHandler
- [ ] 私有仓库认证

#### WP-CLI
- [ ] `wp bridge source` 命令组
- [ ] `wp bridge check` 命令
- [ ] `wp bridge cache` 命令
- [ ] `wp bridge diagnose` 命令
- [ ] `wp bridge config` 命令

#### 诊断工具
- [ ] 诊断页面
- [ ] 源连通性测试
- [ ] 诊断报告导出

---

## 五、v0.3.0 AI 桥接 + 商业插件

### 5.1 目标

实现 AI 服务桥接和商业插件支持。

### 5.2 关键任务

#### AI 桥接
- [ ] AI 设置数据模型AISettings
- [ ] AI 桥接主类AIBridge
- [ ] OpenAI 代理OpenAIProxy
- [ ] WPMind 转发器WPMindForwarder
- [ ] 白名单管理界面

#### 商业插件
- [ ] 商业插件检测
- [ ] 更新源覆盖
- [ ] 版本锁定
- [ ] 回滚机制

#### 源分组
- [ ] 源组数据模型
- [ ] 批量管理界面

---

## 六、v0.4.0 Cloud API

### 6.1 目标

提供云端 API 服务。

### 6.2 关键任务

- [ ] REST API 端点
- [ ] 认证机制
- [ ] 限流策略
- [ ] 文派叶子集成示例

---

## 七、v1.0.0 正式发布

### 7.1 目标

稳定版本,完善文档和用户体验。

### 7.2 关键任务

- [ ] 用户指南
- [ ] 开发者文档
- [ ] API 文档
- [ ] 设置向导
- [ ] 状态仪表板
- [ ] GitHub Release
- [ ] 菲码源库 Release

---

## 八、技术规范

### 8.1 编码规范

- PHP 7.4+ 兼容
- WordPress 编码标准
- PSR-4 自动加载
- 类型声明PHP 7.4 风格)

### 8.2 测试要求

- 单元测试覆盖核心逻辑
- 集成测试覆盖 WordPress 钩子
- 手动测试覆盖 UI 交互

### 8.3 安全要求

- 所有用户输入必须校验
- 敏感数据加密存储
- 遵循 WordPress 安全最佳实践

---

## 九、风险与缓解

| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 第三方 API 变更 | 处理器失效 | 模块化设计,易于更新 |
| 性能问题 | 后台卡顿 | 缓存优先,异步处理 |
| 安全漏洞 | 数据泄露 | 代码审计,安全测试 |
| 兼容性问题 | 插件冲突 | 最小化钩子使用,命名空间隔离 |

---

## 十、里程碑

| 里程碑 | 目标日期 | 交付物 |
|--------|----------|--------|
| M1: MVP 完成 | +3 周 | v0.1.0 可用版本 |
| M2: 性能优化 | +6 周 | v0.2.0 性能版本 |
| M3: AI 桥接 | +9 周 | v0.3.0 完整版本 |
| M4: 正式发布 | +12 周 | v1.0.0 稳定版本 |

---

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

463
DISCUSSION.md Normal file
View file

@ -0,0 +1,463 @@
# WPBridge 讨论记录

> 产品设计和技术讨论的记录

*创建日期: 2026-02-04*

---

## 2026-02-04 - 最终定位确定

### 背景

经过讨论,明确了 WPBridge 在文派生态中的定位:

- **文派叶子 (WPCY)** - 官方源加速WordPress.org → 文派镜像),面向普通用户
- **WPBridge (文派云桥)** - 自定义源桥接,面向开发者/高级用户

### 最终定位

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

### 核心功能

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

### 与文派叶子的分工

| 功能 | 文派叶子 (WPCY) | WPBridge |
|------|-----------------|----------|
| WordPress.org 加速 | ✅ 主要功能 | ❌ 不做 |
| 自托管更新服务器 | ❌ 不做 | ✅ 主要功能 |
| 商业插件更新源 | ❌ 不做 | ✅ 主要功能 |
| AI 服务桥接 | ❌ 不做 | ✅ 主要功能 |
| 目标用户 | 普通用户 | 开发者/高级用户 |

---

## 2026-02-04 - 愿景演进记录

### 初始愿景AI Gateway

最初从 WPMind 的 AI Gateway 功能讨论中独立出来,定位为:
- OpenAI API 兼容层
- 商业插件 AI 适配器

### 扩展愿景WordPress 云桥

讨论中发现更广阔的需求:
- 插件更新桥接
- 资源访问桥接
- 授权验证桥接

### 最终愿景:自定义源桥接器

明确与文派叶子的分工后,聚焦于:
- 自托管更新服务器
- 商业插件更新源
- AI 服务桥接

---

## 文派生态全景

```
文派生态 (WenPai.org)
├── 📦 WPMirror (wpmirror.com)
│ └── 镜像源基础设施
├── 🇨🇳 LitePress (litepress.cn)
│ └── WordPress 中国定制版
├── 🍃 文派叶子 WPCY (wpcy.com)
│ ├── 中国源加速
│ ├── 插件/主题更新加速
│ ├── 翻译下载优化
│ └── 面向普通用户
├── 🤖 WPMind 文派心思
│ └── 纯 AI 应用(国内 AI 服务)
└── 🌉 WPBridge 文派云桥
├── 自定义更新源桥接
├── 商业插件更新源
├── AI 服务桥接
└── 面向开发者/高级用户
```

---

## 待深入讨论

### 1. 更新源桥接技术细节

**问题**:如何实现自托管更新服务器的桥接?

**技术方案**
- 使用 `pre_set_site_transient_update_plugins` 钩子
- 使用 `pre_set_site_transient_update_themes` 钩子
- 自定义更新检查逻辑

**待讨论**
- 更新源 API 格式标准?
- 是否兼容现有的更新服务器方案?

---

### 2. 商业插件更新源

**问题**:如何处理商业插件的更新源覆盖?

**挑战**
- 不同商业插件有不同的更新机制
- 部分插件有授权验证
- 需要保持兼容性

**待讨论**
- 是否提供预置的商业插件配置?
- 如何处理授权验证?

---

### 3. AI 桥接与 WPMind 的关系

**问题**AI 桥接功能是否依赖 WPMind

**方案**
- 无 WPMind仅支持 OpenAI 兼容层(用户自己配置 API Key
- 有 WPMind支持国内 AI 服务商(使用 WPMind 的 Provider

**待讨论**
- 这种可选依赖的设计是否合理?

---

### 4. 与文派叶子的集成

**问题**WPBridge 是否需要与文派叶子集成?

**选项**
- A) 完全独立,互不干扰
- B) 检测文派叶子,提供互补功能
- C) 作为文派叶子的"高级扩展"

**待讨论**

---

### 5. 分发和定价

**问题**WPBridge 如何分发和定价?

**分发选项**
- GitHub Releases
- 文派官网
- WordPress.org如果符合规范

**定价选项**
- 完全免费开源
- 核心免费 + 高级功能付费
- 订阅制

**待讨论**

---

## 2026-02-04 - 预置源与自托管方案讨论

### 背景

讨论了 WPBridge 需要支持的预置更新源和自托管方案。

### 关键决策

#### 1. 预置更新源

| 源 | 类型 | 说明 |
|------|------|------|
| **文派开源更新源** | 预置(默认) | 文派生态的开源插件/主题更新源 |
| **AspireCloud** | 可选预置 | AspirePress 的 CDN/API |
| **FAIR** | 可选预置 | Linux Foundation 的去中心化方案 |

#### 2. 自托管方案支持

| 方案 | 说明 | 兼容方式 |
|------|------|----------|
| **ArkPress** | 文派开源的自托管组件AspireCloud 分叉版本,针对中国用户优化 | 原生支持 |
| **AspireCloud** | AspirePress 的开源镜像/CDN | API 兼容 |
| **UpdatePulse Server** | 支持授权管理的自托管服务器 | JSON API 兼容 |
| **WP Packages Update Server** | GitHub 上的开源方案 | JSON API 兼容 |
| **Plugin Update Checker 格式** | 事实上的行业标准 | 原生支持 |

#### 3. WP-CLI 命令规范

确定使用 `wp bridge` 作为命令前缀(而非 `wp wpbridge`)。

```bash
# 源管理
wp bridge source list # 列出所有源
wp bridge source add <url> [--type=json] # 添加源
wp bridge source remove <id> # 删除源
wp bridge source enable <id> # 启用源
wp bridge source disable <id> # 禁用源

# 更新检查
wp bridge check # 检查所有源
wp bridge check <slug> # 检查指定插件/主题

# 缓存管理
wp bridge cache clear # 清除所有缓存
wp bridge cache status # 查看缓存状态

# 诊断
wp bridge diagnose # 生成诊断报告
wp bridge test <source_id> # 测试源连通性

# 配置
wp bridge config export # 导出配置
wp bridge config import <file> # 导入配置
```

### 待讨论事项

#### 1. 源优先级和冲突处理

**问题**:如果同一个插件在多个源都有更新,如何处理?

**决策**:用户可设置优先级,默认选择版本号最高的

#### 2. 源分组和批量管理

**场景**:用户有多个来自同一厂商的商业插件

**决策**:引入"源组"概念支持批量管理v0.3.0

#### 3. 更新通知策略

**决策**:与 WordPress 原生更新通知合并,邮件/Webhook 作为高级功能

#### 4. 版本锁定

**决策**v0.2.0 实现基础版本锁定功能

#### 5. 安全扫描集成

**决策**本地哈希校验为基础VirusTotal/Patchstack 作为付费功能

#### 6. 统计和分析

**决策**:默认不收集,匿名统计作为可选(需用户同意)

#### 7. 白标/OEM 支持

**决策**作为付费功能v1.0.0 后考虑

---

## 2026-02-04 - Codex 深度讨论(第二轮)

### 讨论主题

基于市场研究报告,与 Codex 进行了 5 个核心问题的深度讨论。

### 关键结论

#### 1. 性能优化作为核心卖点

**结论:应当成为核心价值主张之一**

**"双锚点"价值主张:**
- 对普通痛点:后台加载快
- 对目标用户:可控的自定义源桥接

**7 项性能优化措施:**
1. 请求合并与并行(`Requests::request_multiple`
2. 智能缓存(结果缓存 + 健康状态缓存)
3. 请求去重(短时间合并窗口 + 锁)
4. 分组检查(同一厂商多插件共享一次请求)
5. 条件请求(`If-Modified-Since` / `ETag`
6. 缓存分层Redis/Memcached 优先DB 兜底)
7. 失败兜底(返回旧版本信息,不阻塞后台)

#### 2. 与 FAIR 的关系

- WPBridge 把 FAIR 作为**可选更新源**之一
- 默认不替换 WordPress.org
- 产品表述:**"WPBridge 不替换官方源,只做用户自定义桥接"**

#### 3. 商业插件法律边界

**稳妥策略:**
- 以"配置框架"优先,不预置具体厂商地址
- 仅在得到授权时提供预置
- 坚持"用户提供授权信息"
- 避免"绕过授权"功能
- 本地缓存仅限站点内

**对外表述:**
> "WPBridge 不破解、不绕过授权,仅提供合规的配置与转发能力。"

#### 4. 中国市场特殊性

**与 WPCY 协同:**
- WPBridge 检测 WPCY 存在时,官方源自动走 WPCY
- 避免重复功能

**中国特色功能:**
- 源级别的"优选地域节点/备用源"
- 国内 Git 供应商支持Gitee
- 更短超时 + 智能重试策略

#### 5. 技术架构建议

**WP-Cron 后台任务:**
- 引入 `wpbridge_update_sources` 定时任务
- 前台仅读取缓存,避免后台页面发起大量请求

**降级策略:**
1. 优先使用"上次成功缓存"
2. 标记源为 `degraded/failed`
3. 自动切换备用源
4. 管理界面提示,不阻塞后台

---

## 2026-02-04 - Codex 评审反馈

### 评审概要

使用 OpenAI Codex (gpt-5.2-codex-xhigh) 对项目文档进行了全面评审。

### 主要反馈

#### 1. 项目定位与边界

**认可:**
- 定位整体清晰,"自定义源桥接"与"官方源加速"形成互补
- 目标用户画像准确

**建议:**
- 边界需要再硬化,明确"非目标"
- 云 API 定义为"桥接能力的远程形态",不承担镜像/加速职责
- 补充"使用动机"优先级排序

#### 2. 技术架构

**建议改进:**
- 更新源适配器做成**统一接口+能力矩阵**
- **缓存与降级策略**要提前设计
- **安全边界**URL 校验、密钥加密存储、下载包校验
- AI 拦截做成**显式白名单**
- 云 API 需明确认证、限流与可用性 SLA

#### 3. 路线图优先级

**建议调整:**
- 把"稳定性/可运维能力"前置
- 先做"通用自定义源"跑通,再添加预置模板
- AI 桥接可与核心解耦

#### 4. AI 桥接与 WPMind 依赖

**建议:**
- 明确"无 WPMind 时"的稳定行为
- 定义稳定接口层,减少直接耦合

#### 5. 定价策略

**建议:**
- 考虑"基础终身 + 更新/支持年费"混合模式
- 付费点围绕企业刚需

### 采纳决策

| 建议 | 决策 | 说明 |
|------|------|------|
| 明确"非目标" | ✅ 采纳 | 添加到 CLAUDE.md |
| 统一接口+能力矩阵 | ✅ 采纳 | 更新架构设计 |
| 缓存与降级策略 | ✅ 采纳 | 加入 v0.1.0 |
| 安全边界设计 | ✅ 采纳 | 加入 v0.1.0 |
| AI 拦截白名单 | ✅ 采纳 | 更新设计文档 |
| 稳定性前置 | ✅ 采纳 | 调整路线图 |
| 混合定价模式 | 🔄 待定 | 需进一步讨论 |

---

## 2026-02-04 - Codex 评审问题修复

### 问题清单与修复状态

#### HIGH 严重性

| 问题 | 状态 | 修复说明 |
|------|------|----------|
| 源类型不一致 | ✅ 已修复 | 创建统一的 `SourceType` 枚举包含所有类型json/github/gitlab/gitee/wenpai_git/zip/arkpress/aspirecloud/fair/puc更新数据模型引用 |
| AI 设置配置与实现不匹配 | ✅ 已修复 | 创建 `AISettings` 数据模型,`mode` 和 `whitelist` 由用户配置驱动,不再硬编码 |

#### MEDIUM 严重性

| 问题 | 状态 | 修复说明 |
|------|------|----------|
| Cloud API 不在路线图 | ✅ 已修复 | 添加 v0.4.0 版本专门处理 Cloud API |
| v0.1.0 范围太大 | ✅ 已修复 | 精简为 MVP移除 Git 支持、WP-CLI、诊断工具到 v0.2.0 |
| 性能优化未明确 | ✅ 已修复 | 性能优化明确放在 v0.2.0,作为该版本核心任务 |

### 修改的文件

1. **DESIGN.md**
- 添加 `SourceType` 枚举统一定义
- 添加类型与处理器映射表
- 更新数据模型引用枚举
- 重写 `AISettings` 和 `AIBridge` 类,配置驱动

2. **ROADMAP.md**
- 调整版本规划v0.1.0 → v0.2.0 → v0.3.0 → v0.4.0 → v1.0.0
- 精简 v0.1.0 范围为 MVP
- 性能优化移至 v0.2.0 核心任务
- 商业插件适配移至 v0.3.0
- 新增 v0.4.0 Cloud API

3. **DEVELOPMENT-PLAN.md**(新建)
- 完整的开发计划
- 详细的任务分解
- 文件结构设计
- 验收标准
- 风险与缓解
- 里程碑规划

---

## 下一步行动

1. [x] 深入讨论更新源桥接的技术细节
2. [x] 确定与文派叶子的集成方式
3. [x] 确定 AI 桥接与 WPMind 的依赖关系
4. [x] 确定分发和定价策略
5. [x] 完成业务流程和架构设计
6. [x] 修复 Codex 评审问题
7. [x] 创建完整开发计划
8. [ ] Codex 最终确认
9. [ ] 开始 v0.1.0 开发

---

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

382
RESEARCH.md Normal file
View file

@ -0,0 +1,382 @@
# WPBridge 市场研究报告

> WordPress 更新生态现状与 WPBridge 机会分析

*创建日期: 2026-02-04*

---

## 1. WordPress 更新生态现状

### 1.1 核心痛点:后台卡死问题

WordPress 后台慢是一个普遍问题,主要原因之一是**大量插件各自发起更新检查请求**

```
WordPress 后台加载时
├── 核心更新检查 → api.wordpress.org
├── 插件 A 更新检查 → plugin-a.com/api
├── 插件 B 更新检查 → plugin-b.com/api
├── 插件 C 授权验证 → license.plugin-c.com
├── 主题更新检查 → theme-vendor.com/api
├── ...(可能 10-50 个请求)
└── 结果:后台加载 5-30 秒
```

**问题根源:**
- 每个商业插件都有自己的更新服务器
- 每个插件独立发起 HTTP 请求
- 没有统一的缓存和批量机制
- 授权验证增加额外延迟
- 中国用户访问国外服务器更慢

### 1.2 生态碎片化

| 更新源类型 | 示例 | 问题 |
|-----------|------|------|
| WordPress.org | 官方免费插件 | 中国访问慢 |
| 商业插件自建 | Elementor Pro, ACF Pro | 各自为政 |
| GitHub Releases | 开发者插件 | 需要手动配置 |
| EDD Software Licensing | 大量商业插件 | 每个都要授权检查 |
| WooCommerce.com | WooCommerce 扩展 | 独立的更新系统 |
| Envato Market | ThemeForest 主题 | 又一个独立系统 |

---

## 2. 行业解决方案分析

### 2.1 FAIR Package Manager

**背景:** 2024年9月 WP Engine 事件后Linux Foundation 发起的去中心化项目

**定位:** 联邦式独立仓库Federated and Independent Repository

**核心特点:**
- 去中心化的插件/主题分发
- 多镜像源支持
- 由 Linux Foundation 治理
- 300+ 贡献者参与

**技术实现:**
- WordPress 插件形式
- 替换默认的 WordPress.org 更新源
- 支持多个可信源

**挑战:**
- 需要大规模采用才能有效
- 托管商需要主动支持
- 安全性担忧(多个潜在攻击点)
- 不解决商业插件问题

**与 WPBridge 的区别:**

| 方面 | FAIR | WPBridge |
|------|------|----------|
| 定位 | 基础设施替代 | 用户侧桥接器 |
| 目标 | 替换 WordPress.org | 补充自定义源 |
| 用户 | 所有 WordPress 用户 | 开发者/高级用户 |
| 商业插件 | 不直接解决 | 核心功能 |
| 依赖 | 需要生态采用 | 用户自主配置 |

### 2.2 Plugin Update Checker

**作者:** YahnisElsts

**定位:** 开发者库,用于自定义更新服务器

**特点:**
- PHP 库,嵌入插件代码
- 支持 JSON API、GitHub、GitLab
- 被大量商业插件使用
- 事实上的行业标准

**局限:**
- 每个插件独立集成
- 用户无法统一管理
- 不解决性能问题

### 2.3 现有更新管理插件

| 插件 | 功能 | 局限 |
|------|------|------|
| Easy Updates Manager | 控制自动更新 | 不支持自定义源 |
| ManageWP | 多站点管理 | SaaS 服务,需付费 |
| MainWP | 自托管多站点 | 复杂,面向代理商 |
| InfiniteWP | 多站点管理 | 不解决更新源问题 |

---

## 3. WPBridge 机会分析

### 3.1 差异化定位

```
市场空白
├── FAIR → 基础设施层(替换 WordPress.org
├── Plugin Update Checker → 开发者层(嵌入插件)
└── WPBridge → 用户层(统一管理自定义源)
├── 不替换 WordPress.org
├── 不需要修改插件代码
└── 用户自主配置和管理
```

### 3.2 核心价值主张

**对于企业用户:**
- 内网部署,私有仓库
- 供应链安全控制
- 合规性要求

**对于开发者:**
- 测试环境灵活配置
- 自托管插件分发
- 版本控制和回滚

**对于商业插件用户:**
- 统一管理多个更新源
- 减少授权验证延迟
- 备用更新渠道

### 3.3 性能优化机会

```
当前状态(无 WPBridge
├── 插件 A → HTTP 请求 1
├── 插件 B → HTTP 请求 2
├── 插件 C → HTTP 请求 3
└── 总计N 个串行请求

使用 WPBridge 后
├── WPBridge 统一检查
│ ├── 缓存命中 → 直接返回
│ ├── 批量请求 → 减少连接数
│ └── 并行处理 → 减少等待
└── 总计:显著减少请求时间
```

---

## 4. 竞争格局

### 4.1 直接竞争

| 竞品 | 定位 | WPBridge 优势 |
|------|------|---------------|
| FAIR | 基础设施替代 | 更轻量,用户自主 |
| AspirePress | 类似 FAIR | 同上 |

### 4.2 间接竞争

| 竞品 | 定位 | WPBridge 优势 |
|------|------|---------------|
| ManageWP | SaaS 多站点 | 自托管,无订阅 |
| MainWP | 自托管多站点 | 更简单,专注更新 |
| 文派叶子 | 官方源加速 | 自定义源,互补 |

### 4.3 合作机会

- **文派叶子**:官方源加速 + WPBridge 自定义源 = 完整解决方案
- **FAIR**:可以作为 WPBridge 的一个更新源
- **Plugin Update Checker**:兼容其 JSON 格式

---

## 5. 建议

### 5.1 核心功能优先级

| 优先级 | 功能 | 理由 |
|--------|------|------|
| P0 | 自定义 JSON 更新源 | 最通用,兼容 PUC |
| P0 | 缓存和性能优化 | 解决核心痛点 |
| P1 | GitHub/GitLab 支持 | 开发者刚需 |
| P1 | 源健康检查 | 稳定性保障 |
| P2 | 商业插件预置 | 提升易用性 |
| P3 | AI 桥接 | 差异化功能 |

### 5.2 差异化策略

1. **性能优先**:强调减少后台加载时间
2. **用户自主**:不依赖外部服务,用户完全控制
3. **生态兼容**与文派叶子、FAIR 互补而非竞争
4. **中国优化**:针对中国网络环境优化

### 5.3 市场定位

```
WPBridge 一句话定位:

"让 WordPress 后台不再卡死 —— 统一管理你的插件更新源"


"自定义源桥接器 —— 企业内网、商业插件、私有仓库,一个插件搞定"
```

---

## 6. 自托管更新服务器生态

### 6.1 主要方案对比

| 方案 | 类型 | 特点 | 适用场景 |
|------|------|------|----------|
| **ArkPress** | 自托管镜像 | AspireCloud 分叉,中国优化 | 中国企业/开发者 |
| **AspireCloud** | 开源镜像 | FAIR 基础设施,联邦模式 | 国际用户 |
| **UpdatePulse Server** | 自托管服务器 | 授权管理、VCS 集成 | 商业插件开发者 |
| **WP Packages Update Server** | 自托管服务器 | 轻量级 | 小型团队 |
| **Plugin Update Checker** | 开发者库 | 行业标准 | 插件开发者 |

### 6.2 ArkPress文派开源

**定位**AspireCloud 的中国分叉版本,针对中国网络环境优化

**特点**
- 国内服务器部署
- 中文界面
- 与文派生态深度集成
- 更适合中国用户的网络环境

**与 WPBridge 的关系**
- ArkPress 是服务端(自托管更新服务器)
- WPBridge 是客户端(连接各种更新源的桥接器)
- 两者配合使用,提供完整的自托管更新解决方案

### 6.3 AspirePress 生态

**组件**
- **AspireCloud**CDN/API 服务,为 FAIR 提供基础设施
- **AspireUpdate**WordPress 插件,连接 AspireCloud
- **AspireSync**:同步工具
- **AspireExplorer**:浏览器界面

**参考**
- [AspirePress 官网](https://aspirepress.org/)
- [AspireCloud 文档](https://docs.aspirepress.org/aspirecloud/)
- [AspireUpdate GitHub](https://github.com/aspirepress/AspireUpdate/)

### 6.4 UpdatePulse Server

**特点**
- 支持授权管理License Key
- 支持 VCS 集成GitHub/GitLab/Bitbucket
- 支持云存储S3 兼容)
- 与 Plugin Update Checker 兼容

**参考**
- [UpdatePulse Server - WordPress.org](https://wordpress.org/plugins/updatepulse-server/)
- [UpdatePulse Server - 文派](https://wenpai.org/plugins/updatepulse-server/)

### 6.5 WPBridge 兼容策略

```
WPBridge 兼容层
├── 原生支持
│ ├── 文派开源更新源
│ ├── ArkPress API
│ ├── Plugin Update Checker JSON 格式
│ └── AspireCloud API
├── 通用 JSON API 支持
│ ├── UpdatePulse Server
│ ├── WP Packages Update Server
│ └── 其他兼容 PUC 格式的服务器
└── Git 仓库支持
├── GitHub Releases
├── GitLab Releases
├── Gitee Releases国内
└── 菲码源库(文派)
```

---

## 7. 技术实现研究

- [FAIR Package Manager - WP Umbrella](https://wp-umbrella.com/blog/the-fair-package-manager/)
- [FAIR Package Manager - WPShout](https://wpshout.com/fair-package-manager-wordpress-org-alternative/)
- [Plugin Update Checker - GitHub](https://github.com/YahnisElsts/plugin-update-checker)
- [75 Slow WordPress Plugins](https://onlinemediamasters.com/slow-wordpress-plugins/)
- [Speed Up WordPress Backend - WPShout](https://wpshout.com/speed-up-wordpress-backend/)

---

## 7. 技术实现研究

### 7.1 WordPress 并行请求 API

WordPress 内置 `Requests::request_multiple()` 方法,支持并行 HTTP 请求:

```php
// 并行请求示例
$requests = [
'source1' => ['url' => 'https://api1.example.com/update.json'],
'source2' => ['url' => 'https://api2.example.com/update.json'],
'source3' => ['url' => 'https://api3.example.com/update.json'],
];

$responses = \WpOrg\Requests\Requests::request_multiple(
$requests,
['timeout' => 10]
);

// 结果3 个请求并行执行,总时间 ≈ 最慢的那个请求
// 而非串行执行的 3 倍时间
```

**参考:**
- [WordPress Trac #33055 - Support Parallel HTTP Requests](https://core.trac.wordpress.org/ticket/33055)
- [WordPress Trac #44118 - Unnecessary plugin update checks](https://core.trac.wordpress.org/ticket/44118)

### 7.2 更新检查钩子

WordPress 使用以下钩子处理更新检查:

| 钩子 | 用途 | WPBridge 使用方式 |
|------|------|-------------------|
| `pre_set_site_transient_update_plugins` | 插件更新检查前 | 注入自定义源的更新信息 |
| `pre_set_site_transient_update_themes` | 主题更新检查前 | 注入自定义源的更新信息 |
| `plugins_api` | 插件详情 API | 返回自定义源的插件信息 |
| `themes_api` | 主题详情 API | 返回自定义源的主题信息 |
| `upgrader_pre_download` | 下载前 | 替换下载 URL |

### 7.3 性能优化技术栈

| 技术 | 用途 | WordPress 支持 |
|------|------|----------------|
| `Requests::request_multiple` | 并行 HTTP 请求 | ✅ 内置 |
| `set_transient` / `get_transient` | 数据库缓存 | ✅ 内置 |
| `wp_cache_*` | 对象缓存 | ✅ 内置(需 Redis/Memcached |
| `wp_schedule_event` | 后台定时任务 | ✅ 内置 |
| HTTP 条件请求 | ETag/Last-Modified | ✅ 需手动实现 |

### 7.4 与 WPCY 协同检测

```php
// 检测文派叶子是否存在
function wpbridge_detect_wpcy(): bool {
return defined('STARTER_PLUGIN_VERSION') ||
class_exists('WP_China_Yes') ||
function_exists('wpcy_is_active');
}

// 如果 WPCY 存在,官方源走 WPCY自定义源走 WPBridge
function wpbridge_should_handle_source(string $url): bool {
if (wpbridge_detect_wpcy()) {
// 官方源让 WPCY 处理
if (strpos($url, 'api.wordpress.org') !== false) {
return false;
}
}
return true;
}
```

---

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

404
ROADMAP.md Normal file
View file

@ -0,0 +1,404 @@
# WPBridge 开发路线图

> 自定义源桥接器 - 版本规划和开发任务

*创建日期: 2026-02-04*

---

## 当前版本: v0.8.0

## 版本规划

```
v0.1.0 - MVP最小可用桥接 + 基础缓存/降级 ✅ 已完成
v0.2.0 - Git 仓库支持 + WP-CLI + 性能优化 ✅ 已完成
v0.3.0 - 源分组 + 商业插件检测 ✅ 已完成
v0.4.0 - Bridge API ✅ 已完成
v0.8.0 - 配置导入导出 + 稳定性优化 ✅ 已完成
v0.9.0 - 版本控制 + 用户体验 ← 当前目标
v1.0.0 - 正式发布
```

---

## v0.1.0 - MVP最小可用桥接 + 基础缓存/降级 ✅

### 目标
实现最小可用的更新源桥接功能,确保基础稳定性

### 任务清单

#### 插件基础结构
- [x] 主文件 `wpbridge.php`
- [x] 自动加载器 `Loader.php`
- [x] 设置页面框架 `Settings.php`
- [x] 数据存储结构

#### 预置更新源
- [x] 文派开源更新源(默认启用)
- [x] ArkPress 支持(文派自托管方案)
- [x] AspireCloud 支持(可选)

#### 更新源管理(基础)
- [x] 更新源数据模型(使用统一 SourceType 枚举)
- [x] 更新源 CRUD 操作
- [x] 更新源列表界面
- [x] 添加/编辑更新源表单

#### 核心桥接功能
- [x] `pre_set_site_transient_update_plugins` 钩子
- [x] `pre_set_site_transient_update_themes` 钩子
- [x] JSON API 处理器JsonHandler
- [x] ArkPress 处理器ArkPressHandler
- [x] AspireCloud 处理器AspireCloudHandler
- [x] Plugin Update Checker JSON 格式兼容PUCHandler

#### 基础缓存与降级
- [x] Transient 缓存12 小时 TTL
- [x] 源健康状态缓存1 小时 TTL
- [x] 失败源冷却机制30 分钟)
- [x] 过期缓存兜底(源不可用时返回旧数据)
- [x] 请求超时限制10 秒)

#### 安全基础
- [x] URL 格式校验
- [x] API Key 加密存储
- [x] JSON 响应结构校验

#### 日志与错误处理
- [x] 调试日志(可开关)
- [x] 用户友好的错误信息

---

## v0.2.0 - Git 仓库支持 + WP-CLI + 性能优化 ✅

### 目标
支持 Git 仓库作为更新源,提供命令行工具,实现性能优化

### 任务清单

#### 性能优化(核心)
- [x] 并行请求(`ParallelRequestManager`
- [x] 条件请求ETag/Last-Modified
- [x] WP-Cron 后台预热任务(`BackgroundUpdater`
- [ ] 请求去重与合并窗口
- [ ] 缓存分层对象缓存优先DB 兜底)

#### WP-CLI 支持(`wp bridge`
- [x] `wp bridge source list` - 列出所有源
- [x] `wp bridge source add <url>` - 添加源
- [x] `wp bridge source remove <id>` - 删除源
- [x] `wp bridge source enable/disable <id>` - 启用/禁用源
- [x] `wp bridge check` - 检查所有源
- [x] `wp bridge cache clear` - 清除缓存
- [x] `wp bridge diagnose` - 诊断报告
- [ ] `wp bridge config export/import` - 配置导入导出

#### Git 仓库支持
- [x] 统一接口 SourceHandlerInterface
- [x] GitHub Releases 支持GitHubHandler
- [x] GitLab Releases 支持GitLabHandler
- [x] Gitee Releases 支持GiteeHandler国内
- [x] 菲码源库支持WenPaiGitHandler
- [x] 私有仓库认证

#### 诊断工具
- [x] 诊断页面(源状态、请求日志)
- [x] 一键测试源连通性
- [ ] 导出诊断报告

#### 配置管理
- [ ] 导入/导出配置
- [ ] 配置备份

#### 认证支持
- [x] API Key 认证
- [x] Basic Auth 认证
- [x] 自定义 HTTP 头

#### 源优先级
- [x] 源优先级设置
- [x] 多源冲突处理(版本号最高优先)

#### FAIR 支持
- [x] FAIR Package Manager 处理器FairHandler

---

## v0.3.0 - 源分组 + 商业插件检测 ✅

### 目标
支持源分组管理,实现商业插件检测

### 任务清单

#### 源分组管理
- [x] 源组数据模型GroupModel
- [x] 批量管理界面GroupManager
- [x] 共享认证信息
- [x] 统一启用/禁用

#### 商业插件适配
- [x] 商业插件检测机制CommercialDetector
- [x] 远程 JSON 配置支持
- [x] 检测结果永久缓存
- [x] 手动刷新检测功能
- [ ] 授权验证代理(可选)
- [ ] 版本锁定功能
- [ ] 回滚机制(更新前备份)

#### 通知系统
- [x] 邮件通知EmailHandler
- [x] Webhook 通知WebhookHandler
- [ ] 更新日志聚合显示

---

## v0.4.0 - Bridge API ✅

### 目标
提供 REST API 服务,支持外部调用

### 任务清单

#### Bridge API 基础
- [x] REST API 端点设计(`/wp-json/bridge/v1/`
- [x] 认证机制API Key
- [x] 状态端点 `/status`
- [ ] 限流策略
- [ ] 可用性 SLA 定义

#### API 端点
- [x] GET /bridge/v1/status - 获取状态
- [ ] GET /bridge/v1/sources - 获取可用更新源列表
- [ ] GET /bridge/v1/check/{source_id} - 检查指定源更新
- [ ] GET /bridge/v1/plugins/{slug}/info - 获取插件信息
- [ ] GET /bridge/v1/themes/{slug}/info - 获取主题信息

---

## v0.8.0 - 配置导入导出 + 稳定性优化 ✅

### 目标
完善配置管理,提升稳定性

### 任务清单

#### 配置管理
- [x] 导入配置JSON 格式)
- [x] 导出配置JSON 格式)
- [x] 配置备份/恢复
- [x] WP-CLI 配置命令

#### 稳定性优化
- [x] 完善错误处理
- [x] 安全性检查nonce、权限、输入清理
- [ ] 添加单元测试v1.0 前完成)
- [ ] 性能优化(请求去重)
- [ ] 缓存分层优化

---

## v0.9.0 - 版本控制 + 用户体验 ← 当前目标

### 目标
实现版本锁定和回滚机制,提升用户体验

### 任务清单

#### 版本锁定
- [ ] 锁定插件/主题到当前版本
- [ ] 锁定到指定版本
- [ ] 忽略特定版本更新
- [ ] 版本锁定 UI

#### 回滚机制
- [ ] 更新前自动备份
- [ ] 备份存储管理
- [ ] 一键回滚功能
- [ ] 保留最近 N 个版本

#### 更新日志聚合
- [ ] 从更新源获取 changelog
- [ ] 统一显示格式
- [ ] 在更新页面显示

#### Site Health 集成
- [ ] 更新源连通性检查
- [ ] 配置完整性检查
- [ ] 提供修复建议

#### Bridge API 完善
- [ ] GET /sources - 获取更新源列表
- [ ] GET /check/{source_id} - 检查指定源
- [ ] GET /plugins/{slug}/info - 获取插件信息
- [ ] GET /themes/{slug}/info - 获取主题信息

---

## v1.0.0 - 正式发布

### 目标
稳定版本,完善文档和用户体验

### 任务清单

#### 文档完善
- [x] 用户指南
- [ ] 开发者文档(如何创建兼容的更新服务器)
- [x] API 文档
- [x] 常见问题
- [ ] 视频教程

#### 用户体验
- [ ] 状态仪表板优化
- [ ] 性能基准测试报告

#### 高级功能(延后)
- [ ] 多站点 (Multisite) 支持
- [ ] 安全扫描集成VirusTotal/Patchstack
- [ ] 白标/OEM 支持

#### 发布准备
- [ ] GitHub Release
- [ ] 菲码源库 Release
- [ ] 更新日志
- [ ] 宣传材料

---

## AI 桥接层(暂缓)

> 以下功能暂缓开发,待核心功能稳定后考虑

### OpenAI 兼容层
- [ ] `pre_http_request` 拦截器
- [ ] 用户可配置白名单
- [ ] OpenAI Chat API 转发
- [ ] 响应格式转换
- [ ] 自定义端点支持(透传模式)
- [ ] WPMind 集成(可选)

### 商业插件 AI 适配
- [x] AI 网关基础AIGateway
- [x] Yoast SEO Pro 适配器
- [x] Rank Math 适配器
- [ ] 嗅探模式(收集 API 格式)

---

## 技术架构

```
wpbridge/
├── wpbridge.php # 主文件
├── includes/
│ ├── Core/
│ │ ├── Plugin.php # 插件主类
│ │ ├── Loader.php # 自动加载
│ │ ├── Settings.php # 设置管理
│ │ ├── Logger.php # 日志系统
│ │ ├── CommercialDetector.php # 商业插件检测
│ │ ├── RemoteConfig.php # 远程配置
│ │ └── ItemSourceManager.php # 项目源管理
│ │
│ ├── UpdateSource/
│ │ ├── SourceManager.php # 更新源管理
│ │ ├── SourceModel.php # 数据模型
│ │ ├── SourceType.php # 源类型枚举
│ │ ├── PluginUpdater.php # 插件更新器
│ │ ├── ThemeUpdater.php # 主题更新器
│ │ └── Handlers/ # 各类处理器
│ │ ├── JsonHandler.php
│ │ ├── GitHubHandler.php
│ │ ├── GitLabHandler.php
│ │ ├── GiteeHandler.php
│ │ ├── ArkPressHandler.php
│ │ ├── AspireCloudHandler.php
│ │ ├── FairHandler.php
│ │ └── ...
│ │
│ ├── SourceGroup/
│ │ ├── GroupManager.php # 分组管理
│ │ └── GroupModel.php # 分组模型
│ │
│ ├── Cache/
│ │ ├── CacheManager.php # 缓存管理
│ │ ├── HealthChecker.php # 健康检查
│ │ └── FallbackStrategy.php # 降级策略
│ │
│ ├── Performance/
│ │ ├── ParallelRequestManager.php # 并行请求
│ │ ├── ConditionalRequest.php # 条件请求
│ │ └── BackgroundUpdater.php # 后台更新
│ │
│ ├── API/
│ │ ├── RestController.php # REST API
│ │ └── ApiKeyManager.php # API Key 管理
│ │
│ ├── CLI/
│ │ └── BridgeCommand.php # WP-CLI 命令
│ │
│ ├── Notification/
│ │ ├── NotificationManager.php
│ │ ├── EmailHandler.php
│ │ └── WebhookHandler.php
│ │
│ ├── AIBridge/
│ │ ├── AIGateway.php # AI 网关
│ │ └── Adapters/
│ │ ├── YoastAdapter.php
│ │ └── RankMathAdapter.php
│ │
│ └── Admin/
│ └── AdminPage.php # 管理页面
├── templates/
│ └── admin/
│ ├── main.php
│ └── tabs/
│ ├── overview.php
│ ├── sources.php
│ ├── diagnostics.php
│ └── api.php
└── assets/
├── css/
└── js/
```

---

## 依赖关系

```
WPBridge 核心功能
├── 更新源桥接 - 无依赖,独立运行
├── 商业插件检测 - 无依赖,独立运行
├── Bridge API - 无依赖,独立运行
└── AI 桥接 - 可选依赖 WPMind暂缓
```

---

## 待讨论事项

- [x] 是否需要云端配置同步?→ 暂不需要v1.0 后考虑
- [x] 是否支持多站点?→ 付费功能v1.0.0
- [x] 定价策略(免费 vs 付费)?→ 基础免费 + 高级付费
- [x] 与文派叶子的集成方式?→ 独立运行,检测 WPCY 存在时官方源走 WPCY
- [x] 预置更新源?→ 文派开源默认、ArkPress、AspireCloud、FAIR
- [x] WP-CLI 命令前缀?→ `wp bridge`
- [x] 自托管方案支持?→ ArkPress、AspireCloud、UpdatePulse Server、PUC 格式

---

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

2969
assets/css/admin.css Normal file

File diff suppressed because it is too large Load diff

2675
assets/js/admin.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
{
"version": "1.0.0",
"updated_at": "2026-02-05",
"description": "WPBridge 商业插件检测配置文件 - 由 wpcy.com 维护",
"commercial_plugins": [
"elementor-pro",
"wordpress-seo-premium",
"yoast-seo-premium",
"wpforms-pro",
"gravityforms",
"advanced-custom-fields-pro",
"acf-pro",
"woocommerce-subscriptions",
"woocommerce-memberships",
"woocommerce-bookings",
"sitepress-multilingual-cms",
"wpml-string-translation",
"updraftplus-premium",
"seo-by-rank-math-pro",
"monsterinsights-pro",
"optinmonster-pro",
"wp-rocket",
"perfmatters",
"wp-smush-pro",
"wordfence-premium",
"ithemes-security-pro",
"backupbuddy",
"bb-plugin",
"divi-builder",
"brizy-pro",
"sfwd-lms",
"memberpress",
"restrict-content-pro",
"affiliate-wp",
"easy-digital-downloads-pro",
"ninja-forms-pro",
"formidable-pro",
"fluentformpro",
"wp-all-import-pro",
"searchwp",
"facetwp",
"admin-columns-pro"
],
"commercial_domains": [
"codecanyon.net",
"themeforest.net",
"elegantthemes.com",
"developer.yoast.com",
"developer.wpforms.com",
"developer.monsterinsights.com",
"developer.optinmonster.com",
"developer.seedprod.com",
"developer.ithemes.com"
],
"license_keywords": [
"license_key",
"license_status",
"activate_license",
"deactivate_license",
"check_license",
"is_valid_license",
"license_page",
"enter your license",
"purchase_code",
"activation_key"
],
"commercial_frameworks": [
"EDD_SL_Plugin_Updater",
"Freemius",
"WC_AM_Client",
"Starter_Plugin_Updater"
]
}

View file

@ -0,0 +1,354 @@
<?php
/**
* AI 网关
*
* @package WPBridge
*/

namespace WPBridge\AIBridge;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\Security\Validator;

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

/**
* AI 网关类
* 拦截并转发 AI API 请求
*/
class AIGateway {

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

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

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

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

$this->init_hooks();
}

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

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', [] );
return ! empty( $ai_settings['enabled'] );
}

/**
* 获取桥接模式
*
* @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', [] );
$whitelist = $ai_settings['whitelist'] ?? [];

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

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;
}

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

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

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

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

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

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 );

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

$host = strtolower( $host );

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

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', [] );
$custom_endpoint = $ai_settings['custom_endpoint'] ?? '';

if ( empty( $custom_endpoint ) ) {
Logger::warning( '透传模式未配置自定义端点' );
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 );

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

// 发送请求
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 API
$wpmind_endpoint = apply_filters( 'wpmind_api_endpoint', 'https://api.wpmind.cn/v1' );

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

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

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' );
}

/**
* 替换端点
*
* @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, '/' );

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();
}

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

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

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', [ $this, 'intercept_request' ], 10 );

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

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

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

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 适配器', [ 'name' => $name ] );
}

/**
* 获取适配器
*
* @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_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

@ -0,0 +1,172 @@
<?php
/**
* AI 适配器抽象基类
*
* @package WPBridge
*/

namespace WPBridge\AIBridge\Adapters;

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

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

/**
* AI 适配器抽象基类
*/
abstract class AbstractAdapter implements AdapterInterface {

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

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

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

/**
* 构造函数
*
* @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 $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', [] );
$adapters = $ai_settings['adapters'] ?? [];

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

/**
* 记录日志
*
* @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;
}

$body = $args['body'];

if ( is_string( $body ) ) {
$decoded = json_decode( $body, true );
return json_last_error() === JSON_ERROR_NONE ? $decoded : 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|\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 );

if ( empty( $body ) ) {
return 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;
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* AI 适配器接口
*
* @package WPBridge
*/

namespace WPBridge\AIBridge\Adapters;

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

/**
* AI 适配器接口
*/
interface AdapterInterface {

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

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

/**
* 检查是否支持该插件
*
* @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 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 );

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

View file

@ -0,0 +1,185 @@
<?php
/**
* Rank Math AI 适配器
*
* @package WPBridge
*/

namespace WPBridge\AIBridge\Adapters;

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

/**
* Rank Math AI 适配器类
* 适配 Rank Math SEO 的 Content AI 功能
*/
class RankMathAdapter extends AbstractAdapter {

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

/**
* 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_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 );

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

$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'] );
}

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

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

return [ $url, $args ];
}

/**
* 转换响应
*
* @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 );

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

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

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';
}

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

$content = strtolower( $content );

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

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

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

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

return 'general';
}

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

return $body;
}

/**
* 映射模型名称
*
* @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;
}
}

View file

@ -0,0 +1,145 @@
<?php
/**
* Yoast SEO AI 适配器
*
* @package WPBridge
*/

namespace WPBridge\AIBridge\Adapters;

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

/**
* Yoast SEO AI 适配器类
* 适配 Yoast SEO Premium 的 AI 功能
*/
class YoastAdapter extends AbstractAdapter {

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

/**
* 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_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 );

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

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

// 转换模型名称(如果需要)
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'] );
}

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

return [ $url, $args ];
}

/**
* 转换响应
*
* @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 );

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

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

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

/**
* 映射模型名称
*
* @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;
}

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

return $messages;
}
}

View file

@ -0,0 +1,287 @@
<?php
/**
* API Key 管理器
*
* @package WPBridge
*/

namespace WPBridge\API;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\Security\Encryption;

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

/**
* API Key 管理器类
*/
class ApiKeyManager {

/**
* 设置实例
*
* @var Settings
*/
private 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 {
// 权限检查
if ( ! current_user_can( 'manage_options' ) ) {
throw new \Exception( __( '权限不足', 'wpbridge' ) );
}

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

$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', [] );
$api_settings['keys'] = $api_settings['keys'] ?? [];
$api_settings['keys'][] = $key_data;

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

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

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', [] );
$keys = $api_settings['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', [] );
$keys = $api_settings['keys'] ?? [];

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;
}

/**
* 删除 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;
}
}

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

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

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

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', [] );
$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;
}
}

if ( ! $found ) {
return false;
}

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

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

return true;
}

/**
* 记录 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;
}
}

$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;
}

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

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

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

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

return $count;
}

/**
* 获取统计信息
*
* @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++;
}
}

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

View file

@ -0,0 +1,751 @@
<?php
/**
* REST API 控制器
*
* @package WPBridge
*/

namespace WPBridge\API;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\UpdateSource\SourceManager;
use WPBridge\Cache\CacheManager;

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

/**
* REST API 控制器类
*/
class RestController {

/**
* API 命名空间
*
* @var string
*/
const NAMESPACE = 'bridge/v1';

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

/**
* 源管理器
*
* @var SourceManager
*/
private SourceManager $source_manager;

/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;

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

$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
add_action( 'rest_api_init', [ $this, 'register_routes' ] );
}

/**
* 注册路由
*/
public function register_routes(): void {
// 获取所有更新源
register_rest_route( self::NAMESPACE, '/sources', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_sources' ],
'permission_callback' => [ $this, 'check_api_permission' ],
] );

// 获取单个更新源
register_rest_route( self::NAMESPACE, '/sources/(?P<id>[a-zA-Z0-9_-]+)', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_source' ],
'permission_callback' => [ $this, 'check_api_permission' ],
'args' => [
'id' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
] );

// 检查更新源状态
register_rest_route( self::NAMESPACE, '/check/(?P<source_id>[a-zA-Z0-9_-]+)', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'check_source' ],
'permission_callback' => [ $this, 'check_api_permission' ],
'args' => [
'source_id' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
] );

// 获取插件信息
register_rest_route( self::NAMESPACE, '/plugins/(?P<slug>[a-z0-9-]+)/info', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_plugin_info' ],
'permission_callback' => [ $this, 'check_api_permission' ],
'args' => [
'slug' => [
'required' => true,
'sanitize_callback' => 'sanitize_title',
],
],
] );

// 获取主题信息
register_rest_route( self::NAMESPACE, '/themes/(?P<slug>[a-z0-9-]+)/info', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_theme_info' ],
'permission_callback' => [ $this, 'check_api_permission' ],
'args' => [
'slug' => [
'required' => true,
'sanitize_callback' => 'sanitize_title',
],
],
] );

// 菲码源库 Releases
register_rest_route( self::NAMESPACE, '/wenpai-git/(?P<repo>[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+)/releases', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_wenpai_git_releases' ],
'permission_callback' => [ $this, 'check_api_permission' ],
'args' => [
'repo' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function ( $param ) {
return preg_match( '/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/', $param );
},
],
],
] );

// API 状态
register_rest_route( self::NAMESPACE, '/status', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_status' ],
'permission_callback' => '__return_true',
] );
}

/**
* 检查 API 权限
*
* @param \WP_REST_Request $request 请求对象
* @return bool|\WP_Error
*/
public function check_api_permission( \WP_REST_Request $request ) {
$api_settings = $this->settings->get( 'api', [] );

// 检查 API 是否启用
if ( empty( $api_settings['enabled'] ) ) {
return new \WP_Error(
'api_disabled',
__( 'API 未启用', 'wpbridge' ),
[ 'status' => 403 ]
);
}

// 检查是否需要认证
if ( ! empty( $api_settings['require_auth'] ) ) {
$api_key = $this->get_api_key_from_request( $request );

if ( empty( $api_key ) ) {
return new \WP_Error(
'missing_api_key',
__( '缺少 API Key', 'wpbridge' ),
[ 'status' => 401 ]
);
}

if ( ! $this->validate_api_key( $api_key ) ) {
return new \WP_Error(
'invalid_api_key',
__( '无效的 API Key', 'wpbridge' ),
[ 'status' => 401 ]
);
}
}

// 检查速率限制
$rate_limit_result = $this->check_rate_limit( $request );
if ( is_wp_error( $rate_limit_result ) ) {
return $rate_limit_result;
}

return true;
}

/**
* 从请求中获取 API Key
*
* @param \WP_REST_Request $request 请求对象
* @return string
*/
private function get_api_key_from_request( \WP_REST_Request $request ): string {
// 优先从 Header 获取
$auth_header = $request->get_header( 'X-WPBridge-API-Key' );
if ( ! empty( $auth_header ) ) {
return sanitize_text_field( $auth_header );
}

// 从 Authorization Bearer 获取
$auth_header = $request->get_header( 'Authorization' );
if ( ! empty( $auth_header ) && strpos( $auth_header, 'Bearer ' ) === 0 ) {
return sanitize_text_field( substr( $auth_header, 7 ) );
}

// 从查询参数获取(不推荐,记录警告)
$api_key = $request->get_param( 'api_key' );
if ( ! empty( $api_key ) ) {
Logger::warning( 'API Key 通过 URL 参数传递,建议使用 Header 方式', [
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
] );
return sanitize_text_field( $api_key );
}

return '';
}

/**
* 验证 API Key
*
* @param string $api_key API Key
* @return bool
*/
private function validate_api_key( string $api_key ): bool {
$api_settings = $this->settings->get( 'api', [] );
$valid_keys = $api_settings['keys'] ?? [];

foreach ( $valid_keys as $key_data ) {
// 使用 password_verify 验证哈希
if ( isset( $key_data['key_hash'] ) && password_verify( $api_key, $key_data['key_hash'] ) ) {
// 检查是否过期
if ( ! empty( $key_data['expires_at'] ) ) {
if ( strtotime( $key_data['expires_at'] ) < time() ) {
return false;
}
}

// 记录使用(使用缓存批量更新)
$this->record_api_key_usage( $key_data['id'] );

return true;
}
}

return false;
}

/**
* 记录 API Key 使用
*
* @param string $key_id Key ID
*/
private function record_api_key_usage( string $key_id ): void {
// 使用缓存记录,避免频繁写入数据库
$cache_key = 'api_usage_' . $key_id;
$count = $this->cache->get( $cache_key );

if ( false === $count ) {
$count = 0;
}

$count++;
$this->cache->set( $cache_key, $count, 300 );

// 每 50 次批量写入数据库
if ( $count >= 50 ) {
$this->flush_usage_to_db( $key_id, $count );
$this->cache->set( $cache_key, 0, 300 );
}
}

/**
* 将使用统计写入数据库
*
* @param string $key_id Key ID
* @param int $count 使用次数
*/
private function flush_usage_to_db( string $key_id, int $count ): void {
$api_settings = $this->settings->get( 'api', [] );
$keys = $api_settings['keys'] ?? [];

foreach ( $keys as $index => $key ) {
if ( $key['id'] === $key_id ) {
$keys[ $index ]['last_used'] = current_time( 'mysql' );
$keys[ $index ]['usage_count'] = ( $key['usage_count'] ?? 0 ) + $count;
break;
}
}

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

/**
* 检查速率限制
*
* @param \WP_REST_Request $request 请求对象
* @return true|\WP_Error
*/
private function check_rate_limit( \WP_REST_Request $request ) {
$api_settings = $this->settings->get( 'api', [] );
$rate_limit = $api_settings['rate_limit'] ?? 60; // 默认每分钟 60 次

if ( $rate_limit <= 0 ) {
return true; // 无限制
}

// 获取客户端标识
$client_id = $this->get_client_identifier( $request );
$cache_key = 'rate_limit_' . md5( $client_id );

$current = $this->cache->get( $cache_key );

if ( false === $current ) {
$this->cache->set( $cache_key, 1, 60 );
return true;
}

if ( $current >= $rate_limit ) {
return new \WP_Error(
'rate_limit_exceeded',
__( '请求过于频繁,请稍后再试', 'wpbridge' ),
[
'status' => 429,
'retry_after' => 60,
'x-ratelimit-limit' => $rate_limit,
'x-ratelimit-remaining' => 0,
]
);
}

$this->cache->set( $cache_key, $current + 1, 60 );

return true;
}

/**
* 获取客户端标识
*
* @param \WP_REST_Request $request 请求对象
* @return string
*/
private function get_client_identifier( \WP_REST_Request $request ): string {
// 优先使用 API Key
$api_key = $this->get_api_key_from_request( $request );
if ( ! empty( $api_key ) ) {
return 'key:' . md5( $api_key );
}

// 使用 IP 地址
return 'ip:' . $this->get_client_ip( $request );
}

/**
* 获取客户端 IP 地址
*
* @param \WP_REST_Request $request 请求对象
* @return string
*/
private function get_client_ip( \WP_REST_Request $request ): string {
// 检查是否配置了可信代理
$trusted_proxies = apply_filters( 'wpbridge_trusted_proxies', [] );

if ( ! empty( $trusted_proxies ) ) {
$remote_addr = $_SERVER['REMOTE_ADDR'] ?? '';

// 只有当请求来自可信代理时才信任 X-Forwarded-For
if ( in_array( $remote_addr, $trusted_proxies, true ) ) {
$forwarded = $request->get_header( 'X-Forwarded-For' );
if ( ! empty( $forwarded ) ) {
// 取第一个非代理 IP
$ips = array_map( 'trim', explode( ',', $forwarded ) );
return sanitize_text_field( $ips[0] );
}
}
}

return sanitize_text_field( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
}

/**
* 获取所有更新源
*
* @param \WP_REST_Request $request 请求对象
* @return \WP_REST_Response
*/
public function get_sources( \WP_REST_Request $request ): \WP_REST_Response {
$sources = $this->source_manager->get_enabled_sorted();

$data = array_map( function ( $source ) {
return [
'id' => $source->id,
'name' => $source->name,
'type' => $source->type,
'item_type' => $source->item_type,
'slug' => $source->slug,
'enabled' => $source->enabled,
'priority' => $source->priority,
];
}, $sources );

return new \WP_REST_Response( [
'success' => true,
'data' => array_values( $data ),
'total' => count( $data ),
] );
}

/**
* 获取单个更新源
*
* @param \WP_REST_Request $request 请求对象
* @return \WP_REST_Response|\WP_Error
*/
public function get_source( \WP_REST_Request $request ) {
$id = $request->get_param( 'id' );
$source = $this->source_manager->get( $id );

if ( null === $source ) {
return new \WP_Error(
'source_not_found',
__( '更新源不存在', 'wpbridge' ),
[ 'status' => 404 ]
);
}

return new \WP_REST_Response( [
'success' => true,
'data' => [
'id' => $source->id,
'name' => $source->name,
'type' => $source->type,
'api_url' => $source->api_url,
'item_type' => $source->item_type,
'slug' => $source->slug,
'enabled' => $source->enabled,
'priority' => $source->priority,
],
] );
}

/**
* 检查更新源状态
*
* @param \WP_REST_Request $request 请求对象
* @return \WP_REST_Response|\WP_Error
*/
public function check_source( \WP_REST_Request $request ) {
$source_id = $request->get_param( 'source_id' );
$source = $this->source_manager->get( $source_id );

if ( null === $source ) {
return new \WP_Error(
'source_not_found',
__( '更新源不存在', 'wpbridge' ),
[ 'status' => 404 ]
);
}

$checker = new \WPBridge\Cache\HealthChecker();
$status = $checker->check( $source, true );

return new \WP_REST_Response( [
'success' => true,
'data' => [
'source_id' => $source_id,
'status' => $status->status,
'response_time' => $status->response_time,
'error' => $status->error,
'checked_at' => current_time( 'c' ),
],
] );
}

/**
* 获取插件信息
*
* @param \WP_REST_Request $request 请求对象
* @return \WP_REST_Response|\WP_Error
*/
public function get_plugin_info( \WP_REST_Request $request ) {
$slug = $request->get_param( 'slug' );

// 从缓存获取
$cache_key = 'plugin_info_' . $slug;
$cached = $this->cache->get( $cache_key );

if ( false !== $cached ) {
return new \WP_REST_Response( [
'success' => true,
'data' => $cached,
'cached' => true,
] );
}

// 查找匹配的更新源
$sources = $this->source_manager->get_enabled_sorted();
$info = null;

foreach ( $sources as $source ) {
if ( $source->item_type !== 'plugin' ) {
continue;
}

if ( ! empty( $source->slug ) && $source->slug !== $slug ) {
continue;
}

$handler = $source->get_handler();
if ( null === $handler ) {
continue;
}

try {
$info = $handler->get_info( $slug );
if ( null !== $info ) {
break;
}
} catch ( \Exception $e ) {
Logger::debug( '获取插件信息失败', [
'slug' => $slug,
'source' => $source->id,
'error' => $e->getMessage(),
] );
}
}

if ( null === $info ) {
return new \WP_Error(
'plugin_not_found',
__( '未找到插件信息', 'wpbridge' ),
[ 'status' => 404 ]
);
}

// 缓存结果
$this->cache->set( $cache_key, $info, $this->settings->get_cache_ttl() );

return new \WP_REST_Response( [
'success' => true,
'data' => $info,
'cached' => false,
] );
}

/**
* 获取主题信息
*
* @param \WP_REST_Request $request 请求对象
* @return \WP_REST_Response|\WP_Error
*/
public function get_theme_info( \WP_REST_Request $request ) {
$slug = $request->get_param( 'slug' );

// 从缓存获取
$cache_key = 'theme_info_' . $slug;
$cached = $this->cache->get( $cache_key );

if ( false !== $cached ) {
return new \WP_REST_Response( [
'success' => true,
'data' => $cached,
'cached' => true,
] );
}

// 查找匹配的更新源
$sources = $this->source_manager->get_enabled_sorted();
$info = null;

foreach ( $sources as $source ) {
if ( $source->item_type !== 'theme' ) {
continue;
}

if ( ! empty( $source->slug ) && $source->slug !== $slug ) {
continue;
}

$handler = $source->get_handler();
if ( null === $handler ) {
continue;
}

try {
$info = $handler->get_info( $slug );
if ( null !== $info ) {
break;
}
} catch ( \Exception $e ) {
Logger::debug( '获取主题信息失败', [
'slug' => $slug,
'source' => $source->id,
'error' => $e->getMessage(),
] );
}
}

if ( null === $info ) {
return new \WP_Error(
'theme_not_found',
__( '未找到主题信息', 'wpbridge' ),
[ 'status' => 404 ]
);
}

// 缓存结果
$this->cache->set( $cache_key, $info, $this->settings->get_cache_ttl() );

return new \WP_REST_Response( [
'success' => true,
'data' => $info,
'cached' => false,
] );
}

/**
* 获取菲码源库 Releases
*
* @param \WP_REST_Request $request 请求对象
* @return \WP_REST_Response|\WP_Error
*/
public function get_wenpai_git_releases( \WP_REST_Request $request ) {
$repo = $request->get_param( 'repo' );

// 从缓存获取
$cache_key = 'wenpai_git_' . md5( $repo );
$cached = $this->cache->get( $cache_key );

if ( false !== $cached ) {
return new \WP_REST_Response( [
'success' => true,
'data' => $cached,
'cached' => true,
] );
}

// 调用菲码源库 API
$api_url = 'https://git.wenpai.org/api/v1/repos/' . $repo . '/releases';
$response = wp_remote_get( $api_url, [
'timeout' => $this->settings->get_request_timeout(),
'headers' => [
'Accept' => 'application/json',
],
] );

if ( is_wp_error( $response ) ) {
return new \WP_Error(
'api_error',
$response->get_error_message(),
[ 'status' => 502 ]
);
}

$status_code = wp_remote_retrieve_response_code( $response );
if ( $status_code !== 200 ) {
return new \WP_Error(
'api_error',
sprintf( __( '菲码源库 API 返回错误: %d', 'wpbridge' ), $status_code ),
[ 'status' => $status_code ]
);
}

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

if ( json_last_error() !== JSON_ERROR_NONE ) {
return new \WP_Error(
'json_error',
__( 'JSON 解析失败', 'wpbridge' ),
[ 'status' => 500 ]
);
}

// 格式化数据
$releases = array_map( function ( $release ) {
return [
'id' => $release['id'] ?? 0,
'tag_name' => $release['tag_name'] ?? '',
'name' => $release['name'] ?? '',
'body' => $release['body'] ?? '',
'draft' => $release['draft'] ?? false,
'prerelease' => $release['prerelease'] ?? false,
'created_at' => $release['created_at'] ?? '',
'published_at' => $release['published_at'] ?? '',
'assets' => array_map( function ( $asset ) {
return [
'name' => $asset['name'] ?? '',
'size' => $asset['size'] ?? 0,
'download_url' => $asset['browser_download_url'] ?? '',
'download_count' => $asset['download_count'] ?? 0,
];
}, $release['assets'] ?? [] ),
];
}, $data );

// 缓存结果
$this->cache->set( $cache_key, $releases, $this->settings->get_cache_ttl() );

return new \WP_REST_Response( [
'success' => true,
'data' => $releases,
'cached' => false,
] );
}

/**
* 获取 API 状态
*
* @param \WP_REST_Request $request 请求对象
* @return \WP_REST_Response
*/
public function get_status( \WP_REST_Request $request ): \WP_REST_Response {
return new \WP_REST_Response( [
'success' => true,
'data' => [
'version' => WPBRIDGE_VERSION,
'api_version' => 'v1',
'endpoints' => [
'sources' => rest_url( self::NAMESPACE . '/sources' ),
'check' => rest_url( self::NAMESPACE . '/check/{source_id}' ),
'plugins' => rest_url( self::NAMESPACE . '/plugins/{slug}/info' ),
'themes' => rest_url( self::NAMESPACE . '/themes/{slug}/info' ),
'wenpai_git' => rest_url( self::NAMESPACE . '/wenpai-git/{repo}/releases' ),
],
'timestamp' => current_time( 'c' ),
],
] );
}
}

1004
includes/Admin/AdminPage.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,401 @@
<?php
/**
* 供应商管理后台
*
* @package WPBridge
* @since 0.9.8
*/

declare(strict_types=1);

namespace WPBridge\Admin;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\Commercial\BridgeManager;

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

/**
* VendorAdmin 类
*/
class VendorAdmin {

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

/**
* 桥接管理器
*
* @var BridgeManager|null
*/
private ?BridgeManager $bridge_manager = null;

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

/**
* 初始化钩子
*
* @return void
*/
private function init_hooks(): void {
// 供应商 AJAX 处理
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', [ $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', [ $this, 'ajax_test_bridge_server' ] );
}

/**
* 获取桥接管理器
*
* @return BridgeManager
*/
private function get_bridge_manager(): BridgeManager {
if ( null === $this->bridge_manager ) {
$remote_config = new \WPBridge\Core\RemoteConfig( $this->settings );
$this->bridge_manager = new BridgeManager( $this->settings, $remote_config );
}
return $this->bridge_manager;
}

/**
* AJAX: 添加供应商
*
* @return void
*/
public function ajax_add_vendor(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
$name = sanitize_text_field( $_POST['name'] ?? '' );
$type = sanitize_text_field( $_POST['type'] ?? 'woocommerce' );
$api_url = esc_url_raw( $_POST['api_url'] ?? '' );
$consumer_key = sanitize_text_field( $_POST['consumer_key'] ?? '' );
$consumer_secret = sanitize_text_field( $_POST['consumer_secret'] ?? '' );

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

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

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

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

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

$result = $this->get_bridge_manager()->add_vendor(
$vendor_id,
$name,
$type,
$api_url,
$consumer_key,
$consumer_secret
);

if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}

/**
* AJAX: 移除供应商
*
* @return void
*/
public function ajax_remove_vendor(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

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

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

$result = $this->get_bridge_manager()->remove_vendor( $vendor_id );

if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}

/**
* AJAX: 测试供应商连接
*
* @return void
*/
public function ajax_test_vendor(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

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

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

$result = $this->get_bridge_manager()->test_vendor_connection( $vendor_id );

if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}

/**
* AJAX: 切换供应商状态
*
* @return void
*/
public function ajax_toggle_vendor(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

if ( ! current_user_can( 'manage_options' ) ) {
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( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
}

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

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

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

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

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

/**
* AJAX: 同步供应商插件列表
*
* @return void
*/
public function ajax_sync_vendor_plugins(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

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

$vendor_manager = $this->get_bridge_manager()->get_vendor_manager();

if ( ! empty( $vendor_id ) ) {
// 同步单个供应商
$vendor = $vendor_manager->get( $vendor_id );
if ( ! $vendor ) {
wp_send_json_error( [ 'message' => __( '供应商不存在', 'wpbridge' ) ] );
}

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

/**
* AJAX: 添加自定义插件
*
* @return void
*/
public function ajax_add_custom_plugin(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$plugin_slug = sanitize_key( $_POST['plugin_slug'] ?? '' );
$name = sanitize_text_field( $_POST['name'] ?? '' );
$update_url = esc_url_raw( $_POST['update_url'] ?? '' );

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

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

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

if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}

/**
* AJAX: 移除自定义插件
*
* @return void
*/
public function ajax_remove_custom_plugin(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

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

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

$result = $this->get_bridge_manager()->remove_custom_plugin( $plugin_slug );

if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}

/**
* AJAX: 测试 Bridge Server 连接
*
* @return void
*/
public function ajax_test_bridge_server(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

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

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

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

/**
* 渲染供应商设置页面
*
* @return void
*/
public function render_vendor_settings(): void {
$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';
}

/**
* 获取供应商数据(用于模板)
*
* @return array
*/
public function get_vendor_data(): 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' ),
],
];
}
}

View file

@ -0,0 +1,622 @@
<?php
/**
* WP-CLI 命令
*
* @package WPBridge
*/

namespace WPBridge\CLI;

use WPBridge\Core\Settings;
use WPBridge\Core\Plugin;
use WPBridge\UpdateSource\SourceManager;
use WPBridge\UpdateSource\SourceModel;
use WPBridge\UpdateSource\SourceType;
use WPBridge\Cache\HealthChecker;
use WPBridge\Performance\BackgroundUpdater;
use WP_CLI;
use WP_CLI\Utils;

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

/**
* 管理 WPBridge 更新源和缓存
*
* ## EXAMPLES
*
* # 列出所有更新源
* $ wp bridge source list
*
* # 添加更新源
* $ wp bridge source add https://example.com/updates.json --name="My Source"
*
* # 检查所有源
* $ wp bridge check
*
* # 清除缓存
* $ wp bridge cache clear
*/
class BridgeCommand {

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

/**
* 源管理器
*
* @var SourceManager
*/
private SourceManager $source_manager;

/**
* 构造函数
*/
public function __construct() {
$this->settings = new Settings();
$this->source_manager = new SourceManager( $this->settings );
}

/**
* 列出所有更新源
*
* ## OPTIONS
*
* [--format=<format>]
* : 输出格式
* ---
* default: table
* options:
* - table
* - json
* - csv
* - yaml
* ---
*
* [--enabled]
* : 只显示启用的源
*
* ## EXAMPLES
*
* $ wp bridge source list
* $ wp bridge source list --format=json
* $ wp bridge source list --enabled
*
* @subcommand source list
*/
public function source_list( $args, $assoc_args ) {
$enabled_only = Utils\get_flag_value( $assoc_args, 'enabled', false );

$sources = $enabled_only
? $this->source_manager->get_enabled()
: $this->source_manager->get_all();

if ( empty( $sources ) ) {
WP_CLI::warning( '没有更新源' );
return;
}

$items = [];
foreach ( $sources as $source ) {
$items[] = [
'id' => $source->id,
'name' => $source->name,
'type' => $source->type,
'api_url' => $source->api_url,
'slug' => $source->slug ?: '(all)',
'enabled' => $source->enabled ? 'yes' : 'no',
'priority' => $source->priority,
'preset' => $source->is_preset ? 'yes' : 'no',
];
}

$format = Utils\get_flag_value( $assoc_args, 'format', 'table' );

Utils\format_items( $format, $items, [
'id', 'name', 'type', 'api_url', 'slug', 'enabled', 'priority', 'preset'
] );
}

/**
* 添加更新源
*
* ## OPTIONS
*
* <url>
* : 更新源 URL
*
* [--name=<name>]
* : 源名称
*
* [--type=<type>]
* : 源类型
* ---
* default: json
* options:
* - json
* - github
* - gitlab
* - gitee
* - arkpress
* - aspirecloud
* ---
*
* [--slug=<slug>]
* : 插件/主题 slug
*
* [--item-type=<item_type>]
* : 项目类型
* ---
* default: plugin
* options:
* - plugin
* - theme
* ---
*
* [--priority=<priority>]
* : 优先级 (0-100)
* ---
* default: 50
* ---
*
* ## EXAMPLES
*
* $ wp bridge source add https://example.com/updates.json --name="My Plugin"
* $ wp bridge source add github.com/user/repo --type=github --name="GitHub Plugin"
*
* @subcommand source add
*/
public function source_add( $args, $assoc_args ) {
$url = $args[0];

$source = new SourceModel();
$source->api_url = $url;
$source->name = Utils\get_flag_value( $assoc_args, 'name', '' );
$source->type = Utils\get_flag_value( $assoc_args, 'type', SourceType::JSON );
$source->slug = Utils\get_flag_value( $assoc_args, 'slug', '' );
$source->item_type = Utils\get_flag_value( $assoc_args, 'item-type', 'plugin' );
$source->priority = (int) Utils\get_flag_value( $assoc_args, 'priority', 50 );
$source->enabled = true;

// 自动检测类型
if ( empty( $assoc_args['type'] ) ) {
$source->type = $this->detect_source_type( $url );
}

// 自动生成名称
if ( empty( $source->name ) ) {
$source->name = $this->generate_source_name( $url, $source->type );
}

// 验证
$errors = $source->validate();
if ( ! empty( $errors ) ) {
WP_CLI::error( implode( "\n", $errors ) );
}

if ( $this->source_manager->add( $source ) ) {
WP_CLI::success( sprintf( '已添加更新源: %s', $source->name ) );
} else {
WP_CLI::error( '添加失败' );
}
}

/**
* 删除更新源
*
* ## OPTIONS
*
* <id>
* : 源 ID
*
* [--yes]
* : 跳过确认
*
* ## EXAMPLES
*
* $ wp bridge source remove source_abc123
*
* @subcommand source remove
*/
public function source_remove( $args, $assoc_args ) {
$source_id = $args[0];

$source = $this->source_manager->get( $source_id );

if ( null === $source ) {
WP_CLI::error( '源不存在' );
}

if ( $source->is_preset ) {
WP_CLI::error( '不能删除预置源' );
}

WP_CLI::confirm( sprintf( '确定要删除 "%s" 吗?', $source->name ), $assoc_args );

if ( $this->source_manager->delete( $source_id ) ) {
WP_CLI::success( '已删除' );
} else {
WP_CLI::error( '删除失败' );
}
}

/**
* 启用更新源
*
* ## OPTIONS
*
* <id>
* : 源 ID
*
* ## EXAMPLES
*
* $ wp bridge source enable source_abc123
*
* @subcommand source enable
*/
public function source_enable( $args, $assoc_args ) {
$source_id = $args[0];

if ( $this->source_manager->toggle( $source_id, true ) ) {
WP_CLI::success( '已启用' );
} else {
WP_CLI::error( '操作失败' );
}
}

/**
* 禁用更新源
*
* ## OPTIONS
*
* <id>
* : 源 ID
*
* ## EXAMPLES
*
* $ wp bridge source disable source_abc123
*
* @subcommand source disable
*/
public function source_disable( $args, $assoc_args ) {
$source_id = $args[0];

if ( $this->source_manager->toggle( $source_id, false ) ) {
WP_CLI::success( '已禁用' );
} else {
WP_CLI::error( '操作失败' );
}
}

/**
* 检查所有更新源
*
* ## OPTIONS
*
* [--format=<format>]
* : 输出格式
* ---
* default: table
* options:
* - table
* - json
* ---
*
* ## EXAMPLES
*
* $ wp bridge check
*
* @subcommand check
*/
public function check( $args, $assoc_args ) {
$sources = $this->source_manager->get_enabled_sorted();

if ( empty( $sources ) ) {
WP_CLI::warning( '没有启用的更新源' );
return;
}

$checker = new HealthChecker();
$results = [];

foreach ( $sources as $source ) {
WP_CLI::log( sprintf( '检查 %s...', $source->name ) );

$status = $checker->check( $source, true );

$results[] = [
'id' => $source->id,
'name' => $source->name,
'status' => $status->status,
'response_time' => $status->response_time . 'ms',
'error' => $status->error ?: '-',
];
}

$format = Utils\get_flag_value( $assoc_args, 'format', 'table' );

WP_CLI::log( '' );
Utils\format_items( $format, $results, [
'id', 'name', 'status', 'response_time', 'error'
] );

// 统计
$healthy = count( array_filter( $results, fn( $r ) => $r['status'] === 'healthy' ) );
$total = count( $results );

WP_CLI::log( '' );
WP_CLI::log( sprintf( '健康: %d/%d', $healthy, $total ) );
}

/**
* 清除缓存
*
* ## EXAMPLES
*
* $ wp bridge cache clear
*
* @subcommand cache clear
*/
public function cache_clear( $args, $assoc_args ) {
Plugin::clear_all_cache();
WP_CLI::success( '缓存已清除' );
}

/**
* 查看缓存状态
*
* ## EXAMPLES
*
* $ wp bridge cache status
*
* @subcommand cache status
*/
public function cache_status( $args, $assoc_args ) {
$cache = new \WPBridge\Cache\CacheManager();
$stats = $cache->get_stats();

WP_CLI::log( sprintf( 'Transient 缓存数: %d', $stats['transient_count'] ) );
WP_CLI::log( sprintf( '对象缓存: %s', $stats['object_cache'] ? '是' : '否' ) );
WP_CLI::log( sprintf( '对象缓存类型: %s', $stats['object_cache_type'] ) );

// 后台更新状态
$updater = new BackgroundUpdater( $this->settings );
$status = $updater->get_status();

WP_CLI::log( '' );
WP_CLI::log( '后台更新:' );
WP_CLI::log( sprintf( ' 已调度: %s', $status['scheduled'] ? '是' : '否' ) );
if ( $status['next_run'] ) {
WP_CLI::log( sprintf( ' 下次运行: %s (%s)', $status['next_run'], $status['next_run_human'] ) );
}
}

/**
* 生成诊断报告
*
* ## OPTIONS
*
* [--format=<format>]
* : 输出格式
* ---
* default: text
* options:
* - text
* - json
* ---
*
* ## EXAMPLES
*
* $ wp bridge diagnose
*
* @subcommand diagnose
*/
public function diagnose( $args, $assoc_args ) {
$format = Utils\get_flag_value( $assoc_args, 'format', 'text' );

$report = [
'wpbridge_version' => WPBRIDGE_VERSION,
'wordpress_version' => get_bloginfo( 'version' ),
'php_version' => PHP_VERSION,
'sources' => $this->source_manager->get_stats(),
'settings' => $this->settings->get_all(),
];

if ( $format === 'json' ) {
WP_CLI::log( wp_json_encode( $report, JSON_PRETTY_PRINT ) );
} else {
WP_CLI::log( '=== WPBridge 诊断报告 ===' );
WP_CLI::log( '' );
WP_CLI::log( sprintf( 'WPBridge 版本: %s', $report['wpbridge_version'] ) );
WP_CLI::log( sprintf( 'WordPress 版本: %s', $report['wordpress_version'] ) );
WP_CLI::log( sprintf( 'PHP 版本: %s', $report['php_version'] ) );
WP_CLI::log( '' );
WP_CLI::log( '更新源统计:' );
WP_CLI::log( sprintf( ' 总数: %d', $report['sources']['total'] ) );
WP_CLI::log( sprintf( ' 已启用: %d', $report['sources']['enabled'] ) );
WP_CLI::log( '' );
WP_CLI::log( '设置:' );
WP_CLI::log( sprintf( ' 调试模式: %s', $report['settings']['debug_mode'] ? '是' : '否' ) );
WP_CLI::log( sprintf( ' 缓存时间: %d 秒', $report['settings']['cache_ttl'] ) );
WP_CLI::log( sprintf( ' 请求超时: %d 秒', $report['settings']['request_timeout'] ) );
}
}

/**
* 导出配置
*
* ## OPTIONS
*
* [<file>]
* : 导出文件路径
*
* ## EXAMPLES
*
* $ wp bridge config export
* $ wp bridge config export /path/to/config.json
*
* @subcommand config export
*/
public function config_export( $args, $assoc_args ) {
$sources = $this->source_manager->get_all();
$settings = $this->settings->get_all();

$export = [
'version' => WPBRIDGE_VERSION,
'exported' => current_time( 'mysql' ),
'sources' => array_map( fn( $s ) => $s->to_array(), $sources ),
'settings' => $settings,
];

// 移除敏感信息
foreach ( $export['sources'] as &$source ) {
$source['auth_token'] = '';
}

$json = wp_json_encode( $export, JSON_PRETTY_PRINT );

if ( ! empty( $args[0] ) ) {
$file_path = $args[0];
// 验证路径是否可写
$dir = dirname( $file_path );
if ( ! is_dir( $dir ) || ! is_writable( $dir ) ) {
WP_CLI::error( '目标目录不存在或不可写' );
}
// 使用 WordPress 文件系统 API
global $wp_filesystem;
if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();
if ( $wp_filesystem->put_contents( $file_path, $json, FS_CHMOD_FILE ) ) {
WP_CLI::success( sprintf( '已导出到 %s', $file_path ) );
} else {
WP_CLI::error( '写入文件失败' );
}
} else {
WP_CLI::log( $json );
}
}

/**
* 导入配置
*
* ## OPTIONS
*
* <file>
* : 配置文件路径
*
* [--yes]
* : 跳过确认
*
* ## EXAMPLES
*
* $ wp bridge config import /path/to/config.json
*
* @subcommand config import
*/
public function config_import( $args, $assoc_args ) {
$file = $args[0];

if ( ! file_exists( $file ) ) {
WP_CLI::error( '文件不存在' );
}

// 使用 WordPress 文件系统 API
global $wp_filesystem;
if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();
$json = $wp_filesystem->get_contents( $file );

if ( false === $json ) {
WP_CLI::error( '读取文件失败' );
}

$data = json_decode( $json, true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
WP_CLI::error( 'JSON 解析失败: ' . json_last_error_msg() );
}

WP_CLI::confirm( '这将覆盖现有配置,确定继续吗?', $assoc_args );

// 导入设置
if ( ! empty( $data['settings'] ) ) {
$this->settings->update( $data['settings'] );
WP_CLI::log( '已导入设置' );
}

// 导入源
if ( ! empty( $data['sources'] ) ) {
$count = 0;
foreach ( $data['sources'] as $source_data ) {
// 跳过预置源
if ( ! empty( $source_data['is_preset'] ) ) {
continue;
}

$source = SourceModel::from_array( $source_data );
$source->id = ''; // 生成新 ID

if ( $this->source_manager->add( $source ) ) {
$count++;
}
}
WP_CLI::log( sprintf( '已导入 %d 个更新源', $count ) );
}

WP_CLI::success( '导入完成' );
}

/**
* 检测源类型
*
* @param string $url URL
* @return string
*/
private function detect_source_type( string $url ): string {
if ( strpos( $url, 'github.com' ) !== false || strpos( $url, 'github/' ) !== false ) {
return SourceType::GITHUB;
}

if ( strpos( $url, 'gitlab.com' ) !== false || strpos( $url, 'gitlab/' ) !== false ) {
return SourceType::GITLAB;
}

if ( strpos( $url, 'gitee.com' ) !== false || strpos( $url, 'gitee/' ) !== false ) {
return SourceType::GITEE;
}

return SourceType::JSON;
}

/**
* 生成源名称
*
* @param string $url URL
* @param string $type 类型
* @return string
*/
private function generate_source_name( string $url, string $type ): string {
$parsed = parse_url( $url );
$host = $parsed['host'] ?? '';
$path = $parsed['path'] ?? '';

if ( in_array( $type, [ SourceType::GITHUB, SourceType::GITLAB, SourceType::GITEE ], true ) ) {
// 提取 owner/repo
$path = trim( $path, '/' );
$path = preg_replace( '#\.git$#', '', $path );
return $path ?: $host;
}

return $host ?: $url;
}
}

View file

@ -0,0 +1,198 @@
<?php
/**
* 缓存管理器
*
* @package WPBridge
*/

namespace WPBridge\Cache;

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

/**
* 缓存管理器类
*/
class CacheManager {

/**
* 缓存组名
*
* @var string
*/
const CACHE_GROUP = 'wpbridge';

/**
* 默认 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;
}
}

// 降级到 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 );
}

// 同时存储到 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 );
}

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 );

if ( false !== $value ) {
return $value;
}

// 尝试获取新值
try {
$new_value = $callback();

if ( null !== $new_value && false !== $new_value ) {
$this->set( $key, $new_value, $ttl );

// 同时存储一份过期缓存备份
$this->set( $key . '_stale', $new_value, $stale_ttl );

return $new_value;
}
} catch ( \Exception $e ) {
// 获取新值失败,尝试使用过期缓存
}

// 尝试使用过期缓存
$stale_value = $this->get( $key . '_stale' );

if ( false !== $stale_value ) {
return $stale_value;
}

return false;
}

/**
* 清除所有缓存
*/
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_' ) . '%'
)
);

// 清除对象缓存组(不使用 flush 避免影响其他插件)
if ( wp_using_ext_object_cache() ) {
wp_cache_delete( 'wpbridge', 'wpbridge' );
}
}

/**
* 获取缓存统计
*
* @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_' ) . '%'
)
);

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';
}

global $wp_object_cache;

if ( isset( $wp_object_cache->redis ) || class_exists( 'Redis' ) ) {
return 'redis';
}

if ( isset( $wp_object_cache->mc ) || class_exists( 'Memcached' ) ) {
return 'memcached';
}

return 'unknown';
}
}

View file

@ -0,0 +1,243 @@
<?php
/**
* 降级策略
*
* @package WPBridge
*/

namespace WPBridge\Cache;

use WPBridge\UpdateSource\SourceModel;
use WPBridge\UpdateSource\SourceManager;
use WPBridge\Core\Settings;
use WPBridge\Core\Logger;

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

/**
* 降级策略类
*/
class FallbackStrategy {

/**
* 源不可用时的行为
*/
const ON_FAIL_SKIP = 'skip'; // 跳过,继续下一个源
const ON_FAIL_WARN = 'warn'; // 警告,但继续
const ON_FAIL_BLOCK = 'block'; // 阻止更新检查

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

/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;

/**
* 健康检查器
*
* @var HealthChecker
*/
private HealthChecker $health_checker;

/**
* 最大重试次数
*
* @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 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( '源在冷却期,跳过', [ 'source' => $source->id ] );
continue;
}

// 检查缓存的健康状态
$status = $this->health_checker->get_status( $source->id );

if ( null !== $status && ! $status->is_available() ) {
Logger::debug( '源不可用,跳过', [ 'source' => $source->id ] );
continue;
}

$available[] = $source;
}

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 );

if ( empty( $available ) ) {
Logger::warning( '没有可用的更新源' );

// 尝试使用过期缓存
if ( ! empty( $cache_key ) ) {
$stale = $this->cache->get( $cache_key . '_stale' );
if ( false !== $stale ) {
Logger::info( '使用过期缓存', [ 'key' => $cache_key ] );
return $stale;
}
}

return null;
}

$last_error = null;

foreach ( $available as $source ) {
$retries = 0;

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 天
}

return $result;
}

// 返回 null/false 但没有异常,不重试
break;

} catch ( \Exception $e ) {
$last_error = $e;
$retries++;

Logger::warning( '操作失败,重试中', [
'source' => $source->id,
'retry' => $retries,
'error' => $e->getMessage(),
] );

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( '所有源失败,使用过期缓存', [ 'key' => $cache_key ] );
return $stale;
}
}

if ( null !== $last_error ) {
Logger::error( '所有源都失败', [ 'error' => $last_error->getMessage() ] );
}

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 );

Logger::warning( '源失败', [
'source' => $source->id,
'error' => $error,
'behavior' => $behavior,
] );

switch ( $behavior ) {
case self::ON_FAIL_WARN:
// 添加管理员通知
$this->add_admin_notice( $source, $error );
break;

case self::ON_FAIL_BLOCK:
// 阻止更新检查(不推荐)
throw new \RuntimeException( sprintf(
__( '更新源 %s 不可用: %s', 'wpbridge' ),
$source->name,
$error
) );

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', [] );

$notices[] = [
'type' => 'warning',
'message' => sprintf(
__( '更新源 "%s" 暂时不可用: %s', 'wpbridge' ),
$source->name,
$error
),
'time' => time(),
];

// 只保留最近 10 条通知
$notices = array_slice( $notices, -10 );

update_option( 'wpbridge_admin_notices', $notices );
}
}

View file

@ -0,0 +1,187 @@
<?php
/**
* 健康检查器
*
* @package WPBridge
*/

namespace WPBridge\Cache;

use WPBridge\UpdateSource\SourceModel;
use WPBridge\UpdateSource\Handlers\HealthStatus;
use WPBridge\Core\Logger;

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

/**
* 健康检查器类
*/
class HealthChecker {

/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;

/**
* 健康状态缓存 TTL1 小时)
*
* @var int
*/
const HEALTH_CACHE_TTL = 3600;

/**
* 失败源冷却时间30 分钟)
*
* @var int
*/
const FAILED_COOLDOWN = 1800;

/**
* 构造函数
*/
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;

// 检查缓存
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' ) );
}

// 执行健康检查
$handler = $source->get_handler();

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 );

// 如果失败,设置冷却
if ( ! $status->is_available() ) {
$this->set_cooldown( $source->id );
}

Logger::debug( '健康检查完成', [
'source' => $source->id,
'status' => $status->status,
'time' => $status->response_time,
] );

return $status;
}

/**
* 批量检查源健康状态
*
* @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 );
}

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 );

if ( false !== $cached && $cached instanceof HealthStatus ) {
return $cached;
}

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
*/
private function set_cooldown( string $source_id ): void {
$this->cache->set( 'cooldown_' . $source_id, time(), 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 );
}

/**
* 清除所有健康状态缓存
*/
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_' ) . '%'
)
);
}
}

View file

@ -0,0 +1,370 @@
<?php
/**
* Bridge Server 客户端
*
* 与 wpbridge-server Go 服务端通信
*
* @package WPBridge
* @since 0.9.8
*/

declare(strict_types=1);

namespace WPBridge\Commercial;

use WPBridge\Core\Logger;

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

/**
* BridgeClient 类
*/
class BridgeClient {

/**
* 服务端 URL
*
* @var string
*/
private string $server_url;

/**
* API Key
*
* @var string
*/
private string $api_key;

/**
* 请求超时(秒)
*
* @var int
*/
private int $timeout;

/**
* 构造函数
*
* @param string $server_url 服务端 URL
* @param string $api_key API Key
* @param int $timeout 请求超时(秒)
*/
public function __construct( string $server_url, string $api_key, int $timeout = 30 ) {
$this->server_url = rtrim( $server_url, '/' );
$this->api_key = $api_key;
$this->timeout = $timeout;
}

/**
* 获取插件信息
*
* @param string $slug 插件 slug
* @return array|null
*/
public function get_plugin_info( string $slug ): ?array {
$response = $this->request( 'GET', "/api/v1/plugin/{$slug}" );

if ( is_wp_error( $response ) ) {
Logger::error( 'Failed to get plugin info', [
'slug' => $slug,
'error' => $response->get_error_message(),
] );
return null;
}

return $response;
}

/**
* 获取下载 URL
*
* @param string $slug 插件 slug
* @return string|null
*/
public function get_download_url( string $slug ): ?string {
// 下载端点会返回重定向或直接代理
return $this->server_url . "/api/v1/download/{$slug}";
}

/**
* 列出所有供应商(需要认证)
*
* @return array
*/
public function list_vendors(): array {
$response = $this->request( 'GET', '/api/v1/admin/vendors', [], true );

if ( is_wp_error( $response ) ) {
Logger::error( 'Failed to list vendors', [
'error' => $response->get_error_message(),
] );
return [];
}

return $response ?? [];
}

/**
* 创建供应商(需要认证)
*
* @param array $data 供应商数据
* @return array
*/
public function create_vendor( array $data ): array {
$response = $this->request( 'POST', '/api/v1/admin/vendors', $data, true );

if ( is_wp_error( $response ) ) {
return [
'success' => false,
'message' => $response->get_error_message(),
];
}

return [
'success' => true,
'data' => $response,
];
}

/**
* 更新供应商(需要认证)
*
* @param int $id 供应商 ID
* @param array $data 供应商数据
* @return array
*/
public function update_vendor( int $id, array $data ): array {
$response = $this->request( 'PUT', "/api/v1/admin/vendors/{$id}", $data, true );

if ( is_wp_error( $response ) ) {
return [
'success' => false,
'message' => $response->get_error_message(),
];
}

return [
'success' => true,
'data' => $response,
];
}

/**
* 删除供应商(需要认证)
*
* @param int $id 供应商 ID
* @return array
*/
public function delete_vendor( int $id ): array {
$response = $this->request( 'DELETE', "/api/v1/admin/vendors/{$id}", [], true );

if ( is_wp_error( $response ) ) {
return [
'success' => false,
'message' => $response->get_error_message(),
];
}

return [
'success' => true,
];
}

/**
* 列出所有插件(需要认证)
*
* @return array
*/
public function list_plugins(): array {
$response = $this->request( 'GET', '/api/v1/admin/plugins', [], true );

if ( is_wp_error( $response ) ) {
Logger::error( 'Failed to list plugins', [
'error' => $response->get_error_message(),
] );
return [];
}

return $response ?? [];
}

/**
* 创建插件(需要认证)
*
* @param array $data 插件数据
* @return array
*/
public function create_plugin( array $data ): array {
$response = $this->request( 'POST', '/api/v1/admin/plugins', $data, true );

if ( is_wp_error( $response ) ) {
return [
'success' => false,
'message' => $response->get_error_message(),
];
}

return [
'success' => true,
'data' => $response,
];
}

/**
* 获取插件详情(需要认证)
*
* @param string $slug 插件 slug
* @return array|null
*/
public function get_plugin( string $slug ): ?array {
$response = $this->request( 'GET', "/api/v1/admin/plugins/{$slug}", [], true );

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

return $response;
}

/**
* 更新插件(需要认证)
*
* @param string $slug 插件 slug
* @param array $data 插件数据
* @return array
*/
public function update_plugin( string $slug, array $data ): array {
$response = $this->request( 'PUT', "/api/v1/admin/plugins/{$slug}", $data, true );

if ( is_wp_error( $response ) ) {
return [
'success' => false,
'message' => $response->get_error_message(),
];
}

return [
'success' => true,
'data' => $response,
];
}

/**
* 删除插件(需要认证)
*
* @param string $slug 插件 slug
* @return array
*/
public function delete_plugin( string $slug ): array {
$response = $this->request( 'DELETE', "/api/v1/admin/plugins/{$slug}", [], true );

if ( is_wp_error( $response ) ) {
return [
'success' => false,
'message' => $response->get_error_message(),
];
}

return [
'success' => true,
];
}

/**
* 健康检查
*
* @return bool
*/
public function health_check(): bool {
$response = $this->request( 'GET', '/health' );

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

return isset( $response['status'] ) && $response['status'] === 'ok';
}

/**
* 发送请求
*
* @param string $method HTTP 方法
* @param string $endpoint API 端点
* @param array $data 请求数据
* @param bool $auth 是否需要认证
* @return array|\WP_Error
*/
private function request( string $method, string $endpoint, array $data = [], bool $auth = false ) {
$url = $this->server_url . $endpoint;

$args = [
'method' => $method,
'timeout' => $this->timeout,
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
];

// 添加认证头
if ( $auth && ! empty( $this->api_key ) ) {
$args['headers']['X-API-Key'] = $this->api_key;
}

// 添加请求体
if ( ! empty( $data ) && in_array( $method, [ 'POST', 'PUT', 'PATCH' ], true ) ) {
$args['body'] = wp_json_encode( $data );
}

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

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

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

// 处理 204 No Content
if ( $status_code === 204 ) {
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, [ 'status' => $status_code ] );
}

// 解析 JSON 响应
$decoded = json_decode( $body, true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
return new \WP_Error( 'json_decode_error', 'Invalid JSON response' );
}

return $decoded;
}

/**
* 获取服务端 URL
*
* @return string
*/
public function get_server_url(): string {
return $this->server_url;
}

/**
* 检查是否已配置
*
* @return bool
*/
public function is_configured(): bool {
return ! empty( $this->server_url ) && ! empty( $this->api_key );
}
}

View file

@ -0,0 +1,672 @@
<?php
/**
* 商业插件桥接管理器
*
* 管理桥接插件列表,提供启用/禁用功能
*
* @package WPBridge
* @since 0.9.7
*/

declare(strict_types=1);

namespace WPBridge\Commercial;

use WPBridge\Core\Settings;
use WPBridge\Core\RemoteConfig;
use WPBridge\Core\Logger;
use WPBridge\Commercial\Vendors\VendorManager;
use WPBridge\Security\Encryption;

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

/**
* BridgeManager 类
*/
class BridgeManager {

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

/**
* 远程配置实例
*
* @var RemoteConfig
*/
private RemoteConfig $remote_config;

/**
* GPL 验证器
*
* @var GPLValidator
*/
private GPLValidator $gpl_validator;

/**
* 供应商管理器
*
* @var VendorManager
*/
private VendorManager $vendor_manager;

/**
* Bridge Server 客户端
*
* @var BridgeClient|null
*/
private ?BridgeClient $bridge_client = null;

/**
* 构造函数
*
* @param Settings $settings 设置实例
* @param RemoteConfig $remote_config 远程配置实例
*/
public function __construct( Settings $settings, RemoteConfig $remote_config ) {
$this->settings = $settings;
$this->remote_config = $remote_config;
$this->gpl_validator = new GPLValidator();
$this->vendor_manager = new VendorManager();

// 初始化 Bridge Server 客户端
$this->init_bridge_client();

// 初始化已配置的供应商
$this->init_vendors();
}

/**
* 初始化 Bridge Server 客户端
*
* @return void
*/
private function init_bridge_client(): void {
$server_url = $this->settings->get( 'bridge_server_url', '' );
// API Key 使用加密存储
$api_key = Encryption::get_secure( 'bridge_server_api_key', '' );

if ( ! empty( $server_url ) ) {
$this->bridge_client = new BridgeClient( $server_url, $api_key );
}
}

/**
* 获取 Bridge Server 客户端
*
* @return BridgeClient|null
*/
public function get_bridge_client(): ?BridgeClient {
return $this->bridge_client;
}

/**
* 设置 Bridge Server 配置
*
* @param string $server_url 服务端 URL
* @param string $api_key API Key
* @return array
*/
public function set_bridge_server( string $server_url, string $api_key ): array {
// 验证连接
$client = new BridgeClient( $server_url, $api_key );

if ( ! $client->health_check() ) {
return [
'success' => false,
'message' => __( '无法连接到 Bridge Server', 'wpbridge' ),
];
}

// 保存配置URL 明文存储API Key 加密存储)
$this->settings->set( 'bridge_server_url', $server_url );
Encryption::store_secure( 'bridge_server_api_key', $api_key );

$this->bridge_client = $client;

Logger::info( 'Bridge server configured', [ 'url' => $server_url ] );

return [
'success' => true,
'message' => __( 'Bridge Server 配置成功', 'wpbridge' ),
];
}

/**
* 初始化供应商
*
* @return void
*/
private function init_vendors(): void {
$vendor_configs = $this->settings->get( 'vendors', [] );

foreach ( $vendor_configs as $vendor_id => $config ) {
if ( empty( $config['enabled'] ) ) {
continue;
}

$type = $config['type'] ?? 'woocommerce';

switch ( $type ) {
case 'woocommerce':
$vendor = new Vendors\WooCommerceVendor(
$vendor_id,
$config['name'] ?? $vendor_id,
$config['api_url'] ?? '',
$config['consumer_key'] ?? '',
$config['consumer_secret'] ?? ''
);
$this->vendor_manager->register( $vendor );
break;
// 未来可扩展其他供应商类型
}
}
}

/**
* 获取可桥接的商业插件列表(从服务端)
*
* 优先从 Bridge Server 获取,回退到 RemoteConfig
*
* @return array
*/
public function get_available_plugins(): array {
// 优先使用 Bridge Server
if ( $this->bridge_client && $this->bridge_client->is_configured() ) {
$plugins = $this->bridge_client->list_plugins();
if ( ! empty( $plugins ) ) {
// 转换为 slug => info 格式
$result = [];
foreach ( $plugins as $plugin ) {
$result[ $plugin['slug'] ] = $plugin;
}
return $result;
}
}

// 回退到 RemoteConfig
return $this->remote_config->get( 'bridgeable_plugins', [] );
}

/**
* 获取插件下载 URL
*
* @param string $slug 插件 slug
* @return string|null
*/
public function get_plugin_download_url( string $slug ): ?string {
if ( $this->bridge_client && $this->bridge_client->is_configured() ) {
return $this->bridge_client->get_download_url( $slug );
}

return null;
}

/**
* 获取所有可用插件(混合模式)
*
* 合并三个来源:
* 1. 官方优化列表(服务端)
* 2. 供应商渠道插件
* 3. 用户自定义插件
*
* @return array
*/
public function get_all_available_plugins(): array {
$plugins = [];

// 1. 官方优化列表
$official = $this->get_available_plugins();
foreach ( $official as $slug => $info ) {
$plugins[ $slug ] = array_merge( $info, [
'source' => 'official',
'vendor' => null,
] );
}

// 2. 供应商渠道插件
$vendor_plugins = $this->vendor_manager->get_all_plugins();
foreach ( $vendor_plugins as $slug => $info ) {
if ( ! isset( $plugins[ $slug ] ) ) {
$plugins[ $slug ] = $info;
} else {
// 官方列表优先,但记录供应商也有
$plugins[ $slug ]['also_available_from'] = $info['vendor'];
}
}

// 3. 用户自定义插件
$custom = $this->settings->get( 'custom_plugins', [] );
foreach ( $custom as $slug => $info ) {
if ( ! isset( $plugins[ $slug ] ) ) {
$plugins[ $slug ] = array_merge( $info, [
'source' => 'custom',
'vendor' => null,
] );
}
}

return $plugins;
}

/**
* 获取供应商管理器
*
* @return VendorManager
*/
public function get_vendor_manager(): VendorManager {
return $this->vendor_manager;
}

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

/**
* 启用插件桥接
*
* @param string $plugin_slug 插件 slug
* @param string $plugin_file 插件文件路径(可选,用于 GPL 验证)
* @return array 包含 success 和 message 的结果
*/
public function enable_bridge( string $plugin_slug, string $plugin_file = '' ): array {
// 1. 检查是否在可桥接列表(混合模式)
$all_available = $this->get_all_available_plugins();
if ( ! isset( $all_available[ $plugin_slug ] ) ) {
return [
'success' => false,
'message' => __( '该插件不在可桥接列表中', 'wpbridge' ),
'code' => 'not_available',
];
}

$plugin_info = $all_available[ $plugin_slug ];

// 2. H5 修复: GPL 合规验证
$gpl_result = $this->gpl_validator->validate( $plugin_slug, $plugin_file );
if ( $gpl_result['is_gpl'] === false ) {
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', [
'plugin' => $plugin_slug,
'result' => $gpl_result,
] );
}

// 3. 检查订阅限制
$limit_check = $this->check_subscription_limit();
if ( ! $limit_check['allowed'] ) {
return [
'success' => false,
'message' => $limit_check['message'],
'code' => 'limit_exceeded',
];
}

// 4. 添加到桥接列表
$bridged = $this->get_bridged_plugins();
if ( ! in_array( $plugin_slug, $bridged, true ) ) {
$bridged[] = $plugin_slug;
$this->settings->set( 'bridged_plugins', $bridged );

Logger::info( 'Plugin bridge enabled', [
'plugin' => $plugin_slug,
'gpl_result' => $gpl_result,
] );
}

return [
'success' => true,
'message' => __( '桥接已启用', 'wpbridge' ),
'code' => 'enabled',
'gpl_result' => $gpl_result,
'source' => $plugin_info['source'] ?? 'official',
'vendor' => $plugin_info['vendor'] ?? null,
];
}

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

if ( $result ) {
Logger::info( 'Plugin bridge disabled', [ 'plugin' => $plugin_slug ] );
return [
'success' => true,
'message' => __( '桥接已禁用', 'wpbridge' ),
];
}

return [
'success' => false,
'message' => __( '禁用失败', 'wpbridge' ),
];
}

/**
* 检查插件是否已桥接
*
* @param string $plugin_slug 插件 slug
* @return bool
*/
public function is_bridged( string $plugin_slug ): bool {
return in_array( $plugin_slug, $this->get_bridged_plugins(), true );
}

/**
* 检查订阅限制
*
* @return array
*/
private function check_subscription_limit(): array {
$subscription = $this->get_subscription();

// Agency 计划无限制
if ( $subscription['plan'] === 'agency' ) {
return [ 'allowed' => true ];
}

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

if ( $current_count >= $limit ) {
return [
'allowed' => false,
'message' => sprintf(
/* translators: %d: plugin limit */
__( '已达到插件数量限制 (%d),请升级订阅', 'wpbridge' ),
$limit
),
];
}

return [ 'allowed' => true ];
}

/**
* 获取订阅信息
*
* @return array
*/
public function get_subscription(): array {
$default = [
'plan' => 'free',
'plugins_limit' => 0,
'site_limit' => 1,
'status' => 'active',
'expires_at' => null,
];

$subscription = $this->settings->get( 'subscription', [] );
return array_merge( $default, $subscription );
}

/**
* 获取桥接状态统计
*
* @return array
*/
public function get_stats(): array {
$subscription = $this->get_subscription();
$bridged = $this->get_bridged_plugins();
$available = $this->get_available_plugins();

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'],
];
}

/**
* 获取已安装的可桥接插件
*
* @return array
*/
public function get_installed_bridgeable_plugins(): array {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$all_plugins = get_plugins();
$available = $this->get_available_plugins();
$bridged = $this->get_bridged_plugins();
$result = [];

foreach ( $all_plugins as $file => $data ) {
$slug = dirname( $file );
if ( $slug === '.' ) {
$slug = basename( $file, '.php' );
}

if ( isset( $available[ $slug ] ) ) {
$gpl_result = $this->gpl_validator->validate( $slug, $file );

$result[ $slug ] = [
'file' => $file,
'name' => $data['Name'],
'version' => $data['Version'],
'is_bridged' => in_array( $slug, $bridged, true ),
'gpl_status' => $gpl_result,
'available' => $available[ $slug ],
];
}
}

return $result;
}

/**
* 同步可桥接插件列表
*
* @return bool
*/
public function sync_available_plugins(): bool {
return $this->remote_config->refresh();
}

/**
* 添加供应商
*
* @param string $vendor_id 供应商 ID
* @param string $name 供应商名称
* @param string $type 供应商类型 (woocommerce)
* @param string $api_url API 地址
* @param string $consumer_key Consumer Key
* @param string $consumer_secret Consumer Secret
* @return array
*/
public function add_vendor(
string $vendor_id,
string $name,
string $type,
string $api_url,
string $consumer_key,
string $consumer_secret
): array {
$vendors = $this->settings->get( 'vendors', [] );

if ( isset( $vendors[ $vendor_id ] ) ) {
return [
'success' => false,
'message' => __( '供应商 ID 已存在', 'wpbridge' ),
];
}

$vendors[ $vendor_id ] = [
'name' => $name,
'type' => $type,
'api_url' => $api_url,
'consumer_key' => $consumer_key,
'consumer_secret' => $consumer_secret,
'enabled' => true,
'created_at' => time(),
];

$this->settings->set( 'vendors', $vendors );

// 立即注册供应商
if ( $type === 'woocommerce' ) {
$vendor = new Vendors\WooCommerceVendor(
$vendor_id,
$name,
$api_url,
$consumer_key,
$consumer_secret
);
$this->vendor_manager->register( $vendor );
}

Logger::info( 'Vendor added', [ 'vendor_id' => $vendor_id, 'type' => $type ] );

return [
'success' => true,
'message' => __( '供应商已添加', 'wpbridge' ),
];
}

/**
* 移除供应商
*
* @param string $vendor_id 供应商 ID
* @return array
*/
public function remove_vendor( string $vendor_id ): array {
$vendors = $this->settings->get( 'vendors', [] );

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

unset( $vendors[ $vendor_id ] );
$this->settings->set( 'vendors', $vendors );
$this->vendor_manager->unregister( $vendor_id );

Logger::info( 'Vendor removed', [ 'vendor_id' => $vendor_id ] );

return [
'success' => true,
'message' => __( '供应商已移除', 'wpbridge' ),
];
}

/**
* 获取所有供应商
*
* @return array
*/
public function get_vendors(): array {
return $this->settings->get( 'vendors', [] );
}

/**
* 测试供应商连接
*
* @param string $vendor_id 供应商 ID
* @return array
*/
public function test_vendor_connection( string $vendor_id ): array {
$vendor = $this->vendor_manager->get( $vendor_id );

if ( ! $vendor ) {
return [
'success' => false,
'message' => __( '供应商不存在或未启用', 'wpbridge' ),
];
}

$result = $vendor->test_connection();

return [
'success' => $result,
'message' => $result
? __( '连接成功', 'wpbridge' )
: __( '连接失败', 'wpbridge' ),
];
}

/**
* 添加自定义插件
*
* @param string $plugin_slug 插件 slug
* @param array $info 插件信息
* @return array
*/
public function add_custom_plugin( string $plugin_slug, array $info ): array {
$custom = $this->settings->get( 'custom_plugins', [] );

$custom[ $plugin_slug ] = array_merge( $info, [
'added_at' => time(),
] );

$this->settings->set( 'custom_plugins', $custom );

Logger::info( 'Custom plugin added', [ 'plugin' => $plugin_slug ] );

return [
'success' => true,
'message' => __( '自定义插件已添加', 'wpbridge' ),
];
}

/**
* 移除自定义插件
*
* @param string $plugin_slug 插件 slug
* @return array
*/
public function remove_custom_plugin( string $plugin_slug ): array {
$custom = $this->settings->get( 'custom_plugins', [] );

if ( ! isset( $custom[ $plugin_slug ] ) ) {
return [
'success' => false,
'message' => __( '自定义插件不存在', 'wpbridge' ),
];
}

unset( $custom[ $plugin_slug ] );
$this->settings->set( 'custom_plugins', $custom );

return [
'success' => true,
'message' => __( '自定义插件已移除', 'wpbridge' ),
];
}
}

View file

@ -0,0 +1,391 @@
<?php
/**
* 商业插件管理器
*
* @package WPBridge
*/

namespace WPBridge\Commercial;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\Security\Validator;

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

/**
* 商业插件管理器类
* 处理商业插件的更新源覆盖和版本管理
*/
class CommercialManager {

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

/**
* 已注册的商业插件
*
* @var array
*/
private array $registered_plugins = [];

/**
* 版本锁定列表
*
* @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', [] );

$this->init_hooks();
}

/**
* 初始化钩子
*/
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, [
'name' => $slug,
'license_type' => 'unknown',
'update_source' => '',
'backup_enabled' => true,
] );

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';
}

$all_plugins = get_plugins();
$commercial_plugins = [];

foreach ( $all_plugins as $file => $data ) {
$slug = dirname( $file );

// 检测商业插件特征
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;
}

/**
* 检查是否是商业插件
*
* @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 );

if ( in_array( $slug, $known_commercial, true ) ) {
return true;
}

// 检查插件头信息
$plugin_path = WP_PLUGIN_DIR . '/' . $file;

// 路径安全验证:防止路径遍历
$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 ( file_exists( $real_path ) ) {
$content = file_get_contents( $real_path, false, null, 0, 8192 );

// 检查授权相关关键词
$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;
}
}
}

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;

if ( ! file_exists( $plugin_path ) ) {
return 'unknown';
}

$content = file_get_contents( $plugin_path, false, null, 0, 8192 );

// 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';
}

// Envato
if ( stripos( $content, 'envato' ) !== false ) {
return 'envato';
}

// WPML
if ( stripos( $content, 'OTGS' ) !== false ) {
return 'otgs';
}

return 'custom';
}

/**
* 过滤更新
*
* @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 );

// 检查版本锁定
if ( $this->is_version_locked( $slug ) ) {
$locked_version = $this->get_locked_version( $slug );

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 ] );
}
}
}

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( '无权限锁定版本', [ 'slug' => $slug, 'user' => get_current_user_id() ] );
return false;
}

// 验证版本号格式
if ( ! Validator::is_valid_version( $version ) ) {
return false;
}

$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 );

if ( $result ) {
Logger::info( '版本已锁定', [ 'slug' => $slug, 'version' => $version ] );
}

return $result;
}

/**
* 解锁版本
*
* @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;
}

unset( $this->version_locks[ $slug ] );

$result = $this->settings->set( 'version_locks', $this->version_locks );

if ( $result ) {
Logger::info( '版本已解锁', [ 'slug' => $slug ] );
}

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 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;
}

/**
* 更新完成时触发
*
* @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'] ?? [];

foreach ( $plugins as $file ) {
$slug = dirname( $file );

// 触发更新完成事件
do_action( 'wpbridge_plugin_updated', $slug, $file );

Logger::info( '插件更新完成', [ 'slug' => $slug ] );
}
}

/**
* 获取已注册的商业插件
*
* @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 [
'detected_count' => count( $detected ),
'registered_count' => count( $this->registered_plugins ),
'locked_count' => count( $this->version_locks ),
];
}
}

View file

@ -0,0 +1,359 @@
<?php
/**
* GPL 合规验证器
*
* 自动检测插件是否为 GPL 兼容授权
*
* @package WPBridge
* @since 0.9.7
*/

declare(strict_types=1);

namespace WPBridge\Commercial;

use WPBridge\Core\Logger;

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

/**
* GPLValidator 类
*/
class GPLValidator {

/**
* GPL 兼容的授权标识
*/
private const GPL_COMPATIBLE_LICENSES = [
'gpl',
'gpl-2.0',
'gpl-2.0+',
'gpl-2.0-or-later',
'gpl-3.0',
'gpl-3.0+',
'gpl-3.0-or-later',
'gplv2',
'gplv3',
'gnu general public license',
'gnu gpl',
'lgpl',
'lgpl-2.1',
'lgpl-3.0',
'mit',
'apache-2.0',
'bsd',
];

/**
* 已知的 GPL 商业插件列表
*/
private const KNOWN_GPL_PLUGINS = [
'elementor-pro',
'wordpress-seo-premium',
'advanced-custom-fields-pro',
'gravityforms',
'wpforms',
'wpforms-lite',
'ninja-forms',
'seo-by-rank-math-pro',
'wp-rocket',
'perfmatters',
'flavor',
'updraftplus',
'updraftplus-premium',
'memberpress',
'learndash',
'woocommerce-subscriptions',
'woocommerce-memberships',
];

/**
* 已知的非 GPL 插件列表(不应桥接)
*/
private const NON_GPL_PLUGINS = [
// Envato 独占插件通常不是 GPL
];

/**
* 验证结果缓存
*
* @var array
*/
private array $cache = [];

/**
* 验证插件是否 GPL 兼容
*
* @param string $plugin_slug 插件 slug
* @param string $plugin_file 插件文件路径(可选)
* @return array 包含 is_gpl, confidence, source 的数组
*/
public function validate( string $plugin_slug, string $plugin_file = '' ): array {
// 检查缓存
if ( isset( $this->cache[ $plugin_slug ] ) ) {
return $this->cache[ $plugin_slug ];
}

$result = $this->do_validate( $plugin_slug, $plugin_file );

// 缓存结果
$this->cache[ $plugin_slug ] = $result;

return $result;
}

/**
* 执行验证
*
* @param string $plugin_slug 插件 slug
* @param string $plugin_file 插件文件路径
* @return array
*/
private function do_validate( string $plugin_slug, string $plugin_file ): array {
// 1. 检查已知列表
if ( in_array( $plugin_slug, self::KNOWN_GPL_PLUGINS, true ) ) {
return [
'is_gpl' => true,
'confidence' => 100,
'source' => 'known_list',
'license' => 'GPL-2.0+',
];
}

if ( in_array( $plugin_slug, self::NON_GPL_PLUGINS, true ) ) {
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 [
'is_gpl' => true,
'confidence' => 100,
'source' => 'wordpress_org',
'license' => $wporg_result['license'] ?? 'GPL-2.0+',
];
}

// 3. 检查插件文件
if ( ! empty( $plugin_file ) ) {
$file_result = $this->check_plugin_file( $plugin_file );
if ( $file_result !== null ) {
return $file_result;
}
}

// 4. 无法确定
return [
'is_gpl' => null,
'confidence' => 0,
'source' => 'unknown',
'license' => 'unknown',
];
}

/**
* 检查 WordPress.org API
*
* @param string $plugin_slug 插件 slug
* @return array|null
*/
private function check_wordpress_org( string $plugin_slug ): ?array {
$cache_key = 'wpbridge_wporg_gpl_' . md5( $plugin_slug );
$cached = get_transient( $cache_key );

if ( $cached !== false ) {
return $cached === 'not_found' ? null : $cached;
}

$url = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=' . urlencode( $plugin_slug );
$response = wp_remote_get( $url, [ 'timeout' => 5 ] );

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

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

if ( $code === 200 && ! empty( $body ) ) {
$data = json_decode( $body, true );
if ( isset( $data['slug'] ) ) {
$result = [
'license' => 'GPL-2.0+', // WordPress.org 要求 GPL
'name' => $data['name'] ?? '',
];
set_transient( $cache_key, $result, DAY_IN_SECONDS );
return $result;
}
}

set_transient( $cache_key, 'not_found', HOUR_IN_SECONDS );
return null;
}

/**
* 检查插件文件中的授权信息
*
* @param string $plugin_file 插件文件路径
* @return array|null
*/
private function check_plugin_file( string $plugin_file ): ?array {
$plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;

if ( ! file_exists( $plugin_path ) ) {
return null;
}

// 读取插件头部
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$plugin_data = get_plugin_data( $plugin_path, false, false );
$license = $plugin_data['License'] ?? '';

if ( ! empty( $license ) ) {
$is_gpl = $this->is_gpl_compatible_license( $license );
if ( $is_gpl !== null ) {
return [
'is_gpl' => $is_gpl,
'confidence' => 90,
'source' => 'plugin_header',
'license' => $license,
];
}
}

// 检查 license.txt
$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 [
'is_gpl' => true,
'confidence' => 85,
'source' => 'license_file',
'license' => 'GPL (from license.txt)',
];
}
}

// 检查 readme.txt
$readme_file = $plugin_dir . '/readme.txt';
if ( file_exists( $readme_file ) ) {
$readme_content = file_get_contents( $readme_file );
if ( preg_match( '/License:\s*(.+)/i', $readme_content, $matches ) ) {
$license = trim( $matches[1] );
$is_gpl = $this->is_gpl_compatible_license( $license );
if ( $is_gpl !== null ) {
return [
'is_gpl' => $is_gpl,
'confidence' => 80,
'source' => 'readme_file',
'license' => $license,
];
}
}
}

return null;
}

/**
* 检查授权字符串是否 GPL 兼容
*
* @param string $license 授权字符串
* @return bool|null
*/
private function is_gpl_compatible_license( string $license ): ?bool {
$license_lower = strtolower( trim( $license ) );

foreach ( self::GPL_COMPATIBLE_LICENSES as $gpl_license ) {
if ( strpos( $license_lower, $gpl_license ) !== false ) {
return true;
}
}

// 检查明确的非 GPL 标识
$non_gpl_indicators = [ 'proprietary', 'commercial', 'all rights reserved', 'envato' ];
foreach ( $non_gpl_indicators as $indicator ) {
if ( strpos( $license_lower, $indicator ) !== false ) {
return false;
}
}

return null;
}

/**
* 检查文本是否包含 GPL 授权内容
*
* @param string $content 文本内容
* @return bool
*/
private function contains_gpl_text( string $content ): bool {
$gpl_indicators = [
'GNU General Public License',
'GPL version 2',
'GPL version 3',
'GPLv2',
'GPLv3',
'free software',
'redistribute it and/or modify',
];

foreach ( $gpl_indicators as $indicator ) {
if ( stripos( $content, $indicator ) !== false ) {
return true;
}
}

return false;
}

/**
* 批量验证
*
* @param array $plugins 插件列表 [ slug => file ]
* @return array
*/
public function validate_batch( array $plugins ): array {
$results = [];
foreach ( $plugins as $slug => $file ) {
$results[ $slug ] = $this->validate( $slug, $file );
}
return $results;
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->cache = [];
}

/**
* 添加到已知 GPL 列表(运行时)
*
* @param string $plugin_slug 插件 slug
*/
public function add_known_gpl( string $plugin_slug ): void {
$this->cache[ $plugin_slug ] = [
'is_gpl' => true,
'confidence' => 100,
'source' => 'manual',
'license' => 'GPL (manually verified)',
];
}
}

View file

@ -0,0 +1,572 @@
<?php
/**
* 商业插件授权代理
*
* 拦截商业插件的授权验证请求,转发到文派授权服务
*
* @package WPBridge
* @since 0.9.7
*/

declare(strict_types=1);

namespace WPBridge\Commercial;

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

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

/**
* LicenseProxy 类
*/
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',
],
];

/**
* 敏感参数列表(用于日志过滤)
*/
private const SENSITIVE_PARAMS = [
'license_key',
'license',
'key',
'password',
'secret',
'token',
'api_key',
'apikey',
];

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

/**
* 已桥接的插件列表
*
* @var array
*/
private array $bridged_plugins = [];

/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
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 );
}

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

/**
* 拦截 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 ) {
// 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;
}

// H2 修复: 过滤敏感信息后再记录日志
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 );
}

/**
* H2 修复: 过滤 URL 中的敏感参数
*
* @param string $url 原始 URL
* @return string 过滤后的 URL
*/
private function sanitize_url_for_log( string $url ): string {
$pattern = '/(' . implode( '|', self::SENSITIVE_PARAMS ) . ')=[^&]+/i';
return preg_replace( $pattern, '$1=[REDACTED]', $url );
}

/**
* H2 修复: 过滤请求体中的敏感参数
*
* @param array $body 请求体
* @return array 过滤后的请求体
*/
private function sanitize_body_for_log( array $body ): array {
$sanitized = [];
foreach ( $body as $key => $value ) {
if ( in_array( strtolower( $key ), self::SENSITIVE_PARAMS, true ) ) {
$sanitized[ $key ] = '[REDACTED]';
} else {
$sanitized[ $key ] = $value;
}
}
return $sanitized;
}

/**
* 检测授权系统供应商
*
* @param string $url 请求 URL
* @return string|null
*/
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
*
* @param string $url 请求 URL
* @param array $args 请求参数
* @param string $vendor 供应商
* @return string|null
*/
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( (string) $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( (string) $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;
}

/**
* 解析 item_id 到 slug
*
* @param string $item_id 项目 ID
* @return string|null
*/
private function resolve_item_id( string $item_id ): ?string {
// 从远程配置获取 ID 到 slug 的映射
$mapping = $this->settings->get( 'item_id_mapping', [] );
return $mapping[ $item_id ] ?? null;
}

/**
* 解析 Freemius ID 到 slug
*
* @param string $freemius_id Freemius ID
* @return string|null
*/
private function resolve_freemius_id( string $freemius_id ): ?string {
$mapping = $this->settings->get( 'freemius_id_mapping', [] );
return $mapping[ $freemius_id ] ?? null;
}

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

/**
* H1 修复: 生成安全的站点指纹
*
* 使用多因素生成站点指纹,防止伪造
*
* @return string
*/
private function generate_site_fingerprint(): string {
$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 ) );
}

/**
* H3 修复: 生成请求签名
*
* @param string $api_key API Key
* @param string $plugin_slug 插件 slug
* @param string $action 操作类型
* @param string $timestamp 时间戳
* @return string
*/
private function generate_request_signature( string $api_key, string $plugin_slug, string $action, string $timestamp ): string {
$data = implode( '|', [
$plugin_slug,
$this->generate_site_fingerprint(),
$action,
$timestamp,
] );

return hash_hmac( 'sha256', $data, $api_key );
}

/**
* 代理请求到文派服务
*
* @param string $vendor 供应商
* @param string $plugin_slug 插件 slug
* @param string $original_url 原始 URL
* @param array $args 请求参数
* @return array|false
*/
private function proxy_request( string $vendor, string $plugin_slug, string $original_url, array $args ) {
$proxy_url = $this->settings->get(
'license_proxy_url',
'https://updates.wenpai.net/api/v1/license/proxy'
);

$api_key = $this->get_api_key();
$timestamp = (string) time();
$action = $this->extract_action( $original_url, $args );

// H1 + H3 修复: 使用站点指纹和请求签名
$site_fingerprint = $this->generate_site_fingerprint();
$signature = $this->generate_request_signature( $api_key, $plugin_slug, $action, $timestamp );

$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', [
'error' => $response->get_error_message(),
] );
// 失败时不拦截,让原始请求继续
return false;
}

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

/**
* H4 修复: 转换响应格式以匹配原厂 API
*
* @param string $vendor 供应商
* @param string $plugin_slug 插件 slug
* @param array $response 响应
* @return array|false
*/
private function transform_response( string $vendor, string $plugin_slug, array $response ) {
$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, $plugin_slug );
case 'freemius':
return $this->format_freemius_response( $license, $plugin_slug );
case 'wc_am':
return $this->format_wc_am_response( $license );
default:
return $this->format_generic_response( $license );
}
}

/**
* H4 修复: 格式化 EDD 响应(完整字段)
*
* @param array $license 授权信息
* @param string $plugin_slug 插件 slug
* @return array
*/
private function format_edd_response( array $license, string $plugin_slug ): array {
// 获取插件特定的响应配置
$plugin_config = $this->get_plugin_response_config( $plugin_slug, 'edd' );

$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 [
'response' => [ 'code' => 200, 'message' => 'OK' ],
'body' => $body,
'headers' => [ 'content-type' => 'application/json' ],
];
}

/**
* H4 修复: 格式化 Freemius 响应(完整字段)
*
* @param array $license 授权信息
* @param string $plugin_slug 插件 slug
* @return array
*/
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( [
'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 [
'response' => [ 'code' => 200, 'message' => 'OK' ],
'body' => $body,
'headers' => [ 'content-type' => 'application/json' ],
];
}

/**
* 格式化 WC API Manager 响应
*
* @param array $license 授权信息
* @return array
*/
private function format_wc_am_response( array $license ): array {
$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 [
'response' => [ 'code' => 200, 'message' => 'OK' ],
'body' => $body,
'headers' => [ 'content-type' => 'application/json' ],
];
}

/**
* 格式化通用响应
*
* @param array $license 授权信息
* @return array
*/
private function format_generic_response( array $license ): array {
$body = wp_json_encode( [
'success' => true,
'license' => $license['status'] ?? 'valid',
'expires' => $license['expires'] ?? 'lifetime',
] );

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

/**
* H4 修复: 获取插件特定的响应配置
*
* @param string $plugin_slug 插件 slug
* @param string $vendor 供应商
* @return array
*/
private function get_plugin_response_config( string $plugin_slug, string $vendor ): array {
$configs = $this->settings->get( 'plugin_response_configs', [] );
return $configs[ $plugin_slug ][ $vendor ] ?? [];
}

/**
* 生成校验和
*
* @param array $license 授权信息
* @return string
*/
private function generate_checksum( array $license ): string {
return md5( wp_json_encode( $license ) . home_url() );
}

/**
* 生成 Freemius secret_key
*
* @return string
*/
private function generate_secret_key(): string {
return 'sk_' . bin2hex( random_bytes( 16 ) );
}

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

/**
* 提取操作类型
*
* @param string $url 请求 URL
* @param array $args 请求参数
* @return string
*/
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';
}
}

View file

@ -0,0 +1,269 @@
<?php
/**
* 供应商抽象基类
*
* 提供供应商通用功能实现
*
* @package WPBridge
* @since 0.9.7
*/

declare(strict_types=1);

namespace WPBridge\Commercial\Vendors;

use WPBridge\Core\Logger;

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

/**
* AbstractVendor 抽象类
*/
abstract class AbstractVendor implements VendorInterface {

/**
* 供应商配置
*
* @var array
*/
protected array $config = [];

/**
* 缓存前缀
*
* @var string
*/
protected string $cache_prefix = 'wpbridge_vendor_';

/**
* 缓存时间(秒)
*
* @var int
*/
protected int $cache_ttl = 3600;

/**
* 构造函数
*
* @param array $config 配置
*/
public function __construct( array $config = [] ) {
$this->config = array_merge( $this->get_default_config(), $config );
}

/**
* 获取默认配置
*
* @return array
*/
protected function get_default_config(): array {
return [
'api_url' => '',
'api_key' => '',
'api_secret' => '',
'timeout' => 15,
'enabled' => true,
];
}

/**
* 检查供应商是否可用
*
* @return bool
*/
public function is_available(): bool {
if ( empty( $this->config['enabled'] ) ) {
return false;
}

if ( empty( $this->config['api_url'] ) ) {
return false;
}

return $this->verify_credentials();
}

/**
* 发送 API 请求
*
* @param string $endpoint 端点
* @param array $params 参数
* @param string $method 方法 (GET/POST)
* @return array|null
*/
protected function api_request( string $endpoint, array $params = [], string $method = 'GET' ): ?array {
$url = trailingslashit( $this->config['api_url'] ) . ltrim( $endpoint, '/' );

$args = [
'timeout' => $this->config['timeout'],
'headers' => $this->get_request_headers(),
];

if ( $method === 'GET' && ! empty( $params ) ) {
$url = add_query_arg( $params, $url );
} elseif ( $method === 'POST' ) {
$args['body'] = $params;
}

$response = $method === 'GET'
? wp_remote_get( $url, $args )
: wp_remote_post( $url, $args );

if ( is_wp_error( $response ) ) {
Logger::error( 'Vendor API request failed', [
'vendor' => $this->get_id(),
'endpoint' => $endpoint,
'error' => $response->get_error_message(),
] );
return null;
}

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

if ( $code !== 200 ) {
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', [
'vendor' => $this->get_id(),
'endpoint' => $endpoint,
] );
return null;
}

return $data;
}

/**
* 获取请求头
*
* @return array
*/
protected function get_request_headers(): array {
return [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
];
}

/**
* 获取缓存
*
* @param string $key 缓存键
* @return mixed|null
*/
protected function get_cache( string $key ) {
$cache_key = $this->cache_prefix . $this->get_id() . '_' . md5( $key );
$cached = get_transient( $cache_key );
return $cached !== false ? $cached : null;
}

/**
* 设置缓存
*
* @param string $key 缓存键
* @param mixed $value 缓存值
* @param int $ttl 过期时间(秒)
*/
protected function set_cache( string $key, $value, int $ttl = 0 ): void {
$cache_key = $this->cache_prefix . $this->get_id() . '_' . md5( $key );
set_transient( $cache_key, $value, $ttl ?: $this->cache_ttl );
}

/**
* 清除缓存
*
* @param string $key 缓存键(可选,为空则清除所有)
*/
protected function clear_cache( string $key = '' ): void {
if ( ! empty( $key ) ) {
$cache_key = $this->cache_prefix . $this->get_id() . '_' . md5( $key );
delete_transient( $cache_key );
}
// 清除所有缓存需要遍历,暂不实现
}

/**
* 搜索插件(默认实现:从列表中过滤)
*
* @param string $keyword 关键词
* @return array
*/
public function search_plugins( string $keyword ): array {
$all_plugins = $this->get_plugins( 1, 1000 );
$keyword = strtolower( $keyword );
$results = [];

foreach ( $all_plugins['plugins'] as $plugin ) {
$name = strtolower( $plugin['name'] ?? '' );
$slug = strtolower( $plugin['slug'] ?? '' );

if ( strpos( $name, $keyword ) !== false || strpos( $slug, $keyword ) !== false ) {
$results[] = $plugin;
}
}

return $results;
}

/**
* 获取插件详情(默认实现:从列表中查找)
*
* @param string $slug 插件 slug
* @return array|null
*/
public function get_plugin( string $slug ): ?array {
// 先检查缓存
$cache_key = 'plugin_' . $slug;
$cached = $this->get_cache( $cache_key );
if ( $cached !== null ) {
return $cached;
}

// 从列表中查找
$all_plugins = $this->get_plugins( 1, 1000 );
foreach ( $all_plugins['plugins'] as $plugin ) {
if ( ( $plugin['slug'] ?? '' ) === $slug ) {
$this->set_cache( $cache_key, $plugin );
return $plugin;
}
}

return null;
}

/**
* 标准化插件数据
*
* @param array $raw_plugin 原始插件数据
* @return array
*/
protected function normalize_plugin( array $raw_plugin ): array {
return [
'slug' => $raw_plugin['slug'] ?? '',
'name' => $raw_plugin['name'] ?? $raw_plugin['title'] ?? '',
'version' => $raw_plugin['version'] ?? '',
'author' => $raw_plugin['author'] ?? '',
'description' => $raw_plugin['description'] ?? $raw_plugin['excerpt'] ?? '',
'homepage' => $raw_plugin['homepage'] ?? $raw_plugin['url'] ?? '',
'download_url' => $raw_plugin['download_url'] ?? $raw_plugin['download_link'] ?? '',
'tested' => $raw_plugin['tested'] ?? '',
'requires' => $raw_plugin['requires'] ?? $raw_plugin['requires_at_least'] ?? '',
'requires_php' => $raw_plugin['requires_php'] ?? '',
'last_updated' => $raw_plugin['last_updated'] ?? $raw_plugin['modified'] ?? '',
'vendor' => $this->get_id(),
];
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* 供应商接口
*
* 定义第三方 GPL 插件供应商的标准接口
*
* @package WPBridge
* @since 0.9.7
*/

declare(strict_types=1);

namespace WPBridge\Commercial\Vendors;

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

/**
* VendorInterface 接口
*/
interface VendorInterface {

/**
* 获取供应商唯一标识
*
* @return string
*/
public function get_id(): string;

/**
* 获取供应商信息
*
* @return array {
* @type string $id 供应商 ID
* @type string $name 供应商名称
* @type string $url 供应商网站
* @type string $api_type API 类型 (wc_am, edd, custom)
* @type bool $requires_key 是否需要 API Key
* }
*/
public function get_info(): array;

/**
* 检查供应商是否可用
*
* @return bool
*/
public function is_available(): bool;

/**
* 获取可用插件列表
*
* @param int $page 页码
* @param int $limit 每页数量
* @return array {
* @type array[] $plugins 插件列表
* @type int $total 总数
* @type int $pages 总页数
* }
*/
public function get_plugins( int $page = 1, int $limit = 100 ): array;

/**
* 搜索插件
*
* @param string $keyword 关键词
* @return array
*/
public function search_plugins( string $keyword ): array;

/**
* 获取插件详情
*
* @param string $slug 插件 slug
* @return array|null
*/
public function get_plugin( string $slug ): ?array;

/**
* 检查插件更新
*
* @param string $slug 插件 slug
* @param string $current_version 当前版本
* @return array|null {
* @type string $version 最新版本
* @type string $download_url 下载链接
* @type string $changelog 更新日志
* @type string $tested 测试的 WP 版本
* @type string $requires 最低 WP 版本
* @type string $requires_php 最低 PHP 版本
* }
*/
public function check_update( string $slug, string $current_version ): ?array;

/**
* 获取下载链接
*
* @param string $slug 插件 slug
* @param string $version 版本号(可选,默认最新)
* @return string|null
*/
public function get_download_url( string $slug, string $version = '' ): ?string;

/**
* 验证供应商授权
*
* @return bool
*/
public function verify_credentials(): bool;
}

View file

@ -0,0 +1,374 @@
<?php
/**
* 供应商管理器
*
* 管理所有第三方 GPL 插件供应商
*
* @package WPBridge
* @since 0.9.7
*/

declare(strict_types=1);

namespace WPBridge\Commercial\Vendors;

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

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

/**
* VendorManager 类
*/
class VendorManager {

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

/**
* 已注册的供应商
*
* @var VendorInterface[]
*/
private array $vendors = [];

/**
* 单例实例
*
* @var VendorManager|null
*/
private static ?VendorManager $instance = null;

/**
* 获取单例实例
*
* @param Settings|null $settings 设置实例
* @return VendorManager
*/
public static function get_instance( ?Settings $settings = null ): VendorManager {
if ( self::$instance === null ) {
self::$instance = new self( $settings ?? new Settings() );
}
return self::$instance;
}

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

/**
* 加载已配置的供应商
*/
private function load_vendors(): void {
$vendor_configs = $this->settings->get( 'vendors', [] );

foreach ( $vendor_configs as $vendor_id => $config ) {
if ( empty( $config['enabled'] ) ) {
continue;
}

$vendor = $this->create_vendor( $vendor_id, $config );
if ( $vendor !== null ) {
$this->vendors[ $vendor_id ] = $vendor;
}
}

// 允许通过 hook 注册额外供应商
do_action( 'wpbridge_register_vendors', $this );
}

/**
* 创建供应商实例
*
* @param string $vendor_id 供应商 ID
* @param array $config 配置
* @return VendorInterface|null
*/
private function create_vendor( string $vendor_id, array $config ): ?VendorInterface {
$type = $config['type'] ?? 'woocommerce';
$name = $config['name'] ?? $vendor_id;

switch ( $type ) {
case 'woocommerce':
case 'wc_am':
return new WooCommerceVendor( $vendor_id, $name, $config );

// 未来可以添加更多类型
// case 'edd':
// return new EDDVendor($vendor_id, $name, $config);

default:
Logger::warning( 'Unknown vendor type', [
'vendor_id' => $vendor_id,
'type' => $type,
] );
return null;
}
}

/**
* 注册供应商
*
* @param VendorInterface $vendor 供应商实例
*/
public function register( VendorInterface $vendor ): void {
$this->vendors[ $vendor->get_id() ] = $vendor;
}

/**
* 获取供应商
*
* @param string $vendor_id 供应商 ID
* @return VendorInterface|null
*/
public function get_vendor( string $vendor_id ): ?VendorInterface {
return $this->vendors[ $vendor_id ] ?? null;
}

/**
* 获取所有供应商
*
* @param bool $only_available 只返回可用的供应商
* @return VendorInterface[]
*/
public function get_vendors( bool $only_available = false ): array {
if ( ! $only_available ) {
return $this->vendors;
}

return array_filter(
$this->vendors,
fn( VendorInterface $vendor ) => $vendor->is_available()
);
}

/**
* 获取所有供应商信息
*
* @return array
*/
public function get_vendors_info(): array {
$info = [];
foreach ( $this->vendors as $vendor ) {
$info[ $vendor->get_id() ] = array_merge(
$vendor->get_info(),
[ 'available' => $vendor->is_available() ]
);
}
return $info;
}

/**
* 从所有供应商搜索插件
*
* @param string $keyword 关键词
* @param string $vendor_id 指定供应商(可选)
* @return array
*/
public function search_plugins( string $keyword, string $vendor_id = '' ): array {
$results = [];

$vendors = ! empty( $vendor_id )
? [ $this->get_vendor( $vendor_id ) ]
: $this->get_vendors( true );

foreach ( $vendors as $vendor ) {
if ( $vendor === null ) {
continue;
}

try {
$vendor_results = $vendor->search_plugins( $keyword );
foreach ( $vendor_results as $plugin ) {
$plugin['vendor_id'] = $vendor->get_id();
$plugin['vendor_name'] = $vendor->get_info()['name'] ?? '';
$results[] = $plugin;
}
} catch ( \Exception $e ) {
Logger::error( 'Vendor search failed', [
'vendor' => $vendor->get_id(),
'error' => $e->getMessage(),
] );
}
}

return $results;
}

/**
* 从所有供应商获取插件
*
* @param string $slug 插件 slug
* @return array|null 包含插件信息和供应商信息
*/
public function get_plugin( string $slug ): ?array {
foreach ( $this->get_vendors( true ) as $vendor ) {
$plugin = $vendor->get_plugin( $slug );
if ( $plugin !== null ) {
$plugin['vendor_id'] = $vendor->get_id();
$plugin['vendor_name'] = $vendor->get_info()['name'] ?? '';
return $plugin;
}
}
return null;
}

/**
* 检查插件更新(从所有供应商)
*
* @param string $slug 插件 slug
* @param string $current_version 当前版本
* @return array|null
*/
public function check_update( string $slug, string $current_version ): ?array {
foreach ( $this->get_vendors( true ) as $vendor ) {
$update = $vendor->check_update( $slug, $current_version );
if ( $update !== null ) {
$update['vendor_id'] = $vendor->get_id();
$update['vendor_name'] = $vendor->get_info()['name'] ?? '';
return $update;
}
}
return null;
}

/**
* 获取下载链接
*
* @param string $slug 插件 slug
* @param string $vendor_id 供应商 ID可选自动查找
* @param string $version 版本号(可选)
* @return string|null
*/
public function get_download_url( string $slug, string $vendor_id = '', string $version = '' ): ?string {
if ( ! empty( $vendor_id ) ) {
$vendor = $this->get_vendor( $vendor_id );
if ( $vendor !== null ) {
return $vendor->get_download_url( $slug, $version );
}
}

// 自动查找
foreach ( $this->get_vendors( true ) as $vendor ) {
$plugin = $vendor->get_plugin( $slug );
if ( $plugin !== null ) {
return $vendor->get_download_url( $slug, $version );
}
}

return null;
}

/**
* 添加供应商配置
*
* @param string $vendor_id 供应商 ID
* @param array $config 配置
* @return bool
*/
public function add_vendor_config( string $vendor_id, array $config ): bool {
$vendors = $this->settings->get( 'vendors', [] );
$vendors[ $vendor_id ] = $config;

$result = $this->settings->set( 'vendors', $vendors );

if ( $result ) {
// 重新加载
$vendor = $this->create_vendor( $vendor_id, $config );
if ( $vendor !== null ) {
$this->vendors[ $vendor_id ] = $vendor;
}
}

return $result;
}

/**
* 移除供应商配置
*
* @param string $vendor_id 供应商 ID
* @return bool
*/
public function remove_vendor_config( string $vendor_id ): bool {
$vendors = $this->settings->get( 'vendors', [] );

if ( ! isset( $vendors[ $vendor_id ] ) ) {
return false;
}

unset( $vendors[ $vendor_id ] );
unset( $this->vendors[ $vendor_id ] );

return $this->settings->set( 'vendors', $vendors );
}

/**
* 测试供应商连接
*
* @param string $vendor_id 供应商 ID
* @return array
*/
public function test_vendor( string $vendor_id ): array {
$vendor = $this->get_vendor( $vendor_id );

if ( $vendor === null ) {
return [
'success' => false,
'message' => __( '供应商不存在', 'wpbridge' ),
];
}

$available = $vendor->is_available();

if ( ! $available ) {
return [
'success' => false,
'message' => __( '供应商连接失败,请检查配置', 'wpbridge' ),
];
}

// 尝试获取插件列表
$plugins = $vendor->get_plugins( 1, 10 );

return [
'success' => true,
'message' => __( '连接成功', 'wpbridge' ),
'plugin_count' => $plugins['total'] ?? count( $plugins['plugins'] ),
];
}

/**
* 获取统计信息
*
* @return array
*/
public function get_stats(): array {
$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 );
$total_plugins += $plugins['total'] ?? 0;
}

return [
'total_vendors' => $total_vendors,
'active_vendors' => $active_vendors,
'total_plugins' => $total_plugins,
];
}
}

View file

@ -0,0 +1,465 @@
<?php
/**
* WooCommerce API Manager 供应商
*
* 支持使用 WooCommerce API Manager 的 GPL 插件商店
* 大部分 GPL 分发商店都使用此方案
*
* @package WPBridge
* @since 0.9.7
*/

declare(strict_types=1);

namespace WPBridge\Commercial\Vendors;

use WPBridge\Core\Logger;

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

/**
* WooCommerceVendor 类
*
* 支持的 API 端点:
* - WooCommerce API Manager v2
* - WooCommerce Software Add-on
* - 类似的 WC 扩展
*/
class WooCommerceVendor extends AbstractVendor {

/**
* 供应商 ID
*
* @var string
*/
protected string $vendor_id;

/**
* 供应商名称
*
* @var string
*/
protected string $vendor_name;

/**
* 构造函数
*
* @param string $vendor_id 供应商 ID
* @param string $vendor_name 供应商名称
* @param array $config 配置
*/
public function __construct( string $vendor_id, string $vendor_name, array $config = [] ) {
$this->vendor_id = $vendor_id;
$this->vendor_name = $vendor_name;
parent::__construct( $config );
}

/**
* 获取默认配置
*
* @return array
*/
protected function get_default_config(): array {
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', // 下载端点
] );
}

/**
* 获取供应商 ID
*
* @return string
*/
public function get_id(): string {
return $this->vendor_id;
}

/**
* 获取供应商信息
*
* @return array
*/
public function get_info(): 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,
];
}

/**
* 获取请求头
*
* @return array
*/
protected function get_request_headers(): array {
$headers = parent::get_request_headers();

// WooCommerce REST API 认证
if ( ! empty( $this->config['api_key'] ) && ! empty( $this->config['api_secret'] ) ) {
$headers['Authorization'] = 'Basic ' . base64_encode(
$this->config['api_key'] . ':' . $this->config['api_secret']
);
}

return $headers;
}

/**
* 验证供应商授权
*
* @return bool
*/
public function verify_credentials(): bool {
$cache_key = 'credentials_valid';
$cached = $this->get_cache( $cache_key );

if ( $cached !== null ) {
return (bool) $cached;
}

// 尝试获取产品列表来验证
$response = $this->api_request( $this->config['products_endpoint'], [
'per_page' => 1,
'status' => 'publish',
] );

$valid = $response !== null;
$this->set_cache( $cache_key, $valid, 300 ); // 5分钟缓存

return $valid;
}

/**
* 获取可用插件列表
*
* @param int $page 页码
* @param int $limit 每页数量
* @return array
*/
public function get_plugins( int $page = 1, int $limit = 100 ): array {
$cache_key = "plugins_page_{$page}_limit_{$limit}";
$cached = $this->get_cache( $cache_key );

if ( $cached !== null ) {
return $cached;
}

$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 [
'plugins' => [],
'total' => 0,
'pages' => 0,
];
}

$plugins = [];
foreach ( $response as $product ) {
$plugin = $this->normalize_wc_product( $product );
if ( $plugin !== null ) {
$plugins[] = $plugin;
}
}

$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 );

return $result;
}

/**
* 标准化 WooCommerce 产品数据
*
* @param array $product WC 产品数据
* @return array|null
*/
protected function normalize_wc_product( array $product ): ?array {
// 跳过非插件产品
if ( ! $this->is_plugin_product( $product ) ) {
return null;
}

// 从产品数据中提取插件信息
$slug = $this->extract_plugin_slug( $product );
if ( empty( $slug ) ) {
return null;
}

return [
'slug' => $slug,
'name' => $product['name'] ?? '',
'version' => $this->extract_version( $product ),
'author' => $this->extract_author( $product ),
'description' => wp_strip_all_tags( $product['short_description'] ?? $product['description'] ?? '' ),
'homepage' => $product['permalink'] ?? '',
'download_url' => '', // 需要单独获取
'tested' => $this->extract_meta( $product, '_tested_wp_version' ),
'requires' => $this->extract_meta( $product, '_requires_wp_version' ),
'requires_php' => $this->extract_meta( $product, '_requires_php_version' ),
'last_updated' => $product['date_modified'] ?? '',
'price' => $product['price'] ?? '0',
'product_id' => $product['id'] ?? 0,
'vendor' => $this->get_id(),
];
}

/**
* 检查产品是否是插件
*
* @param array $product 产品数据
* @return bool
*/
protected function is_plugin_product( array $product ): bool {
// 检查分类
$categories = $product['categories'] ?? [];
foreach ( $categories as $cat ) {
$cat_slug = strtolower( $cat['slug'] ?? '' );
if ( in_array( $cat_slug, [ 'plugins', 'wordpress-plugins', 'wp-plugins' ], true ) ) {
return true;
}
}

// 检查标签
$tags = $product['tags'] ?? [];
foreach ( $tags as $tag ) {
$tag_slug = strtolower( $tag['slug'] ?? '' );
if ( strpos( $tag_slug, 'plugin' ) !== false ) {
return true;
}
}

// 检查是否有下载文件
$downloads = $product['downloads'] ?? [];
foreach ( $downloads as $download ) {
$file = strtolower( $download['file'] ?? '' );
if ( strpos( $file, '.zip' ) !== false ) {
return true;
}
}

return false;
}

/**
* 从产品中提取插件 slug
*
* @param array $product 产品数据
* @return string
*/
protected function extract_plugin_slug( array $product ): string {
// 1. 从 meta 中获取
$slug = $this->extract_meta( $product, '_plugin_slug' );
if ( ! empty( $slug ) ) {
return $slug;
}

// 2. 从 SKU 获取
$sku = $product['sku'] ?? '';
if ( ! empty( $sku ) ) {
return sanitize_title( $sku );
}

// 3. 从产品 slug 获取
$product_slug = $product['slug'] ?? '';
if ( ! empty( $product_slug ) ) {
// 移除常见后缀
$product_slug = preg_replace( '/-(pro|premium|plus|addon)$/', '', $product_slug );
return $product_slug;
}

// 4. 从名称生成
return sanitize_title( $product['name'] ?? '' );
}

/**
* 从产品中提取版本号
*
* @param array $product 产品数据
* @return string
*/
protected function extract_version( array $product ): string {
// 从 meta 获取
$version = $this->extract_meta( $product, '_version' );
if ( ! empty( $version ) ) {
return $version;
}

$version = $this->extract_meta( $product, '_plugin_version' );
if ( ! empty( $version ) ) {
return $version;
}

// 从下载文件名提取
$downloads = $product['downloads'] ?? [];
foreach ( $downloads as $download ) {
$file = $download['file'] ?? '';
if ( preg_match( '/[\-_]v?(\d+\.\d+(?:\.\d+)?)/i', $file, $matches ) ) {
return $matches[1];
}
}

return '';
}

/**
* 从产品中提取作者
*
* @param array $product 产品数据
* @return string
*/
protected function extract_author( array $product ): string {
$author = $this->extract_meta( $product, '_plugin_author' );
if ( ! empty( $author ) ) {
return $author;
}

// 使用商店名称作为默认作者
return $this->vendor_name;
}

/**
* 从产品 meta 中提取值
*
* @param array $product 产品数据
* @param string $meta_key Meta 键
* @return string
*/
protected function extract_meta( array $product, string $meta_key ): string {
$meta_data = $product['meta_data'] ?? [];
foreach ( $meta_data as $meta ) {
if ( ( $meta['key'] ?? '' ) === $meta_key ) {
return (string) ( $meta['value'] ?? '' );
}
}
return '';
}

/**
* 检查插件更新
*
* @param string $slug 插件 slug
* @param string $current_version 当前版本
* @return array|null
*/
public function check_update( string $slug, string $current_version ): ?array {
$plugin = $this->get_plugin( $slug );

if ( $plugin === null ) {
return null;
}

$latest_version = $plugin['version'] ?? '';

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

if ( version_compare( $latest_version, $current_version, '<=' ) ) {
return null; // 无更新
}

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'] ?? '',
];
}

/**
* 获取下载链接
*
* @param string $slug 插件 slug
* @param string $version 版本号
* @return string|null
*/
public function get_download_url( string $slug, string $version = '' ): ?string {
$plugin = $this->get_plugin( $slug );

if ( $plugin === null ) {
return null;
}

$product_id = $plugin['product_id'] ?? 0;

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

// 构建 WC API Manager 下载链接
$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;
}

$download_url = add_query_arg(
$params,
trailingslashit( $this->config['api_url'] ) . ltrim( $this->config['download_endpoint'], '/' )
);

return $download_url;
}

/**
* 获取更新日志
*
* @param string $slug 插件 slug
* @return string
*/
protected function get_changelog( string $slug ): string {
$plugin = $this->get_plugin( $slug );

if ( $plugin === null ) {
return '';
}

// 尝试从产品描述中提取 changelog
$description = $plugin['description'] ?? '';

if ( preg_match( '/changelog[:\s]*(.+?)(?=<h|$)/is', $description, $matches ) ) {
return wp_strip_all_tags( $matches[1] );
}

return '';
}

/**
* 生成实例 ID
*
* @return string
*/
protected function generate_instance_id(): string {
return md5( home_url() . AUTH_KEY );
}
}

View file

@ -0,0 +1,502 @@
<?php
/**
* 备份管理器
*
* @package WPBridge
*/

namespace WPBridge\Core;

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

/**
* 备份管理器类
*/
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', [ $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;
}
}

View file

@ -0,0 +1,598 @@
<?php
/**
* 更新日志管理器
*
* 聚合显示插件/主题的更新日志
*
* @package WPBridge
* @since 0.9.0
*/

namespace WPBridge\Core;

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

/**
* 更新日志管理类
*/
class ChangelogManager {

/**
* 单例实例
*
* @var self|null
*/
private static ?self $instance = null;

/**
* 缓存前缀
*/
const CACHE_PREFIX = 'wpbridge_changelog_';

/**
* 缓存时间(秒)
*/
const CACHE_TTL = 3600;

/**
* 获取单例实例
*
* @return self
*/
public static function get_instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* 获取插件更新日志
*
* @param string $slug 插件 slug
* @param string $source_type 源类型wporg, custom, git
* @param string $source_url 自定义源 URL可选
* @return array
*/
public function get_plugin_changelog( string $slug, string $source_type = 'wporg', string $source_url = '' ): array {
$cache_key = self::CACHE_PREFIX . 'plugin_' . md5( $slug . $source_type . $source_url );
$cached = get_transient( $cache_key );

if ( false !== $cached ) {
return $cached;
}

$changelog = [];

switch ( $source_type ) {
case 'wporg':
$changelog = $this->fetch_wporg_plugin_changelog( $slug );
break;
case 'git':
case 'github':
case 'gitea':
$changelog = $this->fetch_git_changelog( $source_url );
break;
case 'json':
case 'custom':
$changelog = $this->fetch_custom_changelog( $source_url, 'plugin', $slug );
break;
default:
$changelog = $this->fetch_wporg_plugin_changelog( $slug );
}

if ( ! empty( $changelog ) ) {
set_transient( $cache_key, $changelog, self::CACHE_TTL );
}

return $changelog;
}

/**
* 获取主题更新日志
*
* @param string $slug 主题 slug
* @param string $source_type 源类型
* @param string $source_url 自定义源 URL
* @return array
*/
public function get_theme_changelog( string $slug, string $source_type = 'wporg', string $source_url = '' ): array {
$cache_key = self::CACHE_PREFIX . 'theme_' . md5( $slug . $source_type . $source_url );
$cached = get_transient( $cache_key );

if ( false !== $cached ) {
return $cached;
}

$changelog = [];

switch ( $source_type ) {
case 'wporg':
$changelog = $this->fetch_wporg_theme_changelog( $slug );
break;
case 'git':
case 'github':
case 'gitea':
$changelog = $this->fetch_git_changelog( $source_url );
break;
case 'json':
case 'custom':
$changelog = $this->fetch_custom_changelog( $source_url, 'theme', $slug );
break;
default:
$changelog = $this->fetch_wporg_theme_changelog( $slug );
}

if ( ! empty( $changelog ) ) {
set_transient( $cache_key, $changelog, self::CACHE_TTL );
}

return $changelog;
}

/**
* 从 WordPress.org 获取插件更新日志
*
* @param string $slug 插件 slug
* @return array
*/
private function fetch_wporg_plugin_changelog( string $slug ): array {
$api_url = 'https://api.wordpress.org/plugins/info/1.2/';
$response = wp_remote_post( $api_url, [
'timeout' => 15,
'body' => [
'action' => 'plugin_information',
'request' => serialize( (object) [
'slug' => $slug,
'fields' => [
'sections' => true,
'versions' => true,
],
] ),
],
] );

if ( is_wp_error( $response ) ) {
return $this->error_response( $response->get_error_message() );
}

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

if ( ! is_object( $data ) || isset( $data->error ) ) {
return $this->error_response( __( '无法获取插件信息', 'wpbridge' ) );
}

return $this->format_wporg_changelog( $data, 'plugin' );
}

/**
* 从 WordPress.org 获取主题更新日志
*
* @param string $slug 主题 slug
* @return array
*/
private function fetch_wporg_theme_changelog( string $slug ): array {
$api_url = 'https://api.wordpress.org/themes/info/1.2/';
$response = wp_remote_post( $api_url, [
'timeout' => 15,
'body' => [
'action' => 'theme_information',
'request' => serialize( (object) [
'slug' => $slug,
'fields' => [
'sections' => true,
'versions' => true,
],
] ),
],
] );

if ( is_wp_error( $response ) ) {
return $this->error_response( $response->get_error_message() );
}

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

if ( ! is_object( $data ) || isset( $data->error ) ) {
return $this->error_response( __( '无法获取主题信息', 'wpbridge' ) );
}

return $this->format_wporg_changelog( $data, 'theme' );
}

/**
* 格式化 WordPress.org 更新日志
*
* @param object $data API 返回数据
* @param string $type 类型plugin/theme
* @return array
*/
private function format_wporg_changelog( object $data, string $type ): array {
$result = [
'success' => true,
'source' => 'WordPress.org',
'name' => $data->name ?? '',
'slug' => $data->slug ?? '',
'version' => $data->version ?? '',
'last_updated' => $data->last_updated ?? '',
'changelog_html' => '',
'versions' => [],
];

// 提取 changelog 部分
if ( isset( $data->sections['changelog'] ) ) {
$result['changelog_html'] = $data->sections['changelog'];
}

// 提取版本历史
if ( isset( $data->versions ) && is_array( $data->versions ) ) {
$versions = array_keys( $data->versions );
rsort( $versions, SORT_NATURAL );
$result['versions'] = array_slice( $versions, 0, 20 );
}

return $result;
}

/**
* 从 Git 仓库获取更新日志
*
* @param string $url 仓库 URL
* @return array
*/
private function fetch_git_changelog( string $url ): array {
$parsed = $this->parse_git_url( $url );

if ( ! $parsed ) {
return $this->error_response( __( '无效的 Git 仓库 URL', 'wpbridge' ) );
}

switch ( $parsed['platform'] ) {
case 'github':
return $this->fetch_github_releases( $parsed['owner'], $parsed['repo'] );
case 'gitea':
case 'wenpai':
return $this->fetch_gitea_releases( $parsed['base_url'], $parsed['owner'], $parsed['repo'] );
default:
return $this->error_response( __( '不支持的 Git 平台', 'wpbridge' ) );
}
}

/**
* 解析 Git URL
*
* @param string $url URL
* @return array|null
*/
private function parse_git_url( string $url ): ?array {
// GitHub
if ( preg_match( '#github\.com/([^/]+)/([^/]+)#', $url, $matches ) ) {
return [
'platform' => 'github',
'owner' => $matches[1],
'repo' => rtrim( $matches[2], '.git' ),
'base_url' => 'https://api.github.com',
];
}

// 菲码源库 (git.wenpai.org)
if ( preg_match( '#git\.wenpai\.org/([^/]+)/([^/]+)#', $url, $matches ) ) {
return [
'platform' => 'gitea',
'owner' => $matches[1],
'repo' => rtrim( $matches[2], '.git' ),
'base_url' => 'https://git.wenpai.org',
];
}

// 通用 Gitea
if ( preg_match( '#(https?://[^/]+)/([^/]+)/([^/]+)#', $url, $matches ) ) {
return [
'platform' => 'gitea',
'owner' => $matches[2],
'repo' => rtrim( $matches[3], '.git' ),
'base_url' => $matches[1],
];
}

return null;
}

/**
* 从 GitHub 获取 Releases
*
* @param string $owner 仓库所有者
* @param string $repo 仓库名
* @return array
*/
private function fetch_github_releases( string $owner, string $repo ): array {
$api_url = "https://api.github.com/repos/{$owner}/{$repo}/releases";
$response = wp_remote_get( $api_url, [
'timeout' => 15,
'headers' => [
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'WPBridge/' . WPBRIDGE_VERSION,
],
] );

if ( is_wp_error( $response ) ) {
return $this->error_response( $response->get_error_message() );
}

$status_code = wp_remote_retrieve_response_code( $response );
if ( $status_code !== 200 ) {
return $this->error_response( sprintf( __( 'GitHub API 返回错误: %d', 'wpbridge' ), $status_code ) );
}

$body = wp_remote_retrieve_body( $response );
$releases = json_decode( $body, true );

if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $releases ) ) {
return $this->error_response( __( 'JSON 解析失败', 'wpbridge' ) );
}

return $this->format_git_releases( $releases, 'GitHub', "{$owner}/{$repo}" );
}

/**
* 从 Gitea 获取 Releases
*
* @param string $base_url API 基础 URL
* @param string $owner 仓库所有者
* @param string $repo 仓库名
* @return array
*/
private function fetch_gitea_releases( string $base_url, string $owner, string $repo ): array {
$api_url = "{$base_url}/api/v1/repos/{$owner}/{$repo}/releases";
$response = wp_remote_get( $api_url, [
'timeout' => 15,
'headers' => [
'Accept' => 'application/json',
],
] );

if ( is_wp_error( $response ) ) {
return $this->error_response( $response->get_error_message() );
}

$status_code = wp_remote_retrieve_response_code( $response );
if ( $status_code !== 200 ) {
return $this->error_response( sprintf( __( 'Gitea API 返回错误: %d', 'wpbridge' ), $status_code ) );
}

$body = wp_remote_retrieve_body( $response );
$releases = json_decode( $body, true );

if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $releases ) ) {
return $this->error_response( __( 'JSON 解析失败', 'wpbridge' ) );
}

$source_name = strpos( $base_url, 'wenpai' ) !== false ? '菲码源库' : 'Gitea';
return $this->format_git_releases( $releases, $source_name, "{$owner}/{$repo}" );
}

/**
* 格式化 Git Releases
*
* @param array $releases Releases 数据
* @param string $source 来源名称
* @param string $repo 仓库标识
* @return array
*/
private function format_git_releases( array $releases, string $source, string $repo ): array {
$changelog_html = '<div class="wpbridge-changelog-releases">';
$versions = [];

foreach ( array_slice( $releases, 0, 10 ) as $release ) {
$tag = $release['tag_name'] ?? '';
$name = $release['name'] ?? $tag;
$body = $release['body'] ?? '';
$date = $release['published_at'] ?? $release['created_at'] ?? '';
$prerelease = $release['prerelease'] ?? false;

$versions[] = $tag;

$changelog_html .= '<div class="wpbridge-release-item">';
$changelog_html .= '<h4 class="wpbridge-release-title">';
$changelog_html .= esc_html( $name );
if ( $prerelease ) {
$changelog_html .= ' <span class="wpbridge-badge wpbridge-badge-warning">' . esc_html__( '预发布', 'wpbridge' ) . '</span>';
}
$changelog_html .= '</h4>';

if ( $date ) {
$formatted_date = wp_date( get_option( 'date_format' ), strtotime( $date ) );
$changelog_html .= '<p class="wpbridge-release-date">' . esc_html( $formatted_date ) . '</p>';
}

if ( $body ) {
$body_html = $this->markdown_to_html( $body );
$changelog_html .= '<div class="wpbridge-release-body">' . wp_kses_post( $body_html ) . '</div>';
}

$changelog_html .= '</div>';
}

$changelog_html .= '</div>';

return [
'success' => true,
'source' => $source,
'name' => $repo,
'slug' => $repo,
'version' => $versions[0] ?? '',
'last_updated' => $releases[0]['published_at'] ?? '',
'changelog_html' => $changelog_html,
'versions' => $versions,
];
}

/**
* 从自定义源获取更新日志
*
* @param string $url 源 URL
* @param string $type 类型
* @param string $slug Slug
* @return array
*/
private function fetch_custom_changelog( string $url, string $type, string $slug ): array {
if ( empty( $url ) ) {
return $this->error_response( __( '未配置更新源 URL', 'wpbridge' ) );
}

$response = wp_remote_get( $url, [
'timeout' => 15,
'headers' => [
'Accept' => 'application/json',
],
] );

if ( is_wp_error( $response ) ) {
return $this->error_response( $response->get_error_message() );
}

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

if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
return $this->error_response( __( '无法解析更新源数据', 'wpbridge' ) );
}

$changelog_html = '';
$version = '';
$versions = [];

if ( isset( $data['sections']['changelog'] ) ) {
$changelog_html = $data['sections']['changelog'];
$version = $data['version'] ?? '';
} elseif ( isset( $data['changelog'] ) ) {
$changelog_html = is_array( $data['changelog'] )
? $this->format_changelog_array( $data['changelog'] )
: $data['changelog'];
$version = $data['version'] ?? '';
} elseif ( isset( $data['releases'] ) && is_array( $data['releases'] ) ) {
return $this->format_git_releases( $data['releases'], __( '自定义源', 'wpbridge' ), $slug );
}

return [
'success' => true,
'source' => __( '自定义源', 'wpbridge' ),
'name' => $data['name'] ?? $slug,
'slug' => $slug,
'version' => $version,
'last_updated' => $data['last_updated'] ?? '',
'changelog_html' => $changelog_html,
'versions' => $versions,
];
}

/**
* 格式化 changelog 数组
*
* @param array $changelog Changelog 数组
* @return string
*/
private function format_changelog_array( array $changelog ): string {
$html = '<ul class="wpbridge-changelog-list">';
foreach ( $changelog as $version => $changes ) {
$html .= '<li>';
$html .= '<strong>' . esc_html( $version ) . '</strong>';
if ( is_array( $changes ) ) {
$html .= '<ul>';
foreach ( $changes as $change ) {
$html .= '<li>' . esc_html( $change ) . '</li>';
}
$html .= '</ul>';
} else {
$html .= '<p>' . esc_html( $changes ) . '</p>';
}
$html .= '</li>';
}
$html .= '</ul>';
return $html;
}

/**
* 简单的 Markdown 转 HTML
*
* @param string $markdown Markdown 文本
* @return string
*/
private function markdown_to_html( string $markdown ): string {
$html = esc_html( $markdown );

$html = preg_replace( '/^### (.+)$/m', '<h5>$1</h5>', $html );
$html = preg_replace( '/^## (.+)$/m', '<h4>$1</h4>', $html );
$html = preg_replace( '/^# (.+)$/m', '<h3>$1</h3>', $html );

$html = preg_replace( '/\*\*(.+?)\*\*/', '<strong>$1</strong>', $html );
$html = preg_replace( '/\*(.+?)\*/', '<em>$1</em>', $html );

$html = preg_replace( '/^[\-\*] (.+)$/m', '<li>$1</li>', $html );
$html = preg_replace( '/(<li>.*<\/li>\n?)+/', '<ul>$0</ul>', $html );

$html = preg_replace( '/`([^`]+)`/', '<code>$1</code>', $html );

$html = nl2br( $html );

return $html;
}

/**
* 返回错误响应
*
* @param string $message 错误消息
* @return array
*/
private function error_response( string $message ): array {
return [
'success' => false,
'error' => $message,
'source' => '',
'name' => '',
'slug' => '',
'version' => '',
'last_updated' => '',
'changelog_html' => '',
'versions' => [],
];
}

/**
* 清除缓存
*
* @param string $type 类型plugin/theme
* @param string $slug Slug
*/
public function clear_cache( string $type, string $slug ): void {
global $wpdb;

$like = $wpdb->esc_like( '_transient_' . self::CACHE_PREFIX . $type . '_' ) . '%';
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$like
)
);
}

/**
* 清除所有缓存
*/
public function clear_all_cache(): void {
global $wpdb;

$like = $wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%';
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$like
)
);
}
}

View file

@ -0,0 +1,746 @@
<?php
/**
* 商业插件检测器
*
* 自动检测插件是否为商业插件,支持:
* - P1: 远程配置 + 已知商业插件列表
* - P2: 用户手动标记(优先级最高)
* - P3: 智能检测(代码分析)
*
* @package WPBridge
* @since 0.7.5
*/

namespace WPBridge\Core;

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

/**
* CommercialDetector 类
*/
class CommercialDetector {

/**
* 插件类型常量
*/
const TYPE_FREE = 'free';
const TYPE_COMMERCIAL = 'commercial';
const TYPE_PRIVATE = 'private';
const TYPE_UNKNOWN = 'unknown';

/**
* 缓存选项名(永久存储)
*/
const CACHE_OPTION = 'wpbridge_plugin_type_cache';

/**
* 远程配置版本选项名
*/
const CONFIG_VERSION_OPTION = 'wpbridge_remote_config_version';

/**
* 单例实例
*
* @var CommercialDetector|null
*/
private static $instance = null;

/**
* 用户标记缓存
*
* @var array
*/
private $user_marks = array();

/**
* 检测结果缓存(永久存储)
*
* @var array
*/
private $detection_cache = array();

/**
* 远程配置实例
*
* @var RemoteConfig|null
*/
private $remote_config = null;

/**
* 获取单例实例
*
* @return CommercialDetector
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* 构造函数
*/
private function __construct() {
$this->load_user_marks();
$this->remote_config = RemoteConfig::get_instance();
$this->load_detection_cache();
}

/**
* 加载用户手动标记
*/
private function load_user_marks() {
$this->user_marks = get_option( 'wpbridge_plugin_types', array() );
}

/**
* 加载检测结果缓存(永久存储)
* 如果远程配置版本变化,自动清除缓存
*/
private function load_detection_cache() {
$cached = get_option( self::CACHE_OPTION, array() );
$this->detection_cache = is_array( $cached ) ? $cached : array();

// 检查远程配置版本是否变化
$current_version = $this->remote_config->get_version();
$cached_version = get_option( self::CONFIG_VERSION_OPTION, '' );

if ( $current_version !== $cached_version ) {
// 远程配置更新了,清除缓存
$this->detection_cache = array();
update_option( self::CONFIG_VERSION_OPTION, $current_version );
}
}

/**
* 保存检测结果缓存(永久存储)
*/
private function save_detection_cache() {
update_option( self::CACHE_OPTION, $this->detection_cache, false );
}

/**
* 检测插件类型
*
* @param string $plugin_slug 插件 slug
* @param string $plugin_file 插件文件路径(可选)
* @param bool $skip_api 是否跳过 API 检查(默认 true
* @param bool $use_cache 是否使用缓存(默认 true
* @return array 包含 type 和 source 的数组
*/
public function detect( $plugin_slug, $plugin_file = '', $skip_api = true, $use_cache = true ) {
if ( empty( $plugin_slug ) ) {
return array(
'type' => self::TYPE_UNKNOWN,
'source' => 'none',
);
}

// P2: 用户手动标记优先(不缓存,实时读取)
if ( isset( $this->user_marks[ $plugin_slug ] ) ) {
return array(
'type' => $this->user_marks[ $plugin_slug ],
'source' => 'manual',
);
}

// 检查缓存
if ( $use_cache && isset( $this->detection_cache[ $plugin_slug ] ) ) {
return $this->detection_cache[ $plugin_slug ];
}

// 执行检测
$result = $this->do_detect( $plugin_slug, $plugin_file, $skip_api );

// 保存到缓存
$this->detection_cache[ $plugin_slug ] = $result;

return $result;
}

/**
* 执行实际检测逻辑
*
* @param string $plugin_slug 插件 slug
* @param string $plugin_file 插件文件路径
* @param bool $skip_api 是否跳过 API 检查
* @return array
*/
private function do_detect( $plugin_slug, $plugin_file, $skip_api ) {
// P1: 远程配置的商业插件列表
$commercial_plugins = $this->remote_config->get_commercial_plugins();
if ( in_array( $plugin_slug, $commercial_plugins, true ) ) {
return array(
'type' => self::TYPE_COMMERCIAL,
'source' => 'remote_list',
);
}

// 检查插件名称模式
if ( $this->has_commercial_pattern( $plugin_slug ) ) {
return array(
'type' => self::TYPE_COMMERCIAL,
'source' => 'pattern',
);
}

// WordPress.org API 检查(可选)
if ( ! $skip_api ) {
$wporg_result = $this->check_wordpress_org( $plugin_slug );
if ( $wporg_result !== null ) {
return array(
'type' => $wporg_result ? self::TYPE_FREE : self::TYPE_UNKNOWN,
'source' => 'wporg_api',
);
}
}

return array(
'type' => self::TYPE_UNKNOWN,
'source' => 'none',
);
}

/**
* P3: 深度扫描检测(智能检测)
*
* @param string $plugin_slug 插件 slug
* @param string $plugin_file 插件文件路径
* @return array 包含 type, source, score, reasons 的数组
*/
public function deep_scan( $plugin_slug, $plugin_file ) {
$score = 0;
$reasons = array();

if ( empty( $plugin_file ) ) {
return array(
'type' => self::TYPE_UNKNOWN,
'source' => 'deep_scan',
'score' => 0,
'reasons' => array( 'no_file' ),
);
}

$plugin_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file );

// 1. 检测 License 关键词
$license_result = $this->check_license_code( $plugin_dir );
if ( $license_result > 0 ) {
$score += $license_result;
$reasons[] = 'license_code';
}

// 2. 检测商业框架
$framework_result = $this->check_commercial_frameworks( $plugin_dir );
if ( $framework_result > 0 ) {
$score += $framework_result;
$reasons[] = 'commercial_framework';
}

// 3. 检测插件头部信息
$header_result = $this->check_plugin_headers( $plugin_file );
if ( $header_result > 0 ) {
$score += $header_result;
$reasons[] = 'commercial_domain';
}

// 4. 检测自定义更新机制
$updater_result = $this->check_custom_updater( $plugin_dir );
if ( $updater_result > 0 ) {
$score += $updater_result;
$reasons[] = 'custom_updater';
}

// 分数 >= 2 判定为商业插件
$type = $score >= 2 ? self::TYPE_COMMERCIAL : self::TYPE_UNKNOWN;

return array(
'type' => $type,
'source' => 'deep_scan',
'score' => $score,
'reasons' => $reasons,
);
}

/**
* 检测 License 相关代码
*
* @param string $plugin_dir 插件目录
* @return int 分数
*/
private function check_license_code( $plugin_dir ) {
$keywords = $this->remote_config->get_license_keywords();
if ( empty( $keywords ) ) {
$keywords = array(
'license_key',
'license_status',
'activate_license',
'deactivate_license',
);
}

$files = $this->get_php_files( $plugin_dir, 2 );

foreach ( $files as $file ) {
$content = @file_get_contents( $file );
if ( $content === false ) {
continue;
}

foreach ( $keywords as $keyword ) {
if ( stripos( $content, $keyword ) !== false ) {
return 1;
}
}
}

return 0;
}

/**
* 检测商业插件框架
*
* @param string $plugin_dir 插件目录
* @return int 分数
*/
private function check_commercial_frameworks( $plugin_dir ) {
$frameworks = $this->remote_config->get_commercial_frameworks();
if ( empty( $frameworks ) ) {
$frameworks = array(
'EDD_SL_Plugin_Updater',
'Freemius',
'WC_AM_Client',
);
}

$files = $this->get_php_files( $plugin_dir, 2 );

foreach ( $files as $file ) {
$content = @file_get_contents( $file );
if ( $content === false ) {
continue;
}

foreach ( $frameworks as $framework ) {
if ( strpos( $content, $framework ) !== false ) {
return 2;
}
}
}

return 0;
}

/**
* 检测插件头部信息
*
* @param string $plugin_file 插件文件
* @return int 分数
*/
private function check_plugin_headers( $plugin_file ) {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
if ( ! file_exists( $plugin_path ) ) {
return 0;
}

$headers = get_plugin_data( $plugin_path, false, false );
$score = 0;

// 检查 Plugin URI 是否为商业域名
$commercial_domains = $this->remote_config->get_commercial_domains();
if ( ! empty( $headers['PluginURI'] ) && ! empty( $commercial_domains ) ) {
foreach ( $commercial_domains as $domain ) {
if ( strpos( $headers['PluginURI'], $domain ) !== false ) {
$score += 2;
break;
}
}
}

// 检查名称是否包含 Pro/Premium
if ( ! empty( $headers['Name'] ) ) {
if ( preg_match( '/(pro|premium|elite|business|agency)$/i', $headers['Name'] ) ) {
$score += 1;
}
}

return $score;
}

/**
* 检测自定义更新机制
*
* @param string $plugin_dir 插件目录
* @return int 分数
*/
private function check_custom_updater( $plugin_dir ) {
$update_hooks = array(
'pre_set_site_transient_update_plugins',
'plugins_api',
);

$files = $this->get_php_files( $plugin_dir, 2 );

foreach ( $files as $file ) {
$content = @file_get_contents( $file );
if ( $content === false ) {
continue;
}

foreach ( $update_hooks as $hook ) {
if ( strpos( $content, $hook ) !== false ) {
return 1;
}
}
}

return 0;
}

/**
* 获取目录下的 PHP 文件
*
* @param string $dir 目录
* @param int $depth 深度
* @return array
*/
private function get_php_files( $dir, $depth = 1 ) {
$files = array();

if ( ! is_dir( $dir ) ) {
return $files;
}

// 主目录 PHP 文件
$main_files = glob( $dir . '/*.php' );
if ( $main_files ) {
$files = array_merge( $files, $main_files );
}

// 子目录(如果深度允许)
if ( $depth > 1 ) {
$subdirs = array( 'includes', 'inc', 'src', 'lib', 'admin' );
foreach ( $subdirs as $subdir ) {
$subdir_path = $dir . '/' . $subdir;
if ( is_dir( $subdir_path ) ) {
$sub_files = glob( $subdir_path . '/*.php' );
if ( $sub_files ) {
$files = array_merge( $files, $sub_files );
}
}
}
}

// 限制文件数量,避免性能问题
return array_slice( $files, 0, 20 );
}

/**
* 检查插件名称是否有商业模式
*
* @param string $plugin_slug 插件 slug
* @return bool
*/
private function has_commercial_pattern( $plugin_slug ) {
$patterns = array(
'-pro$',
'-premium$',
'-elite$',
'-business$',
'-agency$',
'-developer$',
'-enterprise$',
);

foreach ( $patterns as $pattern ) {
if ( preg_match( '/' . $pattern . '/i', $plugin_slug ) ) {
return true;
}
}

return false;
}

/**
* 检查插件是否在 WordPress.org 上存在
*
* @param string $plugin_slug 插件 slug
* @return bool|null
*/
private function check_wordpress_org( $plugin_slug ) {
$cache_key = 'wpbridge_wporg_' . md5( $plugin_slug );
$cached = get_transient( $cache_key );

if ( $cached !== false ) {
return $cached === 'yes';
}

$url = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=' . urlencode( $plugin_slug );
$response = wp_remote_get( $url, array( 'timeout' => 5 ) );

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

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

if ( $code === 200 && ! empty( $body ) ) {
$data = json_decode( $body, true );
if ( isset( $data['slug'] ) ) {
set_transient( $cache_key, 'yes', DAY_IN_SECONDS );
return true;
}
}

set_transient( $cache_key, 'no', DAY_IN_SECONDS );
return false;
}

/**
* 设置用户手动标记
*
* @param string $plugin_slug 插件 slug
* @param string $type 插件类型
* @return bool
*/
public function set_user_mark( $plugin_slug, $type ) {
$valid_types = array(
self::TYPE_FREE,
self::TYPE_COMMERCIAL,
self::TYPE_PRIVATE,
self::TYPE_UNKNOWN,
);

if ( ! in_array( $type, $valid_types, true ) ) {
return false;
}

if ( $type === self::TYPE_UNKNOWN ) {
unset( $this->user_marks[ $plugin_slug ] );
} else {
$this->user_marks[ $plugin_slug ] = $type;
}

return update_option( 'wpbridge_plugin_types', $this->user_marks );
}

/**
* 获取用户手动标记
*
* @param string $plugin_slug 插件 slug
* @return string|null
*/
public function get_user_mark( $plugin_slug ) {
return isset( $this->user_marks[ $plugin_slug ] ) ? $this->user_marks[ $plugin_slug ] : null;
}

/**
* 清除用户手动标记
*
* @param string $plugin_slug 插件 slug
* @return bool
*/
public function clear_user_mark( $plugin_slug ) {
return $this->set_user_mark( $plugin_slug, self::TYPE_UNKNOWN );
}

/**
* 获取类型标签
*
* @param string $type 插件类型
* @return array
*/
public static function get_type_label( $type ) {
$labels = array(
self::TYPE_FREE => array(
'label' => __( '免费', 'wpbridge' ),
'color' => 'success',
'icon' => 'dashicons-wordpress',
),
self::TYPE_COMMERCIAL => array(
'label' => __( '商业', 'wpbridge' ),
'color' => 'warning',
'icon' => 'dashicons-awards',
),
self::TYPE_PRIVATE => array(
'label' => __( '私有', 'wpbridge' ),
'color' => 'info',
'icon' => 'dashicons-lock',
),
self::TYPE_UNKNOWN => array(
'label' => __( '第三方', 'wpbridge' ),
'color' => 'gray',
'icon' => 'dashicons-admin-plugins',
),
);

return isset( $labels[ $type ] ) ? $labels[ $type ] : $labels[ self::TYPE_UNKNOWN ];
}

/**
* 获取远程配置实例
*
* @return RemoteConfig
*/
public function get_remote_config() {
return $this->remote_config;
}

/**
* 批量检测插件类型
*
* @param array $plugins 插件列表
* @param bool $use_cache 是否使用缓存
* @return array
*/
public function detect_batch( $plugins, $use_cache = true ) {
$results = array();
foreach ( $plugins as $slug => $file ) {
$results[ $slug ] = $this->detect( $slug, $file, true, $use_cache );
}
// 批量检测后保存缓存
$this->save_detection_cache();
return $results;
}

/**
* 清除检测缓存
*
* @return bool
*/
public function clear_cache() {
$this->detection_cache = array();
delete_option( self::CONFIG_VERSION_OPTION );
return delete_option( self::CACHE_OPTION );
}

/**
* 重新检测所有插件(同步方式,已废弃)
*
* @deprecated 使用 prepare_refresh() + refresh_batch() 代替
* @return array 检测结果
*/
public function refresh_all() {
// 清除缓存
$this->clear_cache();

// 刷新远程配置
$this->remote_config->refresh();

// 更新配置版本
update_option( self::CONFIG_VERSION_OPTION, $this->remote_config->get_version() );

// 获取所有已安装插件
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();

// 重新检测
$results = array();
foreach ( $all_plugins as $plugin_file => $plugin_data ) {
$plugin_slug = dirname( $plugin_file );
if ( $plugin_slug === '.' ) {
$plugin_slug = basename( $plugin_file, '.php' );
}
$results[ $plugin_slug ] = $this->detect( $plugin_slug, $plugin_file, false, false );
}

// 保存缓存
$this->save_detection_cache();

return $results;
}

/**
* 准备刷新检测(清除缓存,返回插件列表)
*
* @return array 包含 plugins 列表和 total 数量
*/
public function prepare_refresh() {
// 清除缓存
$this->clear_cache();

// 刷新远程配置
$this->remote_config->refresh();

// 更新配置版本
update_option( self::CONFIG_VERSION_OPTION, $this->remote_config->get_version() );

// 获取所有已安装插件
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();

// 构建插件列表
$plugins = array();
foreach ( $all_plugins as $plugin_file => $plugin_data ) {
$plugin_slug = dirname( $plugin_file );
if ( $plugin_slug === '.' ) {
$plugin_slug = basename( $plugin_file, '.php' );
}
$plugins[] = array(
'slug' => $plugin_slug,
'file' => $plugin_file,
'name' => $plugin_data['Name'],
);
}

return array(
'plugins' => $plugins,
'total' => count( $plugins ),
);
}

/**
* 批量检测插件(异步方式)
*
* @param array $plugins 插件列表,每项包含 slug 和 file
* @return array 检测结果
*/
public function refresh_batch( $plugins ) {
$results = array();

foreach ( $plugins as $plugin ) {
if ( ! is_array( $plugin ) ) {
continue;
}
$slug = isset( $plugin['slug'] ) ? $plugin['slug'] : '';
$file = isset( $plugin['file'] ) ? $plugin['file'] : '';

if ( empty( $slug ) ) {
continue;
}

$results[ $slug ] = $this->detect( $slug, $file, false, false );
}

// 保存缓存
$this->save_detection_cache();

return $results;
}

/**
* 获取缓存统计信息
*
* @return array
*/
public function get_cache_stats() {
return array(
'count' => count( $this->detection_cache ),
'storage' => 'wp_options (permanent)',
'config_version' => get_option( self::CONFIG_VERSION_OPTION, 'unknown' ),
);
}
}

View file

@ -0,0 +1,351 @@
<?php
/**
* 配置导入导出管理
*
* @package WPBridge
*/

namespace WPBridge\Core;

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

/**
* 配置管理类
*/
class ConfigManager {

/**
* 配置版本
*/
const CONFIG_VERSION = '1.0';

/**
* 需要导出的选项
*
* @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 = [
'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 );

if ( null !== $value ) {
// 处理敏感信息
if ( ! $include_secrets ) {
$value = $this->sanitize_secrets( $option_name, $value );
}
$config['options'][ $option_name ] = $value;
}
}

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 );
}

/**
* 导入配置
*
* @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;
}

// 导入选项
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 );
}

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;
}

/**
* 从 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 [
'success' => false,
'errors' => [ __( 'JSON 格式无效', 'wpbridge' ) ],
];
}

return $this->import( $config, $merge );
}

/**
* 验证配置格式
*
* @param array $config 配置数据
* @return array
*/
public function validate_config( array $config ): array {
$errors = [];

if ( empty( $config['version'] ) ) {
$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(
__( '配置版本 %s 高于当前支持的版本 %s', 'wpbridge' ),
$config['version'],
self::CONFIG_VERSION
);
}

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;
}

// 更新源中的敏感字段
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***';
}
}

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, [] );

// 如果当前值为空,直接使用新值
if ( empty( $current ) ) {
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_source_groups' === $option_name ) {
return $this->merge_by_id( $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' );

foreach ( $new as $source ) {
if ( empty( $source['id'] ) ) {
continue;
}

$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;
}
}

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' );

foreach ( $new as $item ) {
if ( empty( $item['id'] ) ) {
continue;
}

$index = array_search( $item['id'], $ids, true );

if ( false !== $index ) {
$merged[ $index ] = array_merge( $merged[ $index ], $item );
} else {
$merged[] = $item;
}
}

return $merged;
}

/**
* 创建备份
*
* @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 );
}

/**
* 重置为默认配置
*
* @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();

return true;
}
}

View file

@ -0,0 +1,271 @@
<?php
/**
* 默认规则管理
*
* 方案 B项目优先架构 - 默认规则层
*
* @package WPBridge
* @since 0.6.0
*/

namespace WPBridge\Core;

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

/**
* 默认规则管理类
*
* 管理全局和类型级别的默认更新源配置
*/
class DefaultsManager {

/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_defaults';

/**
* 作用范围
*/
const SCOPE_GLOBAL = 'global';
const SCOPE_PLUGIN = 'plugin';
const SCOPE_THEME = 'theme';
const SCOPE_CORE = 'core';

/**
* 缓存的默认规则
*
* @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, [] );
$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 作用范围
* @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 );
}

/**
* 获取默认源顺序
*
* @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, [ '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 = [];
$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;
}
}

// 如果没有配置的源可用,回退到 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;
}

/**
* 是否需要签名验证
*
* @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_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 );
}

/**
* 确保默认规则存在
*/
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;
}
}

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 = [
'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'] = [ 'wporg' ];
$base['trust_floor'] = 90;
break;

case self::SCOPE_PLUGIN:
case self::SCOPE_THEME:
$base['source_order'] = [ 'wenpai-mirror', 'wporg' ];
break;

case self::SCOPE_GLOBAL:
default:
break;
}

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 );
}

$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;
}
}

View file

@ -0,0 +1,406 @@
<?php
/**
* 项目配置管理
*
* 方案 B项目优先架构 - 项目配置层
*
* @package WPBridge
* @since 0.6.0
*/

namespace WPBridge\Core;

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

/**
* 项目配置管理类
*
* 管理项目(插件/主题)与更新源的绑定关系
*/
class ItemSourceManager {

/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_item_sources';

/**
* 项目类型
*/
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';

/**
* 缓存的配置
*
* @var array|null
*/
private ?array $cached_configs = null;

/**
* 源注册表
*
* @var SourceRegistry
*/
private SourceRegistry $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, [] );
}
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 $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;
}

/**
* 设置项目配置
*
* @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;
}
}

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 );
}

/**
* 删除项目配置
*
* @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;
}

/**
* 设置项目的更新源
*
* @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;
}

// 添加或更新源
$source_ids[ $source_key ] = $priority;
arsort( $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;
}

$source_ids = $config['source_ids'] ?? [];
unset( $source_ids[ $source_key ] );

// 如果没有自定义源了,切回默认模式
$mode = empty( $source_ids ) ? self::MODE_DEFAULT : self::MODE_CUSTOM;

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, [ 'mode' => self::MODE_DISABLED ] );
}

/**
* 启用项目更新(切回默认)
*
* @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, [
'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 );

// 如果禁用更新,返回空
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_DISABLED ) {
return [];
}

// 如果有自定义配置,使用自定义源
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;
}
}

// 按优先级排序
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 前缀推断
*
* @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;
}

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, 'dropin:' ) === 0 ) {
return self::TYPE_DROPIN;
}

// 无前缀时默认为插件类型(向后兼容)
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 项目键列表
* @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, [
'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;
}
}

68
includes/Core/Loader.php Normal file
View file

@ -0,0 +1,68 @@
<?php
/**
* 自动加载器
*
* @package WPBridge
*/

namespace WPBridge\Core;

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

/**
* PSR-4 风格自动加载器
*/
class Loader {

/**
* 命名空间前缀
*
* @var string
*/
private static string $namespace_prefix = 'WPBridge\\';

/**
* 基础目录
*
* @var string
*/
private static string $base_dir = '';

/**
* 注册自动加载器
*/
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;
}

// 获取相对类名
$relative_class = substr( $class, $len );

// 转换为文件路径
$file = self::$base_dir . str_replace( '\\', '/', $relative_class ) . '.php';

// 如果文件存在则加载
if ( file_exists( $file ) ) {
require_once $file;
}
}
}

// 立即注册自动加载器
Loader::register();

175
includes/Core/Logger.php Normal file
View file

@ -0,0 +1,175 @@
<?php
/**
* 日志系统
*
* @package WPBridge
*/

namespace WPBridge\Core;

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

/**
* 日志类
*/
class Logger {

/**
* 日志级别
*/
const LEVEL_DEBUG = 'debug';
const LEVEL_INFO = 'info';
const LEVEL_WARNING = 'warning';
const LEVEL_ERROR = 'error';

/**
* 选项名称
*/
const OPTION_LOGS = 'wpbridge_logs';

/**
* 最大日志条数
*/
const MAX_LOGS = 100;

/**
* 设置实例
*
* @var Settings|null
*/
private static ?Settings $settings = null;

/**
* 设置 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 = [] ): void {
self::log( self::LEVEL_DEBUG, $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 = [] ): void {
self::log( self::LEVEL_WARNING, $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 = [] ): 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, [] );

// 添加新日志
$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 );
}

update_option( self::OPTION_LOGS, $logs, false );
}

/**
* 清理上下文数据(脱敏)
*
* @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 );
}
}

return $context;
}

/**
* 获取所有日志
*
* @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;
} );
}

// 按时间倒序
return array_reverse( $logs );
}

/**
* 清除所有日志
*/
public static function clear(): void {
delete_option( self::OPTION_LOGS );
}
}

View file

@ -0,0 +1,561 @@
<?php
/**
* 数据迁移管理
*
* 方案 B从方案 A 迁移到项目优先架构
*
* @package WPBridge
* @since 0.6.0
*/

namespace WPBridge\Core;

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

/**
* 数据迁移管理类
*/
class MigrationManager {

/**
* 迁移版本选项
*/
const OPTION_VERSION = 'wpbridge_migration_version';

/**
* 备份选项前缀
*/
const BACKUP_PREFIX = 'wpbridge_backup_';

/**
* 当前迁移版本
*/
const CURRENT_VERSION = '0.6.0';

/**
* 旧选项名称(方案 A
*/
const OLD_SOURCES = 'wpbridge_sources';
const OLD_SETTINGS = 'wpbridge_settings';

/**
* 源注册表
*
* @var SourceRegistry
*/
private SourceRegistry $source_registry;

/**
* 项目配置管理器
*
* @var ItemSourceManager
*/
private ItemSourceManager $item_manager;

/**
* 默认规则管理器
*
* @var DefaultsManager
*/
private DefaultsManager $defaults_manager;

/**
* 迁移日志
*
* @var array
*/
private array $log = [];

/**
* 源 ID 映射表(旧 ID → 新 key
*
* @var array<string, string>
*/
private array $source_id_map = [];

/**
* 构造函数
*
* @param SourceRegistry $source_registry 源注册表
* @param ItemSourceManager $item_manager 项目配置管理器
* @param DefaultsManager $defaults_manager 默认规则管理器
*/
public function __construct(
SourceRegistry $source_registry,
ItemSourceManager $item_manager,
DefaultsManager $defaults_manager
) {
$this->source_registry = $source_registry;
$this->item_manager = $item_manager;
$this->defaults_manager = $defaults_manager;
}

/**
* 检查是否需要迁移
*
* @return bool
*/
public function needs_migration(): bool {
$current = get_option( self::OPTION_VERSION, '0.0.0' );
return version_compare( $current, self::CURRENT_VERSION, '<' );
}

/**
* 执行迁移
*
* @return array 迁移结果
*/
public function migrate(): array {
$this->log = [];
$this->source_id_map = [];
$this->log( 'info', '开始迁移到方案 B (v' . self::CURRENT_VERSION . ')' );

try {
// 1. 备份旧数据
$this->backup_old_data();

// 2. 迁移源数据
$this->migrate_sources();

// 3. 迁移项目配置
$this->migrate_item_configs();

// 4. 设置默认规则
$this->setup_defaults();

// 5. 验证迁移
$validation = $this->validate_migration();
if ( ! $validation['success'] ) {
throw new \Exception( '迁移验证失败: ' . implode( ', ', $validation['errors'] ) );
}

// 6. 更新版本号
update_option( self::OPTION_VERSION, self::CURRENT_VERSION );

// 7. 清理旧数据(保留备份以便回滚)
$this->cleanup_old_data();

$this->log( 'success', '迁移完成' );

return [
'success' => true,
'log' => $this->log,
'source_id_map' => $this->source_id_map,
];

} catch ( \Exception $e ) {
$this->log( 'error', '迁移失败: ' . $e->getMessage() );

// 尝试回滚
$this->rollback();

return [
'success' => false,
'error' => $e->getMessage(),
'log' => $this->log,
];
}
}

/**
* 备份旧数据
*/
private function backup_old_data(): void {
$this->log( 'info', '备份旧数据...' );

$old_sources = get_option( self::OLD_SOURCES, [] );
$old_settings = get_option( self::OLD_SETTINGS, [] );

update_option( self::BACKUP_PREFIX . 'sources', $old_sources, false );
update_option( self::BACKUP_PREFIX . 'settings', $old_settings, false );
update_option( self::BACKUP_PREFIX . 'timestamp', time(), false );

$this->log( 'info', '已备份 ' . count( $old_sources ) . ' 个源配置' );
}

/**
* 迁移源数据
*/
private function migrate_sources(): void {
$this->log( 'info', '迁移源数据...' );

$old_sources = get_option( self::OLD_SOURCES, [] );
$migrated = 0;
$skipped = 0;

foreach ( $old_sources as $old_source ) {
$source_key = $this->convert_source( $old_source );
if ( $source_key ) {
$migrated++;
} else {
$skipped++;
}
}

$this->log( 'info', "源迁移完成: {$migrated} 成功, {$skipped} 跳过" );
}

/**
* 转换单个源
*
* @param array $old_source 旧源数据
* @return string|false 成功返回 source_key
*/
private function convert_source( array $old_source ) {
$old_id = $old_source['id'] ?? '';

// 跳过预置源(新系统会自动创建)
if ( ! empty( $old_source['is_preset'] ) ) {
// 预置源映射到新的预置源 key
$preset_map = [
'wporg' => 'wporg',
'wenpai' => 'wenpai-mirror',
'wenpai-mirror' => 'wenpai-mirror',
'fair' => 'fair-aspirecloud',
'fair-aspirecloud' => 'fair-aspirecloud',
];

if ( $old_id && isset( $preset_map[ $old_id ] ) ) {
$this->source_id_map[ $old_id ] = $preset_map[ $old_id ];
}

return false;
}

// 映射旧类型到新类型
$type_map = [
'json' => SourceRegistry::TYPE_JSON,
'github' => SourceRegistry::TYPE_GIT,
'gitlab' => SourceRegistry::TYPE_GIT,
'arkpress' => SourceRegistry::TYPE_ARKPRESS,
'custom' => SourceRegistry::TYPE_CUSTOM,
];

$new_source = [
'source_key' => $old_id,
'name' => $old_source['name'] ?? '',
'type' => $type_map[ $old_source['type'] ?? 'custom' ] ?? SourceRegistry::TYPE_CUSTOM,
'api_url' => $old_source['api_url'] ?? '',
'enabled' => $old_source['enabled'] ?? true,
'default_priority' => $old_source['priority'] ?? 50,
'auth_type' => ! empty( $old_source['auth_token'] ) ? SourceRegistry::AUTH_BEARER : SourceRegistry::AUTH_NONE,
'auth_secret_ref' => ! empty( $old_source['auth_token'] ) ? $this->store_secret( $old_source['auth_token'] ) : '',
];

$new_key = $this->source_registry->add( $new_source );

// 记录映射关系
if ( $new_key && $old_id ) {
$this->source_id_map[ $old_id ] = $new_key;
}

return $new_key;
}

/**
* 迁移项目配置
*/
private function migrate_item_configs(): void {
$this->log( 'info', '迁移项目配置...' );

$old_sources = get_option( self::OLD_SOURCES, [] );
$migrated = 0;
$skipped = 0;

foreach ( $old_sources as $old_source ) {
// 跳过通配符配置(将作为默认规则处理)
if ( empty( $old_source['slug'] ) || $old_source['slug'] === '*' ) {
continue;
}

$item_type = $old_source['item_type'] ?? 'plugin';
$item_key = $this->resolve_item_key( $old_source['slug'], $item_type );

if ( ! $item_key ) {
$skipped++;
continue;
}

// 检查源 ID 是否有效
$old_source_id = $old_source['id'] ?? '';
if ( empty( $old_source_id ) ) {
$this->log( 'warning', "跳过项目 {$item_key}: 源 ID 为空" );
$skipped++;
continue;
}

// 使用映射后的源 key
$new_source_key = $this->source_id_map[ $old_source_id ] ?? $old_source_id;

// 验证新源存在
if ( ! $this->source_registry->get( $new_source_key ) ) {
$this->log( 'warning', "跳过项目 {$item_key}: 源 {$new_source_key} 不存在" );
$skipped++;
continue;
}

$this->item_manager->set( $item_key, [
'item_type' => $item_type,
'item_slug' => $old_source['slug'],
'mode' => ItemSourceManager::MODE_CUSTOM,
'source_ids' => [ $new_source_key => $old_source['priority'] ?? 50 ],
] );
$migrated++;
}

$this->log( 'info', "项目配置迁移完成: {$migrated} 成功, {$skipped} 跳过" );
}

/**
* 解析项目键
*
* @param string $slug 项目 slug
* @param string $item_type 项目类型
* @return string|null
*/
private function resolve_item_key( string $slug, string $item_type ): ?string {
if ( $item_type === 'plugin' ) {
// 尝试查找已安装的插件
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$plugins = get_plugins();
foreach ( $plugins as $plugin_file => $plugin_data ) {
// 匹配 slug
$plugin_slug = dirname( $plugin_file );
if ( $plugin_slug === '.' ) {
$plugin_slug = basename( $plugin_file, '.php' );
}

if ( $plugin_slug === $slug ) {
return 'plugin:' . $plugin_file;
}
}

// 未找到已安装插件,使用 slug 作为预配置
return 'plugin:' . $slug;
}

if ( $item_type === 'theme' ) {
// 检查主题是否存在
$theme = wp_get_theme( $slug );
if ( $theme->exists() ) {
return 'theme:' . $slug;
}

// 预配置
return 'theme:' . $slug;
}

return null;
}

/**
* 设置默认规则
*/
private function setup_defaults(): void {
$this->log( 'info', '设置默认规则...' );

$old_sources = get_option( self::OLD_SOURCES, [] );

// 查找通配符配置
$plugin_defaults = [];
$theme_defaults = [];

foreach ( $old_sources as $old_source ) {
if ( empty( $old_source['slug'] ) || $old_source['slug'] === '*' ) {
$item_type = $old_source['item_type'] ?? 'plugin';
$old_source_id = $old_source['id'] ?? '';

// 使用映射后的源 key
$new_source_key = $this->source_id_map[ $old_source_id ] ?? $old_source_id;

// 验证源存在
if ( ! $this->source_registry->get( $new_source_key ) ) {
continue;
}

if ( $item_type === 'plugin' && $new_source_key ) {
$plugin_defaults[] = $new_source_key;
} elseif ( $item_type === 'theme' && $new_source_key ) {
$theme_defaults[] = $new_source_key;
}
}
}

// 设置默认源顺序
if ( ! empty( $plugin_defaults ) ) {
$this->defaults_manager->set_source_order( DefaultsManager::SCOPE_PLUGIN, $plugin_defaults );
}

if ( ! empty( $theme_defaults ) ) {
$this->defaults_manager->set_source_order( DefaultsManager::SCOPE_THEME, $theme_defaults );
}

$this->log( 'info', '默认规则设置完成' );
}

/**
* 验证迁移
*
* @return array
*/
private function validate_migration(): array {
$errors = [];

// 检查源注册表
$sources = $this->source_registry->get_all();
if ( empty( $sources ) ) {
$errors[] = '源注册表为空';
}

// 检查预置源
$preset_keys = [ 'wporg', 'wenpai-mirror', 'fair-aspirecloud' ];
foreach ( $preset_keys as $key ) {
if ( ! $this->source_registry->get( $key ) ) {
$errors[] = "预置源 {$key} 不存在";
}
}

// 检查默认规则
$defaults = $this->defaults_manager->get_all();
if ( empty( $defaults ) ) {
$errors[] = '默认规则为空';
}

return [
'success' => empty( $errors ),
'errors' => $errors,
];
}

/**
* 回滚迁移
*
* @return bool
*/
public function rollback(): bool {
$this->log( 'warning', '开始回滚...' );

try {
// 恢复备份数据
$backup_sources = get_option( self::BACKUP_PREFIX . 'sources', [] );
$backup_settings = get_option( self::BACKUP_PREFIX . 'settings', [] );

if ( ! empty( $backup_sources ) ) {
update_option( self::OLD_SOURCES, $backup_sources );
}

if ( ! empty( $backup_settings ) ) {
update_option( self::OLD_SETTINGS, $backup_settings );
}

// 删除新数据
delete_option( SourceRegistry::OPTION_NAME );
delete_option( ItemSourceManager::OPTION_NAME );
delete_option( DefaultsManager::OPTION_NAME );
delete_option( self::OPTION_VERSION );

$this->log( 'info', '回滚完成' );
return true;

} catch ( \Exception $e ) {
$this->log( 'error', '回滚失败: ' . $e->getMessage() );
return false;
}
}

/**
* 清理备份数据
*
* @return bool
*/
public function cleanup_backup(): bool {
delete_option( self::BACKUP_PREFIX . 'sources' );
delete_option( self::BACKUP_PREFIX . 'settings' );
delete_option( self::BACKUP_PREFIX . 'timestamp' );
return true;
}

/**
* 清理旧数据
*
* 迁移成功后删除旧的选项数据
* 注意:备份数据保留以便回滚
*/
private function cleanup_old_data(): void {
$this->log( 'info', '清理旧数据...' );

// 删除旧的选项
delete_option( self::OLD_SOURCES );
delete_option( self::OLD_SETTINGS );

$this->log( 'info', '旧数据清理完成' );
}

/**
* 完全清理(包括备份)
*
* 在确认迁移稳定后调用
* 注意:旧数据已在 migrate() 中清理,此方法主要清理备份
*
* @return bool
*/
public function full_cleanup(): bool {
$this->cleanup_backup();
return true;
}

/**
* 存储敏感信息
*
* @param string $secret 敏感信息
* @return string 引用键
*/
private function store_secret( string $secret ): string {
$ref = 'secret_' . wp_generate_uuid4();
update_option( 'wpbridge_secret_' . $ref, $secret, false );
return $ref;
}

/**
* 记录日志
*
* @param string $level 日志级别
* @param string $message 日志消息
*/
private function log( string $level, string $message ): void {
$this->log[] = [
'level' => $level,
'message' => $message,
'timestamp' => current_time( 'mysql' ),
];

// 同时写入 WordPress 日志
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( "[WPBridge Migration] [{$level}] {$message}" );
}
}

/**
* 获取迁移日志
*
* @return array
*/
public function get_log(): array {
return $this->log;
}

/**
* 获取源 ID 映射表
*
* @return array<string, string> 旧 ID → 新 key
*/
public function get_source_id_map(): array {
return $this->source_id_map;
}
}

886
includes/Core/Plugin.php Normal file
View file

@ -0,0 +1,886 @@
<?php
/**
* 插件主类
*
* @package WPBridge
*/

namespace WPBridge\Core;

use WPBridge\UpdateSource\PluginUpdater;
use WPBridge\UpdateSource\ThemeUpdater;
use WPBridge\Admin\AdminPage;
use WPBridge\AIBridge\AIGateway;
use WPBridge\Commercial\CommercialManager;
use WPBridge\Notification\NotificationManager;
use WPBridge\SourceGroup\GroupManager;
use WPBridge\API\RestController;

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

/**
* 插件主类(单例模式)
*/
class Plugin {

/**
* 单例实例
*
* @var Plugin|null
*/
private static ?Plugin $instance = null;

/**
* 设置管理器
*
* @var Settings
*/
private Settings $settings;

/**
* 源注册表(方案 B
*
* @var SourceRegistry|null
*/
private ?SourceRegistry $source_registry = null;

/**
* 项目配置管理器(方案 B
*
* @var ItemSourceManager|null
*/
private ?ItemSourceManager $item_manager = null;

/**
* 默认规则管理器(方案 B
*
* @var DefaultsManager|null
*/
private ?DefaultsManager $defaults_manager = null;

/**
* 插件更新器
*
* @var PluginUpdater|null
*/
private ?PluginUpdater $plugin_updater = null;

/**
* 主题更新器
*
* @var ThemeUpdater|null
*/
private ?ThemeUpdater $theme_updater = null;

/**
* AI 网关
*
* @var AIGateway|null
*/
private ?AIGateway $ai_gateway = null;

/**
* 商业插件管理器
*
* @var CommercialManager|null
*/
private ?CommercialManager $commercial_manager = null;

/**
* 通知管理器
*
* @var NotificationManager|null
*/
private ?NotificationManager $notification_manager = null;

/**
* 源分组管理器
*
* @var GroupManager|null
*/
private ?GroupManager $group_manager = null;

/**
* REST API 控制器
*
* @var RestController|null
*/
private ?RestController $rest_controller = null;

/**
* 商业插件检测器
*
* @var CommercialDetector|null
*/
private ?CommercialDetector $commercial_detector = null;

/**
* 获取单例实例
*
* @return Plugin
*/
public static function get_instance(): Plugin {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* 私有构造函数
*/
private function __construct() {
$this->settings = new Settings();

// 初始化方案 B 数据模型
$this->source_registry = new SourceRegistry();
$this->defaults_manager = new DefaultsManager();
$this->item_manager = new ItemSourceManager( $this->source_registry );

// 检查并执行迁移
$this->maybe_migrate();

$this->init_hooks();
}

/**
* 检查并执行数据迁移
*/
private function maybe_migrate(): void {
$migration = new MigrationManager(
$this->source_registry,
$this->item_manager,
$this->defaults_manager
);

if ( $migration->needs_migration() ) {
$result = $migration->migrate();
if ( ! $result['success'] ) {
// 记录迁移失败
Logger::error( '数据迁移失败', [ 'log' => $result['log'] ] );
}
}
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 加载文本域
add_action( 'init', [ $this, 'load_textdomain' ] );

// 初始化更新器
add_action( 'init', [ $this, 'init_updaters' ] );

// 管理界面 - 在 plugins_loaded 之后立即初始化
if ( is_admin() ) {
$this->init_admin();
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );

// AJAX 处理
add_action( 'wp_ajax_wpbridge_set_plugin_type', [ $this, 'ajax_set_plugin_type' ] );
add_action( 'wp_ajax_wpbridge_refresh_commercial_detection', [ $this, 'ajax_refresh_commercial_detection' ] );
add_action( 'wp_ajax_wpbridge_prepare_refresh', [ $this, 'ajax_prepare_refresh' ] );
add_action( 'wp_ajax_wpbridge_refresh_batch', [ $this, 'ajax_refresh_batch' ] );
add_action( 'wp_ajax_wpbridge_export_config', [ $this, 'ajax_export_config' ] );
add_action( 'wp_ajax_wpbridge_import_config', [ $this, 'ajax_import_config' ] );
add_action( 'wp_ajax_wpbridge_lock_version', [ $this, 'ajax_lock_version' ] );
add_action( 'wp_ajax_wpbridge_unlock_version', [ $this, 'ajax_unlock_version' ] );
add_action( 'wp_ajax_wpbridge_rollback', [ $this, 'ajax_rollback' ] );
add_action( 'wp_ajax_wpbridge_get_backups', [ $this, 'ajax_get_backups' ] );
add_action( 'wp_ajax_wpbridge_get_changelog', [ $this, 'ajax_get_changelog' ] );
}

// 插件链接
add_filter( 'plugin_action_links_' . WPBRIDGE_BASENAME, [ $this, 'add_action_links' ] );
}

/**
* 加载文本域
*/
public function load_textdomain(): void {
load_plugin_textdomain(
'wpbridge',
false,
dirname( WPBRIDGE_BASENAME ) . '/languages'
);
}

/**
* 初始化更新器
*/
public function init_updaters(): void {
$this->plugin_updater = new PluginUpdater( $this->settings );
$this->theme_updater = new ThemeUpdater( $this->settings );
$this->ai_gateway = new AIGateway( $this->settings );
$this->commercial_manager = new CommercialManager( $this->settings );
$this->notification_manager = new NotificationManager( $this->settings );
$this->group_manager = new GroupManager( $this->settings );
$this->rest_controller = new RestController( $this->settings );
$this->commercial_detector = CommercialDetector::get_instance();

// 初始化版本锁定
VersionLock::get_instance();

// 初始化备份管理器
BackupManager::get_instance();

// 初始化 Site Health 集成
new SiteHealth( $this->settings );

// 注册 AI 适配器
$this->register_ai_adapters();
}

/**
* 注册 AI 适配器
*/
private function register_ai_adapters(): void {
if ( null === $this->ai_gateway ) {
return;
}

$this->ai_gateway->register_adapter(
'yoast',
new \WPBridge\AIBridge\Adapters\YoastAdapter( $this->settings )
);

$this->ai_gateway->register_adapter(
'rankmath',
new \WPBridge\AIBridge\Adapters\RankMathAdapter( $this->settings )
);
}

/**
* 初始化管理界面
*/
public function init_admin(): void {
new AdminPage( $this->settings );
}

/**
* 加载管理界面资源
*
* @param string $hook 当前页面钩子
*/
public function enqueue_admin_assets( string $hook ): void {
// 只在插件页面加载
if ( strpos( $hook, 'wpbridge' ) === false ) {
return;
}

wp_enqueue_style(
'wpbridge-admin',
WPBRIDGE_URL . 'assets/css/admin.css',
[],
WPBRIDGE_VERSION
);

wp_enqueue_script(
'wpbridge-admin',
WPBRIDGE_URL . 'assets/js/admin.js',
[ 'jquery' ],
WPBRIDGE_VERSION,
true
);

wp_localize_script( 'wpbridge-admin', 'wpbridge', [
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wpbridge_nonce' ),
'i18n' => [
'confirm_delete' => __( '确定要删除这个更新源吗?', 'wpbridge' ),
'confirm_revoke' => __( '确定要撤销此 API Key 吗?', 'wpbridge' ),
'confirm_clear_logs' => __( '确定要清除所有日志吗?', 'wpbridge' ),
'testing' => __( '测试中...', 'wpbridge' ),
'success' => __( '成功', 'wpbridge' ),
'failed' => __( '操作失败', 'wpbridge' ),
'enabled' => __( '已启用', 'wpbridge' ),
'disabled' => __( '已禁用', 'wpbridge' ),
'healthy' => __( '正常', 'wpbridge' ),
'degraded' => __( '降级', 'wpbridge' ),
'failed_status' => __( '失败', 'wpbridge' ),
'test_success' => __( '连接成功', 'wpbridge' ),
'test_degraded' => __( '连接异常', 'wpbridge' ),
'cache_cleared' => __( '缓存已清除', 'wpbridge' ),
'logs_cleared' => __( '日志已清除', 'wpbridge' ),
'no_logs' => __( '暂无日志记录', 'wpbridge' ),
'enter_key_name' => __( '请输入 API Key 名称:', 'wpbridge' ),
'key_generated' => __( 'API Key 已生成,请妥善保存:', 'wpbridge' ),
'key_warning' => __( '此 Key 只会显示一次,请立即复制保存。', 'wpbridge' ),
'key_revoked' => __( 'API Key 已撤销', 'wpbridge' ),
// 诊断工具相关
'close_notice' => __( '关闭此通知', 'wpbridge' ),
'copied' => __( '已复制到剪贴板', 'wpbridge' ),
'diagnostics_complete' => __( '诊断完成', 'wpbridge' ),
'environment_ok' => __( '环境检查已完成', 'wpbridge' ),
'diagnostics_report' => __( 'WPBridge 诊断报告', 'wpbridge' ),
'generated_at' => __( '生成时间', 'wpbridge' ),
'system_info' => __( '系统信息', 'wpbridge' ),
'environment_check' => __( '环境检查', 'wpbridge' ),
'config_check' => __( '配置检查', 'wpbridge' ),
'source_status' => __( '更新源状态', 'wpbridge' ),
'passed' => __( '通过', 'wpbridge' ),
'warning' => __( '警告', 'wpbridge' ),
'not_tested' => __( '未测试', 'wpbridge' ),
'status' => __( '状态', 'wpbridge' ),
// 插件类型相关
'type_free' => __( '免费', 'wpbridge' ),
'type_commercial' => __( '商业', 'wpbridge' ),
'type_private' => __( '私有', 'wpbridge' ),
'type_unknown' => __( '第三方', 'wpbridge' ),
'type_saved' => __( '插件类型已保存', 'wpbridge' ),
'manual_mark' => __( '手动标记', 'wpbridge' ),
'manual_marked' => __( '当前为手动标记', 'wpbridge' ),
// 配置导入导出
'config_exported' => __( '配置已导出', 'wpbridge' ),
'config_imported' => __( '配置已导入', 'wpbridge' ),
'import_failed' => __( '导入失败', 'wpbridge' ),
'invalid_file' => __( '无效的配置文件', 'wpbridge' ),
'confirm_import' => __( '确定要导入配置吗?这将覆盖当前设置。', 'wpbridge' ),
// 版本锁定
'version_locked' => __( '版本已锁定', 'wpbridge' ),
'version_unlocked' => __( '版本已解锁', 'wpbridge' ),
'lock_current' => __( '锁定当前版本', 'wpbridge' ),
'lock_specific' => __( '锁定指定版本', 'wpbridge' ),
'lock_ignore' => __( '忽略特定版本', 'wpbridge' ),
'confirm_unlock' => __( '确定要解锁此版本吗?', 'wpbridge' ),
// 备份回滚
'rollback_success' => __( '回滚成功', 'wpbridge' ),
'rollback_failed' => __( '回滚失败', 'wpbridge' ),
'confirm_rollback' => __( '确定要回滚到此版本吗?当前版本将被覆盖。', 'wpbridge' ),
'no_backups' => __( '暂无备份', 'wpbridge' ),
// 更新日志
'changelog_title' => __( '更新日志', 'wpbridge' ),
'changelog_error' => __( '获取更新日志失败', 'wpbridge' ),
'loading' => __( '加载中...', 'wpbridge' ),
'last_updated' => __( '最后更新', 'wpbridge' ),
'recent_versions' => __( '最近版本', 'wpbridge' ),
'no_changelog' => __( '暂无更新日志', 'wpbridge' ),
// 模态框通用
'confirm_title' => __( '确认操作', 'wpbridge' ),
'confirm_btn' => __( '确定', 'wpbridge' ),
'cancel_btn' => __( '取消', 'wpbridge' ),
'delete_btn' => __( '删除', 'wpbridge' ),
'copy' => __( '复制', 'wpbridge' ),
// 删除更新源
'confirm_delete_title' => __( '删除更新源', 'wpbridge' ),
// API Key
'generate_api_key' => __( '生成 API Key', 'wpbridge' ),
'key_name_placeholder' => __( '例如:我的应用', 'wpbridge' ),
'key_name_required' => __( '请输入名称', 'wpbridge' ),
'key_generated_title' => __( 'API Key 已生成', 'wpbridge' ),
'revoke_key_title' => __( '撤销 API Key', 'wpbridge' ),
'revoke_btn' => __( '撤销', 'wpbridge' ),
// 清除日志
'clear_logs_title' => __( '清除日志', 'wpbridge' ),
'clear_btn' => __( '清除', 'wpbridge' ),
// 批量操作
'bulk_action_title' => __( '批量操作', 'wpbridge' ),
'confirm_bulk_action' => __( '确定要对选中的 {count} 个项目执行"{action}"操作吗?', 'wpbridge' ),
'action_set_source' => __( '设置更新源', 'wpbridge' ),
'action_reset' => __( '重置为默认', 'wpbridge' ),
'action_disable' => __( '禁用更新', 'wpbridge' ),
// 导入配置
'import_config_title' => __( '导入配置', 'wpbridge' ),
'import_btn' => __( '导入', 'wpbridge' ),
// 解锁版本
'unlock_version_title' => __( '解锁版本', 'wpbridge' ),
'unlock_btn' => __( '解锁', 'wpbridge' ),
// 异步检测
'no_plugins' => __( '没有插件需要检测', 'wpbridge' ),
'detecting' => __( '正在检测插件...', 'wpbridge' ),
'detection_complete' => __( '检测完成', 'wpbridge' ),
'progress' => __( '检测进度', 'wpbridge' ),
],
] );
}

/**
* 添加插件操作链接
*
* @param array $links 现有链接
* @return array
*/
public function add_action_links( array $links ): array {
$settings_link = sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=wpbridge' ),
__( '设置', 'wpbridge' )
);
array_unshift( $links, $settings_link );
return $links;
}

/**
* 获取设置管理器
*
* @return Settings
*/
public function get_settings(): Settings {
return $this->settings;
}

/**
* 获取 AI 网关
*
* @return AIGateway|null
*/
public function get_ai_gateway(): ?AIGateway {
return $this->ai_gateway;
}

/**
* 获取商业插件管理器
*
* @return CommercialManager|null
*/
public function get_commercial_manager(): ?CommercialManager {
return $this->commercial_manager;
}

/**
* 获取通知管理器
*
* @return NotificationManager|null
*/
public function get_notification_manager(): ?NotificationManager {
return $this->notification_manager;
}

/**
* 获取源分组管理器
*
* @return GroupManager|null
*/
public function get_group_manager(): ?GroupManager {
return $this->group_manager;
}

/**
* 获取 REST API 控制器
*
* @return RestController|null
*/
public function get_rest_controller(): ?RestController {
return $this->rest_controller;
}

/**
* 获取商业插件检测器
*
* @return CommercialDetector|null
*/
public function get_commercial_detector(): ?CommercialDetector {
return $this->commercial_detector;
}

/**
* AJAX: 设置插件类型
*/
public function ajax_set_plugin_type(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$plugin_slug = isset( $_POST['plugin_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_slug'] ) ) : '';
$plugin_type = isset( $_POST['plugin_type'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_type'] ) ) : '';

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

if ( empty( $plugin_type ) ) {
wp_send_json_error( array( 'message' => __( '插件类型不能为空', 'wpbridge' ) ) );
}

$detector = CommercialDetector::get_instance();
$result = $detector->set_user_mark( $plugin_slug, $plugin_type );

if ( $result ) {
wp_send_json_success( array(
'message' => __( '插件类型已保存', 'wpbridge' ),
'type' => $plugin_type,
'label' => CommercialDetector::get_type_label( $plugin_type ),
) );
} else {
wp_send_json_error( array( 'message' => __( '保存失败', 'wpbridge' ) ) );
}
}

/**
* AJAX: 刷新商业插件检测
*/
public function ajax_refresh_commercial_detection(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$detector = CommercialDetector::get_instance();
$results = $detector->refresh_all();

$stats = array(
'total' => count( $results ),
'free' => 0,
'commercial' => 0,
'private' => 0,
'unknown' => 0,
);

foreach ( $results as $result ) {
if ( isset( $stats[ $result['type'] ] ) ) {
$stats[ $result['type'] ]++;
}
}

wp_send_json_success( array(
'message' => sprintf(
__( '已重新检测 %d 个插件:%d 免费,%d 商业,%d 第三方', 'wpbridge' ),
$stats['total'],
$stats['free'],
$stats['commercial'],
$stats['unknown'] + $stats['private']
),
'stats' => $stats,
) );
}

/**
* AJAX: 准备刷新检测(返回插件列表)
*/
public function ajax_prepare_refresh(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$detector = CommercialDetector::get_instance();
$data = $detector->prepare_refresh();

wp_send_json_success( $data );
}

/**
* AJAX: 批量检测插件
*/
public function ajax_refresh_batch(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

if ( empty( $_POST['plugins'] ) ) {
wp_send_json_error( array( 'message' => __( '插件列表为空', 'wpbridge' ) ) );
}

$plugins = json_decode( wp_unslash( $_POST['plugins'] ), true );

if ( ! is_array( $plugins ) ) {
wp_send_json_error( array( 'message' => __( '插件列表格式无效', 'wpbridge' ) ) );
}

// 验证并清理每个插件数据
$sanitized_plugins = array();
foreach ( $plugins as $plugin ) {
if ( ! isset( $plugin['slug'] ) || ! isset( $plugin['file'] ) ) {
continue;
}
$sanitized_plugins[] = array(
'slug' => sanitize_text_field( $plugin['slug'] ),
'file' => sanitize_text_field( $plugin['file'] ),
);
}

if ( empty( $sanitized_plugins ) ) {
wp_send_json_error( array( 'message' => __( '没有有效的插件数据', 'wpbridge' ) ) );
}

$detector = CommercialDetector::get_instance();
$results = $detector->refresh_batch( $sanitized_plugins );

wp_send_json_success( array( 'results' => $results ) );
}

/**
* AJAX: 导出配置
*/
public function ajax_export_config(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$include_secrets = isset( $_POST['include_secrets'] ) && 'true' === $_POST['include_secrets'];

$config_manager = new ConfigManager();
$config = $config_manager->export( $include_secrets );

wp_send_json_success( array(
'config' => $config,
'filename' => 'wpbridge-config-' . gmdate( 'Y-m-d' ) . '.json',
) );
}

/**
* AJAX: 导入配置
*/
public function ajax_import_config(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

if ( empty( $_POST['config'] ) ) {
wp_send_json_error( array( 'message' => __( '配置数据为空', 'wpbridge' ) ) );
}

$config = json_decode( wp_unslash( $_POST['config'] ), true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
wp_send_json_error( array( 'message' => __( 'JSON 格式无效', 'wpbridge' ) ) );
}

$merge = isset( $_POST['merge'] ) && 'true' === $_POST['merge'];

$config_manager = new ConfigManager();
$result = $config_manager->import( $config, $merge );

if ( $result['success'] ) {
// 清除设置缓存
$this->settings->clear_cache();

wp_send_json_success( array(
'message' => sprintf(
__( '成功导入 %d 项配置', 'wpbridge' ),
count( $result['imported'] )
),
'imported' => $result['imported'],
'skipped' => $result['skipped'],
) );
} else {
wp_send_json_error( array(
'message' => implode( ', ', $result['errors'] ),
'errors' => $result['errors'],
) );
}
}

/**
* AJAX: 锁定版本
*/
public function ajax_lock_version(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$item_key = isset( $_POST['item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['item_key'] ) ) : '';
$lock_type = isset( $_POST['lock_type'] ) ? sanitize_text_field( wp_unslash( $_POST['lock_type'] ) ) : '';
$version = isset( $_POST['version'] ) ? sanitize_text_field( wp_unslash( $_POST['version'] ) ) : '';

if ( empty( $item_key ) || empty( $lock_type ) ) {
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
}

$version_lock = VersionLock::get_instance();

if ( $version_lock->lock( $item_key, $lock_type, $version ) ) {
// 清除更新缓存
delete_site_transient( 'update_plugins' );
delete_site_transient( 'update_themes' );

wp_send_json_success( array(
'message' => __( '版本已锁定', 'wpbridge' ),
'lock' => $version_lock->get( $item_key ),
) );
} else {
wp_send_json_error( array( 'message' => __( '锁定失败', 'wpbridge' ) ) );
}
}

/**
* AJAX: 解锁版本
*/
public function ajax_unlock_version(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$item_key = isset( $_POST['item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['item_key'] ) ) : '';

if ( empty( $item_key ) ) {
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
}

$version_lock = VersionLock::get_instance();

if ( $version_lock->unlock( $item_key ) ) {
// 清除更新缓存
delete_site_transient( 'update_plugins' );
delete_site_transient( 'update_themes' );

wp_send_json_success( array(
'message' => __( '版本已解锁', 'wpbridge' ),
) );
} else {
wp_send_json_error( array( 'message' => __( '解锁失败', 'wpbridge' ) ) );
}
}

/**
* AJAX: 回滚到备份
*/
public function ajax_rollback(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$item_key = isset( $_POST['item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['item_key'] ) ) : '';
$backup_id = isset( $_POST['backup_id'] ) ? sanitize_text_field( wp_unslash( $_POST['backup_id'] ) ) : '';

if ( empty( $item_key ) || empty( $backup_id ) ) {
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
}

$backup_manager = BackupManager::get_instance();
$result = $backup_manager->rollback( $item_key, $backup_id );

if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}

wp_send_json_success( array(
'message' => __( '回滚成功', 'wpbridge' ),
) );
}

/**
* AJAX: 获取备份列表
*/
public function ajax_get_backups(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$item_key = isset( $_POST['item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['item_key'] ) ) : '';

if ( empty( $item_key ) ) {
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
}

$backup_manager = BackupManager::get_instance();
$backups = $backup_manager->get_item_backups( $item_key );

wp_send_json_success( array(
'backups' => $backups,
) );
}

/**
* AJAX: 获取更新日志
*/
public function ajax_get_changelog(): void {
check_ajax_referer( 'wpbridge_nonce', 'nonce' );

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

$slug = isset( $_POST['slug'] ) ? sanitize_text_field( wp_unslash( $_POST['slug'] ) ) : '';
$type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : 'plugin';
$source_type = isset( $_POST['source_type'] ) ? sanitize_text_field( wp_unslash( $_POST['source_type'] ) ) : 'wporg';
$source_url = isset( $_POST['source_url'] ) ? esc_url_raw( wp_unslash( $_POST['source_url'] ) ) : '';

if ( empty( $slug ) ) {
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
}

$changelog_manager = ChangelogManager::get_instance();

if ( 'theme' === $type ) {
$changelog = $changelog_manager->get_theme_changelog( $slug, $source_type, $source_url );
} else {
$changelog = $changelog_manager->get_plugin_changelog( $slug, $source_type, $source_url );
}

if ( ! $changelog['success'] ) {
wp_send_json_error( array( 'message' => $changelog['error'] ?? __( '获取更新日志失败', 'wpbridge' ) ) );
}

wp_send_json_success( $changelog );
}

/**
* 插件激活
*/
public static function activate(): void {
// 创建默认设置
$settings = new Settings();
$settings->init_defaults();

// 清除更新缓存
delete_site_transient( 'update_plugins' );
delete_site_transient( 'update_themes' );

// 记录激活时间
update_option( 'wpbridge_activated', time() );
}

/**
* 插件停用
*/
public static function deactivate(): void {
// 清除缓存
self::clear_all_cache();

// 移除定时任务
wp_clear_scheduled_hook( 'wpbridge_update_sources' );
}

/**
* 清除所有缓存
*/
public static function clear_all_cache(): void {
global $wpdb;

// 清除所有 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() ) {
if ( function_exists( 'wp_cache_flush_group' ) ) {
wp_cache_flush_group( 'wpbridge' );
} else {
wp_cache_delete( 'wpbridge', 'wpbridge' );
}
}
}
}

View file

@ -0,0 +1,297 @@
<?php
/**
* 远程配置管理器
*
* 从远程服务器获取商业插件检测配置,支持:
* - 定时自动更新
* - 本地缓存
* - 降级到内置配置
*
* @package WPBridge
* @since 0.7.5
*/

namespace WPBridge\Core;

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

/**
* RemoteConfig 类
*/
class RemoteConfig {

/**
* 远程配置 URL
*/
const CONFIG_URL = 'https://wpcy.com/api/bridge/commercial-config.json';

/**
* 缓存键名
*/
const CACHE_KEY = 'wpbridge_remote_config';

/**
* 缓存时间(秒)- 默认 12 小时
*/
const CACHE_TTL = 43200;

/**
* 单例实例
*
* @var RemoteConfig|null
*/
private static $instance = 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;
}

/**
* 构造函数
*/
private function __construct() {
$this->load_config();
}

/**
* 加载配置(优先从缓存)
*/
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;
}

// 降级到内置配置
$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',
),
) );

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;
}

$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 ( ! $this->validate_config( $data ) ) {
Logger::warning( '远程配置结构无效' );
return null;
}

Logger::info( '远程配置加载成功', array(
'version' => $data['version'] ?? 'unknown',
) );

return $data;
}

/**
* 验证配置结构
*
* @param array $data 配置数据
* @return bool
*/
private function validate_config( $data ) {
if ( ! is_array( $data ) ) {
return false;
}

// 必须包含版本号
if ( empty( $data['version'] ) ) {
return false;
}

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
*/
public function get_commercial_plugins() {
return $this->config['commercial_plugins'] ?? 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();
}

/**
* 获取商业框架列表
*
* @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_updated_at() {
return $this->config['updated_at'] ?? 'unknown';
}

/**
* 强制刷新配置
*
* @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;
}

// 刷新失败,保持当前配置
return false;
}

/**
* 检查是否使用内置配置
*
* @return bool
*/
public function is_builtin() {
return strpos( $this->get_version(), 'builtin' ) !== false;
}

/**
* 获取完整配置
*
* @return array
*/
public function get_all() {
return $this->config;
}
}

350
includes/Core/Settings.php Normal file
View file

@ -0,0 +1,350 @@
<?php
/**
* 设置管理
*
* @package WPBridge
*/

namespace WPBridge\Core;

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

/**
* 设置管理类
*/
class Settings {

/**
* 选项名称
*/
const OPTION_SOURCES = 'wpbridge_sources';
const OPTION_SETTINGS = 'wpbridge_settings';
const OPTION_AI_SETTINGS = 'wpbridge_ai_settings';

/**
* 默认设置
*
* @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_sources = null;

/**
* 初始化默认设置
*/
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() );
}

// 初始化 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 [
[
'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, [] ),
$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 $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 );
}

/**
* 批量更新设置
*
* @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 );
}

/**
* 获取所有更新源
*
* @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'] );
} );
}

/**
* 获取单个更新源
*
* @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();

// 生成唯一 ID
if ( empty( $source['id'] ) ) {
$source['id'] = 'source_' . wp_generate_uuid4();
}

// 设置默认值
$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;

$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();

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;
}

/**
* 删除更新源
*
* @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;
}

unset( $sources[ $index ] );
$sources = array_values( $sources ); // 重新索引

$this->cached_sources = $sources;
return update_option( self::OPTION_SOURCES, $sources );
}
}

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, [ 'enabled' => $enabled ] );
}

/**
* 获取 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 );
}

/**
* 是否启用调试模式
*
* @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 );
}

/**
* 获取请求超时时间
*
* @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;
}
}

View file

@ -0,0 +1,306 @@
<?php
/**
* WordPress Site Health 集成
*
* @package WPBridge
*/

namespace WPBridge\Core;

use WPBridge\Cache\HealthChecker;

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

/**
* Site Health 集成类
*/
class SiteHealth {

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

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

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 添加健康检查测试
add_filter( 'site_status_tests', [ $this, 'add_tests' ] );

// 添加调试信息
add_filter( 'debug_information', [ $this, 'add_debug_info' ] );
}

/**
* 添加健康检查测试
*
* @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'] = [
'label' => __( 'WPBridge 配置检查', 'wpbridge' ),
'test' => [ $this, 'test_config' ],
];

return $tests;
}

/**
* 测试更新源状态
*
* @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 = [];

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'];
}
}

$total = count( $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 [
'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 [
'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 [
'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 = [];

// 检查调试模式
if ( $this->settings->is_debug() ) {
$issues[] = __( '调试模式已启用,建议在生产环境中关闭', 'wpbridge' );
}

// 检查缓存时间
$cache_ttl = $this->settings->get_cache_ttl();
if ( $cache_ttl < 3600 ) {
$issues[] = __( '缓存时间设置过短,可能导致频繁请求', 'wpbridge' );
}

// 检查备份功能
if ( ! $this->settings->get( 'backup_enabled', true ) ) {
$issues[] = __( '更新前备份已禁用,建议启用以便回滚', 'wpbridge' );
}

// 检查 ZipArchive
if ( ! class_exists( 'ZipArchive' ) ) {
$issues[] = __( 'PHP ZipArchive 扩展未安装,备份功能将不可用', 'wpbridge' );
}

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 [
'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();

$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;
}
}

View file

@ -0,0 +1,362 @@
<?php
/**
* 源注册表管理
*
* 方案 B项目优先架构 - 源注册表层
*
* @package WPBridge
* @since 0.6.0
*/

namespace WPBridge\Core;

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

/**
* 源注册表管理类
*
* 管理所有可用的更新源WP.org、FAIR、自定义等
*/
class SourceRegistry {

/**
* 选项名称
*/
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 SIGNATURE_NONE = 'none';
const SIGNATURE_ED25519 = 'ed25519';

/**
* 认证类型
*/
const AUTH_NONE = 'none';
const AUTH_BASIC = 'basic';
const AUTH_BEARER = 'bearer';
const AUTH_TOKEN = 'token';

/**
* 缓存的源列表
*
* @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, [] );
$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'] ) );
}

/**
* 按类型获取源
*
* @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;
}

/**
* 通过 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();

if ( empty( $source['source_key'] ) ) {
$source['source_key'] = 'src_' . wp_generate_uuid4();
}

if ( $this->get( $source['source_key'] ) ) {
return false;
}

$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;
}

/**
* 更新源
*
* @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;
}

/**
* 删除源
*
* @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;
}

/**
* 启用/禁用源
*
* @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, [
'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;

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 );
}
}

/**
* 获取预置源列表
*
* @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 [
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;
}
}

View file

@ -0,0 +1,301 @@
<?php
/**
* 版本锁定管理器
*
* @package WPBridge
*/

namespace WPBridge\Core;

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

/**
* 版本锁定管理器类
*/
class VersionLock {

/**
* 选项名称
*/
const OPTION_NAME = 'wpbridge_version_locks';

/**
* 锁定类型常量
*/
const LOCK_CURRENT = 'current'; // 锁定到当前版本
const LOCK_SPECIFIC = 'specific'; // 锁定到指定版本
const LOCK_IGNORE = 'ignore'; // 忽略特定版本

/**
* 单例实例
*
* @var VersionLock|null
*/
private static ?VersionLock $instance = 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;
}

/**
* 私有构造函数
*/
private function __construct() {
$this->init_hooks();
}

/**
* 初始化钩子
*/
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, [] );
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 项目键
* @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 ] = [
'type' => $lock_type,
'version' => $version,
'ignore_versions' => $ignore_versions,
'locked_at' => current_time( 'mysql' ),
];

$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();

if ( ! isset( $locks[ $item_key ] ) ) {
return true;
}

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 项目键
* @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;
}

switch ( $lock['type'] ) {
case self::LOCK_CURRENT:
// 锁定到当前版本,阻止所有更新
return true;

case self::LOCK_SPECIFIC:
// 锁定到指定版本,如果当前版本等于锁定版本则阻止更新
return version_compare( $current_version, $lock['version'], '==' );

case self::LOCK_IGNORE:
// 忽略特定版本
return in_array( $new_version, $lock['ignore_versions'], true );

default:
return false;
}
}

/**
* 过滤插件更新
*
* @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 ( ! 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' );

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
) );
}
}

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;
}

$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';

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
) );
}
}

return $transient;
}

/**
* 获取锁定类型标签
*
* @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 [
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
];
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->locks = null;
}
}

View file

@ -0,0 +1,322 @@
<?php
/**
* FAIR 协议处理器
*
* 实现 FAIR (Federated And Independent Repositories) 协议支持
* 包括 DID 解析和 ED25519 签名验证
*
* @package WPBridge
* @since 0.6.0
* @see https://github.com/nicholaswilson/fair-pm
*/

namespace WPBridge\FAIR;

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

/**
* FAIR 协议处理器类
*/
class FairProtocol {

/**
* DID 方法前缀
*/
const DID_METHOD = 'did:fair:';

/**
* 支持的签名方案
*/
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;
}

$parts = explode( ':', substr( $did, strlen( self::DID_METHOD ) ) );

if ( count( $parts ) < 2 ) {
return null;
}

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;

if ( $version ) {
$did .= ':' . $version;
}

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';

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 );
}

/**
* 验证 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 );

if ( false === $signature_bin || false === $public_key_bin ) {
return false;
}

// 验证签名长度
if ( strlen( $signature_bin ) !== SODIUM_CRYPTO_SIGN_BYTES ) {
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 );

} 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 );

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 false;
}

/**
* 验证包签名
*
* @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;
}

$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'] ?? '';

$result['algorithm'] = $algorithm;
$result['signer'] = $signer_did;

// 目前只支持 ED25519
if ( $algorithm !== self::SIGNATURE_ED25519 ) {
$result['error'] = 'unsupported_algorithm';
return $result;
}

// 构建待验证消息
$message = $this->build_signature_message( $package );

// 验证签名
if ( $this->verify_ed25519_signature( $message, $signature, $public_key ) ) {
$result['valid'] = true;
} else {
$result['error'] = 'invalid_signature';
}

return $result;
}

/**
* 构建签名消息
*
* @param array $package 包数据
* @return string
*/
private function build_signature_message( array $package ): string {
// 移除签名字段
$data = $package;
unset( $data['signature'] );

// 按键排序
ksort( $data );

// 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 = [];

// FAIR 响应格式
if ( isset( $response['packages'] ) ) {
foreach ( $response['packages'] as $package ) {
$parsed = $this->parse_package( $package );
if ( $parsed ) {
$packages[] = $parsed;
}
}
}

return $packages;
}

/**
* 解析单个包
*
* @param array $package 包数据
* @return array|null
*/
private function parse_package( array $package ): ?array {
if ( empty( $package['did'] ) && empty( $package['slug'] ) ) {
return null;
}

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' );
}

/**
* 获取支持的签名算法
*
* @return array
*/
public function get_supported_algorithms(): array {
$algorithms = [];

if ( $this->is_sodium_available() ) {
$algorithms[] = self::SIGNATURE_ED25519;
}

return $algorithms;
}
}

View file

@ -0,0 +1,356 @@
<?php
/**
* FAIR 源适配器
*
* 处理 FAIR 协议源的更新检查和下载
*
* @package WPBridge
* @since 0.6.0
*/

namespace WPBridge\FAIR;

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

use WPBridge\Core\SourceRegistry;

/**
* FAIR 源适配器类
*/
class FairSourceAdapter {

/**
* FAIR 协议处理器
*
* @var FairProtocol
*/
private FairProtocol $protocol;

/**
* 源配置
*
* @var array
*/
private array $source;

/**
* 构造函数
*
* @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_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'] ?? '';

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

// 构建 API 请求 URL
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug;

// 发送请求
$response = $this->make_request( $endpoint );

if ( ! $response ) {
return null;
}

// 解析响应
$package = $this->parse_response( $response );

if ( ! $package ) {
return null;
}

// 检查版本
if ( version_compare( $package['version'], $version, '<=' ) ) {
return null; // 没有更新
}

// 验证签名(如果需要)
if ( ! empty( $this->source['signature_required'] ) ) {
$verification = $this->protocol->verify_package_signature( $response );

if ( ! $verification['valid'] ) {
// 签名验证失败
return null;
}

$package['signature_valid'] = true;
}

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_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'] ?? '';

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

$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug . '/info';
$response = $this->make_request( $endpoint );

if ( ! $response ) {
return null;
}

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 );

if ( ! $parsed ) {
return null;
}

$api_url = $this->source['api_url'] ?? '';

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

// FAIR API 支持 DID 查询
$endpoint = trailingslashit( $api_url ) . 'resolve?did=' . urlencode( $did );
$response = $this->make_request( $endpoint );

if ( ! $response ) {
return null;
}

return $this->parse_response( $response );
}

/**
* 发送 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['headers'] ) && is_array( $this->source['headers'] ) ) {
$args['headers'] = array_merge( $args['headers'], $this->source['headers'] );
}

$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;
}

$code = wp_remote_retrieve_response_code( $response );

if ( $code !== 200 ) {
return null;
}

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

if ( json_last_error() !== JSON_ERROR_NONE ) {
return null;
}

return $data;
}

/**
* 获取认证头
*
* @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;
}

// 获取密钥
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );

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

switch ( $auth_type ) {
case SourceRegistry::AUTH_BEARER:
return 'Bearer ' . $secret;

case SourceRegistry::AUTH_TOKEN:
return 'Token ' . $secret;

case SourceRegistry::AUTH_BASIC:
return 'Basic ' . base64_encode( $secret );

default:
return null;
}
}

/**
* 解析响应
*
* @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;
}

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;
}

// 检查包是否有签名
if ( empty( $package['signature'] ) ) {
return false;
}

// 计算文件哈希
$file_hash = hash_file( 'sha256', $file_path );

// 验证哈希签名
$signature_data = $package['signature'];

if ( isset( $signature_data['file_hash'] ) ) {
if ( $signature_data['file_hash'] !== $file_hash ) {
return false;
}
}

return true;
}
}

View file

@ -0,0 +1,181 @@
<?php
/**
* 邮件通知处理器
*
* @package WPBridge
*/

namespace WPBridge\Notification;

use WPBridge\Core\Settings;

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

/**
* 邮件通知处理器类
*/
class EmailHandler implements HandlerInterface {

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

/**
* 支持的通知类型
*
* @var array
*/
private array $supported_types = [ 'update', 'error', 'recovery' ];

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

/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string {
return 'email';
}

/**
* 是否启用
*
* @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', [] );
$enabled_types = $notification_settings['email']['types'] ?? $this->supported_types;

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 = [] ): void {
$notification_settings = $this->settings->get( 'notifications', [] );
$recipients = $notification_settings['email']['recipients'] ?? [];

if ( empty( $recipients ) ) {
// 默认发送给管理员
$recipients = [ get_option( 'admin_email' ) ];
}

// 验证收件人邮箱格式
$valid_recipients = array_filter( $recipients, function ( $email ) {
return is_email( $email );
} );

if ( empty( $valid_recipients ) ) {
throw new \Exception( __( '没有有效的收件人邮箱', 'wpbridge' ) );
}

// 构建 HTML 邮件
$html_message = $this->build_html_message( $subject, $message, $data );

// 使用 WordPress 默认发件人,避免 SPF/DKIM 问题
$headers = [
'Content-Type: text/html; charset=UTF-8',
];

$sent = wp_mail( $valid_recipients, $subject, $html_message, $headers );

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 = '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>' . esc_html( $subject ) . '</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #0073aa; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background: #f9f9f9; }
.footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
.data-table { width: 100%; border-collapse: collapse; margin-top: 15px; }
.data-table th, .data-table td { padding: 8px; border: 1px solid #ddd; text-align: left; }
.data-table th { background: #f0f0f0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>WPBridge</h1>
</div>
<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>';
}

$html .= '
</div>
<div class="footer">
<p>' . sprintf(
/* 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;
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* 通知处理器接口
*
* @package WPBridge
*/

namespace WPBridge\Notification;

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

/**
* 通知处理器接口
*/
interface HandlerInterface {

/**
* 发送通知
*
* @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;

/**
* 是否支持该通知类型
*
* @param string $type 通知类型
* @return bool
*/
public function supports_type( string $type ): bool;

/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string;
}

View file

@ -0,0 +1,231 @@
<?php
/**
* 通知管理器
*
* @package WPBridge
*/

namespace WPBridge\Notification;

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

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

/**
* 通知管理器类
*/
class NotificationManager {

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

/**
* 通知处理器
*
* @var array
*/
private array $handlers = [];

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

/**
* 初始化处理器
*/
private function init_handlers(): void {
$this->handlers = [
'email' => new EmailHandler( $this->settings ),
'webhook' => new WebhookHandler( $this->settings ),
];

// 允许第三方扩展通知处理器
$this->handlers = apply_filters(
'wpbridge_notification_handlers',
$this->handlers,
$this->settings
);
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
add_action( 'wpbridge_update_available', [ $this, 'on_update_available' ], 10, 2 );
add_action( 'wpbridge_source_error', [ $this, 'on_source_error' ], 10, 2 );
add_action( 'wpbridge_source_recovered', [ $this, 'on_source_recovered' ], 10, 1 );
}

/**
* 发送通知
*
* @param string $type 通知类型
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
*/
public function send( string $type, string $subject, string $message, array $data = [] ): void {
// 速率限制检查:防止通知轰炸
$rate_limit_key = 'wpbridge_notification_' . md5( $type . $subject );
if ( get_transient( $rate_limit_key ) ) {
Logger::debug( '通知被速率限制', [ 'type' => $type, 'subject' => $subject ] );
return;
}

// 设置 5 分钟冷却时间
set_transient( $rate_limit_key, true, 5 * MINUTE_IN_SECONDS );

foreach ( $this->handlers as $name => $handler ) {
if ( $handler->is_enabled() && $handler->supports_type( $type ) ) {
try {
$handler->send( $subject, $message, $data );
Logger::debug( '通知发送成功', [
'handler' => $name,
'type' => $type,
] );
} catch ( \Exception $e ) {
Logger::error( '通知发送失败', [
'handler' => $name,
'error' => $e->getMessage(),
] );
}
}
}
}

/**
* 更新可用时触发
*
* @param string $slug 插件/主题 slug
* @param array $update 更新信息
*/
public function on_update_available( string $slug, array $update ): void {
$subject = sprintf(
/* translators: %s: plugin/theme name */
__( '[WPBridge] %s 有新版本可用', 'wpbridge' ),
$update['name'] ?? $slug
);

$message = sprintf(
/* translators: 1: name, 2: current version, 3: new version */
__( '%1$s 有新版本可用。当前版本: %2$s新版本: %3$s', 'wpbridge' ),
$update['name'] ?? $slug,
$update['current_version'] ?? 'unknown',
$update['new_version'] ?? 'unknown'
);

$this->send( 'update', $subject, $message, $update );
}

/**
* 源错误时触发
*
* @param string $source_id 源 ID
* @param string $error 错误信息
*/
public function on_source_error( string $source_id, string $error ): void {
$subject = sprintf(
/* translators: %s: source ID */
__( '[WPBridge] 更新源 %s 出现错误', 'wpbridge' ),
$source_id
);

$message = sprintf(
/* translators: 1: source ID, 2: error message */
__( '更新源 %1$s 出现错误: %2$s', 'wpbridge' ),
$source_id,
$error
);

$this->send( 'error', $subject, $message, [
'source_id' => $source_id,
'error' => $error,
] );
}

/**
* 源恢复时触发
*
* @param string $source_id 源 ID
*/
public function on_source_recovered( string $source_id ): void {
$subject = sprintf(
/* translators: %s: source ID */
__( '[WPBridge] 更新源 %s 已恢复', 'wpbridge' ),
$source_id
);

$message = sprintf(
/* translators: %s: source ID */
__( '更新源 %s 已恢复正常', 'wpbridge' ),
$source_id
);

$this->send( 'recovery', $subject, $message, [
'source_id' => $source_id,
] );
}

/**
* 获取处理器
*
* @param string $name 处理器名称
* @return HandlerInterface|null
*/
public function get_handler( string $name ): ?HandlerInterface {
return $this->handlers[ $name ] ?? null;
}

/**
* 获取所有处理器
*
* @return array
*/
public function get_handlers(): array {
return $this->handlers;
}

/**
* 测试通知
*
* @param string $handler_name 处理器名称
* @return bool
*/
public function test( string $handler_name ): bool {
$handler = $this->get_handler( $handler_name );

if ( null === $handler ) {
return false;
}

try {
$handler->send(
__( '[WPBridge] 测试通知', 'wpbridge' ),
__( '这是一条测试通知,如果您收到此消息,说明通知配置正确。', 'wpbridge' ),
[ 'test' => true ]
);
return true;
} catch ( \Exception $e ) {
Logger::error( '测试通知失败', [
'handler' => $handler_name,
'error' => $e->getMessage(),
] );
return false;
}
}
}

View file

@ -0,0 +1,320 @@
<?php
/**
* Webhook 通知处理器
*
* @package WPBridge
*/

namespace WPBridge\Notification;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\Security\Validator;

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

/**
* Webhook 通知处理器类
*/
class WebhookHandler implements HandlerInterface {

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

/**
* 支持的通知类型
*
* @var array
*/
private array $supported_types = [ 'update', 'error', 'recovery' ];

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

/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string {
return 'webhook';
}

/**
* 是否启用
*
* @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', [] );
$enabled_types = $notification_settings['webhook']['types'] ?? $this->supported_types;

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 = [] ): 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' ) );
}

// 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 );

// 构建请求头
$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;
}

// 发送请求
$response = wp_remote_post( $webhook_url, [
'headers' => $headers,
'body' => $payload,
'timeout' => 10,
] );

if ( is_wp_error( $response ) ) {
throw new \Exception( $response->get_error_message() );
}

$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
)
);
}

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 = [
'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 );

case 'discord':
return $this->format_discord( $subject, $message, $data );

case 'teams':
return $this->format_teams( $subject, $message, $data );

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 = [
'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 );
}

/**
* 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 );
}

/**
* 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 );
}

/**
* 生成签名
*
* @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 = [
'update' => 'good',
'error' => 'danger',
'recovery' => 'good',
'warning' => 'warning',
];

return $colors[ $type ] ?? '#0073aa';
}

/**
* 获取类型对应的颜色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;
}

/**
* 获取类型对应的颜色(十六进制格式)
*
* @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';
}
}

View file

@ -0,0 +1,179 @@
<?php
/**
* 后台更新器
*
* @package WPBridge
*/

namespace WPBridge\Performance;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\UpdateSource\SourceManager;
use WPBridge\Cache\CacheManager;

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

/**
* 后台更新器类
* 使用 WP-Cron 在后台预先更新缓存
*/
class BackgroundUpdater {

/**
* 定时任务钩子名称
*
* @var string
*/
const CRON_HOOK = 'wpbridge_update_sources';

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

/**
* 源管理器
*
* @var SourceManager
*/
private SourceManager $source_manager;

/**
* 并行请求管理器
*
* @var ParallelRequestManager
*/
private ParallelRequestManager $parallel_manager;

/**
* 缓存管理器
*
* @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();

$this->init_hooks();
}

/**
* 初始化钩子
*/
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 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( '开始后台更新' );

$sources = $this->source_manager->get_enabled_sorted();

if ( empty( $sources ) ) {
Logger::debug( '没有启用的更新源' );
return;
}

// 使用并行请求检查所有源
$results = $this->parallel_manager->check_multiple_sources( $sources );

$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++;
}
}

Logger::info( '后台更新完成', [
'success' => $success_count,
'failed' => $fail_count,
] );
}

/**
* 手动触发更新
*
* @return array 更新结果
*/
public function trigger_update(): array {
$this->run_update();

return [
'status' => 'completed',
'time' => current_time( 'mysql' ),
];
}

/**
* 获取下次更新时间
*
* @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 [
'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

@ -0,0 +1,160 @@
<?php
/**
* 条件请求处理器
*
* @package WPBridge
*/

namespace WPBridge\Performance;

use WPBridge\Cache\CacheManager;
use WPBridge\Core\Logger;

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

/**
* 条件请求处理器类
* 使用 ETag 和 Last-Modified 减少数据传输
*/
class ConditionalRequest {

/**
* 缓存管理器
*
* @var CacheManager
*/
private CacheManager $cache;

/**
* 缓存前缀
*
* @var string
*/
const CACHE_PREFIX = 'conditional_';

/**
* 构造函数
*/
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 = [];

if ( ! empty( $cached['etag'] ) ) {
$headers['If-None-Match'] = $cached['etag'];
}

if ( ! empty( $cached['last_modified'] ) ) {
$headers['If-Modified-Since'] = $cached['last_modified'];
}

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 = [];

if ( ! empty( $headers['etag'] ) ) {
$metadata['etag'] = $headers['etag'];
}

if ( ! empty( $headers['last-modified'] ) ) {
$metadata['last_modified'] = $headers['last-modified'];
}

if ( ! empty( $metadata ) ) {
$this->save_metadata( $source_id, $metadata );
}

// 如果有新数据,缓存并返回
if ( null !== $response ) {
$this->save_cached_data( $source_id, $response );
return $response;
}

// 返回缓存数据
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 : [];
}

/**
* 保存元数据
*
* @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
* @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

@ -0,0 +1,163 @@
<?php
/**
* 并行请求管理器
*
* @package WPBridge
*/

namespace WPBridge\Performance;

use WPBridge\UpdateSource\SourceModel;
use WPBridge\Core\Logger;

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

/**
* 并行请求管理器类
* 使用 WordPress Requests API 实现并行请求
*/
class ParallelRequestManager {

/**
* 默认超时时间(秒)
*
* @var int
*/
private int $timeout = 10;

/**
* 构造函数
*
* @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 [];
}

$requests = [];

foreach ( $sources as $source ) {
$requests[ $source->id ] = [
'url' => $source->get_check_url(),
'type' => \WpOrg\Requests\Requests::GET,
'headers' => $source->get_headers(),
];
}

Logger::debug( '开始并行请求', [ 'count' => count( $requests ) ] );

$start = microtime( true );

// 使用 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 );

Logger::debug( '并行请求完成', [
'count' => count( $requests ),
'time_ms' => $elapsed,
] );

return $this->process_responses( $responses );
}

/**
* 处理响应
*
* @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( '请求失败', [
'source' => $source_id,
'error' => $response->getMessage(),
] );
$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 );

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;
}

return $results;
}

/**
* 批量请求 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 = [];

foreach ( $urls as $key => $url ) {
$requests[ $key ] = [
'url' => $url,
'type' => \WpOrg\Requests\Requests::GET,
'headers' => $headers,
];
}

$responses = \WpOrg\Requests\Requests::request_multiple(
$requests,
[
'timeout' => $this->timeout,
'connect_timeout' => 5,
]
);

return $this->process_responses( $responses );
}
}

View file

@ -0,0 +1,118 @@
<?php
/**
* 请求去重器
*
* @package WPBridge
*/

namespace WPBridge\Performance;

use WPBridge\Core\Logger;

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

/**
* 请求去重器类
* 防止短时间内重复请求同一源
*/
class RequestDeduplicator {

/**
* 合并窗口时间(秒)
*
* @var int
*/
const MERGE_WINDOW = 5;

/**
* 锁前缀
*
* @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;

// 检查是否已有锁
if ( get_transient( $lock_key ) ) {
Logger::debug( '请求被去重', [ 'source' => $source_id ] );
return false;
}

// 设置锁
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
* @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();

while ( $this->has_lock( $source_id ) ) {
if ( ( time() - $start ) >= $max_wait ) {
Logger::warning( '等待锁超时', [ 'source' => $source_id ] );
break;
}
usleep( 100000 ); // 100ms
}

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 );
}

try {
$result = $callback();
return $result;
} finally {
$this->release_lock( $source_id );
}
}
}

View file

@ -0,0 +1,212 @@
<?php
/**
* 密钥加密
*
* @package WPBridge
*/

namespace WPBridge\Security;

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

/**
* 密钥加密类
*/
class Encryption {

/**
* 加密方法
*
* @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;
}

// 使用 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;
}

// 如果都没有,生成并存储一个随机密钥
$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 '';
}

$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 );

if ( false === $encrypted ) {
return '';
}

// 将 IV 和加密数据一起存储
return base64_encode( $iv . $encrypted );
}

/**
* 解密数据
*
* @param string $data 加密数据base64 编码)
* @return string 解密后的明文
*/
public static function decrypt( string $data ): string {
if ( empty( $data ) ) {
return '';
}

$data = base64_decode( $data );

if ( false === $data ) {
return '';
}

$key = hash( 'sha256', self::get_key(), true );
$iv_length = openssl_cipher_iv_length( self::METHOD );

if ( strlen( $data ) < $iv_length ) {
return '';
}

$iv = substr( $data, 0, $iv_length );
$encrypted = substr( $data, $iv_length );

$decrypted = openssl_decrypt( $encrypted, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );

if ( false === $decrypted ) {
return '';
}

return $decrypted;
}

/**
* 检查数据是否已加密
*
* @param string $data 数据
* @return bool
*/
public static function is_encrypted( string $data ): bool {
if ( empty( $data ) ) {
return false;
}

// 检查是否是有效的 base64
$decoded = base64_decode( $data, true );

if ( false === $decoded ) {
return false;
}

// 检查长度是否足够包含 IV
$iv_length = openssl_cipher_iv_length( self::METHOD );

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 $default 默认值
* @return string
*/
public static function get_secure( string $key, string $default = '' ): string {
$encrypted = get_option( 'wpbridge_secure_' . $key, '' );

if ( empty( $encrypted ) ) {
return $default;
}

$decrypted = self::decrypt( $encrypted );

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 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 原始数据
* @param string $hash 哈希值
* @return bool
*/
public static function verify_hash( string $data, string $hash ): bool {
return hash_equals( self::hash( $data ), $hash );
}
}

View file

@ -0,0 +1,229 @@
<?php
/**
* 输入校验
*
* @package WPBridge
*/

namespace WPBridge\Security;

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

/**
* 输入校验类
*/
class Validator {

/**
* 校验 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;
}

// 只允许 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;
}

// 禁止本地地址(安全考虑)
if ( self::is_local_address( $host ) ) {
return false;
}

return true;
}

/**
* 检查是否是本地地址
*
* @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;
}

// 私有 IP 范围
$ip = gethostbyname( $host );

if ( $ip === $host ) {
// 无法解析,为安全起见视为本地地址
return true;
}

// 检查私有 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;
}
}

// 检查 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;
}

/**
* 检查 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 );

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;
}

// 支持语义化版本和 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 );
}

/**
* 校验 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-]+$/';

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 = [];

foreach ( $required as $field ) {
if ( ! isset( $data[ $field ] ) ) {
$errors[] = sprintf( __( '缺少必需字段: %s', 'wpbridge' ), $field );
}
}

return $errors;
}

/**
* 校验更新信息 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' );
}

// 下载 URL
$download_url = $data['download_url'] ?? $data['package'] ?? '';
if ( ! empty( $download_url ) && ! self::is_valid_url( $download_url ) ) {
$errors[] = __( '无效的下载 URL', 'wpbridge' );
}

return $errors;
}

/**
* 清理 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 );
}

/**
* 清理 URL
*
* @param string $url URL
* @return string
*/
public static function sanitize_url( string $url ): string {
return esc_url_raw( $url );
}
}

View file

@ -0,0 +1,354 @@
<?php
/**
* 源分组管理器
*
* @package WPBridge
*/

namespace WPBridge\SourceGroup;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\Security\Encryption;
use WPBridge\UpdateSource\SourceManager;

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

/**
* 源分组管理器类
*/
class GroupManager {

/**
* 选项名称
*
* @var string
*/
const OPTION_NAME = 'wpbridge_source_groups';

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

/**
* 源管理器
*
* @var SourceManager
*/
private SourceManager $source_manager;

/**
* 构造函数
*
* @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, [] );
$groups = [];

foreach ( $groups_data as $data ) {
$groups[] = GroupModel::from_array( $data );
}

return $groups;
}

/**
* 获取单个分组
*
* @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;
}
}

return null;
}

/**
* 添加分组
*
* @param GroupModel $group 分组模型
* @return bool
*/
public function add( GroupModel $group ): bool {
if ( ! $group->is_valid() ) {
return false;
}

$groups = $this->get_all();

// 生成 ID
if ( empty( $group->id ) ) {
$group->id = 'group_' . wp_generate_uuid4();
}

$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 );
}

$groups[] = $group;

Logger::info( '添加源分组', [ 'id' => $group->id, 'name' => $group->name ] );

return $this->save_groups( $groups );
}

/**
* 更新分组
*
* @param GroupModel $group 分组模型
* @return bool
*/
public function update( GroupModel $group ): bool {
if ( ! $group->is_valid() ) {
return 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;

// 处理共享认证令牌
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;
}
}

if ( ! $found ) {
return false;
}

Logger::info( '更新源分组', [ 'id' => $group->id ] );

return $this->save_groups( $groups );
}

/**
* 删除分组
*
* @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;
}
}

if ( count( $new_groups ) === count( $groups ) ) {
return false;
}

Logger::info( '删除源分组', [ 'id' => $id ] );

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 );

if ( null === $group ) {
return false;
}

// 先更新分组状态
$group->enabled = $enabled;
if ( ! $this->update( $group ) ) {
return false;
}

// 然后批量更新源状态,记录失败的源
$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( '部分源状态切换失败', [
'group_id' => $id,
'failed' => $failed_sources,
] );
}

return true;
}

/**
* 获取分组内的所有源
*
* @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 [];
}

$sources = [];
foreach ( $group->source_ids as $source_id ) {
$source = $this->source_manager->get( $source_id );
if ( null !== $source ) {
$sources[] = $source;
}
}

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;
}

$group = $this->get( $group_id );

if ( null === $group ) {
return false;
}

$group->add_source( $source_id );

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;
}

$group = $this->get( $group_id );

if ( null === $group ) {
return false;
}

$group->remove_source( $source_id );

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 = [];

foreach ( $groups as $group ) {
if ( $group->has_source( $source_id ) ) {
$source_groups[] = $group;
}
}

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 );
}

/**
* 获取统计信息
*
* @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();
}

return [
'total' => $total,
'enabled' => $enabled,
'disabled' => $total - $enabled,
'total_sources' => $total_sources,
];
}
}

View file

@ -0,0 +1,199 @@
<?php
/**
* 源分组数据模型
*
* @package WPBridge
*/

namespace WPBridge\SourceGroup;

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

/**
* 源分组模型类
*/
class GroupModel {

/**
* 唯一标识
*
* @var string
*/
public string $id = '';

/**
* 分组名称
*
* @var string
*/
public string $name = '';

/**
* 分组描述
*
* @var string
*/
public string $description = '';

/**
* 包含的源 ID 列表
*
* @var array
*/
public array $source_ids = [];

/**
* 共享认证令牌
*
* @var string
*/
public string $shared_auth_token = '';

/**
* 是否启用
*
* @var bool
*/
public bool $enabled = true;

/**
* 创建时间
*
* @var string
*/
public string $created_at = '';

/**
* 更新时间
*
* @var string
*/
public string $updated_at = '';

/**
* 从数组创建实例
*
* @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'] ?? '' );

// 验证 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'] ?? '' );

return $model;
}

/**
* 转换为数组
*
* @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 ) ? '' : '********' );

return $data;
}

/**
* 验证模型
*
* @return array 错误数组
*/
public function validate(): array {
$errors = [];

if ( empty( $this->name ) ) {
$errors['name'] = __( '分组名称不能为空', 'wpbridge' );
}

return $errors;
}

/**
* 是否有效
*
* @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 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 );
}

/**
* 获取源数量
*
* @return int
*/
public function get_source_count(): int {
return count( $this->source_ids );
}
}

View file

@ -0,0 +1,239 @@
<?php
/**
* 抽象处理器基类
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\UpdateSource\SourceModel;
use WPBridge\Core\Logger;
use WPBridge\Security\Encryption;

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

/**
* 抽象处理器基类
*/
abstract class AbstractHandler implements HandlerInterface {

/**
* 源模型
*
* @var SourceModel
*/
protected SourceModel $source;

/**
* 请求超时时间(秒)
*
* @var int
*/
protected int $timeout = 10;

/**
* 构造函数
*
* @param SourceModel $source 源模型
*/
public function __construct( SourceModel $source ) {
$this->source = $source;
}

/**
* 获取能力列表
*
* @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;
}

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
return $this->source->get_headers();
}

/**
* 验证认证信息
*
* @return bool
*/
public function validate_auth(): bool {
// 默认不需要认证
return true;
}

/**
* 测试连通性
*
* @return HealthStatus
*/
public function test_connection(): HealthStatus {
$start = microtime( true );

$response = wp_remote_get( $this->get_check_url(), [
'timeout' => $this->timeout,
'headers' => $this->get_headers(),
] );

$elapsed = (int) ( ( microtime( true ) - $start ) * 1000 );

if ( is_wp_error( $response ) ) {
return HealthStatus::failed( $response->get_error_message() );
}

$code = wp_remote_retrieve_response_code( $response );

if ( $code >= 200 && $code < 300 ) {
return HealthStatus::healthy( $elapsed );
}

if ( $code >= 500 ) {
return HealthStatus::failed( 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 {
$defaults = [
'timeout' => $this->timeout,
'headers' => $this->get_headers(),
];

$args = wp_parse_args( $args, $defaults );
$response = wp_remote_get( $url, $args );

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 );

if ( $code < 200 || $code >= 300 ) {
Logger::warning( '请求返回非 2xx 状态码', [
'url' => $this->redact_url( $url ),
'code' => $code,
] );
return null;
}

$data = json_decode( $body, true );

if ( json_last_error() !== JSON_ERROR_NONE ) {
Logger::error( 'JSON 解析失败', [
'url' => $this->redact_url( $url ),
'error' => json_last_error_msg(),
] );
return null;
}

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, '>' );
}

/**
* 获取解密后的认证令牌
*
* @return string
*/
protected function get_auth_token(): string {
if ( empty( $this->source->auth_token ) ) {
return '';
}

$token = Encryption::decrypt( $this->source->auth_token );

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 $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;
}

parse_str( $parts['query'], $query );
if ( empty( $query ) ) {
return $url;
}

$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'] : '';

return $scheme . $auth . $host . $port . $path . ( $querystr ? '?' . $querystr : '' ) . $fragment;
}
}

View file

@ -0,0 +1,178 @@
<?php
/**
* ArkPress 处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\Core\Logger;

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

/**
* ArkPress 处理器
* 文派自托管方案AspireCloud 分叉版本
*/
class ArkPressHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @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';
}

/**
* 检查更新
*
* @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;
}

// ArkPress API 响应格式
$remote_version = $data['version'] ?? $data['new_version'] ?? '';

if ( empty( $remote_version ) ) {
Logger::warning( 'ArkPress 响应缺少版本信息', [ 'url' => $url, 'slug' => $slug ] );
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'] ?? [];
$info->banners = $data['banners'] ?? [];

if ( isset( $data['sections'] ) ) {
$info->changelog = $data['sections']['changelog'] ?? '';
$info->description = $data['sections']['description'] ?? '';
}

Logger::info( 'ArkPress: 发现更新', [
'slug' => $slug,
'current' => $version,
'new' => $remote_version,
] );

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 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, [
'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 批量检查失败', [
'error' => $response->get_error_message(),
] );
return [];
}

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

if ( ! is_array( $data ) || ! isset( $data['plugins'] ) ) {
return [];
}

$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;
}
}

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;
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* AspireCloud 处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\Core\Logger;

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

/**
* AspireCloud 处理器
*/
class AspireCloudHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @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';
}

/**
* 检查更新
*
* @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;
}

// AspireCloud API 响应格式
$remote_version = $data['version'] ?? $data['new_version'] ?? '';

if ( empty( $remote_version ) ) {
Logger::warning( 'AspireCloud 响应缺少版本信息', [ 'url' => $url, 'slug' => $slug ] );
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;

Logger::info( 'AspireCloud: 发现更新', [
'slug' => $slug,
'current' => $version,
'new' => $remote_version,
] );

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 );
}

/**
* 构建插件 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

@ -0,0 +1,179 @@
<?php
/**
* Bridge Server 处理器
*
* 通过 wpbridge-server Go 服务获取商业插件更新
*
* @package WPBridge
* @since 0.9.8
*/

declare(strict_types=1);

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\UpdateSource\SourceModel;
use WPBridge\Core\Logger;
use WPBridge\Commercial\BridgeClient;

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

/**
* Bridge Server 处理器类
*/
class BridgeServerHandler extends AbstractHandler {

/**
* Bridge 客户端
*
* @var BridgeClient|null
*/
private ?BridgeClient $client = null;

/**
* 构造函数
*
* @param SourceModel $source 源模型
*/
public function __construct( SourceModel $source ) {
parent::__construct( $source );

// 从 source 配置初始化客户端
$server_url = $source->api_url;
$api_key = $this->get_auth_token();

if ( ! empty( $server_url ) ) {
$this->client = new BridgeClient( $server_url, $api_key );
}
}

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'api_key',
'version' => 'json',
'download' => 'signed_url',
'batch' => true,
];
}

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string {
return rtrim( $this->source->api_url, '/' ) . '/api/v1/health';
}

/**
* 检查更新
*
* @param string $slug 插件 slug
* @param string $version 当前版本
* @return UpdateInfo|null
*/
public function check_update( string $slug, string $version ): ?UpdateInfo {
if ( ! $this->client || ! $this->client->is_configured() ) {
Logger::warning( 'Bridge Server 未配置', [ 'slug' => $slug ] );
return null;
}

$info = $this->client->get_plugin_info( $slug );

if ( empty( $info ) || empty( $info['version'] ) ) {
return null;
}

// 比较版本
if ( ! $this->is_newer_version( $version, $info['version'] ) ) {
return null;
}

// 获取签名下载 URL
$download_url = $this->client->get_download_url( $slug );

if ( empty( $download_url ) ) {
Logger::warning( 'Bridge Server 无法获取下载 URL', [ 'slug' => $slug ] );
return null;
}

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'] ?? '',
] );
}

/**
* 获取项目信息
*
* @param string $slug 插件 slug
* @return array|null
*/
public function get_info( string $slug ): ?array {
if ( ! $this->client || ! $this->client->is_configured() ) {
return null;
}

$info = $this->client->get_plugin_info( $slug );

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

// 添加下载 URL
$info['download_url'] = $this->client->get_download_url( $slug );

return $info;
}

/**
* 验证认证信息
*
* @return bool
*/
public function validate_auth(): bool {
if ( ! $this->client ) {
return false;
}

return $this->client->health_check();
}

/**
* 测试连通性
*
* @return HealthStatus
*/
public function test_connection(): HealthStatus {
if ( ! $this->client ) {
return HealthStatus::failed( 'Bridge Server 未配置' );
}

$start = microtime( true );

if ( $this->client->health_check() ) {
$elapsed = (int) ( ( microtime( true ) - $start ) * 1000 );
return HealthStatus::healthy( $elapsed );
}

return HealthStatus::failed( '连接失败' );
}
}

View file

@ -0,0 +1,114 @@
<?php
/**
* FAIR 处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\Core\SourceRegistry;
use WPBridge\FAIR\FairSourceAdapter;

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

/**
* FAIR 协议处理器
*/
class FairHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @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();

$data = $this->source->item_type === 'theme'
? $adapter->check_theme_update( $slug, $version )
: $adapter->check_plugin_update( $slug, $version );

if ( null === $data ) {
return null;
}

$info = UpdateInfo::from_array( $data );
$info->slug = $slug;

return $info;
}

/**
* 获取项目信息
*
* @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 );
}

/**
* 获取 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 = [
'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;
}
}

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

@ -0,0 +1,238 @@
<?php
/**
* GitHub 处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\Core\Logger;

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

/**
* GitHub 处理器类
*/
class GitHubHandler extends AbstractHandler {

/**
* GitHub API 基础 URL
*
* @var string
*/
const API_BASE = 'https://api.github.com';

/**
* 获取能力列表
*
* @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;
}

/**
* 获取检查 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 );

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 );

if ( null === $data ) {
return null;
}

// 解析版本号(去除 v 前缀)
$remote_version = $data['tag_name'] ?? '';
$remote_version = ltrim( $remote_version, 'v' );

if ( empty( $remote_version ) ) {
Logger::warning( 'GitHub: 响应缺少版本信息', [ 'repo' => $repo ] );
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 );

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'] ?? '';

Logger::info( 'GitHub: 发现更新', [
'repo' => $repo,
'current' => $version,
'new' => $remote_version,
] );

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;
}

// 获取仓库信息
$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 );

if ( null === $repo_data || null === $release_data ) {
return null;
}

$version = ltrim( $release_data['tag_name'] ?? '', 'v' );

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 = trim( $url );

// 移除协议
$url = preg_replace( '#^https?://#', '', $url );

// 移除 github.com
$url = preg_replace( '#^github\.com/#', '', $url );

// 移除 .git 后缀
$url = preg_replace( '#\.git$#', '', $url );

// 验证格式
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
return $url;
}

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'] ?? '';

// 匹配 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;
}
}
}

// 使用 zipball_url 作为后备
return $release['zipball_url'] ?? null;
}
}

View file

@ -0,0 +1,250 @@
<?php
/**
* GitLab 处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\Core\Logger;

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

/**
* GitLab 处理器类
*/
class GitLabHandler extends AbstractHandler {

/**
* GitLab API 基础 URL
*
* @var string
*/
const API_BASE = 'https://gitlab.com/api/v4';

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array {
return [
'auth' => 'token',
'version' => 'release',
'download' => 'release',
];
}

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = [];

$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
// GitLab 使用 PRIVATE-TOKEN 头
$headers['PRIVATE-TOKEN'] = $token;
}

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';
}

/**
* 检查更新
*
* @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', [ 'url' => $this->source->api_url ] );
return null;
}

$url = self::API_BASE . '/projects/' . $project_id . '/releases';
$data = $this->request( $url );

if ( null === $data || empty( $data ) ) {
return null;
}

// 获取最新 Release第一个
$latest = $data[0] ?? null;

if ( null === $latest ) {
return null;
}

// 解析版本号
$remote_version = $latest['tag_name'] ?? '';
$remote_version = ltrim( $remote_version, 'v' );

if ( empty( $remote_version ) ) {
Logger::warning( 'GitLab: 响应缺少版本信息', [ 'project' => $project_id ] );
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 );

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'] ?? '';

Logger::info( 'GitLab: 发现更新', [
'project' => $project_id,
'current' => $version,
'new' => $remote_version,
] );

return $info;
}

/**
* 获取项目信息
*
* @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;
}

// 获取项目信息
$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 );

if ( null === $project_data ) {
return null;
}

$latest = $releases_data[0] ?? [];
$version = ltrim( $latest['tag_name'] ?? '', 'v' );

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 );

// 移除协议
$url = preg_replace( '#^https?://#', '', $url );

// 移除 gitlab.com
$url = preg_replace( '#^gitlab\.com/#', '', $url );

// 移除 .git 后缀
$url = preg_replace( '#\.git$#', '', $url );

// URL 编码路径
if ( preg_match( '#^[\w.-]+/[\w.-]+(?:/[\w.-]+)*$#', $url ) ) {
return urlencode( $url );
}

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'] ?? '';

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;
}
}
}

// 使用归档 URL 作为后备
$tag = $release['tag_name'] ?? '';
if ( ! empty( $tag ) ) {
return self::API_BASE . '/projects/' . $project_id . '/repository/archive.zip?sha=' . $tag;
}

return null;
}
}

View file

@ -0,0 +1,252 @@
<?php
/**
* Gitee 处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\Core\Logger;

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

/**
* Gitee 处理器类(国内 Git 平台)
*/
class GiteeHandler extends AbstractHandler {

/**
* Gitee API 基础 URL
*
* @var string
*/
const API_BASE = 'https://gitee.com/api/v5';

/**
* 获取能力列表
*
* @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;
}

/**
* 获取检查 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 );

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';

$token = $this->get_auth_token();
if ( ! empty( $token ) ) {
$url = add_query_arg( 'access_token', $token, $url );
}

$data = $this->request( $url );

if ( null === $data ) {
return null;
}

// 解析版本号
$remote_version = $data['tag_name'] ?? '';
$remote_version = ltrim( $remote_version, 'v' );

if ( empty( $remote_version ) ) {
Logger::warning( 'Gitee: 响应缺少版本信息', [ 'repo' => $repo ] );
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 );

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'] ?? '';

Logger::info( 'Gitee: 发现更新', [
'repo' => $repo,
'current' => $version,
'new' => $remote_version,
] );

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;
}

// 获取仓库信息
$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 );

if ( null === $repo_data ) {
return null;
}

$version = ltrim( $release_data['tag_name'] ?? '', 'v' );

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 = preg_replace( '#^https?://#', '', $url );

// 移除 gitee.com
$url = preg_replace( '#^gitee\.com/#', '', $url );

// 移除 .git 后缀
$url = preg_replace( '#\.git$#', '', $url );

// 验证格式
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
return $url;
}

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'] ?? '';

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;
}
}
}

// 使用归档 URL 作为后备
$tag = $release['tag_name'] ?? '';
if ( ! empty( $tag ) ) {
return 'https://gitee.com/' . $repo . '/repository/archive/' . $tag . '.zip';
}

return null;
}
}

View file

@ -0,0 +1,339 @@
<?php
/**
* 处理器接口
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\UpdateSource\SourceModel;

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

/**
* 更新源处理器接口
*/
interface HandlerInterface {

/**
* 构造函数
*
* @param SourceModel $source 源模型
*/
public function __construct( SourceModel $source );

/**
* 获取能力列表
*
* @return array
*/
public function get_capabilities(): array;

/**
* 获取检查 URL
*
* @return string
*/
public function get_check_url(): string;

/**
* 获取请求头
*
* @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
* @return array|null
*/
public function get_info( string $slug ): ?array;

/**
* 验证认证信息
*
* @return bool
*/
public function validate_auth(): bool;

/**
* 测试连通性
*
* @return HealthStatus
*/
public function test_connection(): HealthStatus;
}

/**
* 更新信息类
*/
class UpdateInfo {

/**
* 插件/主题 slug
*
* @var string
*/
public string $slug = '';

/**
* 新版本号
*
* @var string
*/
public string $version = '';

/**
* 下载 URL
*
* @var string
*/
public string $download_url = '';

/**
* 详情 URL
*
* @var string
*/
public string $details_url = '';

/**
* 最低 WordPress 版本
*
* @var string
*/
public string $requires = '';

/**
* 测试通过的 WordPress 版本
*
* @var string
*/
public string $tested = '';

/**
* 最低 PHP 版本
*
* @var string
*/
public string $requires_php = '';

/**
* 最后更新时间
*
* @var string
*/
public string $last_updated = '';

/**
* 图标
*
* @var array
*/
public array $icons = [];

/**
* 横幅
*
* @var array
*/
public array $banners = [];

/**
* 更新日志
*
* @var string
*/
public string $changelog = '';

/**
* 描述
*
* @var string
*/
public string $description = '';

/**
* 从数组创建
*
* @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'] ?? [];
$info->banners = $data['banners'] ?? [];
$info->changelog = $data['changelog'] ?? $data['sections']['changelog'] ?? '';
$info->description = $data['description'] ?? $data['sections']['description'] ?? '';

return $info;
}

/**
* 转换为 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) [
'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,
];
}
}

/**
* 健康状态类
*/
class HealthStatus {

const STATUS_HEALTHY = 'healthy';
const STATUS_DEGRADED = 'degraded';
const STATUS_FAILED = 'failed';

/**
* 状态
*
* @var string
*/
public string $status = self::STATUS_FAILED;

/**
* 响应时间(毫秒)
*
* @var int
*/
public int $response_time = 0;

/**
* 错误信息
*
* @var string
*/
public string $error = '';

/**
* 检查时间
*
* @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 响应时间
* @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;
}

/**
* 是否健康
*
* @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;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* HealthStatus 兼容加载文件
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

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

require_once __DIR__ . '/HandlerInterface.php';

View file

@ -0,0 +1,141 @@
<?php
/**
* JSON API 处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\Core\Logger;

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

/**
* JSON API 处理器
* 兼容 Plugin Update Checker 格式
*/
class JsonHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @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
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;
}

/**
* 检查更新
*
* @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;
}

// 处理 Plugin Update Checker 格式
$remote_version = $data['version'] ?? '';

if ( empty( $remote_version ) ) {
Logger::warning( 'JSON 响应缺少版本信息', [ 'url' => $url ] );
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;

Logger::info( '发现更新', [
'slug' => $slug,
'current' => $version,
'new' => $remote_version,
] );

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 );
}

/**
* 构建检查 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, '?' ) !== false ) {
return add_query_arg( 'slug', $slug, $url );
}

return $url;
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* PUC 处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

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

/**
* Plugin Update Checker 处理器
* 复用 JSON 处理逻辑
*/
class PUCHandler extends JsonHandler {
}

View file

@ -0,0 +1,15 @@
<?php
/**
* UpdateInfo 兼容加载文件
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

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

require_once __DIR__ . '/HandlerInterface.php';

View file

@ -0,0 +1,300 @@
<?php
/**
* 菲码源库处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\Core\Logger;

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

/**
* 菲码源库处理器Gitea API
*/
class WenPaiGitHandler extends AbstractHandler {

/**
* API 基础 URL
*
* @var string
*/
const API_BASE = 'https://git.wenpai.org/api/v1';

/**
* 获取能力列表
*
* @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;
}

/**
* 获取检查 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 );

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 );

if ( null === $data || empty( $data ) ) {
return null;
}

$latest = $data[0] ?? null;
if ( null === $latest ) {
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;
}

$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'] ?? '';

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;
}

$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;
}

$latest = $release_data[0] ?? [];
$version = ltrim( $latest['tag_name'] ?? '', 'v' );

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 );

$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( '#^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;
}

$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;
}

$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( '#^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;
}

$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;
}

/**
* 查找下载 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;
}
}
}

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'];
}

$tag = $release['tag_name'] ?? '';
if ( ! empty( $tag ) ) {
return $this->get_api_base() . '/repos/' . $repo . '/archive/' . $tag . '.zip';
}

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 = '';

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 self::API_BASE;
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
* ZIP 处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource\Handlers;

use WPBridge\Core\Logger;

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

/**
* ZIP 处理器(直接下载地址)
*
* 需要 metadata 中提供 version/new_version或从 URL 文件名推断版本
*/
class ZipHandler extends AbstractHandler {

/**
* 获取能力列表
*
* @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();

if ( empty( $remote_version ) ) {
Logger::debug( 'ZIP: 无法解析版本号', [ 'url' => $this->source->api_url ] );
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;

return $info;
}

/**
* 获取项目信息
*
* @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;
}

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 ?? [];

if ( ! empty( $metadata['version'] ) ) {
return (string) $metadata['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 '';
}

$filename = basename( $path );
if ( preg_match( '/(\d+\.\d+\.\d+(?:[-+][\w\.]+)?)/', $filename, $matches ) ) {
return $matches[1];
}

return '';
}
}

View file

@ -0,0 +1,360 @@
<?php
/**
* 插件更新器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\UpdateSource\Handlers\UpdateInfo;
use WPBridge\Core\ItemSourceManager;
use WPBridge\Cache\FallbackStrategy;

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

/**
* 插件更新器类
*/
class PluginUpdater {

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

/**
* 源解析器(方案 B
*
* @var SourceResolver
*/
private SourceResolver $source_resolver;

/**
* 降级策略
*
* @var FallbackStrategy
*/
private FallbackStrategy $fallback_strategy;

/**
* 缓存键前缀
*
* @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 );

$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 插件更新检查
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_updates' ], 10, 1 );

// 插件信息
add_filter( 'plugins_api', [ $this, 'plugin_info' ], 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();
}

if ( ! isset( $transient->response ) ) {
$transient->response = [];
}

if ( ! isset( $transient->no_update ) ) {
$transient->no_update = [];
}

// 获取已安装的插件
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 );

$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) [
'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;

if ( $take_over ) {
// 接管更新检查,清除默认响应
unset( $transient->response[ $plugin_file ] );
}

// 尝试从缓存获取(使用 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 ] );
} 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 );

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 ] );

// 缓存结果
set_transient( $cache_key, [
'update' => (array) $update_object,
], $this->settings->get_cache_ttl() );

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, [
'update' => null,
], $this->settings->get_cache_ttl() );
}
}

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() );

$result = $this->fallback_strategy->execute_with_fallback(
$sources,
function( SourceModel $source ) use ( $slug, $version ) {
$handler = $source->get_handler();

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( '检查更新时发生错误', [
'source' => $source->id,
'slug' => $slug,
'error' => $e->getMessage(),
] );
throw $e;
}
},
$cache_key
);

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;
}

$slug = $args->slug ?? '';

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'];

if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
return $result;
}

// 获取第一个匹配的源
$source = reset( $sources );
$handler = $source->get_handler();

if ( null === $handler ) {
return $result;
}

$info = $handler->get_info( $slug );

if ( null === $info ) {
return $result;
}

// 转换为 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';
}

$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;
}

/**
* 过滤下载
*
* @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 );
}

/**
* 清除插件更新缓存
*
* @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' );
}
}

View file

@ -0,0 +1,126 @@
<?php
/**
* 预置更新源配置
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource;

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

/**
* 预置更新源配置类
*/
class PresetSources {

/**
* 文派开源更新源(默认预置)
*/
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 = [
'id' => 'arkpress',
'name' => 'ArkPress',
'type' => SourceType::ARKPRESS,
'api_url' => '', // 用户自定义
'enabled' => false,
'priority' => 20,
'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 = [
'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 [
self::WENPAI_OPEN,
// 以下预置源默认不添加,用户可手动启用
// self::ARKPRESS,
// self::ASPIRECLOUD,
// 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 = [
'wenpai-open' => self::WENPAI_OPEN,
'arkpress' => self::ARKPRESS,
'aspirecloud' => self::ASPIRECLOUD,
'fair' => self::FAIR,
];

return $all[ $id ] ?? null;
}

/**
* 检查是否是预置源 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

@ -0,0 +1,254 @@
<?php
/**
* 更新源管理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource;

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

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

/**
* 更新源管理器类
*/
class SourceManager {

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

/**
* 缓存的源模型
*
* @var array<string, SourceModel>
*/
private array $source_models = [];

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

/**
* 获取所有源
*
* @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;
}

$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_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 ];
}

$source = $this->settings->get_source( $id );
if ( null === $source ) {
return null;
}

$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;
} );
}

/**
* 添加源
*
* @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();
}

$result = $this->settings->add_source( $source->to_array() );

if ( $result ) {
$this->source_models[ $source->id ] = $source;
Logger::info( '添加源成功', [ 'id' => $source->id, 'name' => $source->name ] );
}

return $result;
}

/**
* 更新源
*
* @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() );

if ( $result ) {
$this->source_models[ $source->id ] = $source;
Logger::info( '更新源成功', [ 'id' => $source->id ] );
}

return $result;
}

/**
* 删除源
*
* @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 );

if ( $result ) {
unset( $this->source_models[ $id ] );
Logger::info( '删除源成功', [ 'id' => $id ] );
}

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 );

if ( $result && isset( $this->source_models[ $id ] ) ) {
$this->source_models[ $id ]->enabled = $enabled;
Logger::info( $enabled ? '启用源' : '禁用源', [ 'id' => $id ] );
}

return $result;
}

/**
* 获取源统计
*
* @return array
*/
public function get_stats(): array {
$all = $this->get_all();
$enabled = $this->get_enabled();

$by_type = [];
foreach ( $all as $source ) {
$type = $source->type;
if ( ! isset( $by_type[ $type ] ) ) {
$by_type[ $type ] = 0;
}
$by_type[ $type ]++;
}

return [
'total' => count( $all ),
'enabled' => count( $enabled ),
'by_type' => $by_type,
];
}

/**
* 清除缓存
*/
public function clear_cache(): void {
$this->source_models = [];
$this->settings->clear_cache();
}
}

View file

@ -0,0 +1,271 @@
<?php
/**
* 更新源数据模型
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource;

use WPBridge\Security\Encryption;
use WPBridge\Core\Logger;

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

/**
* 更新源模型类
*/
class SourceModel {

/**
* 唯一标识
*
* @var string
*/
public string $id = '';

/**
* 源名称
*
* @var string
*/
public string $name = '';

/**
* 源类型(见 SourceType 枚举)
*
* @var string
*/
public string $type = '';

/**
* API URL
*
* @var string
*/
public string $api_url = '';

/**
* 插件/主题 slug
*
* @var string
*/
public string $slug = '';

/**
* 项目类型plugin 或 theme
*
* @var string
*/
public string $item_type = 'plugin';

/**
* 认证令牌
*
* @var string
*/
public string $auth_token = '';

/**
* Git 分支(可选)
*
* @var string
*/
public string $branch = '';

/**
* 是否启用
*
* @var bool
*/
public bool $enabled = true;

/**
* 优先级(数字越小优先级越高)
*
* @var int
*/
public int $priority = 50;

/**
* 是否是预置源
*
* @var bool
*/
public bool $is_preset = false;

/**
* 是否是内联源(项目专属,通过快速设置创建)
*
* @var bool
*/
public bool $is_inline = false;

/**
* 额外元数据
*
* @var array
*/
public array $metadata = [];

/**
* 从数组创建实例
*
* @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'] ?? [];

return $model;
}

/**
* 转换为数组
*
* @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 = [];

// 验证类型
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, [ 'http', 'https' ], true ) ) {
$errors['api_url'] = __( 'URL 必须使用 http 或 https 协议', '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' );
}

return $errors;
}

/**
* 是否有效
*
* @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 );

if ( null === $handler_class || ! class_exists( $handler_class ) ) {
return null;
}

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();
}

/**
* 获取请求头
*
* @return array
*/
public function get_headers(): array {
$headers = [];

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 解密失败', [ '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;
}
}

return $headers;
}
}

View file

@ -0,0 +1,237 @@
<?php
/**
* 更新源解析器
*
* 连接项目配置(方案 B与更新处理器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource;

use WPBridge\Core\DefaultsManager;
use WPBridge\Core\ItemSourceManager;
use WPBridge\Core\SourceRegistry;
use WPBridge\Security\Encryption;
use WPBridge\Core\Logger;

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

/**
* 更新源解析器类
*/
class SourceResolver {

/**
* 源注册表
*
* @var SourceRegistry
*/
private SourceRegistry $source_registry;

/**
* 项目配置管理器
*
* @var ItemSourceManager
*/
private ItemSourceManager $item_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();
}

/**
* 解析指定项目的更新源
*
* @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 [
'mode' => $mode,
'sources' => [],
'has_wporg' => false,
];
}

$sources = $this->item_manager->get_effective_sources( $item_key, $this->defaults_manager );

if ( empty( $sources ) ) {
return [
'mode' => $mode,
'sources' => [],
'has_wporg' => false,
];
}

$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;
}
}

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;
}

$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;
}

$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 );
}
}

return $model;
}

/**
* 映射源类型
*
* @param array $source 源配置
* @return string|null
*/
private function map_type( array $source ): ?string {
$type = $source['type'] ?? '';

switch ( $type ) {
case SourceRegistry::TYPE_WPORG:
return null;

case SourceRegistry::TYPE_MIRROR:
return SourceType::ARKPRESS;

case SourceRegistry::TYPE_FAIR:
return SourceType::FAIR;

case SourceRegistry::TYPE_JSON:
return SourceType::JSON;

case SourceRegistry::TYPE_ARKPRESS:
return SourceType::ARKPRESS;

case SourceRegistry::TYPE_GIT:
return $this->resolve_git_type( $source['api_url'] ?? '' );

case SourceRegistry::TYPE_CUSTOM:
return $this->guess_custom_type( $source['api_url'] ?? '' );

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 ) );

if ( strpos( $host, 'github.com' ) !== false ) {
return SourceType::GITHUB;
}

if ( strpos( $host, 'gitlab' ) !== false ) {
return SourceType::GITLAB;
}

if ( strpos( $host, 'gitee.com' ) !== false ) {
return SourceType::GITEE;
}

if ( strpos( $host, 'wenpai' ) !== false || strpos( $host, 'feicode' ) !== false ) {
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;
}

return SourceType::JSON;
}
}

View file

@ -0,0 +1,205 @@
<?php
/**
* 源类型枚举
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource;

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

/**
* 更新源类型枚举
* 所有源类型的统一定义,确保数据模型与处理器一致
*/
class SourceType {

// === 基础类型(用户自定义源)===

/**
* 标准 JSON APIPlugin Update Checker 格式)
*/
const JSON = 'json';

/**
* GitHub Releases
*/
const GITHUB = 'github';

/**
* GitLab Releases
*/
const GITLAB = 'gitlab';

/**
* Gitee Releases国内
*/
const GITEE = 'gitee';

/**
* 菲码源库
*/
const WENPAI_GIT = 'wenpai_git';

/**
* 直接 ZIP URL
*/
const ZIP = 'zip';

// === 自托管服务器类型(预置源使用)===

/**
* ArkPress文派自托管AspireCloud 分叉)
*/
const ARKPRESS = 'arkpress';

/**
* AspireCloud
*/
const ASPIRECLOUD = 'aspirecloud';

/**
* FAIR Package Manager
*/
const FAIR = 'fair';

/**
* Plugin Update Checker 服务器
*/
const PUC = 'puc';

/**
* WPBridge Server商业插件桥接服务
*/
const BRIDGE_SERVER = 'bridge_server';

// === 类型分组 ===

/**
* Git 平台类型
*/
const GIT_TYPES = [
self::GITHUB,
self::GITLAB,
self::GITEE,
self::WENPAI_GIT,
];

/**
* 自托管服务器类型
*/
const SERVER_TYPES = [
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 [
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 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 );
}

/**
* 检查是否是服务器类型
*
* @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 = [
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;
}
}

View file

@ -0,0 +1,317 @@
<?php
/**
* 主题更新器
*
* @package WPBridge
*/

namespace WPBridge\UpdateSource;

use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\UpdateSource\Handlers\UpdateInfo;
use WPBridge\Core\ItemSourceManager;
use WPBridge\Cache\FallbackStrategy;

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

/**
* 主题更新器类
*/
class ThemeUpdater {

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

/**
* 源解析器(方案 B
*
* @var SourceResolver
*/
private SourceResolver $source_resolver;

/**
* 降级策略
*
* @var FallbackStrategy
*/
private FallbackStrategy $fallback_strategy;

/**
* 缓存键前缀
*
* @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 );

$this->init_hooks();
}

/**
* 初始化钩子
*/
private function init_hooks(): void {
// 主题更新检查
add_filter( 'pre_set_site_transient_update_themes', [ $this, 'check_updates' ], 10, 1 );

// 主题信息
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();
}

if ( ! isset( $transient->response ) ) {
$transient->response = [];
}

if ( ! isset( $transient->no_update ) ) {
$transient->no_update = [];
}

// 获取已安装的主题
$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'] );

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 ] = [
'theme' => $slug,
'new_version' => $theme->get( 'Version' ),
];
}
continue;
}

$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;

if ( $take_over ) {
// 接管更新检查,清除默认响应
unset( $transient->response[ $slug ] );
}

$version = $theme->get( 'Version' );

// 尝试从缓存获取(使用 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 ] );
} else {
if ( $take_over ) {
$transient->no_update[ $slug ] = [
'theme' => $slug,
'new_version' => $version,
];
}
}
continue;
}

// 检查更新
$update_info = $this->check_theme_update( $slug, $version, $matching_sources );

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 ] );

// 缓存结果
set_transient( $cache_key, [
'update' => $update_data,
], $this->settings->get_cache_ttl() );

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, [
'update' => null,
], $this->settings->get_cache_ttl() );
}
}

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() );

$result = $this->fallback_strategy->execute_with_fallback(
$sources,
function( SourceModel $source ) use ( $slug, $version ) {
$handler = $source->get_handler();

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( '检查主题更新时发生错误', [
'source' => $source->id,
'slug' => $slug,
'error' => $e->getMessage(),
] );
throw $e;
}
},
$cache_key
);

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;
}

$slug = $args->slug ?? '';

if ( empty( $slug ) ) {
return $result;
}

$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;
}

// 获取第一个匹配的源
$source = reset( $sources );
$handler = $source->get_handler();

if ( null === $handler ) {
return $result;
}

$info = $handler->get_info( $slug );

if ( null === $info ) {
return $result;
}

// 转换为 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 ) . '%'
)
);
}

// 清除 WordPress 更新缓存
delete_site_transient( 'update_themes' );
}
}

355
languages/wpbridge.pot Normal file
View file

@ -0,0 +1,355 @@
# WPBridge Translation Template
# Copyright (C) 2026 WenPai.org
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: WPBridge 0.1.0\n"
"Report-Msgid-Bugs-To: https://wenpai.org/\n"
"POT-Creation-Date: 2026-02-04 00:00:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#: wpbridge.php
msgid "WPBridge"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "更新源"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "设置"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "安全检查失败"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "权限不足"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "更新源已添加"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "更新源已更新"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "保存失败"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "无效的源 ID"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "更新源已删除"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "删除失败,可能是预置源"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "设置已保存"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "状态已更新"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "更新失败"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "源不存在"
msgstr ""

#: includes/Admin/AdminPage.php
msgid "缓存已清除"
msgstr ""

#: templates/admin/source-list.php
msgid "WPBridge 更新源"
msgstr ""

#: templates/admin/source-list.php
msgid "添加更新源"
msgstr ""

#: templates/admin/source-list.php
msgid "总数"
msgstr ""

#: templates/admin/source-list.php
msgid "已启用"
msgstr ""

#: templates/admin/source-list.php
msgid "清除缓存"
msgstr ""

#: templates/admin/source-list.php
msgid "状态"
msgstr ""

#: templates/admin/source-list.php
msgid "名称"
msgstr ""

#: templates/admin/source-list.php
msgid "类型"
msgstr ""

#: templates/admin/source-list.php
msgid "Slug"
msgstr ""

#: templates/admin/source-list.php
msgid "优先级"
msgstr ""

#: templates/admin/source-list.php
msgid "操作"
msgstr ""

#: templates/admin/source-list.php
msgid "暂无更新源"
msgstr ""

#: templates/admin/source-list.php
msgid "预置"
msgstr ""

#: templates/admin/source-list.php
msgid "全部"
msgstr ""

#: templates/admin/source-list.php
msgid "测试"
msgstr ""

#: templates/admin/source-list.php
msgid "编辑"
msgstr ""

#: templates/admin/source-list.php
msgid "删除"
msgstr ""

#: templates/admin/source-editor.php
msgid "编辑更新源"
msgstr ""

#: templates/admin/source-editor.php
msgid "添加更新源"
msgstr ""

#: templates/admin/source-editor.php
msgid "更新源的显示名称"
msgstr ""

#: templates/admin/source-editor.php
msgid "更新源的类型"
msgstr ""

#: templates/admin/source-editor.php
msgid "API URL"
msgstr ""

#: templates/admin/source-editor.php
msgid "更新源的 API 地址。对于 JSON 类型,可以使用 {slug} 占位符。"
msgstr ""

#: templates/admin/source-editor.php
msgid "插件/主题的 slug。留空表示匹配所有。"
msgstr ""

#: templates/admin/source-editor.php
msgid "项目类型"
msgstr ""

#: templates/admin/source-editor.php
msgid "插件"
msgstr ""

#: templates/admin/source-editor.php
msgid "主题"
msgstr ""

#: templates/admin/source-editor.php
msgid "认证令牌"
msgstr ""

#: templates/admin/source-editor.php
msgid "用于私有仓库或需要认证的 API。留空表示无需认证。"
msgstr ""

#: templates/admin/source-editor.php
msgid "数字越小优先级越高0-100"
msgstr ""

#: templates/admin/source-editor.php
msgid "启用"
msgstr ""

#: templates/admin/source-editor.php
msgid "启用此更新源"
msgstr ""

#: templates/admin/source-editor.php
msgid "更新"
msgstr ""

#: templates/admin/source-editor.php
msgid "添加"
msgstr ""

#: templates/admin/source-editor.php
msgid "取消"
msgstr ""

#: templates/admin/settings.php
msgid "WPBridge 设置"
msgstr ""

#: templates/admin/settings.php
msgid "常规设置"
msgstr ""

#: templates/admin/settings.php
msgid "调试模式"
msgstr ""

#: templates/admin/settings.php
msgid "启用调试日志"
msgstr ""

#: templates/admin/settings.php
msgid "启用后会记录详细的调试信息,仅在排查问题时启用。"
msgstr ""

#: templates/admin/settings.php
msgid "缓存时间"
msgstr ""

#: templates/admin/settings.php
msgid "1 小时"
msgstr ""

#: templates/admin/settings.php
msgid "6 小时"
msgstr ""

#: templates/admin/settings.php
msgid "12 小时"
msgstr ""

#: templates/admin/settings.php
msgid "24 小时"
msgstr ""

#: templates/admin/settings.php
msgid "更新检查结果的缓存时间"
msgstr ""

#: templates/admin/settings.php
msgid "请求超时"
msgstr ""

#: templates/admin/settings.php
msgid "秒"
msgstr ""

#: templates/admin/settings.php
msgid "HTTP 请求的超时时间5-60 秒)"
msgstr ""

#: templates/admin/settings.php
msgid "降级策略"
msgstr ""

#: templates/admin/settings.php
msgid "启用过期缓存兜底"
msgstr ""

#: templates/admin/settings.php
msgid "当更新源不可用时,使用过期的缓存数据。"
msgstr ""

#: templates/admin/settings.php
msgid "保存设置"
msgstr ""

#: templates/admin/settings.php
msgid "调试日志"
msgstr ""

#: templates/admin/settings.php
msgid "暂无日志"
msgstr ""

#: templates/admin/settings.php
msgid "时间"
msgstr ""

#: templates/admin/settings.php
msgid "级别"
msgstr ""

#: templates/admin/settings.php
msgid "消息"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "JSON API"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "GitHub"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "GitLab"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "Gitee"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "菲码源库"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "ZIP URL"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "ArkPress"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "AspireCloud"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "FAIR"
msgstr ""

#: includes/UpdateSource/SourceType.php
msgid "PUC Server"
msgstr ""

#: includes/Core/Settings.php
msgid "文派开源更新源"
msgstr ""

144
templates/admin/main.php Normal file
View file

@ -0,0 +1,144 @@
<?php
/**
* WPBridge 主设置页面模板
*
* 参考 WPMind Gutenberg 风格设计
*
* @package WPBridge
* @since 0.5.0
*/

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

use WPBridge\UpdateSource\SourceType;
use WPBridge\UpdateSource\SourceManager;
use WPBridge\Core\Settings;
use WPBridge\Core\Logger;
use WPBridge\Cache\HealthChecker;

// 获取数据
$settings_obj = new Settings();
$source_manager = new SourceManager( $settings_obj );
$sources = $source_manager->get_all();
$stats = $source_manager->get_stats();
$settings = $settings_obj->get_all();
$logs = Logger::get_logs();

// 健康检查
$health_checker = new HealthChecker( $settings_obj );
$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;
}
}
}
?>
<!-- 标题栏 -->
<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-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>

<!-- 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 include WPBRIDGE_PATH . 'templates/admin/tabs/projects.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 include WPBRIDGE_PATH . 'templates/admin/tabs/vendors.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 include WPBRIDGE_PATH . 'templates/admin/tabs/settings.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 include WPBRIDGE_PATH . 'templates/admin/tabs/logs.php'; ?>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,138 @@
<?php
/**
* 默认规则配置部分模板
*
* @package WPBridge
* @since 0.6.0
* @var array $all_sources 所有可用源
* @var SourceRegistry $source_registry 源注册表
* @var DefaultsManager $defaults_manager 默认规则管理器
*/

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

use WPBridge\Core\DefaultsManager;

// 获取当前默认规则
$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>

<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">
<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-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

@ -0,0 +1,284 @@
<?php
/**
* 插件列表部分模板
*
* @package WPBridge
* @since 0.6.0
* @var array $installed_plugins 已安装插件
* @var array $all_sources 所有可用源
* @var ItemSourceManager $item_manager 项目配置管理器
* @var DefaultsManager $defaults_manager 默认规则管理器
*/

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

use WPBridge\Core\ItemSourceManager;
use WPBridge\Core\CommercialDetector;
use WPBridge\Core\VersionLock;

// 获取商业插件检测器
$commercial_detector = CommercialDetector::get_instance();
// 获取版本锁定管理器
$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>

<!-- 插件列表 -->
<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 );

// 获取插件 slug
$plugin_slug = dirname( $plugin_file );
if ( $plugin_slug === '.' ) {
$plugin_slug = basename( $plugin_file, '.php' );
}

// 判断是否激活
$is_active = is_plugin_active( $plugin_file );

// 获取版本锁定信息
$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>

<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-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>

View file

@ -0,0 +1,188 @@
<?php
/**
* 主题列表部分模板
*
* @package WPBridge
* @since 0.6.0
* @var array $installed_themes 已安装主题
* @var array $all_sources 所有可用源
* @var ItemSourceManager $item_manager 项目配置管理器
* @var DefaultsManager $defaults_manager 默认规则管理器
*/

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

use WPBridge\Core\ItemSourceManager;

// 获取当前主题
$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>

<!-- 主题列表 -->
<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;

// 判断是否当前主题
$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>

<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-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>

View file

@ -0,0 +1,145 @@
<?php
/**
* 设置页面模板
*
* @package WPBridge
* @var array $settings 设置
* @var array $logs 日志
*/

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

<div class="wrap wpbridge-wrap">
<h1><?php esc_html_e( 'WPBridge 设置', 'wpbridge' ); ?></h1>

<?php settings_errors( 'wpbridge' ); ?>

<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>

<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-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>

<p class="submit">
<input type="submit" class="button button-primary" value="<?php esc_attr_e( '保存设置', 'wpbridge' ); ?>">
</p>
</form>

<hr>

<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; ?>
</div>

View file

@ -0,0 +1,213 @@
<?php
/**
* 更新源编辑模板
*
* @package WPBridge
* @since 0.5.0
* @var \WPBridge\UpdateSource\SourceModel|null $source 源模型
* @var array $types 类型列表
*/

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

$is_edit = null !== $source;
$title = $is_edit ? __( '编辑更新源', 'wpbridge' ) : __( '添加更新源', '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>
</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' ); ?>

<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-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>
<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-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>
<?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-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-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

@ -0,0 +1,129 @@
<?php
/**
* 更新源列表模板
*
* @package WPBridge
* @var array $sources 源列表
* @var array $stats 统计信息
*/

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
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">

<?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>

<!-- 源列表 -->
<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>
</div>

View file

@ -0,0 +1,190 @@
<?php
/**
* Bridge API Tab 内容
*
* @package WPBridge
* @since 0.5.0
* @var array $settings 设置
*/

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

$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'] ?? [];
?>

<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">

<!-- 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-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-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>

<?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>

<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>
</div>

View file

@ -0,0 +1,359 @@
<?php
/**
* 诊断工具 Tab 内容
*
* @package WPBridge
* @since 0.7.0
* @var array $sources 源列表
* @var array $stats 统计信息
* @var array $settings 设置
* @var array $health_status 健康状态
*/

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

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>

<!-- 诊断结果概览 -->
<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>

<!-- 更新源连通性测试 -->
<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>

<?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-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
// 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
// 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
// 内存限制检查
$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>
</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-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
// 请求超时检查
$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
// 启用的更新源数量检查
$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>

View file

@ -0,0 +1,60 @@
<?php
/**
* 日志 Tab 内容
*
* @package WPBridge
* @since 0.5.0
* @var array $logs 日志列表
*/

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
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>

<?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' ); ?>
</div>

View file

@ -0,0 +1,269 @@
<?php
/**
* 概览 Tab 内容 - 状态仪表板
*
* @package WPBridge
* @since 0.7.0
* @var array $sources 源列表
* @var array $stats 统计信息
* @var array $settings 设置
* @var array $health_status 健康状态
*/

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

// 计算统计数据
$plugins_count = count( get_plugins() );
$themes_count = count( wp_get_themes() );

// 获取项目配置统计
$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++;
}
}
}

// 健康源统计
$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;
}
}
}

// 缓存状态
$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;
$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>

<!-- 核心指标 -->
<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-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 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-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>

View file

@ -0,0 +1,77 @@
<?php
/**
* 项目管理 Tab 内容
*
* 方案 B项目优先架构 - 显示已安装的插件/主题列表
*
* @package WPBridge
* @since 0.6.0
*/

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

use WPBridge\Core\SourceRegistry;
use WPBridge\Core\ItemSourceManager;
use WPBridge\Core\DefaultsManager;

// 获取管理器实例
$source_registry = new SourceRegistry();
$item_manager = new ItemSourceManager( $source_registry );
$defaults_manager = new DefaultsManager();

// 获取所有可用源
$all_sources = $source_registry->get_enabled();

// 获取已安装的插件
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$installed_plugins = get_plugins();

// 获取已安装的主题
$installed_themes = wp_get_themes();

// 当前子 Tab - 白名单验证
$allowed_subtabs = [ 'plugins', 'themes', 'defaults' ];
$current_subtab = 'plugins';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- 仅用于 UI 显示
if ( isset( $_GET['subtab'] ) && in_array( $_GET['subtab'], $allowed_subtabs, true ) ) {
$current_subtab = $_GET['subtab'];
}
?>

<!-- 子 Tab 导航 -->
<div class="wpbridge-subtabs">
<a href="#" class="wpbridge-subtab <?php echo $current_subtab === 'plugins' ? 'wpbridge-subtab-active' : ''; ?>" data-subtab="plugins">
<span class="dashicons dashicons-admin-plugins"></span>
<?php esc_html_e( '插件', 'wpbridge' ); ?>
<span class="wpbridge-subtab-count"><?php echo count( $installed_plugins ); ?></span>
</a>
<a href="#" class="wpbridge-subtab <?php echo $current_subtab === 'themes' ? 'wpbridge-subtab-active' : ''; ?>" data-subtab="themes">
<span class="dashicons dashicons-admin-appearance"></span>
<?php esc_html_e( '主题', 'wpbridge' ); ?>
<span class="wpbridge-subtab-count"><?php echo count( $installed_themes ); ?></span>
</a>
<a href="#" class="wpbridge-subtab <?php echo $current_subtab === 'defaults' ? 'wpbridge-subtab-active' : ''; ?>" data-subtab="defaults">
<span class="dashicons dashicons-admin-settings"></span>
<?php esc_html_e( '默认规则', 'wpbridge' ); ?>
</a>
</div>

<!-- 插件列表 -->
<div id="subtab-plugins" class="wpbridge-subtab-pane <?php echo $current_subtab === 'plugins' ? 'wpbridge-subtab-pane-active' : ''; ?>">
<?php include WPBRIDGE_PATH . 'templates/admin/partials/project-list-plugins.php'; ?>
</div>

<!-- 主题列表 -->
<div id="subtab-themes" class="wpbridge-subtab-pane <?php echo $current_subtab === 'themes' ? 'wpbridge-subtab-pane-active' : ''; ?>">
<?php include WPBRIDGE_PATH . 'templates/admin/partials/project-list-themes.php'; ?>
</div>

<!-- 默认规则 -->
<div id="subtab-defaults" class="wpbridge-subtab-pane <?php echo $current_subtab === 'defaults' ? 'wpbridge-subtab-pane-active' : ''; ?>">
<?php include WPBRIDGE_PATH . 'templates/admin/partials/defaults-config.php'; ?>
</div>

View file

@ -0,0 +1,194 @@
<?php
/**
* 设置 Tab 内容
*
* @package WPBridge
* @since 0.5.0
* @var array $settings 设置
*/

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

<form method="post" class="wpbridge-settings-form">
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
<input type="hidden" name="wpbridge_action" value="save_settings">

<div class="wpbridge-settings-panel">
<!-- 调试模式 -->
<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( '启用后会记录详细的调试信息,仅在排查问题时启用。', 'wpbridge' ); ?></p>
</div>
<label class="wpbridge-toggle">
<input type="checkbox" name="debug_mode" value="1" <?php checked( $settings['debug_mode'] ?? false ); ?>>
<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( '更新检查结果的缓存时间,较长的缓存时间可以减少请求次数。', 'wpbridge' ); ?></p>
</div>
<select name="cache_ttl" class="wpbridge-form-select" style="max-width: 150px;">
<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>
</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( 'HTTP 请求的超时时间5-60 秒),网络较慢时可适当增加。', 'wpbridge' ); ?></p>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<input type="number"
name="request_timeout"
value="<?php echo esc_attr( $settings['request_timeout'] ?? 10 ); ?>"
min="5"
max="60"
class="wpbridge-form-input"
style="max-width: 80px;">
<span style="color: var(--wpbridge-gray-500);"><?php esc_html_e( '秒', 'wpbridge' ); ?></span>
</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( '当更新源不可用时,使用过期的缓存数据作为兜底。', 'wpbridge' ); ?></p>
</div>
<label class="wpbridge-toggle">
<input type="checkbox" name="fallback_enabled" value="1" <?php checked( $settings['fallback_enabled'] ?? true ); ?>>
<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( '在更新插件/主题前自动创建备份,支持一键回滚。', 'wpbridge' ); ?></p>
</div>
<label class="wpbridge-toggle">
<input type="checkbox" name="backup_enabled" value="1" <?php checked( $settings['backup_enabled'] ?? true ); ?>>
<span class="wpbridge-toggle-track"></span>
</label>
</div>
</div>

<!-- Bridge Server 配置 -->
<div class="wpbridge-settings-panel" style="margin-top: 24px; border-top: 1px solid var(--wpbridge-gray-200); padding-top: 24px;">
<h3 style="margin: 0 0 16px; font-size: 14px; font-weight: 600; color: var(--wpbridge-gray-700);">
<span class="dashicons dashicons-cloud" style="margin-right: 4px;"></span>
<?php esc_html_e( 'Bridge Server', 'wpbridge' ); ?>
</h3>

<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( 'wpbridge-server 服务端 URL用于商业插件下载代理。', 'wpbridge' ); ?></p>
</div>
<input type="url"
name="bridge_server_url"
value="<?php echo esc_attr( $settings['bridge_server_url'] ?? '' ); ?>"
placeholder="https://bridge.example.com"
class="wpbridge-form-input"
style="max-width: 300px;">
</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( '用于访问 Bridge Server 管理 API 的密钥。', 'wpbridge' ); ?></p>
</div>
<input type="password"
name="bridge_server_api_key"
value="<?php echo esc_attr( $settings['bridge_server_api_key'] ?? '' ); ?>"
placeholder="<?php esc_attr_e( '输入 API Key', 'wpbridge' ); ?>"
class="wpbridge-form-input"
style="max-width: 300px;">
</div>

<?php if ( ! empty( $settings['bridge_server_url'] ) ) : ?>
<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( '测试与 Bridge Server 的连接。', 'wpbridge' ); ?></p>
</div>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary" id="wpbridge-test-bridge-server">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '测试连接', 'wpbridge' ); ?>
</button>
</div>
<?php endif; ?>
</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>

<!-- 配置导入导出 -->
<div class="wpbridge-settings-panel" style="margin-top: 32px;">
<h2 class="wpbridge-section-title" style="margin-bottom: 16px;">
<span class="dashicons dashicons-database-export"></span>
<?php esc_html_e( '配置导入导出', 'wpbridge' ); ?>
</h2>

<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( '将当前配置导出为 JSON 文件,用于备份或迁移到其他站点。', 'wpbridge' ); ?></p>
</div>
<div style="display: flex; gap: 8px; align-items: center;">
<label style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--wpbridge-gray-600);">
<input type="checkbox" id="wpbridge-export-secrets">
<?php esc_html_e( '包含敏感信息', 'wpbridge' ); ?>
</label>
<button type="button" class="wpbridge-btn wpbridge-btn-secondary" id="wpbridge-export-config">
<span class="dashicons dashicons-download"></span>
<?php esc_html_e( '导出', 'wpbridge' ); ?>
</button>
</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( '从 JSON 文件导入配置。可选择合并或覆盖现有配置。', 'wpbridge' ); ?></p>
</div>
<div style="display: flex; gap: 8px; align-items: center;">
<label style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--wpbridge-gray-600);">
<input type="checkbox" id="wpbridge-import-merge" checked>
<?php esc_html_e( '合并配置', 'wpbridge' ); ?>
</label>
<input type="file" id="wpbridge-import-file" accept=".json" style="display: none;">
<button type="button" class="wpbridge-btn wpbridge-btn-secondary" id="wpbridge-import-config">
<span class="dashicons dashicons-upload"></span>
<?php esc_html_e( '导入', 'wpbridge' ); ?>
</button>
</div>
</div>
</div>

View file

@ -0,0 +1,162 @@
<?php
/**
* 更新源 Tab 内容
*
* @package WPBridge
* @since 0.5.0
* @var array $sources 源列表
* @var array $stats 统计信息
* @var array $health_status 健康状态
*/

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

use WPBridge\UpdateSource\SourceType;
?>

<!-- 统计面板 -->
<div class="wpbridge-stats-panel">
<div class="wpbridge-stat-card">
<div class="wpbridge-stat-card-header">
<span class="dashicons dashicons-database"></span>
<?php esc_html_e( '总更新源', 'wpbridge' ); ?>
</div>
<div class="wpbridge-stat-value"><?php echo esc_html( $stats['total'] ); ?></div>
</div>
<div class="wpbridge-stat-card">
<div class="wpbridge-stat-card-header">
<span class="dashicons dashicons-yes-alt"></span>
<?php esc_html_e( '已启用', 'wpbridge' ); ?>
</div>
<div class="wpbridge-stat-value success"><?php echo esc_html( $stats['enabled'] ); ?></div>
</div>
<div class="wpbridge-stat-card">
<div class="wpbridge-stat-card-header">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '缓存状态', 'wpbridge' ); ?>
</div>
<div class="wpbridge-stat-value">
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-clear-cache">
<span class="dashicons dashicons-trash"></span>
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
</button>
</div>
</div>
</div>

<!-- 更新源列表 -->
<div class="wpbridge-sources-header">
<h2 class="wpbridge-sources-title"><?php esc_html_e( '更新源列表', 'wpbridge' ); ?></h2>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="wpbridge-btn wpbridge-btn-primary">
<span class="dashicons dashicons-plus-alt2"></span>
<?php esc_html_e( '添加更新源', 'wpbridge' ); ?>
</a>
</div>

<?php if ( empty( $sources ) ) : ?>
<div class="wpbridge-empty">
<span class="dashicons dashicons-cloud"></span>
<h3 class="wpbridge-empty-title"><?php esc_html_e( '暂无更新源', 'wpbridge' ); ?></h3>
<p class="wpbridge-empty-desc"><?php esc_html_e( '添加自定义更新源来管理插件和主题的更新。', 'wpbridge' ); ?></p>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="wpbridge-btn wpbridge-btn-primary">
<?php esc_html_e( '添加第一个更新源', 'wpbridge' ); ?>
</a>
</div>
<?php else : ?>
<div class="wpbridge-sources-grid">
<?php foreach ( $sources as $source ) : ?>
<div class="wpbridge-source-card" data-source-id="<?php echo esc_attr( $source->id ); ?>">
<div class="wpbridge-source-card-header">
<div class="wpbridge-source-card-title">
<h3 class="wpbridge-source-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; ?>
</h3>
<span class="wpbridge-source-url"><?php echo esc_html( $source->api_url ); ?></span>
</div>
<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-track"></span>
</label>
</div>

<div class="wpbridge-source-card-body">
<div class="wpbridge-source-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-meta-item">
<span class="dashicons dashicons-admin-plugins"></span>
<?php echo esc_html( $source->item_type === 'plugin' ? __( '插件', 'wpbridge' ) : __( '主题', 'wpbridge' ) ); ?>
</span>
<?php if ( ! empty( $source->slug ) ) : ?>
<span class="wpbridge-source-meta-item">
<span class="dashicons dashicons-tag"></span>
<?php echo esc_html( $source->slug ); ?>
</span>
<?php endif; ?>
<span class="wpbridge-source-meta-item">
<span class="dashicons dashicons-sort"></span>
<?php
/* translators: %d: priority number */
printf( esc_html__( '优先级 %d', 'wpbridge' ), $source->priority );
?>
</span>
</div>

<?php if ( isset( $health_status[ $source->id ] ) && is_array( $health_status[ $source->id ] ) ) : ?>
<span class="wpbridge-badge wpbridge-badge-status <?php echo esc_attr( $health_status[ $source->id ]['status'] ?? '' ); ?>">
<?php
$status_labels = [
'healthy' => __( '正常', 'wpbridge' ),
'degraded' => __( '降级', 'wpbridge' ),
'failed' => __( '失败', 'wpbridge' ),
];
$current_status = $health_status[ $source->id ]['status'] ?? 'unknown';
echo esc_html( $status_labels[ $current_status ] ?? $current_status );
?>
</span>
<?php endif; ?>
</div>

<div class="wpbridge-source-card-footer">
<div class="wpbridge-source-actions">
<button type="button"
class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-source"
data-source-id="<?php echo esc_attr( $source->id ); ?>">
<span class="dashicons dashicons-admin-site-alt3"></span>
<?php esc_html_e( '测试', 'wpbridge' ); ?>
</button>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>"
class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm">
<span class="dashicons dashicons-edit"></span>
<?php esc_html_e( '编辑', 'wpbridge' ); ?>
</a>
<?php if ( ! $source->is_preset ) : ?>
<button type="button"
class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-delete-source"
data-source-id="<?php echo esc_attr( $source->id ); ?>">
<span class="dashicons dashicons-trash"></span>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>

<!-- 删除确认表单 -->
<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>

View file

@ -0,0 +1,345 @@
<?php
/**
* 供应商管理 Tab
*
* @package WPBridge
* @since 0.9.8
*/

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

use WPBridge\Admin\VendorAdmin;
use WPBridge\Core\Settings;

$settings_obj = new Settings();
$vendor_admin = new VendorAdmin( $settings_obj );
$vendor_data = $vendor_admin->get_vendor_data();

$vendors = $vendor_data['vendors'];
$custom = $vendor_data['custom'];
$all_plugins = $vendor_data['all_plugins'];
$stats = $vendor_data['stats'];
$vendor_types = $vendor_data['vendor_types'];
?>

<div class="wpbridge-vendors-section">
<!-- 统计卡片 -->
<div class="wpbridge-stats-row">
<div class="wpbridge-stat-card">
<span class="wpbridge-stat-number"><?php echo esc_html( count( $vendors ) ); ?></span>
<span class="wpbridge-stat-label"><?php esc_html_e( '供应商', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-stat-card">
<span class="wpbridge-stat-number"><?php echo esc_html( count( $all_plugins ) ); ?></span>
<span class="wpbridge-stat-label"><?php esc_html_e( '可用插件', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-stat-card">
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['bridged_count'] ?? 0 ); ?></span>
<span class="wpbridge-stat-label"><?php esc_html_e( '已桥接', 'wpbridge' ); ?></span>
</div>
<div class="wpbridge-stat-card">
<span class="wpbridge-stat-number"><?php echo esc_html( count( $custom ) ); ?></span>
<span class="wpbridge-stat-label"><?php esc_html_e( '自定义', 'wpbridge' ); ?></span>
</div>
</div>

<!-- 供应商列表 -->
<div class="wpbridge-section">
<div class="wpbridge-section-header">
<h3><?php esc_html_e( '供应商渠道', 'wpbridge' ); ?></h3>
<button type="button" class="button button-primary" id="wpbridge-add-vendor-btn">
<span class="dashicons dashicons-plus-alt2"></span>
<?php esc_html_e( '添加供应商', 'wpbridge' ); ?>
</button>
</div>

<p class="wpbridge-section-desc">
<?php esc_html_e( '接入第三方 GPL 插件分发商,获取更多商业插件的更新支持。', 'wpbridge' ); ?>
</p>

<?php if ( empty( $vendors ) ) : ?>
<div class="wpbridge-empty-state">
<span class="dashicons dashicons-store"></span>
<p><?php esc_html_e( '暂无供应商,点击上方按钮添加', 'wpbridge' ); ?></p>
</div>
<?php else : ?>
<table class="wp-list-table widefat fixed striped wpbridge-vendors-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-plugins"><?php esc_html_e( '插件数', 'wpbridge' ); ?></th>
<th class="column-actions"><?php esc_html_e( '操作', 'wpbridge' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $vendors as $vendor_id => $vendor ) : ?>
<tr data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>">
<td class="column-status">
<label class="wpbridge-toggle">
<input type="checkbox"
class="wpbridge-vendor-toggle"
data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>"
<?php checked( ! empty( $vendor['enabled'] ) ); ?>>
<span class="wpbridge-toggle-slider"></span>
</label>
</td>
<td class="column-name">
<strong><?php echo esc_html( $vendor['name'] ); ?></strong>
<div class="row-actions">
<span class="wpbridge-vendor-url">
<?php echo esc_html( $vendor['api_url'] ?? '' ); ?>
</span>
</div>
</td>
<td class="column-type">
<?php echo esc_html( $vendor_types[ $vendor['type'] ] ?? $vendor['type'] ); ?>
</td>
<td class="column-plugins">
<span class="wpbridge-plugin-count">-</span>
</td>
<td class="column-actions">
<button type="button" class="button wpbridge-test-vendor"
data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>">
<?php esc_html_e( '测试', 'wpbridge' ); ?>
</button>
<button type="button" class="button wpbridge-sync-vendor"
data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>">
<?php esc_html_e( '同步', 'wpbridge' ); ?>
</button>
<button type="button" class="button button-link-delete wpbridge-remove-vendor"
data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>">
<?php esc_html_e( '删除', 'wpbridge' ); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>

<!-- 自定义插件 -->
<div class="wpbridge-section">
<div class="wpbridge-section-header">
<h3><?php esc_html_e( '自定义插件', 'wpbridge' ); ?></h3>
<button type="button" class="button" id="wpbridge-add-custom-btn">
<span class="dashicons dashicons-plus-alt2"></span>
<?php esc_html_e( '添加插件', 'wpbridge' ); ?>
</button>
</div>

<p class="wpbridge-section-desc">
<?php esc_html_e( '手动添加不在官方列表或供应商渠道中的插件。', 'wpbridge' ); ?>
</p>

<?php if ( empty( $custom ) ) : ?>
<div class="wpbridge-empty-state">
<span class="dashicons dashicons-admin-plugins"></span>
<p><?php esc_html_e( '暂无自定义插件', 'wpbridge' ); ?></p>
</div>
<?php else : ?>
<table class="wp-list-table widefat fixed striped wpbridge-custom-table">
<thead>
<tr>
<th class="column-slug"><?php esc_html_e( 'Slug', 'wpbridge' ); ?></th>
<th class="column-name"><?php esc_html_e( '名称', 'wpbridge' ); ?></th>
<th class="column-url"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></th>
<th class="column-actions"><?php esc_html_e( '操作', 'wpbridge' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $custom as $slug => $info ) : ?>
<tr data-plugin-slug="<?php echo esc_attr( $slug ); ?>">
<td class="column-slug">
<code><?php echo esc_html( $slug ); ?></code>
</td>
<td class="column-name">
<?php echo esc_html( $info['name'] ?? $slug ); ?>
</td>
<td class="column-url">
<?php echo esc_html( $info['update_url'] ?? '-' ); ?>
</td>
<td class="column-actions">
<button type="button" class="button button-link-delete wpbridge-remove-custom"
data-plugin-slug="<?php echo esc_attr( $slug ); ?>">
<?php esc_html_e( '删除', 'wpbridge' ); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>

<!-- 可用插件列表 -->
<div class="wpbridge-section">
<div class="wpbridge-section-header">
<h3><?php esc_html_e( '可用插件', 'wpbridge' ); ?></h3>
<button type="button" class="button" id="wpbridge-sync-all-btn">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( '同步全部', 'wpbridge' ); ?>
</button>
</div>

<p class="wpbridge-section-desc">
<?php esc_html_e( '来自官方列表、供应商渠道和自定义的所有可桥接插件。', 'wpbridge' ); ?>
</p>

<?php if ( empty( $all_plugins ) ) : ?>
<div class="wpbridge-empty-state">
<span class="dashicons dashicons-admin-plugins"></span>
<p><?php esc_html_e( '暂无可用插件,请添加供应商或同步官方列表', 'wpbridge' ); ?></p>
</div>
<?php else : ?>
<div class="wpbridge-plugins-grid">
<?php foreach ( $all_plugins as $slug => $info ) : ?>
<div class="wpbridge-plugin-card" data-slug="<?php echo esc_attr( $slug ); ?>">
<div class="wpbridge-plugin-header">
<span class="wpbridge-plugin-name">
<?php echo esc_html( $info['name'] ?? $slug ); ?>
</span>
<span class="wpbridge-plugin-source wpbridge-source-<?php echo esc_attr( $info['source'] ?? 'unknown' ); ?>">
<?php
$source_labels = [
'official' => __( '官方', 'wpbridge' ),
'vendor' => __( '供应商', 'wpbridge' ),
'custom' => __( '自定义', 'wpbridge' ),
];
echo esc_html( $source_labels[ $info['source'] ] ?? $info['source'] );
?>
</span>
</div>
<div class="wpbridge-plugin-meta">
<code><?php echo esc_html( $slug ); ?></code>
<?php if ( ! empty( $info['vendor'] ) ) : ?>
<span class="wpbridge-plugin-vendor">
<?php echo esc_html( $info['vendor'] ); ?>
</span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>

<!-- 添加供应商弹窗 -->
<div id="wpbridge-vendor-modal" class="wpbridge-modal" style="display:none;">
<div class="wpbridge-modal-content">
<div class="wpbridge-modal-header">
<h2><?php esc_html_e( '添加供应商', 'wpbridge' ); ?></h2>
<button type="button" class="wpbridge-modal-close">&times;</button>
</div>
<div class="wpbridge-modal-body">
<form id="wpbridge-vendor-form">
<table class="form-table">
<tr>
<th><label for="vendor_id"><?php esc_html_e( '供应商 ID', 'wpbridge' ); ?></label></th>
<td>
<input type="text" id="vendor_id" name="vendor_id" class="regular-text" required
pattern="[a-z0-9_-]+" placeholder="my-vendor">
<p class="description"><?php esc_html_e( '唯一标识符,只能包含小写字母、数字、下划线和连字符', 'wpbridge' ); ?></p>
</td>
</tr>
<tr>
<th><label for="vendor_name"><?php esc_html_e( '名称', 'wpbridge' ); ?></label></th>
<td>
<input type="text" id="vendor_name" name="name" class="regular-text" required
placeholder="<?php esc_attr_e( '我的供应商', 'wpbridge' ); ?>">
</td>
</tr>
<tr>
<th><label for="vendor_type"><?php esc_html_e( '类型', 'wpbridge' ); ?></label></th>
<td>
<select id="vendor_type" name="type">
<?php foreach ( $vendor_types as $type => $label ) : ?>
<option value="<?php echo esc_attr( $type ); ?>">
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th><label for="vendor_api_url"><?php esc_html_e( 'API 地址', 'wpbridge' ); ?></label></th>
<td>
<input type="url" id="vendor_api_url" name="api_url" class="regular-text" required
placeholder="https://example.com">
<p class="description"><?php esc_html_e( 'WooCommerce 商店的根地址', 'wpbridge' ); ?></p>
</td>
</tr>
<tr>
<th><label for="vendor_consumer_key"><?php esc_html_e( 'Consumer Key', 'wpbridge' ); ?></label></th>
<td>
<input type="text" id="vendor_consumer_key" name="consumer_key" class="regular-text"
placeholder="ck_xxxxxxxx">
<p class="description"><?php esc_html_e( 'WooCommerce REST API Consumer Key可选', 'wpbridge' ); ?></p>
</td>
</tr>
<tr>
<th><label for="vendor_consumer_secret"><?php esc_html_e( 'Consumer Secret', 'wpbridge' ); ?></label></th>
<td>
<input type="password" id="vendor_consumer_secret" name="consumer_secret" class="regular-text"
placeholder="cs_xxxxxxxx">
<p class="description"><?php esc_html_e( 'WooCommerce REST API Consumer Secret可选', 'wpbridge' ); ?></p>
</td>
</tr>
</table>
</form>
</div>
<div class="wpbridge-modal-footer">
<button type="button" class="button wpbridge-modal-cancel"><?php esc_html_e( '取消', 'wpbridge' ); ?></button>
<button type="button" class="button button-primary" id="wpbridge-save-vendor"><?php esc_html_e( '保存', 'wpbridge' ); ?></button>
</div>
</div>
</div>

<!-- 添加自定义插件弹窗 -->
<div id="wpbridge-custom-modal" class="wpbridge-modal" style="display:none;">
<div class="wpbridge-modal-content">
<div class="wpbridge-modal-header">
<h2><?php esc_html_e( '添加自定义插件', 'wpbridge' ); ?></h2>
<button type="button" class="wpbridge-modal-close">&times;</button>
</div>
<div class="wpbridge-modal-body">
<form id="wpbridge-custom-form">
<table class="form-table">
<tr>
<th><label for="custom_slug"><?php esc_html_e( '插件 Slug', 'wpbridge' ); ?></label></th>
<td>
<input type="text" id="custom_slug" name="plugin_slug" class="regular-text" required
pattern="[a-z0-9_-]+" placeholder="my-plugin">
<p class="description"><?php esc_html_e( '插件目录名称', 'wpbridge' ); ?></p>
</td>
</tr>
<tr>
<th><label for="custom_name"><?php esc_html_e( '名称', 'wpbridge' ); ?></label></th>
<td>
<input type="text" id="custom_name" name="name" class="regular-text"
placeholder="<?php esc_attr_e( '我的插件', 'wpbridge' ); ?>">
</td>
</tr>
<tr>
<th><label for="custom_url"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label></th>
<td>
<input type="url" id="custom_url" name="update_url" class="regular-text"
placeholder="https://example.com/update.json">
<p class="description"><?php esc_html_e( '插件更新检查地址(可选)', 'wpbridge' ); ?></p>
</td>
</tr>
</table>
</form>
</div>
<div class="wpbridge-modal-footer">
<button type="button" class="button wpbridge-modal-cancel"><?php esc_html_e( '取消', 'wpbridge' ); ?></button>
<button type="button" class="button button-primary" id="wpbridge-save-custom"><?php esc_html_e( '保存', 'wpbridge' ); ?></button>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show more