[开发]SoSite 插件仓库开发计划

👑Lv.11 元老 🌞 骨干会员
2026-10-25 00:52:11

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:8月13日备份/SoSite-插件仓库开发计划.md。

SoSite 插件仓库开发计划

文档日期:2026-08-09

目标程序:SoSite V5.0.0(自研 MVC,PHP 8.2 / MySQL 5.7-8.0,零 Composer 依赖)

前置依据:SoSite-三功能落地方案-在线升级-插件仓库-定时任务.md(§2.2 插件仓库机制拆解、§7 P1 插件仓库、§8 风险与安全措施)

参考对象:bbs1.org 插件市场(远程 JSON 源 + sha256 强校验 + 安装后默认停用 + 注册表同步)

说明:本文为插件仓库功能的完整开发计划(设计 → 实现 → 测试 → 发布全流程),落地前需按实际代码结构复核函数签名与路由注册方式。


一、背景与目标

1.1 现状

项现状
插件安装仅本地手动放置 plugins/{dir}/ + 后台启停;无市场、无远程安装
插件来源手工复制/上传 zip 解压,无来源校验、无版本管理
插件更新手动覆盖文件,无差异检测、无备份回滚
插件卸载后台卸载(调 Plugin.php::uninstall() + 删注册),但插件文件目录保留

1.2 目标

  • 后台新增插件市场页:浏览 / 搜索 / 一键安装 / 更新 / 卸载 / 重新安装
  • 支持远程市场源(自建 HTTP 源或 Gitee Release),feed JSON 协议下发插件元数据与安装包
  • 全程安全路径:仅管理员 + CSRF + sha256 强校验 + 解压路径白名单 + 安装后默认停用 + 备份回滚 + AuditLog 日志
  • 不改变现有 14 个插件的目录结构、plugin.json 格式、生命周期机制(向后兼容)
  • 为后续"系统插件"(system 标志防停用失去管理入口)预留字段

二、现状分析(必须遵守的既有约束)

2.1 插件系统现状(2026-08-09 核对)

