array(), 'minute' => array(), 'hour' => array(), 'month' => array(), 'year' => array(), 'meridian' => array() ); /** * List of fields created, used with secure forms. * * @var array */ public $fields = array(); /** * Constant used internally to skip the securing process, * and neither add the field to the hash or to the unlocked fields. * * @var string */ const SECURE_SKIP = 'skip'; /** * Defines the type of form being created. Set by FormHelper::create(). * * @var string */ public $requestType = null; /** * The default model being used for the current form. * * @var string */ public $defaultModel = null; /** * Persistent default options used by input(). Set by FormHelper::create(). * * @var array */ protected $_inputDefaults = array(); /** * An array of field names that have been excluded from * the Token hash used by SecurityComponent's validatePost method * * @see FormHelper::_secure() * @see SecurityComponent::validatePost() * @var array */ protected $_unlockedFields = array(); /** * Holds the model references already loaded by this helper * product of trying to inspect them out of field names * * @var array */ protected $_models = array(); /** * Holds all the validation errors for models loaded and inspected * it can also be set manually to be able to display custom error messages * in the any of the input fields generated by this helper * * @var array */ public $validationErrors = array(); /** * Holds already used DOM ID suffixes to avoid collisions with multiple form field elements. * * @var array */ protected $_domIdSuffixes = array(); /** * Copies the validationErrors variable from the View object into this instance * * @param View $View The View this helper is being attached to. * @param array $settings Configuration settings for the helper. */ public function __construct(View $View, $settings = array()) { parent::__construct($View, $settings); $this->validationErrors =& $View->validationErrors; } /** * Guess the location for a model based on its name and tries to create a new instance * or get an already created instance of the model * * @param string $model * @return Model model instance */ protected function _getModel($model) { $object = null; if (!$model || $model === 'Model') { return $object; } if (array_key_exists($model, $this->_models)) { return $this->_models[$model]; } if (ClassRegistry::isKeySet($model)) { $object = ClassRegistry::getObject($model); } elseif (isset($this->request->params['models'][$model])) { $plugin = $this->request->params['models'][$model]['plugin']; $plugin .= ($plugin) ? '.' : null; $object = ClassRegistry::init(array( 'class' => $plugin . $this->request->params['models'][$model]['className'], 'alias' => $model )); } elseif (ClassRegistry::isKeySet($this->defaultModel)) { $defaultObject = ClassRegistry::getObject($this->defaultModel); if ($defaultObject && in_array($model, array_keys($defaultObject->getAssociated()), true) && isset($defaultObject->{$model})) { $object = $defaultObject->{$model}; } } else { $object = ClassRegistry::init($model, true); } $this->_models[$model] = $object; if (!$object) { return null; } $this->fieldset[$model] = array('fields' => null, 'key' => $object->primaryKey, 'validates' => null); return $object; } /** * Inspects the model properties to extract information from them. * Currently it can extract information from the the fields, the primary key and required fields * * The $key parameter accepts the following list of values: * * - key: Returns the name of the primary key for the model * - fields: Returns the model schema * - validates: returns the list of fields that are required * - errors: returns the list of validation errors * * If the $field parameter is passed if will return the information for that sole field. * * `$this->_introspectModel('Post', 'fields', 'title');` will return the schema information for title column * * @param string $model name of the model to extract information from * @param string $key name of the special information key to obtain (key, fields, validates, errors) * @param string $field name of the model field to get information from * @return mixed information extracted for the special key and field in a model */ protected function _introspectModel($model, $key, $field = null) { $object = $this->_getModel($model); if (!$object) { return; } if ($key === 'key') { return $this->fieldset[$model]['key'] = $object->primaryKey; } if ($key === 'fields') { if (!isset($this->fieldset[$model]['fields'])) { $this->fieldset[$model]['fields'] = $object->schema(); foreach ($object->hasAndBelongsToMany as $alias => $assocData) { $this->fieldset[$object->alias]['fields'][$alias] = array('type' => 'multiple'); } } if ($field === null || $field === false) { return $this->fieldset[$model]['fields']; } elseif (isset($this->fieldset[$model]['fields'][$field])) { return $this->fieldset[$model]['fields'][$field]; } return isset($object->hasAndBelongsToMany[$field]) ? array('type' => 'multiple') : null; } if ($key === 'errors' && !isset($this->validationErrors[$model])) { $this->validationErrors[$model] =& $object->validationErrors; return $this->validationErrors[$model]; } elseif ($key === 'errors' && isset($this->validationErrors[$model])) { return $this->validationErrors[$model]; } if ($key === 'validates' && !isset($this->fieldset[$model]['validates'])) { $validates = array(); foreach ($object->validator() as $validateField => $validateProperties) { if ($this->_isRequiredField($validateProperties)) { $validates[$validateField] = true; } } $this->fieldset[$model]['validates'] = $validates; } if ($key === 'validates') { if (empty($field)) { return $this->fieldset[$model]['validates']; } return isset($this->fieldset[$model]['validates'][$field]) ? $this->fieldset[$model]['validates'] : null; } } /** * Returns if a field is required to be filled based on validation properties from the validating object. * * @param CakeValidationSet $validationRules * @return boolean true if field is required to be filled, false otherwise */ protected function _isRequiredField($validationRules) { if (empty($validationRules) || count($validationRules) === 0) { return false; } $isUpdate = $this->requestType === 'put'; foreach ($validationRules as $rule) { $rule->isUpdate($isUpdate); if ($rule->skip()) { continue; } return !$rule->allowEmpty; } return false; } /** * Returns false if given form field described by the current entity has no errors. * Otherwise it returns the validation message * * @return mixed Either false when there are no errors, or an array of error * strings. An error string could be ''. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::tagIsInvalid */ public function tagIsInvalid() { $entity = $this->entity(); $model = array_shift($entity); // 0.Model.field. Fudge entity path if (empty($model) || is_numeric($model)) { array_splice($entity, 1, 0, $model); $model = array_shift($entity); } $errors = array(); if (!empty($entity) && isset($this->validationErrors[$model])) { $errors = $this->validationErrors[$model]; } if (!empty($entity) && empty($errors)) { $errors = $this->_introspectModel($model, 'errors'); } if (empty($errors)) { return false; } $errors = Hash::get($errors, implode('.', $entity)); return $errors === null ? false : $errors; } /** * Returns an HTML FORM element. * * ### Options: * * - `type` Form method defaults to POST * - `action` The controller action the form submits to, (optional). * - `url` The URL the form submits to. Can be a string or a URL array. If you use 'url' * you should leave 'action' undefined. * - `default` Allows for the creation of Ajax forms. Set this to false to prevent the default event handler. * Will create an onsubmit attribute if it doesn't not exist. If it does, default action suppression * will be appended. * - `onsubmit` Used in conjunction with 'default' to create ajax forms. * - `inputDefaults` set the default $options for FormHelper::input(). Any options that would * be set when using FormHelper::input() can be set here. Options set with `inputDefaults` * can be overridden when calling input() * - `encoding` Set the accept-charset encoding for the form. Defaults to `Configure::read('App.encoding')` * * @param mixed $model The model name for which the form is being defined. Should * include the plugin name for plugin models. e.g. `ContactManager.Contact`. * If an array is passed and $options argument is empty, the array will be used as options. * If `false` no model is used. * @param array $options An array of html attributes and options. * @return string An formatted opening FORM tag. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#options-for-create */ public function create($model = null, $options = array()) { $created = $id = false; $append = ''; if (is_array($model) && empty($options)) { $options = $model; $model = null; } if (empty($model) && $model !== false && !empty($this->request->params['models'])) { $model = key($this->request->params['models']); } elseif (empty($model) && empty($this->request->params['models'])) { $model = false; } $this->defaultModel = $model; $key = null; if ($model !== false) { list($plugin, $model) = pluginSplit($model, true); $key = $this->_introspectModel($plugin . $model, 'key'); $this->setEntity($model, true); } if ($model !== false && $key) { $recordExists = ( isset($this->request->data[$model]) && !empty($this->request->data[$model][$key]) && !is_array($this->request->data[$model][$key]) ); if ($recordExists) { $created = true; $id = $this->request->data[$model][$key]; } } $options = array_merge(array( 'type' => ($created && empty($options['action'])) ? 'put' : 'post', 'action' => null, 'url' => null, 'default' => true, 'encoding' => strtolower(Configure::read('App.encoding')), 'inputDefaults' => array()), $options); $this->inputDefaults($options['inputDefaults']); unset($options['inputDefaults']); if (!isset($options['id'])) { $domId = isset($options['action']) ? $options['action'] : $this->request['action']; $options['id'] = $this->domId($domId . 'Form'); } if ($options['action'] === null && $options['url'] === null) { $options['action'] = $this->request->here(false); } elseif (empty($options['url']) || is_array($options['url'])) { if (empty($options['url']['controller'])) { if (!empty($model)) { $options['url']['controller'] = Inflector::underscore(Inflector::pluralize($model)); } elseif (!empty($this->request->params['controller'])) { $options['url']['controller'] = Inflector::underscore($this->request->params['controller']); } } if (empty($options['action'])) { $options['action'] = $this->request->params['action']; } $plugin = null; if ($this->plugin) { $plugin = Inflector::underscore($this->plugin); } $actionDefaults = array( 'plugin' => $plugin, 'controller' => $this->_View->viewPath, 'action' => $options['action'], ); $options['action'] = array_merge($actionDefaults, (array)$options['url']); if (empty($options['action'][0]) && !empty($id)) { $options['action'][0] = $id; } } elseif (is_string($options['url'])) { $options['action'] = $options['url']; } unset($options['url']); switch (strtolower($options['type'])) { case 'get': $htmlAttributes['method'] = 'get'; break; case 'file': $htmlAttributes['enctype'] = 'multipart/form-data'; $options['type'] = ($created) ? 'put' : 'post'; case 'post': case 'put': case 'delete': $append .= $this->hidden('_method', array( 'name' => '_method', 'value' => strtoupper($options['type']), 'id' => null, 'secure' => self::SECURE_SKIP )); default: $htmlAttributes['method'] = 'post'; } $this->requestType = strtolower($options['type']); $action = $this->url($options['action']); unset($options['type'], $options['action']); if (!$options['default']) { if (!isset($options['onsubmit'])) { $options['onsubmit'] = ''; } $htmlAttributes['onsubmit'] = $options['onsubmit'] . 'event.returnValue = false; return false;'; } unset($options['default']); if (!empty($options['encoding'])) { $htmlAttributes['accept-charset'] = $options['encoding']; unset($options['encoding']); } $htmlAttributes = array_merge($options, $htmlAttributes); $this->fields = array(); if ($this->requestType !== 'get') { $append .= $this->_csrfField(); } if (!empty($append)) { $append = $this->Html->useTag('hiddenblock', $append); } if ($model !== false) { $this->setEntity($model, true); $this->_introspectModel($model, 'fields'); } return $this->Html->useTag('form', $action, $htmlAttributes) . $append; } /** * Return a CSRF input if the _Token is present. * Used to secure forms in conjunction with SecurityComponent * * @return string */ protected function _csrfField() { if (empty($this->request->params['_Token'])) { return ''; } if (!empty($this->request['_Token']['unlockedFields'])) { foreach ((array)$this->request['_Token']['unlockedFields'] as $unlocked) { $this->_unlockedFields[] = $unlocked; } } return $this->hidden('_Token.key', array( 'value' => $this->request->params['_Token']['key'], 'id' => 'Token' . mt_rand(), 'secure' => self::SECURE_SKIP )); } /** * Closes an HTML form, cleans up values set by FormHelper::create(), and writes hidden * input fields where appropriate. * * If $options is set a form submit button will be created. Options can be either a string or an array. * * {{{ * array usage: * * array('label' => 'save'); value="save" * array('label' => 'save', 'name' => 'Whatever'); value="save" name="Whatever" * array('name' => 'Whatever'); value="Submit" name="Whatever" * array('label' => 'save', 'name' => 'Whatever', 'div' => 'good')
value="save" name="Whatever" * array('label' => 'save', 'name' => 'Whatever', 'div' => array('class' => 'good'));
value="save" name="Whatever" * }}} * * If $secureAttributes is set, these html attributes will be merged into the hidden input tags generated for the * Security Component. This is especially useful to set HTML5 attributes like 'form' * * @param string|array $options as a string will use $options as the value of button, * @param array $secureAttributes will be passed as html attributes into the hidden input elements generated for the * Security Component. * @return string a closing FORM tag optional submit button. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#closing-the-form */ public function end($options = null, $secureAttributes = array()) { $out = null; $submit = null; if ($options !== null) { $submitOptions = array(); if (is_string($options)) { $submit = $options; } else { if (isset($options['label'])) { $submit = $options['label']; unset($options['label']); } $submitOptions = $options; } $out .= $this->submit($submit, $submitOptions); } if ( $this->requestType !== 'get' && isset($this->request['_Token']) && !empty($this->request['_Token']) ) { $out .= $this->secure($this->fields, $secureAttributes); $this->fields = array(); } $this->setEntity(null); $out .= $this->Html->useTag('formend'); $this->_View->modelScope = false; $this->requestType = null; return $out; } /** * Generates a hidden field with a security hash based on the fields used in * the form. * * If $secureAttributes is set, these html attributes will be merged into * the hidden input tags generated for the Security Component. This is * especially useful to set HTML5 attributes like 'form'. * * @param array|null $fields If set specifies the list of fields to use when * generating the hash, else $this->fields is being used. * @param array $secureAttributes will be passed as html attributes into the hidden * input elements generated for the Security Component. * @return string A hidden input field with a security hash * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::secure */ public function secure($fields = array(), $secureAttributes = array()) { if (!isset($this->request['_Token']) || empty($this->request['_Token'])) { return; } $locked = array(); $unlockedFields = $this->_unlockedFields; foreach ($fields as $key => $value) { if (!is_int($key)) { $locked[$key] = $value; unset($fields[$key]); } } sort($unlockedFields, SORT_STRING); sort($fields, SORT_STRING); ksort($locked, SORT_STRING); $fields += $locked; $locked = implode(array_keys($locked), '|'); $unlocked = implode($unlockedFields, '|'); $fields = Security::hash(serialize($fields) . $unlocked . Configure::read('Security.salt'), 'sha1'); $tokenFields = array_merge($secureAttributes, array( 'value' => urlencode($fields . ':' . $locked), 'id' => 'TokenFields' . mt_rand(), )); $out = $this->hidden('_Token.fields', $tokenFields); $tokenUnlocked = array_merge($secureAttributes, array( 'value' => urlencode($unlocked), 'id' => 'TokenUnlocked' . mt_rand(), )); $out .= $this->hidden('_Token.unlocked', $tokenUnlocked); return $this->Html->useTag('hiddenblock', $out); } /** * Add to or get the list of fields that are currently unlocked. * Unlocked fields are not included in the field hash used by SecurityComponent * unlocking a field once its been added to the list of secured fields will remove * it from the list of fields. * * @param string $name The dot separated name for the field. * @return mixed Either null, or the list of fields. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::unlockField */ public function unlockField($name = null) { if ($name === null) { return $this->_unlockedFields; } if (!in_array($name, $this->_unlockedFields)) { $this->_unlockedFields[] = $name; } $index = array_search($name, $this->fields); if ($index !== false) { unset($this->fields[$index]); } unset($this->fields[$name]); } /** * Determine which fields of a form should be used for hash. * Populates $this->fields * * @param boolean $lock Whether this field should be part of the validation * or excluded as part of the unlockedFields. * @param string $field Reference to field to be secured. Should be dot separated to indicate nesting. * @param mixed $value Field value, if value should not be tampered with. * @return mixed|null Not used yet */ protected function _secure($lock, $field = null, $value = null) { if (!$field) { $field = $this->entity(); } elseif (is_string($field)) { $field = Hash::filter(explode('.', $field)); } foreach ($this->_unlockedFields as $unlockField) { $unlockParts = explode('.', $unlockField); if (array_values(array_intersect($field, $unlockParts)) === $unlockParts) { return; } } $field = implode('.', $field); $field = preg_replace('/(\.\d+)+$/', '', $field); if ($lock) { if (!in_array($field, $this->fields)) { if ($value !== null) { return $this->fields[$field] = $value; } $this->fields[] = $field; } } else { $this->unlockField($field); } } /** * Returns true if there is an error for the given field, otherwise false * * @param string $field This should be "Modelname.fieldname" * @return boolean If there are errors this method returns true, else false. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::isFieldError */ public function isFieldError($field) { $this->setEntity($field); return (bool)$this->tagIsInvalid(); } /** * Returns a formatted error message for given FORM field, NULL if no errors. * * ### Options: * * - `escape` boolean - Whether or not to html escape the contents of the error. * - `wrap` mixed - Whether or not the error message should be wrapped in a div. If a * string, will be used as the HTML tag to use. * - `class` string - The class name for the error message * * @param string $field A field name, like "Modelname.fieldname" * @param string|array $text Error message as string or array of messages. * If array contains `attributes` key it will be used as options for error container * @param array $options Rendering options for
wrapper tag * @return string If there are errors this method returns an error message, otherwise null. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::error */ public function error($field, $text = null, $options = array()) { $defaults = array('wrap' => true, 'class' => 'error-message', 'escape' => true); $options = array_merge($defaults, $options); $this->setEntity($field); $error = $this->tagIsInvalid(); if ($error === false) { return null; } if (is_array($text)) { if (isset($text['attributes']) && is_array($text['attributes'])) { $options = array_merge($options, $text['attributes']); unset($text['attributes']); } $tmp = array(); foreach ($error as &$e) { if (isset($text[$e])) { $tmp[] = $text[$e]; } else { $tmp[] = $e; } } $text = $tmp; } if ($text !== null) { $error = $text; } if (is_array($error)) { foreach ($error as &$e) { if (is_numeric($e)) { $e = __d('cake', 'Error in field %s', Inflector::humanize($this->field())); } } } if ($options['escape']) { $error = h($error); unset($options['escape']); } if (is_array($error)) { if (count($error) > 1) { $listParams = array(); if (isset($options['listOptions'])) { if (is_string($options['listOptions'])) { $listParams[] = $options['listOptions']; } else { if (isset($options['listOptions']['itemOptions'])) { $listParams[] = $options['listOptions']['itemOptions']; unset($options['listOptions']['itemOptions']); } else { $listParams[] = array(); } if (isset($options['listOptions']['tag'])) { $listParams[] = $options['listOptions']['tag']; unset($options['listOptions']['tag']); } array_unshift($listParams, $options['listOptions']); } unset($options['listOptions']); } array_unshift($listParams, $error); $error = call_user_func_array(array($this->Html, 'nestedList'), $listParams); } else { $error = array_pop($error); } } if ($options['wrap']) { $tag = is_string($options['wrap']) ? $options['wrap'] : 'div'; unset($options['wrap']); return $this->Html->tag($tag, $error, $options); } return $error; } /** * Returns a formatted LABEL element for HTML FORMs. Will automatically generate * a `for` attribute if one is not provided. * * ### Options * * - `for` - Set the for attribute, if its not defined the for attribute * will be generated from the $fieldName parameter using * FormHelper::domId(). * * Examples: * * The text and for attribute are generated off of the fieldname * * {{{ * echo $this->Form->label('Post.published'); * * }}} * * Custom text: * * {{{ * echo $this->Form->label('Post.published', 'Publish'); * * }}} * * Custom class name: * * {{{ * echo $this->Form->label('Post.published', 'Publish', 'required'); * * }}} * * Custom attributes: * * {{{ * echo $this->Form->label('Post.published', 'Publish', array( * 'for' => 'post-publish' * )); * * }}} * * @param string $fieldName This should be "Modelname.fieldname" * @param string $text Text that will appear in the label field. If * $text is left undefined the text will be inflected from the * fieldName. * @param array|string $options An array of HTML attributes, or a string, to be used as a class name. * @return string The formatted LABEL element * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::label */ public function label($fieldName = null, $text = null, $options = array()) { if ($fieldName === null) { $fieldName = implode('.', $this->entity()); } if ($text === null) { if (strpos($fieldName, '.') !== false) { $fieldElements = explode('.', $fieldName); $text = array_pop($fieldElements); } else { $text = $fieldName; } if (substr($text, -3) === '_id') { $text = substr($text, 0, -3); } $text = __(Inflector::humanize(Inflector::underscore($text))); } if (is_string($options)) { $options = array('class' => $options); } if (isset($options['for'])) { $labelFor = $options['for']; unset($options['for']); } else { $labelFor = $this->domId($fieldName); } return $this->Html->useTag('label', $labelFor, $options, $text); } /** * Generate a set of inputs for `$fields`. If $fields is null the fields of current model * will be used. * * You can customize individual inputs through `$fields`. * {{{ * $this->Form->inputs(array( * 'name' => array('label' => 'custom label') * )); * }}} * * In addition to controller fields output, `$fields` can be used to control legend * and fieldset rendering. * `$this->Form->inputs('My legend');` Would generate an input set with a custom legend. * Passing `fieldset` and `legend` key in `$fields` array has been deprecated since 2.3, * for more fine grained control use the `fieldset` and `legend` keys in `$options` param. * * @param array $fields An array of fields to generate inputs for, or null. * @param array $blacklist A simple array of fields to not create inputs for. * @param array $options Options array. Valid keys are: * - `fieldset` Set to false to disable the fieldset. If a string is supplied it will be used as * the class name for the fieldset element. * - `legend` Set to false to disable the legend for the generated input set. Or supply a string * to customize the legend text. * @return string Completed form inputs. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::inputs */ public function inputs($fields = null, $blacklist = null, $options = array()) { $fieldset = $legend = true; $modelFields = array(); $model = $this->model(); if ($model) { $modelFields = array_keys((array)$this->_introspectModel($model, 'fields')); } if (is_array($fields)) { if (array_key_exists('legend', $fields) && !in_array('legend', $modelFields)) { $legend = $fields['legend']; unset($fields['legend']); } if (isset($fields['fieldset']) && !in_array('fieldset', $modelFields)) { $fieldset = $fields['fieldset']; unset($fields['fieldset']); } } elseif ($fields !== null) { $fieldset = $legend = $fields; if (!is_bool($fieldset)) { $fieldset = true; } $fields = array(); } if (isset($options['legend'])) { $legend = $options['legend']; } if (isset($options['fieldset'])) { $fieldset = $options['fieldset']; } if (empty($fields)) { $fields = $modelFields; } if ($legend === true) { $actionName = __d('cake', 'New %s'); $isEdit = ( strpos($this->request->params['action'], 'update') !== false || strpos($this->request->params['action'], 'edit') !== false ); if ($isEdit) { $actionName = __d('cake', 'Edit %s'); } $modelName = Inflector::humanize(Inflector::underscore($model)); $legend = sprintf($actionName, __($modelName)); } $out = null; foreach ($fields as $name => $options) { if (is_numeric($name) && !is_array($options)) { $name = $options; $options = array(); } $entity = explode('.', $name); $blacklisted = ( is_array($blacklist) && (in_array($name, $blacklist) || in_array(end($entity), $blacklist)) ); if ($blacklisted) { continue; } $out .= $this->input($name, $options); } if (is_string($fieldset)) { $fieldsetClass = sprintf(' class="%s"', $fieldset); } else { $fieldsetClass = ''; } if ($fieldset) { if ($legend) { $out = $this->Html->useTag('legend', $legend) . $out; } $out = $this->Html->useTag('fieldset', $fieldsetClass, $out); } return $out; } /** * Generates a form input element complete with label and wrapper div * * ### Options * * See each field type method for more information. Any options that are part of * $attributes or $options for the different **type** methods can be included in `$options` for input().i * Additionally, any unknown keys that are not in the list below, or part of the selected type's options * will be treated as a regular html attribute for the generated input. * * - `type` - Force the type of widget you want. e.g. `type => 'select'` * - `label` - Either a string label, or an array of options for the label. See FormHelper::label(). * - `div` - Either `false` to disable the div, or an array of options for the div. * See HtmlHelper::div() for more options. * - `options` - For widgets that take options e.g. radio, select. * - `error` - Control the error message that is produced. Set to `false` to disable any kind of error reporting (field * error and error messages). * - `errorMessage` - Boolean to control rendering error messages (field error will still occur). * - `empty` - String or boolean to enable empty select box options. * - `before` - Content to place before the label + input. * - `after` - Content to place after the label + input. * - `between` - Content to place between the label + input. * - `format` - Format template for element order. Any element that is not in the array, will not be in the output. * - Default input format order: array('before', 'label', 'between', 'input', 'after', 'error') * - Default checkbox format order: array('before', 'input', 'between', 'label', 'after', 'error') * - Hidden input will not be formatted * - Radio buttons cannot have the order of input and label elements controlled with these settings. * * @param string $fieldName This should be "Modelname.fieldname" * @param array $options Each type of input takes different options. * @return string Completed form widget. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#creating-form-elements */ public function input($fieldName, $options = array()) { $this->setEntity($fieldName); $options = $this->_parseOptions($options); $divOptions = $this->_divOptions($options); unset($options['div']); if ($options['type'] === 'radio' && isset($options['options'])) { $radioOptions = (array)$options['options']; unset($options['options']); } $label = $this->_getLabel($fieldName, $options); if ($options['type'] !== 'radio') { unset($options['label']); } $error = $this->_extractOption('error', $options, null); unset($options['error']); $errorMessage = $this->_extractOption('errorMessage', $options, true); unset($options['errorMessage']); $selected = $this->_extractOption('selected', $options, null); unset($options['selected']); if ($options['type'] === 'datetime' || $options['type'] === 'date' || $options['type'] === 'time') { $dateFormat = $this->_extractOption('dateFormat', $options, 'MDY'); $timeFormat = $this->_extractOption('timeFormat', $options, 12); unset($options['dateFormat'], $options['timeFormat']); } $type = $options['type']; $out = array('before' => $options['before'], 'label' => $label, 'between' => $options['between'], 'after' => $options['after']); $format = $this->_getFormat($options); unset($options['type'], $options['before'], $options['between'], $options['after'], $options['format']); $out['error'] = null; if ($type !== 'hidden' && $error !== false) { $errMsg = $this->error($fieldName, $error); if ($errMsg) { $divOptions = $this->addClass($divOptions, 'error'); if ($errorMessage) { $out['error'] = $errMsg; } } } if ($type === 'radio' && isset($out['between'])) { $options['between'] = $out['between']; $out['between'] = null; } $out['input'] = $this->_getInput(compact('type', 'fieldName', 'options', 'radioOptions', 'selected', 'dateFormat', 'timeFormat')); $output = ''; foreach ($format as $element) { $output .= $out[$element]; } if (!empty($divOptions['tag'])) { $tag = $divOptions['tag']; unset($divOptions['tag']); $output = $this->Html->tag($tag, $output, $divOptions); } return $output; } /** * Generates an input element * * @param array $args The options for the input element * @return string The generated input element */ protected function _getInput($args) { extract($args); switch ($type) { case 'hidden': return $this->hidden($fieldName, $options); case 'checkbox': return $this->checkbox($fieldName, $options); case 'radio': return $this->radio($fieldName, $radioOptions, $options); case 'file': return $this->file($fieldName, $options); case 'select': $options += array('options' => array(), 'value' => $selected); $list = $options['options']; unset($options['options']); return $this->select($fieldName, $list, $options); case 'time': $options['value'] = $selected; return $this->dateTime($fieldName, null, $timeFormat, $options); case 'date': $options['value'] = $selected; return $this->dateTime($fieldName, $dateFormat, null, $options); case 'datetime': $options['value'] = $selected; return $this->dateTime($fieldName, $dateFormat, $timeFormat, $options); case 'textarea': return $this->textarea($fieldName, $options + array('cols' => '30', 'rows' => '6')); case 'url': return $this->text($fieldName, array('type' => 'url') + $options); default: return $this->{$type}($fieldName, $options); } } /** * Generates input options array * * @param array $options * @return array Options */ protected function _parseOptions($options) { $options = array_merge( array('before' => null, 'between' => null, 'after' => null, 'format' => null), $this->_inputDefaults, $options ); if (!isset($options['type'])) { $options = $this->_magicOptions($options); } if (in_array($options['type'], array('radio', 'select'))) { $options = $this->_optionsOptions($options); } if (isset($options['rows']) || isset($options['cols'])) { $options['type'] = 'textarea'; } if ($options['type'] === 'datetime' || $options['type'] === 'date' || $options['type'] === 'time' || $options['type'] === 'select') { $options += array('empty' => false); } return $options; } /** * Generates list of options for multiple select * * @param array $options * @return array */ protected function _optionsOptions($options) { if (isset($options['options'])) { return $options; } $varName = Inflector::variable( Inflector::pluralize(preg_replace('/_id$/', '', $this->field())) ); $varOptions = $this->_View->get($varName); if (!is_array($varOptions)) { return $options; } if ($options['type'] !== 'radio') { $options['type'] = 'select'; } $options['options'] = $varOptions; return $options; } /** * Magically set option type and corresponding options * * @param array $options * @return array */ protected function _magicOptions($options) { $modelKey = $this->model(); $fieldKey = $this->field(); $options['type'] = 'text'; if (isset($options['options'])) { $options['type'] = 'select'; } elseif (in_array($fieldKey, array('psword', 'passwd', 'password'))) { $options['type'] = 'password'; } elseif (in_array($fieldKey, array('tel', 'telephone', 'phone'))) { $options['type'] = 'tel'; } elseif ($fieldKey === 'email') { $options['type'] = 'email'; } elseif (isset($options['checked'])) { $options['type'] = 'checkbox'; } elseif ($fieldDef = $this->_introspectModel($modelKey, 'fields', $fieldKey)) { $type = $fieldDef['type']; $primaryKey = $this->fieldset[$modelKey]['key']; $map = array( 'string' => 'text', 'datetime' => 'datetime', 'boolean' => 'checkbox', 'timestamp' => 'datetime', 'text' => 'textarea', 'time' => 'time', 'date' => 'date', 'float' => 'number', 'integer' => 'number', 'decimal' => 'number', 'binary' => 'file' ); if (isset($this->map[$type])) { $options['type'] = $this->map[$type]; } elseif (isset($map[$type])) { $options['type'] = $map[$type]; } if ($fieldKey == $primaryKey) { $options['type'] = 'hidden'; } if ( $options['type'] === 'number' && !isset($options['step']) ) { if ($type === 'decimal') { $decimalPlaces = substr($fieldDef['length'], strpos($fieldDef['length'], ',') + 1); $options['step'] = sprintf('%.' . $decimalPlaces . 'F', pow(10, -1 * $decimalPlaces)); } elseif ($type === 'float') { $options['step'] = 'any'; } } } if (preg_match('/_id$/', $fieldKey) && $options['type'] !== 'hidden') { $options['type'] = 'select'; } if ($modelKey === $fieldKey) { $options['type'] = 'select'; if (!isset($options['multiple'])) { $options['multiple'] = 'multiple'; } } if (in_array($options['type'], array('text', 'number'))) { $options = $this->_optionsOptions($options); } if ($options['type'] === 'select' && array_key_exists('step', $options)) { unset($options['step']); } $options = $this->_maxLength($options); return $options; } /** * Generate format options * * @param array $options * @return array */ protected function _getFormat($options) { if ($options['type'] === 'hidden') { return array('input'); } if (is_array($options['format']) && in_array('input', $options['format'])) { return $options['format']; } if ($options['type'] === 'checkbox') { return array('before', 'input', 'between', 'label', 'after', 'error'); } return array('before', 'label', 'between', 'input', 'after', 'error'); } /** * Generate label for input * * @param string $fieldName * @param array $options * @return boolean|string false or Generated label element */ protected function _getLabel($fieldName, $options) { if ($options['type'] === 'radio') { return false; } $label = null; if (isset($options['label'])) { $label = $options['label']; } if ($label === false) { return false; } return $this->_inputLabel($fieldName, $label, $options); } /** * Calculates maxlength option * * @param array $options * @return array */ protected function _maxLength($options) { $fieldDef = $this->_introspectModel($this->model(), 'fields', $this->field()); $autoLength = ( !array_key_exists('maxlength', $options) && isset($fieldDef['length']) && is_scalar($fieldDef['length']) && $options['type'] !== 'select' ); if ($autoLength && in_array($options['type'], array('text', 'email', 'tel', 'url', 'search')) ) { $options['maxlength'] = $fieldDef['length']; } return $options; } /** * Generate div options for input * * @param array $options * @return array */ protected function _divOptions($options) { if ($options['type'] === 'hidden') { return array(); } $div = $this->_extractOption('div', $options, true); if (!$div) { return array(); } $divOptions = array('class' => 'input'); $divOptions = $this->addClass($divOptions, $options['type']); if (is_string($div)) { $divOptions['class'] = $div; } elseif (is_array($div)) { $divOptions = array_merge($divOptions, $div); } if ( $this->_extractOption('required', $options) !== false && $this->_introspectModel($this->model(), 'validates', $this->field()) ) { $divOptions = $this->addClass($divOptions, 'required'); } if (!isset($divOptions['tag'])) { $divOptions['tag'] = 'div'; } return $divOptions; } /** * Extracts a single option from an options array. * * @param string $name The name of the option to pull out. * @param array $options The array of options you want to extract. * @param mixed $default The default option value * @return mixed the contents of the option or default */ protected function _extractOption($name, $options, $default = null) { if (array_key_exists($name, $options)) { return $options[$name]; } return $default; } /** * Generate a label for an input() call. * * $options can contain a hash of id overrides. These overrides will be * used instead of the generated values if present. * * @param string $fieldName * @param string $label * @param array $options Options for the label element. 'NONE' option is deprecated and will be removed in 3.0 * @return string Generated label element */ protected function _inputLabel($fieldName, $label, $options) { $labelAttributes = $this->domId(array(), 'for'); $idKey = null; if ($options['type'] === 'date' || $options['type'] === 'datetime') { $firstInput = 'M'; if ( array_key_exists('dateFormat', $options) && ($options['dateFormat'] === null || $options['dateFormat'] === 'NONE') ) { $firstInput = 'H'; } elseif (!empty($options['dateFormat'])) { $firstInput = substr($options['dateFormat'], 0, 1); } switch ($firstInput) { case 'D': $idKey = 'day'; $labelAttributes['for'] .= 'Day'; break; case 'Y': $idKey = 'year'; $labelAttributes['for'] .= 'Year'; break; case 'M': $idKey = 'month'; $labelAttributes['for'] .= 'Month'; break; case 'H': $idKey = 'hour'; $labelAttributes['for'] .= 'Hour'; } } if ($options['type'] === 'time') { $labelAttributes['for'] .= 'Hour'; $idKey = 'hour'; } if (isset($idKey) && isset($options['id']) && isset($options['id'][$idKey])) { $labelAttributes['for'] = $options['id'][$idKey]; } if (is_array($label)) { $labelText = null; if (isset($label['text'])) { $labelText = $label['text']; unset($label['text']); } $labelAttributes = array_merge($labelAttributes, $label); } else { $labelText = $label; } if (isset($options['id']) && is_string($options['id'])) { $labelAttributes = array_merge($labelAttributes, array('for' => $options['id'])); } return $this->label($fieldName, $labelText, $labelAttributes); } /** * Creates a checkbox input widget. * * ### Options: * * - `value` - the value of the checkbox * - `checked` - boolean indicate that this checkbox is checked. * - `hiddenField` - boolean to indicate if you want the results of checkbox() to include * a hidden input with a value of ''. * - `disabled` - create a disabled input. * - `default` - Set the default value for the checkbox. This allows you to start checkboxes * as checked, without having to check the POST data. A matching POST data value, will overwrite * the default value. * * @param string $fieldName Name of a field, like this "Modelname.fieldname" * @param array $options Array of HTML attributes. * @return string An HTML text input element. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#options-for-select-checkbox-and-radio-inputs */ public function checkbox($fieldName, $options = array()) { $valueOptions = array(); if (isset($options['default'])) { $valueOptions['default'] = $options['default']; unset($options['default']); } $options += array('value' => 1, 'required' => false); $options = $this->_initInputField($fieldName, $options) + array('hiddenField' => true); $value = current($this->value($valueOptions)); $output = ''; if ( (!isset($options['checked']) && !empty($value) && $value == $options['value']) || !empty($options['checked']) ) { $options['checked'] = 'checked'; } if ($options['hiddenField']) { $hiddenOptions = array( 'id' => $options['id'] . '_', 'name' => $options['name'], 'value' => ($options['hiddenField'] !== true ? $options['hiddenField'] : '0'), 'secure' => false ); if (isset($options['disabled']) && $options['disabled']) { $hiddenOptions['disabled'] = 'disabled'; } $output = $this->hidden($fieldName, $hiddenOptions); } unset($options['hiddenField']); return $output . $this->Html->useTag('checkbox', $options['name'], array_diff_key($options, array('name' => null))); } /** * Creates a set of radio widgets. Will create a legend and fieldset * by default. Use $options to control this * * ### Attributes: * * - `separator` - define the string in between the radio buttons * - `between` - the string between legend and input set or array of strings to insert * strings between each input block * - `legend` - control whether or not the widget set has a fieldset & legend * - `value` - indicate a value that is should be checked * - `label` - boolean to indicate whether or not labels for widgets show be displayed * - `hiddenField` - boolean to indicate if you want the results of radio() to include * a hidden input with a value of ''. This is useful for creating radio sets that non-continuous * - `disabled` - Set to `true` or `disabled` to disable all the radio buttons. * - `empty` - Set to `true` to create a input with the value '' as the first option. When `true` * the radio label will be 'empty'. Set this option to a string to control the label value. * * @param string $fieldName Name of a field, like this "Modelname.fieldname" * @param array $options Radio button options array. * @param array $attributes Array of HTML attributes, and special attributes above. * @return string Completed radio widget set. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#options-for-select-checkbox-and-radio-inputs */ public function radio($fieldName, $options = array(), $attributes = array()) { $attributes = $this->_initInputField($fieldName, $attributes); $showEmpty = $this->_extractOption('empty', $attributes); if ($showEmpty) { $showEmpty = ($showEmpty === true) ? __d('cake', 'empty') : $showEmpty; $options = array('' => $showEmpty) + $options; } unset($attributes['empty']); $legend = false; if (isset($attributes['legend'])) { $legend = $attributes['legend']; unset($attributes['legend']); } elseif (count($options) > 1) { $legend = __(Inflector::humanize($this->field())); } $label = true; if (isset($attributes['label'])) { $label = $attributes['label']; unset($attributes['label']); } $separator = null; if (isset($attributes['separator'])) { $separator = $attributes['separator']; unset($attributes['separator']); } $between = null; if (isset($attributes['between'])) { $between = $attributes['between']; unset($attributes['between']); } $value = null; if (isset($attributes['value'])) { $value = $attributes['value']; } else { $value = $this->value($fieldName); } $disabled = array(); if (isset($attributes['disabled'])) { $disabled = $attributes['disabled']; } $out = array(); $hiddenField = isset($attributes['hiddenField']) ? $attributes['hiddenField'] : true; unset($attributes['hiddenField']); if (isset($value) && is_bool($value)) { $value = $value ? 1 : 0; } $this->_domIdSuffixes = array(); foreach ($options as $optValue => $optTitle) { $optionsHere = array('value' => $optValue, 'disabled' => false); if (isset($value) && strval($optValue) === strval($value)) { $optionsHere['checked'] = 'checked'; } $isNumeric = is_numeric($optValue); if ($disabled && (!is_array($disabled) || in_array((string)$optValue, $disabled, !$isNumeric))) { $optionsHere['disabled'] = true; } $tagName = $attributes['id'] . $this->domIdSuffix($optValue); if ($label) { $labelOpts = is_array($label) ? $label : array(); $labelOpts += array('for' => $tagName); $optTitle = $this->label($tagName, $optTitle, $labelOpts); } if (is_array($between)) { $optTitle .= array_shift($between); } $allOptions = array_merge($attributes, $optionsHere); $out[] = $this->Html->useTag('radio', $attributes['name'], $tagName, array_diff_key($allOptions, array('name' => null, 'type' => null, 'id' => null)), $optTitle ); } $hidden = null; if ($hiddenField) { if (!isset($value) || $value === '') { $hidden = $this->hidden($fieldName, array( 'id' => $attributes['id'] . '_', 'value' => '', 'name' => $attributes['name'] )); } } $out = $hidden . implode($separator, $out); if (is_array($between)) { $between = ''; } if ($legend) { $out = $this->Html->useTag('fieldset', '', $this->Html->useTag('legend', $legend) . $between . $out); } return $out; } /** * Missing method handler - implements various simple input types. Is used to create inputs * of various types. e.g. `$this->Form->text();` will create `` while * `$this->Form->range();` will create `` * * ### Usage * * `$this->Form->search('User.query', array('value' => 'test'));` * * Will make an input like: * * `` * * The first argument to an input type should always be the fieldname, in `Model.field` format. * The second argument should always be an array of attributes for the input. * * @param string $method Method name / input type to make. * @param array $params Parameters for the method call * @return string Formatted input method. * @throws CakeException When there are no params for the method call. */ public function __call($method, $params) { $options = array(); if (empty($params)) { throw new CakeException(__d('cake_dev', 'Missing field name for FormHelper::%s', $method)); } if (isset($params[1])) { $options = $params[1]; } if (!isset($options['type'])) { $options['type'] = $method; } $options = $this->_initInputField($params[0], $options); return $this->Html->useTag('input', $options['name'], array_diff_key($options, array('name' => null))); } /** * Creates a textarea widget. * * ### Options: * * - `escape` - Whether or not the contents of the textarea should be escaped. Defaults to true. * * @param string $fieldName Name of a field, in the form "Modelname.fieldname" * @param array $options Array of HTML attributes, and special options above. * @return string A generated HTML text input element * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::textarea */ public function textarea($fieldName, $options = array()) { $options = $this->_initInputField($fieldName, $options); $value = null; if (array_key_exists('value', $options)) { $value = $options['value']; if (!array_key_exists('escape', $options) || $options['escape'] !== false) { $value = h($value); } unset($options['value']); } return $this->Html->useTag('textarea', $options['name'], array_diff_key($options, array('type' => null, 'name' => null)), $value); } /** * Creates a hidden input field. * * @param string $fieldName Name of a field, in the form of "Modelname.fieldname" * @param array $options Array of HTML attributes. * @return string A generated hidden input * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::hidden */ public function hidden($fieldName, $options = array()) { $options += array('required' => false, 'secure' => true); $secure = $options['secure']; unset($options['secure']); $options = $this->_initInputField($fieldName, array_merge( $options, array('secure' => self::SECURE_SKIP) )); if ($secure === true) { $this->_secure(true, null, '' . $options['value']); } return $this->Html->useTag('hidden', $options['name'], array_diff_key($options, array('name' => null))); } /** * Creates file input widget. * * @param string $fieldName Name of a field, in the form "Modelname.fieldname" * @param array $options Array of HTML attributes. * @return string A generated file input. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::file */ public function file($fieldName, $options = array()) { $options += array('secure' => true); $secure = $options['secure']; $options['secure'] = self::SECURE_SKIP; $options = $this->_initInputField($fieldName, $options); $field = $this->entity(); foreach (array('name', 'type', 'tmp_name', 'error', 'size') as $suffix) { $this->_secure($secure, array_merge($field, array($suffix))); } $exclude = array('name' => null, 'value' => null); return $this->Html->useTag('file', $options['name'], array_diff_key($options, $exclude)); } /** * Creates a `