安全(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(市场 / 开发者中心 / 审核 / 分润)
186 lines
6.6 KiB
PHP
186 lines
6.6 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
// | 语义化版本约束判定(Composer 约束语法子集)
|
||
// +----------------------------------------------------------------------
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace ywxapp\utils;
|
||
|
||
/**
|
||
* 版本约束工具
|
||
*
|
||
* 支持的约束写法:
|
||
* 比较符 >=1.2.0 >1.0 <=2.0 ==1.0.0 =1.0.0 !=1.0.0
|
||
* 裸版本号 1.2.0(等价 ==1.2.0)
|
||
* caret ^1.2.3 => >=1.2.3 <2.0.0 ;^0.2.3 => >=0.2.3 <0.3.0(不跨段原则)
|
||
* tilde ~1.2.3 => >=1.2.3 <1.3.0 ;~1.2 => >=1.2.0 <2.0.0
|
||
* 通配符 1.2.* => >=1.2.0 <1.3.0 ;1.* => >=1.0.0 <2.0.0
|
||
* 与 AND '>=1.2 <2.0'(也可逗号分隔)
|
||
* 或 OR '<1.0 || >=2.0'(写作 | 亦可)
|
||
*
|
||
* 设计取舍:解析不出合法约束时一律**放行**(返回 true)。
|
||
* 依赖声明写错不该让整站无法安装,真实缺失由下游的实际报错兜底——这与上游
|
||
* AddonService::versionSatisfies() 的既有行为保持一致。
|
||
*/
|
||
class Version
|
||
{
|
||
/**
|
||
* 判定版本是否满足约束
|
||
*/
|
||
public static function satisfies(string $version, string $constraint): bool
|
||
{
|
||
$constraint = trim($constraint);
|
||
// 版本号缺失(依赖插件没写 version)或约束为空:一律放行,交给下游的真实报错兜底
|
||
if (trim($version) === '' || $constraint === '') {
|
||
return true;
|
||
}
|
||
$version = self::clean($version);
|
||
// PHP 版本这类写法(8.1、8.1.0)与插件版本一致处理
|
||
foreach (self::splitOr($constraint) as $orGroup) {
|
||
if ($orGroup === []) {
|
||
continue;
|
||
}
|
||
$ok = true;
|
||
foreach ($orGroup as $single) {
|
||
if (!self::matchSingle($version, $single)) {
|
||
$ok = false;
|
||
break;
|
||
}
|
||
}
|
||
if ($ok) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 拆 OR:`||` / `|` 分隔
|
||
*
|
||
* @return array<int, array<int, string>> 每组是 AND 条件数组
|
||
*/
|
||
private static function splitOr(string $constraint): array
|
||
{
|
||
$groups = [];
|
||
foreach (preg_split('/\s*\|\|?\s*/', $constraint) ?: [] as $group) {
|
||
$group = trim($group);
|
||
if ($group === '') {
|
||
continue;
|
||
}
|
||
// AND:空白或逗号分隔
|
||
$ands = array_values(array_filter(array_map(
|
||
'trim',
|
||
preg_split('/[\s,]+/', $group) ?: []
|
||
)));
|
||
$groups[] = $ands;
|
||
}
|
||
return $groups;
|
||
}
|
||
|
||
/**
|
||
* 判定单个条件
|
||
*/
|
||
private static function matchSingle(string $version, string $single): bool
|
||
{
|
||
if (!preg_match('/^(<=|>=|==|!=|<|>|=|\^|~)?\s*v?(\d+(?:\.\d+)*(?:\.\*)?)$/i', $single, $m)) {
|
||
return true; // 非法约束:放行
|
||
}
|
||
$op = strtolower($m[1] ?? '');
|
||
$target = self::clean($m[2]);
|
||
$targetW = str_replace('.*', '', $m[2]); // 通配符段
|
||
|
||
switch ($op) {
|
||
case '^':
|
||
return version_compare($version, $target, '>=')
|
||
&& version_compare($version, self::upperCaret($targetW), '<');
|
||
case '~':
|
||
// ~1.2(两位)=> <2.0.0;~1.2.3(三位)=> <1.3.0
|
||
$upper = substr_count($targetW, '.') >= 1
|
||
? self::upperTilde($targetW)
|
||
: ((string) (((int) $targetW) + 1) . '.0.0');
|
||
return version_compare($version, $target, '>=')
|
||
&& version_compare($version, $upper, '<');
|
||
case '':
|
||
// 裸版本号:1.2.0 等价 ==;1.2.* 是通配
|
||
if (str_contains($m[2], '*')) {
|
||
return self::matchWildcard($version, $targetW);
|
||
}
|
||
return version_compare($version, $target, '==');
|
||
case '=':
|
||
return version_compare($version, $target, '==');
|
||
default:
|
||
// 通配符与 ^ / ~ 组合意义不明,退化为纯比较
|
||
return version_compare($version, $target, $op);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通配在新判定:1.2.* => [1.2.0, 1.3.0);1.* => [1.0.0, 2.0.0)
|
||
*/
|
||
private static function matchWildcard(string $version, string $base): bool
|
||
{
|
||
$parts = explode('.', $base);
|
||
$lower = self::pad(implode('.', $parts));
|
||
$depth = count($parts);
|
||
$parts[$depth - 1] = (string) (((int) end($parts)) + 1);
|
||
$upper = self::pad(implode('.', array_slice($parts, 0, $depth)));
|
||
return version_compare($version, $lower, '>=') && version_compare($version, $upper, '<');
|
||
}
|
||
|
||
/**
|
||
* caret 上界:不改变最左侧非零位
|
||
* 1.2.3 -> 2.0.0 ;0.2.3 -> 0.3.0 ;0.0.3 -> 0.0.4
|
||
*/
|
||
private static function upperCaret(string $v): string
|
||
{
|
||
$parts = array_map('intval', explode('.', $v));
|
||
foreach ($parts as $i => $num) {
|
||
if ($num !== 0 || $i === count($parts) - 1) {
|
||
$parts[$i] = $num + 1;
|
||
$parts = array_slice($parts, 0, $i + 1);
|
||
return self::pad(implode('.', $parts));
|
||
}
|
||
}
|
||
return '9999999.0.0';
|
||
}
|
||
|
||
/**
|
||
* tilde 上界:递增最后给出的次末位(~1.2.3 -> 1.3.0)
|
||
*/
|
||
private static function upperTilde(string $v): string
|
||
{
|
||
$parts = explode('.', $v);
|
||
$parts[count($parts) - 2] = (string) (((int) $parts[count($parts) - 2]) + 1);
|
||
return self::pad(implode('.', array_slice($parts, 0, count($parts) - 1) + []));
|
||
}
|
||
|
||
/**
|
||
* 清理版本号:去 v 前缀,补齐到三段便于比较
|
||
*/
|
||
private static function clean(string $v): string
|
||
{
|
||
$v = strtolower(trim(ltrim(trim($v), 'vV')));
|
||
$v = preg_replace('/[^0-9.]/', '', $v) ?? '';
|
||
return self::pad($v);
|
||
}
|
||
|
||
/**
|
||
* 补齐 1.2 -> 1.2.0(version_compare 认为 1.2 == 1.2.0,但用于展示/拼接时统一)
|
||
*/
|
||
private static function pad(string $v): string
|
||
{
|
||
$parts = explode('.', $v === '' ? '0' : $v);
|
||
while (count($parts) < 3) {
|
||
$parts[] = '0';
|
||
}
|
||
return implode('.', $parts);
|
||
}
|
||
}
|