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:
commit
32e191e55d
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'])) {
|
||||
@ -86,52 +87,21 @@ function DisplaySystem(&$extraFooterScripts)
|
||||
$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
@ -0,0 +1 @@
|
||||
Subproject commit 825f74edf49ae34705deb4625d3f3c571dc69bda
|
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>
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user