Initial
This commit is contained in:
353
web/src/app/System/HestiaApp.php
Normal file
353
web/src/app/System/HestiaApp.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user