<?php /** * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) * * Licensed under The MIT License * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) * @link https://cakephp.org CakePHP(tm) Project * @package Cake.View.Helper * @since CakePHP(tm) v 0.10.0.1076 * @license https://opensource.org/licenses/mit-license.php MIT License */ App::uses('ClassRegistry', 'Utility'); App::uses('AppHelper', 'View/Helper'); App::uses('Hash', 'Utility'); App::uses('Inflector', 'Utility'); /** * Form helper library. * * Automatic generation of HTML FORMs from given data. * * @package Cake.View.Helper * @property HtmlHelper $Html * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html */ class FormHelper extends AppHelper { /** * Other helpers used by FormHelper * * @var array */ public $helpers = array('Html'); /** * Options used by DateTime fields * * @var array */ protected $_options = array( 'day' => 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(); /** * The action attribute value of the last created form. * Used to make form/request specific hashes for SecurityComponent. * * @var string */ protected $_lastAction = ''; /** * 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 Model name. * @return Model|null 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 null; } 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 (iterator_to_array($object->validator(), true) 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 Validation rules set. * @return bool 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 https://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). Deprecated since 2.8, use `url`. * - `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|null $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 A formatted opening FORM tag. * @link https://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( 'type' => ($created && empty($options['action'])) ? 'put' : 'post', 'action' => null, 'url' => null, 'default' => true, 'encoding' => strtolower(Configure::read('App.encoding')), 'inputDefaults' => array() ); $this->inputDefaults($options['inputDefaults']); unset($options['inputDefaults']); if (isset($options['action'])) { trigger_error('Using key `action` is deprecated, use `url` directly instead.', E_USER_DEPRECATED); } if (is_array($options['url']) && isset($options['url']['action'])) { $options['action'] = $options['url']['action']; } 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 (!isset($options['action'][0]) && !empty($id)) { $options['action'][0] = $id; } } elseif (is_string($options['url'])) { $options['action'] = $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' => static::SECURE_SKIP )); default: $htmlAttributes['method'] = 'post'; } $this->requestType = strtolower($options['type']); $action = null; if ($options['action'] !== false && $options['url'] !== false) { $action = $this->url($options['action']); } unset($options['url']); $this->_lastAction($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'); } if ($action === null) { return $this->Html->useTag('formwithoutaction', $htmlAttributes) . $append; } 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' => static::SECURE_SKIP, 'autocomplete' => 'off', )); } /** * 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') <div class="good"> value="save" name="Whatever" * array('label' => 'save', 'name' => 'Whatever', 'div' => array('class' => 'good')); <div 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 https://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->_unlockedFields = array(); $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|null A hidden input field with a security hash, otherwise null. * @link https://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 null; } $debugSecurity = Configure::read('debug'); if (isset($secureAttributes['debugSecurity'])) { $debugSecurity = $debugSecurity && $secureAttributes['debugSecurity']; unset($secureAttributes['debugSecurity']); } $originalFields = $fields; $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); $hashParts = array( $this->_lastAction, serialize($fields), $unlocked, Configure::read('Security.salt') ); $fields = Security::hash(implode('', $hashParts), 'sha1'); $tokenFields = array_merge($secureAttributes, array( 'value' => urlencode($fields . ':' . $locked), 'id' => 'TokenFields' . mt_rand(), 'secure' => static::SECURE_SKIP, 'autocomplete' => 'off', )); $out = $this->hidden('_Token.fields', $tokenFields); $tokenUnlocked = array_merge($secureAttributes, array( 'value' => urlencode($unlocked), 'id' => 'TokenUnlocked' . mt_rand(), 'secure' => static::SECURE_SKIP, 'autocomplete' => 'off', )); $out .= $this->hidden('_Token.unlocked', $tokenUnlocked); if ($debugSecurity) { $tokenDebug = array_merge($secureAttributes, array( 'value' => urlencode(json_encode(array( $this->_lastAction, $originalFields, $this->_unlockedFields ))), 'id' => 'TokenDebug' . mt_rand(), 'secure' => static::SECURE_SKIP, )); $out .= $this->hidden('_Token.debug', $tokenDebug); } 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 https://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 bool $lock Whether this field should be part of the validation * or excluded as part of the unlockedFields. * @param string|array $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 void */ protected function _secure($lock, $field = null, $value = null) { if (!$field) { $field = $this->entity(); } elseif (is_string($field)) { $field = explode('.', $field); } if (is_array($field)) { $field = Hash::filter($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; } elseif (isset($this->fields[$field]) && $value === null) { unset($this->fields[$field]); } $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 bool If there are errors this method returns true, else false. * @link https://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 <div /> wrapper tag * @return string|null If there are errors this method returns an error message, otherwise null. * @link https://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 += $defaults; $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'); * <label for="PostPublished">Published</label> * ``` * * Custom text: * * ``` * echo $this->Form->label('Post.published', 'Publish'); * <label for="PostPublished">Publish</label> * ``` * * Custom class name: * * ``` * echo $this->Form->label('Post.published', 'Publish', 'required'); * <label for="PostPublished" class="required">Publish</label> * ``` * * Custom attributes: * * ``` * echo $this->Form->label('Post.published', 'Publish', array( * 'for' => 'post-publish' * )); * <label for="post-publish">Publish</label> * ``` * * *Warning* Unlike most FormHelper methods, this method does not automatically * escape the $text parameter. You must escape the $text parameter yourself if you * are using user supplied data. * * @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 https://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 https://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']; unset($options['legend']); } if (isset($options['fieldset'])) { $fieldset = $options['fieldset']; unset($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 = array('class' => $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 https://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']); } else { $radioOptions = array(); } $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']); } else { $dateFormat = 'MDY'; $timeFormat = 12; } $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, Hash::get($divOptions, 'errorClass', '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'], $divOptions['errorClass']); $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 += array('value' => $selected); return $this->dateTime($fieldName, null, $timeFormat, $options); case 'date': $options += array('value' => $selected); return $this->dateTime($fieldName, $dateFormat, null, $options); case 'datetime': $options += array('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 Options list. * @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); } $options = $this->_maxLength($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 Options list. * @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 Options list. * @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', 'smallinteger' => 'number', 'tinyinteger' => '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' && isset($fieldDef['length'])) { $decimalPlaces = substr($fieldDef['length'], strpos($fieldDef['length'], ',') + 1); $options['step'] = sprintf('%.' . $decimalPlaces . 'F', pow(10, -1 * $decimalPlaces)); } elseif ($type === 'float' || $type === 'decimal') { $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']); } return $options; } /** * Generate format options * * @param array $options Options list. * @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 Field name. * @param array $options Options list. * @return bool|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 Options list. * @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']) && $fieldDef['length'] < 1000000 && $fieldDef['type'] !== 'decimal' && $fieldDef['type'] !== 'time' && $fieldDef['type'] !== 'datetime' && $options['type'] !== 'select' ); if ($autoLength && in_array($options['type'], array('text', 'textarea', 'email', 'tel', 'url', 'search')) ) { $options['maxlength'] = (int)$fieldDef['length']; } return $options; } /** * Generate div options for input * * @param array $options Options list. * @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 Field name. * @param string|array $label Label text or array with text and options. * @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 https://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'), 'form' => isset($options['form']) ? $options['form'] : null, '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 * * You can also customize each radio input element using an array of arrays: * * ``` * $options = array( * array('name' => 'United states', 'value' => 'US', 'title' => 'My title'), * array('name' => 'Germany', 'value' => 'DE', 'class' => 'de-de', 'title' => 'Another title'), * ); * ``` * * ### 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 * - `fieldset` - sets the class of the fieldset. Fieldset is only generated if legend attribute is provided * - `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 an 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 https://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['options'] = $options; $attributes = $this->_initInputField($fieldName, $attributes); unset($attributes['options']); $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())); } $fieldsetAttrs = ''; if (isset($attributes['fieldset'])) { $fieldsetAttrs = array('class' => $attributes['fieldset']); unset($attributes['fieldset']); } $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 (is_array($optTitle)) { if (isset($optTitle['value'])) { $optionsHere['value'] = $optTitle['value']; } $optionsHere += $optTitle; $optTitle = $optionsHere['name']; unset($optionsHere['name']); } 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 = $optionsHere + $attributes; $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( 'form' => isset($attributes['form']) ? $attributes['form'] : null, 'id' => $attributes['id'] . '_', 'value' => $hiddenField === true ? '' : $hiddenField, 'name' => $attributes['name'] )); } } $out = $hidden . implode($separator, $out); if (is_array($between)) { $between = ''; } if ($legend) { $out = $this->Html->useTag('legend', $legend) . $between . $out; $out = $this->Html->useTag('fieldset', $fieldsetAttrs, $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 `<input type="text" />` while * `$this->Form->range();` will create `<input type="range" />` * * ### Usage * * `$this->Form->search('User.query', array('value' => 'test'));` * * Will make an input like: * * `<input type="search" id="UserQuery" name="data[User][query]" value="test" />` * * 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 https://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 https://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' => static::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 https://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'] = static::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))); } if (version_compare(PHP_VERSION, '8.1.0', '>=')) { $this->_secure($secure, array_merge($field, array('full_path'))); } $exclude = array('name' => null, 'value' => null); return $this->Html->useTag('file', $options['name'], array_diff_key($options, $exclude)); } /** * Creates a `<button>` tag. The type attribute defaults to `type="submit"` * You can change it to a different value by using `$options['type']`. * * ### Options: * * - `escape` - HTML entity encode the $title of the button. Defaults to false. * * @param string $title The button's caption. Not automatically HTML encoded * @param array $options Array of options and HTML attributes. * @return string A HTML button tag. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::button */ public function button($title, $options = array()) { $options += array('type' => 'submit', 'escape' => false, 'secure' => false); if ($options['escape']) { $title = h($title); } if (isset($options['name'])) { $name = str_replace(array('[', ']'), array('.', ''), $options['name']); $this->_secure($options['secure'], $name); } return $this->Html->useTag('button', $options, $title); } /** * Create a `<button>` tag with a surrounding `<form>` that submits via POST. * * This method creates a `<form>` element. So do not use this method in an already opened form. * Instead use FormHelper::submit() or FormHelper::button() to create buttons inside opened forms. * * ### Options: * * - `data` - Array with key/value to pass in input hidden * - Other options is the same of button method. * * @param string $title The button's caption. Not automatically HTML encoded * @param string|array $url URL as string or array * @param array $options Array of options and HTML attributes. * @return string A HTML button tag. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::postButton */ public function postButton($title, $url, $options = array()) { $out = $this->create(false, array('id' => false, 'url' => $url)); if (isset($options['data']) && is_array($options['data'])) { foreach (Hash::flatten($options['data']) as $key => $value) { $out .= $this->hidden($key, array('value' => $value, 'id' => false)); } unset($options['data']); } $out .= $this->button($title, $options); $out .= $this->end(); return $out; } /** * Creates an HTML link, but access the URL using the method you specify (defaults to POST). * Requires javascript to be enabled in browser. * * This method creates a `<form>` element. If you want to use this method inside of an * existing form, you must use the `inline` or `block` options so that the new form is * being set to a view block that can be rendered outside of the main form. * * If all you are looking for is a button to submit your form, then you should use * `FormHelper::submit()` instead. * * ### Options: * * - `data` - Array with key/value to pass in input hidden * - `method` - Request method to use. Set to 'delete' to simulate HTTP/1.1 DELETE request. Defaults to 'post'. * - `confirm` - Can be used instead of $confirmMessage. * - `inline` - Whether or not the associated form tag should be output inline. * Set to false to have the form tag appended to the 'postLink' view block. * Defaults to true. * - `block` - Choose a custom block to append the form tag to. Using this option * will override the inline option. * - Other options are the same of HtmlHelper::link() method. * - The option `onclick` will be replaced. * * @param string $title The content to be wrapped by <a> tags. * @param string|array $url Cake-relative URL or array of URL parameters, or external URL (starts with http://) * @param array $options Array of HTML attributes. * @param bool|string $confirmMessage JavaScript confirmation message. This * argument is deprecated as of 2.6. Use `confirm` key in $options instead. * @return string An `<a />` element. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::postLink */ public function postLink($title, $url = null, $options = array(), $confirmMessage = false) { $options = (array)$options + array('inline' => true, 'block' => null); if (!$options['inline'] && empty($options['block'])) { $options['block'] = __FUNCTION__; } unset($options['inline']); $requestMethod = 'POST'; if (!empty($options['method'])) { $requestMethod = strtoupper($options['method']); unset($options['method']); } if (!empty($options['confirm'])) { $confirmMessage = $options['confirm']; unset($options['confirm']); } $formName = str_replace('.', '', uniqid('post_', true)); $formUrl = $this->url($url); $formOptions = array( 'name' => $formName, 'id' => $formName, 'style' => 'display:none;', 'method' => 'post', ); if (isset($options['target'])) { $formOptions['target'] = $options['target']; unset($options['target']); } $previousLastAction = $this->_lastAction; $this->_lastAction($url); $out = $this->Html->useTag('form', $formUrl, $formOptions); $out .= $this->Html->useTag('hidden', '_method', array( 'value' => $requestMethod )); $out .= $this->_csrfField(); $fields = array(); if (isset($options['data']) && is_array($options['data'])) { foreach (Hash::flatten($options['data']) as $key => $value) { $fields[$key] = $value; $out .= $this->hidden($key, array('value' => $value, 'id' => false, 'secure' => static::SECURE_SKIP)); } unset($options['data']); } $out .= $this->secure($fields); $out .= $this->Html->useTag('formend'); if ($options['block']) { $this->_View->append($options['block'], $out); $out = ''; // Reset security-relevant fields for outer form $this->_lastAction = $previousLastAction; } unset($options['block']); $url = '#'; $onClick = 'document.' . $formName . '.submit();'; if ($confirmMessage) { $options['onclick'] = $this->_confirm($confirmMessage, $onClick, '', $options); } else { $options['onclick'] = $onClick . ' '; } $options['onclick'] .= 'event.returnValue = false; return false;'; $out .= $this->Html->link($title, $url, $options); return $out; } /** * Creates a submit button element. This method will generate `<input />` elements that * can be used to submit, and reset forms by using $options. image submits can be created by supplying an * image path for $caption. * * ### Options * * - `div` - Include a wrapping div? Defaults to true. Accepts sub options similar to * FormHelper::input(). * - `before` - Content to include before the input. * - `after` - Content to include after the input. * - `type` - Set to 'reset' for reset inputs. Defaults to 'submit' * - `confirm` - JavaScript confirmation message. * - Other attributes will be assigned to the input element. * * ### Options * * - `div` - Include a wrapping div? Defaults to true. Accepts sub options similar to * FormHelper::input(). * - Other attributes will be assigned to the input element. * * @param string $caption The label appearing on the button OR if string contains :// or the * extension .jpg, .jpe, .jpeg, .gif, .png use an image if the extension * exists, AND the first character is /, image is relative to webroot, * OR if the first character is not /, image is relative to webroot/img. * @param array $options Array of options. See above. * @return string A HTML submit button * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::submit */ public function submit($caption = null, $options = array()) { $confirmMessage = false; if (!is_string($caption) && empty($caption)) { $caption = __d('cake', 'Submit'); } $out = null; $div = true; if (!empty($options['confirm'])) { $confirmMessage = $options['confirm']; unset($options['confirm']); } if (isset($options['div'])) { $div = $options['div']; unset($options['div']); } $options += array('type' => 'submit', 'before' => null, 'after' => null, 'secure' => false); $divOptions = array('tag' => 'div'); if ($div === true) { $divOptions['class'] = 'submit'; } elseif ($div === false) { unset($divOptions); } elseif (is_string($div)) { $divOptions['class'] = $div; } elseif (is_array($div)) { $divOptions = array_merge(array('class' => 'submit', 'tag' => 'div'), $div); } if (isset($options['name'])) { $name = str_replace(array('[', ']'), array('.', ''), $options['name']); $this->_secure($options['secure'], $name); } unset($options['secure']); $before = $options['before']; $after = $options['after']; unset($options['before'], $options['after']); $isUrl = strpos($caption, '://') !== false; $isImage = preg_match('/\.(jpg|jpe|jpeg|gif|png|ico)$/', $caption); if ($isUrl || $isImage) { $unlockFields = array('x', 'y'); if (isset($options['name'])) { $unlockFields = array( $options['name'] . '_x', $options['name'] . '_y' ); } foreach ($unlockFields as $ignore) { $this->unlockField($ignore); } } if ($confirmMessage) { $okCode = 'return true;'; $cancelCode = 'event.returnValue = false; return false;'; $options['onclick'] = $this->_confirm($confirmMessage, $okCode, $cancelCode, $options); } if ($isUrl) { unset($options['type']); $tag = $this->Html->useTag('submitimage', $caption, $options); } elseif ($isImage) { unset($options['type']); if ($caption[0] !== '/') { $url = $this->webroot(Configure::read('App.imageBaseUrl') . $caption); } else { $url = $this->webroot(trim($caption, '/')); } $url = $this->assetTimestamp($url); $tag = $this->Html->useTag('submitimage', $url, $options); } else { $options['value'] = $caption; $tag = $this->Html->useTag('submit', $options); } $out = $before . $tag . $after; if (isset($divOptions)) { $tag = $divOptions['tag']; unset($divOptions['tag']); $out = $this->Html->tag($tag, $out, $divOptions); } return $out; } /** * Returns a formatted SELECT element. * * ### Attributes: * * - `showParents` - If included in the array and set to true, an additional option element * will be added for the parent of each option group. You can set an option with the same name * and it's key will be used for the value of the option. * - `multiple` - show a multiple select box. If set to 'checkbox' multiple checkboxes will be * created instead. * - `empty` - If true, the empty select option is shown. If a string, * that string is displayed as the empty element. * - `escape` - If true contents of options will be HTML entity encoded. Defaults to true. * - `value` The selected value of the input. * - `class` - When using multiple = checkbox the class name to apply to the divs. Defaults to 'checkbox'. * - `disabled` - Control the disabled attribute. When creating a select box, set to true to disable the * select box. When creating checkboxes, `true` will disable all checkboxes. You can also set disabled * to a list of values you want to disable when creating checkboxes. * * ### Using options * * A simple array will create normal options: * * ``` * $options = array(1 => 'one', 2 => 'two); * $this->Form->select('Model.field', $options)); * ``` * * While a nested options array will create optgroups with options inside them. * ``` * $options = array( * 1 => 'bill', * 'fred' => array( * 2 => 'fred', * 3 => 'fred jr.' * ) * ); * $this->Form->select('Model.field', $options); * ``` * * In the above `2 => 'fred'` will not generate an option element. You should enable the `showParents` * attribute to show the fred option. * * If you have multiple options that need to have the same value attribute, you can * use an array of arrays to express this: * * ``` * $options = array( * array('name' => 'United states', 'value' => 'USA'), * array('name' => 'USA', 'value' => 'USA'), * ); * ``` * * @param string $fieldName Name attribute of the SELECT * @param array $options Array of the OPTION elements (as 'value'=>'Text' pairs) to be used in the * SELECT element * @param array $attributes The HTML attributes of the select element. * @return string Formatted SELECT element * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#options-for-select-checkbox-and-radio-inputs */ public function select($fieldName, $options = array(), $attributes = array()) { $select = array(); $style = null; $tag = null; $attributes += array( 'class' => null, 'escape' => true, 'secure' => true, 'empty' => '', 'showParents' => false, 'hiddenField' => true, 'disabled' => false ); $escapeOptions = $this->_extractOption('escape', $attributes); $secure = $this->_extractOption('secure', $attributes); $showEmpty = $this->_extractOption('empty', $attributes); $showParents = $this->_extractOption('showParents', $attributes); $hiddenField = $this->_extractOption('hiddenField', $attributes); unset($attributes['escape'], $attributes['secure'], $attributes['empty'], $attributes['showParents'], $attributes['hiddenField']); $id = $this->_extractOption('id', $attributes); $attributes = $this->_initInputField($fieldName, array_merge( (array)$attributes, array('secure' => static::SECURE_SKIP) )); if (is_string($options) && isset($this->_options[$options])) { $options = $this->_generateOptions($options); } elseif (!is_array($options)) { $options = array(); } if (isset($attributes['type'])) { unset($attributes['type']); } if (!empty($attributes['multiple'])) { $style = ($attributes['multiple'] === 'checkbox') ? 'checkbox' : null; $template = ($style) ? 'checkboxmultiplestart' : 'selectmultiplestart'; $tag = $template; if ($hiddenField) { $hiddenAttributes = array( 'value' => '', 'id' => $attributes['id'] . ($style ? '' : '_'), 'secure' => false, 'form' => isset($attributes['form']) ? $attributes['form'] : null, 'name' => $attributes['name'], 'disabled' => $attributes['disabled'] === true || $attributes['disabled'] === 'disabled' ); $select[] = $this->hidden(null, $hiddenAttributes); } } else { $tag = 'selectstart'; } if ($tag === 'checkboxmultiplestart') { unset($attributes['required']); } if (!empty($tag) || isset($template)) { $hasOptions = (count($options) > 0 || $showEmpty); // Secure the field if there are options, or its a multi select. // Single selects with no options don't submit, but multiselects do. if ((!isset($secure) || $secure) && empty($attributes['disabled']) && (!empty($attributes['multiple']) || $hasOptions) ) { $this->_secure(true, $this->_secureFieldName($attributes)); } $filter = array('name' => null, 'value' => null); if (is_array($attributes['disabled'])) { $filter['disabled'] = null; } $select[] = $this->Html->useTag($tag, $attributes['name'], array_diff_key($attributes, $filter)); } $emptyMulti = ( $showEmpty !== null && $showEmpty !== false && !( empty($showEmpty) && (isset($attributes) && array_key_exists('multiple', $attributes)) ) ); if ($emptyMulti) { $showEmpty = ($showEmpty === true) ? '' : $showEmpty; $options = array('' => $showEmpty) + $options; } if (!$id) { $attributes['id'] = Inflector::camelize($attributes['id']); } $select = array_merge($select, $this->_selectOptions( array_reverse($options, true), array(), $showParents, array( 'escape' => $escapeOptions, 'style' => $style, 'name' => $attributes['name'], 'value' => $attributes['value'], 'class' => $attributes['class'], 'id' => $attributes['id'], 'disabled' => $attributes['disabled'], ) )); $template = ($style === 'checkbox') ? 'checkboxmultipleend' : 'selectend'; $select[] = $this->Html->useTag($template); return implode("\n", $select); } /** * Generates a valid DOM ID suffix from a string. * Also avoids collisions when multiple values are coverted to the same suffix by * appending a numeric value. * * For pre-HTML5 IDs only characters like a-z 0-9 - _ are valid. HTML5 doesn't have that * limitation, but to avoid layout issues it still filters out some sensitive chars. * * @param string $value The value that should be transferred into a DOM ID suffix. * @param string $type Doctype to use. Defaults to html4. * @return string DOM ID */ public function domIdSuffix($value, $type = 'html4') { if ($type === 'html5') { $value = str_replace(array('@', '<', '>', ' ', '"', '\''), '_', $value); } else { $value = Inflector::camelize(Inflector::slug($value)); } $value = Inflector::camelize($value); $count = 1; $suffix = $value; while (in_array($suffix, $this->_domIdSuffixes)) { $suffix = $value . $count++; } $this->_domIdSuffixes[] = $suffix; return $suffix; } /** * Returns a SELECT element for days. * * ### Attributes: * * - `empty` - If true, the empty select option is shown. If a string, * that string is displayed as the empty element. * - `value` The selected value of the input. * * @param string $fieldName Prefix name for the SELECT element * @param array $attributes HTML attributes for the select element * @return string A generated day select box. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::day */ public function day($fieldName = null, $attributes = array()) { $attributes += array('empty' => true, 'value' => null); $attributes = $this->_dateTimeSelected('day', $fieldName, $attributes); if (strlen($attributes['value']) > 2) { $date = date_create($attributes['value']); $attributes['value'] = null; if ($date) { $attributes['value'] = $date->format('d'); } } elseif ($attributes['value'] === false) { $attributes['value'] = null; } return $this->select($fieldName . ".day", $this->_generateOptions('day'), $attributes); } /** * Returns a SELECT element for years * * ### Attributes: * * - `empty` - If true, the empty select option is shown. If a string, * that string is displayed as the empty element. * - `orderYear` - Ordering of year values in select options. * Possible values 'asc', 'desc'. Default 'desc' * - `value` The selected value of the input. * * @param string $fieldName Prefix name for the SELECT element * @param int $minYear First year in sequence * @param int $maxYear Last year in sequence * @param array $attributes Attribute array for the select elements. * @return string Completed year select input * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::year */ public function year($fieldName, $minYear = null, $maxYear = null, $attributes = array()) { if (is_array($minYear)) { $attributes = $minYear; $minYear = null; } $attributes += array('empty' => true, 'value' => null); if ((empty($attributes['value']) || $attributes['value'] === true) && $value = $this->value($fieldName)) { if (is_array($value)) { $year = null; extract($value); $attributes['value'] = $year; } else { if (empty($value)) { if (!$attributes['empty'] && !$maxYear) { $attributes['value'] = 'now'; } elseif (!$attributes['empty'] && $maxYear && !$attributes['value']) { $attributes['value'] = $maxYear; } } else { $attributes['value'] = $value; } } } if (strlen($attributes['value']) > 4 || $attributes['value'] === 'now') { $date = date_create($attributes['value']); $attributes['value'] = null; if ($date) { $attributes['value'] = $date->format('Y'); } } elseif ($attributes['value'] === false) { $attributes['value'] = null; } $yearOptions = array('value' => $attributes['value'], 'min' => $minYear, 'max' => $maxYear, 'order' => 'desc'); if (isset($attributes['orderYear'])) { $yearOptions['order'] = $attributes['orderYear']; unset($attributes['orderYear']); } return $this->select( $fieldName . '.year', $this->_generateOptions('year', $yearOptions), $attributes ); } /** * Returns a SELECT element for months. * * ### Attributes: * * - `monthNames` - If false, 2 digit numbers will be used instead of text. * If an array, the given array will be used. * - `empty` - If true, the empty select option is shown. If a string, * that string is displayed as the empty element. * - `value` The selected value of the input. * * @param string $fieldName Prefix name for the SELECT element * @param array $attributes Attributes for the select element * @return string A generated month select dropdown. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::month */ public function month($fieldName, $attributes = array()) { $attributes += array('empty' => true, 'value' => null); $attributes = $this->_dateTimeSelected('month', $fieldName, $attributes); if (strlen($attributes['value']) > 2) { $date = date_create($attributes['value']); $attributes['value'] = null; if ($date) { $attributes['value'] = $date->format('m'); } } elseif ($attributes['value'] === false) { $attributes['value'] = null; } $defaults = array('monthNames' => true); $attributes = array_merge($defaults, (array)$attributes); $monthNames = $attributes['monthNames']; unset($attributes['monthNames']); return $this->select( $fieldName . ".month", $this->_generateOptions('month', array('monthNames' => $monthNames)), $attributes ); } /** * Returns a SELECT element for hours. * * ### Attributes: * * - `empty` - If true, the empty select option is shown. If a string, * that string is displayed as the empty element. * - `value` The selected value of the input. * * @param string $fieldName Prefix name for the SELECT element * @param bool $format24Hours True for 24 hours format * @param array $attributes List of HTML attributes * @return string Completed hour select input * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::hour */ public function hour($fieldName, $format24Hours = false, $attributes = array()) { if (is_array($format24Hours)) { $attributes = $format24Hours; $format24Hours = false; } $attributes += array('empty' => true, 'value' => null); $attributes = $this->_dateTimeSelected('hour', $fieldName, $attributes); if (strlen($attributes['value']) > 2) { try { $date = new DateTime($attributes['value']); if ($format24Hours) { $attributes['value'] = $date->format('H'); } else { $attributes['value'] = $date->format('g'); } } catch (Exception $e) { $attributes['value'] = null; } } elseif ($attributes['value'] === false) { $attributes['value'] = null; } if ($attributes['value'] > 12 && !$format24Hours) { $attributes['value'] -= 12; } if (($attributes['value'] === 0 || $attributes['value'] === '00') && !$format24Hours) { $attributes['value'] = 12; } return $this->select( $fieldName . ".hour", $this->_generateOptions($format24Hours ? 'hour24' : 'hour'), $attributes ); } /** * Returns a SELECT element for minutes. * * ### Attributes: * * - `empty` - If true, the empty select option is shown. If a string, * that string is displayed as the empty element. * - `value` The selected value of the input. * * @param string $fieldName Prefix name for the SELECT element * @param array $attributes Array of Attributes * @return string Completed minute select input. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::minute */ public function minute($fieldName, $attributes = array()) { $attributes += array('empty' => true, 'value' => null); $attributes = $this->_dateTimeSelected('min', $fieldName, $attributes); if (strlen($attributes['value']) > 2) { $date = date_create($attributes['value']); $attributes['value'] = null; if ($date) { $attributes['value'] = $date->format('i'); } } elseif ($attributes['value'] === false) { $attributes['value'] = null; } $minuteOptions = array(); if (isset($attributes['interval'])) { $minuteOptions['interval'] = $attributes['interval']; unset($attributes['interval']); } return $this->select( $fieldName . ".min", $this->_generateOptions('minute', $minuteOptions), $attributes ); } /** * Selects values for dateTime selects. * * @param string $select Name of element field. ex. 'day' * @param string $fieldName Name of fieldName being generated ex. Model.created * @param array $attributes Array of attributes, must contain 'empty' key. * @return array Attributes array with currently selected value. */ protected function _dateTimeSelected($select, $fieldName, $attributes) { if ((empty($attributes['value']) || $attributes['value'] === true) && $value = $this->value($fieldName)) { if (is_array($value)) { $attributes['value'] = isset($value[$select]) ? $value[$select] : null; } else { if (empty($value)) { if (!$attributes['empty']) { $attributes['value'] = 'now'; } } else { $attributes['value'] = $value; } } } return $attributes; } /** * Returns a SELECT element for AM or PM. * * ### Attributes: * * - `empty` - If true, the empty select option is shown. If a string, * that string is displayed as the empty element. * - `value` The selected value of the input. * * @param string $fieldName Prefix name for the SELECT element * @param array $attributes Array of Attributes * @return string Completed meridian select input * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::meridian */ public function meridian($fieldName, $attributes = array()) { $attributes += array('empty' => true, 'value' => null); if ((empty($attributes['value']) || $attributes['value'] === true) && $value = $this->value($fieldName)) { if (is_array($value)) { $meridian = null; extract($value); $attributes['value'] = $meridian; } else { if (empty($value)) { if (!$attributes['empty']) { $attributes['value'] = date('a'); } } else { $date = date_create($attributes['value']); $attributes['value'] = null; if ($date) { $attributes['value'] = $date->format('a'); } } } } if ($attributes['value'] === false) { $attributes['value'] = null; } return $this->select( $fieldName . ".meridian", $this->_generateOptions('meridian'), $attributes ); } /** * Returns a set of SELECT elements for a full datetime setup: day, month and year, and then time. * * ### Attributes: * * - `monthNames` If false, 2 digit numbers will be used instead of text. * If an array, the given array will be used. * - `minYear` The lowest year to use in the year select * - `maxYear` The maximum year to use in the year select * - `interval` The interval for the minutes select. Defaults to 1 * - `separator` The contents of the string between select elements. Defaults to '-' * - `empty` - If true, the empty select option is shown. If a string, * that string is displayed as the empty element. * - `round` - Set to `up` or `down` if you want to force rounding in either direction. Defaults to null. * - `value` | `default` The default value to be used by the input. A value in `$this->data` * matching the field name will override this value. If no default is provided `time()` will be used. * * @param string $fieldName Prefix name for the SELECT element * @param string $dateFormat DMY, MDY, YMD, or null to not generate date inputs. * @param string $timeFormat 12, 24, or null to not generate time inputs. * @param array $attributes Array of Attributes * @return string Generated set of select boxes for the date and time formats chosen. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::dateTime */ public function dateTime($fieldName, $dateFormat = 'DMY', $timeFormat = '12', $attributes = array()) { $attributes += array('empty' => true, 'value' => null); $year = $month = $day = $hour = $min = $meridian = null; if (empty($attributes['value'])) { $attributes = $this->value($attributes, $fieldName); } if ($attributes['value'] === null && $attributes['empty'] != true) { $attributes['value'] = time(); if (!empty($attributes['maxYear']) && $attributes['maxYear'] < date('Y')) { $attributes['value'] = strtotime(date($attributes['maxYear'] . '-m-d')); } } if (!empty($attributes['value'])) { list($year, $month, $day, $hour, $min, $meridian) = $this->_getDateTimeValue( $attributes['value'], $timeFormat ); } $defaults = array( 'minYear' => null, 'maxYear' => null, 'separator' => '-', 'interval' => 1, 'monthNames' => true, 'round' => null ); $attributes = array_merge($defaults, (array)$attributes); if (isset($attributes['minuteInterval'])) { $attributes['interval'] = $attributes['minuteInterval']; unset($attributes['minuteInterval']); } $minYear = $attributes['minYear']; $maxYear = $attributes['maxYear']; $separator = $attributes['separator']; $interval = $attributes['interval']; $monthNames = $attributes['monthNames']; $round = $attributes['round']; $attributes = array_diff_key($attributes, $defaults); if (!empty($interval) && $interval > 1 && !empty($min)) { $current = new DateTime(); if ($year !== null) { $current->setDate($year, $month, $day); } if ($hour !== null) { $current->setTime($hour, $min); } $changeValue = $min * (1 / $interval); switch ($round) { case 'up': $changeValue = ceil($changeValue); break; case 'down': $changeValue = floor($changeValue); break; default: $changeValue = round($changeValue); } $change = ($changeValue * $interval) - $min; $current->modify($change > 0 ? "+$change minutes" : "$change minutes"); $format = ($timeFormat == 12) ? 'Y m d h i a' : 'Y m d H i a'; $newTime = explode(' ', $current->format($format)); list($year, $month, $day, $hour, $min, $meridian) = $newTime; } $keys = array('Day', 'Month', 'Year', 'Hour', 'Minute', 'Meridian'); $attrs = array_fill_keys($keys, $attributes); $hasId = isset($attributes['id']); if ($hasId && is_array($attributes['id'])) { // check for missing ones and build selectAttr for each element $attributes['id'] += array( 'month' => '', 'year' => '', 'day' => '', 'hour' => '', 'minute' => '', 'meridian' => '' ); foreach ($keys as $key) { $attrs[$key]['id'] = $attributes['id'][strtolower($key)]; } } if ($hasId && is_string($attributes['id'])) { // build out an array version foreach ($keys as $key) { $attrs[$key]['id'] = $attributes['id'] . $key; } } if (is_array($attributes['empty'])) { $attributes['empty'] += array( 'month' => true, 'year' => true, 'day' => true, 'hour' => true, 'minute' => true, 'meridian' => true ); foreach ($keys as $key) { $attrs[$key]['empty'] = $attributes['empty'][strtolower($key)]; } } $selects = array(); foreach (preg_split('//', $dateFormat, -1, PREG_SPLIT_NO_EMPTY) as $char) { switch ($char) { case 'Y': $attrs['Year']['value'] = $year; $selects[] = $this->year( $fieldName, $minYear, $maxYear, $attrs['Year'] ); break; case 'M': $attrs['Month']['value'] = $month; $attrs['Month']['monthNames'] = $monthNames; $selects[] = $this->month($fieldName, $attrs['Month']); break; case 'D': $attrs['Day']['value'] = $day; $selects[] = $this->day($fieldName, $attrs['Day']); break; } } $opt = implode($separator, $selects); $attrs['Minute']['interval'] = $interval; switch ($timeFormat) { case '24': $attrs['Hour']['value'] = $hour; $attrs['Minute']['value'] = $min; $opt .= $this->hour($fieldName, true, $attrs['Hour']) . ':' . $this->minute($fieldName, $attrs['Minute']); break; case '12': $attrs['Hour']['value'] = $hour; $attrs['Minute']['value'] = $min; $attrs['Meridian']['value'] = $meridian; $opt .= $this->hour($fieldName, false, $attrs['Hour']) . ':' . $this->minute($fieldName, $attrs['Minute']) . ' ' . $this->meridian($fieldName, $attrs['Meridian']); break; } return $opt; } /** * Parse the value for a datetime selected value * * @param string|array $value The selected value. * @param int $timeFormat The time format * @return array Array of selected value. */ protected function _getDateTimeValue($value, $timeFormat) { $year = $month = $day = $hour = $min = $meridian = null; if (is_array($value)) { extract($value); if ($meridian === 'pm') { $hour += 12; } return array($year, $month, $day, $hour, $min, $meridian); } if (is_numeric($value)) { $value = strftime('%Y-%m-%d %H:%M:%S', $value); } $meridian = 'am'; $pos = strpos($value, '-'); if ($pos !== false) { $date = explode('-', $value); $days = explode(' ', $date[2]); $day = $days[0]; $month = $date[1]; $year = $date[0]; } else { $days[1] = $value; } if (!empty($timeFormat)) { $time = explode(':', $days[1]); if ($time[0] >= 12) { $meridian = 'pm'; } $hour = $min = null; if (isset($time[1])) { $hour = $time[0]; $min = $time[1]; } } return array($year, $month, $day, $hour, $min, $meridian); } /** * Gets the input field name for the current tag * * @param array $options Options list. * @param string $field Field name. * @param string $key Key name. * @return array */ protected function _name($options = array(), $field = null, $key = 'name') { if ($this->requestType === 'get') { if ($options === null) { $options = array(); } elseif (is_string($options)) { $field = $options; $options = 0; } if (!empty($field)) { $this->setEntity($field); } if (is_array($options) && isset($options[$key])) { return $options; } $entity = $this->entity(); $model = $this->model(); $name = $model === $entity[0] && isset($entity[1]) ? $entity[1] : $entity[0]; $last = $entity[count($entity) - 1]; if (in_array($last, $this->_fieldSuffixes)) { $name .= '[' . $last . ']'; } if (is_array($options)) { $options[$key] = $name; return $options; } return $name; } return parent::_name($options, $field, $key); } /** * Returns an array of formatted OPTION/OPTGROUP elements * * @param array $elements Elements to format. * @param array $parents Parents for OPTGROUP. * @param bool $showParents Whether to show parents. * @param array $attributes HTML attributes. * @return array */ protected function _selectOptions($elements = array(), $parents = array(), $showParents = null, $attributes = array()) { $select = array(); $attributes = array_merge( array('escape' => true, 'style' => null, 'value' => null, 'class' => null), $attributes ); $selectedIsEmpty = ($attributes['value'] === '' || $attributes['value'] === null); $selectedIsArray = is_array($attributes['value']); // Cast boolean false into an integer so string comparisons can work. if ($attributes['value'] === false) { $attributes['value'] = 0; } $this->_domIdSuffixes = array(); foreach ($elements as $name => $title) { $htmlOptions = array(); if (is_array($title) && (!isset($title['name']) || !isset($title['value']))) { if (!empty($name)) { if ($attributes['style'] === 'checkbox') { $select[] = $this->Html->useTag('fieldsetend'); } else { $select[] = $this->Html->useTag('optiongroupend'); } $parents[] = (string)$name; } $select = array_merge($select, $this->_selectOptions( $title, $parents, $showParents, $attributes )); if (!empty($name)) { $name = $attributes['escape'] ? h($name) : $name; if ($attributes['style'] === 'checkbox') { $select[] = $this->Html->useTag('fieldsetstart', $name); } else { $select[] = $this->Html->useTag('optiongroup', $name, ''); } } $name = null; } elseif (is_array($title)) { $htmlOptions = $title; $name = $title['value']; $title = $title['name']; unset($htmlOptions['name'], $htmlOptions['value']); } if ($name !== null) { $isNumeric = is_numeric($name); if ((!$selectedIsArray && !$selectedIsEmpty && (string)$attributes['value'] == (string)$name) || ($selectedIsArray && in_array((string)$name, $attributes['value'], !$isNumeric)) ) { if ($attributes['style'] === 'checkbox') { $htmlOptions['checked'] = true; } else { $htmlOptions['selected'] = 'selected'; } } if ($showParents || (!in_array($title, $parents))) { $title = ($attributes['escape']) ? h($title) : $title; $hasDisabled = !empty($attributes['disabled']); if ($hasDisabled) { $disabledIsArray = is_array($attributes['disabled']); if ($disabledIsArray) { $disabledIsNumeric = is_numeric($name); } } if ($hasDisabled && $disabledIsArray && in_array((string)$name, $attributes['disabled'], !$disabledIsNumeric) ) { $htmlOptions['disabled'] = 'disabled'; } if ($hasDisabled && !$disabledIsArray && $attributes['style'] === 'checkbox') { $htmlOptions['disabled'] = $attributes['disabled'] === true ? 'disabled' : $attributes['disabled']; } if ($attributes['style'] === 'checkbox') { $htmlOptions['value'] = $name; $tagName = $attributes['id'] . $this->domIdSuffix($name); $htmlOptions['id'] = $tagName; $label = array('for' => $tagName); if (isset($htmlOptions['checked']) && $htmlOptions['checked'] === true) { $label['class'] = 'selected'; } $name = $attributes['name']; if (empty($attributes['class'])) { $attributes['class'] = 'checkbox'; } elseif ($attributes['class'] === 'form-error') { $attributes['class'] = 'checkbox ' . $attributes['class']; } $label = $this->label(null, $title, $label); $item = $this->Html->useTag('checkboxmultiple', $name, $htmlOptions); $select[] = $this->Html->div($attributes['class'], $item . $label); } else { if ($attributes['escape']) { $name = h($name); } $select[] = $this->Html->useTag('selectoption', $name, $htmlOptions, $title); } } } } return array_reverse($select, true); } /** * Generates option lists for common <select /> menus * * @param string $name List type name. * @param array $options Options list. * @return array */ protected function _generateOptions($name, $options = array()) { if (!empty($this->options[$name])) { return $this->options[$name]; } $data = array(); switch ($name) { case 'minute': if (isset($options['interval'])) { $interval = $options['interval']; } else { $interval = 1; } $i = 0; while ($i < 60) { $data[sprintf('%02d', $i)] = sprintf('%02d', $i); $i += $interval; } break; case 'hour': for ($i = 1; $i <= 12; $i++) { $data[sprintf('%02d', $i)] = $i; } break; case 'hour24': for ($i = 0; $i <= 23; $i++) { $data[sprintf('%02d', $i)] = $i; } break; case 'meridian': $data = array('am' => 'am', 'pm' => 'pm'); break; case 'day': for ($i = 1; $i <= 31; $i++) { $data[sprintf('%02d', $i)] = $i; } break; case 'month': if ($options['monthNames'] === true) { $data['01'] = __d('cake', 'January'); $data['02'] = __d('cake', 'February'); $data['03'] = __d('cake', 'March'); $data['04'] = __d('cake', 'April'); $data['05'] = __d('cake', 'May'); $data['06'] = __d('cake', 'June'); $data['07'] = __d('cake', 'July'); $data['08'] = __d('cake', 'August'); $data['09'] = __d('cake', 'September'); $data['10'] = __d('cake', 'October'); $data['11'] = __d('cake', 'November'); $data['12'] = __d('cake', 'December'); } elseif (is_array($options['monthNames'])) { $data = $options['monthNames']; } else { for ($m = 1; $m <= 12; $m++) { $data[sprintf("%02s", $m)] = strftime("%m", mktime(1, 1, 1, $m, 1, 1999)); } } break; case 'year': $current = (int)date('Y'); $min = !isset($options['min']) ? $current - 20 : (int)$options['min']; $max = !isset($options['max']) ? $current + 20 : (int)$options['max']; if ($min > $max) { list($min, $max) = array($max, $min); } if (!empty($options['value']) && (int)$options['value'] < $min && (int)$options['value'] > 0 ) { $min = (int)$options['value']; } elseif (!empty($options['value']) && (int)$options['value'] > $max) { $max = (int)$options['value']; } for ($i = $min; $i <= $max; $i++) { $data[$i] = $i; } if ($options['order'] !== 'asc') { $data = array_reverse($data, true); } break; } $this->_options[$name] = $data; return $this->_options[$name]; } /** * Sets field defaults and adds field to form security input hash. * Will also add a 'form-error' class if the field contains validation errors. * * ### Options * * - `secure` - boolean whether or not the field should be added to the security fields. * Disabling the field using the `disabled` option, will also omit the field from being * part of the hashed key. * * This method will convert a numerically indexed 'disabled' into an associative * value. FormHelper's internals expect associative options. * * @param string $field Name of the field to initialize options for. * @param array $options Array of options to append options into. * @return array Array of options for the input. */ protected function _initInputField($field, $options = array()) { if (isset($options['secure'])) { $secure = $options['secure']; unset($options['secure']); } else { $secure = (isset($this->request['_Token']) && !empty($this->request['_Token'])); } $disabledIndex = array_search('disabled', $options, true); if (is_int($disabledIndex)) { unset($options[$disabledIndex]); $options['disabled'] = true; } $result = parent::_initInputField($field, $options); if ($this->tagIsInvalid() !== false) { $result = $this->addClass($result, 'form-error'); } $isDisabled = false; if (isset($result['disabled'])) { $isDisabled = ( $result['disabled'] === true || $result['disabled'] === 'disabled' || (is_array($result['disabled']) && !empty($result['options']) && array_diff($result['options'], $result['disabled']) === array() ) ); } if ($isDisabled) { return $result; } if (!isset($result['required']) && $this->_introspectModel($this->model(), 'validates', $this->field()) ) { $result['required'] = true; } if ($secure === static::SECURE_SKIP) { return $result; } $this->_secure($secure, $this->_secureFieldName($options)); return $result; } /** * Get the field name for use with _secure(). * * Parses the name attribute to create a dot separated name value for use * in secured field hash. * * @param array $options An array of options possibly containing a name key. * @return string|null */ protected function _secureFieldName($options) { if (isset($options['name'])) { preg_match_all('/\[(.*?)\]/', $options['name'], $matches); if (isset($matches[1])) { return $matches[1]; } } return null; } /** * Sets the last created form action. * * @param string|array $url URL. * @return void */ protected function _lastAction($url) { $action = html_entity_decode($this->url($url, true), ENT_QUOTES); $query = parse_url($action, PHP_URL_QUERY); $query = $query ? '?' . $query : ''; $this->_lastAction = parse_url($action, PHP_URL_PATH) . $query; } /** * Set/Get inputDefaults for form elements * * @param array $defaults New default values * @param bool $merge Merge with current defaults * @return array inputDefaults */ public function inputDefaults($defaults = null, $merge = false) { if ($defaults !== null) { if ($merge) { $this->_inputDefaults = array_merge($this->_inputDefaults, (array)$defaults); } else { $this->_inputDefaults = (array)$defaults; } } return $this->_inputDefaults; } }