mirror of
				https://github.com/billz/raspap-webgui.git
				synced 2025-03-01 10:31:47 +00:00 
			
		
		
		
	Merge pull request #1717 from RaspAP/feat/plugin-manager
Plugin Manager UI
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -3,7 +3,7 @@ node_modules | ||||
| yarn-error.log | ||||
| *.swp | ||||
| includes/config.php | ||||
| plugins/ | ||||
| rootCA.pem | ||||
| vendor | ||||
| .env | ||||
| locale/**/*.mo | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| [submodule "plugins"] | ||||
| 	path = plugins | ||||
| 	url = https://github.com/RaspAP/plugins | ||||
							
								
								
									
										29
									
								
								ajax/plugins/do_plugin_install.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										29
									
								
								ajax/plugins/do_plugin_install.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| <?php | ||||
|  | ||||
| require '../../includes/csrf.php'; | ||||
| require_once '../../includes/session.php'; | ||||
| require_once '../../includes/config.php'; | ||||
| require_once '../../src/RaspAP/Auth/HTTPAuth.php'; | ||||
| require_once '../../includes/authenticate.php'; | ||||
| require_once '../../src/RaspAP/Plugins/PluginInstaller.php'; | ||||
|  | ||||
| $pluginInstaller = \RaspAP\Plugins\PluginInstaller::getInstance(); | ||||
| $plugin_uri = $_POST['plugin_uri'] ?? null; | ||||
| $plugin_version = $_POST['plugin_version'] ?? null; | ||||
|  | ||||
| if (isset($plugin_uri) && isset($plugin_version)) { | ||||
|     $archiveUrl = rtrim($plugin_uri, '/') . '/archive/refs/tags/' . $plugin_version .'.zip'; | ||||
|  | ||||
|     try { | ||||
|         $return = $pluginInstaller->installPlugin($archiveUrl); | ||||
|         echo json_encode($return); | ||||
|     } catch (Exception $e) { | ||||
|         http_response_code(422); // Unprocessable Content | ||||
|         echo json_encode(['error' => $e->getMessage()]); | ||||
|     } | ||||
| } else { | ||||
|     http_response_code(400); // Bad Request | ||||
|     echo json_encode(['error' => 'Plugin URI and version are required']); | ||||
|     exit; | ||||
| } | ||||
|  | ||||
| @@ -377,3 +377,14 @@ button > i.fas { | ||||
|   border: 1px solid #ced4da; | ||||
| } | ||||
|  | ||||
| textarea.plugin-log { | ||||
|   width: 100%; | ||||
|   height: 150px; | ||||
|   resize: none; | ||||
|   border: 1px solid #dee2e6; | ||||
|   border-radius: 0.25rem; | ||||
|   padding: 0.5rem; | ||||
|   background-color: #f8f9fa; | ||||
|   font-family: monospace; | ||||
|   font-size: 0.9rem; | ||||
| } | ||||
|   | ||||
							
								
								
									
										115
									
								
								app/js/custom.js
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								app/js/custom.js
									
									
									
									
									
								
							| @@ -468,6 +468,120 @@ $('#js-sys-reboot, #js-sys-shutdown').on('click', function (e) { | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| $('#install-user-plugin').on('shown.bs.modal', function (e) { | ||||
|     var button = $(e.relatedTarget); | ||||
|     var manifestData = button.data('plugin-manifest'); | ||||
|     var installed = button.data('plugin-installed'); | ||||
|  | ||||
|     if (manifestData) { | ||||
|         $('#plugin-uri').html(manifestData.plugin_uri | ||||
|             ? `<a href="${manifestData.plugin_uri}" target="_blank">${manifestData.plugin_uri}</a>` | ||||
|             : 'Unknown' | ||||
|         ); | ||||
|         $('#plugin-icon').attr('class', `${manifestData.icon || 'fas fa-plug'} link-secondary h5 me-2`); | ||||
|         $('#plugin-name').text(manifestData.name || 'Unknown'); | ||||
|         $('#plugin-version').text(manifestData.version || 'Unknown'); | ||||
|         $('#plugin-description').text(manifestData.description || 'No description provided'); | ||||
|         $('#plugin-author').html(manifestData.author | ||||
|                 ? manifestData.author + (manifestData.author_uri | ||||
|                 ? ` (<a href="${manifestData.author_uri}" target="_blank">profile</a>)` : '') : 'Unknown' | ||||
|             ); | ||||
|         $('#plugin-license').text(manifestData.license || 'Unknown'); | ||||
|         $('#plugin-locale').text(manifestData.default_locale || 'Unknown'); | ||||
|         $('#plugin-configuration').html(formatProperty(manifestData.configuration || {})); | ||||
|         $('#plugin-dependencies').html(formatProperty(manifestData.dependencies || {})); | ||||
|         $('#plugin-sudoers').html(formatProperty(manifestData.sudoers || [])); | ||||
|         $('#plugin-user-name').html(manifestData.user_nonprivileged.name || 'None'); | ||||
|     } | ||||
|     if (installed) { | ||||
|         $('#js-install-plugin-confirm').html('OK'); | ||||
|     } else { | ||||
|         $('#js-install-plugin-confirm').html('Install now'); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| $('#js-install-plugin-confirm').on('click', function (e) { | ||||
|     var progressText = $('#js-install-plugin-confirm').attr('data-message'); | ||||
|     var successHtml = $('#plugin-install-message').attr('data-message'); | ||||
|     var successText = $('<div>').text(successHtml).text(); | ||||
|     var pluginUri = $('#plugin-uri a').attr('href'); | ||||
|     var pluginVersion = $('#plugin-version').text(); | ||||
|     var csrfToken = $('meta[name=csrf_token]').attr('content'); | ||||
|  | ||||
|     $("#install-user-plugin").modal('hide'); | ||||
|  | ||||
|     if ($('#js-install-plugin-confirm').text() === 'Install now') { | ||||
|         $("#install-plugin-progress").modal('show'); | ||||
|  | ||||
|         $.post( | ||||
|             'ajax/plugins/do_plugin_install.php', | ||||
|             { | ||||
|                 'plugin_uri': pluginUri, | ||||
|                 'plugin_version': pluginVersion, | ||||
|                 'csrf_token': csrfToken | ||||
|             }, | ||||
|             function (data) { | ||||
|                 setTimeout(function () { | ||||
|                     response = JSON.parse(data); | ||||
|                     if (response === true) { | ||||
|                         $('#plugin-install-message').contents().first().text(successText); | ||||
|                         $('#plugin-install-message') | ||||
|                             .find('i') | ||||
|                             .removeClass('fas fa-cog fa-spin link-secondary') | ||||
|                             .addClass('fas fa-check'); | ||||
|                         $('#js-install-plugin-ok').removeAttr("disabled"); | ||||
|                     } else { | ||||
|                         const errorMessage = jsonData.error || 'An unknown error occurred.'; | ||||
|                         var errorLog = '<textarea class="plugin-log text-secondary" readonly>' + errorMessage + '</textarea>'; | ||||
|                         $('#plugin-install-message') | ||||
|                             .contents() | ||||
|                             .first() | ||||
|                             .replaceWith('An error occurred installing the plugin:'); | ||||
|                         $('#plugin-install-message').append(errorLog); | ||||
|                         $('#plugin-install-message').find('i').removeClass('fas fa-cog fa-spin link-secondary'); | ||||
|                         $('#js-install-plugin-ok').removeAttr("disabled"); | ||||
|                     } | ||||
|                 }, 200); | ||||
|             } | ||||
|         ).fail(function (xhr) { | ||||
|             const jsonData = JSON.parse(xhr.responseText); | ||||
|             const errorMessage = jsonData.error || 'An unknown error occurred.'; | ||||
|             $('#plugin-install-message') | ||||
|                 .contents() | ||||
|                 .first() | ||||
|                 .replaceWith('An error occurred installing the plugin:'); | ||||
|             var errorLog = '<textarea class="plugin-log text-secondary" readonly>' + errorMessage + '</textarea>'; | ||||
|             $('#plugin-install-message').append(errorLog); | ||||
|             $('#plugin-install-message').find('i').removeClass('fas fa-cog fa-spin link-secondary'); | ||||
|             $('#js-install-plugin-ok').removeAttr("disabled"); | ||||
|         }); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| $('#js-install-plugin-ok').on('click', function (e) { | ||||
|     $("#install-plugin-progress").modal('hide'); | ||||
|     window.location.reload(); | ||||
| }); | ||||
|  | ||||
| function formatProperty(prop) { | ||||
|     if (Array.isArray(prop)) { | ||||
|         if (typeof prop[0] === 'object') { | ||||
|             return prop.map(item => { | ||||
|                 return Object.entries(item) | ||||
|                     .map(([key, value]) => `${key}: ${value}`) | ||||
|                     .join('<br/>'); | ||||
|             }).join('<br/><br/>'); | ||||
|         } | ||||
|         return prop.map(line => `${line}<br/>`).join(''); | ||||
|     } | ||||
|     if (typeof prop === 'object') { | ||||
|         return Object.entries(prop) | ||||
|             .map(([key, value]) => `${key}: ${value}`) | ||||
|             .join('<br/>'); | ||||
|     } | ||||
|     return prop || 'None'; | ||||
| } | ||||
|  | ||||
| $(document).ready(function(){ | ||||
|     $("#PanelManual").hide(); | ||||
|     $('.ip_address').mask('0ZZ.0ZZ.0ZZ.0ZZ', { | ||||
| @@ -507,7 +621,6 @@ $('#wg-upload,#wg-manual').on('click', function (e) { | ||||
|     } | ||||
| }); | ||||
|  | ||||
| // Add the following code if you want the name of the file appear on select | ||||
| $(".custom-file-input").on("change", function() { | ||||
|   var fileName = $(this).val().split("\\").pop(); | ||||
|   $(this).siblings(".custom-file-label").addClass("selected").html(fileName); | ||||
|   | ||||
| @@ -63,6 +63,7 @@ define('RASPI_VNSTAT_ENABLED', true); | ||||
| define('RASPI_SYSTEM_ENABLED', true); | ||||
| define('RASPI_MONITOR_ENABLED', false); | ||||
| define('RASPI_RESTAPI_ENABLED', false); | ||||
| define('RASPI_PLUGINS_ENABLED', true); | ||||
|  | ||||
| // Locale settings | ||||
| define('LOCALE_ROOT', 'locale'); | ||||
|   | ||||
| @@ -65,6 +65,7 @@ $defaults = [ | ||||
|   'RASPI_SYSTEM_ENABLED' => true, | ||||
|   'RASPI_MONITOR_ENABLED' => false, | ||||
|   'RASPI_RESTAPI_ENABLED' => false, | ||||
|   'RASPI_PLUGINS_ENABLED' => true, | ||||
|  | ||||
|   // Locale settings | ||||
|   'LOCALE_ROOT' => 'locale', | ||||
|   | ||||
| @@ -1040,3 +1040,25 @@ function renderStatus($hostapd_led, $hostapd_status, $memused_led, $memused, $cp | ||||
|     <?php | ||||
| } | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Executes a callback with a timeout | ||||
|  * | ||||
|  * @param callable $callback function to execute | ||||
|  * @param int $interval timeout in milliseconds | ||||
|  * @return mixed result of the callback | ||||
|  * @throws \Exception if the execution exceeds the timeout or an error occurs | ||||
|  */ | ||||
| function callbackTimeout(callable $callback, int $interval) | ||||
| { | ||||
|     $startTime = microtime(true); // use high-resolution timer | ||||
|     $result = $callback(); | ||||
|     $elapsed = (microtime(true) - $startTime) * 1000; | ||||
|  | ||||
|     if ($elapsed > $interval) { | ||||
|         throw new \Exception('Operation timed out'); | ||||
|     } | ||||
|  | ||||
|     return $result; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,7 @@ require_once 'config.php'; | ||||
| function DisplaySystem(&$extraFooterScripts) | ||||
| { | ||||
|     $status = new \RaspAP\Messages\StatusMessage; | ||||
|     $pluginInstaller = \RaspAP\Plugins\PluginInstaller::getInstance(); | ||||
|  | ||||
|     if (isset($_POST['SaveLanguage'])) { | ||||
|         if (isset($_POST['locale'])) { | ||||
| @@ -85,53 +86,22 @@ function DisplaySystem(&$extraFooterScripts) | ||||
|     $kernel   = $system->kernelVersion(); | ||||
|     $systime  = $system->systime(); | ||||
|     $revision = $system->rpiRevision(); | ||||
|      | ||||
|     // mem used | ||||
|  | ||||
|     // memory use | ||||
|     $memused  = $system->usedMemory(); | ||||
|     $memused_status = "primary"; | ||||
|     if ($memused > 90) { | ||||
|         $memused_status = "danger"; | ||||
|         $memused_led = "service-status-down"; | ||||
|     } elseif ($memused > 75) { | ||||
|         $memused_status = "warning"; | ||||
|         $memused_led = "service-status-warn"; | ||||
|     } elseif ($memused >  0) { | ||||
|         $memused_status = "success"; | ||||
|         $memused_led = "service-status-up"; | ||||
|     } | ||||
|     $memStatus = getMemStatus($memused); | ||||
|     $memused_status = $memStatus['status']; | ||||
|     $memused_led = $memStatus['led']; | ||||
|  | ||||
|     // cpu load | ||||
|     $cpuload = $system->systemLoadPercentage(); | ||||
|     if ($cpuload > 90) { | ||||
|         $cpuload_status = "danger"; | ||||
|     } elseif ($cpuload > 75) { | ||||
|         $cpuload_status = "warning"; | ||||
|     } elseif ($cpuload >=  0) { | ||||
|         $cpuload_status = "success"; | ||||
|     } | ||||
|     $cpuload_status = getCPULoadStatus($cpuload); | ||||
|  | ||||
|     // cpu temp | ||||
|     $cputemp = $system->systemTemperature(); | ||||
|     if ($cputemp > 70) { | ||||
|         $cputemp_status = "danger"; | ||||
|         $cputemp_led = "service-status-down"; | ||||
|     } elseif ($cputemp > 50) { | ||||
|         $cputemp_status = "warning"; | ||||
|         $cputemp_led = "service-status-warn"; | ||||
|     } else { | ||||
|         $cputemp_status = "success"; | ||||
|         $cputemp_led = "service-status-up"; | ||||
|     } | ||||
|  | ||||
|     // hostapd status | ||||
|     $hostapd = $system->hostapdStatus(); | ||||
|     if ($hostapd[0] == 1) { | ||||
|         $hostapd_status = "active"; | ||||
|         $hostapd_led = "service-status-up"; | ||||
|     } else { | ||||
|         $hostapd_status = "inactive"; | ||||
|         $hostapd_led = "service-status-down"; | ||||
|     } | ||||
|     $cpuStatus = getCPUTempStatus($cputemp); | ||||
|     $cputemp_status = $cpuStatus['status']; | ||||
|     $cputemp_led =  $cpuStatus['led']; | ||||
|  | ||||
|     // theme options | ||||
|     $themes = [ | ||||
| @@ -147,6 +117,9 @@ function DisplaySystem(&$extraFooterScripts) | ||||
|     $extraFooterScripts[] = array('src'=>'app/js/huebee.js', 'defer'=>false); | ||||
|     $logLimit = isset($_SESSION['log_limit']) ? $_SESSION['log_limit'] : RASPI_LOG_SIZE_LIMIT; | ||||
|  | ||||
|     $plugins = $pluginInstaller->getUserPlugins(); | ||||
|     $pluginsTable = $pluginInstaller->getHTMLPluginsTable($plugins); | ||||
|  | ||||
|     echo renderTemplate("system", compact( | ||||
|         "arrLocales", | ||||
|         "status", | ||||
| @@ -167,11 +140,62 @@ function DisplaySystem(&$extraFooterScripts) | ||||
|         "cputemp", | ||||
|         "cputemp_status", | ||||
|         "cputemp_led", | ||||
|         "hostapd", | ||||
|         "hostapd_status", | ||||
|         "hostapd_led", | ||||
|         "themes", | ||||
|         "selectedTheme", | ||||
|         "logLimit" | ||||
|         "logLimit", | ||||
|         "pluginsTable" | ||||
|     )); | ||||
| } | ||||
|  | ||||
| function getMemStatus($memused): array | ||||
| { | ||||
|     $memused_status = "primary"; | ||||
|     $memused_led = ""; | ||||
|  | ||||
|     if ($memused > 90) { | ||||
|         $memused_status = "danger"; | ||||
|         $memused_led = "service-status-down"; | ||||
|     } elseif ($memused > 75) { | ||||
|         $memused_status = "warning"; | ||||
|         $memused_led = "service-status-warn"; | ||||
|     } elseif ($memused > 0) { | ||||
|         $memused_status = "success"; | ||||
|         $memused_led = "service-status-up"; | ||||
|     } | ||||
|  | ||||
|     return [ | ||||
|         'status' => $memused_status, | ||||
|         'led' => $memused_led | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| function getCPULoadStatus($cpuload): string | ||||
| { | ||||
|     if ($cpuload > 90) { | ||||
|         $status = "danger"; | ||||
|     } elseif ($cpuload > 75) { | ||||
|         $status = "warning"; | ||||
|     } elseif ($cpuload >=  0) { | ||||
|         $status = "success"; | ||||
|     } | ||||
|     return $status; | ||||
| } | ||||
|  | ||||
| function getCPUTempStatus($cputemp): array | ||||
| { | ||||
|     if ($cputemp > 70) { | ||||
|         $cputemp_status = "danger"; | ||||
|         $cputemp_led = "service-status-down"; | ||||
|     } elseif ($cputemp > 50) { | ||||
|         $cputemp_status = "warning"; | ||||
|         $cputemp_led = "service-status-warn"; | ||||
|     } else { | ||||
|         $cputemp_status = "success"; | ||||
|         $cputemp_led = "service-status-up"; | ||||
|     } | ||||
|     return [ | ||||
|         'status' => $cputemp_status, | ||||
|         'led' => $cputemp_led | ||||
|     ]; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -51,6 +51,7 @@ function _install_raspap() { | ||||
|     _download_latest_files | ||||
|     _change_file_ownership | ||||
|     _create_hostapd_scripts | ||||
|     _create_plugin_scripts | ||||
|     _create_lighttpd_scripts | ||||
|     _install_lighttpd_configs | ||||
|     _default_configuration | ||||
| @@ -313,6 +314,19 @@ function _create_hostapd_scripts() { | ||||
|     _install_status 0 | ||||
| } | ||||
|  | ||||
| # Generate plugin helper scripts | ||||
| function _create_plugin_scripts() { | ||||
|     _install_log "Creating plugin helper scripts" | ||||
|     sudo mkdir $raspap_dir/plugins || _install_status 1 "Unable to create directory '$raspap_dir/plugins'" | ||||
|  | ||||
|     # Copy plugin helper script | ||||
|     sudo cp "$webroot_dir/installers/"plugin_helper.sh "$raspap_dir/plugins" || _install_status 1 "Unable to move plugin script" | ||||
|     # Change ownership and permissions of plugin script | ||||
|     sudo chown -c root:root "$raspap_dir/plugins/"*.sh || _install_status 1 "Unable change owner and/or group" | ||||
|     sudo chmod 750 "$raspap_dir/plugins/"*.sh || _install_status 1 "Unable to change file permissions" | ||||
|     _install_status 0 | ||||
| } | ||||
|  | ||||
| # Generate lighttpd service control scripts | ||||
| function _create_lighttpd_scripts() { | ||||
|     _install_log "Creating lighttpd control scripts" | ||||
| @@ -584,14 +598,14 @@ function _download_latest_files() { | ||||
|     if [ "$repo" == "RaspAP/raspap-insiders" ]; then | ||||
|         if [ -n "$username" ] && [ -n "$acctoken" ]; then | ||||
|             insiders_source_url="https://${username}:${acctoken}@github.com/$repo" | ||||
|             git clone --branch $branch --depth 1 -c advice.detachedHead=false $insiders_source_url $source_dir || clone=false | ||||
|             git clone --branch $branch --depth 1 --recurse-submodules -c advice.detachedHead=false $insiders_source_url $source_dir || clone=false | ||||
|         else | ||||
|             _install_status 3 | ||||
|             echo "Insiders please read this: https://docs.raspap.com/insiders/#authentication" | ||||
|         fi | ||||
|     fi | ||||
|     if [ -z "$insiders_source_url" ]; then | ||||
|         git clone --branch $branch --depth 1 -c advice.detachedHead=false $git_source_url $source_dir || clone=false | ||||
|         git clone --branch $branch --depth 1 --recurse-submodules -c advice.detachedHead=false $git_source_url $source_dir || clone=false | ||||
|     fi | ||||
|     if [ "$clone" = false ]; then | ||||
|         _install_status 1 "Unable to download files from GitHub" | ||||
|   | ||||
							
								
								
									
										113
									
								
								installers/plugin_helper.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										113
									
								
								installers/plugin_helper.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| #!/bin/bash | ||||
| # | ||||
| # PluginInstaller helper for RaspAP  | ||||
| # @author billz | ||||
| # license: GNU General Public License v3.0 | ||||
|  | ||||
| # Exit on error | ||||
| set -o errexit | ||||
|  | ||||
| readonly raspap_user="www-data" | ||||
|  | ||||
| [ $# -lt 1 ] && { echo "Usage: $0 <action> [parameters...]"; exit 1; } | ||||
|  | ||||
| action="$1"  # action to perform | ||||
| shift 1       | ||||
|  | ||||
| case "$action" in | ||||
|  | ||||
|   "sudoers") | ||||
|     [ $# -ne 1 ] && { echo "Usage: $0 sudoers <file>"; exit 1; } | ||||
|     file="$1" | ||||
|     plugin_name=$(basename "$file") | ||||
|     dest="/etc/sudoers.d/${plugin_name}" | ||||
|  | ||||
|     mv "$file" "$dest" || { echo "Error: Failed to move $file to $dest."; exit 1; } | ||||
|  | ||||
|     chown root:root "$dest" || { echo "Error: Failed to set ownership for $dest."; exit 1; } | ||||
|     chmod 0440 "$dest" || { echo "Error: Failed to set permissions for $dest."; exit 1; } | ||||
|  | ||||
|     echo "OK" | ||||
|     ;; | ||||
|  | ||||
|   "packages") | ||||
|     [ $# -lt 1 ] && { echo "Usage: $0 packages <apt_packages...>"; exit 1; } | ||||
|  | ||||
|     echo "Installing APT packages..." | ||||
|     for package in "$@"; do | ||||
|       echo "Installing package: $package" | ||||
|       apt-get install -y "$package" || { echo "Error: Failed to install $package."; exit 1; } | ||||
|     done | ||||
|     echo "OK" | ||||
|     ;; | ||||
|  | ||||
|   "user") | ||||
|     [ $# -lt 2 ] && { echo "Usage: $0 user <username> <password>."; exit 1; } | ||||
|  | ||||
|     username=$1 | ||||
|     password=$2 | ||||
|      | ||||
|     if id "$username" &>/dev/null; then # user already exists | ||||
|         echo "OK" | ||||
|         exit 0 | ||||
|     fi | ||||
|     # create the user without shell access | ||||
|     useradd -r -s /bin/false "$username" | ||||
|  | ||||
|     # set password non-interactively | ||||
|     echo "$username:$password" | chpasswd | ||||
|  | ||||
|     echo "OK" | ||||
|     ;; | ||||
|  | ||||
|   "config") | ||||
|     [ $# -lt 2 ] && { echo "Usage: $0 config <source> <destination>"; exit 1; } | ||||
|  | ||||
|     source=$1 | ||||
|     destination=$2 | ||||
|  | ||||
|     if [ ! -f "$source" ]; then | ||||
|         echo "Source file $source does not exist." | ||||
|         exit 1 | ||||
|     fi | ||||
|  | ||||
|     mkdir -p "$(dirname "$destination")" | ||||
|     cp "$source" "$destination" | ||||
|  | ||||
|     echo "OK" | ||||
|     ;; | ||||
|  | ||||
|   "plugin") | ||||
|     [ $# -lt 2 ] && { echo "Usage: $0 plugin <source> <destination>"; exit 1; } | ||||
|  | ||||
|     source=$1 | ||||
|     destination=$2 | ||||
|  | ||||
|     if [ ! -d "$source" ]; then | ||||
|         echo "Source directory $source does not exist." | ||||
|         exit 1 | ||||
|     fi | ||||
|  | ||||
|     plugin_dir=$(dirname "$destination") | ||||
|     if [ ! -d "$lugin_dir" ]; then | ||||
|         mkdir -p "$plugin_dir" | ||||
|     fi | ||||
|  | ||||
|     cp -R "$source" "$destination" | ||||
|     chown -R $raspap_user:$raspap_user "$plugin_dir" | ||||
|  | ||||
|     echo "OK" | ||||
|     ;; | ||||
|  | ||||
|   *) | ||||
|     echo "Invalid action: $action" | ||||
|     echo "Usage: $0 <action> [parameters...]" | ||||
|     echo "Actions:" | ||||
|     echo "  sudoers <file>                 Install a sudoers file" | ||||
|     echo "  packages <packages>            Install aptitude package(s)" | ||||
|     echo "  user <name> <password>         Add user non-interactively" | ||||
|     echo "  config <source <destination>   Applies a config file" | ||||
|     echo "  plugin <source <destination>   Copies a plugin directory" | ||||
|     exit 1 | ||||
|     ;; | ||||
| esac | ||||
| @@ -78,3 +78,5 @@ www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/wireguard/wg-*.key | ||||
| www-data ALL=(ALL) NOPASSWD:/usr/sbin/netplan | ||||
| www-data ALL=(ALL) NOPASSWD:/bin/truncate -s 0 /tmp/*.log,/bin/truncate -s 0 /var/log/dnsmasq.log | ||||
| www-data ALL=(ALL) NOPASSWD:/usr/bin/vnstat * | ||||
| www-data ALL=(ALL) NOPASSWD:/usr/sbin/visudo -cf * | ||||
| www-data ALL=(ALL) NOPASSWD:/etc/raspap/plugins/plugin_helper.sh | ||||
|   | ||||
| @@ -920,6 +920,81 @@ msgstr "Changing log limit size to %s KB" | ||||
| msgid "Information provided by raspap.sysinfo" | ||||
| msgstr "Information provided by raspap.sysinfo" | ||||
|  | ||||
| msgid "The following user plugins are available to extend RaspAP's functionality." | ||||
| msgstr "The following user plugins are available to extend RaspAP's functionality." | ||||
|  | ||||
| msgid "Choose <strong>Details</strong> for more information and to install a plugin." | ||||
| msgstr "Choose <strong>Details</strong> for more information and to install a plugin." | ||||
|  | ||||
| msgid "Network error" | ||||
| msgstr "Network error" | ||||
|  | ||||
| msgid "Unable to load plugins" | ||||
| msgstr "Unable to load plugins" | ||||
|  | ||||
| msgid "Reload" | ||||
| msgstr "Reload" | ||||
|  | ||||
| msgid "and try again" | ||||
| msgstr "and try again" | ||||
|  | ||||
| msgid "Plugins" | ||||
| msgstr "Plugins" | ||||
|  | ||||
| msgid "Plugin details" | ||||
| msgstr "Plugin details" | ||||
|  | ||||
| msgid "Name" | ||||
| msgstr "Name" | ||||
|  | ||||
| msgid "Version" | ||||
| msgstr "Version" | ||||
|  | ||||
| msgid "Description" | ||||
| msgstr "Description" | ||||
|  | ||||
| msgid "Plugin source" | ||||
| msgstr "Plugin source" | ||||
|  | ||||
| msgid "Author" | ||||
| msgstr "Author" | ||||
|  | ||||
| msgid "License" | ||||
| msgstr "License" | ||||
|  | ||||
| msgid "Language locale" | ||||
| msgstr "Language locale" | ||||
|  | ||||
| msgid "Configuration files" | ||||
| msgstr "Configuration files" | ||||
|  | ||||
| msgid "Dependencies" | ||||
| msgstr "Dependencies" | ||||
|  | ||||
| msgid "Permissions" | ||||
| msgstr "Permissions" | ||||
|  | ||||
| msgid "Non-privileged users" | ||||
| msgstr "Non-privileged users" | ||||
|  | ||||
| msgid "Install now" | ||||
| msgstr "Install now" | ||||
|  | ||||
| msgid "Installing plugin" | ||||
| msgstr "Installing plugin" | ||||
|  | ||||
| msgid "Plugin installation in progress..." | ||||
| msgstr "Plugin installation in progress..." | ||||
|  | ||||
| msgid "Plugin install completed." | ||||
| msgstr "Plugin install completed." | ||||
|  | ||||
| msgid "Details" | ||||
| msgstr "Details" | ||||
|  | ||||
| msgid "Installed" | ||||
| msgstr "Installed" | ||||
|  | ||||
| #: includes/data_usage.php | ||||
| msgid "Data usage" | ||||
| msgstr "Data usage" | ||||
|   | ||||
							
								
								
									
										1
									
								
								plugins
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								plugins
									
									
									
									
									
										Submodule
									
								
							 Submodule plugins added at 825f74edf4
									
								
							
							
								
								
									
										421
									
								
								src/RaspAP/Plugins/PluginInstaller.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								src/RaspAP/Plugins/PluginInstaller.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,421 @@ | ||||
| <?php | ||||
|  | ||||
| /** | ||||
|  * Plugin Installer class | ||||
|  * | ||||
|  * @description Class to handle installation of user plugins | ||||
|  * @author      Bill Zimmerman <billzimmerman@gmail.com> | ||||
|  * @license     https://github.com/raspap/raspap-webgui/blob/master/LICENSE | ||||
|  */ | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace RaspAP\Plugins; | ||||
|  | ||||
| class PluginInstaller | ||||
| { | ||||
|     private static $instance = null; | ||||
|     private $pluginName; | ||||
|     private $manifestRaw; | ||||
|     private $tempSudoers; | ||||
|     private $destSudoers; | ||||
|     private $refModules; | ||||
|     private $rootPath; | ||||
|     private $pluginsManifest; | ||||
|  | ||||
|     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']; | ||||
|         $this->pluginsManifest = '/plugins/manifest.json'; | ||||
|     } | ||||
|  | ||||
|     // Returns a single instance of PluginInstaller | ||||
|     public static function getInstance(): PluginInstaller | ||||
|     { | ||||
|         if (self::$instance === null) { | ||||
|             self::$instance = new PluginInstaller(); | ||||
|         } | ||||
|         return self::$instance; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns availble user plugin details from a manifest.json file | ||||
|      * | ||||
|      * @return array $plugins | ||||
|      */ | ||||
|     public function getUserPlugins() | ||||
|     { | ||||
|         try { | ||||
|             $manifestPath = $this->rootPath . $this->pluginsManifest; | ||||
|             if (!file_exists($manifestPath)) { | ||||
|                 throw new \Exception("Manifest file not found at " . $manifestPath); | ||||
|             } | ||||
|  | ||||
|             // decode manifest file contents | ||||
|             $manifestContents = file_get_contents($manifestPath); | ||||
|             $manifestData = json_decode($manifestContents, true); | ||||
|  | ||||
|             if (json_last_error() !== JSON_ERROR_NONE) { | ||||
|                 throw new \Exception("Error parsing manifest.json: " . json_last_error_msg()); | ||||
|             } | ||||
|             // fetch installed plugins | ||||
|             $installedPlugins = $this->getPlugins(); | ||||
|  | ||||
|             $plugins = []; | ||||
|  | ||||
|             foreach ($manifestData as $pluginManifest) { | ||||
|                 $installed = false; | ||||
|  | ||||
|                 // Check if the plugin is installed | ||||
|                 foreach ($installedPlugins as $plugin) { | ||||
|                     if (str_contains($plugin, $pluginManifest[0]['namespace'])) { | ||||
|                         $installed = true; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 $plugins[] = [ | ||||
|                     'manifest' => $pluginManifest, | ||||
|                     'installed' => $installed | ||||
|                 ]; | ||||
|             } | ||||
|             return $plugins; | ||||
|         } catch (\Exception $e) { | ||||
|             error_log("An error occurred: " . $e->getMessage()); | ||||
|             throw $e;  // re-throw to global ExceptionHandler | ||||
|             return []; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     public function installPlugin($archiveUrl): bool | ||||
|     { | ||||
|         $tempFile = null; | ||||
|         $extractDir = null; | ||||
|         $pluginDir = null; | ||||
|  | ||||
|         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); | ||||
|                 throw new \Exception('Installation step failed: ' . $e->getMessage()); | ||||
|                 error_log('Plugin installation failed: ' . $e->getMessage()); | ||||
|             } | ||||
|  | ||||
|         } catch (\Exception $e) { | ||||
|             error_log('An error occured: ' .$e->getMessage()); | ||||
|             throw new \Exception( $e->getMessage()); | ||||
|             //throw $e; | ||||
|         } finally { | ||||
|             if (!empty($tempFile) && file_exists($tempFile)) { | ||||
|                 unlink($tempFile); | ||||
|             } | ||||
|             if (!empty($extractDir) && 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 | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|     private function getPluginArchive(string $archiveUrl): array | ||||
|     { | ||||
|         $tempFile = ''; | ||||
|         $extractDir = ''; | ||||
|  | ||||
|         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); // suppress PHP warnings for better exception handling | ||||
|  | ||||
|             if ($data === false) { | ||||
|                 $error = error_get_last(); | ||||
|                 throw new \Exception('Failed to download archive: ' . ($error['message'] ?? 'Unknown error')); | ||||
|             } | ||||
|  | ||||
|             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 plugin archive.'); | ||||
|             } | ||||
|  | ||||
|             $extractedDirs = glob($extractDir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR); | ||||
|             if (empty($extractedDirs)) { | ||||
|                 throw new \Exception('No directories found in plugin archive.'); | ||||
|             } | ||||
|  | ||||
|             $pluginDir = $extractedDirs[0]; | ||||
|  | ||||
|             return [$tempFile, $extractDir, $pluginDir]; | ||||
|         } catch (\Exception $e) { | ||||
|             if (!empty($tempFile) && file_exists($tempFile)) { | ||||
|             unlink($tempFile); | ||||
|             } | ||||
|             if (!empty($extractDir) && is_dir($extractDir)) { | ||||
|                 rmdir($extractDir); | ||||
|             } | ||||
|             throw new \Exception('Error occurred during plugin archive retrieval: ' . $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 | ||||
|      * | ||||
|      * @param array $plugins | ||||
|      * @return string $html | ||||
|      */ | ||||
|     public function getHTMLPluginsTable(array $plugins): string | ||||
|     { | ||||
|         $html = '<table class="table table-striped table-hover">'; | ||||
|         $html .= '<thead><tr>'; | ||||
|         $html .= '<th scope="col">Name</th>'; | ||||
|         $html .= '<th scope="col">Version</th>'; | ||||
|         $html .= '<th scope="col">Description</th>'; | ||||
|         $html .= '<th scope="col"></th>'; | ||||
|         $html .= '</tr></thead><tbody>'; | ||||
|  | ||||
|         foreach ($plugins as $plugin) { | ||||
|  | ||||
|             $manifestData = $plugin['manifest'][0] ?? []; // Access the first manifest entry or default to an empty array | ||||
|  | ||||
|             $manifest = htmlspecialchars(json_encode($manifestData), ENT_QUOTES, 'UTF-8'); | ||||
|             $installed = $plugin['installed']; | ||||
|             if ($installed === true) { | ||||
|                 $button = '<button type="button" class="btn btn-outline btn-primary btn-sm text-nowrap" | ||||
|                     name="plugin-details" data-bs-toggle="modal" data-bs-target="#install-user-plugin" | ||||
|                     data-plugin-manifest="' .$manifest. '" data-plugin-installed="' .$installed. '"> ' . _("Installed") .'</button>'; | ||||
|             } elseif (!RASPI_MONITOR_ENABLED) { | ||||
|                 $button = '<button type="button" class="btn btn-outline btn-primary btn-sm text-nowrap" | ||||
|                     name="install-plugin" data-bs-toggle="modal" data-bs-target="#install-user-plugin" | ||||
|                     data-plugin-manifest="' .$manifest. '"> ' . _("Details") .'</button>'; | ||||
|             } | ||||
|  | ||||
|             $icon = htmlspecialchars($manifestData['icon'] ?? ''); | ||||
|             $pluginUri = htmlspecialchars($manifestData['plugin_uri'] ?? ''); | ||||
|             $nameText = htmlspecialchars($manifestData['name'] ?? 'Unknown Plugin'); | ||||
|  | ||||
|             $name = '<i class="' . $icon . ' link-secondary me-2"></i><a href="' | ||||
|                 . $pluginUri | ||||
|                 . '" target="_blank">' | ||||
|                 . $nameText. '</a>'; | ||||
|  | ||||
|             $version = htmlspecialchars($manifestData['version'] ?? 'N/A'); | ||||
|             $description = htmlspecialchars($manifestData['description'] ?? 'No description available'); | ||||
|  | ||||
|             $html .= '<tr><td>' .$name. '</td>'; | ||||
|             $html .= '<td>' .$version. '</td>'; | ||||
|             $html .= '<td>' .$description. '</td>'; | ||||
|             $html .= '<td>' .$button. '</td></tr>'; | ||||
|         } | ||||
|         $html .= '</tbody></table>'; | ||||
|         return $html; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -18,6 +18,9 @@ | ||||
|           <li role="presentation" class="nav-item"><a class="nav-link" id="themetab" href="#theme" aria-controls="theme" role="tab" data-bs-toggle="tab"><?php echo _("Theme"); ?></a></li> | ||||
|           <li role="presentation" class="nav-item"><a class="nav-link" id="advancedtab" href="#advanced" aria-controls="advanced" role="tab" data-bs-toggle="tab"><?php echo _("Advanced"); ?></a></li> | ||||
|           <li role="presentation" class="nav-item"><a class="nav-link" id="toolstab" href="#tools" aria-controls="tools" role="tab" data-bs-toggle="tab"><?php echo _("Tools"); ?></a></li> | ||||
|           <?php if (RASPI_PLUGINS_ENABLED) : ?> | ||||
|           <li role="presentation" class="nav-item"><a class="nav-link" id="pluginstab" href="#plugins" aria-controls="plugins" role="tab" data-bs-toggle="tab"><?php echo _("Plugins"); ?></a></li> | ||||
|           <?php endif ?> | ||||
|         </ul> | ||||
|           <!-- Tab panes --> | ||||
|           <div class="tab-content"> | ||||
| @@ -26,6 +29,9 @@ | ||||
|             <?php echo renderTemplate("system/theme", $__template_data) ?> | ||||
|             <?php echo renderTemplate("system/advanced", $__template_data) ?> | ||||
|             <?php echo renderTemplate("system/tools", $__template_data) ?> | ||||
|             <?php if (RASPI_PLUGINS_ENABLED) : ?> | ||||
|             <?php echo renderTemplate("system/plugins", $__template_data) ?> | ||||
|             <?php endif ?> | ||||
|           </div><!-- /.tab-content --> | ||||
|         </form> | ||||
|       </div><!-- /.card-body --> | ||||
| @@ -105,3 +111,83 @@ | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <!-- modal install-plugin --> | ||||
| <div class="modal fade" id="install-user-plugin" tabindex="-1" role="dialog" aria-labelledby="ModalLabel" aria-hidden="true"> | ||||
|   <div class="modal-dialog modal-lg" role="document"> | ||||
|     <div class="modal-content"> | ||||
|       <div class="modal-header"> | ||||
|       <div class="modal-title" id="ModalLabel"><i class="fas fa-plug me-2"></i><?php echo _("Plugin details"); ?></div> | ||||
|       </div> | ||||
|       <div class="modal-body"> | ||||
|  | ||||
|         <i id="plugin-icon" class="fas fa-plug link-secondary me-2"></i><span id="plugin-name" class="h4 mb-0"></span> | ||||
|         <p id="plugin-description" class="mb-3"></p> | ||||
|  | ||||
|         <table class="table table-bordered"> | ||||
|           <tbody> | ||||
|             <tr> | ||||
|               <th><?php echo _("Plugin source"); ?></th> | ||||
|               <td><span id="plugin-uri"></span></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th><?php echo _("Version"); ?></th> | ||||
|               <td><span id="plugin-version"></span></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th><?php echo _("Author"); ?></th> | ||||
|               <td><span id="plugin-author"></span></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th><?php echo _("License"); ?></th> | ||||
|               <td><span id="plugin-license"></span></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th><?php echo _("Language locale"); ?></th> | ||||
|               <td><small><code><span id="plugin-locale"></span></span></code></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th><?php echo _("Configuration files"); ?></th> | ||||
|               <td><small><code><span id="plugin-configuration" class="mb-0"></span></code></small></td> | ||||
|               </td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th><?php echo _("Dependencies"); ?></th> | ||||
|               <td><small><code><span id="plugin-dependencies" class="mb-0"></span></code></small></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th><?php echo _("Permissions"); ?></th> | ||||
|               <td><small><code><span id="plugin-sudoers" class="mb-0"></span></code></small></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th><?php echo _("Non-privileged users"); ?></th> | ||||
|               <td><small><code><span id="plugin-user-name"></span></small></code></p></td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|  | ||||
|       </div> | ||||
|       <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"><?php echo _("Cancel"); ?></button> | ||||
|       <button type="button" id="js-install-plugin-confirm" class="btn btn-outline-success btn-activate"></button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <!-- modal plugin-install-progress --> | ||||
| <div class="modal fade" id="install-plugin-progress" tabindex="-1" role="dialog" aria-labelledby="ModalLabel" aria-hidden="true"> | ||||
|   <div class="modal-dialog" role="document"> | ||||
|     <div class="modal-content"> | ||||
|       <div class="modal-header"> | ||||
|       <div class="modal-title" id="ModalLabel"><i class="fas fa-download me-2"></i><?php echo _("Installing plugin"); ?></div> | ||||
|       </div> | ||||
|       <div class="modal-body"> | ||||
|         <div class="col-md-12 mb-3 mt-1" data-message="<?php echo _("Plugin install completed."); ?>" id="plugin-install-message"><?php echo _("Plugin installation in progress..."); ?><i class="fas fa-cog fa-spin link-secondary ms-2"></i></div> | ||||
|       </div> | ||||
|       <div class="modal-footer"> | ||||
|       <button type="button" id="js-install-plugin-ok" class="btn btn-outline-success btn-activate" disabled><?php echo _("OK"); ?></button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
|   | ||||
							
								
								
									
										19
									
								
								templates/system/plugins.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								templates/system/plugins.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| <!-- plugins tab --> | ||||
| <div role="tabpanel" class="tab-pane" id="plugins"> | ||||
|   <h4 class="mt-3"><?php echo _("Plugins") ;?></h4> | ||||
|   <?php echo CSRFTokenFieldTag() ?> | ||||
|   <div class="row"> | ||||
|     <div class="form-group col-lg-8 col-md-8"> | ||||
|     <label> | ||||
|       <?php echo _("The following user plugins are available to extend RaspAP's functionality."); ?> | ||||
|     </label> | ||||
|     <?php if (!RASPI_MONITOR_ENABLED) : ?> | ||||
|       <div class="small mt-2"> | ||||
|         <?php echo _("Choose <strong>Details</strong> for more information and to install a plugin."); ?> | ||||
|       </div> | ||||
|     <?php endif ?> | ||||
|     <?php echo $pluginsTable; ?> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| @@ -1,4 +1,4 @@ | ||||
| <!-- reset tab --> | ||||
| <!-- tools tab --> | ||||
| <div role="tabpanel" class="tab-pane" id="tools"> | ||||
|   <h4 class="mt-3"><?php echo _("System tools") ;?></h4> | ||||
|     <?php if (!RASPI_MONITOR_ENABLED) : ?> | ||||
| @@ -36,4 +36,3 @@ | ||||
|     <?php endif ?> | ||||
| </div> | ||||
|  | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user