1.修复上传漏洞和SQL注入漏洞-增强登录验证功能
This commit is contained in:
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\admin\controller;
|
namespace app\admin\controller;
|
||||||
|
|
||||||
use ba\Terminal;
|
|
||||||
use ba\TableManager;
|
use ba\TableManager;
|
||||||
use support\think\Db;
|
use support\think\Db;
|
||||||
use app\admin\model\AdminLog;
|
use app\admin\model\AdminLog;
|
||||||
@@ -166,18 +165,6 @@ class Ajax extends Backend
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function changeTerminalConfig(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) return $response;
|
|
||||||
|
|
||||||
AdminLog::instance($request)->setTitle(__('Change terminal config'));
|
|
||||||
if (Terminal::changeTerminalConfig()) {
|
|
||||||
return $this->success();
|
|
||||||
}
|
|
||||||
return $this->error(__('Failed to modify the terminal configuration. Please modify the configuration file manually:%s', ['/config/terminal.php']));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function clearCache(Request $request): Response
|
public function clearCache(Request $request): Response
|
||||||
{
|
{
|
||||||
$response = $this->initializeBackend($request);
|
$response = $this->initializeBackend($request);
|
||||||
@@ -193,13 +180,4 @@ class Ajax extends Backend
|
|||||||
event_trigger('cacheClearAfter');
|
event_trigger('cacheClearAfter');
|
||||||
return $this->success(__('Cache cleaned~'));
|
return $this->success(__('Cache cleaned~'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function terminal(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) return $response;
|
|
||||||
|
|
||||||
(new Terminal())->exec();
|
|
||||||
return $this->success();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,14 +46,10 @@ class Index extends Backend
|
|||||||
'siteName' => get_sys_config('site_name'),
|
'siteName' => get_sys_config('site_name'),
|
||||||
'version' => get_sys_config('version'),
|
'version' => get_sys_config('version'),
|
||||||
'apiUrl' => $apiUrl,
|
'apiUrl' => $apiUrl,
|
||||||
'upload' => keys_to_camel_case(get_upload_config($request), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types']),
|
'upload' => keys_to_camel_case(get_upload_config($request), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types', 'forbidden_suffixes']),
|
||||||
'cdnUrl' => full_url(),
|
'cdnUrl' => full_url(),
|
||||||
'cdnUrlParams' => config('buildadmin.cdn_url_params'),
|
'cdnUrlParams' => config('buildadmin.cdn_url_params'),
|
||||||
],
|
],
|
||||||
'terminal' => [
|
|
||||||
'phpDevelopmentServer' => str_contains($_SERVER['SERVER_SOFTWARE'] ?? '', 'Development Server'),
|
|
||||||
'npmPackageManager' => config('terminal.npm_package_manager'),
|
|
||||||
]
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ return [
|
|||||||
'File uploaded successfully' => 'File uploaded successfully',
|
'File uploaded successfully' => 'File uploaded successfully',
|
||||||
'No files were uploaded' => 'No files were uploaded',
|
'No files were uploaded' => 'No files were uploaded',
|
||||||
'The uploaded file format is not allowed' => 'The uploaded file format is no allowance.',
|
'The uploaded file format is not allowed' => 'The uploaded file format is no allowance.',
|
||||||
|
'The uploaded file contains prohibited content' => 'The uploaded file contains prohibited content',
|
||||||
'The uploaded image file is not a valid image' => 'The uploaded image file is not a valid image',
|
'The uploaded image file is not a valid image' => 'The uploaded image file is not a valid image',
|
||||||
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'The uploaded file is too large (%sMiB), maximum file size:%sMiB',
|
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'The uploaded file is too large (%sMiB), maximum file size:%sMiB',
|
||||||
'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No files have been uploaded or the file size exceeds the server upload limit.',
|
'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No files have been uploaded or the file size exceeds the server upload limit.',
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ return [
|
|||||||
'File uploaded successfully' => '文件上传成功!',
|
'File uploaded successfully' => '文件上传成功!',
|
||||||
'No files were uploaded' => '没有文件被上传',
|
'No files were uploaded' => '没有文件被上传',
|
||||||
'The uploaded file format is not allowed' => '上传的文件格式未被允许',
|
'The uploaded file format is not allowed' => '上传的文件格式未被允许',
|
||||||
|
'The uploaded file contains prohibited content' => '上传的文件包含禁止的内容',
|
||||||
'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像',
|
'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像',
|
||||||
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => '上传的文件太大(%sM),最大文件大小:%sM',
|
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => '上传的文件太大(%sM),最大文件大小:%sM',
|
||||||
'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!',
|
'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!',
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class Index extends Frontend
|
|||||||
'siteName' => get_sys_config('site_name'),
|
'siteName' => get_sys_config('site_name'),
|
||||||
'version' => get_sys_config('version'),
|
'version' => get_sys_config('version'),
|
||||||
'cdnUrl' => full_url(),
|
'cdnUrl' => full_url(),
|
||||||
'upload' => keys_to_camel_case(get_upload_config($request), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types']),
|
'upload' => keys_to_camel_case(get_upload_config($request), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types', 'forbidden_suffixes']),
|
||||||
'recordNumber' => get_sys_config('record_number'),
|
'recordNumber' => get_sys_config('record_number'),
|
||||||
'cdnUrlParams' => config('buildadmin.cdn_url_params'),
|
'cdnUrlParams' => config('buildadmin.cdn_url_params'),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ return [
|
|||||||
'File uploaded successfully' => 'File uploaded successfully!',
|
'File uploaded successfully' => 'File uploaded successfully!',
|
||||||
'No files were uploaded' => 'No files were uploaded',
|
'No files were uploaded' => 'No files were uploaded',
|
||||||
'The uploaded file format is not allowed' => 'The uploaded file format is not allowed',
|
'The uploaded file format is not allowed' => 'The uploaded file format is not allowed',
|
||||||
|
'The uploaded file contains prohibited content' => 'The uploaded file contains prohibited content',
|
||||||
'The uploaded image file is not a valid image' => 'The uploaded image is not valid',
|
'The uploaded image file is not a valid image' => 'The uploaded image is not valid',
|
||||||
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'The file is too large (%sMiB), maximum: %sMiB',
|
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'The file is too large (%sMiB), maximum: %sMiB',
|
||||||
'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No file uploaded or size exceeds server limit',
|
'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No file uploaded or size exceeds server limit',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ return [
|
|||||||
'File uploaded successfully' => 'Fail berjaya dimuat naik!',
|
'File uploaded successfully' => 'Fail berjaya dimuat naik!',
|
||||||
'No files were uploaded' => 'Tiada fail dimuat naik',
|
'No files were uploaded' => 'Tiada fail dimuat naik',
|
||||||
'The uploaded file format is not allowed' => 'Format fail yang dimuat naik tidak dibenarkan',
|
'The uploaded file format is not allowed' => 'Format fail yang dimuat naik tidak dibenarkan',
|
||||||
|
'The uploaded file contains prohibited content' => 'Fail yang dimuat naik mengandungi kandungan yang dilarang',
|
||||||
'The uploaded image file is not a valid image' => 'Fail imej yang dimuat naik tidak sah',
|
'The uploaded image file is not a valid image' => 'Fail imej yang dimuat naik tidak sah',
|
||||||
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'Fail terlalu besar (%sM), saiz maksimum: %sM',
|
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'Fail terlalu besar (%sM), saiz maksimum: %sM',
|
||||||
'No files have been uploaded or the file size exceeds the upload limit of the server' => 'Tiada fail dimuat naik atau saiz melebihi had pelayan',
|
'No files have been uploaded or the file size exceeds the upload limit of the server' => 'Tiada fail dimuat naik atau saiz melebihi had pelayan',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ return [
|
|||||||
'File uploaded successfully' => '文件上传成功!',
|
'File uploaded successfully' => '文件上传成功!',
|
||||||
'No files were uploaded' => '没有文件被上传',
|
'No files were uploaded' => '没有文件被上传',
|
||||||
'The uploaded file format is not allowed' => '上传的文件格式未被允许',
|
'The uploaded file format is not allowed' => '上传的文件格式未被允许',
|
||||||
|
'The uploaded file contains prohibited content' => '上传的文件包含禁止的内容',
|
||||||
'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像',
|
'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像',
|
||||||
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => '上传的文件太大(%sM),最大文件大小:%sM',
|
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => '上传的文件太大(%sM),最大文件大小:%sM',
|
||||||
'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!',
|
'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!',
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ class Upload
|
|||||||
|
|
||||||
public function setDriver(string $driver): self
|
public function setDriver(string $driver): self
|
||||||
{
|
{
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $driver)) {
|
||||||
|
throw new InvalidArgumentException(__('Driver %s not supported', [$driver]));
|
||||||
|
}
|
||||||
|
$allowed = $this->checkConfig($this->config['allowed_drivers'] ?? 'local');
|
||||||
|
if ($allowed && !in_array(strtolower($driver), $allowed)) {
|
||||||
|
throw new InvalidArgumentException(__('Driver %s not supported', [$driver]));
|
||||||
|
}
|
||||||
$this->driver['name'] = $driver;
|
$this->driver['name'] = $driver;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -79,12 +86,17 @@ class Upload
|
|||||||
|
|
||||||
protected function resolveDriverClass(string $driver): string|false
|
protected function resolveDriverClass(string $driver): string|false
|
||||||
{
|
{
|
||||||
if ($this->driver['namespace'] || str_contains($driver, '\\')) {
|
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $driver)) {
|
||||||
$class = str_contains($driver, '\\') ? $driver : $this->driver['namespace'] . $this->studly($driver);
|
return false;
|
||||||
|
}
|
||||||
|
$allowed = $this->checkConfig($this->config['allowed_drivers'] ?? 'local');
|
||||||
|
if ($allowed && !in_array(strtolower($driver), $allowed)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$class = $this->driver['namespace'] . $this->studly($driver);
|
||||||
if (class_exists($class)) {
|
if (class_exists($class)) {
|
||||||
return $class;
|
return $class;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +193,10 @@ class Upload
|
|||||||
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->assertSafeFileName($this->fileInfo['name']);
|
||||||
|
$this->assertNoMaliciousContent();
|
||||||
|
$this->assertImageMime();
|
||||||
|
|
||||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $this->topic)) {
|
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $this->topic)) {
|
||||||
throw new RuntimeException(__('Topic format error'));
|
throw new RuntimeException(__('Topic format error'));
|
||||||
}
|
}
|
||||||
@@ -257,4 +273,95 @@ class Upload
|
|||||||
}
|
}
|
||||||
return $configItem ? explode(',', strtolower((string)$configItem)) : [];
|
return $configItem ? explode(',', strtolower((string)$configItem)) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getForbiddenSuffixes(): array
|
||||||
|
{
|
||||||
|
return $this->checkConfig($this->config['forbidden_suffixes'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertSafeFileName(string $fileName): void
|
||||||
|
{
|
||||||
|
$forbidden = $this->getForbiddenSuffixes();
|
||||||
|
if (!$forbidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('.', strtolower($fileName));
|
||||||
|
if (count($parts) < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($parts as $index => $part) {
|
||||||
|
if ($part === '') {
|
||||||
|
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
||||||
|
}
|
||||||
|
if ($index === count($parts) - 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (in_array($part, $forbidden)) {
|
||||||
|
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$suffix = $parts[count($parts) - 1];
|
||||||
|
if (in_array($suffix, $forbidden)) {
|
||||||
|
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertNoMaliciousContent(): void
|
||||||
|
{
|
||||||
|
$path = $this->file->getPathname();
|
||||||
|
if (!is_file($path) || !is_readable($path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($path, 'rb');
|
||||||
|
if (!$handle) {
|
||||||
|
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
||||||
|
}
|
||||||
|
$chunk = fread($handle, 8192);
|
||||||
|
fclose($handle);
|
||||||
|
if ($chunk === false || $chunk === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$patterns = [
|
||||||
|
'/<\?php/i',
|
||||||
|
'/<\?=/i',
|
||||||
|
'/<\?(?!xml)/i',
|
||||||
|
'/<%[@=]?/i',
|
||||||
|
'/<script\b/i',
|
||||||
|
'/\b(eval|assert|shell_exec|system|passthru|proc_open|popen|base64_decode)\s*\(/i',
|
||||||
|
];
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $chunk)) {
|
||||||
|
throw new RuntimeException(__('The uploaded file contains prohibited content'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertImageMime(): void
|
||||||
|
{
|
||||||
|
$imageSuffixes = ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'];
|
||||||
|
if (!in_array($this->fileInfo['suffix'], $imageSuffixes, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $this->file->getPathname();
|
||||||
|
if (!is_file($path)) {
|
||||||
|
throw new RuntimeException(__('The uploaded image file is not a valid image'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_exists('finfo_open')) {
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
if ($finfo) {
|
||||||
|
$detected = finfo_file($finfo, $path);
|
||||||
|
finfo_close($finfo);
|
||||||
|
if (!$detected || !str_starts_with(strtolower($detected), 'image/')) {
|
||||||
|
throw new RuntimeException(__('The uploaded image file is not a valid image'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,9 +99,7 @@ Route::get('/admin/ajax/getDatabaseConnectionList', [\app\admin\controller\Ajax:
|
|||||||
Route::get('/admin/ajax/getTablePk', [\app\admin\controller\Ajax::class, 'getTablePk']);
|
Route::get('/admin/ajax/getTablePk', [\app\admin\controller\Ajax::class, 'getTablePk']);
|
||||||
Route::get('/admin/ajax/getTableList', [\app\admin\controller\Ajax::class, 'getTableList']);
|
Route::get('/admin/ajax/getTableList', [\app\admin\controller\Ajax::class, 'getTableList']);
|
||||||
Route::get('/admin/ajax/getTableFieldList', [\app\admin\controller\Ajax::class, 'getTableFieldList']);
|
Route::get('/admin/ajax/getTableFieldList', [\app\admin\controller\Ajax::class, 'getTableFieldList']);
|
||||||
Route::post('/admin/ajax/changeTerminalConfig', [\app\admin\controller\Ajax::class, 'changeTerminalConfig']);
|
|
||||||
Route::post('/admin/ajax/clearCache', [\app\admin\controller\Ajax::class, 'clearCache']);
|
Route::post('/admin/ajax/clearCache', [\app\admin\controller\Ajax::class, 'clearCache']);
|
||||||
Route::add(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], '/admin/ajax/terminal', [\app\admin\controller\Ajax::class, 'terminal']);
|
|
||||||
|
|
||||||
// admin/auth/admin
|
// admin/auth/admin
|
||||||
Route::get('/admin/auth/admin/index', [\app\admin\controller\auth\Admin::class, 'index']);
|
Route::get('/admin/auth/admin/index', [\app\admin\controller\auth\Admin::class, 'index']);
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
// +----------------------------------------------------------------------
|
|
||||||
// | BuildAdmin - WEB终端配置
|
|
||||||
// | Webman 迁移:migrate 命令需根据实际迁移工具调整
|
|
||||||
// +----------------------------------------------------------------------
|
|
||||||
|
|
||||||
return [
|
|
||||||
'npm_package_manager' => 'pnpm',
|
|
||||||
'commands' => [
|
|
||||||
'migrate' => [
|
|
||||||
'run' => [
|
|
||||||
'cwd' => '',
|
|
||||||
'command' => 'php vendor/bin/phinx migrate',
|
|
||||||
'notes' => 'Start the database migration'
|
|
||||||
],
|
|
||||||
'rollback' => ['cwd' => '', 'command' => 'php vendor/bin/phinx rollback'],
|
|
||||||
'breakpoint' => ['cwd' => '', 'command' => 'php vendor/bin/phinx breakpoint'],
|
|
||||||
],
|
|
||||||
'version' => [
|
|
||||||
'npm' => 'npm -v',
|
|
||||||
'cnpm' => 'cnpm -v',
|
|
||||||
'yarn' => 'yarn -v',
|
|
||||||
'pnpm' => 'pnpm -v',
|
|
||||||
'node' => 'node -v',
|
|
||||||
],
|
|
||||||
'web-install' => [
|
|
||||||
'npm' => ['cwd' => 'web', 'command' => 'npm install'],
|
|
||||||
'cnpm' => ['cwd' => 'web', 'command' => 'cnpm install'],
|
|
||||||
'yarn' => ['cwd' => 'web', 'command' => 'yarn install'],
|
|
||||||
'pnpm' => ['cwd' => 'web', 'command' => 'pnpm install'],
|
|
||||||
'ni' => ['cwd' => 'web', 'command' => 'ni install'],
|
|
||||||
],
|
|
||||||
'nuxt-install' => [
|
|
||||||
'npm' => ['cwd' => 'web-nuxt', 'command' => 'npm install'],
|
|
||||||
'cnpm' => ['cwd' => 'web-nuxt', 'command' => 'cnpm install'],
|
|
||||||
'yarn' => ['cwd' => 'web-nuxt', 'command' => 'yarn install'],
|
|
||||||
'pnpm' => ['cwd' => 'web-nuxt', 'command' => 'pnpm install'],
|
|
||||||
'ni' => ['cwd' => 'web-nuxt', 'command' => 'ni install'],
|
|
||||||
],
|
|
||||||
'web-build' => [
|
|
||||||
'npm' => ['cwd' => 'web', 'command' => 'npm run build', 'notes' => 'Start executing the build command of the web project'],
|
|
||||||
'cnpm' => ['cwd' => 'web', 'command' => 'cnpm run build', 'notes' => 'Start executing the build command of the web project'],
|
|
||||||
'yarn' => ['cwd' => 'web', 'command' => 'yarn run build', 'notes' => 'Start executing the build command of the web project'],
|
|
||||||
'pnpm' => ['cwd' => 'web', 'command' => 'pnpm run build', 'notes' => 'Start executing the build command of the web project'],
|
|
||||||
'ni' => ['cwd' => 'web', 'command' => 'nr build', 'notes' => 'Start executing the build command of the web project'],
|
|
||||||
],
|
|
||||||
'set-npm-registry' => [
|
|
||||||
'npm' => 'npm config set registry https://registry.npmjs.org/ && npm config get registry',
|
|
||||||
'taobao' => 'npm config set registry https://registry.npmmirror.com/ && npm config get registry',
|
|
||||||
'tencent' => 'npm config set registry https://mirrors.cloud.tencent.com/npm/ && npm config get registry'
|
|
||||||
],
|
|
||||||
'set-composer-registry' => [
|
|
||||||
'composer' => 'composer config --unset repos.packagist',
|
|
||||||
'aliyun' => 'composer config -g repos.packagist composer https://mirrors.aliyun.com/composer/',
|
|
||||||
'tencent' => 'composer config -g repos.packagist composer https://mirrors.cloud.tencent.com/composer/',
|
|
||||||
'huawei' => 'composer config -g repos.packagist composer https://mirrors.huaweicloud.com/repository/php/',
|
|
||||||
'kkame' => 'composer config -g repos.packagist composer https://packagist.kr',
|
|
||||||
],
|
|
||||||
'npx' => [
|
|
||||||
'prettier' => ['cwd' => 'web', 'command' => 'npx prettier --write %s', 'notes' => 'Start formatting the web project code'],
|
|
||||||
],
|
|
||||||
'composer' => [
|
|
||||||
'update' => ['cwd' => '', 'command' => 'composer update --no-interaction', 'notes' => 'Start installing the composer dependencies']
|
|
||||||
],
|
|
||||||
'ping' => [
|
|
||||||
'baidu' => 'ping baidu.com',
|
|
||||||
'localhost' => 'ping 127.0.0.1 -n 6',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
];
|
|
||||||
@@ -16,6 +16,10 @@ return [
|
|||||||
* 2. 千万不要允许 php,php5,.htaccess,.user.ini 等可执行或配置文件
|
* 2. 千万不要允许 php,php5,.htaccess,.user.ini 等可执行或配置文件
|
||||||
* 3. 允许 pdf,ppt,docx 等可能含有脚本的文件时,请先从服务器配置此类文件直接下载而不是预览
|
* 3. 允许 pdf,ppt,docx 等可能含有脚本的文件时,请先从服务器配置此类文件直接下载而不是预览
|
||||||
*/
|
*/
|
||||||
'allowed_suffixes' => 'jpg,png,bmp,jpeg,gif,webp,zip,rar,wav,mp4,mp3',
|
'allowed_suffixes' => 'jpg,png,bmp,jpeg,gif,webp,wav,mp4,mp3',
|
||||||
'allowed_mime_types' => [],
|
'allowed_mime_types' => [],
|
||||||
|
// 禁止上传的后缀(含双后缀检测,如 shell.php.jpg)
|
||||||
|
'forbidden_suffixes' => 'php,php3,php4,php5,php7,php8,phtml,phar,htaccess,htpasswd,user.ini,asp,aspx,jsp,jspx,cgi,pl,sh,bat,cmd,exe,dll,vbs,ps1,com,shtml',
|
||||||
|
// 允许使用的上传驱动(禁止通过 driver 参数加载任意类)
|
||||||
|
'allowed_drivers' => 'local',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,404 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace ba;
|
|
||||||
|
|
||||||
use Throwable;
|
|
||||||
use support\Response;
|
|
||||||
use app\admin\library\Auth;
|
|
||||||
use app\common\library\token\TokenExpirationException;
|
|
||||||
use Workerman\Protocols\Http\ServerSentEvents;
|
|
||||||
use Workerman\Protocols\Http\Response as WorkermanResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WEB 终端(SSE 命令执行)
|
|
||||||
* 适配 Webman:使用 request()->connection、config()
|
|
||||||
*/
|
|
||||||
class Terminal
|
|
||||||
{
|
|
||||||
protected static ?Terminal $instance = null;
|
|
||||||
protected string $commandKey = '';
|
|
||||||
protected array $descriptorsPec = [];
|
|
||||||
protected $process = false;
|
|
||||||
protected array $pipes = [];
|
|
||||||
protected int $procStatusMark = 0;
|
|
||||||
protected array $procStatusData = [];
|
|
||||||
protected string $uuid = '';
|
|
||||||
protected string $extend = '';
|
|
||||||
protected string $outputFile = '';
|
|
||||||
protected string $outputContent = '';
|
|
||||||
protected static string $distDir = 'web' . DIRECTORY_SEPARATOR . 'dist';
|
|
||||||
|
|
||||||
protected array $flag = [
|
|
||||||
'link-success' => 'command-link-success',
|
|
||||||
'exec-success' => 'command-exec-success',
|
|
||||||
'exec-completed' => 'command-exec-completed',
|
|
||||||
'exec-error' => 'command-exec-error',
|
|
||||||
];
|
|
||||||
|
|
||||||
public static function instance(): Terminal
|
|
||||||
{
|
|
||||||
if (is_null(self::$instance)) {
|
|
||||||
self::$instance = new static();
|
|
||||||
}
|
|
||||||
return self::$instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$request = request();
|
|
||||||
$this->uuid = $request ? $request->param('uuid', '') : '';
|
|
||||||
$this->extend = $request ? $request->param('extend', '') : '';
|
|
||||||
|
|
||||||
$outputDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'terminal';
|
|
||||||
$this->outputFile = $outputDir . DIRECTORY_SEPARATOR . 'exec.log';
|
|
||||||
if (!is_dir($outputDir)) {
|
|
||||||
mkdir($outputDir, 0755, true);
|
|
||||||
}
|
|
||||||
file_put_contents($this->outputFile, '');
|
|
||||||
|
|
||||||
$this->descriptorsPec = [0 => ['pipe', 'r'], 1 => ['file', $this->outputFile, 'w'], 2 => ['file', $this->outputFile, 'w']];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getCommand(string $key): bool|array
|
|
||||||
{
|
|
||||||
if (!$key) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$commands = config('terminal.commands', []);
|
|
||||||
if (stripos($key, '.')) {
|
|
||||||
$key = explode('.', $key);
|
|
||||||
if (!array_key_exists($key[0], $commands) || !is_array($commands[$key[0]]) || !array_key_exists($key[1], $commands[$key[0]])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$command = $commands[$key[0]][$key[1]];
|
|
||||||
} else {
|
|
||||||
if (!array_key_exists($key, $commands)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$command = $commands[$key];
|
|
||||||
}
|
|
||||||
if (!is_array($command)) {
|
|
||||||
$command = [
|
|
||||||
'cwd' => root_path(),
|
|
||||||
'command' => $command,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
$cwd = $command['cwd'] ?? '';
|
|
||||||
$root = rtrim(root_path(), DIRECTORY_SEPARATOR . '/');
|
|
||||||
$command['cwd'] = $root . DIRECTORY_SEPARATOR . ltrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $cwd), DIRECTORY_SEPARATOR . '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
$request = request();
|
|
||||||
if ($request && str_contains($command['command'], '%')) {
|
|
||||||
$args = $request->param('extend', '');
|
|
||||||
$args = explode('~~', $args);
|
|
||||||
$args = array_map('escapeshellarg', $args);
|
|
||||||
|
|
||||||
array_unshift($args, $command['command']);
|
|
||||||
$command['command'] = call_user_func_array('sprintf', $args);
|
|
||||||
}
|
|
||||||
|
|
||||||
$command['cwd'] = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $command['cwd']);
|
|
||||||
if (DIRECTORY_SEPARATOR === '\\') {
|
|
||||||
$command['command'] = 'cmd /c "cd /d ' . str_replace('"', '""', $command['cwd']) . ' && ' . $command['command'] . '"';
|
|
||||||
$command['cwd'] = root_path();
|
|
||||||
}
|
|
||||||
return $command;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function exec(bool $authentication = true): void
|
|
||||||
{
|
|
||||||
$this->sendHeader();
|
|
||||||
|
|
||||||
while (ob_get_level()) {
|
|
||||||
ob_end_clean();
|
|
||||||
}
|
|
||||||
if (!ob_get_level()) ob_start();
|
|
||||||
|
|
||||||
$request = request();
|
|
||||||
$this->commandKey = $request ? $request->param('command', '') : '';
|
|
||||||
|
|
||||||
$command = self::getCommand($this->commandKey);
|
|
||||||
if (!$command) {
|
|
||||||
$this->execError('The command was not allowed to be executed', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($authentication) {
|
|
||||||
try {
|
|
||||||
$token = get_auth_token();
|
|
||||||
$auth = Auth::instance();
|
|
||||||
$auth->init($token);
|
|
||||||
|
|
||||||
if (!$auth->isLogin() || !$auth->isSuperAdmin()) {
|
|
||||||
$this->execError("You are not super administrator or not logged in", true);
|
|
||||||
}
|
|
||||||
} catch (TokenExpirationException) {
|
|
||||||
$this->execError(__('Token expiration'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->beforeExecution();
|
|
||||||
$this->outputFlag('link-success');
|
|
||||||
|
|
||||||
if (!empty($command['notes'])) {
|
|
||||||
$this->output('> ' . __($command['notes']), false);
|
|
||||||
}
|
|
||||||
$this->output('> ' . $command['command'], false);
|
|
||||||
|
|
||||||
$procEnv = $this->getProcEnv();
|
|
||||||
$this->process = proc_open($command['command'], $this->descriptorsPec, $this->pipes, $command['cwd'], $procEnv);
|
|
||||||
if (!is_resource($this->process)) {
|
|
||||||
$this->execError('Failed to execute', true);
|
|
||||||
}
|
|
||||||
if (isset($this->pipes[0])) {
|
|
||||||
fclose($this->pipes[0]);
|
|
||||||
unset($this->pipes[0]);
|
|
||||||
}
|
|
||||||
$lastHeartbeat = time();
|
|
||||||
$heartbeatInterval = 15;
|
|
||||||
while ($this->getProcStatus()) {
|
|
||||||
$contents = file_get_contents($this->outputFile);
|
|
||||||
if (strlen($contents) && $this->outputContent != $contents) {
|
|
||||||
$newOutput = str_replace($this->outputContent, '', $contents);
|
|
||||||
$this->checkOutput($contents, $newOutput);
|
|
||||||
if (strlen(trim($newOutput)) > 0) {
|
|
||||||
$this->output($newOutput);
|
|
||||||
$this->outputContent = $contents;
|
|
||||||
$lastHeartbeat = time();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->procStatusMark === 1 && (time() - $lastHeartbeat) >= $heartbeatInterval) {
|
|
||||||
$this->sendHeartbeat();
|
|
||||||
$lastHeartbeat = time();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->procStatusMark === 2) {
|
|
||||||
$this->output('exitCode: ' . $this->procStatusData['exitcode']);
|
|
||||||
if ($this->procStatusData['exitcode'] === 0) {
|
|
||||||
if ($this->successCallback()) {
|
|
||||||
$this->outputFlag('exec-success');
|
|
||||||
} else {
|
|
||||||
$this->output('Error: Command execution succeeded, but callback execution failed');
|
|
||||||
$this->outputFlag('exec-error');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->outputFlag('exec-error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usleep(500000);
|
|
||||||
}
|
|
||||||
foreach ($this->pipes as $pipe) {
|
|
||||||
fclose($pipe);
|
|
||||||
}
|
|
||||||
proc_close($this->process);
|
|
||||||
$this->outputFlag('exec-completed');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getProcEnv(): ?array
|
|
||||||
{
|
|
||||||
$env = getenv();
|
|
||||||
if (!is_array($env)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$env['CI'] = '1';
|
|
||||||
$env['FORCE_COLOR'] = '0';
|
|
||||||
return $env;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getProcStatus(): bool
|
|
||||||
{
|
|
||||||
$this->procStatusData = proc_get_status($this->process);
|
|
||||||
if ($this->procStatusData['running']) {
|
|
||||||
$this->procStatusMark = 1;
|
|
||||||
return true;
|
|
||||||
} elseif ($this->procStatusMark === 1) {
|
|
||||||
$this->procStatusMark = 2;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function output(string $data, bool $callback = true): void
|
|
||||||
{
|
|
||||||
$data = self::outputFilter($data);
|
|
||||||
$data = [
|
|
||||||
'data' => $data,
|
|
||||||
'uuid' => $this->uuid,
|
|
||||||
'extend' => $this->extend,
|
|
||||||
'key' => $this->commandKey,
|
|
||||||
];
|
|
||||||
$data = json_encode($data, JSON_UNESCAPED_UNICODE);
|
|
||||||
if ($data) {
|
|
||||||
$this->finalOutput($data);
|
|
||||||
if ($callback) $this->outputCallback($data);
|
|
||||||
@ob_flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkOutput(string $outputs, string $rowOutput): void
|
|
||||||
{
|
|
||||||
if (str_contains($rowOutput, '(Y/n)')) {
|
|
||||||
$this->execError('Interactive output detected, please manually execute the command to confirm the situation.', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function outputFlag(string $flag): void
|
|
||||||
{
|
|
||||||
$this->output($this->flag[$flag], false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sendHeartbeat(): void
|
|
||||||
{
|
|
||||||
$request = request();
|
|
||||||
$connection = $request && isset($request->connection) ? $request->connection : null;
|
|
||||||
if ($connection) {
|
|
||||||
$connection->send(new ServerSentEvents(['' => 'keepalive']));
|
|
||||||
}
|
|
||||||
@ob_flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function outputCallback($data): void
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function successCallback(): bool
|
|
||||||
{
|
|
||||||
if (stripos($this->commandKey, '.')) {
|
|
||||||
$commandKeyArr = explode('.', $this->commandKey);
|
|
||||||
$commandPKey = $commandKeyArr[0] ?? '';
|
|
||||||
} else {
|
|
||||||
$commandPKey = $this->commandKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($commandPKey == 'web-build') {
|
|
||||||
if (!self::mvDist()) {
|
|
||||||
$this->output('Build succeeded, but move file failed. Please operate manually.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function beforeExecution(): void
|
|
||||||
{
|
|
||||||
if ($this->commandKey == 'web-install.pnpm') {
|
|
||||||
@unlink(root_path() . 'web' . DIRECTORY_SEPARATOR . 'pnpm-lock.yaml');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function outputFilter($str): string
|
|
||||||
{
|
|
||||||
$str = trim($str);
|
|
||||||
$preg = '/\x1b\[(.*?)m/i';
|
|
||||||
$str = preg_replace($preg, '', $str);
|
|
||||||
$str = str_replace(["\r\n", "\r", "\n"], "\n", $str);
|
|
||||||
return mb_convert_encoding($str, 'UTF-8', 'UTF-8,GBK,GB2312,BIG5');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function execError($error, $break = false): void
|
|
||||||
{
|
|
||||||
$this->output('Error:' . $error);
|
|
||||||
$this->outputFlag('exec-error');
|
|
||||||
if ($break) $this->break();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function break(): void
|
|
||||||
{
|
|
||||||
throw new TerminalBreakException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getOutputFromProc($commandKey): bool|string
|
|
||||||
{
|
|
||||||
if (!function_exists('proc_open') || !function_exists('proc_close')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$command = self::getCommand($commandKey);
|
|
||||||
if (!$command) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$descriptorsPec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
|
||||||
$process = proc_open($command['command'], $descriptorsPec, $pipes, null, null);
|
|
||||||
if (is_resource($process)) {
|
|
||||||
$info = stream_get_contents($pipes[1]);
|
|
||||||
$info .= stream_get_contents($pipes[2]);
|
|
||||||
fclose($pipes[1]);
|
|
||||||
fclose($pipes[2]);
|
|
||||||
proc_close($process);
|
|
||||||
return self::outputFilter($info);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function mvDist(): bool
|
|
||||||
{
|
|
||||||
$distPath = root_path() . self::$distDir . DIRECTORY_SEPARATOR;
|
|
||||||
$indexHtmlPath = $distPath . 'index.html';
|
|
||||||
$assetsPath = $distPath . 'assets';
|
|
||||||
if (!file_exists($indexHtmlPath) || !file_exists($assetsPath)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$toIndexHtmlPath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'index.html';
|
|
||||||
$toAssetsPath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'assets';
|
|
||||||
@unlink($toIndexHtmlPath);
|
|
||||||
Filesystem::delDir($toAssetsPath);
|
|
||||||
|
|
||||||
if (rename($indexHtmlPath, $toIndexHtmlPath) && rename($assetsPath, $toAssetsPath)) {
|
|
||||||
Filesystem::delDir($distPath);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function changeTerminalConfig($config = []): bool
|
|
||||||
{
|
|
||||||
$request = request();
|
|
||||||
$oldPackageManager = config('terminal.npm_package_manager');
|
|
||||||
$newPackageManager = $request ? $request->post('manager', $config['manager'] ?? $oldPackageManager) : $oldPackageManager;
|
|
||||||
|
|
||||||
if ($oldPackageManager == $newPackageManager) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$buildConfigFile = config_path('terminal.php');
|
|
||||||
$buildConfigContent = @file_get_contents($buildConfigFile);
|
|
||||||
$buildConfigContent = preg_replace("/'npm_package_manager'(\s+)=>(\s+)'$oldPackageManager'/", "'npm_package_manager'\$1=>\$2'$newPackageManager'", $buildConfigContent);
|
|
||||||
$result = @file_put_contents($buildConfigFile, $buildConfigContent);
|
|
||||||
return (bool)$result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function finalOutput(string $data): void
|
|
||||||
{
|
|
||||||
$request = request();
|
|
||||||
$connection = $request && isset($request->connection) ? $request->connection : null;
|
|
||||||
if ($connection) {
|
|
||||||
$connection->send(new ServerSentEvents(['event' => 'message', 'data' => $data]));
|
|
||||||
} else {
|
|
||||||
echo 'data: ' . $data . "\n\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sendHeader(): void
|
|
||||||
{
|
|
||||||
$request = request();
|
|
||||||
$headers = array_merge($request->allowCrossDomainHeaders ?? [], [
|
|
||||||
'X-Accel-Buffering' => 'no',
|
|
||||||
'Content-Type' => 'text/event-stream',
|
|
||||||
'Cache-Control' => 'no-cache',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$connection = $request && isset($request->connection) ? $request->connection : null;
|
|
||||||
if ($connection) {
|
|
||||||
$connection->send(new WorkermanResponse(200, $headers, "\r\n"));
|
|
||||||
} else {
|
|
||||||
foreach ($headers as $name => $val) {
|
|
||||||
header($name . (!is_null($val) ? ':' . $val : ''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace ba;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 终端中断异常(用于 break() 停止 SSE 流)
|
|
||||||
*/
|
|
||||||
class TerminalBreakException extends \Exception
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,13 @@ namespace ba;
|
|||||||
*/
|
*/
|
||||||
class Version
|
class Version
|
||||||
{
|
{
|
||||||
|
private const VERSION_COMMANDS = [
|
||||||
|
'npm' => 'npm -v',
|
||||||
|
'cnpm' => 'cnpm -v',
|
||||||
|
'yarn' => 'yarn -v',
|
||||||
|
'pnpm' => 'pnpm -v',
|
||||||
|
'node' => 'node -v',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 比较两个版本号
|
* 比较两个版本号
|
||||||
@@ -84,14 +91,13 @@ class Version
|
|||||||
*/
|
*/
|
||||||
public static function getCnpmVersion(): string
|
public static function getCnpmVersion(): string
|
||||||
{
|
{
|
||||||
$execOut = Terminal::getOutputFromProc('version.cnpm');
|
$execOut = self::getOutputFromProc('cnpm');
|
||||||
if ($execOut) {
|
if ($execOut) {
|
||||||
$preg = '/cnpm@(.+?) \(/is';
|
$preg = '/cnpm@(.+?) \(/is';
|
||||||
preg_match($preg, $execOut, $result);
|
preg_match($preg, $execOut, $result);
|
||||||
return $result[1] ?? '';
|
return $result[1] ?? '';
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,9 +109,16 @@ class Version
|
|||||||
{
|
{
|
||||||
if ($name == 'cnpm') {
|
if ($name == 'cnpm') {
|
||||||
return self::getCnpmVersion();
|
return self::getCnpmVersion();
|
||||||
} elseif (in_array($name, ['npm', 'yarn', 'pnpm', 'node'])) {
|
}
|
||||||
$execOut = Terminal::getOutputFromProc('version.' . $name);
|
if (!in_array($name, ['npm', 'yarn', 'pnpm', 'node'])) {
|
||||||
if ($execOut) {
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$execOut = self::getOutputFromProc($name);
|
||||||
|
if (!$execOut) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
if (strripos($execOut, 'npm WARN') !== false) {
|
if (strripos($execOut, 'npm WARN') !== false) {
|
||||||
$preg = '/\d+(\.\d+){0,2}/';
|
$preg = '/\d+(\.\d+){0,2}/';
|
||||||
preg_match($preg, $execOut, $matches);
|
preg_match($preg, $execOut, $matches);
|
||||||
@@ -113,16 +126,52 @@ class Version
|
|||||||
return $matches[0];
|
return $matches[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$execOut = preg_split('/\r\n|\r|\n/', $execOut);
|
$execOut = preg_split('/\r\n|\r|\n/', $execOut);
|
||||||
for ($i = 0; $i < 2; $i++) {
|
for ($i = 0; $i < 2; $i++) {
|
||||||
if (isset($execOut[$i]) && self::checkDigitalVersion($execOut[$i])) {
|
if (isset($execOut[$i]) && self::checkDigitalVersion($execOut[$i])) {
|
||||||
return $execOut[$i];
|
return $execOut[$i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function getOutputFromProc(string $name): bool|string
|
||||||
|
{
|
||||||
|
if (!function_exists('proc_open') || !function_exists('proc_close')) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$command = self::VERSION_COMMANDS[$name] ?? '';
|
||||||
|
if (!$command) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DIRECTORY_SEPARATOR === '\\') {
|
||||||
|
$command = 'cmd /c "' . $command . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
$descriptorsPec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
||||||
|
$process = proc_open($command, $descriptorsPec, $pipes, root_path(), null);
|
||||||
|
if (!is_resource($process)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$info = stream_get_contents($pipes[1]);
|
||||||
|
$info .= stream_get_contents($pipes[2]);
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
proc_close($process);
|
||||||
|
|
||||||
|
return self::outputFilter($info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function outputFilter($str): string
|
||||||
|
{
|
||||||
|
$str = trim($str);
|
||||||
|
$preg = '/\x1b\[(.*?)m/i';
|
||||||
|
$str = preg_replace($preg, '', $str);
|
||||||
|
$str = str_replace(["\r\n", "\r", "\n"], "\n", $str);
|
||||||
|
return mb_convert_encoding($str, 'UTF-8', 'UTF-8,GBK,GB2312,BIG5');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ export const getTablePkUrl = '/admin/ajax/getTablePk'
|
|||||||
export const getTableListUrl = '/admin/ajax/getTableList'
|
export const getTableListUrl = '/admin/ajax/getTableList'
|
||||||
export const getTableFieldListUrl = '/admin/ajax/getTableFieldList'
|
export const getTableFieldListUrl = '/admin/ajax/getTableFieldList'
|
||||||
export const getDatabaseConnectionListUrl = '/admin/ajax/getDatabaseConnectionList'
|
export const getDatabaseConnectionListUrl = '/admin/ajax/getDatabaseConnectionList'
|
||||||
export const terminalUrl = adminBaseRoutePath + '/ajax/terminal'
|
|
||||||
export const changeTerminalConfigUrl = '/admin/ajax/changeTerminalConfig'
|
|
||||||
export const clearCacheUrl = '/admin/ajax/clearCache'
|
export const clearCacheUrl = '/admin/ajax/clearCache'
|
||||||
|
|
||||||
// 公共
|
// 公共
|
||||||
@@ -177,27 +175,6 @@ export function postClearCache(type: string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建命令执行窗口url
|
|
||||||
*/
|
|
||||||
export function buildTerminalUrl(commandKey: string, uuid: string, extend: string) {
|
|
||||||
const adminInfo = useAdminInfo()
|
|
||||||
return (
|
|
||||||
getUrl() + terminalUrl + '?command=' + commandKey + '&uuid=' + uuid + '&extend=' + extend + '&batoken=' + adminInfo.getToken() + '&server=1'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 请求修改终端配置
|
|
||||||
*/
|
|
||||||
export function postChangeTerminalConfig(data: { manager?: string; port?: string }) {
|
|
||||||
return createAxios({
|
|
||||||
url: changeTerminalConfigUrl,
|
|
||||||
method: 'POST',
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 远程下拉框数据获取
|
* 远程下拉框数据获取
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,439 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog v-model="terminal.state.show" :title="t('terminal.Terminal')" class="ba-terminal-dialog main-dialog">
|
|
||||||
<el-scrollbar ref="terminalScrollbarRef" :max-height="500" class="terminal-scrollbar">
|
|
||||||
<el-alert
|
|
||||||
class="terminal-warning-alert"
|
|
||||||
v-if="!terminal.state.phpDevelopmentServer"
|
|
||||||
:title="t('terminal.The current terminal is not running under the installation service, and some commands may not be executed')"
|
|
||||||
type="error"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-timeline class="terminal-timeline" v-if="terminal.state.taskList.length">
|
|
||||||
<el-timeline-item
|
|
||||||
v-for="(item, idx) in terminal.state.taskList"
|
|
||||||
:key="idx"
|
|
||||||
class="task-item"
|
|
||||||
:class="'task-status-' + item.status"
|
|
||||||
:type="getTaskStatus(item.status)['statusType']"
|
|
||||||
center
|
|
||||||
:timestamp="item.createTime"
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<el-card>
|
|
||||||
<div>
|
|
||||||
<el-tag :type="getTaskStatus(item.status)['statusType']">{{ getTaskStatus(item.status)['statusText'] }}</el-tag>
|
|
||||||
<el-tag
|
|
||||||
class="block-on-failure-tag"
|
|
||||||
v-if="(item.status == taskStatus.Failed || item.status == taskStatus.Unknown) && item.blockOnFailure"
|
|
||||||
type="warning"
|
|
||||||
>
|
|
||||||
{{ t('terminal.Failure to execute this command will block the execution of the queue') }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag
|
|
||||||
class="block-on-failure-tag"
|
|
||||||
v-if="item.status == taskStatus.Executing || item.status == taskStatus.Connecting"
|
|
||||||
type="danger"
|
|
||||||
>
|
|
||||||
{{ t('terminal.Do not refresh the browser') }}
|
|
||||||
</el-tag>
|
|
||||||
<span class="command">{{ item.command }}</span>
|
|
||||||
<div class="task-opt">
|
|
||||||
<el-button
|
|
||||||
:title="t('Retry')"
|
|
||||||
v-if="item.status == taskStatus.Failed || item.status == taskStatus.Unknown"
|
|
||||||
size="small"
|
|
||||||
v-blur
|
|
||||||
type="warning"
|
|
||||||
icon="el-icon-RefreshRight"
|
|
||||||
circle
|
|
||||||
@click="terminal.retryTask(idx)"
|
|
||||||
/>
|
|
||||||
<el-button
|
|
||||||
@click="terminal.delTask(idx)"
|
|
||||||
:title="t('Delete')"
|
|
||||||
size="small"
|
|
||||||
v-blur
|
|
||||||
type="danger"
|
|
||||||
icon="el-icon-Delete"
|
|
||||||
circle
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="item.status != taskStatus.Waiting">
|
|
||||||
<div
|
|
||||||
v-if="item.status != taskStatus.Connecting && item.status != taskStatus.Executing"
|
|
||||||
@click="terminal.setTaskShowMessage(idx)"
|
|
||||||
class="toggle-message-display"
|
|
||||||
>
|
|
||||||
<span>{{ t('terminal.Command run log') }}</span>
|
|
||||||
<Icon :name="item.showMessage ? 'el-icon-ArrowUp' : 'el-icon-ArrowDown'" size="16" color="#909399" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
item.status == taskStatus.Connecting ||
|
|
||||||
item.status == taskStatus.Executing ||
|
|
||||||
(item.status > taskStatus.Executing && item.showMessage)
|
|
||||||
"
|
|
||||||
class="exec-message"
|
|
||||||
:class="'exec-message-' + item.uuid"
|
|
||||||
>
|
|
||||||
<pre v-for="(msg, index) in item.message" :key="index" class="message-item">{{ msg }}</pre>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-card>
|
|
||||||
</el-timeline-item>
|
|
||||||
</el-timeline>
|
|
||||||
<el-empty v-else :image-size="80" :description="t('terminal.No mission yet')" />
|
|
||||||
</el-scrollbar>
|
|
||||||
|
|
||||||
<div class="terminal-buttons">
|
|
||||||
<el-button class="terminal-menu-item" icon="el-icon-MagicStick" v-blur @click="addTerminalTask('test', true, false)">
|
|
||||||
{{ t('terminal.Test command') }}
|
|
||||||
</el-button>
|
|
||||||
<el-dropdown class="terminal-menu-item">
|
|
||||||
<el-button icon="el-icon-Download" v-blur>
|
|
||||||
{{ t('terminal.Install dependent packages') }}
|
|
||||||
</el-button>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item @click="addTerminalTask('web-install', true)" v-if="terminal.state.packageManager != 'none'">
|
|
||||||
{{ terminal.state.packageManager }} run install
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item @click="addTerminalTask('composer.update', false)">composer update</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
<el-button class="terminal-menu-item" icon="el-icon-Sell" v-blur @click="webBuild()">{{ t('terminal.Republish') }}</el-button>
|
|
||||||
<el-button class="terminal-menu-item" icon="el-icon-Delete" v-blur @click="terminal.clearSuccessTask()">
|
|
||||||
{{ t('terminal.Clean up task list') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button class="terminal-menu-item" icon="el-icon-Tools" v-blur @click="terminal.toggleConfigDialog()">
|
|
||||||
{{ t('terminal.Terminal settings') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
@close="terminal.toggleConfigDialog(false)"
|
|
||||||
:model-value="terminal.state.showConfig"
|
|
||||||
class="ba-terminal-dialog"
|
|
||||||
:title="t('terminal.Terminal settings')"
|
|
||||||
>
|
|
||||||
<el-form label-position="left" label-width="140">
|
|
||||||
<FormItem
|
|
||||||
:label="'NPM ' + t('terminal.Source')"
|
|
||||||
:model-value="terminal.state.npmRegistry"
|
|
||||||
:key="terminal.state.npmRegistry"
|
|
||||||
v-loading="state.registryLoading && state.registryLoadingType == 'npm'"
|
|
||||||
type="select"
|
|
||||||
:input-attr="{
|
|
||||||
border: true,
|
|
||||||
content: getSourceContent('npm'),
|
|
||||||
teleported: false,
|
|
||||||
onChange: (val: string) => changeRegistry(val, 'npm'),
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="'Composer ' + t('terminal.Source')"
|
|
||||||
:model-value="terminal.state.composerRegistry"
|
|
||||||
:key="terminal.state.composerRegistry"
|
|
||||||
v-loading="state.registryLoading && state.registryLoadingType == 'composer'"
|
|
||||||
type="select"
|
|
||||||
:input-attr="{
|
|
||||||
border: true,
|
|
||||||
content: getSourceContent('composer'),
|
|
||||||
teleported: false,
|
|
||||||
onChange: (val: string) => changeRegistry(val, 'composer'),
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="t('terminal.NPM package manager')"
|
|
||||||
:model-value="terminal.state.packageManager"
|
|
||||||
v-loading="state.packageManagerLoading"
|
|
||||||
type="select"
|
|
||||||
:input-attr="{
|
|
||||||
border: true,
|
|
||||||
content: { npm: 'NPM', cnpm: 'CNPM', pnpm: 'PNPM', yarn: 'YARN', ni: 'NI', none: t('terminal.Manual execution') },
|
|
||||||
teleported: false,
|
|
||||||
onChange: (val: string) => changePackageManager(val),
|
|
||||||
}"
|
|
||||||
:tip="t('terminal.NPM package manager tip')"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="t('terminal.Clear successful task')"
|
|
||||||
:model-value="terminal.state.automaticCleanupTask"
|
|
||||||
type="radio"
|
|
||||||
:input-attr="{
|
|
||||||
border: true,
|
|
||||||
content: { '0': t('Disable'), '1': t('Enable') },
|
|
||||||
onChange: terminal.changeAutomaticCleanupTask,
|
|
||||||
}"
|
|
||||||
:tip="t('terminal.Clear successful task tip')"
|
|
||||||
/>
|
|
||||||
</el-form>
|
|
||||||
<div class="config-buttons">
|
|
||||||
<el-button @click="terminal.toggleConfigDialog(false)">{{ t('terminal.Back to terminal') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { TimelineItemProps } from 'element-plus'
|
|
||||||
import { ElMessageBox, ElScrollbar } from 'element-plus'
|
|
||||||
import { nextTick, onMounted, reactive, useTemplateRef } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { postChangeTerminalConfig } from '/@/api/common'
|
|
||||||
import FormItem from '/@/components/formItem/index.vue'
|
|
||||||
import { taskStatus } from '/@/stores/constant/terminalTaskStatus'
|
|
||||||
import { useTerminal } from '/@/stores/terminal'
|
|
||||||
import { changeListenDirtyFileSwitch } from '/@/utils/vite'
|
|
||||||
|
|
||||||
type SourceType = 'npm' | 'composer'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const terminal = useTerminal()
|
|
||||||
const terminalScrollbarRef = useTemplateRef('terminalScrollbarRef')
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
registryLoading: false,
|
|
||||||
registryLoadingType: 'npm',
|
|
||||||
packageManagerLoading: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const getTaskStatus = (status: number) => {
|
|
||||||
let statusText = t('terminal.unknown')
|
|
||||||
let statusType: TimelineItemProps['type'] = 'info'
|
|
||||||
switch (status) {
|
|
||||||
case taskStatus.Waiting:
|
|
||||||
statusText = t('terminal.Waiting for execution')
|
|
||||||
statusType = 'info'
|
|
||||||
break
|
|
||||||
case taskStatus.Connecting:
|
|
||||||
statusText = t('terminal.Connecting')
|
|
||||||
statusType = 'warning'
|
|
||||||
break
|
|
||||||
case taskStatus.Executing:
|
|
||||||
statusText = t('terminal.Executing')
|
|
||||||
statusType = 'warning'
|
|
||||||
break
|
|
||||||
case taskStatus.Success:
|
|
||||||
statusText = t('terminal.Successful execution')
|
|
||||||
statusType = 'success'
|
|
||||||
break
|
|
||||||
case taskStatus.Failed:
|
|
||||||
statusText = t('terminal.Execution failed')
|
|
||||||
statusType = 'danger'
|
|
||||||
break
|
|
||||||
case taskStatus.Unknown:
|
|
||||||
statusText = t('terminal.Unknown execution result')
|
|
||||||
statusType = 'danger'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
statusText: statusText,
|
|
||||||
statusType: statusType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addTerminalTask = (command: string, pm: boolean, blockOnFailure = true, extend = '', callback: Function = () => {}) => {
|
|
||||||
if (pm) {
|
|
||||||
terminal.addTaskPM(command, blockOnFailure, extend, callback)
|
|
||||||
} else {
|
|
||||||
terminal.addTask(command, blockOnFailure, extend, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 任务列表滚动条滚动到底部
|
|
||||||
nextTick(() => {
|
|
||||||
if (terminalScrollbarRef.value && terminalScrollbarRef.value.wrapRef) {
|
|
||||||
terminalScrollbarRef.value.setScrollTop(terminalScrollbarRef.value.wrapRef.scrollHeight)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const webBuild = () => {
|
|
||||||
ElMessageBox.confirm(t('terminal.Are you sure you want to republish?'), t('Reminder'), {
|
|
||||||
confirmButtonText: t('Confirm'),
|
|
||||||
cancelButtonText: t('Cancel'),
|
|
||||||
type: 'warning',
|
|
||||||
}).then(() => {
|
|
||||||
changeListenDirtyFileSwitch(false)
|
|
||||||
addTerminalTask('web-build', true, true, '', () => {
|
|
||||||
changeListenDirtyFileSwitch(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const changePackageManager = (val: string) => {
|
|
||||||
state.packageManagerLoading = true
|
|
||||||
postChangeTerminalConfig({ manager: val })
|
|
||||||
.then((res) => {
|
|
||||||
if (res.code == 1) {
|
|
||||||
terminal.changePackageManager(val)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
state.packageManagerLoading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeRegistry = (val: string, type: SourceType) => {
|
|
||||||
const oldVal = type == 'npm' ? terminal.state.npmRegistry : terminal.state.composerRegistry
|
|
||||||
terminal.changeRegistry(val, type)
|
|
||||||
state.registryLoading = true
|
|
||||||
state.registryLoadingType = type
|
|
||||||
terminal.addTask(`set-${type}-registry.${val}`, false, '', (res: taskStatus) => {
|
|
||||||
state.registryLoading = false
|
|
||||||
if (res == taskStatus.Failed || res == taskStatus.Unknown) {
|
|
||||||
ElMessageBox.confirm(t('terminal.Failed to modify the source command, Please try again manually'), t('Reminder'), {
|
|
||||||
confirmButtonText: t('Confirm'),
|
|
||||||
showCancelButton: false,
|
|
||||||
type: 'warning',
|
|
||||||
}).then(() => {
|
|
||||||
terminal.changeRegistry(oldVal, type)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSourceContent = (type: SourceType) => {
|
|
||||||
let content: anyObj = {}
|
|
||||||
if (type == 'npm') {
|
|
||||||
content = { npm: 'npm', taobao: 'taobao', tencent: 'tencent' }
|
|
||||||
} else if (type == 'composer') {
|
|
||||||
content = {
|
|
||||||
composer: 'composer',
|
|
||||||
huawei: 'huawei',
|
|
||||||
aliyun: 'aliyun',
|
|
||||||
tencent: 'tencent',
|
|
||||||
kkame: 'kkame',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果值为 unknown,则 unknown 选项
|
|
||||||
if (terminal.state[type == 'npm' ? 'npmRegistry' : 'composerRegistry'] == 'unknown') {
|
|
||||||
content.unknown = t('Unknown')
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
terminal.init()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.terminal-warning-alert {
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
}
|
|
||||||
.terminal-timeline {
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
.command {
|
|
||||||
font-size: var(--el-font-size-large);
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.exec-message {
|
|
||||||
color: var(--ba-bg-color-overlay);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
padding: 6px;
|
|
||||||
background-color: #424251;
|
|
||||||
margin-top: 10px;
|
|
||||||
min-height: 30px;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow: auto;
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: #c8c9cc;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: none;
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: var(--ba-bg-color);
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
&::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #909399;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@supports not (selector(::-webkit-scrollbar)) {
|
|
||||||
.exec-message {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: #c8c9cc #eaeaea;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toggle-message-display {
|
|
||||||
padding-top: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.task-opt {
|
|
||||||
display: none;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.task-item.task-status-0:hover,
|
|
||||||
.task-item.task-status-3:hover,
|
|
||||||
.task-item.task-status-4:hover,
|
|
||||||
.task-item.task-status-5:hover {
|
|
||||||
.task-opt {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.block-on-failure-tag {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.terminal-menu-item {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.terminal-menu-item + .terminal-menu-item {
|
|
||||||
margin-left: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.terminal-buttons {
|
|
||||||
display: block;
|
|
||||||
width: fit-content;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
.config-buttons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-top: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
:deep(.main-dialog) {
|
|
||||||
--el-dialog-padding-primary: 16px 16px 0 16px;
|
|
||||||
.el-dialog__body {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.ba-terminal-dialog) {
|
|
||||||
--el-dialog-width: 46% !important;
|
|
||||||
.el-loading-spinner {
|
|
||||||
--el-loading-spinner-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
:deep(.ba-terminal-dialog) {
|
|
||||||
--el-dialog-width: 80% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 540px) {
|
|
||||||
:deep(.ba-terminal-dialog) {
|
|
||||||
--el-dialog-width: 94% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -71,13 +71,6 @@
|
|||||||
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" v-else name="el-icon-FullScreen" size="18" />
|
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" v-else name="el-icon-FullScreen" size="18" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 终端 - 仅超管 -->
|
|
||||||
<div v-if="adminInfo.super" @click="terminal.toggle()" class="nav-menu-item pt2">
|
|
||||||
<el-badge :is-dot="terminal.state.showDot">
|
|
||||||
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="local-terminal" size="26" />
|
|
||||||
</el-badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 清理缓存 - 仅超管 -->
|
<!-- 清理缓存 - 仅超管 -->
|
||||||
<el-dropdown
|
<el-dropdown
|
||||||
v-if="adminInfo.super"
|
v-if="adminInfo.super"
|
||||||
@@ -140,7 +133,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Config />
|
<Config />
|
||||||
<Terminal />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -152,14 +144,12 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import Config from './config.vue'
|
import Config from './config.vue'
|
||||||
import { logout } from '/@/api/backend/index'
|
import { logout } from '/@/api/backend/index'
|
||||||
import { postClearCache } from '/@/api/common'
|
import { postClearCache } from '/@/api/common'
|
||||||
import Terminal from '/@/components/terminal/index.vue'
|
|
||||||
import { editDefaultLang } from '/@/lang'
|
import { editDefaultLang } from '/@/lang'
|
||||||
import router from '/@/router'
|
import router from '/@/router'
|
||||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||||
import { useConfig } from '/@/stores/config'
|
import { useConfig } from '/@/stores/config'
|
||||||
import { ADMIN_INFO, BA_ACCOUNT } from '/@/stores/constant/cacheKey'
|
import { ADMIN_INFO, BA_ACCOUNT } from '/@/stores/constant/cacheKey'
|
||||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||||
import { useTerminal } from '/@/stores/terminal'
|
|
||||||
import { fullUrl, timeFormat } from '/@/utils/common'
|
import { fullUrl, timeFormat } from '/@/utils/common'
|
||||||
import { routePush } from '/@/utils/router'
|
import { routePush } from '/@/utils/router'
|
||||||
import { Local, Session } from '/@/utils/storage'
|
import { Local, Session } from '/@/utils/storage'
|
||||||
@@ -169,7 +159,6 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
const adminInfo = useAdminInfo()
|
const adminInfo = useAdminInfo()
|
||||||
const configStore = useConfig()
|
const configStore = useConfig()
|
||||||
const terminal = useTerminal()
|
|
||||||
const siteConfig = useSiteConfig()
|
const siteConfig = useSiteConfig()
|
||||||
const reloadHotServerPopover = useTemplateRef('reloadHotServerPopover')
|
const reloadHotServerPopover = useTemplateRef('reloadHotServerPopover')
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import { useConfig } from '/@/stores/config'
|
import { useConfig } from '/@/stores/config'
|
||||||
import { useNavTabs } from '/@/stores/navTabs'
|
import { useNavTabs } from '/@/stores/navTabs'
|
||||||
import { useTerminal } from '/@/stores/terminal'
|
|
||||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
@@ -29,7 +28,6 @@ defineOptions({
|
|||||||
components: { Default, Classic, Streamline, Double },
|
components: { Default, Classic, Streamline, Double },
|
||||||
})
|
})
|
||||||
|
|
||||||
const terminal = useTerminal()
|
|
||||||
const navTabs = useNavTabs()
|
const navTabs = useNavTabs()
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -59,8 +57,6 @@ const init = () => {
|
|||||||
index()
|
index()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
siteConfig.dataFill(res.data.siteConfig)
|
siteConfig.dataFill(res.data.siteConfig)
|
||||||
terminal.changePackageManager(res.data.terminal.npmPackageManager)
|
|
||||||
terminal.changePHPDevelopmentServer(res.data.terminal.phpDevelopmentServer)
|
|
||||||
siteConfig.setInitialize(true)
|
siteConfig.setInitialize(true)
|
||||||
|
|
||||||
if (!isEmpty(res.data.adminInfo)) {
|
if (!isEmpty(res.data.adminInfo)) {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export const ADMIN_INFO = 'adminInfo'
|
|||||||
export const STORE_CONFIG = 'storeConfig_v2'
|
export const STORE_CONFIG = 'storeConfig_v2'
|
||||||
// 后台标签页
|
// 后台标签页
|
||||||
export const STORE_TAB_VIEW_CONFIG = 'storeTabViewConfig'
|
export const STORE_TAB_VIEW_CONFIG = 'storeTabViewConfig'
|
||||||
// 终端
|
|
||||||
export const STORE_TERMINAL = 'storeTerminal'
|
|
||||||
|
|
||||||
// 工作时间
|
// 工作时间
|
||||||
export const WORKING_TIME = 'workingTime'
|
export const WORKING_TIME = 'workingTime'
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export const enum taskStatus {
|
|
||||||
Waiting,
|
|
||||||
Connecting,
|
|
||||||
Executing,
|
|
||||||
Success,
|
|
||||||
Failed,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
@@ -134,48 +134,6 @@ export interface UserInfo {
|
|||||||
refresh_token: string
|
refresh_token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskItem {
|
|
||||||
// 任务唯一标识
|
|
||||||
uuid: string
|
|
||||||
// 创建时间
|
|
||||||
createTime: string
|
|
||||||
// 状态
|
|
||||||
status: number
|
|
||||||
// 命令
|
|
||||||
command: string
|
|
||||||
// 命令执行日志
|
|
||||||
message: string[]
|
|
||||||
// 显示命令执行日志
|
|
||||||
showMessage: boolean
|
|
||||||
// 失败阻断后续命令执行
|
|
||||||
blockOnFailure: boolean
|
|
||||||
// 扩展信息,自动发送到后台
|
|
||||||
extend: string
|
|
||||||
// 执行结果回调
|
|
||||||
callback: Function
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Terminal {
|
|
||||||
// 显示终端窗口
|
|
||||||
show: boolean
|
|
||||||
// 在后台终端按钮上显示一个红点
|
|
||||||
showDot: boolean
|
|
||||||
// 任务列表
|
|
||||||
taskList: TaskItem[]
|
|
||||||
// 包管理器
|
|
||||||
packageManager: string
|
|
||||||
// 显示终端设置窗口
|
|
||||||
showConfig: boolean
|
|
||||||
// 开始任务时自动清理已完成任务
|
|
||||||
automaticCleanupTask: string
|
|
||||||
// PHP 开发服务环境
|
|
||||||
phpDevelopmentServer: boolean
|
|
||||||
// NPM 源
|
|
||||||
npmRegistry: string
|
|
||||||
// composer 源
|
|
||||||
composerRegistry: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SiteConfig {
|
export interface SiteConfig {
|
||||||
// 站点名称
|
// 站点名称
|
||||||
siteName: string
|
siteName: string
|
||||||
|
|||||||
@@ -1,292 +0,0 @@
|
|||||||
import { ElNotification } from 'element-plus'
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import { nextTick, reactive } from 'vue'
|
|
||||||
import { buildTerminalUrl } from '/@/api/common'
|
|
||||||
import { i18n } from '/@/lang/index'
|
|
||||||
import { STORE_TERMINAL } from '/@/stores/constant/cacheKey'
|
|
||||||
import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
|
|
||||||
import { taskStatus } from '/@/stores/constant/terminalTaskStatus'
|
|
||||||
import type { Terminal } from '/@/stores/interface/index'
|
|
||||||
import { timeFormat } from '/@/utils/common'
|
|
||||||
import { uuid } from '/@/utils/random'
|
|
||||||
import { closeHotUpdate, openHotUpdate } from '/@/utils/vite'
|
|
||||||
|
|
||||||
export const useTerminal = defineStore(
|
|
||||||
'terminal',
|
|
||||||
() => {
|
|
||||||
const state: Terminal = reactive({
|
|
||||||
show: false,
|
|
||||||
showDot: false,
|
|
||||||
taskList: [],
|
|
||||||
packageManager: 'pnpm',
|
|
||||||
showConfig: false,
|
|
||||||
automaticCleanupTask: '1',
|
|
||||||
phpDevelopmentServer: false,
|
|
||||||
npmRegistry: 'unknown',
|
|
||||||
composerRegistry: 'unknown',
|
|
||||||
})
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
for (const key in state.taskList) {
|
|
||||||
if (state.taskList[key].status == taskStatus.Connecting || state.taskList[key].status == taskStatus.Executing) {
|
|
||||||
state.taskList[key].status = taskStatus.Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(val = !state.show) {
|
|
||||||
state.show = val
|
|
||||||
if (val) {
|
|
||||||
toggleDot(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleDot(val = !state.showDot) {
|
|
||||||
state.showDot = val
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleConfigDialog(val = !state.showConfig) {
|
|
||||||
toggle(!val)
|
|
||||||
state.showConfig = val
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeRegistry(val: string, type: 'npm' | 'composer') {
|
|
||||||
state[type == 'npm' ? 'npmRegistry' : 'composerRegistry'] = val
|
|
||||||
}
|
|
||||||
|
|
||||||
function changePackageManager(val: string) {
|
|
||||||
state.packageManager = val
|
|
||||||
}
|
|
||||||
|
|
||||||
function changePHPDevelopmentServer(val: boolean) {
|
|
||||||
state.phpDevelopmentServer = val
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeAutomaticCleanupTask(val: '0' | '1') {
|
|
||||||
state.automaticCleanupTask = val
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTaskStatus(idx: number, status: number) {
|
|
||||||
state.taskList[idx].status = status
|
|
||||||
if ((status == taskStatus.Failed || status == taskStatus.Unknown) && state.taskList[idx].blockOnFailure) {
|
|
||||||
setTaskShowMessage(idx, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function taskCompleted(idx: number) {
|
|
||||||
// 命令执行完毕,重新打开热更新
|
|
||||||
openHotUpdate('terminal')
|
|
||||||
|
|
||||||
if (typeof state.taskList[idx].callback != 'function') return
|
|
||||||
|
|
||||||
const status = state.taskList[idx].status
|
|
||||||
if (status == taskStatus.Failed || status == taskStatus.Unknown) {
|
|
||||||
state.taskList[idx].callback(taskStatus.Failed)
|
|
||||||
} else if (status == taskStatus.Success) {
|
|
||||||
state.taskList[idx].callback(taskStatus.Success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTaskShowMessage(idx: number, val = !state.taskList[idx].showMessage) {
|
|
||||||
state.taskList[idx].showMessage = val
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTaskMessage(idx: number, message: string) {
|
|
||||||
if (!state.show) toggleDot(true)
|
|
||||||
state.taskList[idx].message = state.taskList[idx].message.concat(message)
|
|
||||||
nextTick(() => {
|
|
||||||
execMessageScrollbarKeep(state.taskList[idx].uuid)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTask(command: string, blockOnFailure = true, extend = '', callback: Function = () => {}) {
|
|
||||||
if (!state.show) toggleDot(true)
|
|
||||||
state.taskList = state.taskList.concat({
|
|
||||||
uuid: uuid(),
|
|
||||||
createTime: timeFormat(),
|
|
||||||
status: taskStatus.Waiting,
|
|
||||||
command: command,
|
|
||||||
message: [],
|
|
||||||
showMessage: false,
|
|
||||||
blockOnFailure: blockOnFailure,
|
|
||||||
extend: extend,
|
|
||||||
callback: callback,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 清理任务列表
|
|
||||||
if (parseInt(state.automaticCleanupTask) === 1) {
|
|
||||||
clearSuccessTask()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有已经失败的任务
|
|
||||||
if (state.show === false) {
|
|
||||||
for (const key in state.taskList) {
|
|
||||||
if (state.taskList[key].status == taskStatus.Failed || state.taskList[key].status == taskStatus.Unknown) {
|
|
||||||
ElNotification({
|
|
||||||
type: 'error',
|
|
||||||
message: i18n.global.t('terminal.Newly added tasks will never start because they are blocked by failed tasks'),
|
|
||||||
zIndex: SYSTEM_ZINDEX,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startTask()
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTaskPM(command: string, blockOnFailure = true, extend = '', callback: Function = () => {}) {
|
|
||||||
addTask(command + '.' + state.packageManager, blockOnFailure, extend, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
function delTask(idx: number) {
|
|
||||||
if (state.taskList[idx].status != taskStatus.Connecting && state.taskList[idx].status != taskStatus.Executing) {
|
|
||||||
state.taskList.splice(idx, 1)
|
|
||||||
}
|
|
||||||
startTask()
|
|
||||||
}
|
|
||||||
|
|
||||||
function startTask() {
|
|
||||||
let taskKey = null
|
|
||||||
|
|
||||||
// 寻找可以开始执行的命令
|
|
||||||
for (const key in state.taskList) {
|
|
||||||
if (state.taskList[key].status == taskStatus.Waiting) {
|
|
||||||
taskKey = parseInt(key)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (state.taskList[key].status == taskStatus.Connecting || state.taskList[key].status == taskStatus.Executing) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (state.taskList[key].status == taskStatus.Success) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (state.taskList[key].status == taskStatus.Failed || state.taskList[key].status == taskStatus.Unknown) {
|
|
||||||
if (state.taskList[key].blockOnFailure) {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (taskKey !== null) {
|
|
||||||
setTaskStatus(taskKey, taskStatus.Connecting)
|
|
||||||
startEventSource(taskKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startEventSource(taskKey: number) {
|
|
||||||
// 命令执行期间禁用热更新
|
|
||||||
closeHotUpdate('terminal')
|
|
||||||
|
|
||||||
window.eventSource = new EventSource(
|
|
||||||
buildTerminalUrl(state.taskList[taskKey].command, state.taskList[taskKey].uuid, state.taskList[taskKey].extend)
|
|
||||||
)
|
|
||||||
window.eventSource.onmessage = function (e) {
|
|
||||||
const data = JSON.parse(e.data)
|
|
||||||
if (!data || !data.data) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskIdx = findTaskIdxFromUuid(data.uuid)
|
|
||||||
if (taskIdx === false) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.data == 'command-exec-error') {
|
|
||||||
setTaskStatus(taskIdx, taskStatus.Failed)
|
|
||||||
window.eventSource.close()
|
|
||||||
taskCompleted(taskIdx)
|
|
||||||
startTask()
|
|
||||||
} else if (data.data == 'command-exec-completed') {
|
|
||||||
window.eventSource.close()
|
|
||||||
if (state.taskList[taskIdx].status != taskStatus.Success) {
|
|
||||||
setTaskStatus(taskIdx, taskStatus.Failed)
|
|
||||||
}
|
|
||||||
taskCompleted(taskIdx)
|
|
||||||
startTask()
|
|
||||||
} else if (data.data == 'command-link-success') {
|
|
||||||
setTaskStatus(taskIdx, taskStatus.Executing)
|
|
||||||
} else if (data.data == 'command-exec-success') {
|
|
||||||
setTaskStatus(taskIdx, taskStatus.Success)
|
|
||||||
} else {
|
|
||||||
addTaskMessage(taskIdx, data.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.eventSource.onerror = function () {
|
|
||||||
window.eventSource.close()
|
|
||||||
const taskIdx = findTaskIdxFromGuess(taskKey)
|
|
||||||
if (taskIdx === false) return
|
|
||||||
setTaskStatus(taskIdx, taskStatus.Failed)
|
|
||||||
taskCompleted(taskIdx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function retryTask(idx: number) {
|
|
||||||
state.taskList[idx].message = []
|
|
||||||
setTaskStatus(idx, taskStatus.Waiting)
|
|
||||||
startTask()
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearSuccessTask() {
|
|
||||||
state.taskList = state.taskList.filter((item) => item.status != taskStatus.Success)
|
|
||||||
}
|
|
||||||
|
|
||||||
function findTaskIdxFromUuid(uuid: string) {
|
|
||||||
for (const key in state.taskList) {
|
|
||||||
if (state.taskList[key].uuid == uuid) {
|
|
||||||
return parseInt(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function findTaskIdxFromGuess(idx: number) {
|
|
||||||
if (!state.taskList[idx]) {
|
|
||||||
let taskKey = -1
|
|
||||||
for (const key in state.taskList) {
|
|
||||||
if (state.taskList[key].status == taskStatus.Connecting || state.taskList[key].status == taskStatus.Executing) {
|
|
||||||
taskKey = parseInt(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return taskKey === -1 ? false : taskKey
|
|
||||||
} else {
|
|
||||||
return idx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function execMessageScrollbarKeep(uuid: string) {
|
|
||||||
const execMessageEl = document.querySelector('.exec-message-' + uuid) as Element
|
|
||||||
if (execMessageEl && execMessageEl.scrollHeight) {
|
|
||||||
execMessageEl.scrollTop = execMessageEl.scrollHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
init,
|
|
||||||
toggle,
|
|
||||||
toggleDot,
|
|
||||||
setTaskStatus,
|
|
||||||
setTaskShowMessage,
|
|
||||||
addTaskMessage,
|
|
||||||
addTask,
|
|
||||||
addTaskPM,
|
|
||||||
delTask,
|
|
||||||
startTask,
|
|
||||||
retryTask,
|
|
||||||
clearSuccessTask,
|
|
||||||
toggleConfigDialog,
|
|
||||||
changeRegistry,
|
|
||||||
changePackageManager,
|
|
||||||
changePHPDevelopmentServer,
|
|
||||||
changeAutomaticCleanupTask,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
persist: {
|
|
||||||
key: STORE_TERMINAL,
|
|
||||||
pick: ['state.showDot', 'state.taskList', 'state.automaticCleanupTask', 'state.npmRegistry', 'state.composerRegistry'],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -287,7 +287,25 @@ export const checkFileMimetype = (fileName: string, fileType: string) => {
|
|||||||
? siteConfig.upload.allowedMimeTypes
|
? siteConfig.upload.allowedMimeTypes
|
||||||
: siteConfig.upload.allowedMimeTypes.toLowerCase().split(',')
|
: siteConfig.upload.allowedMimeTypes.toLowerCase().split(',')
|
||||||
|
|
||||||
|
const forbiddenSuffixes = siteConfig.upload.forbiddenSuffixes
|
||||||
|
? isArray(siteConfig.upload.forbiddenSuffixes)
|
||||||
|
? siteConfig.upload.forbiddenSuffixes
|
||||||
|
: siteConfig.upload.forbiddenSuffixes.toLowerCase().split(',')
|
||||||
|
: []
|
||||||
|
|
||||||
|
const nameParts = fileName.toLowerCase().split('.')
|
||||||
|
if (nameParts.length > 1) {
|
||||||
|
for (let i = 0; i < nameParts.length - 1; i++) {
|
||||||
|
if (forbiddenSuffixes.includes(nameParts[i])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fileSuffix = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
const fileSuffix = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
||||||
|
if (forbiddenSuffixes.includes(fileSuffix)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (allowedSuffixes.includes(fileSuffix) || allowedSuffixes.includes('.' + fileSuffix)) {
|
if (allowedSuffixes.includes(fileSuffix) || allowedSuffixes.includes('.' + fileSuffix)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -691,7 +691,6 @@ import { getDatabaseConnectionListUrl, getTableFieldList, getTableListUrl } from
|
|||||||
import BaInput from '/@/components/baInput/index.vue'
|
import BaInput from '/@/components/baInput/index.vue'
|
||||||
import FormItem from '/@/components/formItem/index.vue'
|
import FormItem from '/@/components/formItem/index.vue'
|
||||||
import { useConfig } from '/@/stores/config'
|
import { useConfig } from '/@/stores/config'
|
||||||
import { useTerminal } from '/@/stores/terminal'
|
|
||||||
import { getArrayKey } from '/@/utils/common'
|
import { getArrayKey } from '/@/utils/common'
|
||||||
import { uuid } from '/@/utils/random'
|
import { uuid } from '/@/utils/random'
|
||||||
import { buildValidatorData, regularVarName } from '/@/utils/validate'
|
import { buildValidatorData, regularVarName } from '/@/utils/validate'
|
||||||
@@ -702,7 +701,6 @@ import { changeStep, state as crudState, designTypes, fieldItem, getTableAttr, t
|
|||||||
let nameRepeatCount = 1
|
let nameRepeatCount = 1
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const terminal = useTerminal()
|
|
||||||
const formRef = useTemplateRef('formRef')
|
const formRef = useTemplateRef('formRef')
|
||||||
const tabsRefs = useTemplateRefsList<HTMLElement>()
|
const tabsRefs = useTemplateRefsList<HTMLElement>()
|
||||||
const designWindowRef = useTemplateRef('designWindowRef')
|
const designWindowRef = useTemplateRef('designWindowRef')
|
||||||
@@ -1161,13 +1159,7 @@ const startGenerate = () => {
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
const webViewsDir = state.table.webViewsDir.replace(/^web/, '.')
|
|
||||||
terminal.toggle(true)
|
|
||||||
terminal.addTask('npx.prettier', false, webViewsDir, () => {
|
|
||||||
terminal.toggle(false)
|
|
||||||
terminal.toggleDot(true)
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 要求 Vite 服务端重启
|
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
reloadServer('crud')
|
reloadServer('crud')
|
||||||
} else {
|
} else {
|
||||||
@@ -1177,7 +1169,6 @@ const startGenerate = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((state.sync > 0 && config.crud.syncedUpdate === 'yes') || (state.sync == 0 && config.crud.syncType == 'automatic')) {
|
if ((state.sync > 0 && config.crud.syncedUpdate === 'yes') || (state.sync == 0 && config.crud.syncType == 'automatic')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user