项现状
插件总数14 个(announcements / content_review / daily_checkin / dice / edit_info / friend_links / icon_generator / invite / post_favorite / qa / quiz_duel / red_packet / seo / user_profile),11 启用 / 3 停用
目录结构plugins/{dir}/:plugin.json + 可选 Plugin.php + hook/.php + views/.php + assets/*
清单文件plugin.json:name / version / author / description / activated / permissions / hooks
生命周期Plugin.php::activate(): bool(false 回滚)/ deactivate() / uninstall()
缓存plugins/plugins_cache.php(var_export 生成,json_decode 读取;原子写入 tmp+rename + opcache 失效)
命名空间Plugin\{Dir}(Autoloader 映射),类文件 {Xxx}Controller.php
视图A+ 规则:view('plugins/xxx/yyy') → plugins/xxx/views/yyy.php(resolveTemplate 插件目录优先,回落 app/Views)
资源直接 /plugins/{dir}/assets/...(勿用 asset(),避免 assets/plugins/ 双份漂移)
后台管理app/Controllers/Admin/PluginController.php(列表/启用/禁用/编辑配置/卸载)+ app/Views/admin/plugins.php
路由hook route_register(前台)/ admin_route_register(后台),$router->get/post

2.2 实现前必须复核的接口

  1. app/Helpers/Plugin.php 的 loadPlugins() / saveCache() / refresh()(缓存重建挂载点)
  2. Settings::get/update(存放市场地址、更新源地址、最后检查时间)
  3. AuditLog::log($action, $targetType, $targetId, $detail)(安装/卸载审计)
  4. Csrf::token()/verifyOrDie()(后台表单)
  5. Router 后台路由注册(admin_route_register hook,URL 不带 /admin 前缀)
  6. Admin\BaseController 鉴权(isAdmin + 二次密码验证)
  7. PHP 是否启用 zip 扩展(phpstudy 默认开启;未开启需 extension=php_zip.dll)
  8. protected/ 目录可写性(备份目录)

三、总体架构

┌─────────────────────────┐         ┌────────────────────────────────┐
│  市场服务端(发布方)      │  HTTP   │  SoSite 站点(客户端)            │
│  feed.json(元数据)      │ ──────► │  后台"插件市场"页                │
│  files/{plugin}-{ver}.zip│  GET    │  PluginMarket.php(安装器)      │
│  发布脚本(打包+sha256)   │         │  校验→下载→解压→备份→落盘→停用    │
└─────────────────────────┘         └────────────────────────────────┘

3.1 市场源(发布方)

  • 静态托管即可:feed.json + files/{plugin_id}-{version}.zip
  • 可放自建服务器 / Gitee Release / 对象存储(需 HTTPS + 证书)
  • 不含插件源码明文:zip 内是完整插件目录(代码 + plugin.json + hook + views + assets)

3.2 站点(客户端)

  • 新增 app/Helpers/PluginMarket.php(核心安装器)
  • 新增 app/Controllers/Admin/PluginMarketController.php(后台路由)
  • 新增 app/Views/admin/plugin_market.php(市场页)
  • 新增 protected/backups/plugins/(安装/升级备份目录)
  • 新增 protected/plugins-market-state.json(市场缓存 + 已安装元数据,运行时生成)

四、市场协议设计(feed.json 规范)

4.1 feed.json 结构

{
    "schema": 1,
    "generated_at": "2026-08-09T00:00:00+08:00",
    "source": "https://market.example.com/",
    "plugins": [
        {
            "id": "daily_checkin",
            "name": "每日签到",
            "version": "1.0.0",
            "author": "SoSite",
            "description": "用户每日签到获取积分",
            "homepage": "https://example.com/plugin/daily_checkin",
            "license": "MIT",
            "tags": ["签到", "积分"],
            "category": "utility",
            "system": false,
            "requires": {
                "sosite": ">=5.0.0",
                "php": ">=8.0",
                "plugins": []
            },
            "install": {
                "archive": "files/daily_checkin-1.0.0.zip",
                "sha256": "a1b2c3...64位hex",
                "size": 20480,
                "files": 12
            },
            "updated_at": "2026-08-09T00:00:00+08:00",
            "changelog": "v1.0.0 首次发布"
        }
    ]
}

4.2 字段说明

字段必填说明
schema✅协议版本号,客户端不兼容则拒绝
generated_at✅生成时间,客户端缓存判断用
source✅源地址,客户端校验同源(防中间人篡改 feed 指向)
plugins[].id✅插件目录名(小写下划线,与 plugins/{id}/ 一致)
plugins[].version✅语义化版本 X.Y.Z
plugins[].system❌系统插件标志(预留:true 时禁止停用/卸载)
plugins[].requires.sosite❌SoSite 版本约束(>=5.0.0,version_compare 校验)
plugins[].requires.php❌PHP 版本约束
plugins[].requires.plugins❌依赖的其他插件 id 列表(安装前检查已启用)
plugins[].install.archive✅zip 相对路径(相对 source)
plugins[].install.sha256✅zip 文件 sha256(强校验)
plugins[].install.size❌文件大小(下载前预检)

4.3 客户端拉取

  • 后台"插件市场"页首次打开 → 拉取 feed.json → 缓存到 protected/plugins-market-state.json(含拉取时间)
  • 缓存 TTL 默认 5 分钟,过期自动刷新;刷新失败回退旧缓存(页面提示"缓存已过期")
  • feed.json 拉取用 cURL(CURLOPT_SSL_VERIFYPEER=true 正规证书,不关 SSL 校验)

五、客户端模块设计:PluginMarket.php

5.1 方法清单

namespace app\Helpers;

class PluginMarket
{
    // —— 市场源 ——
    public static function getSourceUrl(): string;          // 读 Settings 市场地址
    public static function fetchFeed(bool $force = false): array;  // 拉取+缓存 feed
    public static function getCachedFeed(): array;          // 读缓存(失败回退)
    public static function clearCache(): void;              // 清市场缓存

    // —— 安装/更新/卸载 ——
    public static function install(string $pluginId): array;        // 安装(核心)
    public static function update(string $pluginId): array;         // 更新(= 安装到已存在目录)
    public static function uninstall(string $pluginId): array;      // 卸载(调 Plugin::uninstall + 删目录)
    public static function reinstall(string $pluginId): array;      // 重新安装(备份→删→装)

    // —— 校验 ——
    public static function validateFeedItem(array $item): bool;     // 元数据白名单校验
    public static function checkRequirements(array $item): array;   // 版本/依赖检查
    public static function verifyZip(string $zipPath, string $sha256): bool; // sha256 校验

    // —— 解压安全 ——
    public static function extractZip(string $zipPath, string $dest): array; // 安全解压

    // —— 备份 ——
    public static function backupPlugin(string $pluginId): ?string; // 备份到 protected/backups/plugins/

    // —— 辅助 ——
    public static function compareVersions(string $remote, string $local): int; // version_compare
    public static function log(string $action, string $pluginId, string $detail): void; // AuditLog
}

5.2 核心安装流程(install 详细步骤)

1. 后台 POST /admin/plugin-market/install(CSRF 校验 + BaseController 鉴权)
2. fetchFeed() 拉取/读取缓存,按 id 找插件项;不存在 → 返回错误
3. validateFeedItem():字段白名单、sha256 格式、archive 相对路径(拒绝绝对路径/../)
4. checkRequirements():sosite/php/依赖插件版本检查,不满足 → 返回错误(不下载)
5. 下载 zip → protected/tmp/(file_get_contents/cURL,超时 60s,校验 size 上限如 20MB)
6. sha256_file(zip) 与 feed 的 sha256 比对;不一致 → 删除临时 zip,返回错误
7. 并发锁:flock(protected/plugins-market.lock, LOCK_EX|LOCK_NB),占用中 → "正在处理其他安装"
8. 解压安全:逐 entry 校验落点(见 5.3),解压到 protected/tmp/{pluginId}-{rand}/
9. 校验解压结果:plugin.json 存在且 id/name 与 feed 一致;目录名必须 = pluginId
10. 备份现有插件(如已存在):copy 到 protected/backups/plugins/{pluginId}-{时间戳}/
11. 落盘:rmdir 旧目录(如存在)→ 原子移入 plugins/{pluginId}/
12. opcache_invalidate(全部新文件, true)
13. Plugin::refresh() 重建插件缓存
14. 写市场状态:本地记录 {id, version, sha256, installed_at, source}
15. AuditLog::log('plugin_install', 'plugin', 0, "安装 {name} v{version}")
16. 释放锁;删除临时文件
17. 返回成功(提示"已安装,默认停用,请到插件管理启用")

5.3 解压安全(zip 路径穿越防护)

校验规则
entry 路径拒绝含 ../、绝对路径(/ 开头、盘符)的 entry
符号链接拒绝 entry 为 symlink(ZipArchive::STAT_NAME 类型判断)
落点校验逐 entry realpath 后必须落在目标目录内(strpos(real, dest) === 0)
大小上限单 entry ≤ 20MB,总解压 ≤ 50MB(防 zip 炸弹)
条目上限≤ 500 个文件
压缩比解压后大小/压缩后大小 ≤ 100(防 zip bomb)
顶层目录强制插件内容以 plugins/{id}/ 为根(zip 内可带或不带顶层目录,归一化处理)

5.4 失败回滚

  • 任何步骤失败:若已备份旧版本 → 恢复备份(rmdir 新 → 移回备份);未落盘前失败直接清理临时文件
  • 备份保留:protected/backups/plugins/ 保留最近 3 份(旧备份自动清理),供手动恢复
  • 回滚后 Plugin::refresh() 重建缓存,保证插件状态一致

六、后台管理页设计

6.1 路由

// hook/admin_route_register.php(新增插件市场钩子,或并入核心)
$router->get('/admin/plugin-market', ['\app\Controllers\Admin\PluginMarketController', 'index']);
$router->post('/admin/plugin-market/install', ['\app\Controllers\Admin\PluginMarketController', 'install']);
$router->post('/admin/plugin-market/update',   ['\app\Controllers\Admin\PluginMarketController', 'update']);
$router->post('/admin/plugin-market/uninstall',['\app\Controllers\Admin\PluginMarketController', 'uninstall']);
$router->post('/admin/plugin-market/reinstall',['\app\Controllers\Admin\PluginMarketController', 'reinstall']);
$router->post('/admin/plugin-market/refresh',  ['\app\Controllers\Admin\PluginMarketController', 'refresh']);

6.2 市场页布局(app/Views/admin/plugin_market.php)

┌─ 插件市场 ─────────────────────────────────────────────┐
│ [市场地址: https://...] [刷新]  [搜索框] [分类筛选]      │
├───────────────────────────────────────────────────────┤
│ 插件卡片列表(每项):                                  │
│   名称 / 版本 / 作者 / 描述 / 标签 / 分类                │
│   [安装] [更新] [重新安装] [卸载]  ← 按本地状态动态显示    │
│   本地状态:未安装 / 已装 v1.0.0(停用) / 已装并启用      │
│   可更新提示:远端 v1.1.0 > 本地 v1.0.0(绿色徽标)       │
│   sha256 摘要(前 8 位展示,点击展开完整值)              │
└───────────────────────────────────────────────────────┘

6.3 状态判断逻辑

本地状态按钮显示
未安装[安装]
已安装(本地版本 < 远端)[更新] [重新安装] [卸载]
已安装(本地版本 = 远端)[重新安装] [卸载]
已安装且 system=true[更新] 仅(禁停用/禁卸载,预留)

  • 本地版本读取:Plugin::getPlugin($id)['version'](缓存)
  • 已安装元数据(sha256/来源/时间)读 protected/plugins-market-state.json 中的 installed 段

6.4 交互安全

  • 所有 POST 带 CSRF;按钮点击后 confirm 提示(卸载/重新安装二次确认)
  • 安装/更新/卸载均为同步阻塞执行(插件规模小、秒级完成),页面顶部提示"处理中请勿关闭"
  • 操作结果 flash 提示(成功/失败原因)

七、发布脚本(发布方工具)

7.1 打包脚本 tools/plugin-publish.php(发布方运行,不进站点)

用法:php plugin-publish.php   [--source https://market.example.com/]
步骤:
1. 读取 plugins/{id}/plugin.json,校验 id/name/version 与参数一致
2. 校验目录结构(plugin.json 必含 name;views/assets/hook 存在性提示)
3. 生成 zip:仅打包插件目录内容(排除 .git/README 可选保留)
4. 计算 sha256 + size + 文件数
5. 更新 feed.json(按 id 合并/替换该插件条目,保留其他插件)
6. 输出:feed.json + files/{id}-{version}.zip + 校验信息

7.2 发布流程(人工)

改插件代码 → 提交版本号到 plugin.json → 跑发布脚本
  → 产出 zip + 更新 feed.json
  → 上传 feed.json 与 zip 到市场源(保持目录结构)
  → 站点后台"刷新"拉取新 feed → 显示可更新

7.3 市场源目录规范

market/
├── feed.json
└── files/
    ├── daily_checkin-1.0.0.zip
    └── seo-1.2.0.zip


八、安全设计(全部强制)

风险措施
未授权操作仅管理员:Admin\BaseController 鉴权 + 全部 POST Csrf::verifyOrDie
恶意/被篡改安装包sha256 强校验(下载后 hash_file 与 feed 比对,不符即弃)
feed 被篡改HTTPS + 证书校验;source 同源校验;schema 版本校验
zip 解压路径穿越逐 entry 落点校验、拒绝 ..//绝对路径/符号链接/zip 炸弹(见 5.3)
未审查代码立即执行安装后默认停用(落盘后 plugin.json activated:false,须后台手动启用)
安装失败全流程备份 + 失败自动回滚 + 备份保留 3 份
恶意插件删库安装/卸载全程 AuditLog 审计;卸载时由插件自身 uninstall() 负责删表(框架不越权删数据)
并发安装flock 互斥锁,重复触发拒绝
临时文件残留安装结束清理 protected/tmp/ 临时 zip/解压目录
大文件/流量下载前 size 预检(≤20MB)、超时 60s、失败中止
版本错配requires.sosite/php version_compare 校验;依赖插件检查


九、边界情况与错误处理(穷举)

9.1 网络与源

情况处理
feed.json 拉取失败(DNS/超时/5xx)回退缓存;无缓存 → 页面提示"市场不可达",不崩溃
feed.json 格式非法(JSON 解析失败/schema 不符)拒绝使用,保留旧缓存,提示"市场数据异常"
下载 zip 超时/中断删除临时文件,返回"下载失败,请重试"
zip 下载后 sha256 不符删临时文件,返回"校验失败,安装包可能被篡改"(AuditLog 记录告警)
市场源 HTTP(非 HTTPS)允许但页面黄条提示"非加密源,建议 HTTPS"(不强制)
证书校验失败视为不可达,提示检查证书

9.2 插件安装/更新

情况处理
插件 id 在本地已存在(重复安装)提示"已安装,如需覆盖请用更新/重新安装";或 install 检测到存在自动转 update(二选一,默认提示)
插件 id 与 zip 内 plugin.json 的 id 不一致拒绝(防伪装包)
zip 解压后缺 plugin.json / 无 name 字段拒绝,清理临时目录
zip 内容不是单插件(多目录/顶层混乱)归一化失败 → 拒绝
安装磁盘空间不足file_put_contents 失败 → 回滚 + 提示空间不足
插件目录已存在且被占用(Windows 文件锁)备份/替换失败 → 提示"目录被占用,稍后重试"(Windows 特有)
plugins_cache.php 写入失败(目录只读)安装回滚 + 提示"插件目录不可写"
安装后启用时 activate() 返回 false保持停用,提示"安装成功但启用失败:原因"(插件自身错误)
更新时本地插件有本地修改更新前自动备份;页面提示"更新将覆盖本地修改,备份已保留在 protected/backups/plugins/"

9.3 依赖与版本

情况处理
SoSite 版本不满足 requires.sosite安装前拒绝,提示所需版本
PHP 版本不满足 requires.php安装前拒绝
依赖插件未安装/未启用安装前检查 requires.plugins,缺失则提示先装依赖
依赖循环(A 依赖 B,B 依赖 A)feed 校验时检测环,拒绝
远端版本 < 本地版本(降级)市场页不显示"更新",仅显示"重新安装"(提示版本低于本地)

9.4 卸载

情况处理
卸载已启用插件先 Plugin::deactivate() 再卸载;插件 uninstall() 清数据
插件 uninstall() 抛异常捕获记录,继续删注册(文件保留,提示数据清理失败)
卸载 system 插件拒绝(预留 system 标志)
卸载后文件残留提示用户手动删除 plugins/{id}/(或提供"删除文件"二次确认选项)
卸载正在被其他插件依赖的插件检查 requires.plugins 反向依赖,提示先卸载依赖方

9.5 并发与一致性

情况处理
并发安装(两个管理员同时操作)flock 互斥锁,第二个直接拒绝
安装过程中页面刷新同步阻塞 + 结果 flash;刷新后按实际状态显示(幂等判断)
安装后缓存与磁盘不一致每步操作后 Plugin::refresh() 强制重建;异常路径也刷


十、测试计划

10.1 单元/脚本测试(本地)

测试项方法
feed 解析构造合法/非法 feed JSON → validateFeedItem / fetchFeed
sha256 校验正确/篡改 zip → verifyZip 结果
解压安全构造含 ../、绝对路径、symlink、超大文件、超多条目的 zip → extractZip 全部拒绝
版本比较version_compare 边界(1.0.0 vs 1.0.1、1.1 vs 1.10)
依赖检查requires 满足/不满足/依赖缺失/循环依赖

10.2 集成测试(真实站点)

场景步骤预期
全新安装发布一个测试插件 zip → 市场页安装落盘 plugins/{id}/,activated=false,缓存刷新,AuditLog 记录
安装后启用后台插件管理启用插件功能可用
更新发布 v1.0.1 → 市场页更新备份旧版,新版本生效,缓存刷新
回滚验证构造一个激活时抛异常的插件安装/更新失败自动回滚到备份
卸载卸载测试插件数据表删除(插件 uninstall 内)、注册移除
并发两个请求同时安装第二个被锁拒绝
权限非管理员访问安装接口重定向登录/拒绝
CSRF无 token 提交安装verifyOrDie 终止
路径穿越恶意 zip解压被拒,无文件落盘
停用插件更新更新已停用插件更新后保持停用

10.3 回归

  • 现有 14 个插件逐个启用/访问主页面,确认 A+ 模板解析、资源加载、hooks 正常(插件仓库不改变任何现有机制)
  • 安装新插件后 php -l 全部新文件 + 页面访问 + 后台启用

十一、分期实施计划

阶段内容涉及文件预估
P0 基础市场协议文档定稿 + PluginMarket.php 核心(fetchFeed/校验/下载/sha256/解压/落盘/备份回滚)+ flock 锁 + AuditLogapp/Helpers/PluginMarket.php、发布脚本1.5~2 天
P1 后台PluginMarketController + 市场页视图(列表/搜索/分类/状态/按钮)+ 路由 hook + Settings 市场地址控制器、视图、hook、settings 配置1 天
P2 安全加固解压路径穿越全套校验、zip 炸弹防护、依赖/版本检查、system 标志预留、反向依赖检查PluginMarket.php 扩展1 天
P3 发布侧发布脚本完善(打包/feed 更新/校验输出)+ 示例测试插件 + 市场源部署文档tools/plugin-publish.php、示例0.5~1 天
P4 测试收尾10.x 测试计划全部执行 + 回归 14 插件 + 部署文档(市场地址配置、HTTPS 建议)文档、修复1 天

合计约 5~6 天(单人)。与在线升级(P2)相互独立,本计划为插件仓库专项。


十二、部署与配置

12.1 站点侧配置

  • 后台"设置 → 插件"新增字段:市场地址(Settings::set('plugin_market_url', 'https://market.example.com/feed.json'))
  • 默认空 = 市场页提示"未配置市场源"
  • 可选:市场检查间隔(默认 5 分钟缓存)

12.2 服务器要求

  • PHP 需启用 zip 扩展(phpstudy 默认开;php -m | grep zip 验证)
  • plugins/、protected/ 目录需可写(安装/备份)
  • 出站 HTTP/HTTPS 可达市场源
  • 建议:Nginx 下 protected/ 已配置 deny all(交接文档 P3 待办,若未配需补 location ~ ^/protected/ { deny all; },否则备份 zip 可被下载)

12.3 与现有 update 包的关系

  • 插件仓库是独立能力,不替代 update 手动包(那是核心程序升级通道)
  • 插件市场分发包保持现有 manifest 格式(plugin.json 结构不变),14 个现有插件可直接作为首批市场内容发布

十三、附录

13.1 关键新增文件总览

app/Helpers/PluginMarket.php              # 插件市场安装器(核心)
app/Controllers/Admin/PluginMarketController.php
app/Views/admin/plugin_market.php         # 后台市场页
app/Views/admin/_sidebar.php              # 侧边栏入口(复用 admin_sidebar_links hook)
tools/plugin-publish.php                  # 发布脚本(发布方工具,不入站点)
protected/plugins-market-state.json       # 市场缓存 + 已安装元数据(运行时生成)
protected/backups/plugins/                # 安装/升级备份目录
protected/plugins-market.lock             # 并发锁文件

13.2 Settings 新增键

键默认用途
plugin_market_url''市场 feed 地址
plugin_market_last_check0上次拉取时间(缓存 TTL)

13.3 待复核清单(实现前)

  • [ ] zip 扩展确认
  • [ ] Router admin_route_register 挂载点
  • [ ] Settings::get/update 签名
  • [ ] AuditLog::log 签名($action/$targetType/$targetId/$detail)
  • [ ] Plugin::refresh() 与 plugins_cache.php 结构
  • [ ] protected/ Nginx 访问隔离
  • [ ] Windows 文件锁行为(目录占用时的备份/替换)
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| Views 2 | Replies 0

All Replies (0)

No replies yet

Please Log in