* Modifications to the captive portal system
@ 2026-09-17 9:19 IPFire Italia
2026-09-17 11:08 ` Adolf Belka
0 siblings, 1 reply; 3+ messages in thread
From: IPFire Italia @ 2026-09-17 9:19 UTC (permalink / raw)
To: development
[-- Attachment #1: Type: text/plain, Size: 1032 bytes --]
Good morning everyone,
As indicated in the forum thread
https://community.ipfire.org/t/modified-captive-system/16141, I have
modified the pages that manage the captive portal system, adding features I
felt were missing.
Three pages were modified:
/srv/web/ipfire/cgi-bin/captive.cgi
/srv/web/ipfire/cgi-bin/captive/index.cgi
/srv/web/ipfire/html/captive/template.html
The first file I am posting here is `captive.cgi`.
It includes a text box to specify the Wi-Fi network SSID, which is then
printed on the PDF when exporting coupons.
I added a second language option for the terms of use: one version in the
local language and a second in English.
There is also a new section allowing devices to be added without going
through the coupon authentication page-useful for IoT devices and the like.
I have tested all the code on my home device with various mobile devices and
PCs, and everything worked perfectly.
I am not familiar with Git,
https://github.com/ParmaU/IPFire_Captive/pull/1/changes
Umberto P.
[-- Attachment #2: Type: text/html, Size: 3106 bytes --]
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: Modifications to the captive portal system
2026-09-17 9:19 Modifications to the captive portal system IPFire Italia
@ 2026-09-17 11:08 ` Adolf Belka
0 siblings, 0 replies; 3+ messages in thread
From: Adolf Belka @ 2026-09-17 11:08 UTC (permalink / raw)
To: IPFire Italia; +Cc: IPFire: Development-List
Hallo Umberto,
Sorry for my latish reply to your original development list email and your PM on the forum but I have been very involved in a lot of other things and have had limited time available.
In your PM and in the message below you have referred to doing your git work in github. You need to be using git and not github. All the IPFire documents are related to using git and not github.
I am using a desktop system for my git work but it could also be done on a server system with console or ssh access or on a laptop, or even on an IPFire system.
First you need to set up a build environment.
https://www.ipfire.org/docs/devel/ipfire-2-x/build-initial
then after having set that up you need to clone the IPFire-2.x git repo into the environment you have set up.
https://www.ipfire.org/docs/devel/ipfire-2-x/build-howto
Then in that cloned git tree you can make your branch for the captive project you are working on and then make the changes you want in that branch.
Then you carry out a commit on your ipfire repo with the command
git commit -sv
To be able to send your commit via email you need to edit the file .git/config and add the lines
[sendemail]
to = development@lists.ipfire.org
to it and save.
For the global git settings follow the instructions in
https://www.ipfire.org/docs/devel/git/setup
which includes setting up how to send the email to the IPFire email system, using your IPFire People username and email address.
Once all the above is set up ( it takes less time than it has taken me to write it ) then when you have made your edits to captive.cgi in you git repo and run git commit -sv to add in your commit message explaining the changes then you run
git format-patch -1 -o /"path to where you want to save the patch file to be emailed"
The -1 means to create the formatted patch file using the single commit that was last made.
You will then end up with a .patch file containing the details of your changes in the directory that you specified.
Then run git send-email "path to the patch file that was saved".
A question will then be asked and the simplest is to enter a for All and it will then send that patch file to the IPFire email server as defined by the global settings defined earlier above.
Have a go with the above and come back with any questions that you have.
Regards,
Adolf.
On 17/09/2026 11:19, IPFire Italia wrote:
> Good morning everyone,
>
> As indicated in the forum thread https://community.ipfire.org/t/modified-captive-system/16141 <https://community.ipfire.org/t/modified-captive-system/16141>, I have modified the pages that manage the captive portal system, adding features I felt were missing.
>
> Three pages were modified:
>
> /srv/web/ipfire/cgi-bin/captive.cgi
>
> /srv/web/ipfire/cgi-bin/captive/index.cgi
>
> /srv/web/ipfire/html/captive/template.html
>
> The first file I am posting here is `captive.cgi`.
>
> It includes a text box to specify the Wi-Fi network SSID, which is then printed on the PDF when exporting coupons.
>
> I added a second language option for the terms of use: one version in the local language and a second in English.
>
> There is also a new section allowing devices to be added without going through the coupon authentication page—useful for IoT devices and the like.
>
> I have tested all the code on my home device with various mobile devices and PCs, and everything worked perfectly.
>
> I am not familiar with Git,
>
> https://github.com/ParmaU/IPFire_Captive/pull/1/changes
>
> Umberto P.
>
^ permalink raw reply [flat|nested] 3+ messages in thread
* Modifications to the captive portal system
@ 2026-09-13 12:43 IPFire Italia
0 siblings, 0 replies; 3+ messages in thread
From: IPFire Italia @ 2026-09-13 12:43 UTC (permalink / raw)
To: development
[-- Attachment #1: Type: text/plain, Size: 48152 bytes --]
Good morning everyone,
As indicated in the forum thread
https://community.ipfire.org/t/modified-captive-system/16141, I have
modified the pages that manage the captive portal system, adding features I
felt were missing.
Three pages were modified:
/srv/web/ipfire/cgi-bin/captive.cgi
/srv/web/ipfire/cgi-bin/captive/index.cgi
/srv/web/ipfire/html/captive/template.html
The first file I am posting here is `captive.cgi`.
It includes a text box to specify the Wi-Fi network SSID, which is then
printed on the PDF when exporting coupons.
I added a second language option for the terms of use: one version in the
local language and a second in English.
There is also a new section allowing devices to be added without going
through the coupon authentication pageuseful for IoT devices and the like.
I have tested all the code on my home device with various mobile devices and
PCs, and everything worked perfectly.
I am not familiar with Git, so I am pasting the full code here.
#!/usr/bin/perl
############################################################################
###
#
#
# IPFire.org - A linux based firewall
#
# Copyright (C) 2016 IPFire Team <alexander.marx@ipfire.org
<mailto:alexander.marx@ipfire.org> > #
#
#
# This program is free software: you can redistribute it and/or modify
#
# it under the terms of the GNU General Public License as published by
#
# the Free Software Foundation, either version 3 of the License, or
#
# (at your option) any later version.
#
#
#
# This program is distributed in the hope that it will be useful,
#
# but WITHOUT ANY WARRANTY; without even the implied warranty of
#
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#
# GNU General Public License for more details.
#
#
#
# You should have received a copy of the GNU General Public License
#
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
#
#Version del 13-09-2026 ora 09.10
#
#
#
#
#
#
#
############################################################################
###
#use strict;
use Encode;
use HTML::Entities();
use File::Basename;
use PDF::API2;
use constant mm => 25.4 / 72;
use File::LibMagic;
use File::Copy;
# enable only the following on debugging purpose
#use warnings;
#use CGI::Carp 'fatalsToBrowser';
require '/var/ipfire/general-functions.pl';
require "${General::swroot}/lang.pl";
require "${General::swroot}/header.pl";
my %session_times = (
3600 => $Lang::tr{'one hour'},
14400 => $Lang::tr{'four hours'},
28800 => $Lang::tr{'eight hours'},
43200 => $Lang::tr{'twelve hours'},
86400 => $Lang::tr{'24 hours'},
604800 => $Lang::tr{'one week'},
1209600 => $Lang::tr{'two
weeks'},
2592000 => $Lang::tr{'one month'},
31536000 => $Lang::tr{'one year'},
0 => "- $Lang::tr{'unlimited'} -",
);
my %selected = ();
my $coupons = "${General::swroot}/captive/coupons";
my %couponhash = ();
my $logo = "${General::swroot}/captive/logo.dat";
my $logotmp = "/tmp/logo.tmp";
my %settings=();
my %mainsettings;
my %color;
my %cgiparams=();
my %netsettings=();
my %checked=();
my $errormessage='';
my $clients="${General::swroot}/captive/clients";
my %clientshash=();
my $settingsfile="${General::swroot}/captive/settings";
unless (-e $settingsfile) { &General::system("touch $settingsfile"); }
&Header::getcgihash(\%cgiparams);
&General::readhash("${General::swroot}/main/settings", \%mainsettings);
&General::readhash("/srv/web/ipfire/html/themes/ipfire/include/colors.txt",
\%color);
&General::readhash("$settingsfile", \%settings) if(-f $settingsfile);
&General::readhash("${General::swroot}/ethernet/settings", \%netsettings);
if ($cgiparams{'ACTION'} eq "export-coupons") {
my $pdf = &generate_pdf();
print "Content-Type: application/pdf\n";
print "Content-Disposition: attachment;
filename=captive-portal-coupons.pdf\n";
print "\n"; # end headers
# Send PDF
print $pdf;
exit(0);
}
&Header::showhttpheaders();
if ($cgiparams{'ACTION'} eq $Lang::tr{'save'}) {
my $file = $cgiparams{'logo'};
if ($file) {
chomp $file;
# Save logo to /tmp/ to carry out content type
check
my ($filehandle) = CGI::upload("logo");
open(FILE, ">$logotmp");
binmode $filehandle;
while (<$filehandle>) {
print FILE;
}
close(FILE);
# Check if the file content type is PNG or
JPEG
my $magic = File::LibMagic->new;
my $file_info =
$magic->info_from_filename($logotmp);
my $file_mime_type = $file_info->{mime_type};
if ($file_mime_type ne "image/png" &&
$file_mime_type ne "image/jpeg") {
$errormessage =
$Lang::tr{'Captive wrong type'}." - ".$file_mime_type;
# Remove temporary logo file if
there was an error.
unlink $logotmp;
}
}
$settings{'ENABLE_GREEN'} =
$cgiparams{'ENABLE_GREEN'};
$settings{'ENABLE_BLUE'} =
$cgiparams{'ENABLE_BLUE'};
$settings{'AUTH'}
= $cgiparams{'AUTH'};
$settings{'TITLE'} =
$cgiparams{'TITLE'};
$settings{'COLOR'} =
$cgiparams{'COLOR'};
# SSID is mandatory and is stored in the captive settings
file.
my $ssid = $cgiparams{'ssid'} // '';
$ssid =~ s/^\s+|\s+$//g;
if ($ssid eq '') {
$errormessage = 'SSID is required.';
} else {
$settings{'ssid'} = $ssid;
}
$settings{'SESSION_TIME'} =
$cgiparams{'SESSION_TIME'};
if (!$errormessage){
#Check if we need to upload a new logo
if ($file) {
# Move uploaded logo file from
/tmp/
File::Copy::move $logotmp,
$logo;
}
&General::writehash("$settingsfile",
\%settings);
# Save terms
# Save terms 1
# Save terms
open(FH, ">:utf8",
"/var/ipfire/captive/terms.txt") or die("$!");
print FH $cgiparams{'TERMS1'};
close(FH);
$cgiparams{'TERMS'} = "";
# Save terms 2
open(FH, ">:utf8",
"/var/ipfire/captive/terms2.txt") or die("$!");
print FH $cgiparams{'TERMS2'};
close(FH);
#$cgiparams{'TERMS1'} = "";
$cgiparams{'TERMS2'} = "";
#execute binary to reload firewall rules
system("/usr/local/bin/captivectrl");
if ($cgiparams{'ENABLE_BLUE'} eq 'on'){
system("/usr/local/bin/wirelessctrl");
}
}
}
if ($cgiparams{'ACTION'} eq "$Lang::tr{'Captive delete logo'}") {
unlink $logo;
}
if ($cgiparams{'ACTION'} eq "$Lang::tr{'Captive generate coupons'}") {
#check valid remark
if ($cgiparams{'REMARK'} ne '' &&
!&validremark($cgiparams{'REMARK'})){
$errormessage=$Lang::tr{'fwhost err remark'};
}
if (!$errormessage) {
# Remember selected values
foreach my $val (("SESSION_TIME", "COUNT",
"REMARK")) {
$settings{$val} =
$cgiparams{$val};
}
&General::writehash($settingsfile,
\%settings);
&General::readhasharray($coupons,
\%couponhash) if (-e $coupons);
my $now = time();
# Expiry time in seconds
my $expires = $settings{'SESSION_TIME'};
my $count = $settings{'COUNT'} || 1;
while($count-- > 0) {
# Generate a new code
my $code = &gencode();
# Check if the coupon code
already exists
foreach my $key (keys
%couponhash) {
if($couponhash{$key}[1] eq $code) {
#
Code already exists, so try again
$code = "";
$count++;
last;
}
}
next if ($code eq "");
# Get a new key from hash
my $key =
&General::findhasharraykey(\%couponhash);
# Initialize all fields
foreach my $i (0 .. 3) {
$couponhash{$key}[$i] = ""; }
$couponhash{$key}[0] = $now;
$couponhash{$key}[1] = $code;
$couponhash{$key}[2] = $expires;
$couponhash{$key}[3] =
$settings{'REMARK'};
}
# Save everything to disk
&General::writehasharray($coupons,
\%couponhash);
}
}
if ($cgiparams{'ACTION'} eq 'delete-coupon') {
#deletes an already generated but unused voucher
#read all generated vouchers
&General::readhasharray($coupons, \%couponhash) if (-e
$coupons);
foreach my $key (keys %couponhash) {
if($cgiparams{'key'} eq $couponhash{$key}[0]){
#write logenty with decoded
remark
my
$rem=HTML::Entities::decode_entities($couponhash{$key}[4]);
&General::log("Captive", "Delete
unused coupon $couponhash{$key}[1] $couponhash{$key}[2] hours valid expires
on $couponhash{$key}[3] remark $rem");
#delete line from hash
delete $couponhash{$key};
last;
}
}
#write back hash
&General::writehasharray($coupons, \%couponhash);
}
if ($cgiparams{'ACTION'} eq 'add-client') {
my $macaddr = $cgiparams{'MACADDR'};
my $ipaddr = $cgiparams{'IPADDR'};
my $session_time = $cgiparams{'SESSION_TIME'};
my $key = $cgiparams{'KEY'};
my $note = $cgiparams{'NOTE'};
if (!$macaddr || !$ipaddr || !$key || !$note) {
$errormessage = "All fields must be
completed.";
}
elsif ($macaddr =~ /[,\r\n]/ ||
$ipaddr =~ /[,\r\n]/ ||
$key =~ /[,\r\n]/ ||
$note =~ /[,\r\n]/) {
$errormessage = "Fields cannot contain commas
or line breaks.";
}
elsif (!exists $session_times{$session_time}) {
$errormessage = "Invalid session time.";
}
else {
my $max_sequence = 0;
open(my $clientfh, "+<", $clients)
or die("Could not open $clients:
$!");
flock($clientfh, 2)
or die("Could not lock $clients:
$!");
while (my $line = <$clientfh>) {
chomp($line);
my ($sequence) = split(/,/,
$line, 2);
if (defined($sequence) &&
$sequence =~ /^\d+$/ &&
$sequence > $max_sequence) {
$max_sequence =
$sequence;
}
}
my $new_sequence = $max_sequence + 1;
seek($clientfh, 0, 2);
print $clientfh join(",",
$new_sequence,
$macaddr,
$ipaddr,
time(),
$session_time,
$key,
$note
) . "\n";
close($clientfh);
}
}
if ($cgiparams{'ACTION'} eq 'delete-client') {
#delete voucher and connection in use
#read all active clients
&General::readhasharray($clients, \%clientshash) if (-e
$clients);
foreach my $key (keys %clientshash) {
if($cgiparams{'key'} eq
$clientshash{$key}[0]){
#prepare log entry with decoded
remark
my
$rem=HTML::Entities::decode_entities($clientshash{$key}[7]);
#write logentry
&General::log("Captive",
"Deleted client in use $clientshash{$key}[1] $clientshash{$key}[2] hours
valid expires on $clientshash{$key}[3] remark $rem - Connection will be
terminated");
#delete line from hash
delete $clientshash{$key};
last;
}
}
#write back hash
&General::writehasharray("$clients", \%clientshash);
#reload firewallrules to kill connection of client
system("/usr/local/bin/captivectrl");
}
#open webpage, print header and open box
&Header::openpage($Lang::tr{'Captive'}, 1, '');
&Header::openbigbox();
# If an error message exists, show a box with the error message
if ($errormessage) {
&Header::openbox('100%', 'left', $Lang::tr{'error
messages'});
print $errormessage;
&Header::closebox();
}
# Prints the config box on the website
&Header::openbox('100%', 'left', $Lang::tr{'Captive config'});
print <<END
<form method='post' action='$ENV{'SCRIPT_NAME'}'
enctype="multipart/form-data">\n
<table width='100%' border="0">
END
;
#check which parameters have to be enabled (from settings file)
$checked{'ENABLE_GREEN'}{'off'} = '';
$checked{'ENABLE_GREEN'}{'on'} = '';
$checked{'ENABLE_GREEN'}{$settings{'ENABLE_GREEN'}} = "checked='checked'";
$checked{'ENABLE_BLUE'}{'off'} = '';
$checked{'ENABLE_BLUE'}{'on'} = '';
$checked{'ENABLE_BLUE'}{$settings{'ENABLE_BLUE'}} = "checked='checked'";
$checked{'UNLIMITED'}{'off'} = '';
$checked{'UNLIMITED'}{'on'} = '';
$checked{'UNLIMITED'}{$settings{'UNLIMITED'}} = "checked='checked'";
$selected{'AUTH'} = ();
$selected{'AUTH'}{'COUPON'} = "";
$selected{'AUTH'}{'TERMS'} = "";
$selected{'AUTH'}{$settings{'AUTH'}} = "selected";
if ($netsettings{'GREEN_DEV'}){
print <<END;
<tr>
<td width='30%'>
$Lang::tr{'Captive active on'}
<font
color='$Header::colourgreen'>$Lang::tr{'green'}</font>
</td>
<td>
<input
type='checkbox' name='ENABLE_GREEN' $checked{'ENABLE_GREEN'}{'on'} />
</td>
</tr>
END
}
if ($netsettings{'BLUE_DEV'}){
print <<END;
<tr>
<td width='30%'>
$Lang::tr{'Captive active on'}
<font
color='$Header::colourblue'>$Lang::tr{'blue'}</font>
</td>
<td>
<input
type='checkbox' name='ENABLE_BLUE' $checked{'ENABLE_BLUE'}{'on'} />
</td>
</tr>
END
}
print<<END
<tr>
<td>
$Lang::tr{'Captive
authentication'}
</td>
<td>
<select name='AUTH'>
<option
value="TERMS" $selected{'AUTH'}{'TERMS'} >$Lang::tr{'Captive
terms'}</option>
<option
value="COUPON" $selected{'AUTH'}{'COUPON'}>$Lang::tr{'Captive
coupon'}</option>
</select>
</td>
</tr>
END
;
if ($settings{'AUTH'} eq 'TERMS') {
$selected{'SESSION_TIME'} = ();
foreach my $session_time (keys %session_times) {
$selected{'SESSION_TIME'}{$session_time} = "";
}
$selected{'SESSION_TIME'}{$settings{'SESSION_TIME'}} =
"selected";
print <<END;
<tr>
<td>$Lang::tr{'Captive client
session expiry time'}</td>
<td>
<select
name="SESSION_TIME">
END
foreach my $session_time (sort { $a <=> $b } keys
%session_times) {
print <<END;
<option value="$session_time" $selected{'SESSION_TIME'}{$session_time}>
$session_times{$session_time}
</option>
END
}
print <<END;
</select>
</td>
</tr>
END
}
print<<END;
<tr>
<td colspan="2">
<br>
<strong>$Lang::tr{'Captive
branding'}</strong>
</td>
</tr>
<tr>
<td>
$Lang::tr{'Captive title'}
</td>
<td>
<input type='text' name='TITLE'
value="@{[ &Header::escape($settings{'TITLE'}) ]}" size='40'>
</td>
</tr>
<tr>
<td>
SSID
</td>
<td>
<input type='text' name='ssid'
value="@{[ &Header::escape($settings{'ssid'}) ]}" size='40' required>
</td>
</tr>
<tr>
<td>$Lang::tr{'Captive brand color'}</td>
<td>
<input type="color" name="COLOR"
value="$settings{'COLOR'}">
</td>
</tr>
<tr>
<td>
$Lang::tr{'Captive upload logo'}
</td>
<td>
<input type="file" name="logo">
<br>$Lang::tr{'Captive upload
logo recommendations'}
</td>
</tr>
END
if (-e $logo) {
print <<END;
<tr>
<td>$Lang::tr{'Captive logo
uploaded'}</td>
<td>
$Lang::tr{'yes'}
<input
type='submit' name='ACTION' value="$Lang::tr{'Captive delete logo'}"/>
</td>
</tr>
END
}
my $terms1 = &getterms();
my $terms2 = &getterms2();
print <<END;
<tr>
<td>Terms & Conditions 1</td>
<td>
<textarea cols="50" rows="10" name="TERMS1">@{[ &Header::escape($terms1)
]}</textarea>
</td>
</tr>
<tr>
<td>Terms & Conditions 2</td>
<td>
<textarea cols="50" rows="10" name="TERMS2">@{[ &Header::escape($terms2)
]}</textarea>
</td>
</tr>
<tr>
<td></td>
<td align='right'>
<input type='submit'
name='ACTION' value="$Lang::tr{'save'}"/>
</td>
</tr>
</table></form>
END
&Header::closebox();
#if settings is set to use coupons, the coupon part has to be displayed
if ($settings{'AUTH'} eq 'COUPON') {
&coupons();
}
# Add client
&add_client();
# Show active clients
&show_clients();
sub getterms() {
my @ret;
open(FILE, "<:utf8", "/var/ipfire/captive/terms.txt");
while(<FILE>) {
push(@ret,
HTML::Entities::decode_entities($_));
}
close(FILE);
return join(/\n/, @ret);
}
sub getterms2() {
my @ret;
open(FILE, "<:utf8", "/var/ipfire/captive/terms2.txt");
while(<FILE>) {
push(@ret,
HTML::Entities::decode_entities($_));
}
close(FILE);
return join(/\n/, @ret);
}
sub gencode(){
#generate a random code only letters from A-Z except 'O' and
0-9
my @chars = ("A".."N", "P".."Z", "0".."9");
my $randomstring;
$randomstring .= $chars[rand @chars] for 1..8;
return $randomstring;
}
sub add_client() {
&Header::openbox('100%', 'left', 'Add Client');
my $epoch = time();
my $generated_key = &gencode();
print <<END;
<form method='post'
action='$ENV{'SCRIPT_NAME'}'>
<table width='100%' cellpadding='2'
cellspacing='0'>
<tr>
<td
width='25%'>$Lang::tr{'mac address'}</td>
<td>
<input type='text'
name='MACADDR'
size='40'
maxlength='17'
required>
</td>
</tr>
<tr>
<td>$Lang::tr{'Captive ip'}</td>
<td>
<input type='text'
name='IPADDR'
size='40'
required>
</td>
</tr>
<input type='hidden'
name='EPOCH' value='$epoch'>
<tr>
<td>$Lang::tr{'Captive vouchervalid'}</td>
<td>
<select name='SESSION_TIME'>
END
foreach my $session_time (sort { $a <=> $b } keys
%session_times) {
print "<option
value='$session_time'>$session_times{$session_time}</option>\n";
}
print <<END;
</select>
</td>
</tr>
<tr>
<input
type='hidden' name='KEY' value='$generated_key'>
</tr>
<tr>
<td>$Lang::tr{'remark'}</td>
<td>
<input type='text'
name='NOTE'
size='40'
required>
</td>
</tr>
</table>
<div align="right">
<input type='hidden'
name='ACTION'
value='add-client'>
<input type='submit'
value='Add client'>
</form>
</div>
END
&Header::closebox();
}
sub coupons() {
&Header::openbox('100%', 'left', $Lang::tr{'Captive generate
coupons'});
$selected{'SESSION_TIME'} = ();
foreach my $session_time (keys %session_times) {
$selected{'SESSION_TIME'}{$session_time} = "";
}
$selected{'SESSION_TIME'}{$settings{'SESSION_TIME'}} =
"selected";
print <<END;
<form method='post'
action='$ENV{'SCRIPT_NAME'}'>
<table border='0' width='100%'>
<tr>
<td width='30%'>
$Lang::tr{'Captive vouchervalid'}
</td>
<td width='70%'>
<select name="SESSION_TIME">
END
foreach my $session_time (sort { $a <=> $b } keys
%session_times) {
print <<END;
<option value="$session_time" $selected{'SESSION_TIME'}{$session_time}>
$session_times{$session_time}
</option>
END
}
print <<END;
</select>
</td>
</tr>
<tr>
<td>$Lang::tr{'remark'}</td>
<td>
<input type='text' name='REMARK' size=40>
</td>
</tr>
<tr>
<td>$Lang::tr{'Captive generated coupon no'}</td>
<td>
<select name="COUNT">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</td>
</tr>
</table>
<div align="right">
<input
type="submit" name="ACTION" value="$Lang::tr{'Captive generate coupons'}">
</div>
</form>
END
&Header::closebox();
# Show all coupons if exist
if (! -z $coupons) {
&show_coupons();
}
}
sub cleanup_expired_coupons
{
my $acttime=time();
&General::readhasharray($clients, \%clientshash) if (-e
$clients);
foreach my $key (keys %clientshash) {
#calculate endtime from clientshash
my $endtime;
if ($clientshash{$key}[3] > '0'){
$endtime =
$clientshash{$key}[2]+$clientshash{$key}[3];
if ($acttime > $endtime) {
delete
$clientshash{$key};
}
}
}
#write back hash
&General::writehasharray("$clients", \%clientshash);
}
sub show_coupons() {
&General::readhasharray($coupons, \%couponhash) if (-e
$coupons);
#if there are already generated but unsused coupons, print a
table
&Header::openbox('100%', 'left', $Lang::tr{'Captive issued
coupons'});
print <<END;
<table class='tbl' border='0'>
<tr>
<th
align='center' width='15%'>
$Lang::tr{'Captive coupon'}
</th>
<th
align='center' width='15%'>$Lang::tr{'Captive expiry time'}</th>
<th
align='center' width='65%'>$Lang::tr{'remark'}</th>
<th
align='center' width='5%'>$Lang::tr{'delete'}</th>
</tr>
END
foreach my $key (keys %couponhash) {
my $expirytime = $Lang::tr{'Captive nolimit'};
if ($couponhash{$key}[2] > 0) {
$expirytime =
&General::format_time($couponhash{$key}[2]);
}
if ($count++ % 2) {
$col="bgcolor='$color{'color20'}'";
} else {
$col="bgcolor='$color{'color22'}'";
}
print <<END;
<tr>
<td $col
align="center">
<b>$couponhash{$key}[1]</b>
</td>
<td $col
align="center">
$expirytime
</td>
<td $col
align="center">
$couponhash{$key}[3]
</td>
<td $col
align="center">
<form method='post'>
<input type='image' src='/images/delete.gif' align='middle'
alt='$Lang::tr{'delete'}' title='$Lang::tr{'delete'}' />
<input type='hidden' name='ACTION' value='delete-coupon' />
<input type='hidden' name='key' value='$couponhash{$key}[0]' />
</form>
</td>
</tr>
END
}
print "</table>";
# Download PDF
print <<END;
<div align="right">
<form method="POST">
<input
type="hidden" name="ACTION" value="export-coupons">
<input
type="submit" value="$Lang::tr{'Captive export coupons'}">
</form>
</div>
END
&Header::closebox();
}
sub show_clients() {
# if there are active clients which use coupons show table
return if ( -z $clients || ! -f $clients );
my $count=0;
my $col;
&Header::openbox('100%', 'left', $Lang::tr{'Captive
clients'});
print <<END;
<table class='tbl' width='100%'>
<tr>
<th
align='center' width='15%'>$Lang::tr{'Captive coupon'}</th>
<th
align='center' width='15%'>$Lang::tr{'Captive activated'}</th>
<th
align='center' width='15%'>$Lang::tr{'Captive expiry time'}</th>
<th
align='center' width='10%'>$Lang::tr{'Captive mac'}</th>
<th
align='center' width='43%'>$Lang::tr{'remark'}</th>
<th
align='center' width='5%'>$Lang::tr{'delete'}</th>
</tr>
END
&cleanup_expired_coupons();
&General::readhasharray($clients, \%clientshash) if (-e
$clients);
foreach my $key (sort {$clientshash{$a}[2] <=>
$clientshash{$b}[2]} keys %clientshash) {
#calculate time from clientshash (starttime)
my $starttime = sub{sprintf '%02d.%02d.%04d
%02d:%02d', $_[3], $_[4]+1, $_[5]+1900, $_[2], $_[1]
}->(localtime($clientshash{$key}[2]));
#calculate endtime from clientshash
my $endtime;
if ($clientshash{$key}[3] eq '0'){
$endtime=$Lang::tr{'Captive
nolimit'};
} else {
$endtime = sub{sprintf
'%02d.%02d.%04d %02d:%02d', $_[3], $_[4]+1, $_[5]+1900, $_[2], $_[1]
}->(localtime($clientshash{$key}[2]+$clientshash{$key}[3]));
}
if ($count++ % 2) {
$col="bgcolor='$color{'color20'}'";
} else {
$col="bgcolor='$color{'color22'}'";
}
my $coupon = ($clientshash{$key}[4] eq
"LICENSE") ? $Lang::tr{'Captive terms short'} : $clientshash{$key}[4];
print <<END;
<tr>
<td $col
align="center"><b>$coupon</b></td>
<td $col
align="center">$starttime</td>
<td $col
align="center">$endtime</td>
<td $col
align="center">$clientshash{$key}[0]</td>
<td $col
align="center">$clientshash{$key}[5]</td>
<td $col
align="center">
<form method='post'>
<input type='image' src='/images/delete.gif' align='middle'
alt='$Lang::tr{'delete'}' title='$Lang::tr{'delete'}' />
<input type='hidden' name='ACTION' value='delete-client' />
<input type='hidden' name='key' value='$clientshash{$key}[0]' />
</form>
</td>
</tr>
END
}
print "</table>";
&Header::closebox();
}
sub validremark
{
# Checks a hostname against RFC1035
my $remark = $_[0];
# Each part should be at least two characters in length
# but no more than 63 characters
if (length ($remark) < 1 || length ($remark) > 255) {
return 0;}
# Only valid characters are a-z, A-Z, 0-9 and -
if ($remark !~ /^[a-zäöüA-ZÖÄÜ0-9-.:;\|_()\/\s]*$/) {
return 0;}
# First character can only be a letter or a digit
if (substr ($remark, 0, 1) !~ /^[a-zäöüA-ZÖÄÜ0-9]*$/) {
return 0;}
# Last character can only be a letter or a digit
if (substr ($remark, -1, 1) !~ /^[a-zöäüA-ZÖÄÜ0-9.:;_)]*$/) {
return 0;}
return 1;
}
sub generate_pdf() {
my $pdf = PDF::API2->new();
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday,
$isdst) = gmtime(time);
my $timestamp = sprintf("D:%04d%02d%02d%02d%02d%02d+00;00",
$year+1900, $mon+1, $mday, $hour, $min, $sec);
$pdf->info(
"Creator" => $Lang::tr{'Captive portal'},
"Title" => $Lang::tr{'Captive portal
coupons'},
"CreationDate" => $timestamp,
"ModDate" => $timestamp,
);
# Set page size
$pdf->mediabox("A4");
$pdf->trimbox(28/mm, 27/mm, 182/mm, 270/mm);
# Set font
my $font = $pdf->ttfont("/usr/share/fonts/Ubuntu-R.ttf");
my $page_h_margin = 27/mm;
my $page_v_margin = 28/mm;
my $height = 68/mm;
my $width = 91/mm;
my $margin = 2/mm;
# Tux Image
my $tux_image =
$pdf->image_png("/srv/web/ipfire/html/captive/assets/UP.png");
my $logo_height = 12/mm;
my $logo_width = 12/mm;
my @coupons = ();
my %coupon_expiry_times = ();
# Read coupons
&General::readhasharray($coupons, \%couponhash) if (-e
$coupons);
foreach my $key (keys %couponhash) {
$coupon_expiry_times{$couponhash{$key}[1]} =
$couponhash{$key}[2];
push @coupons, $couponhash{$key}[1];
}
while (@coupons) {
# Make a new page
my $page = $pdf->page();
# Graphics
$gfx = $page->gfx();
# Headline font
my $f_headline = $page->text();
$f_headline->font($font, 20);
# Subheadline font
my $f_subheadline = $page->text();
$f_subheadline->font($font, 14);
# Coupon font
my $f_coupon = $page->text();
$f_coupon->font($font, 36);
# Lifetime
my $f_lifetime = $page->text();
$f_lifetime->font($font, 14);
# Watermark font
my $f_watermark = $page->text();
$f_watermark->fillcolor("#666666");
$f_watermark->font($font, 10);
my $i = 0;
while (@coupons && $i < 8) {
my $coupon = shift @coupons;
# Box corners
my $x = ($page_v_margin / 2) +
(($i % 2) ? $width : 0);
my $y = ($page_h_margin / 2) +
(int($i / 2) * $height);
# Weidth and height of the box
my $w = $width - $margin;
my $h = $height - $margin;
# Center
my $cx = $x + ($w / 2);
my $cy = $y + ($h / 2);
# Draw border box
$gfx->strokecolor("#333333");
$gfx->linedash(1/mm, 1/mm);
$gfx->rect($x, $y, $w, $h);
$gfx->stroke();
$gfx->endpath();
# Headline
$f_headline->translate($cx, ($y
+ $h - $cy) / 1.7 + $cy);
$f_subheadline->translate($cx,
($y + $h - $cy) / 2.4 + $cy);
if ($settings{'TITLE'}) {
$f_headline->text_center(decode("utf8",
&Header::escape($settings{'TITLE'})));
$f_subheadline->text_center(decode("utf8", $Lang::tr{'Captive WiFi
coupon'}));
} else {
$f_headline->text_center(decode("utf8", $Lang::tr{'Captive WiFi coupon'}));
}
# Coupon
$f_coupon->translate($cx, $cy);
$f_coupon->text_center(decode("utf8", $coupon));
# Show lifetime
my $expiry_time =
$coupon_expiry_times{$coupon};
$f_lifetime->translate($cx, $cy
- ($y + $h - $cy) / 4);
if ($expiry_time > 0) {
my $lifetime =
&General::format_time($expiry_time);
$f_lifetime->text_center(decode("utf8", $Lang::tr{'Captive valid for'} . " "
. $lifetime));
} else {
$f_lifetime->text_center(decode("utf8", $Lang::tr{'Captive nolimit'}));
}
# Add watermark
$gfx->image($tux_image, $x + $w
- $logo_width - $margin, $y + $margin, $logo_width, $logo_height);
$f_lifetime->translate($cx, $cy
- ($y + $h - $cy) / 2);
$f_lifetime->text_center(decode("utf8", "Ssid " . $settings{'ssid'} . " "));
$f_watermark->translate($x + $w
- ($margin * 2) - 30, $y + 25);
$f_watermark->text_right("Powered by IPFire");
$f_watermark->translate($x + $w
- ($margin * 2) - $logo_width, $y + ($logo_height / 2));
$i++;
}
}
# Write out the PDF document
return $pdf->stringify();
}
&Header::closebigbox();
&Header::closepage();
Umberto P.
[-- Attachment #2: Type: text/html, Size: 240522 bytes --]
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-17 11:08 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-17 9:19 Modifications to the captive portal system IPFire Italia
2026-09-17 11:08 ` Adolf Belka
-- strict thread matches above, loose matches on Subject: below --
2026-09-13 12:43 IPFire Italia
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox