nodeProvider = $nodeProvider; } public function getSupportedApps(): array { return [ 'wordpress' => [ 'name' => 'WordPress', 'description' => 'Popular blogging and CMS platform', 'icon' => 'wordpress', 'min_php' => '7.4', 'recommended_php' => '8.2', ], 'joomla' => [ 'name' => 'Joomla', 'description' => 'Flexible content management system', 'icon' => 'joomla', 'min_php' => '7.4', 'recommended_php' => '8.1', ], 'drupal' => [ 'name' => 'Drupal', 'description' => 'Enterprise-grade CMS', 'icon' => 'drupal', 'min_php' => '8.1', 'recommended_php' => '8.2', ], 'opencart' => [ 'name' => 'OpenCart', 'description' => 'E-commerce platform', 'icon' => 'opencart', 'min_php' => '8.0', 'recommended_php' => '8.1', ], 'magento' => [ 'name' => 'Magento', 'description' => 'Enterprise e-commerce platform', 'icon' => 'magento', 'min_php' => '8.1', 'recommended_php' => '8.2', 'min_ram_mb' => 2048, ], ]; } public function getAppVersions(string $appType): array { return match ($appType) { 'wordpress' => ['6.4', '6.3', '6.2'], 'joomla' => ['5.0', '4.4', '4.3'], 'drupal' => ['10.2', '10.1', '10.0'], 'opencart' => ['4.0', '3.0'], 'magento' => ['2.4.6', '2.4.5'], default => [], }; } public function install(HostedSite $site, string $appType, array $config): AppInstallation { $account = $site->account; $supportedApps = $this->getSupportedApps(); if (!isset($supportedApps[$appType])) { throw new \InvalidArgumentException("Unsupported app type: {$appType}"); } // Create database for the app $dbName = $account->username . '_' . Str::random(4); $dbUser = $account->username . '_' . Str::random(4); $dbPassword = Str::password(16); $database = HostedDatabase::create([ 'hosting_account_id' => $account->id, 'hosted_site_id' => $site->id, 'name' => $dbName, 'username' => $dbUser, 'password_encrypted' => encrypt($dbPassword), 'type' => 'mysql', 'status' => 'active', ]); $this->nodeProvider->createDatabase($database, $dbPassword); // Create installation record $installation = AppInstallation::create([ 'hosted_site_id' => $site->id, 'app_type' => $appType, 'app_version' => $config['version'] ?? $this->getAppVersions($appType)[0], 'admin_username' => $config['admin_username'] ?? 'admin', 'admin_email' => $config['admin_email'] ?? $account->user->email, 'status' => 'installing', 'config' => [ 'db_name' => $dbName, 'db_user' => $dbUser, 'site_title' => $config['site_title'] ?? $site->domain, ], ]); try { $result = match ($appType) { 'wordpress' => $this->installWordPress($site, $installation, $database, $dbPassword, $config), 'joomla' => $this->installJoomla($site, $installation, $database, $dbPassword, $config), 'drupal' => $this->installDrupal($site, $installation, $database, $dbPassword, $config), 'opencart' => $this->installOpenCart($site, $installation, $database, $dbPassword, $config), 'magento' => $this->installMagento($site, $installation, $database, $dbPassword, $config), default => throw new \InvalidArgumentException("No installer for: {$appType}"), }; $installation->update([ 'status' => 'active', 'admin_path' => $result['admin_path'] ?? null, 'installed_at' => now(), ]); $site->update([ 'installed_app' => $appType, 'installed_app_version' => $installation->app_version, ]); } catch (\Exception $e) { Log::error("App installation failed", [ 'app' => $appType, 'site' => $site->domain, 'error' => $e->getMessage(), ]); $installation->update([ 'status' => 'failed', 'config' => array_merge($installation->config ?? [], ['error' => $e->getMessage()]), ]); throw $e; } return $installation; } public function uninstall(AppInstallation $installation): bool { $site = $installation->site; $account = $site->account; $appType = $installation->app_type; try { // For Node.js/Python apps, stop and remove the systemd service first if (in_array($appType, ['nodejs', 'python'], true)) { try { $this->nodeProvider->removeAppService($site); } catch (\Exception $e) { Log::warning("Failed to remove app service during uninstall: " . $e->getMessage()); } // Node.js specific cleanup if ($appType === 'nodejs') { $this->nodeProvider->executeCommand( $account, "rm -rf {$site->document_root}/node_modules {$site->document_root}/package.json {$site->document_root}/package-lock.json 2>&1 || true" ); } // Python specific cleanup if ($appType === 'python') { $this->nodeProvider->executeCommand( $account, "rm -rf {$site->document_root}/venv {$site->document_root}/__pycache__ {$site->document_root}/.venv 2>&1 || true" ); $this->nodeProvider->executeCommand( $account, "find {$site->document_root} -name '*.pyc' -delete 2>&1 || true" ); } } // For apps with subdirectories (Drupal with web/, Laravel with public/, Magento with pub/) // we need to clean the parent directory too $docRoot = $site->document_root; if (in_array($appType, ['drupal', 'laravel', 'magento'], true)) { // Try to clean up parent directory if it's a subdirectory installation $parentDir = dirname($docRoot); if ($parentDir !== "/home/{$account->username}" && $parentDir !== "/home/{$account->username}/public_html") { $this->nodeProvider->executeCommandAsRoot( $account, "rm -rf " . escapeshellarg($parentDir) . " 2>&1 || true" ); } } // Remove all files from document root (use root to handle permission issues) $escapedDocRoot = escapeshellarg($docRoot); $this->nodeProvider->executeCommandAsRoot( $account, "rm -rf {$escapedDocRoot}/* {$escapedDocRoot}/.[!.]* {$escapedDocRoot}/..?* 2>&1 || true" ); // Drop database and database user if exists $dbName = $installation->config['db_name'] ?? null; if ($dbName) { $database = HostedDatabase::where('name', $dbName)->first(); if ($database) { // Drop both database and user $dbUser = $database->username; $this->nodeProvider->executeCommandAsRoot( $account, "mysql -e \"DROP DATABASE IF EXISTS \`{$dbName}\`; DROP USER IF EXISTS '{$dbUser}'@'localhost';\" 2>&1 || true" ); $database->delete(); } else { // Database record not found, try cleanup anyway $this->nodeProvider->executeCommandAsRoot( $account, "mysql -e \"DROP DATABASE IF EXISTS \`{$dbName}\`; DROP USER IF EXISTS '{$dbName}'@'localhost';\" 2>&1 || true" ); } } $installation->update(['status' => 'removed']); $site->update(['installed_app' => null, 'installed_app_version' => null]); Log::info("App uninstalled successfully", [ 'app' => $appType, 'site' => $site->domain, 'installation_id' => $installation->id, ]); return true; } catch (\Exception $e) { Log::error("Failed to uninstall app: " . $e->getMessage(), [ 'app' => $appType, 'site' => $site->domain, 'installation_id' => $installation->id, ]); throw $e; } } public function update(AppInstallation $installation, ?string $version = null): bool { $site = $installation->site; $account = $site->account; $installation->update(['status' => 'updating']); try { if ($installation->app_type === 'wordpress') { $this->nodeProvider->executeCommand($account, "cd {$site->document_root} && wp core update" . ($version ? " --version={$version}" : "") ); $this->nodeProvider->executeCommand($account, "cd {$site->document_root} && wp plugin update --all"); $this->nodeProvider->executeCommand($account, "cd {$site->document_root} && wp theme update --all"); } $installation->update([ 'status' => 'active', 'app_version' => $version ?? $installation->app_version, 'last_updated_at' => now(), ]); return true; } catch (\Exception $e) { $installation->update(['status' => 'active']); throw $e; } } public function getStatus(AppInstallation $installation): array { $site = $installation->site; $account = $site->account; $status = [ 'app_type' => $installation->app_type, 'version' => $installation->app_version, 'status' => $installation->status, 'installed_at' => $installation->installed_at, ]; if ($installation->app_type === 'wordpress') { $result = $this->nodeProvider->executeCommand($account, "cd {$site->document_root} && wp core version 2>/dev/null" ); $status['current_version'] = trim($result['output']); } return $status; } public function backup(AppInstallation $installation): string { $site = $installation->site; $account = $site->account; $backupName = "{$installation->app_type}_{$site->domain}_" . date('Y-m-d_His'); $backupPath = "/home/{$account->username}/backups/{$backupName}"; $this->nodeProvider->executeCommand($account, "mkdir -p /home/{$account->username}/backups"); // Backup files $this->nodeProvider->executeCommand($account, "tar -czf {$backupPath}_files.tar.gz -C {$site->document_root} ." ); // Backup database $dbName = $installation->config['db_name'] ?? null; if ($dbName) { $this->nodeProvider->executeCommand($account, "mysqldump {$dbName} > {$backupPath}_db.sql" ); } return $backupPath; } public function restore(AppInstallation $installation, string $backupPath): bool { $site = $installation->site; $account = $site->account; // Restore files $this->nodeProvider->executeCommand($account, "rm -rf {$site->document_root}/* && tar -xzf {$backupPath}_files.tar.gz -C {$site->document_root}" ); // Restore database $dbName = $installation->config['db_name'] ?? null; if ($dbName && file_exists("{$backupPath}_db.sql")) { $this->nodeProvider->executeCommand($account, "mysql {$dbName} < {$backupPath}_db.sql" ); } return true; } private function installWordPress(HostedSite $site, AppInstallation $installation, HostedDatabase $database, string $dbPassword, array $config): array { $account = $site->account; $docRoot = $site->document_root; $adminPassword = $config['admin_password'] ?? Str::password(12); $commands = [ "cd {$docRoot} && wp core download --version={$installation->app_version}", "cd {$docRoot} && wp config create --dbname={$database->name} --dbuser={$database->username} --dbpass={$dbPassword} --dbhost=localhost", "cd {$docRoot} && wp core install --url=https://{$site->domain} --title='" . addslashes($config['site_title'] ?? $site->domain) . "' --admin_user={$installation->admin_username} --admin_password={$adminPassword} --admin_email={$installation->admin_email}", "cd {$docRoot} && wp rewrite structure '/%postname%/'", "cd {$docRoot} && chmod -R 755 .", "cd {$docRoot} && chmod -R 775 wp-content/uploads", ]; foreach ($commands as $cmd) { $result = $this->nodeProvider->executeCommand($account, $cmd); if ($result['exit_code'] !== 0) { throw new \RuntimeException("WordPress install failed: " . $result['output']); } } return [ 'admin_path' => '/wp-admin', 'admin_password' => $adminPassword, ]; } private function installJoomla(HostedSite $site, AppInstallation $installation, HostedDatabase $database, string $dbPassword, array $config): array { $account = $site->account; $docRoot = $site->document_root; $adminPassword = $config['admin_password'] ?? Str::password(12); $commands = [ "cd {$docRoot} && curl -sL https://downloads.joomla.org/cms/joomla5/{$installation->app_version}/Joomla_{$installation->app_version}-Stable-Full_Package.zip -o joomla.zip", "cd {$docRoot} && unzip -q joomla.zip && rm joomla.zip", "cd {$docRoot} && php installation/joomla.php install --site-name='" . addslashes($config['site_title'] ?? $site->domain) . "' --admin-user={$installation->admin_username} --admin-username={$installation->admin_username} --admin-password={$adminPassword} --admin-email={$installation->admin_email} --db-type=mysqli --db-host=localhost --db-user={$database->username} --db-pass={$dbPassword} --db-name={$database->name} --db-prefix=jos_ --db-encryption=0", "cd {$docRoot} && rm -rf installation", "cd {$docRoot} && chmod -R 755 .", "cd {$docRoot} && chmod 775 .", "cd {$docRoot} && chmod -R 775 cache tmp administrator/cache administrator/logs", "cd {$docRoot} && echo ' administrator/cache/autoload_psr4.php && echo ' cache/autoload_psr4.php", ]; foreach ($commands as $cmd) { $this->nodeProvider->executeCommand($account, $cmd); } $this->nodeProvider->setWebServerGroupOwnership($account, $docRoot); return ['admin_path' => '/administrator']; } private function installDrupal(HostedSite $site, AppInstallation $installation, HostedDatabase $database, string $dbPassword, array $config): array { $account = $site->account; $docRoot = $site->document_root; $adminPassword = $config['admin_password'] ?? Str::password(12); $commands = [ "cd {$docRoot} && composer create-project drupal/recommended-project:{$installation->app_version} . --no-interaction", "cd {$docRoot}/web && ../vendor/bin/drush site:install standard --db-url=mysql://{$database->username}:{$dbPassword}@localhost/{$database->name} --site-name='" . addslashes($config['site_title'] ?? $site->domain) . "' --account-name={$installation->admin_username} --account-pass={$adminPassword} --account-mail={$installation->admin_email} -y", "cd {$docRoot} && chmod -R 755 .", ]; foreach ($commands as $cmd) { $this->nodeProvider->executeCommand($account, $cmd); } return ['admin_path' => '/user/login']; } private function installOpenCart(HostedSite $site, AppInstallation $installation, HostedDatabase $database, string $dbPassword, array $config): array { $account = $site->account; $docRoot = $site->document_root; $adminPassword = $config['admin_password'] ?? Str::password(12); $commands = [ "cd {$docRoot} && curl -sL https://github.com/opencart/opencart/releases/download/{$installation->app_version}.0.0/opencart-{$installation->app_version}.0.0.zip -o opencart.zip", "cd {$docRoot} && unzip -q opencart.zip && mv upload/* . && rm -rf upload opencart.zip", "cd {$docRoot} && php install/cli_install.php install --db_hostname localhost --db_username {$database->username} --db_password {$dbPassword} --db_database {$database->name} --username {$installation->admin_username} --password {$adminPassword} --email {$installation->admin_email} --http_server https://{$site->domain}/", "cd {$docRoot} && rm -rf install", "cd {$docRoot} && chmod -R 755 .", ]; foreach ($commands as $cmd) { $this->nodeProvider->executeCommand($account, $cmd); } return ['admin_path' => '/admin']; } private function installMagento(HostedSite $site, AppInstallation $installation, HostedDatabase $database, string $dbPassword, array $config): array { $account = $site->account; $docRoot = $site->document_root; $adminPassword = $config['admin_password'] ?? Str::password(12); $commands = [ "cd {$docRoot} && composer create-project --repository-url=https://repo.magento.com/ magento/project-community-edition={$installation->app_version} . --no-interaction", "cd {$docRoot} && bin/magento setup:install --base-url=https://{$site->domain}/ --db-host=localhost --db-name={$database->name} --db-user={$database->username} --db-password={$dbPassword} --admin-firstname=Admin --admin-lastname=User --admin-email={$installation->admin_email} --admin-user={$installation->admin_username} --admin-password={$adminPassword} --language=en_US --currency=USD --timezone=UTC --use-rewrites=1", "cd {$docRoot} && bin/magento deploy:mode:set production", "cd {$docRoot} && chmod -R 755 .", ]; foreach ($commands as $cmd) { $this->nodeProvider->executeCommand($account, $cmd); } return ['admin_path' => '/admin']; } }