Merge pull request #1693 from RaspAP/feat/plugin-architecture

Plugin architecture to support user add-ons
This commit is contained in:
Bill Zimmerman 2024-11-10 09:15:11 +01:00 committed by GitHub
commit b74db9464e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 322 additions and 148 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@ node_modules
yarn-error.log
*.swp
includes/config.php
plugins/
rootCA.pem
vendor
.env

View file

@ -542,9 +542,13 @@ function ConvertToSecurity($security)
/**
* Renders a simple PHP template
*/
function renderTemplate($name, $__template_data = [])
function renderTemplate($name, $__template_data = [], $pluginName = null)
{
$file = realpath(dirname(__FILE__) . "/../templates/$name.php");
if (is_string($pluginName)) {
$file = realpath(dirname(__FILE__) . "/../plugins/$pluginName/templates/$name.php");
} else {
$file = realpath(dirname(__FILE__) . "/../templates/$name.php");
}
if (!file_exists($file)) {
return "template $name ($file) not found";
}
@ -1005,3 +1009,27 @@ function lightenColor($color, $percent)
return sprintf("#%02x%02x%02x", $r, $g, $b);
}
function renderStatus($hostapd_led, $hostapd_status, $memused_led, $memused, $cputemp_led, $cputemp)
{
?>
<div class="row g-0">
<div class="col-4 ms-2 sidebar-brand-icon">
<img src="app/img/raspAP-logo.php" class="navbar-logo" width="60" height="60">
</div>
<div class="col ml-2">
<div class="ml-1 sb-status">Status</div>
<div class="info-item-xs"><span class="icon">
<i class="fas fa-circle <?php echo ($hostapd_led); ?>"></i></span> <?php echo _("Hotspot").' '. _($hostapd_status); ?>
</div>
<div class="info-item-xs"><span class="icon">
<i class="fas fa-circle <?php echo ($memused_led); ?>"></i></span> <?php echo _("Mem Use").': '. htmlspecialchars(strval($memused), ENT_QUOTES); ?>%
</div>
<div class="info-item-xs"><span class="icon">
<i class="fas fa-circle <?php echo ($cputemp_led); ?>"></i></span> <?php echo _("CPU").': '. htmlspecialchars($cputemp, ENT_QUOTES); ?>°C
</div>
</div>
</div>
<?php
}

View file

@ -1,8 +1,27 @@
<?php
$extraFooterScripts = array();
$page = $_SERVER['PATH_INFO'];
// handle page actions
switch ($page) {
<?php
$pluginManager = \RaspAP\Plugins\PluginManager::getInstance();
// Get the requested page
$extraFooterScripts = array();
$page = $_SERVER['PATH_INFO'];
// Check if any plugin wants to handle the request
if (!$pluginManager->handlePageAction($page)) {
// If no plugin is available fall back to core page action handlers
handleCorePageAction($page, $extraFooterScripts);
}
/**
* Core application page handling
*
* @param string $page
* @param array $extraFooterScripts
* @return void
*/
function handleCorePageAction(string $page, array &$extraFooterScripts): void
{
switch ($page) {
case "/wlan0_info":
DisplayDashboard($extraFooterScripts);
break;
@ -53,6 +72,6 @@
break;
default:
DisplayDashboard($extraFooterScripts);
}
?>
}
}

View file

