installer.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. <?php
  2. function join_paths() {
  3. $paths = array();
  4. foreach (func_get_args() as $arg) {
  5. if ($arg !== '') { $paths[] = $arg; }
  6. }
  7. return preg_replace('#/+#','/',join('/', $paths));
  8. }
  9. function generate_string($length = 16) {
  10. $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~`!@|#[]$%^&*() _-=+{}:;<>?,./';
  11. $random_string = '';
  12. for($i = 0; $i < $length; $i++) {
  13. $random_string .= $chars[random_int(0, strlen($chars) - 1)];
  14. }
  15. return $random_string;
  16. }
  17. abstract class BaseSetup {
  18. protected $domain;
  19. public function __construct($domain, $appcontext) {
  20. // validate domain name
  21. if(filter_var($domain, FILTER_VALIDATE_DOMAIN) === false) {
  22. throw new Exception("Invalid domain name");
  23. }
  24. $this->domain = $domain;
  25. $this->appcontext = $appcontext;
  26. }
  27. public function getConfig($section=null) {
  28. return (!empty($section))? $this->config[$section] : $this->config;
  29. }
  30. public function getOptions() {
  31. return $this->getConfig('form');
  32. }
  33. public function withDatabase() {
  34. return ($this->getConfig('database') === true);
  35. }
  36. public function getDocRoot($docrelative=null) {
  37. $domain_path = $this->appcontext->getWebDomainPath($this->domain);
  38. if(empty($domain_path)){
  39. return false;
  40. }
  41. return join_paths($domain_path, "public_html", $docrelative);
  42. }
  43. public function retrieveResources() {
  44. return $this->appcontext->archiveExtract(
  45. $this->getConfig('url'),
  46. $this->getDocRoot(), 1);
  47. }
  48. public function install($options) {
  49. return $this->retrieveResources();
  50. }
  51. }
  52. class WordpressSetup extends BaseSetup {
  53. protected $appname = 'wordpress';
  54. protected $config = [
  55. 'form' => [
  56. 'protocol' => [
  57. 'type' => 'select',
  58. 'options' => ['http','ion','https'],
  59. 'value' => 'ion',
  60. ],
  61. 'subdir' => ['type'=>'text', 'value'=>'/'],
  62. 'site_name' => ['type'=>'text', 'value'=>'Wordpress Blog'],
  63. 'site_description' => ['value'=>'Another wordpresss site'],
  64. 'wordpress_account_username' => ['value'=>'wpadmin'],
  65. 'wordpress_account_email' => 'text',
  66. 'wordpress_account_password' => 'password',
  67. ],
  68. 'database' => true,
  69. 'url' => 'https://wordpress.org/wordpress-5.2.2.tar.gz'
  70. ];
  71. public function install($options) {
  72. parent::install($options);
  73. $this->appcontext->runUser('v-open-fs-file',[$this->getDocRoot("wp-config-sample.php")], $result);
  74. $distconfig = preg_replace( [
  75. '/database_name_here/', '/username_here/', '/password_here/'
  76. ], [
  77. $this->appcontext->user() . '_' . $options['database_name'],
  78. $this->appcontext->user() . '_' . $options['database_user'],
  79. $options['database_password']
  80. ],
  81. $result->text);
  82. while (strpos($distconfig, 'put your unique phrase here') !== false) {
  83. $distconfig = preg_replace( '/put your unique phrase here/', generate_string(64), $distconfig, 1);
  84. }
  85. $tmp_configpath = $this->appcontext->saveTempFile($distconfig);
  86. if(!$this->appcontext->runUser('v-copy-fs-file',[$tmp_configpath, $this->getDocRoot("wp-config.php")], $result)) {
  87. return false;
  88. }
  89. exec("/usr/bin/curl --post301 --insecure --resolve ".$this->domain.":80:".$this->appcontext->getWebDomainIp($this->domain)." "
  90. . escapeshellarg("http://".$this->domain."/wp-admin/install.php?step=2")
  91. . " -d " . escapeshellarg(
  92. "weblog_title=" . rawurlencode($options['site_name'])
  93. . "&user_name=" . rawurlencode($options['wordpress_account_username'])
  94. . "&admin_password=" . rawurlencode($options['wordpress_account_password'])
  95. . "&admin_password2=". rawurlencode($options['wordpress_account_password'])
  96. . "&admin_email=" . rawurlencode($options['wordpress_account_email'])), $output, $return_var);
  97. return ($return_var === 0);
  98. }
  99. }
  100. class OpencartSetup extends BaseSetup {
  101. protected $appname = 'opencart';
  102. protected $config = [
  103. 'form' => [
  104. 'protocol' => [
  105. 'type' => 'select',
  106. 'options' => ['http','ion','https'],
  107. 'value' => 'ion',
  108. ],
  109. 'subdir' => ['type'=>'text', 'value'=>'/'],
  110. 'opencart_account_username' => ['value'=>'ocadmin'],
  111. 'opencart_account_email' => 'text',
  112. 'opencart_account_password' => 'password',
  113. ],
  114. 'database' => true,
  115. 'url' => 'https://github.com/opencart/opencart/releases/download/3.0.3.2/opencart-3.0.3.2.zip'
  116. //'url' => 'https://github.com/opencart/opencart/archive/3.0.3.2.tar.gz'
  117. ];
  118. public function retrieveResources() {
  119. #cleanup temp folder
  120. $this->appcontext->runUser('v-delete-fs-directory', [$this->getDocRoot("/tmp-opencart")], $result);
  121. $this->appcontext->archiveExtract($this->getConfig('url'), $this->getDocRoot("/tmp-opencart"), 1);
  122. $this->appcontext->runUser('v-copy-fs-directory',[
  123. $this->getDocRoot("/tmp-opencart/upload/."),
  124. $this->getDocRoot()], $result);
  125. $this->appcontext->runUser('v-delete-fs-directory',[$this->getDocRoot("/tmp-opencart")], $result);
  126. return true;
  127. }
  128. public function install($options) {
  129. parent::install($options);
  130. $this->appcontext->runUser('v-copy-fs-file',[$this->getDocRoot("config-dist.php"), $this->getDocRoot("config.php")]);
  131. $this->appcontext->runUser('v-copy-fs-file',[$this->getDocRoot("admin/config-dist.php"), $this->getDocRoot("admin/config.php")]);
  132. $this->appcontext->runUser('v-change-fs-file-permission',[$this->getDocRoot("config.php"), '666']);
  133. $this->appcontext->runUser('v-change-fs-file-permission',[$this->getDocRoot("admin/config.php"), '666']);
  134. exec("/usr/bin/php " . escapeshellarg($this->getDocRoot("/install/cli_install.php")) . " install"
  135. . " --db_username " . escapeshellarg($this->appcontext->user() . '_' .$options['database_user'])
  136. . " --db_password " . escapeshellarg($options['database_password'])
  137. . " --db_database " . escapeshellarg($this->appcontext->user() . '_' .$options['database_name'])
  138. . " --username " . escapeshellarg($options['opencart_account_username'])
  139. . " --password " . escapeshellarg($options['opencart_account_password'])
  140. . " --email " . escapeshellarg($options['opencart_account_email'])
  141. . " --http_server " . escapeshellarg("http://" . $this->domain . "/")
  142. , $output, $return_var);
  143. $this->appcontext->runUser('v-change-fs-file-permission',[$this->getDocRoot("config.php"), '640']);
  144. $this->appcontext->runUser('v-change-fs-file-permission',[$this->getDocRoot("admin/config.php"), '640']);
  145. return ($return_var === 0);
  146. }
  147. }
  148. class HestiaApp {
  149. protected const TMPDIR_DOWNLOADS="/tmp/hestia-webapp";
  150. public function __construct() {
  151. mkdir(self::TMPDIR_DOWNLOADS);
  152. }
  153. public function run($cmd, $args, &$cmd_result=null) {
  154. $cli_script = HESTIA_CMD . '/' . basename($cmd);
  155. $cli_arguments = '';
  156. if (!empty($args) && is_array($args)) {
  157. foreach ($args as $arg) {
  158. $cli_arguments .= escapeshellarg($arg) . ' ';
  159. }
  160. } else {
  161. $cli_arguments = escapeshellarg($args);
  162. }
  163. exec ($cli_script . ' ' . $cli_arguments, $output, $exit_code);
  164. $result['code'] = $exit_code;
  165. $result['args'] = $cli_arguments;
  166. $result['raw'] = $output;
  167. $result['text'] = implode( PHP_EOL, $output);
  168. $result['json'] = json_decode($result['text'], true);
  169. $cmd_result = (object)$result;
  170. return ($exit_code === 0);
  171. }
  172. public function runUser($cmd, $args, &$cmd_result=null) {
  173. if (!empty($args) && is_array($args)) {
  174. array_unshift($args, $this->user());
  175. }
  176. else {
  177. $args = [$this->user(), $args];
  178. }
  179. return $this->run($cmd, $args, $cmd_result);
  180. }
  181. // Logged in user
  182. public function realuser() {
  183. return $_SESSION['user'];
  184. }
  185. // Effective user
  186. public function user() {
  187. $user = $this->realuser();
  188. if ($user == 'admin' && !empty($_SESSION['look'])) {
  189. $user = $_SESSION['look'];
  190. }
  191. if(strpos($user, DIRECTORY_SEPARATOR) !== false) {
  192. throw new Exception("illegal characthers in username");
  193. }
  194. return $user;
  195. }
  196. public function userOwnsDomain($domain) {
  197. return $this->runUser('v-list-web-domain', [$domain, 'json']);
  198. }
  199. public function databaseAdd($dbname, $dbuser, $dbpass) {
  200. $v_password = tempnam("/tmp","hst");
  201. $fp = fopen($v_password, "w");
  202. fwrite($fp, $dbpass."\n");
  203. fclose($fp);
  204. $status = $this->runUser('v-add-database', [$dbname, $dbuser, $v_password]);
  205. unlink($v_password);
  206. return $status;
  207. }
  208. public function getWebDomainIp($domain) {
  209. $this->runUser('v-list-web-domain', [$domain, 'json'], $result);
  210. $ip = $result->json[$domain]['IP'];
  211. return filter_var($ip, FILTER_VALIDATE_IP);
  212. }
  213. public function getWebDomainPath($domain) {
  214. return join_paths("/home", $this->user() , "/web", $domain);
  215. }
  216. public function downloadUrl($src, $path=null, &$result=null) {
  217. if (strpos($src,'http://') !== 0 &&
  218. strpos($src,'https://')!== 0 ) {
  219. return false;
  220. }
  221. exec("/usr/bin/wget --tries 3 -nv " . escapeshellarg($src). " -P " . escapeshellarg(self::TMPDIR_DOWNLOADS) . ' 2>&1', $output, $return_var);
  222. if ($return_var !== 0) {
  223. return false;
  224. }
  225. if(!preg_match('/URL:\s*(.+?)\s*\[(.+?)\]\s*->\s*"(.+?)"/', implode(PHP_EOL, $output), $matches)) {
  226. return false;
  227. }
  228. if(empty($matches) || count($matches) != 4) {
  229. return false;
  230. }
  231. $status['url'] = $matches[1];
  232. $status['file'] = $matches[3];
  233. $result = (object)$status;
  234. return true;
  235. }
  236. public function archiveExtract($src, $path, $skip_components=null) {
  237. if (file_exists($src)) {
  238. $archive_file = $src;
  239. } else {
  240. if( !$this->downloadUrl($src, null, $download_result) ) {
  241. return false;
  242. }
  243. $archive_file = $download_result->file;
  244. }
  245. $status = $this->runUser('v-extract-fs-archive', [ $archive_file, $path, null, $skip_components]);
  246. unlink($download_result->file);
  247. return status;
  248. }
  249. public function saveTempFile($data) {
  250. $tmp_file = tempnam("/tmp","hst");
  251. chmod($tmp_file, 0644);
  252. if (file_put_contents($tmp_file, $data) > 0) {
  253. return $tmp_file;
  254. }
  255. return false;
  256. }
  257. }
  258. class AppInstaller {
  259. private $domain;
  260. private $appsetup;
  261. private $appcontext;
  262. private $formNamespace = 'webapp';
  263. private $errors;
  264. private $database_config = [
  265. 'database_create' => ['type'=>'boolean', 'value'=>false],
  266. 'database_name' => 'text',
  267. 'database_user' => 'text',
  268. 'database_password' => 'password',
  269. ];
  270. public function __construct($app, $domain, $context) {
  271. $this->domain = $domain;
  272. $this->appcontext = $context;
  273. if (!$this->appcontext->userOwnsDomain($domain)) {
  274. throw new Exception("User does not have access to domain [$domain]");
  275. }
  276. $appclass = ucfirst($app).'Setup';
  277. if (class_exists($appclass)) {
  278. $this->appsetup = new $appclass($domain, $this->appcontext);
  279. }
  280. if (!$this->appsetup) {
  281. throw new Exception( "Application [".ucfirst($app)."] does not have a installer" );
  282. }
  283. }
  284. public function getStatus() {
  285. return $this->errors;
  286. }
  287. public function formNs() {
  288. return $this->formNamespace;
  289. }
  290. public function getOptions() {
  291. if(!$this->appsetup) return;
  292. $options = $this->appsetup->getOptions();
  293. if ($this->appsetup->withDatabase()) {
  294. $options = array_merge($options, $this->database_config);
  295. }
  296. return $options;
  297. }
  298. public function filterOptions($options)
  299. {
  300. $filteredoptions = [];
  301. array_walk($options, function($value, $key) use(&$filteredoptions) {
  302. if (strpos($key, $this->formNs().'_')===0) {
  303. $option = str_replace($this->formNs().'_','',$key);
  304. $filteredoptions[$option] = $value;
  305. }
  306. });
  307. return $filteredoptions;
  308. }
  309. public function execute($options) {
  310. if (!$this->appsetup) return;
  311. $options = $this->filterOptions($options);
  312. $random_num = random_int(10000, 99999);
  313. if ($this->appsetup->withDatabase() && !empty($options['database_create'])) {
  314. if(empty($options['database_name'])) {
  315. $options['database_name'] = $random_num;
  316. }
  317. if(empty($options['database_user'])) {
  318. $options['database_user'] = $random_num;
  319. }
  320. if(empty($options['database_password'])) {
  321. $options['database_password'] = bin2hex(random_bytes(10));
  322. }
  323. if(!$this->appcontext->databaseAdd($options['database_name'], $options['database_user'], $options['database_password'])) {
  324. $this->errors[] = "Error adding database";
  325. return false;
  326. }
  327. }
  328. if(empty($this->errors)) {
  329. return $this->appsetup->install($options);
  330. }
  331. }
  332. }
  333. // TO DO : create a WebDomain model class, hidrate from v-list-web-domain(json)