Просмотр исходного кода

Improvements Quick installer (#2503)

* #2468 Fix mixed ssl content OC

* Add PHP version detection to Quick installers

Change version if need to a supported version
Update Quick installers in general
Update versions for MediaWiki, Prestashop and Nextcloud

* Bug in wordpress installer

* Set drupal installer to the correct dir

* Use Supported php version to install the software

Prestashop refuses to load with php8.0

* install into specific directory

I add the option to install to specific directory, but i dont know if there is a charset filtrate function

* I add a number an letters filters only

adding a filter numbers and letters

* adding directory and other

i add directory form text input , utf8mb4 instead of utf8 and random prefix table

* Update WordpressSetup.php

* databaseAdd utf8mb4 by default

adding utf8mb4 on apps by default

* Update HestiaApp.php

* Change error message when Quick install is disabled

* Add French to list 

And "Remove" code that hides submit button

* Typo and set default language to en_US

* Add error message why app is not availble

Also resolved preformace issue v-list-sys-php caused a lot of delays as it got looked every time a new app was loaded
With the current 8 apps it was manageble how ever could go out of hand quickly

Co-authored-by: Leonardo Allende Pasten <alnux@users.noreply.github.com>
Jaap Marcus 3 лет назад
Родитель
Сommit
1084a16e7d

+ 1 - 0
bin/v-run-cli-cmd

@@ -66,6 +66,7 @@ if [ "$basecmd" != 'ps'         -a \
      "$basecmd" != 'php7.4'     -a \
      "$basecmd" != 'php8.0'     -a \
      "$basecmd" != 'php'        -a \
+     "$basecmd" != "wp"      -a \
      "$basecmd" != 'composer' ]; then
     check_result "$E_FORBIDEN" "Error: Cli command not enabled"
 fi

+ 31 - 5
web/add/webapp/index.php

@@ -21,10 +21,12 @@ if (($_SESSION['user'] == 'admin') && (!empty($_GET['user']))) {
 // Check if domain belongs to the user
 $v_domain = $_GET['domain'];
 exec(HESTIA_CMD."v-list-web-domain ".$user." ".escapeshellarg($v_domain)." json", $output, $return_var);
-if ($return_var > 0){
+if ($return_var > 0) {
     check_return_code_redirect($return_var, $output, '/list/web/');
 }
-
+unset($output);
+exec(HESTIA_CMD."v-list-sys-php json", $output, $return_var);
+$php_versions =  json_decode(implode('', $output), true);
 unset($output);
 
 // Check GET request
@@ -37,9 +39,19 @@ if (!empty($_GET['app'])) {
         try {
             $app_installer = new $app_installer_class($v_domain, $hestia);
             $info = $app_installer -> info();
-            if ($info['enabled'] != true) {
-                $_SESSION['error_msg'] = sprintf(_('%s installer missing'), $app);
+            foreach ($php_versions as $version) {
+                if (in_array($version, $info['php_support'])) {
+                    $supported = true;
+                    $supported_versions[] = $version;
+                }
+            }
+            if ($supported) {
+                $info['enabled'] = true;
             } else {
+                $info['enabled'] = false;
+                $_SESSION['error_msg'] = sprintf(_('Unable to install %s, %s is not available'), $app, 'PHP-'.end($info['php_support']));
+            }
+            if ($info['enabled'] == true) {
                 $installer = new \Hestia\WebApp\AppWizard($app_installer, $v_domain, $hestia);
                 $GLOBALS['WebappInstaller'] = $installer;
             }
@@ -90,7 +102,21 @@ if (!empty($installer)) {
             if ($matches[1] != "Resources") {
                 $app_installer_class = '\Hestia\WebApp\Installers\\'.$matches[1].'\\' . $matches[1] . 'Setup';
                 $app_installer = new $app_installer_class($v_domain, $hestia);
-                $v_web_apps[] = $app_installer -> info();
+                $appInstallerInfo = $app_installer -> info();
+                $supported = false;
+                $supported_versions = array();
+                foreach ($php_versions as $version) {
+                    if (in_array($version, $appInstallerInfo['php_support'])) {
+                        $supported = true;
+                        $supported_versions[] = $version;
+                    }
+                }
+                if ($supported) {
+                    $appInstallerInfo['enabled'] = true;
+                } else {
+                    $appInstallerInfo['enabled'] = false;
+                }
+                $v_web_apps[] = $appInstallerInfo;
             }
         }
     }

+ 97 - 36
web/src/app/System/HestiaApp.php

@@ -4,15 +4,17 @@ declare(strict_types=1);
 
 namespace Hestia\System;
 
-class HestiaApp {
-
+class HestiaApp
+{
     protected const TMPDIR_DOWNLOADS="/tmp/hestia-webapp";
+    protected $phpsupport =  false;
 
-    public function __construct() {
+    public function __construct()
+    {
         @mkdir(self::TMPDIR_DOWNLOADS);
     }
 
-    public function run(string $cmd, $args, &$cmd_result=null) : bool
+    public function run(string $cmd, $args, &$cmd_result=null): bool
     {
         $cli_script = HESTIA_CMD . '/' . basename($cmd);
         $cli_arguments = '';
@@ -25,19 +27,24 @@ class HestiaApp {
             $cli_arguments = escapeshellarg($args);
         }
 
-        exec ($cli_script . ' ' . $cli_arguments . ' 2>&1', $output, $exit_code);
+        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['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
+    public function runUser(string $cmd, $args, &$cmd_result=null): bool
     {
         if (!empty($args) && is_array($args)) {
             array_unshift($args, $this->user());
@@ -53,13 +60,13 @@ class HestiaApp {
 
         $signature = implode(PHP_EOL, $output);
         if (empty($signature)) {
-           throw new \Exception("Error reading composer 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 " . escapeshellarg($composer_setup), $output, $return_code);
-        if ($return_code !== 0 ) {
+        if ($return_code !== 0) {
             throw new \Exception("Error downloading composer");
         }
 
@@ -69,33 +76,34 @@ class HestiaApp {
         }
 
         $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){
+            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 ) {
+        if ($status->code !== 0) {
             throw new \Exception("Error installing composer");
         }
     }
-    
-    public function updateComposer($version){
+
+    public function updateComposer($version)
+    {
         $this->runUser('v-run-cli-cmd', ["composer", "selfupdate","--$version"]);
     }
 
-    public function runComposer($args, &$cmd_result=null,$version=1) : bool
+    public function runComposer($args, &$cmd_result=null, $version=1): bool
     {
         $composer = $this->getUserHomeDir() . DIRECTORY_SEPARATOR . '.composer' . DIRECTORY_SEPARATOR . 'composer';
-        if(!is_file($composer)) {
+        if (!is_file($composer)) {
             $this->installComposer($version);
-        }else{
+        } else {
             $this->updateComposer($version);
         }
 
@@ -108,21 +116,34 @@ class HestiaApp {
         return $this->runUser('v-run-cli-cmd', $args, $cmd_result);
     }
 
+    public function runWp($args, &$cmd_result=null): bool
+    {
+        $wp = $this->getUserHomeDir() . DIRECTORY_SEPARATOR . '.wp' . 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
+    public function realuser(): string
     {
         return $_SESSION['user'];
     }
 
     // Effective user
-    public function user() : string
+    public function user(): string
     {
         $user = $this->realuser();
         if ($_SESSION['userContext'] === 'admin' && !empty($_SESSION['look'])) {
             $user = $_SESSION['look'];
         }
 
-        if(strpos($user, DIRECTORY_SEPARATOR) !== false) {
+        if (strpos($user, DIRECTORY_SEPARATOR) !== false) {
             throw new \Exception("illegal characters in username");
         }
         return $user;
@@ -134,25 +155,65 @@ class HestiaApp {
         return $info['dir'];
     }
 
-    public function userOwnsDomain(string $domain) : bool
+    public function userOwnsDomain(string $domain): bool
     {
         return $this->runUser('v-list-web-domain', [$domain, 'json']);
     }
 
-    public function databaseAdd(string $dbname, string $dbuser, string $dbpass)
+    public function databaseAdd(string $dbname, string $dbuser, string $dbpass, string $charset = 'utf8mb4')
     {
-        $v_password = tempnam("/tmp","hst");
+        $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]);
+        $status = $this->runUser('v-add-database', [$dbname, $dbuser, $v_password, 'mysql', 'localhost', $charset]);
         unlink($v_password);
         return $status;
     }
-    
-    public function changeWebTemplate(string $domain, string $template){
+
+    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 = array();
+
+        foreach ($versions as $version) {
+            if (in_array($version, $support)) {
+                $supported = true;
+                $supported_versions[] = $version;
+            }
+        }
+        if ($supported) {
+            return $supported_versions[count($supported_versions) - 1];
+        } else {
+            return false;
+        }
+    }
 
     public function getWebDomainIp(string $domain)
     {
@@ -163,13 +224,13 @@ class HestiaApp {
 
     public function getWebDomainPath(string $domain)
     {
-        return Util::join_paths( $this->getUserHomeDir() , "web", $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 ) {
+        if (strpos($src, 'http://') !== 0 &&
+            strpos($src, 'https://')!== 0) {
             return false;
         }
 
@@ -178,11 +239,11 @@ class HestiaApp {
             return false;
         }
 
-        if(!preg_match('/URL:\s*(.+?)\s*\[(.+?)\]\s*->\s*"(.+?)"/', implode(PHP_EOL, $output), $matches)) {
+        if (!preg_match('/URL:\s*(.+?)\s*\[(.+?)\]\s*->\s*"(.+?)"/', implode(PHP_EOL, $output), $matches)) {
             return false;
         }
 
-        if(empty($matches) || count($matches) != 4) {
+        if (empty($matches) || count($matches) != 4) {
             return false;
         }
 
@@ -200,8 +261,8 @@ class HestiaApp {
 
         if (realpath($src)) {
             $archive_file = $src;
-        } else  {
-            if( !$this->downloadUrl($src, null, $download_result) ) {
+        } else {
+            if (!$this->downloadUrl($src, null, $download_result)) {
                 throw new \Exception("Error downloading archive");
             }
             $archive_file = $download_result->file;

+ 5 - 2
web/src/app/System/Util.php

@@ -21,9 +21,12 @@ class Util
         return preg_replace('#/+#', '/', join('/', $paths));
     }
 
-    public static function generate_string(int $length = 16)
+    public static function generate_string(int $length = 16, $full = true)
     {
-        $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~`!@|#[]$%^&*() _-=+{}:;<>?,./';
+        $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+        if ($full) {
+            $chars .= '~`!@|#[]$%^&*() _-=+{}:;<>?,./';
+        }
         $random_string = '';
         for ($i = 0; $i < $length; $i++) {
             $random_string .= $chars[random_int(0, strlen($chars) - 1)];

+ 70 - 32
web/src/app/WebApp/Installers/BaseSetup.php

@@ -8,19 +8,44 @@ use Hestia\WebApp\InstallerInterface;
 use Hestia\Models\WebDomain;
 
 use Hestia\WebApp\Installers\Resources\ComposerResource;
+use Hestia\WebApp\Installers\Resources\WpResource;
 
-
-abstract class BaseSetup implements InstallerInterface {
-
+abstract class BaseSetup implements InstallerInterface
+{
     protected $domain;
     protected $extractsubdir;
-    
-    public function info(){
+    protected $AppDirInstall;
+
+    public function setAppDirInstall(string $appDir)
+    {
+        if (!empty($appDir)) {
+            if (strpos('.', $appDir) !== false) {
+                throw new \Exception("Invalid install folder");
+            }
+            if (!is_dir($this -> getDocRoot($appDir))) {
+                $this->appcontext->runUser('v-add-fs-directory', [$this->getDocRoot($appDir)], $result);
+            }
+            $this->AppDirInstall = $appDir;
+        }
+    }
+    public function getAppDirInstall()
+    {
+        return $this->AppDirInstall;
+    }
+
+    public function info()
+    {
+        $this -> appInfo['enabled'] = true;
+        if (isset($this -> config['server']['php']['supported'])) {
+            $this -> appInfo['php_support'] = $this -> config['server']['php']['supported'];
+        } else {
+            $this -> appInfo['php_support'] = array('5.6','7.0','7.1','7.2','7.3','7.4'.'8.0','8.1');
+        }
         return $this -> appInfo;
     }
     public function __construct($domain, HestiaApp $appcontext)
     {
-        if(filter_var($domain, FILTER_VALIDATE_DOMAIN) === false) {
+        if (filter_var($domain, FILTER_VALIDATE_DOMAIN) === false) {
             throw new \Exception("Invalid domain name");
         }
 
@@ -30,7 +55,7 @@ abstract class BaseSetup implements InstallerInterface {
 
     public function getConfig($section=null)
     {
-        return (!empty($section))? $this->config[$section] : $this->config;
+        return (!empty($section)) ? $this->config[$section] : $this->config;
     }
 
     public function getOptions()
@@ -38,24 +63,28 @@ abstract class BaseSetup implements InstallerInterface {
         return $this->getConfig('form');
     }
 
-    public function withDatabase() : bool
+    public function withDatabase(): bool
     {
         return ($this->getConfig('database') === true);
     }
 
-    public function getDocRoot($append_relative_path=null) : string
+    public function getDocRoot($append_relative_path=null): string
     {
         $domain_path = $this->appcontext->getWebDomainPath($this->domain);
-        if(empty($domain_path) || ! is_dir($domain_path)) {
+
+        if (empty($domain_path) || ! is_dir($domain_path)) {
             throw new \Exception("Error finding domain folder ($domain_path)");
         }
-        return Util::join_paths($domain_path, "public_html", $append_relative_path);
+        if (! $this->AppDirInstall) {
+            return Util::join_paths($domain_path, "public_html", $append_relative_path);
+        } else {
+            return Util::join_paths($domain_path, "public_html", $this->AppDirInstall, $append_relative_path);
+        }
     }
 
     public function retrieveResources($options)
     {
         foreach ($this->getConfig('resources') as $res_type => $res_data) {
-
             if (!empty($res_data['dst']) && is_string($res_data['dst'])) {
                 $resource_destination = $this->getDocRoot($res_data['dst']);
             } else {
@@ -64,30 +93,41 @@ abstract class BaseSetup implements InstallerInterface {
 
             if ($res_type === 'composer') {
                 new ComposerResource($this->appcontext, $res_data, $resource_destination);
+            } elseif ($res_type === 'wp') {
+                new WpResource($this->appcontext, $res_data, $resource_destination, $options);
             } else {
-                $this->appcontext->archiveExtract($res_data['src'], $resource_destination, 1); 
+                $this->appcontext->archiveExtract($res_data['src'], $resource_destination, 1);
             }
         }
         return true;
     }
-    public function setup( array $options=null)
+    public function setup(array $options=null)
     {
-        if ( $_SESSION['WEB_SYSTEM'] == "nginx" )
-        {
-            if( isset($this -> config['server']['nginx']['template']) ){
+        if ($_SESSION['WEB_SYSTEM'] == "nginx") {
+            if (isset($this -> config['server']['nginx']['template'])) {
                 $this -> appcontext -> changeWebTemplate($this -> domain, $this -> config['server']['nginx']['template']);
-            }else{
+            } else {
                 $this -> appcontext -> changeWebTemplate($this -> domain, 'default');
             }
-        }else{
-            if( isset($this -> config['server']['apache2']['template']) ){
+        } else {
+            if (isset($this -> config['server']['apache2']['template'])) {
                 $this -> appcontext -> changeWebTemplate($this -> domain, $this -> config['server']['apache2']['template']);
-            }else{
+            } else {
                 $this -> appcontext -> changeWebTemplate($this -> domain, 'default');
             }
-        }        
+        }
+        if ($_SESSION['WEB_BACKEND'] == 'php-fpm') {
+            if (isset($this -> config['server']['php']['supported'])) {
+                $php_version = $this -> appcontext -> getSupportedPHP($this -> config['server']['php']['supported']);
+                if (!$php_version) {
+                    throw new \Exception('Required PHP version is not supported');
+                }
+                //convert from x.x to PHP-x_x  to accepted..
+                $this -> appcontext -> changeBackendTemplate($this -> domain, 'PHP-'.str_replace('.', '_', $php_version));
+            }
+        }
     }
-    
+
     public function install(array $options=null)
     {
         $this->appcontext->runUser('v-delete-fs-file', [$this->getDocRoot('robots.txt')]);
@@ -98,31 +138,29 @@ abstract class BaseSetup implements InstallerInterface {
     public function cleanup()
     {
         // Remove temporary folder
-        if(!empty($this->extractsubdir)) {
-            $this->appcontext->runUser('v-delete-fs-directory',[$this->getDocRoot($this->extractsubdir)], $result);
+        if (!empty($this->extractsubdir)) {
+            $this->appcontext->runUser('v-delete-fs-directory', [$this->getDocRoot($this->extractsubdir)], $result);
         }
     }
 
     public function saveTempFile(string $data)
     {
         $tmp_file = tempnam("/tmp", "hst.");
-        if(empty($tmp_file)) {
+        if (empty($tmp_file)) {
             throw new \Exception("Error creating temp file");
         }
 
         if (file_put_contents($tmp_file, $data) > 0) {
             chmod($tmp_file, 0644);
-            $user_tmp_file = Util::join_paths($this->appcontext->getUserHomeDir(), $tmp_file );
-            $this->appcontext->runUser('v-copy-fs-file',[$tmp_file, $user_tmp_file], $result);
+            $user_tmp_file = Util::join_paths($this->appcontext->getUserHomeDir(), $tmp_file);
+            $this->appcontext->runUser('v-copy-fs-file', [$tmp_file, $user_tmp_file], $result);
             unlink($tmp_file);
             return $user_tmp_file;
         }
 
-        if(file_exists($tmp_file)) {
+        if (file_exists($tmp_file)) {
             unlink($tmp_file);
         }
         return false;
     }
-
-
-}
+}

+ 19 - 12
web/src/app/WebApp/Installers/DokuWiki/DokuWikiSetup.php

@@ -49,30 +49,34 @@ class DokuWikiSetup extends BaseSetup {
 		 ],
 		'resources' => [
 			'archive'  => [ 'src' => 'https://github.com/splitbrain/dokuwiki/archive/refs/tags/release_stable_2020-07-29.zip' ],
+		],
+		'server' => [
+			'nginx' => [
+				'template' => 'default'
+			],
+			'php' => [ 
+				'supported' => [ '7.3','7.4' ],
+			]
 		], 
 	];
 	
-	public function install(array $options = null)
+	public function install(array $options = null, &$status=null)
 	{
 		parent::install($options);
-		
+		parent::setup($options);
+			
 		//check if ssl is enabled 
         $this->appcontext->run('v-list-web-domain', [$this->appcontext->user(), $this->domain, 'json'], $status);
-		
-        if($status->code !== 0) {
-            throw new \Exception("Cannot list domain");
-        }
-        
 		$sslEnabled = ($status->json[$this->domain]['SSL'] == 'no' ? 0 : 1);
 
 		$webDomain = ($sslEnabled ? "https://" : "http://") . $this->domain . "/";
 		
 		$this->appcontext->runUser('v-copy-fs-directory',[
 			$this->getDocRoot($this->extractsubdir . "/dokuwiki-release_stable_2020-07-29/."),
-			$this->getDocRoot()], $result);
+			$this->getDocRoot()], $status);
 
 		// enable htaccess
-		$this->appcontext->runUser('v-move-fs-file', [$this->getDocRoot(".htaccess.dist"), $this->getDocRoot(".htaccess")], $result);
+		$this->appcontext->runUser('v-move-fs-file', [$this->getDocRoot(".htaccess.dist"), $this->getDocRoot(".htaccess")], $status);
 
 		$installUrl = $webDomain . "install.php";
 
@@ -92,12 +96,15 @@ class DokuWikiSetup extends BaseSetup {
 		  . "--data 'd[license]=" . explode(":", $options['content_license'])[0] . "' "
 		  . "--data submit=";
 
-		exec($cmd, $msg, $code);
+		exec($cmd, $output, $return_var);
+		if($return_var > 0){
+			throw new \Exception(implode( PHP_EOL, $output));
+		}
 
 		// remove temp folder
-		$this->appcontext->runUser('v-delete-fs-file', [$this->getDocRoot("install.php")], $result);
+		$this->appcontext->runUser('v-delete-fs-file', [$this->getDocRoot("install.php")], $status);
 		$this->cleanup();
 
-		return ($code === 0);
+		return ($status->code === 0);
 	}
 }

+ 17 - 2
web/src/app/WebApp/Installers/Drupal/DrupalSetup.php

@@ -30,16 +30,31 @@ class DrupalSetup extends BaseSetup {
             'nginx' => [
                 'template' => 'drupal-composer'
             ],
+            'php' => [ 
+                'supported' => [ '8.0','8.1' ],
+            ]
         ],
     ];
 
     public function install(array $options=null) : bool
     {
         parent::install($options);
-        $this->appcontext->runComposer(["require", "-d " . $this->getDocRoot(), "drush/drush:^10"], $result);
+        parent::setup($options);
         
+        $this->appcontext->runComposer(["require", "-d " . $this->getDocRoot(), "drush/drush:^10"]);
+        
+        $htaccess_rewrite = '
+<IfModule mod_rewrite.c>
+    RewriteEngine On
+    RewriteRule ^(.*)$ web/$1 [L]
+</IfModule>';
+        
+        $tmp_configpath = $this->saveTempFile($htaccess_rewrite);
+        $this->appcontext->runUser('v-move-fs-file',[$tmp_configpath, $this->getDocRoot(".htaccess")], $result);
+        
+        $php_version = $this -> appcontext -> getSupportedPHP($this -> config['server']['php']['supported']);
         $this -> appcontext -> runUser('v-run-cli-cmd', [
-            'php',
+            "/usr/bin/php$php_version",
             $this -> getDocRoot('/vendor/drush/drush/drush'), 
             'site-install',
             'standard',

+ 9 - 3
web/src/app/WebApp/Installers/Grav/GravSetup.php

@@ -32,19 +32,25 @@ class GravSetup extends BaseSetup {
 			'nginx' => [
 				'template' => 'grav',
 			],
+			'php' => [ 
+				'supported' => [ '7.4', '8.0','8.1' ],
+			]
 		],
 	];
 	
 	public function install(array $options = null)
 	{
 		parent::install($options);
+		parent::setup($options);
+			
 		if ( $options['admin'] == true ){
 			chdir($this->getDocRoot());
-			$this -> appcontext -> runUser('v-run-cli-cmd', ['php', 
+			$php_version = $this -> appcontext -> getSupportedPHP($this -> config['server']['php']['supported']);
+			$this -> appcontext -> runUser('v-run-cli-cmd', ["/usr/bin/php$php_version", 
 			$this->getDocRoot('/bin/gpm'),
 				'install admin'
 		    ], $status);
-			$this -> appcontext -> runUser('v-run-cli-cmd', ['php', 
+			$this -> appcontext -> runUser('v-run-cli-cmd', ["/usr/bin/php$php_version", 
 				$this->getDocRoot('/bin/plugin'),
 				'login new-user',
 				'-u '.$options['username'],
@@ -54,6 +60,6 @@ class GravSetup extends BaseSetup {
 				'-N '.$options['username']
 			 ], $status);
 		}
-		return (1);
+		return ($status -> code === 1);
 	}
 }

+ 5 - 0
web/src/app/WebApp/Installers/Laravel/LaravelSetup.php

@@ -27,12 +27,17 @@ class LaravelSetup extends BaseSetup {
             'nginx' => [
                 'template' => 'laravel',
             ],
+            'php' => [ 
+                'supported' => [ '8.0','8.1' ],
+            ]
         ],
     ];
 
     public function install(array $options=null) : bool
     {
         parent::install($options);
+        parent::setup($options);
+            
         $result = null;
 
         $htaccess_rewrite = '

+ 13 - 4
web/src/app/WebApp/Installers/MediaWiki/MediaWikiSetup.php

@@ -11,7 +11,7 @@ class MediaWikiSetup extends BaseSetup
         'name' => 'MediaWiki',
         'group' => 'cms',
         'enabled' => true,
-        'version' => '1.36.2',
+        'version' => '1.37.2',
         'thumbnail' => 'MediaWiki-2020-logo.svg' //Max size is 300px by 300px
     ];
 
@@ -27,13 +27,22 @@ class MediaWikiSetup extends BaseSetup
             ],
         'database' => true,
         'resources' => [
-            'archive'  => [ 'src' => 'https://releases.wikimedia.org/mediawiki/1.36/mediawiki-1.36.2.zip' ],
+            'archive'  => [ 'src' => 'https://releases.wikimedia.org/mediawiki/1.37/mediawiki-1.37.1.zip' ],
         ],
+        'server' => [
+            'nginx' => [
+                'template' => 'default'
+            ],
+            'php' => [ 
+                'supported' => [ '7.3','7.4' ],
+            ]
+        ], 
     ];
 
     public function install(array $options = null)
     {
         parent::install($options);
+        parent::setup($options);
 
         //check if ssl is enabled
         $this->appcontext->run('v-list-web-domain', [$this->appcontext->user(), $this->domain, 'json'], $status);
@@ -49,8 +58,8 @@ class MediaWikiSetup extends BaseSetup
         $this->appcontext->runUser('v-copy-fs-directory', [
             $this->getDocRoot($this->extractsubdir . "/mediawiki-1.36.1/."),
             $this->getDocRoot()], $result);
-
-        $this->appcontext->runUser('v-run-cli-cmd', ['/usr/bin/php',
+        $php_version = $this -> appcontext -> getSupportedPHP($this -> config['server']['php']['supported']);
+        $this->appcontext->runUser('v-run-cli-cmd', ["/usr/bin/php$php_version",
             $this->getDocRoot('maintenance/install.php'),
             '--dbserver=localhost',
             '--dbname=' . $this->appcontext->user() . '_' . $options['database_name'],

+ 13 - 11
web/src/app/WebApp/Installers/Nextcloud/NextcloudSetup.php

@@ -10,7 +10,7 @@ class NextcloudSetup extends BaseSetup
         'name' => 'Nextcloud',
         'group' => 'cloud',
         'enabled' => true,
-        'version' => '22.2.0',
+        'version' => '23.0.2',
         'thumbnail' => 'nextcloud-thumb.png'
     ];
 
@@ -23,23 +23,26 @@ class NextcloudSetup extends BaseSetup
             ],
         'database' => true,
         'resources' => [
-            'archive'  => [ 'src' => 'https://download.nextcloud.com/server/releases/nextcloud-22.2.0.tar.bz2' ]
+            'archive'  => [ 'src' => 'https://download.nextcloud.com/server/releases/nextcloud-23.0.2.tar.bz2' ]
         ],
         'server' => [
             'nginx' => [
-                'template' => 'owncloud',
+                'template' => 'owncloud'
             ],
-        ],
+            'php' => [ 
+                'supported' => [ '7.3','7.4','8.0' ],
+            ]
+        ], 
     ];
 
     public function install(array $options = null): bool
     {
         parent::install($options);
-
-        $this->appcontext->runUser('v-copy-fs-file', [$this->getDocRoot(".htaccess.txt"), $this->getDocRoot(".htaccess")]);
-
+        parent::setup($options);
+        
         // install nextcloud
-        $this->appcontext->runUser('v-run-cli-cmd', ['/usr/bin/php',
+        $php_version = $this -> appcontext -> getSupportedPHP($this -> config['server']['php']['supported']);
+        $this->appcontext->runUser('v-run-cli-cmd', ["/usr/bin/php$php_version",
             $this->getDocRoot('occ'),
             'maintenance:install',
             '--database mysql',
@@ -57,8 +60,7 @@ class NextcloudSetup extends BaseSetup
                 'config:system:set',
                 'trusted_domains 2 --value='.$this->domain
             ],
-            $status2
-        );
-        return ($status->code === 0 && $status2->code === 0);
+            $status);
+        return ($status->code === 0);
     }
 }

+ 22 - 4
web/src/app/WebApp/Installers/Opencart/OpencartSetup.php

@@ -29,14 +29,19 @@ class OpencartSetup extends BaseSetup
         ],
         'server' => [
             'nginx' => [
-                'template' => 'opencart',
+                'template' => 'opencart'
             ],
-        ],
+            'php' => [ 
+                'supported' => [ '7.3','7.4' ],
+            ]
+        ], 
     ];
 
     public function install(array $options = null): bool
     {
+        
         parent::install($options);
+        parent::setup($options);
 
         $this->appcontext->runUser('v-copy-fs-directory', [
             $this->getDocRoot($this->extractsubdir . "/upload/."),
@@ -45,8 +50,21 @@ class OpencartSetup extends BaseSetup
         $this->appcontext->runUser('v-copy-fs-file', [$this->getDocRoot("config-dist.php"), $this->getDocRoot("config.php")]);
         $this->appcontext->runUser('v-copy-fs-file', [$this->getDocRoot("admin/config-dist.php"), $this->getDocRoot("admin/config.php")]);
         $this->appcontext->runUser('v-copy-fs-file', [$this->getDocRoot(".htaccess.txt"), $this->getDocRoot(".htaccess")]);
+        #Check if SSL is enabled 
+        $this->appcontext->run('v-list-web-domain', [$this -> appcontext->user(),$this -> domain,'json'], $status);
+        if ($status->code !== 0) {
+            throw new \Exception("Cannot list domain");
+        }
+        if ($status -> json[$this -> domain]['SSL'] == 'no') {
+            $protocol = 'http://';
+        } else {
+            $protocol = 'https://';
+        }
+        
+        $php_version = $this -> appcontext -> getSupportedPHP($this -> config['server']['php']['supported']);
+        
         $this->appcontext->runUser('v-run-cli-cmd', [
-            "/usr/bin/php",
+            "/usr/bin/php$php_version",
             $this->getDocRoot("/install/cli_install.php"),
             "install",
             "--db_username " . $this->appcontext->user() . '_' .$options['database_user'],
@@ -55,7 +73,7 @@ class OpencartSetup extends BaseSetup
             "--username "    . $options['opencart_account_username'],
             "--password "    . $options['opencart_account_password'],
             "--email "       . $options['opencart_account_email'],
-            "--http_server " . "http://" . $this->domain . "/"], $status);
+            "--http_server " . $protocol . $this->domain . "/"], $status);
 
         // After install, 'storage' folder must be moved to a location where the web server is not allowed to serve file
         // - Opencart Nginx template and Apache ".htaccess" forbids acces to /storage folder

+ 13 - 5
web/src/app/WebApp/Installers/Prestashop/PrestashopSetup.php

@@ -10,7 +10,7 @@ class PrestashopSetup extends BaseSetup
         'name' => 'Prestashop',
         'group' => 'ecommerce',
         'enabled' => true,
-        'version' => '1.7.7.8',
+        'version' => '1.7.8.4',
         'thumbnail' => 'prestashop-thumb.png'
     ];
 
@@ -26,12 +26,15 @@ class PrestashopSetup extends BaseSetup
             ],
         'database' => true,
         'resources' => [
-            'archive'  => [ 'src' => 'https://github.com/PrestaShop/PrestaShop/releases/download/1.7.7.8/prestashop_1.7.7.8.zip' ],
+            'archive'  => [ 'src' => 'https://github.com/PrestaShop/PrestaShop/releases/download/1.7.8.4/prestashop_1.7.8.4.zip' ],
         ],
         'server' => [
             'nginx' => [
                 'template' => 'prestashop',
             ],
+            'php' => [ 
+                'supported' => [ '7.3','7.4' ],
+            ]
         ],
 
     ];
@@ -39,6 +42,9 @@ class PrestashopSetup extends BaseSetup
     public function install(array $options=null): bool
     {
         parent::install($options);
+        parent::setup($options);
+        
+        
         $this->appcontext->archiveExtract($this->getDocRoot($this->extractsubdir . '/prestashop.zip'), $this->getDocRoot());
         //check if ssl is enabled
         $this->appcontext->run('v-list-web-domain', [$this -> appcontext->user(),$this -> domain,'json'], $status);
@@ -51,9 +57,11 @@ class PrestashopSetup extends BaseSetup
         } else {
             $ssl_enabled = 1;
         }
-
+        
+        $php_version = $this -> appcontext -> getSupportedPHP($this -> config['server']['php']['supported']);
+        
         $this->appcontext->runUser('v-run-cli-cmd', [
-            "/usr/bin/php",
+            "/usr/bin/php$php_version",
             $this->getDocRoot("/install/index_cli.php"),
             "--db_user=" . $this->appcontext->user() . '_' .$options['database_user'],
             "--db_password=" . $options['database_password'],
@@ -64,7 +72,7 @@ class PrestashopSetup extends BaseSetup
             "--email="       . $options['prestashop_account_email'],
             "--domain="      . $this->domain,
             "--ssl="         . $ssl_enabled,], $status);
-
+            
         // remove install folder
         $this->appcontext->runUser('v-delete-fs-directory', [$this->getDocRoot("/install")]);
         $this->cleanup();

+ 3 - 3
web/src/app/WebApp/Installers/Resources/ComposerResource.php

@@ -15,13 +15,13 @@ class ComposerResource
         $this->folder = dirname($destination);
         $this->project = basename($destination);
         $this->appcontext = $appcontext;
-        if (empty($data['version'])){
+        if (empty($data['version'])) {
             $data['version'] = 2;
         }
-        
+
         $this->appcontext->runComposer(["create-project", "--no-progress", "--prefer-dist", $data['src'], "-d " . $this->folder, $this->project ], $status, $data['version']);
 
-        if($status->code !== 0){
+        if ($status->code !== 0) {
             throw new \Exception("Error fetching Composer resource: " . $status->text);
         }
     }

+ 21 - 0
web/src/app/WebApp/Installers/Resources/WpResource.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Hestia\WebApp\Installers\Resources;
+
+use Hestia\System\HestiaApp;
+
+class WpResource
+{
+    private $appcontext;
+    private $options;
+
+    public function __construct(HestiaApp $appcontext, $data, $destination, $options)
+    {
+        $this->appcontext = $appcontext;
+        $this->appcontext->runWp(['core', 'download', '--locale='.$options['language'], '--path='.$destination], $status);
+
+        if ($status->code !== 0) {
+            throw new \Exception("Error fetching WP resource: " . $status->text);
+        }
+    }
+}

+ 8 - 0
web/src/app/WebApp/Installers/Symfony/SymfonySetup.php

@@ -23,6 +23,14 @@ class SymfonySetup extends BaseSetup
         'resources' => [
             'composer' => [ 'src' => 'symfony/website-skeleton', 'dst' => '/' ],
         ],
+        'server' => [
+            'nginx' => [
+                'template' => 'symfony4-5',
+            ],
+            'php' => [ 
+                'supported' => [ '8.0','8.1' ],
+            ]
+        ],
     ];
 
     public function install(array $options=null): bool

+ 75 - 26
web/src/app/WebApp/Installers/Wordpress/WordpressSetup.php

@@ -3,77 +3,126 @@
 namespace Hestia\WebApp\Installers\Wordpress;
 
 use Hestia\System\Util;
-use \Hestia\WebApp\Installers\BaseSetup as BaseSetup;
+use Hestia\WebApp\Installers\BaseSetup as BaseSetup;
 
-class WordpressSetup extends BaseSetup {
-
-    protected $appInfo = [ 
+class WordpressSetup extends BaseSetup
+{
+    protected $appInfo = [
         'name' => 'Wordpress',
         'group' => 'cms',
         'enabled' => true,
         'version' => 'latest',
         'thumbnail' => 'wp-thumb.png'
     ];
-    
+
     protected $appname = 'wordpress';
     protected $config = [
         'form' => [
-            //'protocol' => [ 
+            //'protocol' => [
             //    'type' => 'select',
             //    'options' => ['http','https'],
             //],
+
             'site_name' => ['type'=>'text', 'value'=>'WordPress Blog'],
             'wordpress_account_username' => ['value'=>'wpadmin'],
             'wordpress_account_email' => 'text',
             'wordpress_account_password' => 'password',
+            'install_directory' => ['type'=>'text', 'value'=>'', 'placeholder'=>'/'],
+            'language' => [
+                'type' => 'select',
+                'value' => 'en_US',
+                'options' => [
+                        'cs_CZ' => 'Czech',
+                        'de_DE' => 'German',
+                        'es_ES' => 'Spanish',
+                        'en_US' => 'English',
+                        'fr_FR' => 'French',
+                        'hu_HU' => 'Hungarian',
+                        'it_IT' => 'Italian',
+                        'nl_NL' => 'Dutch',
+                        'pt_PT' => 'Portuguese',
+                        'sk_SK' => 'Slovak',
+                        'sr_RS' => 'Serbian',
+                        'tr_TR' => 'Turkish',
+                        'ru_RU' => 'Russian',
+                        'uk' => 'Ukrainian',
+                        'zh-CN' => 'Simplified Chinese (China)',
+                        'zh_TW' => 'Traditional Chinese',
+                    ]
+                ],
             ],
         'database' => true,
         'resources' => [
-            'archive'  => [ 'src' => 'https://wordpress.org/latest.tar.gz' ],
+            'wp'  => [ 'src' => 'https://wordpress.org/latest.tar.gz' ],
         ],
         'server' => [
             'nginx' => [
                 'template' => 'wordpress',
             ],
+            'php' => [
+                'supported' => [ '7.4','8.0','8.1' ],
+            ]
         ],
-        
+
     ];
-    
+
     public function install(array $options = null)
     {
+        parent::setAppDirInstall($options['install_directory']);
         parent::install($options);
         parent::setup($options);
-        $this->appcontext->runUser('v-open-fs-file',[$this->getDocRoot("wp-config-sample.php")], $result);
-
-        $distconfig = preg_replace( [
-                '/database_name_here/', '/username_here/', '/password_here/'
-            ], [
-                $this->appcontext->user() . '_' . $options['database_name'],
-                $this->appcontext->user() . '_' . $options['database_user'],
-                $options['database_password']
+
+        $this->appcontext->runUser('v-open-fs-file', [$this->getDocRoot("wp-config-sample.php")], $result);
+        $distconfig = preg_replace(
+            [
+            '/database_name_here/', '/username_here/', '/password_here/', '/utf8/', '/wp_/'
+        ],
+            [
+            $this->appcontext->user() . '_' . $options['database_name'],
+            $this->appcontext->user() . '_' . $options['database_user'],
+            $options['database_password'],
+            'utf8mb4',
+            Util::generate_string(3, false).'_'
             ],
-            $result->text);
+            $result->text
+        );
 
         while (strpos($distconfig, 'put your unique phrase here') !== false) {
-            $distconfig = preg_replace( '/put your unique phrase here/', Util::generate_string(64), $distconfig, 1);
+            $distconfig = preg_replace('/put your unique phrase here/', Util::generate_string(64), $distconfig, 1);
         }
 
         $tmp_configpath = $this->saveTempFile($distconfig);
 
-        if(!$this->appcontext->runUser('v-move-fs-file',[$tmp_configpath, $this->getDocRoot("wp-config.php")], $result)) {
-            throw new \Exception("Error installing config file in: " . $tmp_configpath . " to:" . $this->getDocRoot("wp-config.php") . $result->text );
+        if (!$this->appcontext->runUser('v-move-fs-file', [$tmp_configpath, $this->getDocRoot("wp-config.php")], $result)) {
+            throw new \Exception("Error installing config file in: " . $tmp_configpath . " to:" . $this->getDocRoot("wp-config.php") . $result->text);
         }
 
-        exec("/usr/bin/curl --location --post301 --insecure --resolve ".$this->domain.":80:".$this->appcontext->getWebDomainIp($this->domain)." "
-            . escapeshellarg("http://".$this->domain."/wp-admin/install.php?step=2")
+        $this->appcontext->run('v-list-web-domain', [$this->appcontext->user(), $this->domain, 'json'], $status);
+        $sslEnabled = ($status->json[$this->domain]['SSL'] == 'no' ? 0 : 1);
+        $webDomain = ($sslEnabled ? "https://" : "http://") . $this->domain . "/";
+        $webPort= ($sslEnabled ? "443" : "80");
+
+        if (substr($options['install_directory'], 0, 1) == '/') {
+            $options['install_directory'] = substr($options['install_directory'], 1);
+        }
+        if (substr($options['install_directory'], -1, 1) == '/') {
+            $options['install_directory'] = substr($options['install_directory'], 0, strlen($options['install_directory']) - 1);
+        }
+
+        exec("/usr/bin/curl --location --post301 --insecure --resolve ".$this->domain.":$webPort:".$this->appcontext->getWebDomainIp($this->domain)." "
+            . escapeshellarg($webDomain.$options['install_directory']."/wp-admin/install.php?step=2")
             . " -d " . escapeshellarg(
                 "weblog_title=" . rawurlencode($options['site_name'])
             . "&user_name="      . rawurlencode($options['wordpress_account_username'])
             . "&admin_password=" . rawurlencode($options['wordpress_account_password'])
             . "&admin_password2=". rawurlencode($options['wordpress_account_password'])
-            . "&admin_email="    . rawurlencode($options['wordpress_account_email'])), $output, $return_var);
-        
-    
+            . "&admin_email="    . rawurlencode($options['wordpress_account_email'])
+            ), $output, $return_var);
+
+
+        if ($return_var > 0) {
+            throw new \Exception(implode(PHP_EOL, $output));
+        }
         return ($return_var === 0);
     }
 }

+ 2 - 0
web/templates/pages/list_webapps.html

@@ -32,8 +32,10 @@
 							$msg_id = 'vst-ok';
 						}
 					}
+					if(!empty($msg_id)){
 				?>
 				<span class="<?=$msg_id;?>"> <i class="fas <?=$msg_icon;?>"></i> <?=$msg_text;?></span>
+				<?php }; ?> 
 			</span>
 		</div>
 	</div>

+ 7 - 3
web/templates/pages/setup_webapp.html

@@ -66,10 +66,14 @@
 						<?php if (in_array($f_type, ['select']) && count($form_control['options']) ):?>
 							<p style="margin-top:0;"></p>
 							<select class="vst-list" name="<?=$f_name?>">
-								<?php foreach ($form_control['options'] as $option):?>
+								<?php foreach ($form_control['options'] as $key => $option){
+									if(is_numeric($key)){
+										$key = $option;
+									}
+								?>
 									<?php $selected = (!empty($form_control['value']) && $option === $form_control['value'])?'selected':''?>
-									<option value="<?=$option?>" <?=$selected?>><?=htmlentities($option)?></option>
-								<?php endforeach; ?>
+									<option value="<?=$key?>" <?=$selected?>><?=htmlentities($option)?></option>
+								<?php }; ?>
 							</select>
 							</p>
 						<?php elseif (in_array($f_type, ['boolean'])):?>