feat: secure storage artifact preview
This commit is contained in:
@@ -22,18 +22,19 @@ Route::group("/guest", function () {
|
||||
|
||||
|
||||
# 系统配置
|
||||
Route::group("/system", function () {
|
||||
Route::get("/user/info", [AdminUser::class, "info"])->name("AdminUser@info");
|
||||
Route::get("/user/list", [AdminUser::class, "getAdminUserList"])->name("AdminUser@getAdminUserList");
|
||||
Route::post("/user/save", [AdminUser::class, "saveAdminUser"])->name("AdminUser@saveAdminUser");
|
||||
Route::post("/user/del", [AdminUser::class, "delAdminUser"])->name("AdminUser@delAdminUser");
|
||||
|
||||
Route::group("/system", function () {
|
||||
Route::get("/user/info", [AdminUser::class, "info"])->name("AdminUser@info");
|
||||
Route::get("/user/list", [AdminUser::class, "getAdminUserList"])->name("AdminUser@getAdminUserList");
|
||||
Route::post("/user/save", [AdminUser::class, "saveAdminUser"])->name("AdminUser@saveAdminUser");
|
||||
Route::post("/user/del", [AdminUser::class, "delAdminUser"])->name("AdminUser@delAdminUser");
|
||||
|
||||
Route::get("/config/list", [SystemConfig::class, "getSystemConfigList"])->name("SystemConfig@getSystemConfigList");
|
||||
Route::post("/config/save", [SystemConfig::class, "saveSystemConfig"])->name("SystemConfig@saveSystemConfig");
|
||||
Route::post("/config/seo-ai-rules/reset", [SystemConfig::class, "resetSeoAiRules"])->name("SystemConfig@resetSeoAiRules");
|
||||
|
||||
Route::get("/cjpz/list", [SystemConfig::class, "getCaiJiPeiZhiList"])->name("SystemConfig@getCaiJiPeiZhiList");
|
||||
Route::post("/cjpz/save", [SystemConfig::class, "saveCaiJiPeiZhi"])->name("SystemConfig@saveCaiJiPeiZhi");
|
||||
Route::get("/storage/file", [SystemConfig::class, "previewStorageFile"])->name("SystemConfig@previewStorageFile");
|
||||
|
||||
Route::get("/cjpz/list", [SystemConfig::class, "getCaiJiPeiZhiList"])->name("SystemConfig@getCaiJiPeiZhiList");
|
||||
Route::post("/cjpz/save", [SystemConfig::class, "saveCaiJiPeiZhi"])->name("SystemConfig@saveCaiJiPeiZhi");
|
||||
|
||||
Route::get("/plan/list", [SystemConfig::class, "getPlanTaskList"])->name("SystemConfig@getPlanTaskList");
|
||||
Route::post("/plan/save", [SystemConfig::class, "savePlanTask"])->name("SystemConfig@savePlanTask");
|
||||
|
||||
@@ -25,6 +25,44 @@ use think\Response;
|
||||
|
||||
class SystemConfig extends BaseController
|
||||
{
|
||||
public function previewStorageFile(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||
{
|
||||
$strRelativePath = trim(str_replace('\\', '/', (string)$Request->get('path', '')));
|
||||
if ($strRelativePath === '') {
|
||||
return response('storage path is required', 400, ['Content-Type' => 'text/plain; charset=UTF-8']);
|
||||
}
|
||||
|
||||
$strRelativePath = ltrim($strRelativePath, '/');
|
||||
if (str_starts_with($strRelativePath, 'storage/')) {
|
||||
$strRelativePath = substr($strRelativePath, strlen('storage/'));
|
||||
}
|
||||
|
||||
$strStorageRoot = rtrim(str_replace('\\', '/', (string)root_path()), '/') . '/storage';
|
||||
$strStorageRootReal = realpath($strStorageRoot);
|
||||
$strCandidatePath = $strStorageRoot . '/' . $strRelativePath;
|
||||
$strCandidateReal = realpath($strCandidatePath);
|
||||
|
||||
if ($strStorageRootReal === false || $strCandidateReal === false || !is_file($strCandidateReal)) {
|
||||
return response('storage file not found', 404, ['Content-Type' => 'text/plain; charset=UTF-8']);
|
||||
}
|
||||
|
||||
$strStorageRootReal = str_replace('\\', '/', $strStorageRootReal);
|
||||
$strCandidateReal = str_replace('\\', '/', $strCandidateReal);
|
||||
if (!str_starts_with($strCandidateReal, $strStorageRootReal . '/')) {
|
||||
return response('storage file is out of allowed root', 403, ['Content-Type' => 'text/plain; charset=UTF-8']);
|
||||
}
|
||||
|
||||
$strMimeType = (string)(mime_content_type($strCandidateReal) ?: 'application/octet-stream');
|
||||
$strFilename = basename($strCandidateReal);
|
||||
$strContent = (string)file_get_contents($strCandidateReal);
|
||||
|
||||
return response($strContent, 200, [
|
||||
'Content-Type' => $strMimeType,
|
||||
'Content-Disposition' => 'inline; filename="' . addslashes($strFilename) . '"',
|
||||
'X-Storage-Relative-Path' => $strRelativePath,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 采集配置列表
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
3. `video-metadata-missing-task-pool` 已迁到 `storage/video-metadata-missing/task-pool`
|
||||
4. `domain-seo-external/manual-import` 已迁到 `storage/external-seo/manual-import`
|
||||
5. `domain-seo-external/search-console` 模板目录已迁到 `storage/external-seo/search-console`
|
||||
6. `Vue 后台` 打开 `external-seo/*`、`video-metadata-missing/*` 这类产物时,不再假设文件直接暴露在 `public`,而是通过后台鉴权预览入口读取 `storage` 文件
|
||||
|
||||
以后如果再新增类似后台产物目录,优先原则是:
|
||||
|
||||
@@ -90,6 +91,13 @@
|
||||
2. 默认不要放 `public`
|
||||
3. 只有明确需要前台公开访问的静态资源,才考虑进入 `public`
|
||||
|
||||
补充一条执行口径:
|
||||
|
||||
1. 如果后台页面需要“打开 HTML / JSON / CSV / prompt / payload”
|
||||
2. 优先保留文件在 `storage`
|
||||
3. 不要因为前台要看,就把私有运行产物重新搬回 `public`
|
||||
4. 应优先补 `后台鉴权读取接口 + Vue 打开工具函数`
|
||||
|
||||
### 3. 公共规则侧
|
||||
|
||||
已确立为全项目公共主线的内容:
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
3. 需要确认后台真实页面入口现在在 Vue,而不是 PHP 直出页
|
||||
4. 需要同步今天补上的 `Spider MD 异步任务消费器`、`GitHub 推送`、`主表一键复制MD` 这些后台能力
|
||||
5. 需要同步“所有后台运行产物、工作台快照、手工导入中间文件默认落 `storage`,不要再放 `public`”这条新口径
|
||||
6. 需要同步“Vue 后台打开 `storage` 产物时,走后台鉴权预览入口,不要再直接拼 `public` URL”这条新口径
|
||||
|
||||
### 1.3 AI文案与视频元数据公共规则
|
||||
|
||||
|
||||
Reference in New Issue
Block a user