/** * Sets the attributes of the form according to the passed data, performing * validation on the way. If the form is submitted, this checks and processes * the form. * * @param array $data The form description hash */ public function __construct($data) { /*{{{*/ $GLOBALS['_PIEFORM_REGISTRY'][] = $this; if (!isset($data['name']) || !preg_match('/^[a-z_][a-z0-9_]*$/', $data['name'])) { throw new PieformException('Forms must have a name, and that name must be valid (validity test: could you give a PHP function the name?)'); } $this->name = $data['name']; // If the form has global configuration, get it now if (function_exists('pieform_configure')) { $formconfig = pieform_configure(); $defaultelements = isset($formconfig['elements']) ? $formconfig['elements'] : array(); foreach ($defaultelements as $name => $element) { if (!isset($data['elements'][$name])) { $data['elements'][$name] = $element; } } } else { $formconfig = array(); } // Assign defaults for the form $this->defaults = self::get_pieform_defaults(); $this->data = array_merge($this->defaults, $formconfig, $data); // Set the method - only get/post allowed $this->data['method'] = strtolower($this->data['method']); if ($this->data['method'] != 'post') { $this->data['method'] = 'get'; } // Make sure that the javascript callbacks are valid if ($this->data['jsform']) { $this->validate_js_callbacks(); } if (!$this->data['validatecallback']) { $this->data['validatecallback'] = $this->name . '_validate'; } if (!$this->data['successcallback']) { $this->data['successcallback'] = $this->name . '_submit'; } if (!$this->data['replycallback']) { $this->data['replycallback'] = $this->name . '_reply'; } $this->data['configdirs'] = array_map(create_function('$a', 'return substr($a, -1) == "/" ? substr($a, 0, -1) : $a;'), (array) $this->data['configdirs']); if (empty($this->data['tabindex'])) { $this->data['tabindex'] = 0; } if (!is_array($this->data['elements']) || count($this->data['elements']) == 0) { throw new PieformException('Forms must have a list of elements'); } if (isset($this->data['spam'])) { // Enable form tricks to make it harder for bots to fill in the form. // This was moved from lib/antispam.php, see: // http://wiki.mahara.org/Developer_Area/Specifications_in_Development/Anti-spam#section_7 // // Use the spam_error() method in your _validate function to check whether a submitted form // has failed any of these checks. // // Available options: // - hash: An array of element names to be hashed. Currently ids of input elements // are also hashed, so you need to be careful if you include 'elementname' in // the hash array, and make sure you rewrite any css or js so it doesn't rely on // an id like 'formname_elementname'. // - secret: String used to hash the fields. // - mintime: Minimum number of seconds that must pass between page load & form submission. // - maxtime: Maximum number of seconds that must pass between page load & form submission. // - reorder: Array of element names to be reordered at random. if (empty($this->data['spam']['secret']) || !isset($this->data['elements']['submit'])) { // @todo don't rely on submit element throw new PieformException('Forms with spam config must have a secret and submit element'); } $this->time = isset($_POST['__timestamp']) ? $_POST['__timestamp'] : time(); $spamelements1 = array('__invisiblefield' => array('type' => 'text', 'title' => get_string('spamtrap'), 'defaultvalue' => '', 'class' => 'dontshow')); $spamelements2 = array('__timestamp' => array('type' => 'hidden', 'value' => $this->time), '__invisiblesubmit' => array('type' => 'submit', 'value' => get_string('spamtrap'), 'class' => 'dontshow')); $insert = rand(0, count($this->data['elements'])); $this->data['elements'] = array_merge(array_slice($this->data['elements'], 0, $insert, true), $spamelements1, array_slice($this->data['elements'], $insert, count($this->data['elements']) - $insert, true), $spamelements2); // Min & max number of seconds between page load & submission if (!isset($this->data['spam']['mintime'])) { $this->data['spam']['mintime'] = 0.01; } if (!isset($this->data['spam']['maxtime'])) { $this->data['spam']['maxtime'] = 86400; } if (empty($this->data['spam']['hash'])) { $this->data['spam']['hash'] = array(); } $this->data['spam']['hash'][] = '__invisiblefield'; $this->data['spam']['hash'][] = '__invisiblesubmit'; $this->hash_fieldnames(); if (isset($this->data['spam']['reorder'])) { // Reorder form fields randomly $order = $this->data['spam']['reorder']; shuffle($order); $order = array_combine($this->data['spam']['reorder'], $order); $temp = array(); foreach (array_keys($this->data['elements']) as $k) { if (isset($order[$k])) { $temp[$order[$k]] = $this->data['elements'][$order[$k]]; } else { $temp[$k] = $this->data['elements'][$k]; } } $this->data['elements'] = $temp; } $this->spamerror = false; } // Get references to all the elements in the form, excluding fieldsets/containers foreach ($this->data['elements'] as $name => &$element) { // The name can be in the element itself. This is compatibility for // the perl version if (isset($element['name'])) { $name = $element['name']; } if (isset($element['type']) && ($element['type'] == 'fieldset' || $element['type'] == 'container')) { // Load the fieldset/container plugin as we know this form has one now $this->include_plugin('element', $element['type']); if ($this->get_property('template')) { self::info("Your form '{$this->name}' has a " . $element['type'] . ", but is using a template. Fieldsets/containers make no sense when using templates"); } foreach ($element['elements'] as $subname => &$subelement) { if (isset($subelement['name'])) { $subname = $subelement['name']; } $this->elementrefs[$subname] =& $subelement; $subelement['name'] = $subname; } unset($subelement); } else { $this->elementrefs[$name] =& $element; } $element['name'] = isset($this->hashedfields[$name]) ? $this->hashedfields[$name] : $name; } unset($element); // Check that all elements have names compliant to PHP's variable naming policy // (otherwise things get messy later) foreach (array_keys($this->elementrefs) as $name) { if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name)) { throw new PieformException('Element "' . $name . '" is badly named (validity test: could you give a PHP variable the name?)'); } } // Remove elements to ignore // This can't be done using $this->elementrefs, because you can't unset // an entry in there and have it unset the entry in $this->data['elements'] foreach ($this->data['elements'] as $name => $element) { if (isset($element['type']) && ($element['type'] == 'fieldset' || $element['type'] == 'container')) { foreach ($element['elements'] as $subname => $subelement) { if (!empty($subelement['ignore'])) { unset($this->data['elements'][$name]['elements'][$subname]); unset($this->elementrefs[$subname]); } } } else { if (!empty($element['ignore'])) { unset($this->data['elements'][$name]); unset($this->elementrefs[$name]); } } } // Set some attributes for all elements $autofocusadded = false; foreach ($this->elementrefs as $name => &$element) { if (count($element) == 0) { throw new PieformException('An element in form "' . $this->name . '" has no data (' . $name . ')'); } if (!isset($element['type']) || $element['type'] == 'markup') { $element['type'] = 'markup'; if (!isset($element['value'])) { throw new PieformException('The markup element "' . $name . '" has no value'); } } else { // Now we know what type the element is, we can load the plugin for it $this->include_plugin('element', $element['type']); // All elements should have at least the title key set if (!isset($element['title'])) { $element['title'] = ''; } // This function can be defined by the application using Pieforms, // and applies to all elements of this type $function = 'pieform_element_' . $element['type'] . '_configure'; if (function_exists($function)) { $element = $function($element); } // vvv --------------------------------------------------- vvv // After this point Pieforms can set or override attributes // without fear that the developer will be able to change them. // This function is defined by the plugin itself, to set // fields on the element that need to be set but should not // be set by the application $function = 'pieform_element_' . $element['type'] . '_set_attributes'; if (function_exists($function)) { $element = $function($element); // Allow an element to remove itself from the form if (!$element) { unset($this->data['elements'][$name]); unset($this->elementrefs[$name]); continue; } } // Force the form method to post if there is a file to upload if (!empty($element['needsmultipart'])) { $this->fileupload = true; if ($this->data['method'] == 'get') { $this->data['method'] = 'post'; self::info("Your form '{$this->name}' had the method 'get' and also a file element - it has been converted to 'post'"); } } // Add the autofocus flag to the element if required if (!$autofocusadded && $this->data['autofocus'] === true && empty($element['nofocus'])) { $element['autofocus'] = true; $autofocusadded = true; } elseif (!empty($this->data['autofocus']) && $this->data['autofocus'] !== true && $name == $this->data['autofocus']) { $element['autofocus'] = true; } if (!empty($element['autofocus']) && $element['type'] == 'text' && !empty($this->data['autoselect']) && $name == $this->data['autoselect']) { $element['autoselect'] = true; } // All elements inherit the form tabindex $element['tabindex'] = $this->data['tabindex']; } } unset($element); // Check if the form was submitted, and if so, validate and process it $global = $this->data['method'] == 'get' ? $_GET : $_POST; if ($this->data['validate'] && isset($global['pieform_' . $this->name])) { if ($this->data['submit']) { $this->submitted = true; // If the hidden value the JS code inserts into the form is // present, then the form was submitted by JS if (!empty($global['pieform_jssubmission'])) { $this->submitted_by_js = true; } // If the form was submitted via the dropzone if (!empty($global['dropzone'])) { $this->submitted_by_dropzone = true; } // Check if the form has been cancelled if ($this->data['iscancellable']) { foreach ($global as $key => $value) { if (substr($key, 0, 7) == 'cancel_') { // Check for and call the cancel function handler, if defined $function = $this->name . '_' . $key; if (function_exists($function)) { $function($this); } // Redirect the user to where they should go, if the cancel handler didn't already $element = $this->get_element(substr($key, 7)); if (!isset($element['goto'])) { throw new PieformException('Cancel element "' . $element['name'] . '" has no page to go to'); } if ($this->submitted_by_js) { $this->json_reply(PIEFORM_CANCEL, array('location' => $element['goto']), false); } header('HTTP/1.1 303 See Other'); header('Location:' . $element['goto']); exit; } } } } // Get the values that were submitted $values = $this->get_submitted_values(); // Perform general validation first $this->validate($values); // Submit the form if things went OK if ($this->data['submit'] && !$this->has_errors()) { $submitted = false; foreach ($this->elementrefs as $name => $element) { if (!empty($element['submitelement']) && isset($global[$element['name']])) { if (!is_array($this->data['successcallback'])) { $function = "{$this->data['successcallback']}_{$name}"; if (function_exists($function)) { $function($this, $values); log_debug('button-submit form ' . $function . ' should provide a redirect.'); return; } } } } $function = $this->data['successcallback']; if (!$submitted && is_callable($function)) { // Call the user defined function for processing a submit // This function should really redirect/exit after it has // finished processing the form. call_user_func_array($function, array($this, $values)); if ($this->data['dieaftersubmit']) { if ($this->data['jsform']) { $message = 'Your ' . $this->name . '_submit function should use $form->reply to send a response, which should redirect or exit when it is done. Perhaps you want to make your reply callback do this?'; } else { $message = 'Your ' . $this->name . '_submit function should redirect or exit when it is done'; } throw new PieformException($message); } else { // Successful submission, and the user doesn't care about replying, so... return; } } else { if (!$submitted) { throw new PieformException('No function registered to handle form submission for form "' . $this->name . '"'); } } } // If we get here, the form was submitted but failed validation // Auto focus the first element with an error if required if ($this->data['autofocus'] !== false) { $this->auto_focus_first_error(); } // Call the user-defined PHP error function, if it exists $function = $this->data['errorcallback']; if (is_callable($function)) { call_user_func_array($function, array($this)); } // If the form has been submitted by javascript, return json if ($this->submitted_by_js) { // TODO: get error messages in a 'third person' type form to // use here maybe? Would have to work for non js forms too. See // the TODO file //$errors = $this->get_errors(); //$json = array(); //foreach ($errors as $element) { // $json[$element['name']] = $element['error']; //} $message = $this->get_property('jserrormessage'); $this->json_reply(PIEFORM_ERR, array('message' => $message)); } else { global $SESSION; $SESSION->add_error_msg($this->get_property('errormessage')); } } }
/** * Sets the attributes of the form according to the passed data, performing * validation on the way. If the form is submitted, this checks and processes * the form. * * @param array $data The form description hash */ public function __construct($data) { /*{{{*/ $GLOBALS['_PIEFORM_REGISTRY'][] = $this; if (!isset($data['name']) || !preg_match('/^[a-z_][a-z0-9_]*$/', $data['name'])) { throw new PieformException('Forms must have a name, and that name must be valid (validity test: could you give a PHP function the name?)'); } $this->name = $data['name']; // If the form has global configuration, get it now if (function_exists('pieform_configure')) { $formconfig = pieform_configure(); $defaultelements = isset($formconfig['elements']) ? $formconfig['elements'] : array(); foreach ($defaultelements as $name => $element) { if (!isset($data['elements'][$name])) { $data['elements'][$name] = $element; } } } else { $formconfig = array(); } // Assign defaults for the form $this->data = array_merge(self::get_pieform_defaults(), $formconfig, $data); // Set the method - only get/post allowed $this->data['method'] = strtolower($this->data['method']); if ($this->data['method'] != 'post') { $this->data['method'] = 'get'; } // Make sure that the javascript callbacks are valid if ($this->data['jsform']) { $this->validate_js_callbacks(); } if (!$this->data['validatecallback']) { $this->data['validatecallback'] = $this->name . '_validate'; } if (!$this->data['successcallback']) { $this->data['successcallback'] = $this->name . '_submit'; } if (!$this->data['replycallback']) { $this->data['replycallback'] = $this->name . '_reply'; } $this->data['configdirs'] = array_map(create_function('$a', 'return substr($a, -1) == "/" ? substr($a, 0, -1) : $a;'), (array) $this->data['configdirs']); if (empty($this->data['tabindex'])) { $this->data['tabindex'] = self::$formtabindex++; } if (!is_array($this->data['elements']) || count($this->data['elements']) == 0) { throw new PieformException('Forms must have a list of elements'); } // Get references to all the elements in the form, excluding fieldsets foreach ($this->data['elements'] as $name => &$element) { // The name can be in the element itself. This is compatibility for // the perl version if (isset($element['name'])) { $name = $element['name']; } if (isset($element['type']) && $element['type'] == 'fieldset') { // Load the fieldset plugin as we know this form has one now $this->include_plugin('element', 'fieldset'); if ($this->get_property('template')) { self::info("Your form '{$this->name}' has a fieldset, but is using a template. Fieldsets make no sense when using templates"); } foreach ($element['elements'] as $subname => &$subelement) { if (isset($subelement['name'])) { $subname = $subelement['name']; } $this->elementrefs[$subname] =& $subelement; $subelement['name'] = $subname; } unset($subelement); } else { $this->elementrefs[$name] =& $element; } $element['name'] = $name; } unset($element); // Check that all elements have names compliant to PHP's variable naming policy // (otherwise things get messy later) foreach (array_keys($this->elementrefs) as $name) { if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name)) { throw new PieformException('Element "' . $name . '" is badly named (validity test: could you give a PHP variable the name?)'); } } // Remove elements to ignore // This can't be done using $this->elementrefs, because you can't unset // an entry in there and have it unset the entry in $this->data['elements'] foreach ($this->data['elements'] as $name => $element) { if (isset($element['type']) && $element['type'] == 'fieldset') { foreach ($element['elements'] as $subname => $subelement) { if (!empty($subelement['ignore'])) { unset($this->data['elements'][$name]['elements'][$subname]); unset($this->elementrefs[$subname]); } } } else { if (!empty($element['ignore'])) { unset($this->data['elements'][$name]); unset($this->elementrefs[$name]); } } } // Set some attributes for all elements $autofocusadded = false; foreach ($this->elementrefs as $name => &$element) { if (count($element) == 0) { throw new PieformException('An element in form "' . $this->name . '" has no data (' . $name . ')'); } if (!isset($element['type']) || $element['type'] == 'markup') { $element['type'] = 'markup'; if (!isset($element['value'])) { throw new PieformException('The markup element "' . $name . '" has no value'); } } else { // Now we know what type the element is, we can load the plugin for it $this->include_plugin('element', $element['type']); // All elements should have at least the title key set if (!isset($element['title'])) { $element['title'] = ''; } // This function can be defined by the application using Pieforms, // and applies to all elements of this type $function = 'pieform_element_' . $element['type'] . '_configure'; if (function_exists($function)) { $element = $function($element); } // vvv --------------------------------------------------- vvv // After this point Pieforms can set or override attributes // without fear that the developer will be able to change them. // This function is defined by the plugin itself, to set // fields on the element that need to be set but should not // be set by the application $function = 'pieform_element_' . $element['type'] . '_set_attributes'; if (function_exists($function)) { $element = $function($element); } // Force the form method to post if there is a file to upload if (!empty($element['needsmultipart'])) { $this->fileupload = true; if ($this->data['method'] == 'get') { $this->data['method'] = 'post'; self::info("Your form '{$this->name}' had the method 'get' and also a file element - it has been converted to 'post'"); } } // Add the autofocus flag to the element if required if (!$autofocusadded && $this->data['autofocus'] === true && empty($element['nofocus'])) { $element['autofocus'] = true; $autofocusadded = true; } elseif (!empty($this->data['autofocus']) && $this->data['autofocus'] !== true && $name == $this->data['autofocus']) { $element['autofocus'] = true; } // All elements inherit the form tabindex $element['tabindex'] = $this->data['tabindex']; } } unset($element); // Check if the form was submitted, and if so, validate and process it $global = $this->data['method'] == 'get' ? $_GET : $_POST; if ($this->data['validate'] && isset($global['pieform_' . $this->name])) { if ($this->data['submit']) { $this->submitted = true; // If the hidden value the JS code inserts into the form is // present, then the form was submitted by JS if (!empty($global['pieform_jssubmission'])) { $this->submitted_by_js = true; } // Check if the form has been cancelled if ($this->data['iscancellable']) { foreach ($global as $key => $value) { if (substr($key, 0, 7) == 'cancel_') { // Check for and call the cancel function handler, if defined $function = $this->name . '_' . $key; if (function_exists($function)) { $function($this); } // Redirect the user to where they should go, if the cancel handler didn't already $element = $this->get_element(substr($key, 7)); if (!isset($element['goto'])) { throw new PieformException('Cancel element "' . $element['name'] . '" has no page to go to'); } if ($this->submitted_by_js) { $this->json_reply(PIEFORM_CANCEL, array('location' => $element['goto']), false); } header('HTTP/1.1 303 See Other'); header('Location:' . $element['goto']); exit; } } } } // Get the values that were submitted $values = $this->get_submitted_values(); // Perform general validation first $this->validate($values); // Submit the form if things went OK if ($this->data['submit'] && !$this->has_errors()) { $submitted = false; foreach ($this->elementrefs as $element) { if (!empty($element['submitelement']) && isset($global[$element['name']])) { $function = "{$this->data['successcallback']}_{$element['name']}"; if (function_exists($function)) { $function($this, $values); $submitted = true; break; } } } $function = $this->data['successcallback']; if (!$submitted && is_callable($function)) { // Call the user defined function for processing a submit // This function should really redirect/exit after it has // finished processing the form. call_user_func_array($function, array($this, $values)); if ($this->data['dieaftersubmit']) { if ($this->data['jsform']) { $message = 'Your ' . $this->name . '_submit function should use $form->reply to send a response, which should redirect or exit when it is done. Perhaps you want to make your reply callback do this?'; } else { $message = 'Your ' . $this->name . '_submit function should redirect or exit when it is done'; } throw new PieformException($message); } else { // Successful submission, and the user doesn't care about replying, so... return; } } else { if (!$submitted) { throw new PieformException('No function registered to handle form submission for form "' . $this->name . '"'); } } } // If we get here, the form was submitted but failed validation // Auto focus the first element with an error if required if ($this->data['autofocus'] !== false) { $this->auto_focus_first_error(); } // Call the user-defined PHP error function, if it exists $function = $this->data['errorcallback']; if (is_callable($function)) { call_user_func_array($function, array($this)); } // If the form has been submitted by javascript, return json if ($this->submitted_by_js) { // TODO: get error messages in a 'third person' type form to // use here maybe? Would have to work for non js forms too. See // the TODO file //$errors = $this->get_errors(); //$json = array(); //foreach ($errors as $element) { // $json[$element['name']] = $element['error']; //} $message = $this->get_property('jserrormessage'); $this->json_reply(PIEFORM_ERR, array('message' => $message)); } } }