ActiveFormspecs Mod v2.6
formspecs (by sorcerykid)
ActiveFormspecs is a self-contained API that provides secure session tracking, session-based state tables, and localized event handling of formspecs for individual mods as well as entire subgames in Minetest. It evolved out of a recurring need for secure "node formspecs" on my server, but without the burden of "reinventing the wheel" with every new project. ActiveFormspecs adheres to the best practices described in sofar's excellent Securing Formspec Code topic.
Since I've had many requests for source code from the Just Test Tribute subgame, I finally decided to release ActiveFormspecs with instructions and code examples so that other mod authors can start making use of this framework as well. It's easy to install and use, and arguably a more robust alternative to the builtin formspecs API.
ActiveFormspecs is intended to be compatible with all versions of Minetest 0.4.15+. It has been in continuous use on my server since December 2016 with only minor revisions. So it should prove secure and stable enough for any production environment, as long as you follow the instructions below.
Repository:
https://bitbucket.org/sorcerykid/formspecs
https://github.com/sorcerykid/formspecs (mirror)
Download Archive (.zip)
Download Archive (.tar.gz)
Dependencies:
None.
Revision History:
- v1.0 -- initial alpha version (15-Dec-2016)
- v1.1 -- added better comments (16-Dec-2016)
- v1.2 -- renamed public methods (18-Dec-2016)
- v1.3 -- fixed logic of quit event to require valid session (04-Jan-2017)
- v1.4 -- added method to update formspec for existing session (26-Jul-2017)
- v2.0 -- separated routines into new mod for public release (24-Dec-2017)
- v2.1 -- change log and release notes (08-Jan-2018)
- v2.2 -- change log and release notes (19-Jan-2018)
- v2.3 -- change log and release notes (28-Jan-2018)
- v2.4 -- change log and release notes (12-Feb-2018)
- v2.5 -- change log and release notes (01-Feb-2019)
- v2.6 -- change log and release notes (02-Feb-2020)
The MIT License (MIT)
Installation:
- Unzip the archive into the mods directory of your subgame.
- Rename the formspecs-master directory to "formspecs".
- Add "formspecs" as a dependency to any mods using the API.
ActiveFormspecs is a framework that abstracts the builtin formspec API of Minetest. It is intended to address a number of known security issues related to formspecs:
- Secure Session Tracking
Formspec names have been deprecated as they can be easily forged. Now each formspec session is assigned a unique session ID. Due to the persistent nature of the Minetest client-server protocol (unlike HTTP, for example), all session tracking is performed server-side. Negotiation and validation with the client is entirely unnecessary. Thus, integrity of the session ID is always guaranteed.
Session-Based State Table
Since the session ID token is retained throughout the lifetime of the formspec, it is therefore possible to update a formspec dynamically (e.g. in response to an event) with contextual data spanning multiple instances. This data is stored server-side via a session-based state table and it can even be initialized from within the formspec string itself using a new "hidden" element.
Localized Event Handling
The minetest.register_on_player_receive_fields( ) method has also been deprecated. Instead, each formspec is assigned its own callback function at runtime, which allows for completely localized event handling. This callback function is invoked after any event associated with the formspec (hence the moniker "ActiveFormspecs"). Both the meta table and form fields are passed as arguments.
Usage Instructions:
An interactive form session monitor can be accessed in-game via the /fs chat command (requires "server" privilege). A realtime summary of form sessions is displayed along with navigation buttons and related statistics.
- player - the name of the player viewing the formspec
- origin - the mod (or node, if attached) that created the formspec
- idletime - the elapsed time since a form-related event or signal
- lifetime - the elapsed time since the form was first opened
- minetest.create_form( state, player_name, formspec, on_close, signal )
- state - an optional session-based state-table (can be nil)
- player_name - a valid player name
- formspec - a standard formspec string
- on_close - an optional callback function to be invoked after an event or signal
- signal - an optional signal to pass to the on_close( ) callback function
- minetest.update_form( player_name, formspec )
- player_name - a valid player name
- formspec - a standard formspec string
- player_name - a valid player name
- on_close( state, player, fields )
- state - the session-based state table
- player - the player object
- fields - the table of submitted fields
- minetest.FORMSPEC_SIGEXIT (player clicked exit button or pressed esc key)
- minetest.FORMSPEC_SIGQUIT (player logged off)
- minetest.FORMSPEC_SIGKILL (player was killed)
- minetest.FORMSPEC_SIGTERM (server is shutting down)
- minetest.FORMSPEC_SIGPROC (programmatic closure)
- minetest.FORMSPEC_SIGTIME (timeout reached)
- minetest.FORMSPEC_SIGHOLD (child form opened)
- minetest.FORMSPEC_SIGCONT (child form closed)
Code: Select all
local function open_alertbox( player_name, message )
local function get_formspec( )
local formspec = <generate a basic formspec string here>
return formspec
end
minetest.create_form( nil, player_name, get_formspec( ), nil, minetest.FORMSPEC_SIGSTOP )
end
Cascading forms such as dialog boxes, alerts, etc. can be implemented through the use of the SIGHOLD and SIGCONT signals. By passing a SIGHOLD signal to minetest.create_form(), the callback will be notified of the signal and the form session will be suspended (this also stops any form timers). As soon as the child form is closed, the previous form session will be restored and the callback will be notified of a SIGCONT signal. ActiveFormspecs manages all of the session housekeeping behind the scenes.
Here is an example of a popup alert that, when opened, will temporarily suspend the current form session until closed:
Code: Select all
local function open_alert( player_name, message )
local get_formspec = function ( )
local formspec = "size[4,3]"
.. default.gui_bg_img
.. string.format( "label[0.5,0.5;%s]", minetest.formspec_escape( message ) )
.. "button_exit[1.0,2;2.0,1;close;Close]"
return formspec
end
minetest.create_form( nil, player_name, get_formspec( ), nil, minetest.FORMSPEC_SIGHOLD )
end
Code: Select all
if fields.quit == minetest.FORMSPEC_SIGCONT then minetest.update_form( player_name, get_formspec( ) ) end
- nodedef.on_open( pos, player, fields )
- pos - the position of the node (or your own state table)
- player - the player object
- nodedef.on_close( pos, player, fields )
- pos - the position of the node (or your own state table)
- player - the player object
- fields - the table of submitted fields
- nodedef.before_open( pos, node, player )
- pos - the position of the node (or your own state table)
- node - the node
- player - the player object
- hidden[name;value;string]
hidden[name;value;boolean]
hidden[name;value;number]
hidden[name;value]
Code: Select all
hidden[pos_x;-240;number]"
hidden[wool_color;darkgreen;string]"
hidden[is_dead;true;boolean]"
- minetest.get_form_timer( player_name, form_name )
Returns a form timer object associated with the form session of a given player.
- timer.start( timeout )
Starts a timer with the given timeout in seconds.
timer.stop( )
Cancels a running timer.
timer.get_state( )
Returns a table with information about the running timer including- elapsed - the number of seconds since the timer started
- remain - the number of seconds until the timer expires
- overrun - the number of milliseconds overrun for this period (only valid within an on_close( ) callback)
- counter - the number of periods that have accrued
To better understand this methodology, here are some working code examples. Let's say that we want to register an "uptime" chat command that displays the current server uptime to any player, with an option to automatically refresh the formspec each second.
Code: Select all
minetest.register_chatcommand( "uptime", {
description = "View the uptime of the server interactively",
func = function( player_name, param )
local is_refresh = true
local get_formspec = function( )
local uptime = minetest.get_server_uptime( )
local formspec = "size[4,2]"
.. string.format( "label[0.5,0.5;%s %d secs]",
minetest.colorize( "#FFFF00", "Server Uptime:" ), uptime
)
.. "checkbox[0.5,1;is_refresh;Auto Refresh;" .. tostring( is_refresh ) .. "]"
return formspec
end
local on_close = function( state, player, fields )
if fields.quit == minetest.FORMSPEC_SIGTIME then
minetest.update_form( player_name, get_formspec( ) )
elseif fields.is_refresh then
is_refresh = fields.is_refresh == "true"
if is_refresh == true then
minetest.get_form_timer( player_name ).start( 1 )
else
minetest.get_form_timer( player_name ).stop( )
end
end
end
minetest.create_form( nil, player_name, get_formspec( ), on_close )
minetest.get_form_timer( player_name ).start( 1 )
end
} )
Code: Select all
minetest.register_privilege( "uptime", "View the uptime of the server interactively" )
local function open_system_monitor( player_name, is_minutes )
local view_count = 0
local view_limit = 10
local function get_formspec( )
local uptime = minetest.get_server_uptime( )
local formspec = "size[4,3]"
.. string.format( "label[0.5,0.5;%s %0.1f %s]",
minetest.colorize( "#FFFF00", "Server Uptime:" ),
is_minutes and uptime / 60 or uptime,
is_minutes and "mins" or "secs"
)
.. "checkbox[0.5,1;is_minutes;Show Minutes;" .. tostring( is_minutes ) .. "]"
.. "button[0.5,2;2.5,1;update;Refresh]"
.. "hidden[view_count;1;number]"
.. "hidden[view_limit;10;number]"
return formspec
end
minetest.create_form( nil, player_name, get_formspec( ), function( state, player, fields )
if not minetest.check_player_privs( player_name, "uptime" ) then -- sanity check
return
end
if fields.update then
-- limit the number of refreshes!
if view_count == view_limit then
minetest.destroy_form( player_name )
minetest.chat_send_player( player_name, "You've exceeded the refresh limit." )
else
view_count = view_count + 1
minetest.update_form( player_name, get_formspec( ) )
end
elseif fields.is_minutes then
is_minutes = fields.is_minutes == "true"
minetest.update_form( player_name, get_formspec( ) )
end
end )
end
minetest.override_item( "nyancat:nyancat", {
description = "System Monitor",
on_rightclick = function( pos, node, player )
local player_name = player:get_player_name( )
if minetest.check_player_privs( player_name, "uptime" ) then
open_system_monitor( player_name, true )
else
minetest.chat_send_player( player_name, "Your privileges are insufficient." )
end
end,
} )