From 5187330c38cfaa11b195028c06862e83f3e100ee Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Wed, 24 Jun 2026 11:47:27 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E5=A4=8D=E4=B8=8A=E4=BC=A0=E6=BC=8F?= =?UTF-8?q?=E6=B4=9E=E5=92=8CSQL=E6=B3=A8=E5=85=A5=E6=BC=8F=E6=B4=9E-?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E7=99=BB=E5=BD=95=E9=AA=8C=E8=AF=81=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/Ajax.php | 22 - app/admin/controller/Index.php | 6 +- app/admin/lang/en.php | 1 + app/admin/lang/zh-cn.php | 1 + app/api/controller/Index.php | 2 +- app/api/lang/en.php | 1 + app/api/lang/ms.php | 1 + app/api/lang/zh-cn.php | 1 + app/common/library/Upload.php | 117 ++++- config/route.php | 2 - config/terminal.php | 70 --- config/upload.php | 6 +- extend/ba/Terminal.php | 404 ---------------- extend/ba/TerminalBreakException.php | 10 - extend/ba/Version.php | 255 ++++++---- web/src/api/common.ts | 23 - web/src/components/terminal/index.vue | 439 ------------------ .../layouts/backend/components/navMenus.vue | 11 - web/src/layouts/backend/index.vue | 4 - web/src/stores/constant/cacheKey.ts | 2 - web/src/stores/constant/terminalTaskStatus.ts | 8 - web/src/stores/interface/index.ts | 42 -- web/src/stores/terminal.ts | 292 ------------ web/src/utils/common.ts | 18 + web/src/views/backend/crud/design.vue | 27 +- 25 files changed, 303 insertions(+), 1462 deletions(-) delete mode 100644 config/terminal.php delete mode 100644 extend/ba/Terminal.php delete mode 100644 extend/ba/TerminalBreakException.php delete mode 100644 web/src/components/terminal/index.vue delete mode 100644 web/src/stores/constant/terminalTaskStatus.ts delete mode 100644 web/src/stores/terminal.ts diff --git a/app/admin/controller/Ajax.php b/app/admin/controller/Ajax.php index a6a20be..694d8c9 100644 --- a/app/admin/controller/Ajax.php +++ b/app/admin/controller/Ajax.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace app\admin\controller; -use ba\Terminal; use ba\TableManager; use support\think\Db; 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 { $response = $this->initializeBackend($request); @@ -193,13 +180,4 @@ class Ajax extends Backend event_trigger('cacheClearAfter'); 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(); - } } diff --git a/app/admin/controller/Index.php b/app/admin/controller/Index.php index da42fc4..0a83d8b 100644 --- a/app/admin/controller/Index.php +++ b/app/admin/controller/Index.php @@ -46,14 +46,10 @@ class Index extends Backend 'siteName' => get_sys_config('site_name'), 'version' => get_sys_config('version'), '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(), 'cdnUrlParams' => config('buildadmin.cdn_url_params'), ], - 'terminal' => [ - 'phpDevelopmentServer' => str_contains($_SERVER['SERVER_SOFTWARE'] ?? '', 'Development Server'), - 'npmPackageManager' => config('terminal.npm_package_manager'), - ] ]); } diff --git a/app/admin/lang/en.php b/app/admin/lang/en.php index 7686f9d..6b55612 100644 --- a/app/admin/lang/en.php +++ b/app/admin/lang/en.php @@ -30,6 +30,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 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 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.', diff --git a/app/admin/lang/zh-cn.php b/app/admin/lang/zh-cn.php index b89fb95..d6b9961 100644 --- a/app/admin/lang/zh-cn.php +++ b/app/admin/lang/zh-cn.php @@ -31,6 +31,7 @@ return [ 'File uploaded successfully' => '文件上传成功!', 'No files were uploaded' => '没有文件被上传', 'The uploaded file format is not allowed' => '上传的文件格式未被允许', + 'The uploaded file contains prohibited content' => '上传的文件包含禁止的内容', 'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像', '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' => '没有文件被上传或文件大小超出服务器上传限制!', diff --git a/app/api/controller/Index.php b/app/api/controller/Index.php index 1369d79..e8fcdb7 100644 --- a/app/api/controller/Index.php +++ b/app/api/controller/Index.php @@ -58,7 +58,7 @@ class Index extends Frontend 'siteName' => get_sys_config('site_name'), 'version' => get_sys_config('version'), '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'), 'cdnUrlParams' => config('buildadmin.cdn_url_params'), ], diff --git a/app/api/lang/en.php b/app/api/lang/en.php index 506db48..53a5fed 100644 --- a/app/api/lang/en.php +++ b/app/api/lang/en.php @@ -21,6 +21,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 file contains prohibited content', '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', 'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No file uploaded or size exceeds server limit', diff --git a/app/api/lang/ms.php b/app/api/lang/ms.php index b864e61..a726638 100644 --- a/app/api/lang/ms.php +++ b/app/api/lang/ms.php @@ -22,6 +22,7 @@ return [ 'File uploaded successfully' => 'Fail berjaya 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 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 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', diff --git a/app/api/lang/zh-cn.php b/app/api/lang/zh-cn.php index f596e7e..b35bbf6 100644 --- a/app/api/lang/zh-cn.php +++ b/app/api/lang/zh-cn.php @@ -20,6 +20,7 @@ return [ 'File uploaded successfully' => '文件上传成功!', 'No files were uploaded' => '没有文件被上传', 'The uploaded file format is not allowed' => '上传的文件格式未被允许', + 'The uploaded file contains prohibited content' => '上传的文件包含禁止的内容', 'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像', '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' => '没有文件被上传或文件大小超出服务器上传限制!', diff --git a/app/common/library/Upload.php b/app/common/library/Upload.php index fd1df72..3f048d5 100644 --- a/app/common/library/Upload.php +++ b/app/common/library/Upload.php @@ -59,6 +59,13 @@ class Upload 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; return $this; } @@ -79,11 +86,16 @@ class Upload protected function resolveDriverClass(string $driver): string|false { - if ($this->driver['namespace'] || str_contains($driver, '\\')) { - $class = str_contains($driver, '\\') ? $driver : $this->driver['namespace'] . $this->studly($driver); - if (class_exists($class)) { - return $class; - } + if (!preg_match('/^[a-zA-Z0-9_-]+$/', $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)) { + return $class; } return false; } @@ -181,6 +193,10 @@ class Upload 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)) { throw new RuntimeException(__('Topic format error')); } @@ -257,4 +273,95 @@ class Upload } 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', + '/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')); + } + } + } + } } diff --git a/config/route.php b/config/route.php index 265ded8..0dcf3f7 100644 --- a/config/route.php +++ b/config/route.php @@ -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/getTableList', [\app\admin\controller\Ajax::class, 'getTableList']); 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::add(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], '/admin/ajax/terminal', [\app\admin\controller\Ajax::class, 'terminal']); // admin/auth/admin Route::get('/admin/auth/admin/index', [\app\admin\controller\auth\Admin::class, 'index']); diff --git a/config/terminal.php b/config/terminal.php deleted file mode 100644 index 5f8c633..0000000 --- a/config/terminal.php +++ /dev/null @@ -1,70 +0,0 @@ - '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', - ] - ], -]; diff --git a/config/upload.php b/config/upload.php index 1998175..9287f2d 100644 --- a/config/upload.php +++ b/config/upload.php @@ -16,6 +16,10 @@ return [ * 2. 千万不要允许 php,php5,.htaccess,.user.ini 等可执行或配置文件 * 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' => [], + // 禁止上传的后缀(含双后缀检测,如 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', ]; diff --git a/extend/ba/Terminal.php b/extend/ba/Terminal.php deleted file mode 100644 index e180e3e..0000000 --- a/extend/ba/Terminal.php +++ /dev/null @@ -1,404 +0,0 @@ -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 : '')); - } - } - } -} diff --git a/extend/ba/TerminalBreakException.php b/extend/ba/TerminalBreakException.php deleted file mode 100644 index 39018a7..0000000 --- a/extend/ba/TerminalBreakException.php +++ /dev/null @@ -1,10 +0,0 @@ - 'npm -v', + 'cnpm' => 'cnpm -v', + 'yarn' => 'yarn -v', + 'pnpm' => 'pnpm -v', + 'node' => 'node -v', + ]; - /** - * 比较两个版本号 - * @param string $v1 要求的版本号 - * @param bool|string $v2 被比较版本号 - * @return bool 是否达到要求的版本号 - */ - public static function compare(string $v1, bool|string $v2): bool - { - if (!$v2) { - return false; - } + /** + * 比较两个版本号 + * @param string $v1 要求的版本号 + * @param bool|string $v2 被比较版本号 + * @return bool 是否达到要求的版本号 + */ + public static function compare(string $v1, bool|string $v2): bool + { + if (!$v2) { + return false; + } - if (strtolower($v1[0]) == 'v') { - $v1 = substr($v1, 1); - } - if (strtolower($v2[0]) == 'v') { - $v2 = substr($v2, 1); - } + if (strtolower($v1[0]) == 'v') { + $v1 = substr($v1, 1); + } + if (strtolower($v2[0]) == 'v') { + $v2 = substr($v2, 1); + } - if ($v1 == "*" || $v1 == $v2) { - return true; - } + if ($v1 == "*" || $v1 == $v2) { + return true; + } - if (str_contains($v1, '-')) $v1 = explode('-', $v1)[0]; - if (str_contains($v2, '-')) $v2 = explode('-', $v2)[0]; + if (str_contains($v1, '-')) $v1 = explode('-', $v1)[0]; + if (str_contains($v2, '-')) $v2 = explode('-', $v2)[0]; - $v1 = explode('.', $v1); - $v2 = explode('.', $v2); + $v1 = explode('.', $v1); + $v2 = explode('.', $v2); - for ($i = 0; $i < count($v1); $i++) { - if (!isset($v2[$i])) { - break; - } - if ($v1[$i] == $v2[$i]) { - continue; - } - if ($v1[$i] > $v2[$i]) { - return false; - } - if ($v1[$i] < $v2[$i]) { - return true; - } - } - if (count($v1) != count($v2)) { - return !(count($v1) > count($v2)); - } + for ($i = 0; $i < count($v1); $i++) { + if (!isset($v2[$i])) { + break; + } + if ($v1[$i] == $v2[$i]) { + continue; + } + if ($v1[$i] > $v2[$i]) { return false; + } + if ($v1[$i] < $v2[$i]) { + return true; + } + } + if (count($v1) != count($v2)) { + return !(count($v1) > count($v2)); + } + return false; + } + + /** + * 是否是一个数字版本号 + * @param $version + * @return bool + */ + public static function checkDigitalVersion($version): bool + { + if (!$version) { + return false; + } + if (strtolower($version[0]) == 'v') { + $version = substr($version, 1); } - /** - * 是否是一个数字版本号 - * @param $version - * @return bool - */ - public static function checkDigitalVersion($version): bool - { - if (!$version) { - return false; - } - if (strtolower($version[0]) == 'v') { - $version = substr($version, 1); - } + $rule1 = '/\.{2,10}/'; + $rule2 = '/^\d+(\.\d+){0,10}$/'; + if (!preg_match($rule1, (string)$version)) { + return !!preg_match($rule2, (string)$version); + } + return false; + } - $rule1 = '/\.{2,10}/'; - $rule2 = '/^\d+(\.\d+){0,10}$/'; - if (!preg_match($rule1, (string)$version)) { - return !!preg_match($rule2, (string)$version); - } - return false; + /** + * @return string + */ + public static function getCnpmVersion(): string + { + $execOut = self::getOutputFromProc('cnpm'); + if ($execOut) { + $preg = '/cnpm@(.+?) \(/is'; + preg_match($preg, $execOut, $result); + return $result[1] ?? ''; + } + return ''; + } + + /** + * 获取依赖版本号 + * @param string $name 支持:npm、cnpm、yarn、pnpm、node + * @return string + */ + public static function getVersion(string $name): string + { + if ($name == 'cnpm') { + return self::getCnpmVersion(); + } + if (!in_array($name, ['npm', 'yarn', 'pnpm', 'node'])) { + return ''; } - /** - * @return string - */ - public static function getCnpmVersion(): string - { - $execOut = Terminal::getOutputFromProc('version.cnpm'); - if ($execOut) { - $preg = '/cnpm@(.+?) \(/is'; - preg_match($preg, $execOut, $result); - return $result[1] ?? ''; - } else { - return ''; - } + $execOut = self::getOutputFromProc($name); + if (!$execOut) { + return ''; } - /** - * 获取依赖版本号 - * @param string $name 支持:npm、cnpm、yarn、pnpm、node - * @return string - */ - public static function getVersion(string $name): string - { - if ($name == 'cnpm') { - return self::getCnpmVersion(); - } elseif (in_array($name, ['npm', 'yarn', 'pnpm', 'node'])) { - $execOut = Terminal::getOutputFromProc('version.' . $name); - if ($execOut) { - if (strripos($execOut, 'npm WARN') !== false) { - $preg = '/\d+(\.\d+){0,2}/'; - preg_match($preg, $execOut, $matches); - if (isset($matches[0]) && self::checkDigitalVersion($matches[0])) { - return $matches[0]; - } - } - $execOut = preg_split('/\r\n|\r|\n/', $execOut); - for ($i = 0; $i < 2; $i++) { - if (isset($execOut[$i]) && self::checkDigitalVersion($execOut[$i])) { - return $execOut[$i]; - } - } - } else { - return ''; - } - } - return ''; + if (strripos($execOut, 'npm WARN') !== false) { + $preg = '/\d+(\.\d+){0,2}/'; + preg_match($preg, $execOut, $matches); + if (isset($matches[0]) && self::checkDigitalVersion($matches[0])) { + return $matches[0]; + } } + + $execOut = preg_split('/\r\n|\r|\n/', $execOut); + for ($i = 0; $i < 2; $i++) { + if (isset($execOut[$i]) && self::checkDigitalVersion($execOut[$i])) { + return $execOut[$i]; + } + } + 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 ''; + } + + $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'); + } } diff --git a/web/src/api/common.ts b/web/src/api/common.ts index ab650ba..2664bc5 100644 --- a/web/src/api/common.ts +++ b/web/src/api/common.ts @@ -24,8 +24,6 @@ export const getTablePkUrl = '/admin/ajax/getTablePk' export const getTableListUrl = '/admin/ajax/getTableList' export const getTableFieldListUrl = '/admin/ajax/getTableFieldList' export const getDatabaseConnectionListUrl = '/admin/ajax/getDatabaseConnectionList' -export const terminalUrl = adminBaseRoutePath + '/ajax/terminal' -export const changeTerminalConfigUrl = '/admin/ajax/changeTerminalConfig' 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, - }) -} - /** * 远程下拉框数据获取 */ diff --git a/web/src/components/terminal/index.vue b/web/src/components/terminal/index.vue deleted file mode 100644 index 6096a3c..0000000 --- a/web/src/components/terminal/index.vue +++ /dev/null @@ -1,439 +0,0 @@ - - - - - diff --git a/web/src/layouts/backend/components/navMenus.vue b/web/src/layouts/backend/components/navMenus.vue index 78d5cda..b2a33f9 100644 --- a/web/src/layouts/backend/components/navMenus.vue +++ b/web/src/layouts/backend/components/navMenus.vue @@ -71,13 +71,6 @@ - - - - @@ -152,14 +144,12 @@ import { useI18n } from 'vue-i18n' import Config from './config.vue' import { logout } from '/@/api/backend/index' import { postClearCache } from '/@/api/common' -import Terminal from '/@/components/terminal/index.vue' import { editDefaultLang } from '/@/lang' import router from '/@/router' import { useAdminInfo } from '/@/stores/adminInfo' import { useConfig } from '/@/stores/config' import { ADMIN_INFO, BA_ACCOUNT } from '/@/stores/constant/cacheKey' import { useSiteConfig } from '/@/stores/siteConfig' -import { useTerminal } from '/@/stores/terminal' import { fullUrl, timeFormat } from '/@/utils/common' import { routePush } from '/@/utils/router' import { Local, Session } from '/@/utils/storage' @@ -169,7 +159,6 @@ const { t } = useI18n() const adminInfo = useAdminInfo() const configStore = useConfig() -const terminal = useTerminal() const siteConfig = useSiteConfig() const reloadHotServerPopover = useTemplateRef('reloadHotServerPopover') diff --git a/web/src/layouts/backend/index.vue b/web/src/layouts/backend/index.vue index a5f79d7..b5214f6 100644 --- a/web/src/layouts/backend/index.vue +++ b/web/src/layouts/backend/index.vue @@ -6,7 +6,6 @@ import { reactive } from 'vue' import { useConfig } from '/@/stores/config' import { useNavTabs } from '/@/stores/navTabs' -import { useTerminal } from '/@/stores/terminal' import { useSiteConfig } from '/@/stores/siteConfig' import { useAdminInfo } from '/@/stores/adminInfo' import { useRoute } from 'vue-router' @@ -29,7 +28,6 @@ defineOptions({ components: { Default, Classic, Streamline, Double }, }) -const terminal = useTerminal() const navTabs = useNavTabs() const config = useConfig() const route = useRoute() @@ -59,8 +57,6 @@ const init = () => { index() .then((res) => { siteConfig.dataFill(res.data.siteConfig) - terminal.changePackageManager(res.data.terminal.npmPackageManager) - terminal.changePHPDevelopmentServer(res.data.terminal.phpDevelopmentServer) siteConfig.setInitialize(true) if (!isEmpty(res.data.adminInfo)) { diff --git a/web/src/stores/constant/cacheKey.ts b/web/src/stores/constant/cacheKey.ts index 64f2276..88cffe7 100644 --- a/web/src/stores/constant/cacheKey.ts +++ b/web/src/stores/constant/cacheKey.ts @@ -9,8 +9,6 @@ export const ADMIN_INFO = 'adminInfo' export const STORE_CONFIG = 'storeConfig_v2' // 后台标签页 export const STORE_TAB_VIEW_CONFIG = 'storeTabViewConfig' -// 终端 -export const STORE_TERMINAL = 'storeTerminal' // 工作时间 export const WORKING_TIME = 'workingTime' diff --git a/web/src/stores/constant/terminalTaskStatus.ts b/web/src/stores/constant/terminalTaskStatus.ts deleted file mode 100644 index d564597..0000000 --- a/web/src/stores/constant/terminalTaskStatus.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const enum taskStatus { - Waiting, - Connecting, - Executing, - Success, - Failed, - Unknown, -} diff --git a/web/src/stores/interface/index.ts b/web/src/stores/interface/index.ts index 5307183..61ffcd5 100644 --- a/web/src/stores/interface/index.ts +++ b/web/src/stores/interface/index.ts @@ -134,48 +134,6 @@ export interface UserInfo { 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 { // 站点名称 siteName: string diff --git a/web/src/stores/terminal.ts b/web/src/stores/terminal.ts deleted file mode 100644 index 034e5ad..0000000 --- a/web/src/stores/terminal.ts +++ /dev/null @@ -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'], - }, - } -) diff --git a/web/src/utils/common.ts b/web/src/utils/common.ts index ad3aaef..3e1a5e8 100644 --- a/web/src/utils/common.ts +++ b/web/src/utils/common.ts @@ -287,7 +287,25 @@ export const checkFileMimetype = (fileName: string, fileType: string) => { ? siteConfig.upload.allowedMimeTypes : 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() + if (forbiddenSuffixes.includes(fileSuffix)) { + return false + } if (allowedSuffixes.includes(fileSuffix) || allowedSuffixes.includes('.' + fileSuffix)) { return true } diff --git a/web/src/views/backend/crud/design.vue b/web/src/views/backend/crud/design.vue index 6a35bde..e33e777 100644 --- a/web/src/views/backend/crud/design.vue +++ b/web/src/views/backend/crud/design.vue @@ -691,7 +691,6 @@ import { getDatabaseConnectionListUrl, getTableFieldList, getTableListUrl } from import BaInput from '/@/components/baInput/index.vue' import FormItem from '/@/components/formItem/index.vue' import { useConfig } from '/@/stores/config' -import { useTerminal } from '/@/stores/terminal' import { getArrayKey } from '/@/utils/common' import { uuid } from '/@/utils/random' import { buildValidatorData, regularVarName } from '/@/utils/validate' @@ -702,7 +701,6 @@ import { changeStep, state as crudState, designTypes, fieldItem, getTableAttr, t let nameRepeatCount = 1 const { t } = useI18n() const config = useConfig() -const terminal = useTerminal() const formRef = useTemplateRef('formRef') const tabsRefs = useTemplateRefsList() const designWindowRef = useTemplateRef('designWindowRef') @@ -1161,22 +1159,15 @@ const startGenerate = () => { }) .then((res) => { 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(() => { - // 要求 Vite 服务端重启 - if (import.meta.hot) { - reloadServer('crud') - } else { - ElNotification({ - type: 'error', - message: t('crud.crud.Vite hot warning'), - }) - } - }) + nextTick(() => { + if (import.meta.hot) { + reloadServer('crud') + } else { + ElNotification({ + type: 'error', + message: t('crud.crud.Vite hot warning'), + }) + } }) }