HestiaApp.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. <?php
  2. declare(strict_types=1);
  3. namespace Hestia\System;
  4. use function Hestiacp\quoteshellarg\quoteshellarg;
  5. class HestiaApp
  6. {
  7. protected const TMPDIR_DOWNLOADS="/tmp/hestia-webapp";
  8. protected $phpsupport = false;
  9. public function __construct()
  10. {
  11. @mkdir(self::TMPDIR_DOWNLOADS);
  12. }
  13. public function run(string $cmd, $args, &$cmd_result=null): bool
  14. {
  15. $cli_script = realpath(HESTIA_DIR_BIN . $cmd);
  16. if (!str_starts_with((string) $cli_script, HESTIA_DIR_BIN)) {
  17. $errstr = "$cmd is trying to traverse outside of " . HESTIA_DIR_BIN;
  18. trigger_error($errstr);
  19. throw new \Exception($errstr);
  20. }
  21. $cli_script = '/usr/bin/sudo ' . quoteshellarg($cli_script);
  22. $cli_arguments = '';
  23. if (!empty($args) && is_array($args)) {
  24. foreach ($args as $arg) {
  25. $cli_arguments .= quoteshellarg((string)$arg) . ' ';
  26. }
  27. } else {
  28. $cli_arguments = quoteshellarg($args);
  29. }
  30. exec($cli_script . ' ' . $cli_arguments . ' 2>&1', $output, $exit_code);
  31. $result['code'] = $exit_code;
  32. $result['args'] = $cli_arguments;
  33. $result['raw'] = $output;
  34. $result['text'] = implode(PHP_EOL, $output);
  35. $result['json'] = json_decode($result['text'], true);
  36. $cmd_result = (object)$result;
  37. if ($exit_code > 0) {
  38. //log error message in nginx-error.log
  39. trigger_error($result['text']);
  40. //throw exception if command fails
  41. throw new \Exception($result['text']);
  42. }
  43. return ($exit_code === 0);
  44. }
  45. public function runUser(string $cmd, $args, &$cmd_result=null): bool
  46. {
  47. if (!empty($args) && is_array($args)) {
  48. array_unshift($args, $this->user());
  49. } else {
  50. $args = [$this->user(), $args];
  51. }
  52. return $this->run($cmd, $args, $cmd_result);
  53. }
  54. public function installComposer($version)
  55. {
  56. exec("curl https://composer.github.io/installer.sig", $output);
  57. $signature = implode(PHP_EOL, $output);
  58. if (empty($signature)) {
  59. throw new \Exception("Error reading composer signature");
  60. }
  61. $composer_setup = self::TMPDIR_DOWNLOADS . DIRECTORY_SEPARATOR . 'composer-setup-' . $signature . '.php';
  62. exec("wget https://getcomposer.org/installer --quiet -O " . quoteshellarg($composer_setup), $output, $return_code);
  63. if ($return_code !== 0) {
  64. throw new \Exception("Error downloading composer");
  65. }
  66. if ($signature !== hash_file('sha384', $composer_setup)) {
  67. unlink($composer_setup);
  68. throw new \Exception("Invalid composer signature");
  69. }
  70. $install_folder = $this->getUserHomeDir() . DIRECTORY_SEPARATOR . '.composer';
  71. if (!file_exists($install_folder)) {
  72. exec(HESTIA_CMD .'v-rebuild-user '.$this -> user(), $output, $return_code);
  73. if ($return_code !== 0) {
  74. throw new \Exception("Unable to rebuild user");
  75. }
  76. }
  77. $this->runUser('v-run-cli-cmd', ["/usr/bin/php", $composer_setup, "--quiet", "--install-dir=".$install_folder, "--filename=composer", "--$version" ], $status);
  78. unlink($composer_setup);
  79. if ($status->code !== 0) {
  80. throw new \Exception("Error installing composer");
  81. }
  82. }
  83. public function updateComposer($version)
  84. {
  85. $this->runUser('v-run-cli-cmd', ["composer", "selfupdate","--$version"]);
  86. }
  87. public function runComposer($args, &$cmd_result=null, $version=2): bool
  88. {
  89. $composer = $this->getUserHomeDir() . DIRECTORY_SEPARATOR . '.composer' . DIRECTORY_SEPARATOR . 'composer';
  90. if (!is_file($composer)) {
  91. $this->installComposer($version);
  92. } else {
  93. $this->updateComposer($version);
  94. }
  95. if (!empty($args) && is_array($args)) {
  96. array_unshift($args, 'composer');
  97. } else {
  98. $args = ['composer', $args];
  99. }
  100. return $this->runUser('v-run-cli-cmd', $args, $cmd_result);
  101. }
  102. public function runWp($args, &$cmd_result=null): bool
  103. {
  104. $wp = $this->getUserHomeDir() . DIRECTORY_SEPARATOR . '.wp-cli' . DIRECTORY_SEPARATOR . 'wp';
  105. if (!is_file($wp)) {
  106. $this -> runUser('v-add-user-wp-cli', []);
  107. } else {
  108. $this->runUser('v-run-cli-cmd', [$wp, 'cli', 'update']);
  109. }
  110. array_unshift($args, $wp);
  111. return $this->runUser('v-run-cli-cmd', $args, $cmd_result);
  112. }
  113. // Logged in user
  114. public function realuser(): string
  115. {
  116. return $_SESSION['user'];
  117. }
  118. // Effective user
  119. public function user(): string
  120. {
  121. $user = $this->realuser();
  122. if ($_SESSION['userContext'] === 'admin' && !empty($_SESSION['look'])) {
  123. $user = $_SESSION['look'];
  124. }
  125. if (strpos($user, DIRECTORY_SEPARATOR) !== false) {
  126. throw new \Exception("illegal characters in username");
  127. }
  128. return $user;
  129. }
  130. public function getUserHomeDir()
  131. {
  132. $info = posix_getpwnam($this->user());
  133. return $info['dir'];
  134. }
  135. public function userOwnsDomain(string $domain): bool
  136. {
  137. return $this->runUser('v-list-web-domain', [$domain, 'json']);
  138. }
  139. public function checkDatabaseLimit(){
  140. $status = $this -> runUser('v-list-user', ['json'], $result);
  141. $result -> json[$this -> user()];
  142. if($result -> json[$this -> user()]['DATABASES'] != "unlimited" ){
  143. if($result -> json[$this -> user()]['DATABASES'] - $result -> json[$this -> user()]['U_DATABASES'] < 1){
  144. return false;
  145. }
  146. }
  147. return true;
  148. }
  149. public function databaseAdd(string $dbname, string $dbuser, string $dbpass, string $charset = 'utf8mb4')
  150. {
  151. $v_password = tempnam("/tmp", "hst");
  152. $fp = fopen($v_password, "w");
  153. fwrite($fp, $dbpass."\n");
  154. fclose($fp);
  155. $status = $this->runUser('v-add-database', [$dbname, $dbuser, $v_password, 'mysql', 'localhost', $charset]);
  156. if(!$status){
  157. $this->errors[] = _('Unable to add database!');
  158. }
  159. unlink($v_password);
  160. return $status;
  161. }
  162. public function getCurrentBackendTemplate(string $domain){
  163. $status = $this->runUser('v-list-web-domain', [$domain, 'json'],$return_message);
  164. $version = $return_message -> json[$domain]['BACKEND'];
  165. $test= preg_match('/^.*PHP-([0-9])\_([0-9])/',$version, $match);
  166. return $match[1].'.'.$match[2];
  167. }
  168. public function changeWebTemplate(string $domain, string $template)
  169. {
  170. $status = $this->runUser('v-change-web-domain-tpl', [$domain, $template]);
  171. }
  172. public function changeBackendTemplate(string $domain, string $template)
  173. {
  174. $status = $this->runUser('v-change-web-domain-backend-tpl', [$domain, $template]);
  175. }
  176. public function listSuportedPHP()
  177. {
  178. if (!$this -> phpsupport) {
  179. $status = $this -> run('v-list-sys-php', 'json', $result);
  180. $this -> phpsupport = $result -> json;
  181. }
  182. return $this -> phpsupport;
  183. }
  184. /*
  185. Return highest available supported php version
  186. Eg: Package requires: 7.3 or 7.4 and system has 8.0 and 7.4 it will return 7.4
  187. Package requires: 8.0 or 8.1 and system has 8.0 and 7.4 it will return 8.0
  188. Package requires: 7.4 or 8.0 and system has 8.0 and 7.4 it will return 8.0
  189. If package isn't supported by the available php version false will returned
  190. */
  191. public function getSupportedPHP($support)
  192. {
  193. $versions = $this -> listSuportedPHP();
  194. $supported = false;
  195. $supported_versions = array();
  196. foreach ($versions as $version) {
  197. if (in_array($version, $support)) {
  198. $supported = true;
  199. $supported_versions[] = $version;
  200. }
  201. }
  202. if($supported){
  203. return $supported_versions;
  204. }else{
  205. return false;
  206. }
  207. }
  208. public function getWebDomainIp(string $domain)
  209. {
  210. $this->runUser('v-list-web-domain', [$domain, 'json'], $result);
  211. $ip = $result->json[$domain]['IP'];
  212. return filter_var($ip, FILTER_VALIDATE_IP);
  213. }
  214. public function getWebDomainPath(string $domain)
  215. {
  216. return Util::join_paths($this->getUserHomeDir(), "web", $domain);
  217. }
  218. public function downloadUrl(string $src, $path=null, &$result=null)
  219. {
  220. if (strpos($src, 'http://') !== 0 &&
  221. strpos($src, 'https://')!== 0) {
  222. return false;
  223. }
  224. exec("/usr/bin/wget --tries 3 --timeout=30 --no-dns-cache -nv " . quoteshellarg($src). " -P " . quoteshellarg(self::TMPDIR_DOWNLOADS) . ' 2>&1', $output, $return_var);
  225. if ($return_var !== 0) {
  226. return false;
  227. }
  228. if (!preg_match('/URL:\s*(.+?)\s*\[(.+?)\]\s*->\s*"(.+?)"/', implode(PHP_EOL, $output), $matches)) {
  229. return false;
  230. }
  231. if (empty($matches) || count($matches) != 4) {
  232. return false;
  233. }
  234. $status['url'] = $matches[1];
  235. $status['file'] = $matches[3];
  236. $result = (object)$status;
  237. return true;
  238. }
  239. public function archiveExtract(string $src, string $path, $skip_components=null)
  240. {
  241. if (empty($path)) {
  242. throw new \Exception("Error extracting archive: missing target folder");
  243. }
  244. if (realpath($src)) {
  245. $archive_file = $src;
  246. } else {
  247. if (!$this->downloadUrl($src, null, $download_result)) {
  248. throw new \Exception("Error downloading archive");
  249. }
  250. $archive_file = $download_result->file;
  251. }
  252. $result = $this->runUser('v-extract-fs-archive', [$archive_file, $path, null, $skip_components]);
  253. unlink($archive_file);
  254. return $result;
  255. }
  256. }