├── home └── api.ospanel │ ├── public_html │ ├── favicon.ico │ ├── .htaccess │ └── index.php │ ├── .osp │ └── project.ini │ └── core │ ├── Controllers │ ├── Controller.php │ ├── ModuleController.php │ ├── SettingsController.php │ ├── IndexController.php │ ├── FilesController.php │ └── SitesController.php │ ├── Traits │ └── Makeable.php │ ├── Services │ ├── Http.php │ ├── Api.php │ ├── Modules.php │ ├── IniFile.php │ └── Domains.php │ ├── DTO │ ├── Module.php │ └── Domain.php │ ├── Router │ ├── Response.php │ ├── Router.php │ └── Request.php │ └── functions.php ├── system └── templates │ ├── data.php.template │ ├── dev_index.html.template │ └── index.html.template ├── LICENSE ├── README.md └── config └── program.ini /home/api.ospanel/public_html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delphinpro/oscp/HEAD/home/api.ospanel/public_html/favicon.ico -------------------------------------------------------------------------------- /home/api.ospanel/.osp/project.ini: -------------------------------------------------------------------------------- 1 | [api.ospanel] 2 | ;enabled = on 3 | php_engine = PHP-8.1 4 | public_dir = {base_dir}\public_html 5 | ssl = off 6 | -------------------------------------------------------------------------------- /home/api.ospanel/public_html/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^ index.php [L] 6 | 7 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | OSPanel 9 | 10 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /system/templates/index.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Open Server Panel 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Controllers/ModuleController.php: -------------------------------------------------------------------------------- 1 | Modules::make()->toArray(), 21 | ])->asJson(); 22 | 23 | } catch (\Exception $e) { 24 | 25 | return Response::json() 26 | ->status(500) 27 | ->message($e->getMessage()); 28 | 29 | } 30 | } 31 | 32 | public function engines(): Response 33 | { 34 | $modules = Modules::make(); 35 | 36 | return Response::json([ 37 | 'php_engines' => $modules->getPhpEngines(), 38 | 'nginx_engines' => $modules->getNginxEngines(), 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Сергей 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Controllers/SettingsController.php: -------------------------------------------------------------------------------- 1 | get(); 19 | 20 | return Response::json([ 21 | 'settings' => [ 22 | 'main' => $settings['main'] ?? [], 23 | 'menu' => $settings['menu'] ?? [], 24 | 'projects' => $settings['projects'] ?? [], 25 | ], 26 | ]); 27 | } 28 | 29 | public function save(Request $request): Response 30 | { 31 | $settings = IniFile::open('config/program.ini'); 32 | $input = $request->all(); 33 | 34 | $settings 35 | ->update('main', $input['main'] ?? []) 36 | ->update('menu', $input['menu'] ?? []) 37 | ->update('projects', $input['projects'] ?? []) 38 | ->save(); 39 | 40 | return Response::json(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Controllers/IndexController.php: -------------------------------------------------------------------------------- 1 | get(API_DOMAIN); 20 | 21 | return Response::json([ 22 | 'main' => [ 23 | 'ospVersion' => OSP_VERSION, 24 | 'ospDate' => OSP_DATE, 25 | 'apiDomain' => API_DOMAIN, 26 | 'apiEngine' => $apiDomain->php_engine, 27 | // 'apiEngine' => 'php-8.1',//$apiDomain->engine, 28 | 'webApiUrl' => WEB_API_URL, 29 | 'cliApiUrl' => CLI_API_URL, 30 | 31 | 'totalDomains' => $domains->count(), 32 | 'disabledDomains' => $domains->countDisabled(), 33 | 'problemDomains' => $domains->countProblems(), 34 | ], 35 | 'settings' => IniFile::open('config/program.ini')->get(), 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Services/Http.php: -------------------------------------------------------------------------------- 1 | $value) { 40 | $result[$key] = match (true) { 41 | $value === 'False' => false, 42 | $value === 'True' => true, 43 | is_array($value) => self::normalize($value), 44 | default => $value, 45 | }; 46 | } 47 | 48 | return $result; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /home/api.ospanel/core/DTO/Module.php: -------------------------------------------------------------------------------- 1 | $this->name, 34 | 'alt_name' => $this->altName(), 35 | 'opt_name' => $this->optName(), 36 | 'enabled' => $this->enabled, 37 | 'init' => $this->init, 38 | 'version' => $this->version, 39 | 'arch' => $this->arch, 40 | 'category' => $this->category, 41 | 42 | 'ip' => $this->ip(), 43 | 'port' => $this->port(), 44 | ]; 45 | } 46 | 47 | public function altName(): string 48 | { 49 | $name = str_replace('-', ' ', $this->name); 50 | if (str_starts_with($name, 'PHP')) { 51 | if (str_contains($name, 'FCGI')) { 52 | return str_replace(' FCGI', '', $name).' FastCGI'; 53 | } 54 | 55 | return $name.' Apache'; 56 | } 57 | 58 | return $name; 59 | } 60 | 61 | public function optName(): string 62 | { 63 | return ($this->enabled ? '✅ ' : '❌ ').$this->altName(); 64 | } 65 | 66 | public function ip(): ?string 67 | { 68 | return $this->params['ip'] ?? null; 69 | } 70 | 71 | public function port(): ?string 72 | { 73 | return $this->params['port'] ?? null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Server Control Panel 2 | 3 | Web-панель управления для OSPanel v6. 4 | 5 | ## Важно 6 | 7 | Данная панель управления написана пользователем, не имеющим отношения к команде разработки пакета OSPanel. Я стараюсь 8 | обновлять код по мере выхода обновлений OSP, но не могу гарантировать работоспособность в полной мере. 9 | 10 | ## Об этом выпуске 11 | 12 | Данный релиз тестировался на версии OSPanel 6.0.0 от 06.05.2024. 13 | На других версиях, в том числе будущих, работоспособность не гарантируется. 14 | 15 | Возможности управления носят ограниченный характер. Для тонкой настройки вам может потребоваться правка конфигурационных 16 | файлов. 17 | 18 | Возможности: 19 | 20 | * Просмотр списка зарегистрированных локальных сайтов 21 | * Создание локальных сайтов 22 | * Изменения базовых настроек сайта 23 | * Удаление сайта 24 | * Изменение настроек программы (не всех) 25 | * Просмотр списка установленных модулей 26 | * Включение/отключение/перезагрузка модулей 27 | 28 | ## Установка 29 | 30 | Процесс установки максимально прост. 31 | 32 | **Важный аспект:** для версии 6.0.0 этот архив содержит файл program.ini с настройками по умолчанию и необходимыми 33 | дополнениями. _При распаковке следует согласиться с его заменой_. При этом любые ваши изменения в нём, сделанные прежде, 34 | сбросятся к умолчанию. Но не расстраивайтесь, позже вы сможете изменить любые настройки через визуальный интерфейс =). 35 | 36 | * Скачайте архив из раздела релизов 37 | * Распакуйте файлы в соответствующие директории, согласитесь с заменой файлов 38 | * Перезапустите программу 39 | * Откройте в браузере страницу http://ospanel 40 | 41 | ## Скриншоты 42 | 43 | | . | . | 44 | |---|---| 45 | | ![20240823-013556](https://github.com/user-attachments/assets/6ad3aabd-62f9-4088-8da0-7eb2540a7c55) | ![20240823-013618](https://github.com/user-attachments/assets/ab514a64-ae17-4872-9fc7-9b84f800c358) | 46 | | ![20240823-013807](https://github.com/user-attachments/assets/cbbdb8fa-d3e3-4573-b73b-b2e2748b585f) | ![20240823-013832](https://github.com/user-attachments/assets/bede3b14-f107-4d3a-a496-67a5201c3228) | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Router/Response.php: -------------------------------------------------------------------------------- 1 | setContent($data) 26 | ->asJson(); 27 | } 28 | 29 | /** 30 | * @throws \JsonException 31 | */ 32 | public function __toString(): string 33 | { 34 | foreach ($this->headers as $header) { 35 | header($header); 36 | } 37 | 38 | if ($this->json) { 39 | header('Content-Type: application/json'); 40 | 41 | return json_encode([ 42 | 'status' => $this->status, 43 | 'message' => $this->message, 44 | 'payload' => $this->content, 45 | ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); 46 | } 47 | 48 | header('Content-Type: text/html'); 49 | 50 | return $this->content; 51 | } 52 | 53 | public function status(int $status): static 54 | { 55 | $this->status = $status; 56 | 57 | return $this; 58 | } 59 | 60 | public function message(string $message): static 61 | { 62 | $this->message = $message; 63 | 64 | return $this; 65 | } 66 | 67 | public function headers(array $headers): static 68 | { 69 | $this->headers = array_merge( 70 | $this->headers, 71 | $headers, 72 | ); 73 | 74 | return $this; 75 | } 76 | 77 | 78 | public function setContent(string|array|null $content): static 79 | { 80 | $this->content = $content; 81 | 82 | return $this; 83 | } 84 | 85 | public function asJson(): static 86 | { 87 | $this->json = true; 88 | 89 | return $this; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Services/Api.php: -------------------------------------------------------------------------------- 1 | apiUrl = CLI_API_URL; 27 | $this->apiHost = str_replace(parse_url(CLI_API_URL, PHP_URL_PATH), '', CLI_API_URL); 28 | } 29 | 30 | /** @throws \JsonException */ 31 | public function getProjects(): array 32 | { 33 | $response = $this->request('projects'); 34 | 35 | return json_decode($response, true, flags: JSON_THROW_ON_ERROR); 36 | } 37 | 38 | private function makeUrl(string $input): string 39 | { 40 | return match ($input) { 41 | 'projects' => $this->apiHost.'/getprojects', 42 | 'modules' => $this->apiHost.'/getmodules', 43 | default => $this->apiUrl.'/'.ltrim($input, '/'), 44 | }; 45 | } 46 | 47 | /** 48 | * @param string $url 49 | * 50 | * @return null|array|string|string[] 51 | * @throws \RuntimeException 52 | */ 53 | private function request(string $url): array|string|null 54 | { 55 | $url1 = $this->makeUrl($url); 56 | $ch = curl_init($url1); 57 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); 58 | curl_setopt($ch, CURLOPT_FAILONERROR, true); 59 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 60 | $data = curl_exec($ch); 61 | 62 | if ($data === false) { 63 | $error = curl_error($ch); 64 | curl_close($ch); 65 | throw new RuntimeException($error.' '.$url1); 66 | } 67 | 68 | curl_close($ch); 69 | 70 | return $this->clearString($data); 71 | } 72 | 73 | private function clearString(string $string): array|string|null 74 | { 75 | $string = trim($string); 76 | $string = str_replace(["\r", ""], "", $string); 77 | 78 | return preg_replace('/\[\d+m/', '', $string); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /home/api.ospanel/public_html/index.php: -------------------------------------------------------------------------------- 1 | get('/ping', static fn() => Response::json()); 41 | 42 | $router->get('/main', IndexController::class); 43 | 44 | $router->get('/settings', [SettingsController::class, 'edit']); 45 | $router->post('/settings', [SettingsController::class, 'save']); 46 | 47 | $router->get('/modules', ModuleController::class); 48 | $router->get('/modules/engines', [ModuleController::class, 'engines']); 49 | 50 | $router->get('/sites', [SitesController::class, 'index']); 51 | $router->get('/sites/defaults', [SitesController::class, 'defaults']); 52 | $router->get('/sites/edit', [SitesController::class, 'edit']); 53 | $router->post('/sites/store', [SitesController::class, 'store']); 54 | $router->post('/sites/save', [SitesController::class, 'save']); 55 | $router->post('/sites/delete', [SitesController::class, 'delete']); 56 | $router->post('/sites/console', [SitesController::class, 'openConsole']); 57 | 58 | $router->post('/fs', FilesController::class); 59 | $router->post('/fs/create', [FilesController::class, 'create']); 60 | 61 | $router->resolve(); 62 | 63 | } catch (\Throwable $e) { 64 | 65 | header("{$_SERVER['SERVER_PROTOCOL']} 500 Server Error"); 66 | echo $e->getMessage(); 67 | 68 | } 69 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Services/Modules.php: -------------------------------------------------------------------------------- 1 | readConfig(); 22 | } 23 | 24 | public static function make(): Modules 25 | { 26 | if (!self::$instance) { 27 | self::$instance = new Modules(); 28 | } 29 | 30 | return self::$instance; 31 | } 32 | 33 | /** 34 | * @return \OpenServer\DTO\Module[] 35 | */ 36 | public function toArray(): array 37 | { 38 | return array_map(static fn(Module $module) => $module->toArray(), $this->modules); 39 | } 40 | 41 | public function getPhpEngines(): array 42 | { 43 | $filtered = array_filter($this->modules, static fn(Module $module) => $module->category === 'PHP'); 44 | 45 | return array_map(static fn(Module $m) => $m->toArray(), array_values($filtered)); 46 | } 47 | 48 | public function getNginxEngines(): array 49 | { 50 | $filtered = array_filter($this->modules, static fn(Module $module) => $module->category === 'Nginx'); 51 | 52 | return array_map(static fn(Module $m) => $m->toArray(), array_values($filtered)); 53 | } 54 | 55 | public function get($moduleName): ?Module 56 | { 57 | foreach ($this->modules as $module) { 58 | if ($module->name === $moduleName) { 59 | return $module; 60 | } 61 | } 62 | 63 | return null; 64 | } 65 | 66 | private function readConfig(): void 67 | { 68 | $modules = Http::getJson('getmodules'); 69 | $this->modules = array_map(static function ($item, $name) { 70 | $profile = $item['profile']; 71 | $settings = $item['profiles'][$profile]; 72 | 73 | return Module::make( 74 | name: $name, 75 | enabled: $item['enabled'], 76 | init: $item['inited'], 77 | version: $item['version'], 78 | arch: $item['architecture'], 79 | category: $item['category'], 80 | params: [ 81 | 'ip' => $settings['ip'] ?? null, 82 | 'port' => $settings['port'] ?? null, 83 | ] 84 | ); 85 | }, array_values($modules), array_keys($modules)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Controllers/FilesController.php: -------------------------------------------------------------------------------- 1 | findDirectory($request->input('directory')); 19 | $showFiles = (bool)$request->input('files'); 20 | 21 | return Response::json([ 22 | 'directory' => $directory, 23 | 'list' => $this->readFs($directory, $showFiles), 24 | ]); 25 | } 26 | 27 | public function create(Request $request): Response 28 | { 29 | $directory = $request->input('directory'); 30 | $showFiles = (bool)$request->input('files'); 31 | $newDir = $request->input('newDir'); 32 | 33 | $result = @mkdir($directory.'/'.$newDir); 34 | 35 | if ($result) { 36 | return Response::json([ 37 | 'directory' => $directory, 38 | 'list' => $this->readFs($directory, $showFiles), 39 | ]); 40 | } 41 | 42 | return Response::json() 43 | ->status(500) 44 | ->message('Не удалось создать папку'); 45 | } 46 | 47 | private function readFs(string $directory, bool $showFiles): array 48 | { 49 | $result = []; 50 | $dir = new \DirectoryIterator($directory); 51 | 52 | if ($directory !== dirname($directory)) { 53 | $result[dirname($directory)] = '..'; 54 | } 55 | 56 | /** @var \DirectoryIterator $file */ 57 | foreach ($dir as $file) { 58 | if ($file->isDot()) continue; 59 | if (!$showFiles && !$file->isDir()) continue; 60 | 61 | $result[$file->getPathname()] = $file->getFilename(); 62 | } 63 | 64 | ksort($result); 65 | 66 | return $result; 67 | } 68 | 69 | private function findDirectory(string $dir): string 70 | { 71 | if (file_exists($dir)) return $dir; 72 | 73 | $paths = IniFile::open('config/program.ini')->get('main')['projects_search_path'] ?? '{root_dir}\home'; 74 | [$searchPath] = explode(';', $paths, 2); 75 | 76 | $defaultStartDirectory = str_replace(['/', '{root_dir}'], ['\\', ROOT_DIR], $searchPath); 77 | 78 | if (!$dir) { 79 | return $defaultStartDirectory; 80 | } 81 | 82 | $protect = 0; 83 | while (!file_exists($dir) && $protect++ < 100) { 84 | $dir = dirname($dir); 85 | } 86 | 87 | if (strlen($dir) < 4) return $defaultStartDirectory; 88 | 89 | return $dir; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /home/api.ospanel/core/functions.php: -------------------------------------------------------------------------------- 1 | array ('.count($var).') ['.PHP_EOL; 15 | $keys = array_keys($var); 16 | $max = array_reduce($keys, static fn($carry, $key) => max($carry, strlen($key)), 0); 17 | foreach ($var as $k => $v) { 18 | $offset = $max - strlen($k) + 1; 19 | echo str_repeat(' ', $level + 1).'\''.$k.'\''.str_repeat(' ', $offset).'=> '; 20 | if ($level > 7) { 21 | echo '** MAX DEPTH **'.PHP_EOL; 22 | } else { 23 | dump($v, $level + 1); 24 | } 25 | } 26 | echo str_repeat(' ', $level).']'.PHP_EOL; 27 | break; 28 | 29 | case is_object($var): 30 | if ($var::class === 'Closure') { 31 | echo 'Closure() { }'.PHP_EOL; 32 | } else { 33 | echo 'object ('.$var::class.') ['.PHP_EOL; 34 | $var = (array)$var; 35 | $keys = array_keys($var); 36 | $max = array_reduce($keys, static fn($carry, $key) => max($carry, strlen($key)), 0); 37 | foreach ($var as $k => $v) { 38 | $offset = $max - strlen($k) + 1; 39 | echo str_repeat(' ', $level + 1).''.$k.''.str_repeat(' ', $offset).'=> '; 40 | if ($level > 2) { 41 | echo '**RECURSION**'.PHP_EOL; 42 | } else { 43 | dump($v, $level + 1); 44 | } 45 | } 46 | echo str_repeat(' ', $level).']'.PHP_EOL; 47 | } 48 | break; 49 | 50 | case is_string($var): 51 | echo 'string('.mb_strlen($var).') '; 52 | var_export($var); 53 | echo PHP_EOL; 54 | break; 55 | 56 | case is_scalar($var): 57 | echo ''.gettype($var).'('; 58 | var_export($var); 59 | echo ')'; 60 | echo PHP_EOL; 61 | break; 62 | 63 | default: 64 | var_export($var); 65 | echo PHP_EOL; 66 | } 67 | } 68 | 69 | function dd(...$vars): never 70 | { 71 | foreach ($vars as $var) { 72 | echo '
';
73 |         dump($var);
74 |         echo '
'; 75 | } 76 | exit; 77 | } 78 | 79 | function templatePath(?string $path): ?string 80 | { 81 | if ($path === null) return null; 82 | 83 | return str_replace(str_replace('/', '\\', ROOT_DIR), '{root_dir}', $path); 84 | } 85 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Router/Router.php: -------------------------------------------------------------------------------- 1 | request = $request; 28 | } 29 | 30 | public function __call($name, $args) 31 | { 32 | [$route, $method] = $args; 33 | 34 | if (!in_array(strtoupper($name), $this->supportedHttpMethods, true)) { 35 | $this->invalidMethodHandler($name); 36 | } 37 | 38 | $this->routes[strtolower($name)][$this->formatRoute($route)] = $method; 39 | } 40 | 41 | /** 42 | * Resolves a route 43 | */ 44 | public function resolve(): void 45 | { 46 | $methodDictionary = $this->routes[strtolower($this->request->requestMethod)]; 47 | $formattedRoute = $this->formatRoute($this->request->requestUri); 48 | $callable = $methodDictionary[$formattedRoute] ?? null; 49 | 50 | switch (true) { 51 | case is_string($callable): 52 | $controller = new $callable(); 53 | echo $controller($this->request); 54 | break; 55 | 56 | case is_array($callable) && count($callable) === 2: 57 | $controller = new $callable[0](); 58 | $method = $callable[1]; 59 | echo $controller->$method($this->request); 60 | break; 61 | 62 | case is_callable($callable): 63 | echo $callable($this->request); 64 | break; 65 | 66 | default: 67 | $this->defaultRequestHandler(); 68 | } 69 | } 70 | 71 | private function invalidMethodHandler(string $method): void 72 | { 73 | echo Response::json() 74 | ->headers(["{$this->request->serverProtocol} 405 Method Not Allowed"]) 75 | ->message('405 Method Not Allowed: '.$method) 76 | ->status(405); 77 | } 78 | 79 | /** 80 | * Removes trailing forward slashes from the right of the route. 81 | */ 82 | private function formatRoute(string $route): string 83 | { 84 | $result = trim($route, '/'); 85 | if ($result === '') { 86 | return '/'; 87 | } 88 | 89 | return $result; 90 | } 91 | 92 | private function defaultRequestHandler(): void 93 | { 94 | echo Response::json() 95 | ->headers(["{$this->request->serverProtocol} 404 Not Found"]) 96 | ->message('404 Not Found: '.$this->request->requestMethod.' '.$this->request->requestUri) 97 | ->status(404); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Router/Request.php: -------------------------------------------------------------------------------- 1 | bootstrapSelf(); 44 | $this->parseBody(); 45 | } 46 | 47 | public function __get(string $name) 48 | { 49 | return $this->requestVariables[$name] ?? null; 50 | } 51 | 52 | public function get(string $key, string|int|bool|null $default = null): string|int|bool|null 53 | { 54 | return $this->queryParams[$key] ?? $default; 55 | } 56 | 57 | public function input(string $key, string|int|bool|null $default = null): string|int|bool|null 58 | { 59 | return $this->body[$key] ?? $default; 60 | } 61 | 62 | public function all(): array 63 | { 64 | return $this->body; 65 | } 66 | 67 | public function except(array $keys): array 68 | { 69 | return array_filter( 70 | $this->body, 71 | static fn($key) => !in_array($key, $keys, true), 72 | ARRAY_FILTER_USE_KEY 73 | ); 74 | } 75 | 76 | public function only(array $keys): array 77 | { 78 | return array_filter( 79 | $this->body, 80 | static fn($key) => in_array($key, $keys, true), 81 | ARRAY_FILTER_USE_KEY 82 | ); 83 | } 84 | 85 | public function getBody(): ?array 86 | { 87 | return $this->body; 88 | } 89 | 90 | public function toArray(): array 91 | { 92 | return $this->requestVariables; 93 | } 94 | 95 | private function bootstrapSelf(): void 96 | { 97 | foreach ($_SERVER as $key => $value) { 98 | $this->requestVariables[$this->toCamelCase($key)] = $value; 99 | } 100 | [$requestUri] = explode('?', $this->requestUri); 101 | $this->requestVariables['requestUri'] = $requestUri; 102 | $this->queryParams = $_GET; 103 | } 104 | 105 | private function parseBody(): void 106 | { 107 | if ($this->requestMethod === "POST") { 108 | $this->body = []; 109 | foreach ($_POST as $key => $value) { 110 | $this->body[$key] = $this->castInputValue($value); 111 | } 112 | } 113 | } 114 | 115 | private function castInputValue(mixed $value) 116 | { 117 | if (is_array($value)) { 118 | return array_map(fn($value) => $this->castInputValue($value), $value); 119 | } 120 | 121 | return match (true) { 122 | $value === 'true' => true, 123 | $value === 'false' => false, 124 | $value === 'null' => null, 125 | is_numeric($value) => (int)$value, 126 | default => $value, 127 | }; 128 | 129 | } 130 | 131 | private function toCamelCase($string): array|string 132 | { 133 | $result = strtolower($string); 134 | 135 | preg_match_all('/_[a-z]/', $result, $matches); 136 | 137 | foreach ($matches[0] as $match) { 138 | $c = str_replace('_', '', strtoupper($match)); 139 | $result = str_replace($match, $c, $result); 140 | } 141 | 142 | return $result; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /config/program.ini: -------------------------------------------------------------------------------- 1 | [main] 2 | 3 | api_domain = ospanel 4 | api_ip = 127.127.127.127 5 | api_port = 80 6 | clear_dns_cache = on 7 | lang = auto 8 | log_max_filesize = 1 9 | projects_search_depth = 3 10 | projects_search_path = {root_dir}\home 11 | task_scheduler = on 12 | terminal_ansi_fix = auto 13 | update_check = on 14 | use_hosts_file = on 15 | use_win_terminal = off 16 | 17 | [menu] 18 | 19 | do_not_group_single_item = on 20 | show_icons = on 21 | show_addons = on 22 | show_addons_in_groups = on 23 | show_addons_in_submenu = off 24 | show_modules = on 25 | show_modules_in_groups = on 26 | show_modules_in_submenu = on 27 | show_projects = on 28 | show_projects_in_groups = off 29 | show_projects_in_submenu = off 30 | show_tray_icon = on 31 | show_hr_after_addons = on 32 | show_hr_after_modules = on 33 | show_hr_after_projects = on 34 | 35 | [projects] 36 | 37 | aliases = www 38 | enabled = on 39 | environment = System 40 | ip = 41 | nginx_engine = 42 | node_engine = 43 | php_engine = 44 | project_dir = {base_dir} 45 | project_url = https://{host_decoded} 46 | public_dir = {base_dir} 47 | ssl = on 48 | ssl_cert_file = 49 | ssl_key_file = 50 | start_command = 51 | terminal_codepage = 65001 52 | 53 | [modules] 54 | 55 | allowed_system_env_vars = allusersprofile appdata commonprogramfiles commonprogramfiles(x86) commonprogramw6432 computername comspec driverdata homedrive homepath localappdata logonserver number_of_processors os path pathext processor_architecture processor_identifier processor_level processor_revision programdata programfiles programfiles(x86) programw6432 psmodulepath public sessionname systemdrive systemroot temp tmp userdomain userdomain_roamingprofile username userprofile windir 56 | log_max_filesize = 1 57 | log_write_title = on 58 | max_probation_fails = 1 59 | max_shutdown_time = 0 60 | probation = 30 61 | silent_mode = on 62 | terminal_codepage = 65001 63 | time_zone = system 64 | used_addons_environment = 65 | 66 | [smtp] 67 | 68 | open_email_after_saving = off 69 | saved_email_extension = .eml 70 | smtp_port = 25 71 | smtp_server = on 72 | 73 | [environment] 74 | 75 | CURL_CA_BUNDLE = {root_dir}\data\ssl\cacert.pem 76 | HOME = {root_dir}\user 77 | HOMEDRIVE = {root_drive} 78 | HOMEPATH = {root_path}\user 79 | PATH = {root_dir}\bin;%SYSTEMROOT%\system32;%SYSTEMROOT%;%SYSTEMROOT%\System32\Wbem;%SYSTEMROOT%\System32\WindowsPowerShell\v1.0 80 | TERM = dumb 81 | TEMP = {root_dir}\temp 82 | TMP = {root_dir}\temp 83 | 84 | [index.html] 85 | 86 | destination = {root_dir}\system\public_html\index.html 87 | directory_separator = / 88 | enabled = on 89 | encoding = UTF8 90 | 91 | [default.html] 92 | 93 | destination = {root_dir}\system\public_html\default\index.html 94 | directory_separator = / 95 | enabled = on 96 | encoding = UTF8 97 | 98 | [osp.bat] 99 | 100 | comment = :: 101 | destination = {root_dir}\bin\osp.bat 102 | directory_separator = \ 103 | enabled = on 104 | encoding = UTF8 105 | 106 | [tail.bat] 107 | 108 | comment = :: 109 | destination = {root_dir}\system\bin\tail.bat 110 | directory_separator = \ 111 | enabled = on 112 | encoding = UTF8 113 | 114 | ; Open Server Web Panel by delphinpro 115 | 116 | [index.html.template] 117 | 118 | destination = {root_dir}\system\public_html\index.html 119 | directory_separator = / 120 | enabled = on 121 | encoding = UTF8 122 | 123 | [data.php.template] 124 | 125 | destination = {root_dir}\home\api.ospanel\data.php 126 | directory_separator = / 127 | enabled = on 128 | encoding = UTF8 129 | 130 | [dev_index.html.template] 131 | 132 | destination = {root_dir}\home\api.ospanel\public\index.html 133 | directory_separator = / 134 | enabled = on 135 | encoding = UTF8 136 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Controllers/SitesController.php: -------------------------------------------------------------------------------- 1 | domains = Domains::make(); 24 | } 25 | 26 | public function index(): Response 27 | { 28 | $settings = IniFile::open('config/program.ini')->get('menu'); 29 | $isGroupDomains = $settings['show_projects_in_groups'] ?? false; 30 | 31 | $sites = $isGroupDomains 32 | ? $this->domains->toGroups() 33 | : $this->domains->toArray(); 34 | 35 | return Response::json([ 36 | 'grouped' => $isGroupDomains, 37 | 'sites' => $sites, 38 | ]); 39 | } 40 | 41 | public function defaults(): Response 42 | { 43 | return Response::json( 44 | Domain::getDefaults() 45 | ); 46 | } 47 | 48 | public function store(Request $request): Response 49 | { 50 | $host = $request->input('host'); 51 | $data = $request->only(Domain::$params); 52 | 53 | if ($this->domains->has($host)) { 54 | return Response::json()->status(500)->message("Хост $host уже существует"); 55 | } 56 | 57 | $baseDir = $data['base_dir']; 58 | $ospDir = $baseDir.'/.osp'; 59 | 60 | if (!is_dir($ospDir) && !mkdir($ospDir, true) && !is_dir($ospDir)) { 61 | return Response::json()->status(500)->message("Не удалось создать каталог $ospDir"); 62 | } 63 | 64 | $domain = Domain::create($data); 65 | 66 | $domain->save(); 67 | 68 | return Response::json([ 69 | ])->message('Saved'); 70 | } 71 | 72 | public function edit(Request $request): Response 73 | { 74 | $host = $request->get('host'); 75 | 76 | if (!$this->domains->has($host)) { 77 | return Response::json()->status(404)->message('Хост не найден'); 78 | } 79 | 80 | $site = $this->domains->get($host)->toArray(); 81 | 82 | return Response::json([ 83 | 'host' => $host, 84 | 'site' => $site, 85 | ]); 86 | } 87 | 88 | public function save(Request $request): Response 89 | { 90 | $oldHost = $request->input('old_host'); 91 | $host = $request->input('host'); 92 | 93 | $data = $request->only(Domain::$params); 94 | 95 | if ($oldHost !== $host && $this->domains->has($host)) { 96 | return Response::json()->status(500)->message("Хост $host уже существует"); 97 | } 98 | 99 | $this->domains->get($oldHost)->delete(); 100 | Domain::create($data)->save(); 101 | 102 | return Response::json([ 103 | ])->message('Saved'); 104 | } 105 | 106 | public function delete(Request $request): Response 107 | { 108 | try { 109 | $host = $request->input('host'); 110 | 111 | if (!$this->domains->has($host)) { 112 | return Response::json()->status(404)->message('Хост не найден'); 113 | } 114 | 115 | $this->domains->get($host)->delete(); 116 | 117 | return Response::json()->message('Сайт удалён'); 118 | 119 | } catch (\Exception $e) { 120 | 121 | return Response::json() 122 | ->status(500) 123 | ->message($e->getMessage()); 124 | 125 | } 126 | } 127 | 128 | public function openConsole(Request $request): Response 129 | { 130 | $host = $request->input('host'); 131 | 132 | Http::apiCall('cli/'.$host); 133 | 134 | // $file = ROOT_DIR.'/bin/osp__exec.bat'; 135 | // file_put_contents($file, "osp project $host".PHP_EOL); 136 | // 137 | // $cmd = str_replace('/', DIRECTORY_SEPARATOR, $file); 138 | // 139 | // pclose(popen("start $cmd", 'r')); 140 | 141 | return Response::json(); 142 | } 143 | 144 | private function updateDomain(string $host, array $data, ?string $oldHost = null): Response 145 | { 146 | try { 147 | 148 | if (!$this->domains->has($oldHost ?? $host)) { 149 | throw new \RuntimeException('Ошибка: Хост ['.htmlspecialchars($oldHost ?? $host).'] отсутствует.'); 150 | } 151 | 152 | $this->domains 153 | ->update($oldHost, $data) 154 | ->save(); 155 | 156 | return Response::json()->message('Сайт сохранён'); 157 | 158 | } catch (\Exception $e) { 159 | 160 | return Response::json() 161 | ->status(500) 162 | ->message($e->getMessage()); 163 | 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Services/IniFile.php: -------------------------------------------------------------------------------- 1 | filename = $absolute 19 | ? $filename 20 | : ROOT_DIR.'/'.ltrim($filename, '/'); 21 | } 22 | 23 | public static function make(string $filename, bool $absolute = false): IniFile 24 | { 25 | return new self($filename, $absolute); 26 | } 27 | 28 | public static function open(string $filename, bool $absolute = false): IniFile 29 | { 30 | return (new self($filename, $absolute)) 31 | ->read(); 32 | } 33 | 34 | public static function castValue(string $value): float|bool|int|string|null 35 | { 36 | if (in_array(strtolower($value), ['on', 'true', 'yes'])) { 37 | return true; 38 | } 39 | if (in_array(strtolower($value), ['off', 'false', 'no'])) { 40 | return false; 41 | } 42 | if (strtolower($value) === 'null') { 43 | return null; 44 | } 45 | if (is_numeric($value)) { 46 | if (str_contains($value, '.')) { 47 | return (float)$value; 48 | } 49 | 50 | return (int)$value; 51 | } 52 | 53 | return $value; 54 | } 55 | 56 | public static function iniValue(float|bool|int|string|null $value): string 57 | { 58 | return match (true) { 59 | is_bool($value) => $value ? 'on' : 'off', 60 | is_null($value) => 'null', 61 | is_string($value) => str_replace('\\\\', '\\', $value), 62 | default => (string)$value 63 | }; 64 | } 65 | 66 | public function get(string $section = null): array 67 | { 68 | return $section ? $this->data[$section] ?? [] : $this->data; 69 | } 70 | 71 | public function set(array $data): IniFile 72 | { 73 | $this->data = $data; 74 | 75 | return $this; 76 | } 77 | 78 | public function read(): IniFile 79 | { 80 | if (!file_exists($this->filename)) { 81 | return $this; 82 | } 83 | 84 | $iniString = implode(PHP_EOL, 85 | array_map(static function ($s) { 86 | $s = trim($s); 87 | if (!str_contains($s, '=')) return $s; 88 | [$k, $v] = explode('=', $s); 89 | $v = trim($v); 90 | 91 | if (str_contains($v, ';')) $v = '"'.$v.'"'; 92 | 93 | return $k.' = '.$v; 94 | }, file($this->filename)) 95 | ); 96 | 97 | $iniSections = parse_ini_string($iniString, true, INI_SCANNER_RAW); 98 | 99 | $this->data = []; 100 | 101 | foreach ($iniSections as $section => $params) { 102 | $this->data[$section] = []; 103 | foreach ($params as $key => $value) { 104 | $this->data[$section][$key] = self::castValue($value); 105 | } 106 | } 107 | 108 | return $this; 109 | } 110 | 111 | public function write(bool $totalAlign = true): void 112 | { 113 | $ini = ''; 114 | 115 | if (!empty($this->data)) { 116 | $length = $this->maxLengthKey($this->data); 117 | 118 | if ($totalAlign) { 119 | $lengths = []; 120 | foreach ($this->data as $k => $item) { 121 | if (is_array($item)) { 122 | $lengths[] = $this->maxLengthKey($item); 123 | } else { 124 | $lengths[] = strlen($k); 125 | } 126 | } 127 | $length = max($lengths); 128 | } 129 | 130 | foreach ($this->data as $k => $item) { 131 | if (is_array($item)) { 132 | $ini .= PHP_EOL.'['.$k.']'.PHP_EOL.PHP_EOL; 133 | $len = $totalAlign ? $length : max($this->maxLengthKey($item), $length); 134 | foreach ($item as $key => $value) { 135 | $ini .= $key.str_repeat(' ', $len - strlen($key)).' = '.self::iniValue($value).PHP_EOL; 136 | } 137 | } else { 138 | $ini .= $k.str_repeat(' ', $length - strlen($k)).' = '.self::iniValue($item).PHP_EOL; 139 | } 140 | } 141 | } 142 | 143 | file_put_contents($this->filename, ltrim($ini)); 144 | } 145 | 146 | public function update(string $section, array $data): IniFile 147 | { 148 | if (!array_key_exists($section, $this->data)) { 149 | $this->data[$section] = []; 150 | } 151 | foreach ($data as $key => $value) { 152 | $this->data[$section][$key] = $value; 153 | } 154 | 155 | return $this; 156 | } 157 | 158 | public function replace(string $section, array $data): IniFile 159 | { 160 | $this->data[$section] = $data; 161 | 162 | return $this; 163 | } 164 | 165 | public function delete(string $section): IniFile 166 | { 167 | unset($this->data[$section]); 168 | 169 | return $this; 170 | } 171 | 172 | public function save(): void 173 | { 174 | $this->write(); 175 | } 176 | 177 | private function maxLengthKey(array $array): int 178 | { 179 | return array_reduce( 180 | array_keys($array), 181 | static fn($carry, $key) => max($carry, strlen($key)), 182 | 0 183 | ); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /home/api.ospanel/core/Services/Domains.php: -------------------------------------------------------------------------------- 1 | readConfig(); 27 | } 28 | 29 | /** 30 | * @return \OpenServer\DTO\Domain[] 31 | */ 32 | public function getAll(): array 33 | { 34 | return $this->domains; 35 | } 36 | 37 | /** 38 | * @return \OpenServer\DTO\Domain[] 39 | */ 40 | public function toArray(): array 41 | { 42 | $domains = $this->filterDomains(); 43 | 44 | usort($domains, static function (Domain $a, Domain $b) { 45 | if ($a->host === $b->host) return 0; 46 | 47 | return $a->host < $b->host ? -1 : 1; 48 | }); 49 | 50 | return array_map(static fn(Domain $d) => $d->toArray(), $domains); 51 | } 52 | 53 | public function toGroups(): array 54 | { 55 | $result = []; 56 | 57 | foreach ($this->filterDomains() as $item) { 58 | if (str_contains($item->host, '.')) { 59 | [, $group] = explode('.', $item->host, 2); 60 | } else { 61 | $group = TLD; 62 | } 63 | 64 | if (!array_key_exists($group, $result)) $result[$group] = []; 65 | 66 | $result[$group][$item->host] = $item; 67 | } 68 | 69 | $result = $this->groupSubDomains($result); 70 | 71 | ksort($result); 72 | 73 | return $result; 74 | } 75 | 76 | public function has(string $host): bool 77 | { 78 | return array_key_exists($host, $this->domains); 79 | } 80 | 81 | public function get(string $host): Domain 82 | { 83 | return $this->domains[$host]; 84 | } 85 | 86 | public function update(string $oldHost, array $data): static 87 | { 88 | $domain = new Domain($this->domains[$oldHost]->getRawData()); 89 | $domain->update($data); 90 | unset($this->domains[$oldHost]); 91 | $this->domains[$domain->host] = $domain; 92 | 93 | return $this; 94 | } 95 | 96 | public function save(): void 97 | { 98 | $iniData = []; 99 | 100 | foreach ($this->domains as $domain) { 101 | $iniData[$domain->host] = array_filter( 102 | $domain->getRawData(), 103 | static fn($value, $key) => $key !== 'host' && $value !== null, 104 | ARRAY_FILTER_USE_BOTH 105 | ); 106 | } 107 | 108 | IniFile::open('config/domains.ini')->set($iniData)->write(20); 109 | } 110 | 111 | public function count(): int 112 | { 113 | return count($this->filterDomains()); 114 | } 115 | 116 | public function countDisabled(): int 117 | { 118 | return count( 119 | array_filter($this->filterDomains(), static function (Domain $d) { 120 | return $d->enabled === false; 121 | }) 122 | ); 123 | } 124 | 125 | public function countProblems(): int 126 | { 127 | return count( 128 | array_filter($this->filterDomains(), static function (Domain $d) { 129 | return !$d->isAvailable() || !$d->isValidRoot(); 130 | }) 131 | ); 132 | } 133 | 134 | /** 135 | * @return \OpenServer\DTO\Domain[] 136 | */ 137 | private function filterDomains(): array 138 | { 139 | return array_filter($this->domains, static fn(Domain $d) => $d->host !== API_DOMAIN); 140 | } 141 | 142 | private function groupSubdomains(array $input): array 143 | { 144 | $result = []; 145 | foreach ($input as $groupName => $group) { 146 | $parts = explode('.', $groupName); 147 | $result[$groupName] = $group; 148 | if ((count($parts) > 1) && isset($input[$parts[1]][$groupName])) { 149 | $result[$groupName][$groupName] = $input[$parts[1]][$groupName] ?? null; 150 | } 151 | } 152 | 153 | foreach ($result as $groupName => &$group) { 154 | foreach ($group as $domainName => $domain) { 155 | if (array_key_exists($domainName, $result) && $domainName !== $groupName) unset($group[$domainName]); 156 | if ($domainName === TLD) unset($group[$domainName]); 157 | } 158 | } 159 | unset($group); 160 | 161 | 162 | foreach ($result as $name => $group) { 163 | usort($group, static function (?Domain $a, ?Domain $b) { 164 | if (!$a || !$b) return 0; 165 | $d1 = $a->host; 166 | $d2 = $b->host; 167 | $l1 = count(explode('.', $d1)); 168 | $l2 = count(explode('.', $d2)); 169 | if ($d1 === $d2) return 0; 170 | if ($l1 === $l2) return $d1 < $d2 ? -1 : 1; 171 | 172 | return $l1 - $l2; 173 | }); 174 | 175 | $hasActive = false; 176 | $group = array_map(static function (Domain $d) use (&$hasActive) { 177 | if ($d->enabled && $d->isAvailable() && $d->isValidRoot()) $hasActive = true; 178 | 179 | return $d->toArray(); 180 | }, $group); 181 | 182 | $result[$name] = [ 183 | 'name' => $name, 184 | 'hasActive' => $hasActive, 185 | 'domains' => $group, 186 | ]; 187 | } 188 | 189 | return array_filter($result, static fn(array $group) => count($group['domains'])); 190 | } 191 | 192 | private function readConfig(): void 193 | { 194 | try { 195 | $domainsData = Api::make()->getProjects(); 196 | } catch (JsonException $e) { 197 | $domainsData = []; 198 | } 199 | 200 | foreach ($domainsData as $host => $domain) { 201 | $this->domains[$host] = new Domain(['host' => $host, ...$domain]); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /home/api.ospanel/core/DTO/Domain.php: -------------------------------------------------------------------------------- 1 | host_aliases 22 | * @property-read Array host_modules 23 | * @property-read string ip 24 | * @property-read string nginx_engine 25 | * @property-read string node_engine 26 | * @property-read string php_engine 27 | * @property-read string project_dir 28 | * @property-read string project_url 29 | * @property-read string public_dir 30 | * @property-read string realhost 31 | * @property-read bool ssl 32 | * @property-read string ssl_cert_file 33 | * @property-read string ssl_key_file 34 | * @property-read string start_command 35 | * @property-read string tag 36 | * @property-read string terminal_codepage 37 | * @property-read string webhost 38 | * 39 | * @property-read string admin_path 40 | */ 41 | class Domain 42 | { 43 | private const DEFAULT_CGI_DIR = '{root_dir}/home/{host}/cgi-bin'; 44 | private const DEFAULT_PUBLIC_DIR = '{root_dir}/home/{host}/public_html'; 45 | // 46 | // private const DEFAULT_CERT_FILE = '{root_dir}/user/ssl/default/cert.crt'; 47 | // private const DEFAULT_CERT_KEY = '{root_dir}/user/ssl/default/cert.key'; 48 | // 49 | // private const DEFAULT_AUTO_CERT_FILE = '{root_dir}/data/ssl/domains/{host}/cert.crt'; 50 | // private const DEFAULT_AUTO_CERT_KEY = '{root_dir}/data/ssl/domains/{host}/cert.key'; 51 | 52 | public static array $params = [ 53 | 'host', 54 | 'aliases', 55 | 'enabled', 56 | 'environment', 57 | 'php_engine', 58 | 'nginx_engine', 59 | 'node_engine', 60 | 'ip', 61 | 'base_dir', 62 | 'project_dir', 63 | 'public_dir', 64 | 'ssl', 65 | 'ssl_cert_file', 66 | 'ssl_key_file', 67 | 'start_command', 68 | 'terminal_codepage', 69 | 'admin_path', 70 | ]; 71 | 72 | protected static ?array $defaults = null; 73 | 74 | protected array $config = []; 75 | 76 | protected array $computed = []; 77 | 78 | protected ?Module $phpEngine; 79 | 80 | protected ?Module $nginxEngine; 81 | 82 | public function __construct(array $data, bool $create = false) 83 | { 84 | if (self::$defaults === null) { 85 | self::$defaults = self::getDefaults(); 86 | } 87 | 88 | if ($create) { 89 | $this->config = $data; 90 | } else { 91 | $this->computed = $this->normalizeData($data); 92 | $this->config = IniFile::open($data['base_dir'].'/.osp/project.ini', absolute: true)->get($this->host); 93 | } 94 | 95 | 96 | $modules = Modules::make(); 97 | $this->phpEngine = $modules->get($this->php_engine); 98 | $this->nginxEngine = $modules->get($this->nginx_engine); 99 | } 100 | 101 | public static function getDefaults(): array 102 | { 103 | if (self::$defaults === null) { 104 | self::$defaults = array_filter( 105 | IniFile::open('config/program.ini')->get('projects'), 106 | static fn($value) => $value !== '' && $value !== 'auto', 107 | ); 108 | self::$defaults = array_map( 109 | static fn($val) => is_string($val) ? str_replace('\\', '/', $val) : $val, 110 | self::$defaults 111 | ); 112 | } 113 | 114 | return self::$defaults; 115 | } 116 | 117 | public static function create(array $data): Domain 118 | { 119 | $data = array_map(static fn($val) => is_string($val) ? str_replace('\\', '/', $val) : $val, $data); 120 | 121 | foreach (['public_dir', 'project_dir'] as $key) { 122 | if ($data[$key] ?? '') { 123 | $data[$key] = str_replace($data['base_dir'], '{base_dir}', $data[$key]); 124 | } 125 | } 126 | 127 | $data = array_filter($data, static function ($val, $key) { 128 | if (!$val && in_array($key, [ 129 | 'ssl_cert_file', 130 | 'ssl_key_file', 131 | 'terminal_codepage', 132 | 'start_command', 133 | 'ip', 134 | 'admin_path', 135 | ]) 136 | ) { 137 | return false; 138 | } 139 | 140 | if (in_array($key, ['environment', 'aliases']) 141 | && strtolower(self::$defaults[$key] ?? '') === strtolower($val) 142 | ) { 143 | return false; 144 | } 145 | 146 | if ($val === '' 147 | && (self::$defaults[$key] ?? '') === '' 148 | && in_array($key, ['php_engine', 'nginx_engine', 'node_engine']) 149 | ) { 150 | return false; 151 | } 152 | 153 | return true; 154 | }, ARRAY_FILTER_USE_BOTH); 155 | 156 | return new self($data, true); 157 | } 158 | 159 | public function __get(string $name) 160 | { 161 | return $this->computed[$name] ?? $this->config[$name] ?? null; 162 | } 163 | 164 | public function isValidRoot(): bool 165 | { 166 | return $this->isValidPath($this->public_dir); 167 | } 168 | 169 | public function isAvailable(): bool 170 | { 171 | return $this->isReadyPhpEngine() 172 | && $this->isReadyNginxEngine(); 173 | } 174 | 175 | public function isReadyPhpEngine(): bool 176 | { 177 | return !$this->php_engine || $this->phpEngine?->enabled; 178 | } 179 | 180 | public function isReadyNginxEngine(): bool 181 | { 182 | return !$this->nginx_engine || $this->nginxEngine?->enabled; 183 | } 184 | 185 | public function realIp(): string 186 | { 187 | return (string)$this->phpEngine?->ip(); 188 | } 189 | 190 | public function realPort(): string 191 | { 192 | return (string)$this->phpEngine?->port(); 193 | } 194 | 195 | public function siteUrl(): string 196 | { 197 | return 'http'.($this->ssl ? 's' : '').'://'.$this->host; 198 | } 199 | 200 | public function adminUrl(): string 201 | { 202 | return 'http'.($this->ssl ? 's' : '').'://'.$this->host.'/'.ltrim($this->admin_path, '/'); 203 | } 204 | 205 | public function update(array $data): void 206 | { 207 | $sslEnabled = (bool)($data['ssl'] ?? false); 208 | $baseDir = $data['base_dir']; 209 | 210 | $data = array_filter($data, function ($value, $key) use ($sslEnabled) { 211 | if ($value === null) return false; // Исключаем не заданные параметры 212 | if ($value === '') return false; // Исключаем не заданные параметры 213 | 214 | if ($key === 'host') return false; 215 | 216 | // Исключаем параметры со значениями по умолчанию 217 | if ($this->isDefault($key, $value) && !$this->has($key)) { 218 | return false; 219 | } 220 | // if ($key === 'enabled' && $value === true) return false; 221 | // if ($key === 'auto_configure' && $value === true) return false; 222 | // if ($key === 'ssl' && $value === false) return false; 223 | // if ($key === 'project_use_sys_env' && $value === false) return false; 224 | // if ($key === 'ip' && $value === 'auto') return false; 225 | // if ($key === 'log_format' && $value === 'combined') return false; 226 | 227 | // 228 | // $defaultPublicDir = $this->resolvePath(self::DEFAULT_PUBLIC_DIR); 229 | // $defaultCgiDir = $this->resolvePath(self::DEFAULT_CGI_DIR); 230 | // 231 | // if ($key === 'public_dir' && 232 | // $defaultPublicDir === $value 233 | // ) { 234 | // return false; 235 | // } 236 | // if ($key === 'public_dir' && $defaultPublicDir === $this->resolvePath($value)) { 237 | // return false; 238 | // } 239 | // 240 | // if ($key === 'cgi_dir' && 241 | // !array_key_exists($key, $this->rawData) && 242 | // $defaultCgiDir === $this->cgi_dir 243 | // ) { 244 | // return false; 245 | // } 246 | // 247 | // if (!$sslEnabled) { 248 | // if ($key === 'ssl_auto_cert') return false; 249 | // if ($key === 'ssl_cert_file') return false; 250 | // if ($key === 'ssl_key_file') return false; 251 | // } 252 | 253 | return true; 254 | }, ARRAY_FILTER_USE_BOTH); 255 | 256 | $this->config = $data; 257 | } 258 | 259 | public function save(): void 260 | { 261 | $config = $this->config; 262 | $host = $this->host; 263 | unset($config['host'], $config['base_dir']); 264 | IniFile::open($this->base_dir.'/.osp/project.ini', true) 265 | ->replace($host, $config) 266 | ->write(); 267 | } 268 | 269 | public function delete(): void 270 | { 271 | IniFile::open($this->base_dir.'/.osp/project.ini', true) 272 | ->delete($this->host) 273 | ->write(); 274 | } 275 | // public function getRawData(): array 276 | // { 277 | // return $this->rawData; 278 | // } 279 | 280 | public function toArray(): array 281 | { 282 | return [ 283 | 'host' => $this->host, 284 | 285 | 'isActive' => $this->enabled && $this->isAvailable() && $this->isValidRoot(), 286 | 'isProblem' => $this->enabled && !($this->isAvailable() && $this->isValidRoot()), 287 | 'isDisabled' => !$this->enabled, 288 | 'adminUrl' => $this->admin_path ? $this->adminUrl() : null, 289 | 'siteUrl' => $this->siteUrl(), 290 | 'isValidRoot' => $this->isValidRoot(), 291 | 'isAvailable' => $this->isAvailable(), 292 | 'isReadyPhpEngine' => $this->isReadyPhpEngine(), 293 | 'isReadyNginxEngine' => $this->isReadyNginxEngine(), 294 | 295 | 'config' => [ 296 | ...$this->config, 297 | //'admin_path' => $this->admin_path, 298 | ], 299 | 300 | 'computed' => [ 301 | ...$this->computed, 302 | ], 303 | 'defaults' => self::$defaults, 304 | ]; 305 | } 306 | 307 | public function getConfig(): array 308 | { 309 | return $this->config; 310 | } 311 | 312 | private function isValidPath(?string $path): bool 313 | { 314 | return file_exists($this->resolvePath($path)); 315 | } 316 | 317 | private function resolvePath(?string $path): string 318 | { 319 | $replace = [ 320 | '{root_dir}' => ROOT_DIR, 321 | '{host}' => $this->host, 322 | '/' => '\\', 323 | ]; 324 | 325 | return str_replace(array_keys($replace), array_values($replace), $path ?? ''); 326 | } 327 | 328 | private function normalizeData(array $data): array 329 | { 330 | $result = []; 331 | 332 | foreach ($data as $key => $value) { 333 | $result[$key] = match ($value) { 334 | 'False' => false, 335 | 'True' => true, 336 | default => $value, 337 | }; 338 | } 339 | 340 | return $result; 341 | } 342 | 343 | private function has(string $key): bool 344 | { 345 | return array_key_exists($key, $this->config); 346 | } 347 | 348 | private function isDefault(string $key, $value): bool 349 | { 350 | if (array_key_exists($key, $this->computed)) { 351 | return $this->computed[$key] === $value; 352 | } 353 | 354 | return false; 355 | } 356 | } 357 | --------------------------------------------------------------------------------