安全(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(市场 / 开发者中心 / 审核 / 分润)
191 lines
6.8 KiB
PHP
191 lines
6.8 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>
|
||
// +----------------------------------------------------------------------
|
||
|
||
if (!function_exists('addon_url')) {
|
||
/**
|
||
* 生成插件访问地址(MultiApp 多应用模型:/<addon>/<controller>/<action>)
|
||
* @param string $name 插件标识
|
||
* @param string $url 相对路径,如 'admin/index' 或 '/admin/index'
|
||
* @param array $vars 附加 GET 参数
|
||
* @return string
|
||
*/
|
||
function addon_url(string $name, string $url = '', array $vars = []): string
|
||
{
|
||
$path = '/' . trim($name, '/');
|
||
if ($url !== '' && $url !== '/') {
|
||
$path .= '/' . ltrim($url, '/');
|
||
}
|
||
return $vars ? $path . '?' . http_build_query($vars) : $path;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('get_addon_instance')) {
|
||
/**
|
||
* 获取插件主类实例(addon\<name>\Addon)
|
||
* @param string $name 插件标识
|
||
* @return object|null
|
||
*/
|
||
function get_addon_instance(string $name)
|
||
{
|
||
$class = '\\addon\\' . $name . '\\Addon';
|
||
if (!class_exists($class)) {
|
||
return null;
|
||
}
|
||
return app($class);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('get_addon_info')) {
|
||
/**
|
||
* 读取插件信息(优先缓存,force=true 强制重读)
|
||
* @param string $name 插件标识
|
||
* @param bool $force
|
||
* @return array
|
||
*/
|
||
function get_addon_info(string $name = '', bool $force = false): array
|
||
{
|
||
$instance = get_addon_instance($name);
|
||
return $instance ? $instance->getInfo($name, $force) : [];
|
||
}
|
||
}
|
||
|
||
if (!function_exists('get_addon_config')) {
|
||
/**
|
||
* 读取插件配置(优先缓存,force=true 强制重读)
|
||
* @param string $name 插件标识
|
||
* @param bool $force
|
||
* @return array
|
||
*/
|
||
function get_addon_config(string $name = '', bool $force = false): array
|
||
{
|
||
$instance = get_addon_instance($name);
|
||
return $instance ? $instance->getConfig($name, $force) : [];
|
||
}
|
||
}
|
||
|
||
if (!function_exists('is_market_client')) {
|
||
/**
|
||
* 市场是否处于「客户机」模式(「谁是中心站」由 config/ywxapp.php 决定):
|
||
* - market_mode=center → 强制中心站
|
||
* - market_mode=client → 强制客户机
|
||
* - market_mode=auto(默认):
|
||
* · api_url 为空 → 本机即中心站(直接读写本地市场 wxapp_appmall_addon_list)
|
||
* · api_url 指向本机(同主机)→ 仍视为中心站(配置纠偏)
|
||
* · api_url 指向其他主机 → 本机作为客户机,经 RemoteService 连接该中心站
|
||
*
|
||
* auto 模式下依赖「当前请求主机」判定自身:改用框架 Request 抽象(可测试、CLI 可控),
|
||
* 不再直接读 $_SERVER['HTTP_HOST']。CLI(路由缓存/refresh-menu)无法取得主机时保守判为客户机——
|
||
* 因此中心站请保持 api_url 为空,或显式设置 market_mode=center 以彻底解除该约束。
|
||
* @return bool
|
||
*/
|
||
function is_market_client(): bool
|
||
{
|
||
$mode = \think\facade\Config::get('ywxapp.market_mode', 'auto');
|
||
if ($mode === 'center') {
|
||
return false;
|
||
}
|
||
if ($mode === 'client') {
|
||
return true;
|
||
}
|
||
|
||
// auto:基于 api_url + 自身主机推导
|
||
$apiUrl = \think\facade\Config::get('ywxapp.api_url', '');
|
||
if (empty($apiUrl)) {
|
||
return false;
|
||
}
|
||
$host = parse_url($apiUrl, PHP_URL_HOST);
|
||
if (empty($host)) {
|
||
return false;
|
||
}
|
||
|
||
// 用框架 Request 抽象主机(可 mock / CLI 可控),替代直接读 $_SERVER['HTTP_HOST']
|
||
$req = function_exists('request') ? request() : null;
|
||
$self = $req ? (string) $req->host() : '';
|
||
if ($self === '') {
|
||
// CLI(如路由缓存/refresh-menu)无法判定主机:回退到 market_mode 显式配置。
|
||
// 只有显式声明 market_mode=client 才当作客户机;否则(auto/center)按中心站处理,
|
||
// 避免中心站 CLI 刷新菜单时误删 centerOnly 项(应用市场审核/开发者/收益等运营菜单)。
|
||
return \think\facade\Config::get('ywxapp.market_mode', 'auto') === 'client';
|
||
}
|
||
$selfHost = preg_replace('/:\d+$/', '', $self);
|
||
$cmpHost = preg_replace('/:\d+$/', '', $host);
|
||
if (strcasecmp($selfHost, $cmpHost) === 0) {
|
||
return false; // api_url 指向自身 = 视为中心站(配置纠偏)
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('aurl')) {
|
||
function aurl(string $appName, string $url = '', array $vars = [], string $suffix = ''): string
|
||
{
|
||
$map = config('app.domain_bind', []);
|
||
$mainDomain = config('app.domain_bind');
|
||
|
||
$domainKey = '';
|
||
foreach ($map as $key => $mappedApp) {
|
||
if ($mappedApp === $appName) {
|
||
$domainKey = $key;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 解析 host 和 port
|
||
$host = '';
|
||
$port = '';
|
||
|
||
if ($domainKey === '') {
|
||
$host = true; // 用当前域名
|
||
} elseif (str_contains($domainKey, ':')) {
|
||
// 带端口:api.example.com:8080
|
||
[$host, $port] = explode(':', $domainKey, 2);
|
||
$port = ':' . $port;
|
||
} elseif (str_contains($domainKey, '.')) {
|
||
// 完整域名
|
||
$host = $domainKey;
|
||
} else {
|
||
// 子域名
|
||
$host = "{$domainKey}.{$mainDomain}";
|
||
}
|
||
|
||
return (string) \think\facade\Route::buildUrl($url)
|
||
->vars($vars)
|
||
->suffix($suffix)
|
||
->domain($host);
|
||
}
|
||
}
|
||
|
||
|
||
if (!function_exists('L')) {
|
||
function L($value)
|
||
{
|
||
return \think\facade\Lang::get($value);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('hook')) {
|
||
/**
|
||
* 模板钩子(Discuz 式扩展点)
|
||
*
|
||
* 模板里写 {:hook('backend.score.toolbar', ['row' => $row])},
|
||
* 插件用 HookService::mount()/register() 或监听 HookRender 事件返回 HTML 片段,
|
||
* 多个插件的输出按顺序拼接后插到埋点位置。无插件挂载时返回空串。
|
||
*
|
||
* 注意:{:...} 输出不做 HTML 转义,插件需自行保证输出内容安全。
|
||
*
|
||
* @param string $name 钩子名,如 backend.layout.head / backend.score.toolbar
|
||
* @param array $vars 传给插件模板/回调的变量
|
||
* @return string
|
||
*/
|
||
function hook(string $name, array $vars = []): string
|
||
{
|
||
return \ywxapp\service\HookService::render($name, $vars);
|
||
}
|
||
}
|