安全(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(市场 / 开发者中心 / 审核 / 分润)
205 lines
6.6 KiB
PHP
205 lines
6.6 KiB
PHP
<?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);
|
||
}
|
||
}
|
||
}
|