Services: Captive Portal - various (style) cleanups

o slightly refactor strip_template.py including our exclude.list to skip library files and internal files.
o replace htdocs_default directory references to use relative paths
o change ServiceController to implement our standard ApiMutableServiceControllerBase and add missing status call
o array() -> [] style fixes
o add jquery-3.5.1.min.js into htdocs_default, keep legacy version for existing templates
This commit is contained in:
Ad Schellevis 2025-08-13 20:54:28 +02:00
parent caaa30de30
commit d8519a06a8
11 changed files with 89 additions and 140 deletions

1
plist
View file

@ -1057,6 +1057,7 @@
/usr/local/opnsense/scripts/captiveportal/htdocs_default/index.html
/usr/local/opnsense/scripts/captiveportal/htdocs_default/js/bootstrap.min.js
/usr/local/opnsense/scripts/captiveportal/htdocs_default/js/jquery-1.11.2.min.js
/usr/local/opnsense/scripts/captiveportal/htdocs_default/js/jquery-3.5.1.min.js
/usr/local/opnsense/scripts/captiveportal/lib/__init__.py
/usr/local/opnsense/scripts/captiveportal/lib/arp.py
/usr/local/opnsense/scripts/captiveportal/lib/daemonize.py

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2015 Deciso B.V.
* Copyright (C) 2015-2025 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -28,9 +28,8 @@
namespace OPNsense\CaptivePortal\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Base\ApiMutableServiceControllerBase;
use OPNsense\Base\UIModelGrid;
use OPNsense\CaptivePortal\CaptivePortal;
use OPNsense\Core\AppConfig;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
@ -40,43 +39,20 @@ use OPNsense\Core\SanitizeFilter;
* Class ServiceController
* @package OPNsense\CaptivePortal
*/
class ServiceController extends ApiControllerBase
class ServiceController extends ApiMutableServiceControllerBase
{
/**
* reconfigure captive portal
*/
public function reconfigureAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$bckresult = trim($backend->configdRun("filter reload"));
if ($bckresult == "OK") {
// generate captive portal config
$bckresult = trim($backend->configdRun('template reload OPNsense/Captiveportal'));
if ($bckresult == "OK") {
$mdlCP = new CaptivePortal();
if ($mdlCP->isEnabled()) {
$bckresult = trim($backend->configdRun("captiveportal restart"));
if ($bckresult == "OK") {
$status = "ok";
} else {
$status = "error reloading captive portal";
}
} else {
$backend->configdRun("captiveportal stop");
$status = "ok";
}
} else {
$status = "error reloading captive portal template";
}
} else {
$status = "error reloading captive portal rules (" . $bckresult . ")";
}
protected static $internalServiceClass = '\OPNsense\CaptivePortal\CaptivePortal';
protected static $internalServiceTemplate = 'OPNsense/Captiveportal';
protected static $internalServiceName = 'captiveportal';
return array("status" => $status);
} else {
return array("status" => "failed");
}
protected function serviceEnabled()
{
return $this->getModel()->isEnabled();
}
protected function invokeFirewallReload()
{
return true;
}
/**
@ -87,15 +63,10 @@ class ServiceController extends ApiControllerBase
public function getTemplateAction($fileid = null)
{
// get template name
if ($fileid != null) {
$templateFileId = (new SanitizeFilter())->sanitize($fileid, 'alnum');
} else {
$templateFileId = 'default';
}
$templateFileId = $fileid != null ? (new SanitizeFilter())->sanitize($fileid, 'alnum') : 'default';
// request template data and output result (zipfile)
$backend = new Backend();
$response = $backend->configdpRun("captiveportal fetch_template", array($templateFileId));
$response = (new Backend())->configdpRun("captiveportal fetch_template", [$templateFileId]) ?? '';
$result = json_decode($response, true);
if ($result != null) {
$response = $result['payload'];
@ -116,55 +87,48 @@ class ServiceController extends ApiControllerBase
public function saveTemplateAction()
{
if ($this->request->isPost() && $this->request->hasPost("name")) {
Config::getInstance()->lock();
$content = $this->request->getPost("content", "striptags", "");
$templateName = $this->request->getPost("name", "striptags");
$mdlCP = new CaptivePortal();
if ($this->request->hasPost("uuid")) {
$uuid = $this->request->getPost("uuid", "striptags");
$template = $mdlCP->getNodeByReference('templates.template.' . $uuid);
$template = $this->getModel()->getNodeByReference('templates.template.' . $uuid);
if ($template == null) {
return array("name" => $templateName, "error" => "node not found");
return ["name" => $templateName, "error" => "node not found"];
}
} else {
$template = $mdlCP->getTemplateByName($templateName);
$template = $this->getModel()->getTemplateByName($templateName);
}
// cleanse input content, we only want to save changed files into our config
if (
strlen($this->request->getPost("content", "striptags", "")) > 20
|| strlen((string)$template->content) == 0
) {
if (strlen($content) > 20 || strlen((string)$template->content) == 0) {
$temp_filename = (new AppConfig())->application->tempDir;
$temp_filename .= '/cp_' . $template->getAttributes()['uuid'] . '.tmp';
file_put_contents($temp_filename, $this->request->getPost("content", "striptags", ""));
file_put_contents($temp_filename, $content);
// strip defaults and unchanged files from template (standard js libs, etc)
$backend = new Backend();
$response = $backend->configdpRun("captiveportal strip_template", array($temp_filename));
$response = (new Backend())->configdpRun("captiveportal strip_template", [$temp_filename]) ?? '';
unlink($temp_filename);
$result = json_decode($response, true);
if ($result != null && !array_key_exists('error', $result)) {
if (is_array($result) && !array_key_exists('error', $result)) {
$template->content = $result['payload'];
} else {
return array("name" => $templateName, "error" => $result['error']);
return ["name" => $templateName, "error" => $result['error']];
}
}
$template->name = $templateName;
$valMsgs = $mdlCP->performValidation();
$errorMsg = "";
foreach ($valMsgs as $field => $msg) {
if ($errorMsg != "") {
$errorMsg .= " , ";
}
$errorMsg .= $msg->getMessage();
$errorMsg = [];
foreach ($this->getModel()->performValidation() as $validation_message) {
$errorMsg[] = (string)$validation_message;
}
if ($errorMsg != "") {
return array("name" => (string)$template->name, "error" => $errorMsg);
if (!empty($errorMsg)) {
return ["name" => (string)$template->name, "error" => implode("\n", $errorMsg)];
} else {
// data is valid, save and return.
$mdlCP->serializeToConfig();
$this->getModel()->serializeToConfig();
Config::getInstance()->save();
return array("name" => (string)$template->name);
return ["name" => (string)$template->name];
}
}
return null;
@ -177,13 +141,13 @@ class ServiceController extends ApiControllerBase
*/
public function delTemplateAction($uuid)
{
$result = array("result" => "failed");
$result = ["result" => "failed"];
if ($this->request->isPost()) {
$mdlCP = new CaptivePortal();
Config::getInstance()->lock();
if ($uuid != null) {
if ($mdlCP->templates->template->del($uuid)) {
if ($this->getModel()->templates->template->del($uuid)) {
// if item is removed, serialize to config and save
$mdlCP->serializeToConfig();
$this->getModel()->serializeToConfig();
Config::getInstance()->save();
$result['result'] = 'deleted';
} else {
@ -201,12 +165,7 @@ class ServiceController extends ApiControllerBase
*/
public function searchTemplatesAction()
{
$mdlCP = new CaptivePortal();
$grid = new UIModelGrid($mdlCP->templates->template);
return $grid->fetchBindRequest(
$this->request,
array("name", "fileid"),
"name"
);
$grid = new UIModelGrid($this->getModel()->templates->template);
return $grid->fetchBindRequest($this->request, ["name", "fileid"], "name");
}
}

View file

@ -1,7 +1,7 @@
<?php
/**
* Copyright (C) 2015-2024 Deciso B.V.
* Copyright (C) 2015-2025 Deciso B.V.
*
* All rights reserved.
*
@ -47,10 +47,8 @@ class SessionController extends ApiControllerBase
*/
public function listAction($zoneid = 0)
{
$mdlCP = new CaptivePortal();
$cpZone = $mdlCP->getByZoneID($zoneid);
if ($cpZone != null) {
$allClientsRaw = (new Backend())->configdpRun("captiveportal list_clients", [$cpZone->zoneid]);
if ((new CaptivePortal())->getByZoneID($zoneid) != null) {
$allClientsRaw = (new Backend())->configdpRun("captiveportal list_clients", [$zoneid]);
return json_decode($allClientsRaw ?? '', true);
} else {
// illegal zone, return empty response
@ -80,8 +78,7 @@ class SessionController extends ApiControllerBase
public function zonesAction()
{
$response = [];
$mdlCP = new CaptivePortal();
foreach ($mdlCP->zones->zone->iterateItems() as $zone) {
foreach ((new CaptivePortal())->zones->zone->iterateItems() as $zone) {
$response[(string)$zone->zoneid] = (string)$zone->description;
}
asort($response);

View file

@ -1,7 +1,7 @@
<?php
/**
* Copyright (C) 2015 Deciso B.V.
* Copyright (C) 2015-2025 Deciso B.V.
*
* All rights reserved.
*
@ -97,10 +97,6 @@ class SettingsController extends ApiMutableModelControllerBase
*/
public function searchZonesAction()
{
return $this->searchBase(
"zones.zone",
null,
"description"
);
return $this->searchBase("zones.zone", null, "description");
}
}

View file

@ -1,7 +1,7 @@
<?php
/**
* Copyright (C) 2015 Deciso B.V.
* Copyright (C) 2015-2025 Deciso B.V.
*
* All rights reserved.
*
@ -45,9 +45,8 @@ class VoucherController extends ApiControllerBase
*/
public function listProvidersAction()
{
$result = array();
$authFactory = new AuthenticationFactory();
foreach ($authFactory->listServers() as $authName => $authProps) {
$result = [];
foreach ((new AuthenticationFactory())->listServers() as $authName => $authProps) {
if ($authProps['type'] == 'voucher') {
$result[] = $authName;
}
@ -62,12 +61,11 @@ class VoucherController extends ApiControllerBase
*/
public function listVoucherGroupsAction($provider)
{
$authFactory = new AuthenticationFactory();
$auth = $authFactory->get(urldecode($provider));
$auth = (new AuthenticationFactory())->get(urldecode($provider));
if ($auth != null && method_exists($auth, 'listVoucherGroups')) {
return $auth->listVoucherGroups();
} else {
return array();
return [];
}
}
@ -79,12 +77,11 @@ class VoucherController extends ApiControllerBase
*/
public function listVouchersAction($provider, $group)
{
$authFactory = new AuthenticationFactory();
$auth = $authFactory->get(urldecode($provider));
$auth = (new AuthenticationFactory())->get(urldecode($provider));
if ($auth != null && method_exists($auth, 'listVouchers')) {
return $auth->listVouchers(urldecode($group));
} else {
return array();
return [];
}
}
@ -97,14 +94,13 @@ class VoucherController extends ApiControllerBase
public function dropVoucherGroupAction($provider, $group)
{
if ($this->request->isPost()) {
$authFactory = new AuthenticationFactory();
$auth = $authFactory->get(urldecode($provider));
$auth = (new AuthenticationFactory())->get(urldecode($provider));
if ($auth != null && method_exists($auth, 'dropVoucherGroup')) {
$auth->dropVoucherGroup(urldecode($group));
return array("status" => "drop");
}
}
return array("status" => "error");
return ["status" => "error"];
}
/**
@ -116,13 +112,12 @@ class VoucherController extends ApiControllerBase
public function dropExpiredVouchersAction($provider, $group)
{
if ($this->request->isPost()) {
$authFactory = new AuthenticationFactory();
$auth = $authFactory->get(urldecode($provider));
$auth = (new AuthenticationFactory())->get(urldecode($provider));
if ($auth != null && method_exists($auth, 'dropExpired')) {
return array("status" => "drop", "count" => $auth->dropExpired(urldecode($group)));
}
}
return array("status" => "error");
return ["status" => "error"];
}
@ -133,10 +128,9 @@ class VoucherController extends ApiControllerBase
*/
public function generateVouchersAction($provider)
{
$response = array("status" => "error");
$response = ["status" => "error"];
if ($this->request->isPost()) {
$authFactory = new AuthenticationFactory();
$auth = $authFactory->get(urldecode($provider));
$auth = (new AuthenticationFactory())->get(urldecode($provider));
if ($auth != null && method_exists($auth, 'generateVouchers')) {
$count = $this->request->getPost('count', 'int', 0);
$validity = $this->request->getPost('validity', 'int', 0);
@ -162,11 +156,10 @@ class VoucherController extends ApiControllerBase
*/
public function expireVoucherAction($provider)
{
$response = array("status" => "error");
$response = ["status" => "error"];
$username = $this->request->getPost('username', 'string', null);
if ($this->request->isPost() && $username != null) {
$authFactory = new AuthenticationFactory();
$auth = $authFactory->get(urldecode($provider));
$auth = (new AuthenticationFactory())->get(urldecode($provider));
if ($auth != null && method_exists($auth, 'expireVoucher')) {
$auth->expireVoucher($username);
$response['status'] = 'ok';

View file

@ -141,6 +141,8 @@
}
});
});
updateServiceControlUI('captiveportal');
});
@ -181,7 +183,7 @@
</div>
</div>
{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/captiveportal/service/reconfigure'}) }}
{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/captiveportal/service/reconfigure', 'data_service_widget': 'captiveportal'}) }}
{# include dialogs #}
{{ partial("layout_partials/base_dialog",['fields':formDialogZone,'id':formGridZone['edit_dialog_id'],'label':lang._('Edit zone')])}}

View file

@ -1,7 +1,7 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2015-2019 Ad Schellevis <ad@opnsense.org>
Copyright (c) 2015-2025 Ad Schellevis <ad@opnsense.org>
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -38,7 +38,7 @@ from io import BytesIO
from lib import OPNsenseConfig
response = dict()
source_directory = '/usr/local/opnsense/scripts/captiveportal/htdocs_default'
source_directory = '%s/htdocs_default' % os.path.realpath(os.path.dirname(__file__))
output_data = BytesIO()
@ -71,8 +71,7 @@ with zipfile.ZipFile(output_data, mode='w', compression=zipfile.ZIP_DEFLATED) as
filename = '%s/%s' % (root, filename)
output_filename = filename[len(source_directory)+1:]
if output_filename not in user_filenames:
tmp = open(filename, 'rb').read()
zf.writestr(output_filename, tmp)
zf.writestr(output_filename, open(filename, 'rb').read())
response['payload'] = base64.b64encode(output_data.getvalue()).decode()
response['size'] = len(response['payload'])

View file

@ -1,5 +1,6 @@
# This list contains the filenames that may not be overwritten by the user when creating custom templates.
# All items in this list won't be stored in the config.xml by our framework
css/bootstrap-theme.min
css/bootstrap.css.map
css/bootstrap.min.css
fonts/glyphicons-halflings-regular.eot
@ -8,3 +9,5 @@ fonts/glyphicons-halflings-regular.ttf
fonts/glyphicons-halflings-regular.woff
fonts/glyphicons-halflings-regular.woff2
js/bootstrap.min.js
js/jquery-1.11.2.min.js
js/jquery-3.5.1.min.js

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2015-2019 Ad Schellevis <ad@opnsense.org>
Copyright (c) 2015-2025 Ad Schellevis <ad@opnsense.org>
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -28,7 +28,7 @@
--------------------------------------------------------------------------------------
read a user provided base64 encoded zip file and generate a new one without standard
files. (strips javascript etc)
Filename should be provided by user parameter and must be available in /tmp/
Filename should be provided by user parameter and must be available in /var/lib/php/tmp
"""
import sys
import os.path
@ -39,19 +39,7 @@ import base64
from io import BytesIO
from hashlib import md5
htdocs_default_root = '/usr/local/opnsense/scripts/captiveportal/htdocs_default'
def load_exclude_list():
""" load exclude list, files that should be removed from the input stream
"""
result = []
excl_filename = '%s/exclude.list' % htdocs_default_root
for line in open(excl_filename, 'r').read().split('\n'):
line = line.strip()
if len(line) > 1 and line[0] != '#':
result.append(line)
return result
htdocs_default_root = '%s/htdocs_default' % os.path.realpath(os.path.dirname(__file__))
response = dict()
@ -71,7 +59,8 @@ else:
response['error'] = 'Error reading file'
if 'error' not in response:
exclude_list = load_exclude_list()
with open('%s/exclude.list' % htdocs_default_root, 'r') as f_in:
exclude_list = [x.strip() for x in f_in if len(x) > 2 and not x.strip().startswith('#')]
input_data = BytesIO(zip_content)
output_data = BytesIO()
with zipfile.ZipFile(input_data, mode='r', compression=zipfile.ZIP_DEFLATED) as zf_in:
@ -81,16 +70,17 @@ if 'error' not in response:
for zf_info in zf_in.infolist():
if zf_info.filename.find('index.html') > -1:
if index_location is None or len(index_location) > len(zf_info.filename):
index_location = zf_info.filename
index_location = zf_info.filename.replace('index.html', '')
if index_location is not None:
for zf_info in zf_in.infolist():
if zf_info.filename[-1] != '/':
filename = zf_info.filename.replace(index_location.replace('index.html', ''), '')
filename = zf_info.filename.replace(index_location, '')
# ignore internal osx metadata files, maybe we need to ignore some others (windows?) as well
# here.
if filename.split('/')[0] == '__MACOSX' or filename.split('/')[-1] == '.DS_Store':
skip_list = set(['__MACOSX', '.DS_Store', '._.'])
if len(set(filename.split('/')).intersection(skip_list)) > 0:
continue
if filename not in exclude_list:
elif filename not in exclude_list:
file_data = zf_in.read(zf_info.filename)
src_filename = '%s/%s' % (htdocs_default_root, filename)
if os.path.isfile(src_filename):

View file

@ -41,6 +41,13 @@ type:script
message:restarting captiveportal services
description:Restart Captive Portal service
[status]
command:/usr/local/sbin/pluginctl -s captiveportal status
parameters:
type:script_output
message:request captiveportal status
[fetch_template]
command:/usr/local/opnsense/scripts/captiveportal/fetch_template.py
parameters:%s