feat: secure storage artifact preview
This commit is contained in:
@@ -31,6 +31,7 @@ Route::group("/system", function () {
|
|||||||
Route::get("/config/list", [SystemConfig::class, "getSystemConfigList"])->name("SystemConfig@getSystemConfigList");
|
Route::get("/config/list", [SystemConfig::class, "getSystemConfigList"])->name("SystemConfig@getSystemConfigList");
|
||||||
Route::post("/config/save", [SystemConfig::class, "saveSystemConfig"])->name("SystemConfig@saveSystemConfig");
|
Route::post("/config/save", [SystemConfig::class, "saveSystemConfig"])->name("SystemConfig@saveSystemConfig");
|
||||||
Route::post("/config/seo-ai-rules/reset", [SystemConfig::class, "resetSeoAiRules"])->name("SystemConfig@resetSeoAiRules");
|
Route::post("/config/seo-ai-rules/reset", [SystemConfig::class, "resetSeoAiRules"])->name("SystemConfig@resetSeoAiRules");
|
||||||
|
Route::get("/storage/file", [SystemConfig::class, "previewStorageFile"])->name("SystemConfig@previewStorageFile");
|
||||||
|
|
||||||
Route::get("/cjpz/list", [SystemConfig::class, "getCaiJiPeiZhiList"])->name("SystemConfig@getCaiJiPeiZhiList");
|
Route::get("/cjpz/list", [SystemConfig::class, "getCaiJiPeiZhiList"])->name("SystemConfig@getCaiJiPeiZhiList");
|
||||||
Route::post("/cjpz/save", [SystemConfig::class, "saveCaiJiPeiZhi"])->name("SystemConfig@saveCaiJiPeiZhi");
|
Route::post("/cjpz/save", [SystemConfig::class, "saveCaiJiPeiZhi"])->name("SystemConfig@saveCaiJiPeiZhi");
|
||||||
|
|||||||
@@ -25,6 +25,44 @@ use think\Response;
|
|||||||
|
|
||||||
class SystemConfig extends BaseController
|
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`
|
3. `video-metadata-missing-task-pool` 已迁到 `storage/video-metadata-missing/task-pool`
|
||||||
4. `domain-seo-external/manual-import` 已迁到 `storage/external-seo/manual-import`
|
4. `domain-seo-external/manual-import` 已迁到 `storage/external-seo/manual-import`
|
||||||
5. `domain-seo-external/search-console` 模板目录已迁到 `storage/external-seo/search-console`
|
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`
|
2. 默认不要放 `public`
|
||||||
3. 只有明确需要前台公开访问的静态资源,才考虑进入 `public`
|
3. 只有明确需要前台公开访问的静态资源,才考虑进入 `public`
|
||||||
|
|
||||||
|
补充一条执行口径:
|
||||||
|
|
||||||
|
1. 如果后台页面需要“打开 HTML / JSON / CSV / prompt / payload”
|
||||||
|
2. 优先保留文件在 `storage`
|
||||||
|
3. 不要因为前台要看,就把私有运行产物重新搬回 `public`
|
||||||
|
4. 应优先补 `后台鉴权读取接口 + Vue 打开工具函数`
|
||||||
|
|
||||||
### 3. 公共规则侧
|
### 3. 公共规则侧
|
||||||
|
|
||||||
已确立为全项目公共主线的内容:
|
已确立为全项目公共主线的内容:
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
3. 需要确认后台真实页面入口现在在 Vue,而不是 PHP 直出页
|
3. 需要确认后台真实页面入口现在在 Vue,而不是 PHP 直出页
|
||||||
4. 需要同步今天补上的 `Spider MD 异步任务消费器`、`GitHub 推送`、`主表一键复制MD` 这些后台能力
|
4. 需要同步今天补上的 `Spider MD 异步任务消费器`、`GitHub 推送`、`主表一键复制MD` 这些后台能力
|
||||||
5. 需要同步“所有后台运行产物、工作台快照、手工导入中间文件默认落 `storage`,不要再放 `public`”这条新口径
|
5. 需要同步“所有后台运行产物、工作台快照、手工导入中间文件默认落 `storage`,不要再放 `public`”这条新口径
|
||||||
|
6. 需要同步“Vue 后台打开 `storage` 产物时,走后台鉴权预览入口,不要再直接拼 `public` URL”这条新口径
|
||||||
|
|
||||||
### 1.3 AI文案与视频元数据公共规则
|
### 1.3 AI文案与视频元数据公共规则
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user