From bf0d9f88e261fc6170715729015b62f72ca8e0d1 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 25 Dec 2024 13:08:32 -0800 Subject: [PATCH] Implement installPlugin() and related methods --- src/RaspAP/Plugins/PluginInstaller.php | 303 ++++++++++++++++++++++--- 1 file changed, 274 insertions(+), 29 deletions(-) diff --git a/src/RaspAP/Plugins/PluginInstaller.php b/src/RaspAP/Plugins/PluginInstaller.php index f6a0f96f..9f50321a 100644 --- a/src/RaspAP/Plugins/PluginInstaller.php +++ b/src/RaspAP/Plugins/PluginInstaller.php @@ -15,10 +15,21 @@ namespace RaspAP\Plugins; class PluginInstaller { private static $instance = null; - + private $pluginName; + private $manifestRaw; + private $tempSudoers; + private $destSudoers; + private $refModules; + private $rootPath; + public function __construct() { $this->pluginPath = 'plugins'; + $this->manifestRaw = '/blob/master/manifest.json?raw=true'; + $this->tempSudoers = '/tmp/090_'; + $this->destSudoers = '/etc/sudoers.d/'; + $this->refModules = '/refs/heads/master/.gitmodules'; + $this->rootPath = $_SERVER['DOCUMENT_ROOT']; } // Returns a single instance of PluginInstaller @@ -43,7 +54,7 @@ class PluginInstaller $submodules = $this->getSubmodules(RASPI_PLUGINS_URL); $plugins = []; foreach ($submodules as $submodule) { - $manifestUrl = $submodule['url'] .'/blob/master/manifest.json?raw=true'; + $manifestUrl = $submodule['url'] .$this->manifestRaw; $manifest = $this->getPluginManifest($manifestUrl); if ($manifest) { @@ -63,7 +74,7 @@ class PluginInstaller } } return $plugins; - } catch (Exception $e) { + } catch (\Exception $e) { echo "An error occured: " .$e->getMessage(); } } @@ -105,11 +116,11 @@ class PluginInstaller */ public function getSubmodules(string $repoUrl): array { - $gitmodulesUrl = $repoUrl . '/refs/heads/master/.gitmodules'; + $gitmodulesUrl = $repoUrl .$this->refModules; $gitmodulesContent = file_get_contents($gitmodulesUrl); if ($gitmodulesContent === false) { - throw new Exception('Unable to fetch .gitmodules file from the repository'); + throw new \Exception('Unable to fetch .gitmodules file from the repository'); } $submodules = []; @@ -138,6 +149,263 @@ class PluginInstaller return $submodules; } + /** + * Returns an array of installed plugins in pluginPath + * + * @return array $plugins + */ + public function getPlugins(): array + { + $plugins = []; + if (file_exists($this->pluginPath)) { + $directories = scandir($this->pluginPath); + + foreach ($directories as $directory) { + $pluginClass = "RaspAP\\Plugins\\$directory\\$directory"; + $pluginFile = $this->pluginPath . "/$directory/$directory.php"; + + if (file_exists($pluginFile) && class_exists($pluginClass)) { + $plugins[] = $pluginClass; + } + } + } + return $plugins; + } + + /** + * Retrieves a plugin archive and performs install actions defined in the manifest + * + * @param string $archiveUrl + * @return boolean + */ + public function installPlugin($archiveUrl): bool + { + try { + list($tempFile, $extractDir, $pluginDir) = $this->getPluginArchive($archiveUrl); + + $manifest = $this->parseManifest($pluginDir); + $this->pluginName = preg_replace('/\s+/', '', $manifest['name']); + $rollbackStack = []; // store actions to rollback on failure + + try { + if (!empty($manifest['sudoers'])) { + $this->addSudoers($manifest['sudoers']); + $rollbackStack[] = 'removeSudoers'; + } + if (!empty($manifest['dependencies'])) { + $this->installDependencies($manifest['dependencies']); + $rollbackStack[] = 'uninstallDependencies'; + } + if (!empty($manifest['user_nonprivileged'])) { + $this->createUser($manifest['user_nonprivileged']); + $rollbackStack[] = 'deleteUser'; + } + if (!empty($manifest['configuration'])) { + $this->copyConfigFiles($manifest['configuration'], $pluginDir); + $rollbackStack[] = 'removeConfigFiles'; + } + $this->copyPluginFiles($pluginDir, $this->rootPath); + $rollbackStack[] = 'removePluginFiles'; + + return true; + + } catch (\Exception $e) { + //$this->rollback($rollbackStack, $manifest, $pluginDir); + error_log('Plugin installation failed: ' . $e->getMessage()); + return false; + } + + } catch (\Exception $e) { + throw new \Exception('error: ' .$e->getMessage()); + } finally { + // cleanup tmp files + if (file_exists($tempFile)) { + unlink($tempFile); + } + if (is_dir($extractDir)) { + $this->deleteDir($extractDir); + } + } + } + + /** + * Adds sudoers entries to a temp file and copies to /etc/sudoers.d/ + * + * @param array $sudoers + */ + private function addSudoers(array $sudoers): void + { + $tmpSudoers = $this->tempSudoers . $this->pluginName; + $destination = $this->destSudoers; + $content = implode("\n", $sudoers); + + if (file_put_contents($tmpSudoers, $content) === false) { + throw new \Exception('Failed to update sudoers file.'); + } + + $cmd = sprintf('sudo visudo -cf %s', escapeshellarg($tmpSudoers)); + $return = shell_exec($cmd); + if (strpos(strtolower($return), 'parsed ok') !== false) { + $cmd = sprintf('sudo /etc/raspap/plugins/plugin_helper.sh sudoers %s', escapeshellarg($tmpSudoers)); + $return = shell_exec($cmd); + if (strpos(strtolower($return), 'ok') === false) { + throw new \Exception('Plugin helper failed to install sudoers.'); + } + } else { + throw new \Exception('Sudoers check failed.'); + } + } + + /** + * Installs plugin dependencies from the aptitude package repository + * + * @param array $dependencies + */ + private function installDependencies(array $dependencies): void + { + $packages = array_keys($dependencies); + $packageList = implode(' ', $packages); + + $cmd = sprintf('sudo /etc/raspap/plugins/plugin_helper.sh packages %s', escapeshellarg($packageList)); + $return = shell_exec($cmd); + if (strpos(strtolower($return), 'ok') === false) { + throw new \Exception('Plugin helper failed to install depedencies.'); + } + } + + /** + * Creates a non-priviledged Linux user + * + * @param array $user + */ + private function createUser(array $user): void + { + if (empty($user['name']) || empty($user['pass'])) { + throw new \InvalidArgumentException('User name or password is missing.'); + } + $username = escapeshellarg($user['name']); + $password = escapeshellarg($user['pass']); + + $cmd = sprintf('sudo /etc/raspap/plugins/plugin_helper.sh user %s %s', $username, $password); + $return = shell_exec($cmd); + if (strpos(strtolower($return), 'ok') === false) { + throw new \Exception('Plugin helper failed to create user: ' . $user['name']); + } + } + + /** + * Copies plugin configuration files to their destination + * + * @param array $configurations + * @param string $pluginDir + */ + private function copyConfigFiles(array $configurations, string $pluginDir): void + { + foreach ($configurations as $config) { + $source = escapeshellarg($pluginDir . DIRECTORY_SEPARATOR . $config['source']); + $destination = escapeshellarg($config['destination']); + $cmd = sprintf('sudo /etc/raspap/plugins/plugin_helper.sh config %s %s', $source, $destination); + $return = shell_exec($cmd); + if (strpos(strtolower($return), 'ok') === false) { + throw new \Exception("Failed to copy configuration file: $source to $destination"); + } + } + } + + /** + * Copies an extracted plugin directory from /tmp to /plugins + * + * @param string $source + * @param string $destination + */ + private function copyPluginFiles(string $source, string $destination): void + { + $source = escapeshellarg($source); + $destination = escapeshellarg($destination . DIRECTORY_SEPARATOR .$this->pluginPath . DIRECTORY_SEPARATOR . $this->pluginName); + $cmd = sprintf('sudo /etc/raspap/plugins/plugin_helper.sh plugin %s %s', $source, $destination); + $return = shell_exec($cmd); + if (strpos(strtolower($return), 'ok') === false) { + throw new \Exception('Failed to copy plugin files to: ' . $destination); + } + } + + /** + * Parses and returns a downloaded plugin manifest + * + * @param string $pluginDir + * @return array json + */ + private function parseManifest($pluginDir): array + { + $manifestPath = $pluginDir . DIRECTORY_SEPARATOR . 'manifest.json'; + if (!file_exists($manifestPath)) { + throw new \Exception('manifest.json file not found.'); + } + $json = file_get_contents($manifestPath); + $manifest = json_decode($json, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('Failed to parse manifest.json: ' . json_last_error_msg()); + } + return $manifest; + } + + /** + * Retrieves a plugin archive and extracts it to /tmp + * + * @param string $archiveUrl + * @return array + */ + private function getPluginArchive(string $archiveUrl): array + { + try { + + $tempFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('plugin_', true) . '.zip'; + $extractDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('plugin_', true); + $data = file_get_contents($archiveUrl); + + if ($data === false) { + throw new \Exception('Failed to download archive.'); + } + + file_put_contents($tempFile, $data); + + if (!mkdir($extractDir) && !is_dir($extractDir)) { + throw new \Exception('Failed to create temp directory.'); + } + + $cmd = escapeshellcmd("unzip -o $tempFile -d $extractDir"); + $output = shell_exec($cmd); + if ($output === null) { + throw new \Exception('Failed to extract archive.'); + } + + $extractedDirs = glob($extractDir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR); + if (empty($extractedDirs)) { + throw new \Exception('No directories found in archive.'); + } + $pluginDir = $extractedDirs[0]; + + return [$tempFile, $extractDir, $pluginDir]; + + } catch (\Exception $e) { + throw new \Exception('Error occurred: ' .$e->getMessage()); + } + } + + private function deleteDir(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $items = array_diff(scandir($dir), ['.', '..']); + foreach ($items as $item) { + $itemPath = $dir . DIRECTORY_SEPARATOR . $item; + is_dir($itemPath) ? $this->deleteDir($itemPath) : unlink($itemPath); + } + rmdir($dir); + } + /** * Returns a list of available plugins formatted as an HTML table * @@ -177,29 +445,6 @@ class PluginInstaller $html .= ''; return $html; } - - - /** Returns an array of installed plugins in pluginPath - * - * @return array $plugins - */ - public function getPlugins(): array - { - $plugins = []; - if (file_exists($this->pluginPath)) { - $directories = scandir($this->pluginPath); - - foreach ($directories as $directory) { - $pluginClass = "RaspAP\\Plugins\\$directory\\$directory"; - $pluginFile = $this->pluginPath . "/$directory/$directory.php"; - - if (file_exists($pluginFile) && class_exists($pluginClass)) { - $plugins[] = $pluginClass; - } - } - } - return $plugins; - } - } +