--[[
- @author Bruno Massa
- @file form.lua
- Ajato provides a rich featured Form API to let developers
- easely create forms.
]]
--[[
- Convert a table into HTML tag attributes. Some tag need
- attributes, and this function provides a fast and reliable
- way to insert them. Like
-
- @param attributes
- Table, the tag attributes and values
- @return
- HTML, the tag attribute format
]]
function ajato_element_attributes(attributes)
local output = {}
attributes = attributes or {}
for index, value in pairs(attributes) do
if type(value) == 'table' or type(value) == 'string' or type(value) == 'number' then
if type(value) == 'table' then
value = table.concat(value)
end
-- All (X)HTML values should come between double quotes
output[#output + 1] = string.format('%s=%q', index, value)
end
end
return table.concat(output, ' ')
end
--[[
- Return a list of attributes from a given form field or
- a list of children fields
-
- @param element
- Table, the tag element
- @param only_attributes
- Boolean, TRUE to list only attributes (those that start with "#")
- @param content
- Boolean, TRUE to add the field content
- @return
- Function, a pairs() with all fields that fill the requirements
]]
function ajato_element_children(element, only_attributes, content)
if type(element) ~= 'table' then
return
end
local children = {}
local attribute
-- Check every field
for field in pairs(element) do
if type(field) == 'string' then
-- Check if the index start with the "#"
attribute = string.find(tostring(field), '#', 1)
if (only_attributes and attribute) or (not only_attributes and not attribute) then
-- Add the content to
if content then
children[field] = element[field]
else
children[#children + 1] = field
end
end
end
end
return children
end
--[[
- Retrieve the default properties for the defined element type.
-
- @param el_type
- String, the element type
- @param reset
- Boolean, TRUE to remake the "cache"
- @return
- Table, the element properties
]]
function ajato_element_info(el_type, reset)
local cache
return function(el_type, reset)
if not hv(cache) or reset then
cache = {}
for _, module in pairs(ajato_module_implements('elements')) do
local elements = _G[module ..'_elements']()
if elements and type(elements) == 'table' then
cache = ajato_tables_merge(cache, elements)
end
end
if #cache then
for element_type, info in pairs(cache) do
cache[element_type] = ajato_tables_merge({
['#description'] = nil,
['#required'] = false,
['#tree'] = false
}, info)
end
end
end
return cache[el_type] or {}
end
end; ajato_element_info = ajato_element_info()
--[[
- Return a pairs function with a list of attributes from a
- given form field or a list of children fields
-
- @param element
- Table, the tag element
- @param only_attributes
- Boolean, true in case to list only attributes (those that
- start with "#")
- @return
- Function, a pairs() with all fields that fill the requirements
]]
function ajato_element_pairs(element, only_attributes)
-- Return the pairs function
return pairs(ajato_element_children(element, only_attributes, true))
end
--[[
- Renders HTML given a structured table tree. Recursively iterates over each
- of the table elements, generating HTML code. This function is usually
- called from within a another function, like ajato_form_get() or node_view().
-
- @param elements
- Table, The data to be rendered.
- @return
- HTML, The rendered HTML.
]]
function ajato_element_render(elements)
if not elements or elements['#rendered'] or
(elements['#access'] and not hv(elements['#access'])) then
return ''
end
elements['#rendered'] = true
-- If the default values for this element haven't been loaded yet,
-- populate them.
if not hv(elements['#defaults_loaded']) then
local info = ajato_element_info(elements['#type'])
if not hv(elements['#type']) and hv(info) then
elements = ajato_tables_merge(elements, info)
end
end
-- Make any final changes to the element before it is rendered. This means
-- that the element or the children can be altered or corrected before the
-- element is rendered into the final text.
if elements['#pre_render'] then
for _, func in pairs(elements['#pre_render']) do
if _G[func] then
elements = _G[func](elements)
end
end
end
-- Element output
local content = ajato_string()
-- Either the elements did not go through form_builder
-- or one of the children has a #weight.
if not elements['#sorted'] then
-- uasort(elements, '_element_sort')
end
elements = ajato_tables_merge({['#title'] = nil, ['#description'] = nil}, elements)
-- If there is no children elements
if not elements['#children'] then
local children = ajato_element_children(elements)
-- Render all the children that use a theme function
if elements['#theme'] and not hv(elements['#theme_used']) then
elements['#theme_used'] = true
local previous = {}
for _, key in pairs({'#value', '#type', '#prefix', '#suffix'}) do
previous[key] = elements[key]
end
-- If we rendered a single element, then we will skip the renderer.
if not hv(children) then
elements['#printed'] = true
else
elements['#value'] = ''
end
elements['#type'] = 'markup'
elements['#prefix'] = nil
elements['#suffix'] = nil
content = content .. theme(elements['#theme'], elements)
for _, key in pairs({'#value', '#type', '#prefix', '#suffix'}) do
elements[key] = previous[key]
end
end
-- Render each of the children using ajato_element_render and concatenate them
for _, key in pairs(children) do
content = content .. ajato_element_render(elements[key])
end
end
if hv(tostring(content)) then
elements['#children'] = tostring(content)
end
-- Until now, we rendered the children, here we render the element itself
if not hv(elements['#printed']) then
content = theme((elements['#type'] or 'markup'), elements)
elements['#printed'] = true
end
content = tostring(content)
if hv(content) then
-- Filter the outputted content and make any last changes before the
-- content is sent to the browser. The changes are made on content
-- which allows the output'ed text to be filtered.
if elements['#post_render'] then
for _, func in pairs(elements['#post_render']) do
if _G[func] then
content = _G[func](content, elements)
end
end
end
local prefix = elements['#prefix'] or ''
local suffix = elements['#suffix'] or ''
return prefix .. content .. suffix
end
end
--[[
- @defgroup form Form generation
- @{
- Functions to enable the processing and display of HTML forms.
-
- Ajato uses these functions to achieve consistency in its form processing and
- presentation, while simplifying code and reducing the amount of HTML that
- must be explicitly generated by modules.
-
- The ajato_form_get() function handles retrieving, processing, and
- displaying a rendered HTML form for modules automatically. For example:
-
- -- Display the user registration form.
- output = ajato_form_get('user_register')
-
- Forms can also be built and submitted programmatically without any user input
- using the ajato_form_execute() function.
]]
--[[
- Walk through the structured form table, adding any required
- properties to each element and mapping the incoming sys['post']
- data to the proper elements.
-
- @param form_id
- String, A unique string identifying the form for validation,
- submission, theming, and hook_form_alter functions.
- @param form
- Table, The structure of the form.
- @param form_state
- Table, The current state of the form. In this
- context, it is used to accumulate information about which button
- was clicked when the form was submitted, as well as the sanitized
- sys['post'] data.
]]
function ajato_form_build(form_id, form, form_state)
-- Initialize as unprocessed.
form['#processed'] = false
-- Use element defaults
local info = ajato_element_info(form['#type'])
if hv(form['#type']) and info then
-- Overlay info onto form, retaining preexisting keys in form.
form = ajato_tables_merge(info, form)
end
if form['#type'] and form['#type'] == 'form' and hv(form['#programmed']) then
form_state['submitted'] = true
end
if form['#input'] and hv(form['#input']) then
ajato_form_build_element(form_id, form, form_state)
end
form['#defaults_loaded'] = true
-- We start off assuming all form elements are in the correct order.
form['#sorted'] = true
-- Recurse through all child elements.
local count = 0
local children = ajato_element_children(form)
if hv(children) then
for _, key in pairs(children) do
form[key]['#post'] = form['#post']
form[key]['#programmed'] = form['#programmed']
-- Don't squash an existing tree value.
if not form[key]['#tree'] then
form[key]['#tree'] = form['#tree']
end
-- Deny access to child elements if parent is denied.
if form['#access'] and not hv(form['#access']) then
form[key]['#access'] = false
end
-- Don't squash existing parents value.
if not form[key]['#parents'] then
-- Check to see if a tree of child elements is present. If so,
-- continue down the tree if required.
if form[key]['#tree'] and form['#tree'] then
form[key]['#parents'] = ajato_tables_merge({key}, form['#parents'], true)
else
form[key]['#parents'] = {key}
end
end
-- Assign a decimal placeholder weight to preserve original table order.
if not form[key]['#weight'] then
form[key]['#weight'] = count/1000
else
-- If one of the child elements has a weight then we will need to sort
-- later.
form['#sorted'] = nil
end
form[key] = ajato_form_build(form_id, form[key], form_state)
count = count + 1
end
end
-- The #after_build flag allows any piece of a form to be altered
-- after normal input parsing has been completed.
if form['#after_build'] and not hv(form['#after_build_done']) then
for func in pairs(form['#after_build']) do
form = _G[func](form, form_state)
form['#after_build_done'] = true
end
end
-- Now that we've processed everything, we can go back to handle the funky
-- Internet Explorer button-click scenario.
ajato_form_build_ie_cleanup(form, form_state)
-- After handling the special IE case, we no longer need the buttons collection.
form_state['buttons'] = nil
return form
end
--[[
- Populate the #value and #name properties of input elements so they
- can be processed and rendered. Also, execute any #process handlers
- attached to a specific element.
-
- @param form_id
- String, A unique string identifying the form for validation,
- submission, theming, and hook_form_alter functions.
- @param form
- Table, The structure of the form.
- @param form_state
- Table, The current state of the form. In this
- context, it is used to accumulate information about which button
- was clicked when the form was submitted, as well as the sanitized
- sys['post'] data.
]]
function ajato_form_build_element(form_id, form, form_state)
-- Define the attribute name for those dont have yet
if not form['#name'] then
local name = table.remove(form['#parents'])
form['#name'] = name
if form['#type'] == 'file' then
-- To make it easier to handle files in file.lua, we place all
-- file fields in the 'files' table. Also, we do not support
-- nested file names.
form['#name'] = 'files['.. form['#name'] ..']'
elseif hv(#form['#parents']) then
form['#name'] = form['#name'] .. '['.. table.concat(form['#parents'], '][') ..']'
end
table.insert(form['#parents'], 1, name)
end
-- Define the attribute id for those dont have yet
if not form['#id'] then
form['#id'] = ajato_form_clean_id('edit-'.. table.concat(form['#parents'], '-'))
end
-- Disabled fields
if hv(form['#disabled']) then
form['#attributes'] = form['#attributes'] or {}
form['#attributes']['disabled'] = 'disabled'
end
if not form['#value'] then
local func = form['#value_callback'] or 'ajato_form_type_'..
form['#type'] ..'_value'
-- Check if its was already submitted
if form['#programmed'] or ((not form['#access'] or hv(form['#access'])) and
form['#post'] and form['#post']['form_id'] and
form['#post']['form_id'] == form_id) then
local edit = form['#post']
for _, parent in pairs(form['#parents']) do
if edit[parent] then
edit = edit[parent]
else
edit = nil
break
end
end
if not hv(form['#programmed']) or edit then
-- Call #type_value to set the form value;
if _G[func] then
form['#value'] = _G[func](form, edit)
end
if not form['#value'] and edit then
form['#value'] = edit
end
end
-- Mark all posted values for validation.
if form['#value'] or hv(form['#required']) then
form['#needs_validation'] = true
end
end
-- Load defaults.
if not form['#value'] then
-- Call #type_value without a second argument to
-- request default_value handling.
if _G[func] then
form['#value'] = _G[func](form)
end
-- Final catch. If we haven't set a value yet,
-- use the explicit default value.
if not form['#value'] then
form['#value'] = form['#default_value'] or ''
end
end
end
-- Determine which button (if any) was clicked to submit the form.
-- We compare the incoming values with the buttons defined in the form,
-- and flag the one that matches. We have to do some funky tricks to
-- deal with Internet Explorer's handling of single-button forms, though.
if hv(form['#post']) and form['#executes_submit_callback'] then
-- First, accumulate a collection of buttons, divided into two bins:
-- those that execute full submit callbacks and those that only validate.
if not form_state['buttons'] then
form_state['buttons'] = {}
end
button_type = ((form['#executes_submit_callback'] and 'submit') or 'button')
if not form_state['buttons'][button_type] then
form_state['buttons'][button_type] = {}
end
form_state['buttons'][button_type][#form_state['buttons'][button_type] + 1] = form
if ajato_form_button_was_clicked(form) then
form_state['submitted'] = form_state['submitted'] or form['#executes_submit_callback']
-- In most cases, we want to use form_set_value() to manipulate
-- the global variables. In this special case, we want to make sure that
-- the value of this element is listed in form_variables under 'op'.
form_state['values'][form['#name']] = form['#value']
form_state['clicked_button'] = form
if form['#validate'] then
form_state['validate_handlers'] = form['#validate']
end
if form['#submit'] then
form_state['submit_handlers'] = form['#submit']
end
end
end
-- Allow for elements to expand to multiple elements, e.g., radios,
-- checkboxes and files.
if form['#process'] and not form['#processed'] then
for _, process in pairs(form['#process']) do
if _G[process] then
_G[process](form)
else
ajato_add_message(t('The function %func does not exist', {['%func'] = process}),
'error')
end
end
form['#processed'] = true
end
ajato_form_set_value(form, form_state, form['#value'])
end
--[[
- In IE, if only one submit button is present, AND the enter key is
- used to submit the form, no form value is sent for it and our normal
- button detection code will never detect a match. We call this
- function after all other button-detection is complete to check
- for the proper conditions, and treat the single button on the form
- as 'clicked' if they are met.
-
- @param form
- Table, The structure of the form.
- @param form_state
- Table, The current state of the form. In this
- context, it is used to accumulate information about which button
- was clicked when the form was submitted, as well as the sanitized
- sys['post'] data.
]]
function ajato_form_build_ie_cleanup(form, form_state)
-- Quick check to make sure we're always looking at the full form
-- and not a sub-element.
if hv(form['#type']) and form['#type'] == 'form' then
-- If we haven't recognized a submission yet, and there's a single
-- submit button, we know that we've hit the right conditions. Grab
-- the first one and treat it as the clicked button.
if not hv(form_state['submitted']) and hv(form_state['buttons']) and
hv(form_state['buttons']['submit']) and not hv(form_state['buttons']['button']) then
button = form_state['buttons']['submit'][0]
-- Set up all the form_state information that would have been
-- populated had the button been recognized earlier.
form_state['submitted'] = true
form_state['submit_handlers'] = button['#submit'] or nil
form_state['validate_handlers'] = button['#validate'] or nil
form_state['values'][button['#name']] = button['#value']
form_state['clicked_button'] = button
end
end
end
--[[
- Helper function to handle the sometimes-convoluted logic of button
- click detection.
-
- In Internet Explorer, if ONLY one submit button is present, AND the
- enter key is used to submit the form, no form value is sent for it
- and we'll never detect a match. That special case is handled by
- ajato_form_builder_ie_cleanup().
-
- @param form
- Table, the entire form table
- @return
- Boolean, TRUE when its ok and valid
]]
function ajato_form_button_was_clicked(form)
-- First detect normal 'vanilla' button clicks. Traditionally, all
-- standard buttons on a form share the same name (usually 'op'),
-- and the specific return value is used to determine which was
-- clicked. This ONLY works as long as form['#name'] puts the
-- value at the top level of the tree of sys['post'] data.
--
-- When image buttons are clicked, browsers do NOT pass the form element
-- value in sys['post']. Instead they pass an integer representing the
-- coordinates of the click on the button image. This means that image
-- buttons MUST have unique form['#name'] values, but the details of
-- their sys['post'] data should be ignored.
if form['#post'][form['#name']] and form['#post'][form['#name']] == form['#value'] then
return true
elseif hv(form['#has_garbage_value']) and form['#value'] and form['#value'] ~= '' then
return true
end
return false
end
--[[
- Fetch a form from cache
]]
function ajato_form_cache_get(form_build_id, form_state)
-- Get the form structure, if available
local cached = ajato_cache_get('form_'.. form_build_id, 'cache_form')
if hv(cached) then
form = cached['data']
-- Get the form state, if available
cached = ajato_cache_get('storage_'.. form_build_id, 'cache_form')
if hv(cached) then
form_state['storage'] = cached['data']
end
end
return form, form_state
end
--[[
- Store a form in the cache
]]
function ajato_form_cache_set(form_build_id, form, form_state)
local expire = max(ini_get('session.cookie_lifetime'), 86400)
-- Store the form structure
ajato_cache_set('form_'.. form_build_id, form, 'cache_form', expire)
-- Store the form state, if available
if hv(form_state['storage']) then
ajato_cache_set('storage_'.. form_build_id, form_state['storage'], 'cache_form', expire)
end
end
--[[
- Remove invalid characters from an HTML ID attribute string
-
- @param id
- The ID to clean.
- @return
- The cleaned ID.
]]
function ajato_form_clean_id(id)
id = id or ''
id = id:gsub('][', '-'):gsub('_', '-'):gsub(' ', '-')
return id
end
--[[
- Flag an element as having an error.
-
- @param element
- Table, the element table
- @param message
- String, the error message
]]
function ajato_form_error(element, message)
ajato_form_error_set(table.concat(element['#parents'], ']['), (message or ''))
end
--[[
- Return an associative array of all errors.
-
- @return
- Table, the elements that have error
]]
function ajato_form_errors_get()
form = ajato_form_error_set()
if hv(form) then
return form
end
end
--[[
- File an error against a form element. If the name of the element is
- edit[foo][bar] then you may pass either foo or foo][bar as name
- foo will set an error for all its children.
-
- @param name
- String, The name of the element
- @param message
- String, the error message
]]
function ajato_form_error_set(name, message)
-- Static variables
local form = {}
return function(name, message)
if hv(name) and not form[name] then
form[name] = message
if hv(message) then
ajato_add_message(message, 'error')
end
end
return form
end
end; ajato_form_error_set = ajato_form_error_set()
--[[
- A helper function used to execute custom validation and submission
- handlers for a given form. Button-specific handlers are checked
- first. If none exist, the function falls back to form-level handlers.
-
- @param handler_type
- String, The type of handler to execute. 'validate' or 'submit' are the
- defaults used by Form API.
- @param form
- Table, The structure of the form.
- @param form_state
- Table, The current state of the form. If the user
- submitted the form by clicking a button with custom handler functions
- defined, those handlers will be stored here.
]]
function ajato_form_execute_handlers(handler_type, form, form_state)
local result = false
local handlers = {}
if form_state[handler_type ..'_handlers'] then
handlers = form_state[handler_type ..'_handlers']
elseif form['#'.. handler_type] then
handlers = form['#'.. handler_type]
else
handlers = {}
end
for _, func in pairs(handlers) do
if _G[func] then
-- batch = batch_get()
if type == 'submit' and batch then
-- Some previous _submit handler has set a batch. We store the call
-- in a special 'control' batch set, for execution at the correct
-- time during the batch processing workflow.
batch['sets'][#batch['sets'] + 1] = {['form_submit'] = func}
else
_G[func](form, form_state)
end
result = true
end
end
return result
end
--[[
- Get form data to build the form in HMTL. The form is then passesed
- on for processing and rendered for display if necessary.
-
- @param form_id
- String, the form name
- @return
- HTML, the printed form
]]
function ajato_form_get(form_id, ...)
local form_state = {['storage'] = nil, ['submitted'] = nil}
local form
-- local batch_form_state = cgilua.session.load('batch_form_state')
if batch_form_state then
-- We've been redirected here after a batch processing : the form has
-- already been processed, so we grab the post-process form_state value
-- and move on to form display. See _batch_finished() function.
form_state = cgilua.session.load('batch_form_state')
cgilua.session.set('batch_form_state', nil)
else
-- If the incoming sys['post'] contains a form_build_id, we'll check the
-- cache for a copy of the form in question. If it's there, we don't
-- have to rebuild the form to proceed. In addition, if there is stored
-- form_state data from a previous step, we'll retrieve it so it can
-- be passed on to the form processing code.
if hv(sys['post']['form_build_id']) and sys['post']['form_id'] == form_id then
form, form_state = ajato_form_cache_get(sys['post']['form_build_id'], form_state)
end
-- We're hitting the form for the first time and we need
-- to build it from scratch.
if not hv(form) then
form_state['post'] = sys['post']
form = _G[form_id]()
require('md5')
form['#build_id'] = 'form-'.. md5.sumhexa(math.random())
-- Fill all missing values
form = ajato_form_prepare(form_id, form)
-- Cache the form
if form['#cache'] then
-- By not sending the form state, we avoid storing the storage which
-- won't have been touched yet.
ajato_form_cache_set(form_build_id, form, nil)
end
form_state['post'] = nil
end
form['#post'] = sys['post']
-- Now that we know we have a form, we'll process it (validating,
-- submitting, and handling the results returned by its submission
-- handlers. Submit handlers accumulate data in the form_state by
-- altering the form_state variable, which is passed into them by
-- reference.
form = ajato_form_process(form_id, form, form_state)
end
-- Render the form
return ajato_form_render(form_id, form)
end
--[[
- Prepares a structured form table by adding required elements,
- executing any hook_form_alter functions, and optionally inserting
- a validation token to prevent tampering.
-
- @param form_id
- String, A unique string identifying the form for validation,
- submission, theming, and hook_form_alter functions.
- @param form
- Table, The structure of the form.
- @param form_state
- Table, The current state of the form. Passed in here so that
- hook_form_alter() calls can use it, as well.
]]
function ajato_form_prepare(form_id, form, form_state)
-- global user
form['#type'] = 'form';
form['#programmed'] = hv(form['#post'])
if form['#build_id'] then
form['form_build_id'] = {
['#type'] = 'hidden',
['#value'] = form['#build_id'],
['#id'] = form['#build_id'],
['#name'] = 'form_build_id',
}
end
-- Add a token, based on either #token or form_id, to any form displayed to
-- authenticated users. This ensures that any submitted form was actually
-- requested previously by the user and protects against cross site request
-- forgeries.
if form['#token'] then
if form['#token'] == false or user['uid'] == 0 or form['#programmed'] then
form['#token'] = nil
else
form['form_token'] = {
['#type'] = 'token',
['#default_value'] = ajato_get_token(form['#token'])
}
end
elseif user and user['uid'] and hv(user['uid']) and form['#programmed'] then
form['#token'] = form_id
form['form_token'] = {
['#id'] = ajato_form_clean_id('edit-'.. form_id ..'-form-token'),
['#type'] = 'token',
['#default_value'] = ajato_get_token(form['#token'])
}
end
if form_id then
form['form_id'] = {
['#type'] = 'hidden',
['#value'] = form_id,
['#id'] = ajato_form_clean_id('edit-'.. form_id)
}
end
if not form['#id'] then
form['#id'] = ajato_form_clean_id(form_id)
end
form = ajato_tables_merge(ajato_element_info('form'), form)
-- Add the submit and the validate hooks
if not form['#validate'] and _G[form_id ..'_validate'] then
form['#validate'] = {form_id ..'_validate'}
end
if not form['#submit'] and _G[form_id ..'_submit'] then
form['#submit'] = {form_id ..'_submit'}
end
ajato_alter('form_'.. form_id, form, form_state)
ajato_alter('form', form, form_state, form_id)
return form
end
--[[
- This function is the heart of form API. The form gets built, validated and in
- appropriate cases, submitted.
-
- @param form_id
- String, The unique string identifying the current form.
- @param form
- Table, The structure of the form.
- @param form_state
- Table, The current state of the form. This
- includes the current persistent storage data for the form, and
- any data passed along by earlier steps when displaying a
- multi-step form. Additional information, like the sanitized sys['post']
- data, is also accumulated here.
]]
function ajato_form_process(form_id, form, form_state)
form_state['values'] = {}
-- Build the form. Its needed when its the first time it
-- is displayed and also when submitted and now we need to
-- validate and execute the actions
form = ajato_form_build(form_id, form, form_state)
-- If the form was submited and the form_id matches
-- with the current form_id
if hv(form['#programmed']) or hv(form['#post']) and form['#post']['form_id'] == form_id then
ajato_form_validate(form_id, form, form_state)
if hv(form_state['submitted']) and not ajato_form_errors_get() and
not hv(form_state['rebuild']) then
form_state['redirect'] = nil
ajato_form_execute_handlers('submit', form, form_state)
-- We'll clear out the cached copies of the form and its stored data
-- here, as we've finished with them. The in-memory copies are still
-- here, though.
if ajato_variable_get('cache', C.CACHE_DISABLED) == C.CACHE_DISABLED and
hv(form_state['values']['form_build_id']) then
ajato_cache_clear_all('form_'.. form_state['values']['form_build_id'], 'cache_form')
ajato_cache_clear_all('storage_'.. form_state['values']['form_build_id'], 'cache_form')
end
-- If batches were set in the submit handlers, we process them now,
-- possibly ending execution.
-- local batch = batch_get()
if batch then
-- The batch uses its own copies of form and form_state for
-- late execution of submit handlers and post-batch redirection.
batch['form'] = form
batch['form_state'] = form_state
batch['progressive'] = not form['#programmed']
batch_process()
-- Execution continues only for programmatic forms.
-- For 'regular' forms, we get redirected to the batch processing
-- page. Form redirection will be handled in _batch_finished(),
-- after the batch is processed.
end
-- If no submit handlers have populated the form_state['storage']
-- bundle, and the form_state['rebuild'] flag has not been set,
-- we're finished and should redirect to a new destination page
-- if one has been set (and a fresh, unpopulated copy of the form
-- if one hasn't). If the form was called by ajato_form_execute(),
-- however, we'll skip this and let the calling function examine
-- the resulting form_state bundle itself.
if not form['#programmed'] and not hv(form_state['rebuild']) and
not hv(form_state['storage']) then
ajato_form_redirect(form, form_state['redirect'])
end
end
end
return form
end
--[[
- Redirect the user to a URL after a form has been processed.
-
- @param form
- Table, The structure of the form.
- @param redirect
- String (Optional), the destination path to redirect to
- if none is specified by the form.
]]
function ajato_form_redirect(form, redirect)
local goto
if redirect == false or hv(redirect) then
goto = redirect
end
if goto ~= false and form['#redirect'] then
goto = form['#redirect']
end
if goto == false or hv(goto) then
if hv(goto) then
if type(goto) == 'table' then
ajato_goto(unpack(goto))
else
ajato_goto(goto)
end
end
ajato_goto(sys['get']['q']);
end
end
--[[
- Render the form table into HTML
-
- @param form
- Table, the form table
- @return
- HTML, the redered form
]]
function ajato_form_render(form_id, form)
-- Don't override #theme if someone already set it.
if not form['#theme'] and _G['themes'][form_id] then
form['#theme'] = form_id
end
return ajato_element_render(form)
end
--[[
- Use this function to make changes to form values in the form validate
- phase, so they will be available in the submit phase in form_state.
-
- Specifically, if form['#parents'] is {'foo', 'bar'}
- and value is 'baz' then this function will make
- form_state['values']['foo']['bar'] to be 'baz'.
-
- @param form
- Table, The form item. Keys used: #parents, #value
- @param value
- Any type, The value for the form item.
- @param form_state
- Table, The current state of the form. This
- includes the current persistent storage data for the form, and
- any data passed along by earlier steps when displaying a
- multi-step form. Additional information, like the sanitized sys['post']
- data, is also accumulated here.
]]
function ajato_form_set_value(form, form_state, value)
local parents = ajato_tables_merge(form['#parents'], {})
_ajato_form_set_value(form_state['values'], parents, value)
end
--[[
- Helper function for ajato_form_set_value().
-
- We iterate over parents and create nested tables for them
- in form_state['values'] if needed. Then we insert the value into
- the right table.
-
- @param form
- Table, The form item. Keys used: #parents, #value
- @param value
- Any type, The value for the form item.
- @param form_state
- Table, The current state of the form. This
- includes the current persistent storage data for the form, and
- any data passed along by earlier steps when displaying a
- multi-step form. Additional information, like the sanitized sys['post']
- data, is also accumulated here.
]]
function _ajato_form_set_value(form_values, parents, value)
local parent = table.remove(parents, 1)
if not hv(parents) then
form_values[parent] = value
else
if not form_values[parent] then
form_values[parent] = {}
end
_ajato_form_set_value(form_values[parent], parents, value)
end
end
--[[
- Helper function to determine the value for a checkbox form element.
-
- @param form
- Table, The form element whose value is being populated.
- @param edit
- Table, The incoming POST data to populate the form element. If this is FALSE,
- the element's default value should be returned.
- @return
- Table, The data that will appear in the form_state['values'] collection
- for this element. Return nothing to use the default.
]]
function ajato_form_type_checkbox_value(form, edit)
if edit then
return ((hv(edit) and form['#return_value']) or 0)
end
end
--[[
- Helper function to determine the value for a checkboxes form element.
-
- @param form
- Table, The form element whose value is being populated.
- @param edit
- Table, The incoming POST data to populate the form element. If this is FALSE,
- the element's default value should be returned.
- @return
- Table, The data that will appear in the form_state['values'] collection
- for this element. Return nothing to use the default.
]]
function ajato_form_type_checkboxes_value(form, edit)
if not edit then
local value = {}
form['#default_value'] = form['#default_value'] or {}
for _, key in pairs(form['#default_value']) do
value[key] = 1
end
return value
end
end
--[[
- Helper function to determine the value for a password_confirm form
- element.
-
- @param form
- Table, The form element whose value is being populated.
- @param edit
- Table, The incoming POST data to populate the form element. If this is FALSE,
- the element's default value should be returned.
- @return
- Table, The data that will appear in the form_state['values'] collection
- for this element. Return nothing to use the default.
- @todo convert this function
]]
function ajato_form_type_password_confirm_value(form, edit)
if edit == false then
form = ajato_tables_merge({['#default_value'] = {}}, form)
return ajato_tables_merge(form['#default_value'], {
['pass1'] = '', ['pass2'] = ''})
end
end
--[[
- Helper function to determine the value for a select form element.
-
- @param form
- The form element whose value is being populated.
- @param edit
- The incoming POST data to populate the form element. If this is FALSE,
- the element's default value should be returned.
- @return
- The data that will appear in the form_state['values'] collection
- for this element. Return nothing to use the default.
- @todo convert this function
]]
function ajato_form_type_select_value(form, edit)
if edit ~= false then
if form['#multiple'] and hv(form['#multiple']) then
return (type(edit) == 'table' and ajato_map_assoc(edit)) or {}
else
return edit
end
end
end
--[[
- Helper function to determine the value for a textfield form element.
-
- @param form
- Table, The form element whose value is being populated.
- @param edit
- Table, The incoming POST data to populate the form element. If this is FALSE,
- the element's default value should be returned.
- @return
- Table, The data that will appear in the form_state['values'] collection
- for this element. Return nothing to use the default.
]]
function ajato_form_type_textfield_value(form, edit)
if edit then
-- Equate edit to the form value to ensure it's marked for
-- validation.
return string.gsub(string.gsub(edit, '\n', ''), '\r', '')
end
end
--[[
- Validates user-submitted form data from the form_state using
- the validate functions defined in a structured form array.
-
- @param form_id
- Sting, A unique string identifying the form for validation, submission,
- theming, and hook_form_alter functions.
- @param form
- Table, The structure of the form.
- @param form_state
- Table, The current state of the form. The current
- user-submitted data is stored in form_state['values'], though
- form validation functions are passed an explicit copy of the
- values for the sake of simplicity. Validation handlers can also
- form_state to pass information on to submit handlers. For example:
- form_state['data_for_submision'] = data
- This technique is useful when validation requires file parsing,
- web service requests, or other expensive requests that should
- not be repeated in the submission step.
]]
function ajato_form_validate(form_id, form, form_state)
-- Static variables
local validated_forms = {}
return function(form_id, form, form_state)
if validated_forms[form_id] then
return
end
-- If the session token was set by ajato_form_prepare(), ensure that it
-- matches the current user's session.
if form['#token'] then
if ajato_valid_token(form_state['values']['form_token'], form['#token']) then
-- Setting this error will cause the form to fail validation.
ajato_form_error_set('form_token',
t('Validation error, please try again. If this error persists, please contact the site administrator.'))
end
end
_ajato_form_validate(form, form_state, form_id)
validated_forms[form_id] = true
end
end; ajato_form_validate = ajato_form_validate()
--[[
- Performs validation on form elements. First ensures required fields are
- completed, #maxlength is not exceeded, and selected options were in the
- list of options given to the user. Then calls user-defined validators.
-
- @param elements
- Table, Structure of the form.
- @param form_state
- Table, The current state of the form. The current
- user-submitted data is stored in form_state['values'], though
- form validation functions are passed an explicit copy of the
- values for the sake of simplicity. Validation handlers can also
- form_state to pass information on to submit handlers. For example:
- form_state['data_for_submision'] = data
- This technique is useful when validation requires file parsing,
- web service requests, or other expensive requests that should
- not be repeated in the submission step.
- @param form_id
- String, A unique string identifying the form for validation, submission,
- theming, and hook_form_alter functions.
]]
function _ajato_form_validate(elements, form_state, form_id)
-- Recurse through all children.
for key in ajato_element_pairs(elements) do
if hv(elements[key]) then
_ajato_form_validate(elements[key], form_state)
end
end
-- Validate the current input
if not hv(elements['#validated']) then
if elements['#needs_validation'] then
-- An empty textfield returns '' and empty checkbox
-- and a textfield could return '0' so we use hv()
if elements['#required'] and not hv(elements['#value']) then
ajato_form_error(elements, t('!name field is required.', {['!name'] = elements['#title']}))
end
-- Verify that the value is not longer than #maxlength.
if elements['#maxlength'] and string.len(elements['#value']) > elements['#maxlength'] then
form_error(elements,
t('!name cannot be longer than %max characters but is currently %length characters long.',
{['!name'] = elements['#title'] or elements['#parents'][0],
['%max'] = elements['#maxlength'],
['%length'] = string.len(elements['#value'])} ));
end
-- Add legal choice check if element has #options. Can be skipped, but
-- then you must validate your own element.
if elements['#options'] and elements['#value'] and
not elements['#DANGEROUS_SKIP_CHECK'] then
local options
if elements['#type'] == 'select' then
options = form_options_flatten(elements['#options'])
else
options = elements['#options']
end
if type(elements['#value']) == 'table' then
local value
if elements['#type'] == 'checkboxes' then
value = ajato_map_assoc(elements['#value'])
for index in pairs(value) do
if not hv(value[index]) then
value[index] = nil
end
end
else
value = elements['#value']
end
for v in pairs(value) do
if not options[v] then
ajato_form_error(elements, t('An illegal choice has been detected. Please contact the site administrator.'))
watchdog('form', t('Illegal choice %choice in !name element.',
{['%choice'] = v,
['!name'] = elements['#title'] or elements['#parents'][0]}),
C.WATCHDOG_ERROR)
end
end
elseif not options[elements['#value']] then
ajato_form_error(elements, t('An illegal choice has been detected. Please contact the site administrator.'))
watchdog('form', t('Illegal choice %choice in %name element.',
{['%choice'] = elements['#value'],
['!name'] = elements['#title'] or elements['#parents'][0]}),
C.WATCHDOG_ERROR)
end
end
end
-- Call user-defined form level validators.
-- or
-- Call any element-specific validators. These must act on the element
-- #value data.
if form_id then
ajato_form_execute_handlers('validate', elements, form_state)
elseif elements['#element_validate'] then
for func in pairs(elements['#element_validate']) do
if _G[func] then
_G[func](elements, form_state)
end
end
end
-- The field passed in all checks
elements['#validated'] = true
end
end
--[[
- @} End of "defgroup form".
]]
--[[
- Add AHAH information about a form element to the page to communicate with
- javascript. If #ahah_path is set on an element, this additional javascript is
- added to the page header to attach the AHAH behaviors. See ahah.js for more
- information.
-
- @param element
- Table, The properties of the element.
- Properties used: ahah_event, ahah_path, ahah_wrapper, ahah_parameters,
- ahah_effect.
- @return
- Table, no modifications from the param
]]
function expand_ahah(element)
local js_added = {}
return function(element)
-- Adding the same javascript settings twice will cause a recursion error,
-- we avoid the problem by checking if the javascript has already been added.
if not js_added[element['#id']] and element['#ahah_event'] and element['#ahah_path'] then
ajato_add_js('misc/ahah.js')
ajato_add_js('misc/progress.js')
local ahah_binding = {
['id'] = element['#id'],
['uri'] = ajato_path_format(element['#ahah_path']),
['event'] = element['#ahah_event'],
['effect'] = element['#ahah_effect'] or 'none',
['method'] = element['#ahah_method'] or 'replace'
}
if hv(element['#ahah_wrapper']) then
ahah_binding['wrapper'] = element['#ahah_wrapper']
end
-- ajato_add_js({['ahah'] = {[element['#id']] = ahah_binding}}, 'setting')
js_added[element['#id']] = true
end
return element
end
end; expand_ahah = expand_ahah()
--[[
- Expand all checkboxes options
-
- @param element
- Table, The properties of the element
- ahah_effect.
- @return
- Table, the checkboxes options
]]
function expand_checkboxes(element)
local value = element['#value'] or {}
element['#tree'] = true
if #element['#options'] then
if not element['#default_value'] or element['#default_value'] == 0 then
element['#default_value'] = {}
end
for key, choice in pairs(element['#options']) do
if not element[key] then
element[key] = {
['#type'] = 'checkbox',
['#processed'] = true,
['#title'] = choice,
['#return_value'] = key,
['#attributes'] = ajato_tables_merge({}, element['#attributes'])
}
if value[key] then
element[key]['#default_value'] = value[key]
end
end
end
end
return element
end
--[[
- Format the HTML a button.
-
- @param element
- Table, the properties of the element.
- Properties used: title, value, return_value, description, required
- @return
- HTML, the button.
- @ingroup theme
]]
function themes.button(element)
local attr = element['#attributes'] or {}
attr['type'] = 'submit'
attr['name'] = element['#name']
attr['id'] = element['#id']
attr['value'] = element['#value']
attr['class'] = ajato_tables_merge(attr['class'], {'form-button'}, true)
return '\n"
end
--[[
- Format a checkbox.
-
- @param element
- Table, the properties of the element.
- Properties used: title, value, return_value, description, required
- @return
- HTML, the checkbox.
- @ingroup theme
]]
function themes.checkbox(element)
local attr = element['#attributes'] or {}
attr['type'] = 'checkbox'
attr['name'] = element['#name']
attr['id'] = element['#id']
attr['value'] = element['#return_value']
if hv(element['#value']) then
attr['checked'] = tostring(element['#value'])
end
attr['class'] = attr['class'] or {}
attr['class'][#attr['class'] + 1] = 'form-checkbox'
output = ''
if element['#title'] then
output = ''
end
element['#title'] = nil
return theme('form_element', element, output)
end
--[[
- Format a set of checkboxes.
-
- @param element
- Table, the properties of the element.
- @return
- HTML, the checkbox. set.
- @ingroup theme
]]
function themes.checkboxes(element)
local attr = element['#attributes'] or {}
attr['class'] = attr['class'] or {}
attr['class'][#attr['class'] + 1] = 'form-checkbox'
element['#children'] = '