feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
1 parent
1d49e6f5ee
commit
18101cf11a
636 files changed
+58517
-9519
No files matched your search
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
/**
|
||||
* 批量打包所有本地插件为市场 zip(结构对齐 ywxapp\service\AddonService::package())。
|
||||
*
|
||||
* - addon/<name>/ 内容 -> zip 根
|
||||
* - public/static/<name>/ 静态资源 -> zip 的 static/ 前缀
|
||||
*
|
||||
* 每个插件自动从 addon/<name>/info.php 读取 title/author/intro/version,
|
||||
* 输出到 build/addons/,并生成 wxapp_appmall_addon_list 测试记录 SQL(build/sql/<name>-<ver>-appmall_addon_list.sql)。
|
||||
*
|
||||
* 用法:
|
||||
* php scripts/package_all_addons.php [bump] [skip:name1,name2,...]
|
||||
*
|
||||
* 例:
|
||||
* php scripts/package_all_addons.php # 按 info.php 当前版本打包(不改版本号)
|
||||
* php scripts/package_all_addons.php bump # 每个插件版本按 patch +1 并写回 info.php
|
||||
* php scripts/package_all_addons.php bump skip:market # 同上但跳过 market
|
||||
* php scripts/package_all_addons.php skip:market # 跳过 market,版本号不变
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__) . DIRECTORY_SEPARATOR; // d:/wwwroot/ywxapp_dev/
|
||||
|
||||
// 参数:[bump] [skip:name1,name2,...]
|
||||
// bump 可选:每个插件版本按 patch +1 并写回 addons/<name>/info.php
|
||||
// skip: 可选:跳过指定插件(逗号分隔)
|
||||
$bumpArg = $argv[1] ?? '';
|
||||
$bump = false;
|
||||
if ($bumpArg === 'bump') {
|
||||
$bump = true;
|
||||
$skipArg = $argv[2] ?? '';
|
||||
} else {
|
||||
$skipArg = $bumpArg;
|
||||
}
|
||||
$skip = [];
|
||||
if (str_starts_with($skipArg, 'skip:')) {
|
||||
$skip = array_filter(explode(',', substr($skipArg, 5)), static fn($s) => $s !== '');
|
||||
}
|
||||
|
||||
$addonsDir = $root . 'addon' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($addonsDir)) {
|
||||
fwrite(STDERR, "addons 目录不存在: {$addonsDir}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!class_exists('ZipArchive')) {
|
||||
fwrite(STDERR, "ZipArchive 扩展不可用\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$marketDir = $root . 'build' . DIRECTORY_SEPARATOR . 'addons' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($marketDir)) {
|
||||
mkdir($marketDir, 0755, true);
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$ok = [];
|
||||
$fail = [];
|
||||
|
||||
foreach (array_diff(scandir($addonsDir), ['.', '..']) as $name) {
|
||||
$addonDir = $addonsDir . $name . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($addonDir)) {
|
||||
continue;
|
||||
}
|
||||
$infoFile = $addonDir . 'info.php';
|
||||
if (!is_file($infoFile)) {
|
||||
$fail[] = $name . ' (缺 info.php)';
|
||||
continue;
|
||||
}
|
||||
if (in_array($name, $skip, true)) {
|
||||
echo "[skip] {$name}\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$info = include $infoFile;
|
||||
$title = (string) ($info['title'] ?? $name);
|
||||
$author = (string) ($info['author'] ?? '');
|
||||
$intro = (string) ($info['intro'] ?? '');
|
||||
$version = (string) ($info['version'] ?? '');
|
||||
if (!$version) {
|
||||
$fail[] = $name . ' (info.php 缺 version)';
|
||||
continue;
|
||||
}
|
||||
// 自动递增版本号(bump 模式):先记录旧值,+1 后写回 info.php
|
||||
$oldVersion = $version;
|
||||
if ($bump) {
|
||||
$version = bumpVersion($oldVersion, 'patch');
|
||||
writeAddonVersion($infoFile, $version);
|
||||
}
|
||||
|
||||
$zipFile = $marketDir . $name . '-' . $version . '.zip';
|
||||
$staticDir = $root . 'public' . DIRECTORY_SEPARATOR . 'static' . DIRECTORY_SEPARATOR
|
||||
. $name . DIRECTORY_SEPARATOR;
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
$fail[] = $name . ' (无法创建 zip)';
|
||||
continue;
|
||||
}
|
||||
addDirToZip($zip, $addonDir, '');
|
||||
if (is_dir($staticDir)) {
|
||||
addDirToZip($zip, $staticDir, 'static/');
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
$hash = hash_file('sha256', $zipFile);
|
||||
$size = filesize($zipFile);
|
||||
$absPath = realpath($zipFile);
|
||||
|
||||
$esc = static function (string $v): string {
|
||||
return str_replace(["\\", "'"], ["\\\\", "\\'"], $v);
|
||||
};
|
||||
$sql = "INSERT INTO `wxapp_appmall_addon_list` "
|
||||
. "(`name`,`title`,`version`,`price`,`author`,`description`,`logo`,`file_path`,`file_hash`,`status`,`create_at`,`update_at`) VALUES ("
|
||||
. "'" . $esc($name) . "',"
|
||||
. "'" . $esc($title) . "',"
|
||||
. "'" . $esc($version) . "',"
|
||||
. "0.00,"
|
||||
. "'" . $esc($author) . "',"
|
||||
. "'" . $esc($intro) . "',"
|
||||
. "'',"
|
||||
. "'" . $esc($absPath) . "',"
|
||||
. "'" . $hash . "',"
|
||||
. "1,UNIX_TIMESTAMP(),UNIX_TIMESTAMP());";
|
||||
|
||||
$sqlDir = $root . 'build' . DIRECTORY_SEPARATOR . 'sql' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($sqlDir)) {
|
||||
mkdir($sqlDir, 0755, true);
|
||||
}
|
||||
$sqlFile = $sqlDir . $name . '-' . $version . '-appmall_addon_list.sql';
|
||||
file_put_contents($sqlFile, "-- 插件市场测试记录(由 scripts/package_all_addons.php 生成)\n-- zip: {$absPath}\n-- sha256: {$hash}\n" . $sql . "\n");
|
||||
|
||||
$verLabel = $bump ? ($oldVersion . ' -> ' . $version) : $version;
|
||||
$rows[] = [$name, $version, $size, $absPath];
|
||||
$ok[] = $name;
|
||||
printf(" [ok] %-12s v%s %8d bytes\n", $name, $verLabel, $size);
|
||||
}
|
||||
|
||||
echo "\n=== 打包汇总 ===\n";
|
||||
echo "成功: " . count($ok) . " 失败: " . count($fail) . "\n";
|
||||
if ($fail) {
|
||||
echo "失败列表:\n - " . implode("\n - ", $fail) . "\n";
|
||||
}
|
||||
echo "输出目录: {$marketDir}\n";
|
||||
|
||||
/**
|
||||
* 在 x.y.z 基础上递增指定段(major/minor/patch)
|
||||
*/
|
||||
function bumpVersion(string $ver, string $level = 'patch'): string
|
||||
{
|
||||
if (!preg_match('/^(\d+)\.(\d+)\.(\d+)$/', $ver, $m)) {
|
||||
return $ver;
|
||||
}
|
||||
$major = (int) $m[1];
|
||||
$minor = (int) $m[2];
|
||||
$patch = (int) $m[3];
|
||||
switch ($level) {
|
||||
case 'major':
|
||||
$major++; $minor = 0; $patch = 0; break;
|
||||
case 'minor':
|
||||
$minor++; $patch = 0; break;
|
||||
case 'patch':
|
||||
default:
|
||||
$patch++; break;
|
||||
}
|
||||
return $major . '.' . $minor . '.' . $patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将新版本号写回插件 info.php
|
||||
*/
|
||||
function writeAddonVersion(string $infoFile, string $ver): void
|
||||
{
|
||||
if (!is_file($infoFile)) {
|
||||
return;
|
||||
}
|
||||
$c = file_get_contents($infoFile);
|
||||
$c = preg_replace(
|
||||
"/(['\"])version['\"]\s*=>\s*['\"][^'\"]*['\"]/",
|
||||
"'version' => '" . $ver . "'",
|
||||
$c
|
||||
);
|
||||
file_put_contents($infoFile, $c);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将目录递归加入 zip(结构对齐 AddonService::addDirectoryToZip)
|
||||
*/
|
||||
function addDirToZip(ZipArchive $zip, string $dir, string $prefix): void
|
||||
{
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
foreach ($files as $file) {
|
||||
$filePath = $file->getRealPath();
|
||||
$rel = ltrim(str_replace(DIRECTORY_SEPARATOR, '/', substr($filePath, strlen($dir))), '/');
|
||||
if ($file->isDir()) {
|
||||
$zip->addEmptyDir($prefix . $rel);
|
||||
} else {
|
||||
$zip->addFile($filePath, $prefix . $rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user