* @license PHP License * @package WB * @subpackage content */ /** * Generic Form Processor Class * * There are many classes derived from this one * * @version 0.6.3 * @package WB * @subpackage content */ class WBFormProcessor extends WBStdClass { /** * whether to use template engine or not * @var bool */ protected $withFormTmpl = false; /** * automatically parse template and return HTML * @var bool */ protected $parseFormTmpl = true; /** * Template's name - allow sub-classes to override this value * @var string */ protected $formTmplName = 'snippet'; /** * * @var WBFormProcessor_Fiddler */ private $formFiddler = array(); /** * Add Fiddler * * @param WBFormProcessor_Fiddler */ public function addFiddler($object) { $this->formFiddler[] = $object; } /** * Configure All Form Fiddlers * * @param array */ public function configureFiddler($config) { foreach ($this->formFiddler as $ff) { $ff->configure($config); } } /** * location of form config * * Return sub directory where form element definitions are located * * @return string folder */ protected function getFormConfigDir() { return '.'; } /** * Fetch list of form elements * * The standard behaviour loads form defintions from XML config * file located in form config dir {@link getFormConfigDir() } * Overwrite this to implement your own behaviour. * * @param string name of the xml- and template-filename * @return array $elements */ protected function getFormElementList($name) { // standard behaviour /** @var WBConfig */ $config = WBClass::create('WBConfig'); $config->load($this->getFormConfigDir(). '/' . $name); // load elements $elements = $config->get(array('form', 'elements')); if (is_array($elements)) { return $elements; } // it has to be an array! if (!empty($elements)) { return array(); } // for backward compatibility $elements = $config->get(array('form')); if (is_array($elements)) { return $elements; } return array(); } /** * Fetch list of beforehand actions * * @see getFormElementList() * @param string name of the xml filename * @return array $rules */ protected function getFormBeforehandList($name) { // standard behaviour $config = WBClass::create('WBConfig'); $config->load($this->getFormConfigDir(). '/' . $name , 'xml'); // load rules $bh = $config->get(array('form', 'beforehand')); if( is_array($bh)) { return $bh; } return array(); } /** * Fetch list of form rules * * The standard behaviour loads rules from XML form defintions * file located in form config dir {@link getFormConfigDir() } * Overwrite this to implement your own behaviour. * * @see getFormElementList() * @param string name of the xml filename * @return array $rules */ protected function getFormRuleList($name) { // standard behaviour $config = WBClass::create('WBConfig'); $config->load($this->getFormConfigDir(). '/' . $name , 'xml'); // load rules $rules = $config->get(array('form', 'rules')); if( is_array($rules)) { return $rules; } return array(); } /** * merge form parameters * * This allows to overwrite default form parameters * * @param array $param list of form parameters * @return bool */ protected function mergeFormParameters(&$params) { return true; } /** * Get Name To Build Callback Method Call * * Use name to call on${name}Valid() or on${name}Invalid() * * @param string * @return string */ protected function getCallBackMethodName($name) { return implode('', array_map('ucfirst', explode('/', $name))); } /** * Create a brand new form object * * Loaded form defintion, create the form object and inserts rules. * * @param string $name of the xml- and template-filename * @param array preset form values * @return patForms $form * @see getFormElementList() * @see insertFormRules() */ final public function getForm($name, $values = array()) { $search = $this->mkSearchReplace($values); $elements = $this->getFormElementList($name); foreach ($this->formFiddler as $ff) { $ff->mangleFormElementList($name, $values, $elements); } $this->populateWithValues($elements, $search); $rules = $this->getFormRuleList($name); $this->populateWithValues($rules, $search); $formParams = array( 'action' => '[[SELF]][[PATH]]', 'renderer' => 'Array', 'elements' => &$elements, 'rules' => &$rules, 'values' => $values ); $this->mergeFormParameters($formParams); $beforehand = $this->getFormBeforehandList($name); $this->executeBeforehandCommand($beforehand); $form = WBClass::Create('patForms', $formParams); $this->insertFormRules($form, $name, $elements); return $form; } /** * build search and replace pattern * * Use list of values to build common pattern for replacing placeholers. * * Return value is an array: * $search = array( * 0 => array( * {PATTERN}... * ) * 1 => array( * replacement... * ) *); * * @param array $values * @return array */ private function mkSearchReplace($values) { if (empty($values)) { return array(); } $search = array(array(), array()); foreach ($values as $k => $v) { if (!is_scalar($v)) { continue; } $search[0][] = '{' . strtoupper($k) . '}'; $search[1][] = $v; } return $search; } /** * populate data with replacements * * Walk through data and apply replacment. Recursive method! * * @see mkSearchReplace() * @param array $data * @param array $search */ private function populateWithValues(&$data, $search) { if (empty($search) || empty($data)) { return; } foreach ($data as &$d) { if (is_array($d)) { $this->populateWithValues($d, $search); continue; } $d = str_replace($search[0],$search[1], $d); } } /** * Execute Beforehand Commands * * @see WBFormProcessor_Beforehand_Command * @param array $list */ private function executeBeforehandCommand($list) { foreach ($list as $l) { if (!isset($l['name']) || empty($l['name'])) { continue; } $cmd = WBClass::create('WBFormProcessor_Beforehand_Command_' . $l['name']); if (is_array($l['params'])) { $cmd->setParams($l['params']); } $cmd->execute(); } } /** * Add form rules to just created form * * This is just an emtpy function meant to bo overwriten by sub classes * * @param patForms $form object * @param string name of the xml- and template-filename * @param array $elements list of elements current form * @return bool true on success */ protected function insertFormRules(&$form, $name, $elements) { return true; } /** * Gerneric form processing helper function * * This makes form processing a lot easier and more unified. Usually, * you just call @link processForm() to receive and display the returned HTML. * * Still, there are two hooks where you can add you code: "onXXXValid" and * "onInvalid". Both "events" try to find a method named accodingly and fire * the function call. The function name rendered out of the event's name * (either "onXXXValidated" or "onXXXInvalid") where XXX is replaced with the * form's name * * * function doSomething * { * return $this->processForm( 'login' ); * } * * function onLoginValidated( $form, $values ) * { * // do login stuff here * return true; * } * * * In there is no "onXXXValid" or "onXXXInvalid" function, standard * behaviour applies * * In case there is "onXXXValid" or "onXXXInvalid" (or both) the return value * controlls whether normal processing is continued * * @param string $name named form to prrocess * @param array $values form values to pre-set * @param string $saveParam name of POST / GET parameter that triggers "save" * @param bool $always - always process form, ignore save parameter, "false" * @return mixed string HTML in case template engine will be used, or bool * @see getForm(); */ public function processForm($name, $values = array(), $saveParam = 'save', $always = false) { if (!is_array($values)) { $values = array(); } // process form $form = $this->getForm($name, $values); $req = WBClass::create('WBRequest'); if ($always) { $save = true; } else { $save = $req->get($saveParam); } if ($save || empty($values)) { $form->setValues($values); $keys = array_keys($values); $values = $req->export(); foreach ($keys as $k) { if (!isset($values[$k])) { $values[$k] = ''; } } } if (!$save) { // pre-fill form even if (!empty($values)) { $form->setValues($values, true); } if (!$this->withFormTmpl) { return true; } $this->loadTemplates($name); if (isset($values['id'])) { $this->tmpl->addGlobalVar('id', $values['id']); } $this->renderForm($form); if ($this->parseFormTmpl) { return $this->tmpl->getParsedTemplate($this->formTmplName); } else { return true; } } // fake post parameter $_POST = $values; $form->setSubmitted(true); $function = $this->getCallBackMethodName($name); if (!$form->validateForm()) { $method = sprintf('on%sInvalid', $function); if (method_exists($this, $method)) { if (!call_user_func(array($this, $method), $form)) { return false; } } if (!$this->withFormTmpl) { return false; } $this->loadTemplates($name); if (isset($values['id'])) { $this->tmpl->addGlobalVar('id', $values['id']); } $this->renderFormError($form); $this->renderForm($form); if ($this->parseFormTmpl) { return $this->tmpl->getParsedTemplate($this->formTmplName); } else { return false; } } $method = sprintf('on%sValid', $function); if (method_exists( $this, $method)) { $values = $form->getValues(); if (!call_user_func(array($this, $method), $form, $values)) { return true; } } if (!$this->withFormTmpl) { return true; } $this->loadTemplates($name . 'Valid'); if (isset($values['id'])) { $this->tmpl->addGlobalVar('id', $values['id']); } $this->addValues2GlobalVars($values); if ($this->parseFormTmpl) { return $this->tmpl->getParsedTemplate($this->formTmplName); } return true; } protected function addValues2GlobalVars($values, $prefix = 'form_values_') { $tmp = array(); foreach ($values as $k => $v) { if (!is_array($v)) { $tmp[$k . '_value_current'] = $v; } else { foreach ($v as $i) { $tmp[$k .'_value_' . $i . '_checked'] = 'checked="checked"'; } } } $this->tmpl->addGlobalVars($tmp, $prefix); } /** * Add form to template engine * * @param patForms $form * @param array $values * @return bool true */ protected function renderForm($form, $values = array()) { $elements = $form->renderForm(); $values = $form->getValues(); // add basic form tags $this->tmpl->addGlobalVar('form_timestamp', time()); $this->tmpl->addGlobalVar('form_start', $form->serializeStart()); $this->tmpl->addGlobalVar('form_end', $form->serializeEnd()); $this->tmpl->addGlobalVar('form_id', $form->getAttribute('id')); $this->tmpl->addGlobalVar('form_class', $form->getAttribute('class')); // add all form elements foreach ($elements as $e) { $name = $e['name']; $data = array(); // add value if (!empty($values[$name])) { if (is_scalar($values[$name])) { $data['value_current'] = $values[$name]; } else { foreach ($values[$name] as $v) { $data['value_' . $v . '_checked'] = 'checked="checked"'; } } } // add attributes foreach ($e as $k => $v) { if (is_scalar($v)) { $data[$k] = $v; } } $this->tmpl->addGlobalVars($data, $name . '_'); } return true; } /** * put form errors to template * * @param object $form * @return bool true on success */ protected function renderFormError($form) { $errors = $form->getValidationErrors(); // load error template if (!$this->tmpl->exists('form_error')) { $this->tmpl->readTemplatesFromInput('formError.tmpl'); } // convert errors to simple list $list = array(); // render each form validation error if (!is_array($errors)) { $errors = array(); } foreach ($errors as $fieldname => $errs) { if (empty($errs)) { continue; } $l = array(); $field = $form->getElement($fieldname); $atts = $field->getAttributes(); if (!isset($atts['class'])) { $atts['class'] = ''; } foreach ($atts as $att => $value) { if (is_scalar($value)) { $l['field_' . $att] = $value; } } if ($fieldname == '__form') { $l['error_type'] = 'form'; } else { $l['error_type'] = 'field'; $field->setAttribute('class', $atts['class'] . ' is-invalid'); $this->tmpl->addGlobalVar($fieldname . '_status', 'wb-is-invalid'); } foreach ($errs as $err) { $l['error_code'] = $err['code']; $l['error_element'] = $err['element']; $l['error_message'] = $err['message']; $list[] = $l; } } if ($this->tmpl->exists('form_error_entry')) { $this->tmpl->addRows('form_error_entry', $list); } $tmp = $this->tmpl->getParsedTemplate('form_error'); $this->tmpl->addGlobalVar('form_error', $tmp); return true; } /** * Add CAPTCHA Form Element * * Create config for form element CAPTCHA. Add classname from other elements * * @param array $list List of other form elements * @param bool authentication status (true for logged in users) * @param string mode CAPTCHA mode like auto, 1 or 0 */ protected function injectElement4Captcha(&$list, $auth = false, $mode = 'auto') { /** @var WBUte_Captcha */ $mc = WBClass::create('WBUte_Captcha'); $mc->setAuth($auth); $mc->addRequest(); if (!$mc->isRequired($mode)) { return; } $config = WBClass::create('WBConfig'); if ($config->load('captcha', true)) { $att = $config->get('image/form/element/attributes', array()); if (!empty($att)) { $stripTags = array('title', 'label', 'placeholder'); foreach ($att as $n => &$a) { if (in_array($n, $stripTags)) { $a = strip_tags($a); } } $list['captcha'] = array( 'type' => 'String', 'attributes' => $att, 'rule' => array('Captcha') ); return; } } $clazz = 'captcha'; foreach ($list as $f => $p) { if (!isset($p['attributes']['class']) || empty($p['attributes']['class'])) { continue; } $clazz = $p['attributes']['class']; break; } $list['captcha'] = array( 'type' => 'String', 'attributes' => array( 'title' => strip_tags(patI18n::dgettext('wombat', 'Are you human?')), 'label' => strip_tags(patI18n::dgettext('wombat', 'CAPTCHA')), 'description' => patI18n::dgettext('wombat', 'Please type in what the picture shows.'), 'placeholder' => strip_tags(patI18n::dgettext('wombat', 'Can you read this?')), 'class' => $clazz, 'minlength' => 3, 'maxlength' => 255, ), 'rule' => array('Captcha') ); } }