minetest ActiveFormspecs
========================

**active formspecs** self-contained API that provides formspecs secure session 
tracking, formspecs session-based state tables, and localized event handling of 
formspecs

Information
------------

This mod is named `formspecs` an improved version of dinamic secured forms 

![](screenshot.png?raw=true)

ActiveFormspecs (named `formspecs` 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 games. It evolved out of a recurring need for secure "node formspecs" on my
server, while avoiding the burden of "reinventing the wheel" with every new project.

## Tecnical info

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 as possible, 
currently tested with 0.4.16 and 5.2.

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.

#### Dependences

none, core minetest api

#### Overview:

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.

#### Progress and status

https://forum.minetest.net/viewtopic.php?f=9&t=19303

The project is a WIP and will be undergoing continuous development based upon your suggestions as well 
as my personal needs. Version 3.0 is already underway, and I am planning to introduce substantial 
improvements to the core functionality. New features and bug-fixes will be announced here as they become 
available. During significant milestones, I will include a roadmap so as to gauge your feedback about 
long-term goals. I will make every effort to ensure backward compatibility, when possible.

#### 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.

![](screenshot.png?raw=true)

The summary is sorted chronologically and divided into four columns per player:
* 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

A detached formspec can be opened using the minetest.create_form( ) method which returns a hashed session 
token (guaranteed to be unique):

* 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

The form can subsequently be re-opened or closed using the following methods:

* minetest.update_form( player_name, formspec )
    * player_name - a valid player name
    * formspec - a standard formspec string
    * minetest.destroy_form( player_name )
    * player_name - a valid player name

The associated on_close( ) callback function will be automatically invoked with three arguments during a 
form-related event or signal, including closure. The return value is currently ignored, but in the future 
it may allow for additional behavior.

* on_close( state, player, fields )
    * state - the session-based state table
    * player - the player object
    * fields - the table of submitted fields

In the event of abnormal form session termination, the callback function will receive a signal indicating 
the specific condition that led to the closure. One of the following values will be stored in the "quit" 
field:
* 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)

A non-trappable SIGSTOP signal can also be passed to minetest.destroy_form( ) or minetest.create_form( ) 
to forcibly kill the current form session, thereby avoiding recursion in callbacks during a procedural 
form closure.

```
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
```

When form closure may produce unanticipated side-effects, such as the example above, then the SIGSTOP 
signal can prove essential to avoid or even defer such actions.

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:

```
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
```

One caveat is that the parent form must be updated whenever a SIGCONT signal is received. This is for 
security reasons, given that element states (like dropdowns, scrollbars, etc.) cannot be preserved 
automatically, and some elements may even contain stale data (as with the case of textareas, fields, 
etc.) which should not be preserved anyway. Thankfully, only one line of code is required:

```
if fields.quit == minetest.FORMSPEC_SIGCONT then minetest.update_form( player_name, get_formspec( ) ) end
```

Formspecs are also supported within a node definition. If an on_open( ) method is defined, then it will 
be invoked whenever the player right-clicks the node. The return value must be a standard formspec string, 
or nil to abort.

* nodedef.on_open( pos, player, fields )
    * pos - the position of the node (or your own state table)
    * player - the player object

The on_close( ) method is invoked during any formspec event or signal. If defined, 
it will receive three arguments. Currently, the return value is ignored, but in the 
future it may allow for additional behavior.

* 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

An optional before_open( ) method may also be defined. It should return a custom 
state table to be subsequently passed to the on_open( ) method in place of the 
default "pos" parameter.

* nodedef.before_open( pos, node, player )
    * pos - the position of the node (or your own state table)
    * node - the node
    * player - the player object

The "hidden" formspec element of allows for a slightly different workflow, by 
presetting the state table from the formspec string itself. The key-value pairs 
are used only for initialization and will never be transmitted to the client. 
An optional data type can be specified within the element as follows:
* hidden[name;value;string]
* hidden[name;value;boolean]
* hidden[name;value;number]
* hidden[name;value]

If the data type is not specified, then no type-conversion will occur. Here are 
some examples:

* hidden[pos_x;-240;number]"
* hidden[wool_color;darkgreen;string]"
* hidden[is_dead;true;boolean]"

Through the use of form timers, it possible to re-open a formspec at regular intervals,
 or even to close the formspec after a specified period. There is no need for complex 
chains of minetest.after( ) or additional globalstep registrations. A form timer persists 
for the duration of a form session.

* minetest.get_form_timer( player_name, form_name )

Returns a form timer object associated with the form session of a given player.
Three methods are exposed by the form timer object, providing similar behavior to node timers.
    * 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 

The table returned 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

The associated on_close( ) callback function will be notified of timer expiration via a SIG_TIME signal.

#### Code examples

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.

```
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
} )
```

Of course, we could implement similar functionality without the need for a chat command. Perhaps we want to display the server uptime only when a privileged player clicks on a Nyan Cat while limiting the total number of refreshes to ten.

```
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,
} )
```

Revision History
----------------------

Version 1.0a (15-Dec-2016)
  - initial version within default mod

Version 1.1b (16-Dec-2016)
  - added better comments

Version 1.2b (18-Dec-2016)
  - renamed public methods

Version 1.3b (04-Jan-2017)
  - fixed logic of quit event to require valid session

Version 1.4b (26-Jul-2017)
  - added method to update formspec and maintain session

Version 2.0 (24-Dec-2017)
  - separated all routines into new mod for public release

Version 2.1 (08-Jan-2018)
  - various code refactoring and better comments
  - introduced password hashing of form names
  - improved sanity checks during form submission
  - fully reworked parsing of hidden elements
  - ensured hidden elements are always stripped
  - gave hidden elements default-state behavior
  - localized old node registration functions
  - included player object within form table
  - added signal handling on formspec termination
  - added support for callbacks in node overrides

Version 2.2 (19-Jan-2018)
  - introduced player-name arguments for all API calls
  - added signal for programmatic formspec closure
  - ensured callbacks are notified of session resets
  - renamed some local variables to improve clarity

Version 2.3 (28-Jan-2018)
  - corrected erroneous value of formspec exit signal
  - removed two experimental form session methods
  - included timestamp and origin within form table
  - added form session validation on destroy and update
  - introduced form timers with start and stop methods
  - created routine to notify callbacks of timeout
  - added support for internal statistical tracking
  - added chat command to view form session summary

Version 2.4 (12-Feb-2018)
  - various code refactoring and better comments
  - full rewrite of timer queue for higher precision

Version 2.5 (01-Feb-2019)
  - made callback function optional with default no-op
  - added non-trappable form session termination signal
  - properly reset timestamp for lifetime calculation

Version 2.6 (02-Feb-2020)
  - added callback to preset state of node formspecs
  - removed experimental property from node definition
  - implemented reverse-lookups for dropdown fields
  - extended dropdown element with optional parameter
  - added signal to suspend and restore form sessions
  - combined element parsers into dedicated function
  - added functions for escaping formspec strings
  - revamped element parsers to ignore malformed tags
  - added conditional pattern matching helper function
  - compatability bumped to Minetest 0.4.15+

Compatibility
----------------------

Minetest 0.4.15+ required or 5.X

Installation
----------------------

1. Unzip the archive into the mods directory or game mods directory
2. Get sure to the name of this directory to "formspecs" (if you cloned or decompress)
3. Add "formspecs" as a dependency to any mods using the API.

Source Code License
----------------------

The MIT License (MIT)

Copyright (c) 2016-2020, Leslie E. Krause (leslie@searstower.org) aka sorceredkid

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

For more details:
https://opensource.org/licenses/MIT
