mirror of
https://github.com/billz/raspap-webgui.git
synced 2025-07-15 07:17:40 +02:00
Merge branch 'master' into feat/dashboard-redesign
This commit is contained in:
commit
ba4507bd22
13
.github/ISSUE_TEMPLATE/issue_form.yml
vendored
13
.github/ISSUE_TEMPLATE/issue_form.yml
vendored
@ -42,7 +42,7 @@ body:
|
||||
required: true
|
||||
- label: I am using an [external wireless adapter](https://docs.raspap.com/issues/#external-hardware).
|
||||
required: true
|
||||
- label: I have generated a [RaspAP debug log](https://docs.raspap.com/ap-basics/#debug-log) and performed a [self-diagnosis](https://docs.raspap.com/ap-basics/#diagnosing-problems).
|
||||
- label: I have generated a [RaspAP debug log](https://docs.raspap.com/troubleshooting/#debug-log) and performed a [self-diagnosis](https://docs.raspap.com/troubleshooting/#diagnosing-problems).
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
@ -56,15 +56,15 @@ body:
|
||||
- Raspberry Pi OS (64-bit) Lite Bullseye
|
||||
- Raspberry Pi OS (32-bit) Lite Bullseye
|
||||
- Armbian 23.05 (Suni)
|
||||
- Debian Bookworm
|
||||
- Ubuntu Server 23.04 (Lunar)
|
||||
- Debian Bookworm
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install
|
||||
attributes:
|
||||
label: Quick install or Manual setup?
|
||||
label: Installation method
|
||||
options:
|
||||
- Pre-built image
|
||||
- Quick install
|
||||
- Manual setup
|
||||
validations:
|
||||
@ -99,8 +99,8 @@ body:
|
||||
attributes:
|
||||
label: RaspAP version
|
||||
options:
|
||||
- 3.2.5 (Latest)
|
||||
- Other
|
||||
- Latest
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@ -113,7 +113,6 @@ body:
|
||||
- Not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
|
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
@ -29,7 +29,23 @@ jobs:
|
||||
cat > stage-raspap/package-raspap/00-run-chroot.sh <<-EOF
|
||||
#!/bin/bash
|
||||
apt-get update -y && apt-get install -y curl dhcpcd5 iptables procps
|
||||
curl -sL https://install.raspap.com | bash -s -- --yes --openvpn 1 --restapi 1 --adblock 1 --wireguard 1 --tcp-bbr 1
|
||||
curl -sL https://install.raspap.com | bash -s -- --yes --openvpn 1 --restapi 1 --adblock 1 --wireguard 1 --tcp-bbr 1 --check 0
|
||||
|
||||
# Set Wi-Fi country to prevent RF kill
|
||||
raspi-config nonint do_wifi_country "US"
|
||||
|
||||
# Fetch RaspAP version and set MOTD
|
||||
RASPAP_VERSION=\$(curl -sL https://install.raspap.com | bash -s -- --version)
|
||||
echo "\$RASPAP_VERSION" | tee /etc/motd
|
||||
EOF
|
||||
} &&
|
||||
chmod +x stage-raspap/package-raspap/00-run-chroot.sh &&
|
||||
{
|
||||
cat > stage-raspap/prerun.sh <<-EOF
|
||||
#!/bin/bash -e
|
||||
if [ ! -d "\${ROOTFS_DIR}" ]; then
|
||||
copy_previous
|
||||
fi
|
||||
EOF
|
||||
} &&
|
||||
chmod +x stage-raspap/package-raspap/00-run-chroot.sh &&
|
||||
@ -47,7 +63,7 @@ jobs:
|
||||
id: build
|
||||
uses: usimd/pi-gen-action@v1
|
||||
with:
|
||||
image-name: "raspap-${{ github.ref_name }}-${{ matrix.arch }}"
|
||||
image-name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}"
|
||||
enable-ssh: 1
|
||||
stage-list: stage0 stage1 stage2 ./stage-raspap
|
||||
verbose-output: true
|
||||
@ -57,8 +73,8 @@ jobs:
|
||||
- name: Upload Artifact
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
asset_name: raspap-image-${{ github.ref_name }}-${{ matrix.arch }}.zip
|
||||
asset_name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}.img.zip"
|
||||
file: ${{ steps.build.outputs.image-path }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
tag: ${{ github.event.inputs.tag || github.ref }}
|
||||
overwrite: true
|
||||
|
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
|
||||
|
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
[submodule "plugins"]
|
||||
path = plugins
|
||||
url = https://github.com/RaspAP/plugins
|
||||
branch = master
|
@ -1,4 +1,4 @@
|
||||
<img width="465" alt="Insiders logo" src="https://user-images.githubusercontent.com/229399/115766971-e19e1900-a3a8-11eb-8c6f-379deb4313d2.png">
|
||||
<img width="465" alt="Insiders logo" src="https://i.imgur.com/62TMUy5.png">
|
||||
|
||||
Development of RaspAP is made possible thanks to a sponsorware release model. This means that new features are first exclusively released to sponsors as part of **Insiders**. Read on to learn how sponsorship works, and how easy it is to get access to Insiders.
|
||||
|
||||
|
62
README.md
62
README.md
@ -1,10 +1,9 @@
|
||||

|
||||
[](https://github.com/raspap/raspap-webgui/releases) [](https://github.com/thibmaek/awesome-raspberry-pi) [](https://github.com/sponsors/RaspAP) [](https://app.travis-ci.com/RaspAP/raspap-webgui) [](https://crowdin.com/project/raspap) [](https://twitter.com/rasp_ap) [](https://reddit.com/r/RaspAP) [](https://discord.gg/KVAsaAR)
|
||||

|
||||
[](https://github.com/raspap/raspap-webgui/releases) [](https://github.com/thibmaek/awesome-raspberry-pi) [](https://github.com/sponsors/RaspAP) [](https://app.travis-ci.com/RaspAP/raspap-webgui) [](https://crowdin.com/project/raspap) [](https://twitter.com/rasp_ap) [](https://reddit.com/r/RaspAP) [](https://discord.gg/KVAsaAR)
|
||||
|
||||
RaspAP is feature-rich wireless router software that _just works_ on many popular [Debian-based devices](#supported-operating-systems), including the Raspberry Pi. Our [custom OS images](#pre-built-image), [Quick installer](#quick-installer) and [Docker container](#docker-support) create a known-good default configuration for all current Raspberry Pis with onboard wireless. A fully responsive, mobile-ready interface gives you control over the relevant services and networking options. Advanced DHCP settings, WireGuard and OpenVPN support, [SSL certificates](https://docs.raspap.com/ssl/), [ad blocking](#ad-blocking), security audits, [captive portal integration](https://docs.raspap.com/captive/), themes and [multilingual options](https://docs.raspap.com/translations/) are included.
|
||||
|
||||
RaspAP is feature-rich wireless router software that _just works_ on many popular [Debian-based devices](#supported-operating-systems), including the Raspberry Pi. Our popular [Quick installer](#quick-installer) and [Docker container](#docker-support) create a known-good default configuration for all current Raspberry Pis with onboard wireless. A fully responsive, mobile-ready interface gives you control over the relevant services and networking options. Advanced DHCP settings, WireGuard and OpenVPN support, [SSL certificates](https://docs.raspap.com/ssl/), security audits, [captive portal integration](https://docs.raspap.com/captive/), themes and [multilingual options](https://docs.raspap.com/translations/) are included.
|
||||
|
||||
RaspAP has been featured on sites such as [Instructables](http://www.instructables.com/id/Raspberry-Pi-As-Completely-Wireless-Router/), [Adafruit](https://blog.adafruit.com/2016/06/24/raspap-wifi-configuration-portal-piday-raspberrypi-raspberry_pi/), [Raspberry Pi Weekly](https://www.raspberrypi.org/weekly/commander/) and [Awesome Raspberry Pi](https://project-awesome.org/thibmaek/awesome-raspberry-pi) and implemented in countless projects.
|
||||
RaspAP has been featured by [PC World](https://www.pcwelt.de/article/1789512/raspberry-pi-als-wlan-router.html), [Adafruit](https://blog.adafruit.com/2016/06/24/raspap-wifi-configuration-portal-piday-raspberrypi-raspberry_pi/), [Raspberry Pi Weekly](https://www.raspberrypi.org/weekly/commander/), and [Awesome Raspberry Pi](https://project-awesome.org/thibmaek/awesome-raspberry-pi) and implemented in [countless projects](https://github.com/RaspAP/raspap-awesome#projects).
|
||||
|
||||
We hope you enjoy using RaspAP as much as we do creating it. Tell us how you use this with [your own projects](https://github.com/raspap/raspap-awesome).
|
||||
|
||||
@ -18,15 +17,13 @@ We hope you enjoy using RaspAP as much as we do creating it. Tell us how you use
|
||||
|
||||
## Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Quick installer](#quick-installer)
|
||||
- [Quick start](#quick-start)
|
||||
- [Join Insiders](#join-insiders)
|
||||
- [WireGuard support](#wireguard-support)
|
||||
- [OpenVPN support](#openvpn-support)
|
||||
- [VPN Provider support](#vpn-provider-support)
|
||||
- [Ad Blocking](#ad-blocking)
|
||||
- [Bridged AP](#bridged-ap)
|
||||
- [Simultaneous AP and Wifi client](#simultaneous-ap-and-wifi-client)
|
||||
- [Manual installation](#manual-installation)
|
||||
- [802.11ac 5GHz support](#80211ac-5ghz-support)
|
||||
- [Supported operating systems](#supported-operating-systems)
|
||||
@ -38,30 +35,43 @@ We hope you enjoy using RaspAP as much as we do creating it. Tell us how you use
|
||||
- [Reporting issues](#reporting-issues)
|
||||
- [License](#license)
|
||||
|
||||
## Prerequisites
|
||||
Start with a clean install of the [latest release of Raspberry Pi OS Lite](https://www.raspberrypi.com/software/operating-systems/). Both the 32- and 64-bit Lite versions are supported. The Raspberry Pi OS desktop distro is [unsupported](https://docs.raspap.com/faq/#distros).
|
||||
## Quick start
|
||||
RaspAP gives you two different ways to get up and running quickly. The simplest and recommended approach is to use a custom Raspberry Pi OS image with RaspAP preinstalled. This option eliminates guesswork and gives you a base upon which to build. Alternatively, you may execute the Quick installer on an existing [compatible OS](https://docs.raspap.com/#compatible-operating-systems).
|
||||
|
||||
### Pre-built image
|
||||
Custom Raspberry Pi OS Lite images with the latest RaspAP are available for [direct download](https://github.com/RaspAP/raspap-webgui/releases/latest). This includes both 32- and 64-bit builds for ARM architectures.
|
||||
|
||||
| Operating system | Debian version | Kernel version | RaspAP version | Size |
|
||||
| ---------------------| ---------------|-----------------|----------------|-------|
|
||||
| Raspberry Pi OS (64-bit) Lite | 12 (bookworm) | 6.6 | Latest | 777 MB|
|
||||
| Raspberry Pi OS (32-bit) Lite | 12 (bookworm) | 6.6 | Latest | 805 MB|
|
||||
|
||||
These images are automatically generated with each release of RaspAP. You may choose between an `arm64` or `armhf` (32-bit) based build. Refer to [this resource](https://www.raspberrypi.com/software/operating-systems/) to ensure compatibility with your hardware.
|
||||
|
||||
After downloading your desired image from the [latest release page](https://github.com/RaspAP/raspap-webgui/releases/latest), use a utility such as the Raspberry Pi Imager or [balenaEtcher](https://www.balena.io/etcher) to flash the OS image onto a microSD card. Insert the card into your device and boot it up. The latest RaspAP release version with the most popular optional components will be active and ready for you to configure.
|
||||
|
||||
### Quick installer
|
||||
Alternatively, start with a clean install of a [latest release of Raspberry Pi OS](https://www.raspberrypi.org/software/operating-systems/). Both the 32- and 64-bit release versions are supported, as well as the latest 64-bit Desktop distribution.
|
||||
|
||||
Update RPi OS to its latest version, including the kernel and firmware, followed by a reboot:
|
||||
|
||||
1. Update Raspbian, including the kernel and firmware, followed by a reboot:
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get full-upgrade
|
||||
sudo reboot
|
||||
```
|
||||
2. Set the "WLAN country" option in `raspi-config`'s **Localisation Options**: `sudo raspi-config`
|
||||
Set the WiFi country in raspi-config's **Localisation Options**: `sudo raspi-config`.
|
||||
|
||||
3. If you have a device without an onboard wireless chipset, the [**Edimax Wireless 802.11b/g/n nano USB adapter**](https://www.edimax.com/edimax/merchandise/merchandise_detail/data/edimax/global/wireless_adapters_n150/ew-7811un) is an excellent option – it's small, cheap and has good driver support.
|
||||
|
||||
With the prerequisites done, you can proceed with either the Quick installer or Manual installation steps below.
|
||||
|
||||
## Quick installer
|
||||
Install RaspAP from your device's shell prompt:
|
||||
```sh
|
||||
curl -sL https://install.raspap.com | bash
|
||||
```
|
||||
The [installer](https://docs.raspap.com/quick/) will complete the steps in the manual installation (below) for you.
|
||||
|
||||
After the reboot at the end of the installation the wireless network will be
|
||||
configured as an access point as follows:
|
||||
The Quick installer will respond to several [command line arguments](https://docs.raspap.com/quick/), or switches, to customize your installation in a variety of ways, or install one of RaspAP's optional helper tools.
|
||||
|
||||
### Initial settings
|
||||
After completing either of these setup options, the wireless AP network will be configured as follows:
|
||||
|
||||
* IP address: 10.3.141.1
|
||||
* Username: admin
|
||||
* Password: secret
|
||||
@ -69,7 +79,7 @@ configured as an access point as follows:
|
||||
* SSID: `raspi-webgui`
|
||||
* Password: ChangeMe
|
||||
|
||||
**Note:** As the name suggests, the Quick Installer is a great way to quickly setup a new AP. However, it does not automagically detect the unique configuration of your system. Best results are obtained by connecting to ethernet (`eth0`) or as a WiFi client, also known as managed mode, with `wlan0`. For the latter, refer to [this FAQ](https://docs.raspap.com/faq/#headless). Special instructions for the Pi Zero W are [available here](https://docs.raspap.com/ap-sta/).
|
||||
It's _strongly recommended_ that your first post-install action is to change the default admin [authentication](https://docs.raspap.com/authentication/) settings. Thereafter, your AP's [basic settings](https://docs.raspap.com/ap-basics/) and many [advanced options](https://docs.raspap.com/ap-basics#advanced-options) are now ready to be modified by RaspAP.
|
||||
|
||||
Please [read this](https://docs.raspap.com/issues/) before reporting an issue.
|
||||
|
||||
@ -118,11 +128,6 @@ By default RaspAP configures a routed AP for your clients to connect to. A bridg
|
||||
|
||||
More information on Bridged AP mode is provided [in our documentation](https://docs.raspap.com/bridged/).
|
||||
|
||||
## Simultaneous AP and Wifi client
|
||||
RaspAP lets you create an AP with a Wifi client configuration, often called [AP-STA mode](https://docs.raspap.com/ap-sta/). With your system configured in managed mode, enable the AP from the **Advanced** tab of **Configure hotspot** by sliding the **Wifi client AP mode** toggle. Save settings and start the hotspot. The managed mode AP is functional without restart.
|
||||
|
||||
**Note:** This option is disabled until you configure your system as a wireless client. For a device operating in [managed mode](https://docs.raspap.com/faq/#headless) without an `eth0` connection, this configuration must be enabled [_before_ a reboot](https://docs.raspap.com/ap-sta/).
|
||||
|
||||
## Manual installation
|
||||
Detailed manual setup instructions are provided [on our documentation site](https://docs.raspap.com/manual/).
|
||||
|
||||
@ -139,11 +144,10 @@ RaspAP was originally made for Raspbian, but now also installs on the following
|
||||
| Raspberry Pi OS | (64-bit) Desktop Bookworm | ARM | Official |
|
||||
| Raspberry Pi OS | (64-bit) Lite Bullseye | ARM | Official |
|
||||
| Raspberry Pi OS | (32-bit) Lite Bullseye | ARM | Official |
|
||||
| Armbian | 23.11 (Jammy) | [ARM](https://docs.armbian.com/#supported-socs) | Official |
|
||||
| Armbian | 23.11 (Jammy) | [ARM](https://docs.armbian.com/#supported-socs) | Beta |
|
||||
| Debian | Bookworm | ARM / x86_64 | Beta |
|
||||
| Ubuntu | Server 23.04 (Lunar) | ARM / x86_64 | Beta |
|
||||
|
||||
<img src="https://github.com/RaspAP/raspap-webgui/assets/229399/6fe62f2d-631a-46c9-8ceb-83ebf0ade6a9" style="width:640px;" />
|
||||
<img src="https://i.imgur.com/XiAJNKb.png" style="width:480px;" />
|
||||
|
||||
You are also encouraged to use RaspAP's community-led [Docker container](#docker-support). Please note that "supported" is not a guarantee. If you are able to improve support for your preferred distro, we encourage you to [actively contribute](#how-to-contribute) to the project.
|
||||
|
||||
|
28
ajax/plugins/do_plugin_install.php
Executable file
28
ajax/plugins/do_plugin_install.php
Executable file
@ -0,0 +1,28 @@
|
||||
<?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;
|
||||
$install_path = $_POST['install_path'] ?? null;
|
||||
|
||||
if (isset($plugin_uri, $plugin_version, $install_path)) {
|
||||
try {
|
||||
$return = $pluginInstaller->installPlugin($plugin_uri, $plugin_version, $install_path);
|
||||
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, version, and install path are required']);
|
||||
exit;
|
||||
}
|
||||
|
@ -378,6 +378,18 @@ 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;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
margin: 1rem;
|
||||
}
|
||||
@ -627,5 +639,3 @@ button > i.fas {
|
||||
.device-illustration {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
|
||||
|
133
app/js/custom.js
133
app/js/custom.js
@ -468,6 +468,138 @@ $('#js-sys-reboot, #js-sys-shutdown').on('click', function (e) {
|
||||
});
|
||||
});
|
||||
|
||||
$('#install-user-plugin').on('shown.bs.modal', function (e) {
|
||||
var button = $(e.relatedTarget);
|
||||
$(this).data('button', button);
|
||||
var manifestData = button.data('plugin-manifest');
|
||||
var installed = button.data('plugin-installed') || false;
|
||||
var repoPublic = button.data('repo-public') || false;
|
||||
var installPath = manifestData.install_path;
|
||||
|
||||
if (!installed && repoPublic && installPath === 'plugins-available') {
|
||||
insidersHTML = 'Available with <i class="fas fa-heart heart me-1"></i><a href="https://docs.raspap.com/insiders" target="_blank" rel="noopener">Insiders</a>';
|
||||
$('#plugin-additional').html(insidersHTML);
|
||||
} else {
|
||||
$('#plugin-additional').empty();
|
||||
}
|
||||
if (manifestData) {
|
||||
$('#plugin-docs').html(manifestData.plugin_docs
|
||||
? `<a href="${manifestData.plugin_docs}" target="_blank">${manifestData.plugin_docs}</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 || 'None'));
|
||||
$('#plugin-dependencies').html(formatProperty(manifestData.dependencies || 'None'));
|
||||
$('#plugin-javascript').html(formatProperty(manifestData.javascript || 'None'));
|
||||
$('#plugin-sudoers').html(formatProperty(manifestData.sudoers || 'None'));
|
||||
$('#plugin-user-name').html((manifestData.user_nonprivileged && manifestData.user_nonprivileged.name) || 'None');
|
||||
}
|
||||
if (installed) {
|
||||
$('#js-install-plugin-confirm').html('OK');
|
||||
} else if (!installed && repoPublic && installPath == 'plugins-available') {
|
||||
$('#js-install-plugin-confirm').html('Get Insiders');
|
||||
} else {
|
||||
$('#js-install-plugin-confirm').html('Install now');
|
||||
}
|
||||
});
|
||||
|
||||
$('#js-install-plugin-confirm').on('click', function (e) {
|
||||
var button = $('#install-user-plugin').data('button');
|
||||
var manifestData = button.data('plugin-manifest');
|
||||
var installPath = manifestData.install_path;
|
||||
var pluginUri = manifestData.plugin_uri;
|
||||
var pluginVersion = manifestData.version;
|
||||
var pluginConfirm = $('#js-install-plugin-confirm').text();
|
||||
var progressText = $('#js-install-plugin-confirm').attr('data-message');
|
||||
var successHtml = $('#plugin-install-message').attr('data-message');
|
||||
var successText = $('<div>').text(successHtml).text();
|
||||
var csrfToken = $('meta[name=csrf_token]').attr('content');
|
||||
|
||||
if (pluginConfirm === 'Install now') {
|
||||
$("#install-user-plugin").modal('hide');
|
||||
$("#install-plugin-progress").modal('show');
|
||||
$.post(
|
||||
'ajax/plugins/do_plugin_install.php',
|
||||
{
|
||||
'plugin_uri': pluginUri,
|
||||
'plugin_version': pluginVersion,
|
||||
'install_path': installPath,
|
||||
'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");
|
||||
});
|
||||
} else if (pluginConfirm === 'Get Insiders') {
|
||||
window.open('https://docs.raspap.com/insiders/', '_blank');
|
||||
return;
|
||||
} else if (pluginConfirm === 'OK') {
|
||||
$("#install-user-plugin").modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
$('#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 +639,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');
|
||||
|
@ -7,7 +7,7 @@ if (!defined('RASPI_CONFIG')) {
|
||||
$defaults = [
|
||||
'RASPI_BRAND_TEXT' => 'RaspAP',
|
||||
'RASPI_BRAND_TITLE' => RASPI_BRAND_TEXT.' Admin Panel',
|
||||
'RASPI_VERSION' => '3.2.5',
|
||||
'RASPI_VERSION' => '3.2.9',
|
||||
'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json',
|
||||
'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json',
|
||||
'RASPI_CONFIG_API' => RASPI_CONFIG.'/api',
|
||||
@ -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',
|
||||
|
@ -177,15 +177,17 @@ function getDefaultNetOpts($svc,$key)
|
||||
* @param string $key
|
||||
* @return object $json
|
||||
*/
|
||||
function getProviderValue($id,$key)
|
||||
function getProviderValue($id, $key)
|
||||
{
|
||||
$obj = json_decode(file_get_contents(RASPI_CONFIG_PROVIDERS), true);
|
||||
if ($obj === null) {
|
||||
if (!isset($obj['providers']) || !is_array($obj['providers'])) {
|
||||
return false;
|
||||
} else {
|
||||
$id--;
|
||||
return $obj['providers'][$id][$key];
|
||||
}
|
||||
$id--;
|
||||
if (!isset($obj['providers'][$id]) || !is_array($obj['providers'][$id])) {
|
||||
return false;
|
||||
}
|
||||
return $obj['providers'][$id][$key] ?? false;
|
||||
}
|
||||
|
||||
/* Functions to write ini files */
|
||||
@ -629,6 +631,13 @@ function mb_escapeshellarg($arg)
|
||||
}
|
||||
}
|
||||
|
||||
function safeOutputValue($def, $arr)
|
||||
{
|
||||
if (array_key_exists($def, $arr)) {
|
||||
echo htmlspecialchars($arr[$def], ENT_QUOTES);
|
||||
}
|
||||
}
|
||||
|
||||
function dnsServers()
|
||||
{
|
||||
$data = json_decode(file_get_contents("./config/dns-servers.json"));
|
||||
@ -1040,3 +1049,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
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
* @author Lawrence Yau <sirlagz@gmail.com>
|
||||
* @author Bill Zimmerman <billzimmerman@gmail.com>
|
||||
* @license GNU General Public License, version 3 (GPL-3.0)
|
||||
* @version 3.2.5
|
||||
* @version 3.2.9
|
||||
* @link https://github.com/RaspAP/raspap-webgui/
|
||||
* @link https://raspap.com/
|
||||
* @see http://sirlagz.net/2013/02/08/raspap-webgui/
|
||||
|
@ -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
|
||||
@ -74,6 +75,7 @@ function _update_raspap() {
|
||||
_download_latest_files
|
||||
_change_file_ownership
|
||||
_patch_system_files
|
||||
_create_plugin_scripts
|
||||
_install_complete
|
||||
}
|
||||
|
||||
@ -313,6 +315,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 +599,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"
|
||||
|
137
installers/plugin_helper.sh
Executable file
137
installers/plugin_helper.sh
Executable file
@ -0,0 +1,137 @@
|
||||
#!/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"
|
||||
chown -R $raspap_user:$raspap_user "$destination"
|
||||
|
||||
echo "OK"
|
||||
;;
|
||||
|
||||
"javascript")
|
||||
[ $# -lt 2 ] && { echo "Usage: $0 javascript <source> <destination>"; exit 1; }
|
||||
|
||||
source=$1
|
||||
destination=$2
|
||||
|
||||
if [ ! -f "$source" ]; then
|
||||
echo "Source file $source does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$destination" ]; then
|
||||
mkdir -p "$destination"
|
||||
fi
|
||||
|
||||
cp "$source" "$destination"
|
||||
chown -R $raspap_user:$raspap_user "$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 "$plugin_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 " javascript <source> <destination> Applies a JavaScript 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
|
||||
|
@ -54,6 +54,7 @@ OPTIONS:
|
||||
-p, --path <path> Used with -d, --update, sets the existing install path
|
||||
-i, --insiders Installs from the Insiders Edition (RaspAP/raspap-insiders)
|
||||
-m, --minwrite Configures a microSD card for minimum write operation
|
||||
-k, --check <flag> Sets the connectivity check flag (default is 1=perform check)
|
||||
-v, --version Outputs release info and exits
|
||||
-n, --uninstall Loads and executes the uninstaller
|
||||
-h, --help Outputs usage notes and exits
|
||||
@ -81,13 +82,16 @@ EOF
|
||||
set -eo pipefail
|
||||
|
||||
function _main() {
|
||||
_setup_colors
|
||||
_check_internet
|
||||
|
||||
# set defaults
|
||||
repo="RaspAP/raspap-webgui" # override with -r, --repo option
|
||||
repo_common="$repo"
|
||||
|
||||
_parse_params "$@"
|
||||
_setup_colors
|
||||
if [ "${check}" == 1 ]; then
|
||||
_check_internet
|
||||
fi
|
||||
|
||||
_log_output
|
||||
_load_installer
|
||||
}
|
||||
@ -105,6 +109,7 @@ function _parse_params() {
|
||||
minwrite=0
|
||||
acctoken=""
|
||||
path=""
|
||||
check=1
|
||||
|
||||
while :; do
|
||||
case "${1-}" in
|
||||
@ -175,6 +180,10 @@ function _parse_params() {
|
||||
path="$2"
|
||||
shift
|
||||
;;
|
||||
-k|--check)
|
||||
check="$2"
|
||||
shift
|
||||
;;
|
||||
-v|--version)
|
||||
_version
|
||||
;;
|
||||
|
@ -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 38331709b6c8198c0dbf1c7c85cb52b8ae3ea79a
|
@ -47,7 +47,7 @@ class IwParser
|
||||
"(no IR)"
|
||||
];
|
||||
$excluded_pattern = implode('|', array_map('preg_quote', $excluded));
|
||||
$pattern = '/\*\s+(\d+)\s+MHz \[(\d+)\] \(([\d.]+) dBm\)\s(?!' .$excluded_pattern. ')/';
|
||||
$pattern = '/\*\s+([\d.]+)\s+MHz \[(\d+)\] \(([\d.]+) dBm\)\s(?!' .$excluded_pattern. ')/';
|
||||
$supportedFrequencies = [];
|
||||
|
||||
// Match iw_output containing supported frequencies
|
||||
|
512
src/RaspAP/Plugins/PluginInstaller.php
Normal file
512
src/RaspAP/Plugins/PluginInstaller.php
Normal file
@ -0,0 +1,512 @@
|
||||
<?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;
|
||||
private $repoPublic;
|
||||
|
||||
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';
|
||||
$this->repoPublic = $this->getRepository();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
$pluginEntries = [];
|
||||
|
||||
foreach ($pluginManifest as $plugin) {
|
||||
$installed = false;
|
||||
|
||||
if (!empty($plugin['namespace'])) {
|
||||
foreach ($installedPlugins as $installedPlugin) {
|
||||
if (str_contains($installedPlugin['class'], $plugin['namespace'])) {
|
||||
$installed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$pluginEntries[] = [
|
||||
'manifest' => $plugin,
|
||||
'installed' => $installed
|
||||
];
|
||||
}
|
||||
$plugins[] = $pluginEntries;
|
||||
}
|
||||
return array_merge(...$plugins);
|
||||
} catch (\Exception $e) {
|
||||
error_log("An error occurred: " . $e->getMessage());
|
||||
throw $e; // re-throw to global ExceptionHandler
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of installed plugins in pluginPath
|
||||
*
|
||||
* @param string|null $path; optional path to search for plugins. Defaults to $this->pluginPath.
|
||||
* @return array $plugins
|
||||
*/
|
||||
public function getPlugins(?string $path = null): array
|
||||
{
|
||||
$plugins = [];
|
||||
$pluginPath = $path ?? $this->pluginPath;
|
||||
|
||||
if (file_exists($pluginPath)) {
|
||||
$directories = scandir($pluginPath);
|
||||
|
||||
foreach ($directories as $directory) {
|
||||
if ($directory === '.' || $directory === '..') {
|
||||
continue;
|
||||
}
|
||||
$pluginClass = "RaspAP\\Plugins\\$directory\\$directory";
|
||||
$pluginFile = "$pluginPath/$directory/$directory.php";
|
||||
|
||||
if (file_exists($pluginFile)) {
|
||||
if ($path === 'plugins-available') {
|
||||
require_once $pluginFile;
|
||||
}
|
||||
if (class_exists($pluginClass)) {
|
||||
$plugins[] = [
|
||||
'class' => $pluginClass,
|
||||
'installPath' => $pluginPath
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a plugin by either extracting an archive or creating a symlink,
|
||||
* then performs required actions as defined in the plugin manifest
|
||||
*
|
||||
* @param string $pluginUri
|
||||
* @param string $pluginVersion
|
||||
* @param string $installPath
|
||||
* @return boolean
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function installPlugin(string $pluginUri, string $pluginVersion, string $installPath): bool
|
||||
{
|
||||
$tempFile = null;
|
||||
$extractDir = null;
|
||||
$pluginDir = null;
|
||||
|
||||
try {
|
||||
if ($installPath === 'plugins-available') {
|
||||
// extract plugin name from URI
|
||||
$pluginName = basename($pluginUri);
|
||||
$sourcePath = $this->rootPath . '/plugins-available/' . $pluginName;
|
||||
$targetPath = $this->rootPath . '/plugins/' . $pluginName;
|
||||
|
||||
if (!is_dir($sourcePath)) {
|
||||
throw new \Exception("Plugin '$pluginName' not found in plugins-available");
|
||||
}
|
||||
|
||||
// ensure target does not already exist
|
||||
if (file_exists($targetPath)) {
|
||||
throw new \Exception("Plugin '$pluginName' is already installed.");
|
||||
}
|
||||
|
||||
// create symlink
|
||||
if (!symlink($sourcePath, $targetPath)) {
|
||||
throw new \Exception("Failed to symlink '$pluginName' to plugins/");
|
||||
}
|
||||
$pluginDir = $targetPath;
|
||||
} else {
|
||||
// fetch and extract the plugin archive
|
||||
$archiveUrl = rtrim($pluginUri, '/') . '/archive/refs/tags/' .$pluginVersion.'.zip';
|
||||
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';
|
||||
}
|
||||
if (!empty($manifest['javascript'])) {
|
||||
$this->copyJavaScriptFiles($manifest['javascript'], $pluginDir);
|
||||
$rollbackStack[] = 'removeJavaScript';
|
||||
}
|
||||
if ($installPath === 'plugins') {
|
||||
$this->copyPluginFiles($pluginDir, $this->rootPath);
|
||||
$rollbackStack[] = 'removePluginFiles';
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception('Installation step failed: ' . $e->getMessage());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log('Plugin installation failed: ' . $e->getMessage());
|
||||
throw new \Exception($e->getMessage());
|
||||
} finally {
|
||||
if (isset($tempFile) && file_exists($tempFile)) {
|
||||
unlink($tempFile);
|
||||
}
|
||||
if (isset($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 = $config['destination'];
|
||||
|
||||
if (!str_starts_with($destination, '/')) {
|
||||
$destination = $this->rootPath . '/' . ltrim($destination, '/');
|
||||
}
|
||||
$destination = escapeshellarg($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 plugin JavaScript files to their destination
|
||||
*
|
||||
* @param array $javascript
|
||||
* @param string $pluginDir
|
||||
*/
|
||||
private function copyJavaScriptFiles(array $javascript, string $pluginDir): void
|
||||
{
|
||||
foreach ($javascript as $js) {
|
||||
$source = escapeshellarg($pluginDir . DIRECTORY_SEPARATOR . $js);
|
||||
$destination = escapeshellarg($this->rootPath . DIRECTORY_SEPARATOR . 'app/js/plugins/');
|
||||
$cmd = sprintf('sudo /etc/raspap/plugins/plugin_helper.sh javascript %s %s', $source, $destination);
|
||||
$return = shell_exec($cmd);
|
||||
if (strpos(strtolower($return), 'ok') === false) {
|
||||
throw new \Exception("Failed to copy JavaScript file: $source");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] ?? [];
|
||||
$installed = $plugin['installed'] ?? false;
|
||||
$manifest = htmlspecialchars(json_encode($manifestData), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
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. '" data-repo-public="' .$this->repoPublic. '">' . _("Details") .'</button>';
|
||||
}
|
||||
|
||||
$icon = htmlspecialchars($manifestData['icon'] ?? '');
|
||||
$pluginDocs = htmlspecialchars($manifestData['plugin_docs'] ?? '');
|
||||
$nameText = htmlspecialchars($manifestData['name'] ?? 'Unknown Plugin');
|
||||
$name = '<i class="' .$icon. ' link-secondary me-2"></i><a href="'
|
||||
.$pluginDocs
|
||||
.'" 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines remote repository of installed application
|
||||
*
|
||||
* @return boolean; true if public repo
|
||||
*/
|
||||
public function getRepository(): bool
|
||||
{
|
||||
$output = [];
|
||||
exec('git -C ' . escapeshellarg($this->rootPath) . ' remote -v', $output);
|
||||
|
||||
foreach ($output as $line) {
|
||||
if (preg_match('#github\.com/RaspAP/(raspap-\w+)#', $line, $matches)) {
|
||||
$repo = $matches[1];
|
||||
$public = ($repo === 'raspap-webgui');
|
||||
return $public;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -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,88 @@
|
||||
</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-1"></p>
|
||||
<p id="plugin-additional" class="mb-3"></p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><?php echo _("Plugin docs"); ?></th>
|
||||
<td><span id="plugin-docs"></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 _("JavaScript"); ?></th>
|
||||
<td><small><code><span id="plugin-javascript" 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