This commit is contained in:
Alexey Berezhok
2024-03-19 22:05:27 +03:00
commit 346a50856b
1572 changed files with 182163 additions and 0 deletions

View File

@@ -0,0 +1,353 @@
<?php
declare(strict_types=1);
namespace Hestia\System;
use function Hestiacp\quoteshellarg\quoteshellarg;
class HestiaApp {
/** @var string[] */
public $errors;
protected const TMPDIR_DOWNLOADS = "/tmp/hestia-webapp";
protected $phpsupport = false;
public function __construct() {
@mkdir(self::TMPDIR_DOWNLOADS);
}
public function run(string $cmd, $args, &$cmd_result = null): bool {
$cli_script = realpath(HESTIA_DIR_BIN . $cmd);
if (!str_starts_with((string) $cli_script, HESTIA_DIR_BIN)) {
$errstr = "$cmd is trying to traverse outside of " . HESTIA_DIR_BIN;
trigger_error($errstr);
throw new \Exception($errstr);
}
$cli_script = "/usr/bin/sudo " . quoteshellarg($cli_script);
$cli_arguments = "";
if (!empty($args) && is_array($args)) {
foreach ($args as $arg) {
$cli_arguments .= quoteshellarg((string) $arg) . " ";
}
} else {
$cli_arguments = quoteshellarg($args);
}
exec($cli_script . " " . $cli_arguments . " 2>&1", $output, $exit_code);
$result["code"] = $exit_code;
$result["args"] = $cli_arguments;
$result["raw"] = $output;
$result["text"] = implode(PHP_EOL, $output);
$result["json"] = json_decode($result["text"], true);
$cmd_result = (object) $result;
if ($exit_code > 0) {
//log error message in nginx-error.log
trigger_error($result["text"]);
//throw exception if command fails
throw new \Exception($result["text"]);
}
return $exit_code === 0;
}
public function runUser(string $cmd, $args, &$cmd_result = null): bool {
if (!empty($args) && is_array($args)) {
array_unshift($args, $this->user());
} else {
$args = [$this->user(), $args];
}
return $this->run($cmd, $args, $cmd_result);
}
public function installComposer($version) {
exec("curl https://composer.github.io/installer.sig", $output);
$signature = implode(PHP_EOL, $output);
if (empty($signature)) {
throw new \Exception("Error reading composer signature");
}
$composer_setup =
self::TMPDIR_DOWNLOADS . DIRECTORY_SEPARATOR . "composer-setup-" . $signature . ".php";
exec(
"wget https://getcomposer.org/installer --quiet -O " . quoteshellarg($composer_setup),
$output,
$return_code,
);
if ($return_code !== 0) {
throw new \Exception("Error downloading composer");
}
if ($signature !== hash_file("sha384", $composer_setup)) {
unlink($composer_setup);
throw new \Exception("Invalid composer signature");
}
$install_folder = $this->getUserHomeDir() . DIRECTORY_SEPARATOR . ".composer";
if (!file_exists($install_folder)) {
exec(HESTIA_CMD . "v-rebuild-user " . $this->user(), $output, $return_code);
if ($return_code !== 0) {
throw new \Exception("Unable to rebuild user");
}
}
$this->runUser(
"v-run-cli-cmd",
[
"/usr/bin/php",
$composer_setup,
"--quiet",
"--install-dir=" . $install_folder,
"--filename=composer",
"--$version",
],
$status,
);
unlink($composer_setup);
if ($status->code !== 0) {
throw new \Exception("Error installing composer");
}
}
public function updateComposer($version) {
$this->runUser("v-run-cli-cmd", ["composer", "selfupdate", "--$version"]);
}
public function runComposer($args, &$cmd_result = null, $data = []): bool {
$composer =
$this->getUserHomeDir() .
DIRECTORY_SEPARATOR .
".composer" .
DIRECTORY_SEPARATOR .
"composer";
if (!is_file($composer)) {
$this->installComposer($data["version"]);
} else {
$this->updateComposer($data["version"]);
}
if (empty($data["php_version"])) {
$data["php_version"] = "";
}
if (!empty($args) && is_array($args)) {
array_unshift($args, "php" . $data["php_version"], $composer);
} else {
$args = ["php" . $data["php_version"], $composer, $args];
}
return $this->runUser("v-run-cli-cmd", $args, $cmd_result);
}
public function runWp($args, &$cmd_result = null): bool {
$wp =
$this->getUserHomeDir() . DIRECTORY_SEPARATOR . ".wp-cli" . DIRECTORY_SEPARATOR . "wp";
if (!is_file($wp)) {
$this->runUser("v-add-user-wp-cli", []);
} else {
$this->runUser("v-run-cli-cmd", [$wp, "cli", "update"]);
}
array_unshift($args, $wp);
return $this->runUser("v-run-cli-cmd", $args, $cmd_result);
}
// Logged in user
public function realuser(): string {
return $_SESSION["user"];
}
// Effective user
public function user(): string {
$user = $this->realuser();
if ($_SESSION["userContext"] === "admin" && !empty($_SESSION["look"])) {
$user = $_SESSION["look"];
}
if (strpos($user, DIRECTORY_SEPARATOR) !== false) {
throw new \Exception("illegal characters in username");
}
return $user;
}
public function getUserHomeDir() {
$info = posix_getpwnam($this->user());
return $info["dir"];
}
public function userOwnsDomain(string $domain): bool {
return $this->runUser("v-list-web-domain", [$domain, "json"]);
}
public function checkDatabaseLimit() {
$status = $this->runUser("v-list-user", ["json"], $result);
$result->json[$this->user()];
if ($result->json[$this->user()]["DATABASES"] != "unlimited") {
if (
$result->json[$this->user()]["DATABASES"] -
$result->json[$this->user()]["U_DATABASES"] <
1
) {
return false;
}
}
return true;
}
public function databaseAdd(
string $dbname,
string $dbuser,
string $dbpass,
string $dbtype = "mysql",
string $charset = "utf8mb4",
) {
$v_password = tempnam("/tmp", "hst");
$fp = fopen($v_password, "w");
fwrite($fp, $dbpass . "\n");
fclose($fp);
$status = $this->runUser("v-add-database", [
$dbname,
$dbuser,
$v_password,
$dbtype,
"localhost",
$charset,
]);
if (!$status) {
$this->errors[] = _("Unable to add database!");
}
unlink($v_password);
return $status;
}
public function getCurrentBackendTemplate(string $domain) {
$status = $this->runUser("v-list-web-domain", [$domain, "json"], $return_message);
$version = $return_message->json[$domain]["BACKEND"];
if (!empty($version)) {
if ($version != "default") {
$test = preg_match("/^.*PHP-([0-9])\_([0-9])/", $version, $match);
return $match[1] . "." . $match[2];
} else {
$supported = $this->run("v-list-sys-php", "json", $result);
return $result->json[0];
}
} else {
$supported = $this->run("v-list-sys-php", "json", $result);
return $result->json[0];
}
}
public function changeWebTemplate(string $domain, string $template) {
$status = $this->runUser("v-change-web-domain-tpl", [$domain, $template]);
}
public function changeBackendTemplate(string $domain, string $template) {
$status = $this->runUser("v-change-web-domain-backend-tpl", [$domain, $template]);
}
public function listSuportedPHP() {
if (!$this->phpsupport) {
$status = $this->run("v-list-sys-php", "json", $result);
$this->phpsupport = $result->json;
}
return $this->phpsupport;
}
/*
Return highest available supported php version
Eg: Package requires: 7.3 or 7.4 and system has 8.0 and 7.4 it will return 7.4
Package requires: 8.0 or 8.1 and system has 8.0 and 7.4 it will return 8.0
Package requires: 7.4 or 8.0 and system has 8.0 and 7.4 it will return 8.0
If package isn't supported by the available php version false will returned
*/
public function getSupportedPHP($support) {
$versions = $this->listSuportedPHP();
$supported = false;
$supported_versions = [];
foreach ($versions as $version) {
if (in_array($version, $support)) {
$supported = true;
$supported_versions[] = $version;
}
}
if ($supported) {
return $supported_versions;
} else {
return false;
}
}
public function getWebDomainIp(string $domain) {
$this->runUser("v-list-web-domain", [$domain, "json"], $result);
$ip = $result->json[$domain]["IP"];
return filter_var($ip, FILTER_VALIDATE_IP);
}
public function getWebDomainPath(string $domain) {
return Util::join_paths($this->getUserHomeDir(), "web", $domain);
}
public function downloadUrl(string $src, $path = null, &$result = null) {
if (strpos($src, "http://") !== 0 && strpos($src, "https://") !== 0) {
return false;
}
exec(
"/usr/bin/wget --tries 3 --timeout=30 --no-dns-cache -nv " .
quoteshellarg($src) .
" -P " .
quoteshellarg(self::TMPDIR_DOWNLOADS) .
" 2>&1",
$output,
$return_var,
);
if ($return_var !== 0) {
return false;
}
if (
!preg_match(
'/URL:\s*(.+?)\s*\[(.+?)\]\s*->\s*"(.+?)"/',
implode(PHP_EOL, $output),
$matches,
)
) {
return false;
}
if (empty($matches) || count($matches) != 4) {
return false;
}
$status["url"] = $matches[1];
$status["file"] = $matches[3];
$result = (object) $status;
return true;
}
public function archiveExtract(string $src, string $path, $skip_components = null) {
if (empty($path)) {
throw new \Exception("Error extracting archive: missing target folder");
}
if (realpath($src)) {
$archive_file = $src;
} else {
if (!$this->downloadUrl($src, null, $download_result)) {
throw new \Exception("Error downloading archive");
}
$archive_file = $download_result->file;
}
$result = $this->runUser("v-extract-fs-archive", [
$archive_file,
$path,
null,
$skip_components,
]);
unlink($archive_file);
return $result;
}
}