@ -1,89 +1,15 @@
<div class="row g-0">
<div class="col-4 ms-2 sidebar-brand-icon">
<img src="app/img/raspAP-logo.php" class="navbar-logo" width="60" height="60">
</div>
<div class="col ml-2">
<div class="ml-1 sb-status">Status</div>
<div class="info-item-xs"><span class="icon">
<i class="fas fa-circle <?php echo ($hostapd_led); ?>"></i></span> <?php echo _("Hotspot").' '. _($hostapd_status); ?>
</div>
<div class="info-item-xs"><span class="icon">
<i class="fas fa-circle <?php echo ($memused_led); ?>"></i></span> <?php echo _("Mem Use").': '. htmlspecialchars(strval($memused), ENT_QUOTES); ?>%
</div>
<div class="info-item-xs"><span class="icon">
<i class="fas fa-circle <?php echo ($cputemp_led); ?>"></i></span> <?php echo _("CPU").': '. htmlspecialchars($cputemp, ENT_QUOTES); ?>°C
</div>
</div>
</div>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="wlan0_info"><i class="sb-nav-link-icon fa-solid fa-gauge-high fa-fw mr-2"></i><span class="nav-label"><?php echo _("Dashboard"); ?></span></a>
</div>
<?php if (RASPI_HOTSPOT_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="hostapd_conf"><i class="sb-nav-link-icon far fa-dot-circle fa-fw mr-2"></i><span class="nav-label"><?php echo _("Hotspot"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_DHCP_ENABLED && !$_SESSION["bridgedEnabled"]) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="dhcpd_conf"><i class="sb-nav-link-icon fas fa-exchange-alt fa-fw mr-2"></i><span class="nav-label"><?php echo _("DHCP Server"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_ADBLOCK_ENABLED && !$_SESSION["bridgedEnabled"]) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="adblock_conf"><i class="sb-nav-link-icon far fa-hand-paper fa-fw mr-2"></i><span class="nav-label"><?php echo _("Ad Blocking"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_NETWORK_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="network_conf"><i class="sb-nav-link-icon fas fa-network-wired fa-fw mr-2"></i><span class="nav-label"><?php echo _("Networking"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_WIFICLIENT_ENABLED && !$_SESSION["bridgedEnabled"]) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="wpa_conf"><i class="sb-nav-link-icon fas fa-wifi fa-fw mr-2"></i><span class="nav-label"><?php echo _("WiFi client"); ?></span></a>
</div>
<?php endif; ?>
<?php if (RASPI_OPENVPN_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="openvpn_conf"><i class="sb-nav-link-icon fas fa-key fa-fw mr-2"></i><span class="nav-label"><?php echo _("OpenVPN"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_WIREGUARD_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="wg_conf"><i class="sb-nav-link-icon ra-wireguard mr-2"></i><span class="nav-label"><?php echo _("WireGuard"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_VPN_PROVIDER_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="provider_conf"><i class="sb-nav-link-icon fas fa-shield-alt fa-fw mr-2"></i><span class="nav-label"><?php echo _(getProviderValue($_SESSION["providerID"], "name")); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_TORPROXY_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="torproxy_conf"><i class="sb-nav-link-icon fas fa-eye-slash fa-fw mr-2"></i><span class="nav-label"><?php echo _("TOR proxy"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_CONFAUTH_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="auth_conf"><i class="sb-nav-link-icon fas fa-user-lock fa-fw mr-2"></i><span class="nav-label"><?php echo _("Authentication"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_VNSTAT_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="data_use"><i class="sb-nav-link-icon fas fa-chart-area fa-fw mr-2"></i><span class="nav-label"><?php echo _("Data usage"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_RESTAPI_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="restapi_conf"><i class="sb-nav-link-icon fas fa-puzzle-piece mr-2"></i><span class="nav-label"><?php echo _("RestAPI"); ?></a>
</div>
<?php endif; ?>
<?php if (RASPI_SYSTEM_ENABLED) : ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="system_info"><i class="sb-nav-link-icon fas fa-cube fa-fw mr-2"></i><span class="nav-label"><?php echo _("System"); ?></a>
</div>
<?php endif; ?>
<div class="sb-nav-link-icon px-2">
<a class="nav-link" href="about"><i class="sb-nav-link-icon fas fa-info-circle fa-fw mr-2"></i><span class="nav-label"><?php echo _("About RaspAP"); ?></a>
</div>
<?php
use RaspAP\Plugins\PluginManager;
$pluginManager = PluginManager::getInstance();
// Display logo and status LEDs
renderStatus($hostapd_led, $hostapd_status,
$memused_led, $memused,
$cputemp_led, $cputemp);
// Render sidebar via the PluginManager
$sidebar = $pluginManager->getSidebar();
$sidebar->render();

View file

@ -1,50 +0,0 @@
<?php
/**
* Manage Tor Proxy configuration
*/
function DisplayTorProxyConfig()
{
exec('cat '. RASPI_TORPROXY_CONFIG, $return);
exec('pidof tor | wc -l', $torproxystatus);
$arrConfig = array();
foreach ($return as $a) {
if ($a[0] != "#") {
$arrLine = explode(" ", $a);
$arrConfig[$arrLine[0]]=$arrLine[1];
}
}
echo renderTemplate(
"torproxy", compact(
"status",
"torproxystatus"
)
);
}
/**
*
*
*/
function SaveTORAndVPNConfig()
{
if (isset($_POST['SaveTORProxySettings'])) {
// TODO
} elseif (isset($_POST['StartTOR'])) {
echo "Attempting to start TOR";
exec('sudo systemctl start tor.service', $return);
foreach ($return as $line) {
$status->addMessage($line, 'info');
}
} elseif (isset($_POST['StopTOR'])) {
echo "Attempting to stop TOR";
exec('sudo systemctl stop tor.service', $return);
foreach ($return as $line) {
$status->addMessage($line, 'info');
}
}
}
?>

View file

@ -32,6 +32,8 @@ require_once 'includes/autoload.php';
require_once 'includes/defaults.php';
require_once 'includes/locale.php';
require_once 'includes/functions.php';
// Default page actions
require_once 'includes/dashboard.php';
require_once 'includes/authenticate.php';
require_once 'includes/admin.php';
@ -48,7 +50,6 @@ require_once 'includes/openvpn.php';
require_once 'includes/wireguard.php';
require_once 'includes/provider.php';
require_once 'includes/restapi.php';
require_once 'includes/torproxy.php';
initializeApp();
?>

View file

@ -0,0 +1,34 @@
<?php
/**
* Plugin Interface
*
* @description Basic plugin interface for RaspAP
* @author Bill Zimmerman <billzimmerman@gmail.com>
* @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
* @see
*/
declare(strict_types=1);
namespace RaspAP\Plugins;
use RaspAP\UI\Sidebar;
interface PluginInterface
{
/**
* Initialize the plugin
* @param Sidebar $sidebar Sidebar instance for adding items
*/
public function initialize(Sidebar $sidebar): void;
/**
* Render a template within the plugin's template directory
* @param string $templateName
* @param array $__data
* @return string
*/
public function renderTemplate(string $templateName, array $__data = []): string;
}

View file

@ -0,0 +1,117 @@
<?php
/**
* Plugin Manager class
*
* @description Class supporting user plugins to extend RaspAP
* @author Bill Zimmerman <billzimmerman@gmail.com>
* Special thanks to GitHub user @assachs
* @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
*/
declare(strict_types=1);
namespace RaspAP\Plugins;
use RaspAP\UI\Sidebar;
class PluginManager
{
private static $instance = null;
private $plugins = [];
private $sidebar;
private function __construct()
{
$this->pluginPath = 'plugins';
$this->sidebar = new Sidebar();
$this->autoloadPlugins(); // autoload plugins on instantiation
}
// Get the single instance of PluginManager
public static function getInstance(): PluginManager
{
if (self::$instance === null) {
self::$instance = new PluginManager();
}
return self::$instance;
}
// Autoload plugins found in pluginPath
private function autoloadPlugins(): void
{
if (!is_dir($this->pluginPath)) {
return;
}
$directories = array_filter(glob($this->pluginPath . '/*'), 'is_dir');
foreach ($directories as $dir) {
$pluginName = basename($dir);
$pluginFile = "$dir/$pluginName.php";
$pluginClass = "RaspAP\\Plugins\\$pluginName\\$pluginName"; // fully qualified class name
if (file_exists($pluginFile)) {
require_once $pluginFile;
if (class_exists($pluginClass)) {
$plugin = new $pluginClass($this->pluginPath, $pluginName);
$this->registerPlugin($plugin);
}
}
}
}
// Registers a plugin by its interface implementation
private function registerPlugin(PluginInterface $plugin)
{
$plugin->initialize($this->sidebar); // pass sidebar to initialize method
$this->plugins[] = $plugin; // store the plugin instance
}
// Returns the sidebar
public function getSidebar(): Sidebar
{
return $this->sidebar;
}
/**
* Iterates over registered plugins and calls its associated method
* @param string $page
*/
public function handlePageAction(string $page): bool
{
foreach ($this->getInstalledPlugins() as $pluginClass) {
$plugin = new $pluginClass($this->pluginPath, $pluginClass);
if ($plugin instanceof PluginInterface) {
// Check if this plugin can handle the page action
if ($plugin->handlePageAction($page)) {
return true;
}
}
}
return false; // no plugins handled the page
}
// Returns all installed plugins with full class names
public function getInstalledPlugins(): array
{
$plugins = [];
if (file_exists($this->pluginPath)) {
$directories = scandir($this->pluginPath);
foreach ($directories as $directory) {
if ($directory === "." || $directory === "..") continue;
$pluginClass = "RaspAP\\Plugins\\$directory\\$directory";
$pluginFile = $this->pluginPath . "/$directory/$directory.php";
// Check if the file and class exist
if (file_exists($pluginFile) && class_exists($pluginClass)) {
$plugins[] = $pluginClass;
}
}
}
return $plugins;
}
}

98
src/RaspAP/UI/Sidebar.php Normal file
View file

@ -0,0 +1,98 @@
<?php
/**
* Sidebar UI class
*
* @description A sidebar class for the RaspAP UI, extendable by user plugins
* @author Bill Zimmerman <billzimmerman@gmail.com>
* @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
*/
namespace RaspAP\UI;
class Sidebar {
private $items = [];
public function __construct() {
// Load default sidebar items
$this->addItem(_('Dashboard'), 'fa-solid fa-gauge-high', 'wlan0_info', 10);
$this->addItem(_('Hotspot'), 'far fa-dot-circle', 'hostapd_conf', 20,
fn() => RASPI_HOTSPOT_ENABLED
);
$this->addItem(_('DHCP Server'), 'fas fa-exchange-alt', 'dhcpd_conf', 30,
fn() => RASPI_DHCP_ENABLED && !$_SESSION["bridgedEnabled"]
);
$this->addItem(_('Ad Blocking'), 'far fa-hand-paper', 'adblock_conf', 40,
fn() => RASPI_ADBLOCK_ENABLED && RASPI_HOTSPOT_ENABLED && !$_SESSION["bridgedEnabled"]
);
$this->addItem(_('Networking'), 'fas fa-network-wired', 'network_conf', 50,
fn() => RASPI_NETWORK_ENABLED
);
$this->addItem(_('WiFi client'), 'fas fa-wifi', 'wpa_conf', 60,
fn() => RASPI_WIFICLIENT_ENABLED && !$_SESSION["bridgedEnabled"]
);
$this->addItem(_('OpenVPN'), 'fas fa-key', 'openvpn_conf', 70,
fn() => RASPI_OPENVPN_ENABLED
);
$this->addItem(_('WireGuard'), 'ra-wireguard', 'wg_conf', 80,
fn() => RASPI_WIREGUARD_ENABLED
);
$this->addItem(_(getProviderValue($_SESSION["providerID"], "name")), 'fas fa-shield-alt', 'provider_conf', 90,
fn() => RASPI_VPN_PROVIDER_ENABLED
);
$this->addItem(_('Authentication'), 'fas fa-user-lock', 'auth_conf', 100,
fn() => RASPI_CONFAUTH_ENABLED
);
$this->addItem(_('Data usage'), 'fas fa-chart-area', 'data_use', 110,
fn() => RASPI_VNSTAT_ENABLED
);
$this->addItem(_('RestAPI'), 'fas fa-puzzle-piece', 'restapi_conf', 120,
fn() => RASPI_RESTAPI_ENABLED
);
$this->addItem(_('System'), 'fas fa-cube', 'system_info', 130,
fn() => RASPI_SYSTEM_ENABLED
);
$this->addItem(_('About RaspAP'), 'fas fa-info-circle', 'about', 140);
}
/**
* Adds an item to the sidebar
* @param string $label
* @param string $iconClass
* @param string $link
* @param int $priority
* @param callable $condition
*/
public function addItem(string $label, string $iconClass, string $link, int $priority, callable $condition = null) {
$this->items[] = [
'label' => $label,
'icon' => $iconClass,
'link' => $link,
'priority' => $priority,
'condition' => $condition
];
}
public function getItems(): array {
// Sort items by priority and filter by enabled condition
$filteredItems = array_filter($this->items, function ($item) {
return $item['condition'] === null || call_user_func($item['condition']);
});
usort($filteredItems, function ($a, $b) {
return $a['priority'] <=> $b['priority'];
});
return $filteredItems;
}
public function render() {
foreach ($this->getItems() as $item) {
echo "<div class=\"sb-nav-link-icon px-2\">
<a class=\"nav-link\" href=\"{$item['link']}\">
<i class=\"sb-nav-link-icon {$item['icon']} fa-fw mr-2\"></i>
<span class=\"nav-label\">{$item['label']}</span>
</a>
</div>";
}
}
}