Commit 62a95292 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Merge branch '5414-checktype-auto' into 'master'

5414 checktype auto

See merge request !39
parents 5ffcd33f 4ccd1dcf
......@@ -982,12 +982,28 @@ For QFQ variables and FormElements:
Only in FormElement:
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **auto** | Form | | Only supported for FormElements. Most suitable checktype is dynamically evaluated based |
| | | | native column definition, the FormElement type, and other info. See below for details. |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **email** | Form | Query | [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,} |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **pattern** | Form | | Compares the value against a regexp. |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
Rules for CheckType Auto (by priority):
* TypeAheadSQL or TypeAheadLDAP defined: **alnumx**
* Table definition
* integer type: **digit**
* floating point number: **numerical**
* FE Type
* 'password', 'note': **all**
* 'editor', 'text' and encode = 'specialchar': **all**
* None of the above: **alnumx**
.. _`variable-escape`:
Escape
......@@ -2327,9 +2343,10 @@ Fields:
+---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+
|Encode | 'none', 'specialchar' | With 'specialchar' (default) the chars <>"'& will be encoded to their htmlentity. _`field-encode` |
+---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+
|Check Type | enum('alnumx','digit', | _`field-checktype` |
| | 'numerical','email', | |
| | 'pattern','allbut','all') | |
|Check Type | enum('auto', 'alnumx', | _`sanitize-class` |
| | 'digit', 'numerical', | |
| | 'email', 'pattern', | |
| | 'allbut', 'all') | |
+---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+
|Check Pattern | 'regexp' |_`field-checkpattern`: If $checkType=='pattern': pattern to match |
+---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+
......
......@@ -84,6 +84,7 @@ abstract class AbstractBuildForm {
* @param array $formSpec
* @param array $feSpecAction
* @param array $feSpecNative
* @param array $db
*/
public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $db) {
$this->formSpec = $formSpec;
......@@ -165,11 +166,13 @@ abstract class AbstractBuildForm {
*
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
*
* @return string|array $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all
* @param bool $htmlElementNameIdZero
* @param array $latestFeSpecNative
* @return array|string $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all
* formElement.dynamicUpdate-yes values/states
* @throws CodeException
* @throws DbException
* @throws \qfq\UserFormException
* @throws UserFormException
*/
public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) {
$htmlHead = '';
......@@ -1095,8 +1098,6 @@ abstract class AbstractBuildForm {
$attribute .= Support::doAttribute('data-match', '[name=' . str_replace(':', '\\:', $htmlFormElementNamePrimary) . ']');
}
$this->adjustMaxLength($formElement);
if ($formElement[FE_MAX_LENGTH] > 0 && $value !== '') {
// crop string only if it's not empty (substr returns false on empty strings)
$value = substr($value, 0, $formElement[FE_MAX_LENGTH]);
......@@ -1268,186 +1269,6 @@ abstract class AbstractBuildForm {
return $sql;
}
/**
* Calculates the maxlength of an input field, based on formElement type, formElement user definition and
* table.field definition.
*
* @param array $formElement
*/
private function adjustMaxLength(array &$formElement) {
// MIN( $formElement['maxLength'], tabledefinition)
$maxLength = $this->getColumnSize($formElement);
$feMaxLength = false;
switch ($formElement[FE_TYPE]) {
case 'date':
$feMaxLength = 10;
break;
case 'datetime':
$feMaxLength = 19;
break;
case 'time':
$feMaxLength = 8;
break;
}
// In case there is no limit of the underlying table column, or a non primary table column, and the FE_TYPE is date/time.
if ($maxLength === false && $feMaxLength !== false) {
$maxLength = $feMaxLength;
}
// In case the underlying table column is not of type date/time, the $maxLength might be to high: correct
if ($feMaxLength !== false && $maxLength !== false && $feMaxLength < $maxLength) {
$maxLength = $feMaxLength;
}
// date/datetime
if ($maxLength !== false) {
if (is_numeric($formElement['maxLength']) && $formElement['maxLength'] != 0) {
if ($formElement['maxLength'] > $maxLength) {
$formElement['maxLength'] = $maxLength;
}
} else {
$formElement['maxLength'] = $maxLength;
}
}
}
/**
* Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
*
* @param $formElement
* @return bool|int a) 'false' if there is no length definition, b) length definition, c)
* date|time|datetime|timestamp use hardcoded length
*
*/
private function getColumnSize(array &$formElement) {
$column = $formElement[FE_NAME];
$matches = array();
$inputType = 'number';
$typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);
switch ($typeSpec) {
case 'date': // yyyy-mm-dd
return 10;
case 'datetime': // yyyy-mm-dd hh:mm:ss
case 'timestamp': // yyyy-mm-dd hh:mm:ss
return 19;
case 'time': // hh:mm:ss
return 8;
default:
if (substr($typeSpec, 0, 4) === 'set(' || substr($typeSpec, 0, 5) === 'enum(') {
return $this->maxLengthSetEnum($typeSpec);
}
break;
}
// $typeSpec = 'tinyint(3) UNSIGNED NOT NULL' | 'int(11) NOT NULL'
$arr = explode(' ', $typeSpec, 2);
if (empty($arr[1])) {
$sign = 'signed';
} else {
$arr = explode(' ', $arr[1], 2);
$sign = $arr[0] == 'unsigned' ? 'unsigned' : 'signed';
}
$arr = explode('(', $typeSpec, 2);
$token = $arr[0];
# s: signed, u: unsigned.
# s-min, s-max, s-checktype, u-min, u-max, u-checktype
$control = [
'tinyint' => [-128, 127, SANITIZE_ALLOW_NUMERICAL, 0, 255, SANITIZE_ALLOW_DIGIT],
'smallint' => [-32768, 32767, SANITIZE_ALLOW_NUMERICAL, 0, 65535, SANITIZE_ALLOW_DIGIT],
'mediumint' => [-8388608, 8388607, SANITIZE_ALLOW_NUMERICAL, 0, 16777215, SANITIZE_ALLOW_DIGIT],
'int' => [0, 4294967295, SANITIZE_ALLOW_NUMERICAL, -2147483648, 2147483647, SANITIZE_ALLOW_DIGIT],
'bigint' => [-9223372036854775808, 9223372036854775807, SANITIZE_ALLOW_NUMERICAL, 0, 18446744073709551615, SANITIZE_ALLOW_DIGIT],
];
$min = false;
$max = false;
$checkType = false;
switch ($token) {
case 'tinyint':
case 'smallint':
case 'mediumint':
case 'int':
case 'bigint':
$arr = $control[$token];
if ($sign == 'signed') {
$min = $arr[0];
$max = $arr[1];
$checkType = $arr[2];
} else {
$min = $arr[3];
$max = $arr[4];
$checkType = $arr[5];
}
break;
case 'decimal':
case 'float':
case 'double':
$checkType = SANITIZE_ALLOW_NUMERICAL;
$inputType = 'text';
break;
case 'bit':
$checkType = SANITIZE_ALLOW_DIGIT;
break;
}
if ($min !== false && $formElement[FE_MIN] == '') {
$formElement[FE_MIN] = $min;
}
if ($max !== false && $formElement[FE_MAX] == '') {
$formElement[FE_MAX] = $max;
}
// if given, force Checktype
if ($checkType !== false) {
$formElement[FE_CHECK_TYPE] = $checkType;
if (empty($formElement[FE_INPUT_TYPE])) {
$formElement[FE_INPUT_TYPE] = $inputType;
}
}
// e.g.: string(64) >> 64, enum('yes','no') >> false
if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
if (is_numeric($matches[1]))
return $matches[1];
}
return false;
}
/**
* Get the strlen of the longest element in enum('val1','val2',...,'valn') or set('val1','val2',...,'valn')
*
* @param string $typeSpec
*
* @return int
*/
private function maxLengthSetEnum($typeSpec) {
$startPos = (substr($typeSpec, 0, 4) === 'set(') ? 4 : 5;
$max = 0;
$valueList = substr($typeSpec, $startPos, strlen($typeSpec) - $startPos - 1);
$valueArr = explode(',', $valueList);
foreach ($valueArr as $value) {
$value = trim($value, "'");
$len = strlen($value);
if ($len > $max) {
$max = $len;
}
}
return $max;
}
/**
* Builds a HTML attribute list, based on $attributeList.
*
......@@ -3137,7 +2958,6 @@ abstract class AbstractBuildForm {
$attribute .= Support::doAttribute('name', $htmlFormElementName);
$attribute .= Support::doAttribute('class', 'form-control');
$this->adjustMaxLength($formElement);
$showTime = ($formElement[FE_TYPE] == 'time' || $formElement[FE_TYPE] == 'datetime') ? 1 : 0;
if ($value == 'CURRENT_TIMESTAMP') {
$value = date('Y-m-d H:i:s');
......@@ -3283,7 +3103,6 @@ abstract class AbstractBuildForm {
// throw new UserFormException("Checktype not applicable for date/time: '" . $formElement['checkType'] . "'", ERROR_NOT_APPLICABLE);
// }
$this->adjustMaxLength($formElement);
$showTime = ($formElement[FE_TYPE] == 'time' || $formElement[FE_TYPE] == 'datetime') ? 1 : 0;
$value = Support::convertDateTime($value, $formElement[FE_DATE_FORMAT], $formElement[FE_SHOW_ZERO], $showTime, $formElement[FE_SHOW_SECONDS]);
......@@ -3332,8 +3151,6 @@ abstract class AbstractBuildForm {
//TODO plugin autoresize nutzen um Editorgroesse anzugeben
$this->adjustMaxLength($formElement);
$attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
$attribute .= Support::doAttribute('name', $htmlFormElementName);
// $attribute .= Support::doAttribute('id', $htmlFormElementName);
......
......@@ -65,6 +65,7 @@ const NAME_TG_COPIES = '_tgCopies'; // Number of templatesGroup copies to creat
const FE_TG_INDEX = '_tgIndex'; // Index of the current copy of a templateGroup FE.
// SANITIZE Classifier
const SANITIZE_ALLOW_AUTO = "auto"; // Default for FormElements
const SANITIZE_ALLOW_ALNUMX = "alnumx";
const SANITIZE_ALLOW_DIGIT = "digit";
const SANITIZE_ALLOW_NUMERICAL = "numerical";
......@@ -72,7 +73,7 @@ const SANITIZE_ALLOW_EMAIL = "email";
const SANITIZE_ALLOW_PATTERN = "pattern";
const SANITIZE_ALLOW_ALLBUT = "allbut";
const SANITIZE_ALLOW_ALL = "all";
const SANITIZE_DEFAULT = SANITIZE_ALLOW_DIGIT;
const SANITIZE_DEFAULT = SANITIZE_ALLOW_DIGIT; // for {{variable}} expressions without checkType
const SANITIZE_EXCEPTION = 'exception';
const SANITIZE_EMPTY_STRING = 'empty string';
......
......@@ -104,6 +104,7 @@ $UPDATE_ARRAY = array(
],
'0.25.12' => [
"ALTER TABLE `FormElement` CHANGE `checkType` `checkType` ENUM('auto','alnumx','digit','numerical','email','pattern','allbut','all') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'auto';",
"ALTER TABLE `FormElement` CHANGE `label` `label` VARCHAR(511) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT ''",
],
......
......@@ -681,7 +681,6 @@ class Support {
*/
public static function mergeUrlComponents($host, $hostOrPath, $query) {
$url = '';
$pre = '';
if ($host != '' && substr($host, -1, 1) != '/') {
......@@ -716,8 +715,10 @@ class Support {
* Set Defaults for the current formElement.
*
* @param array $formElement
*
* @param array $formSpec
* @return array
*
* @throws CodeException
*/
public static function setFeDefaults(array $formElement, array $formSpec = array()) {
......@@ -774,14 +775,139 @@ class Support {
self::setIfNotSet($formElement, FE_MIN);
self::setIfNotSet($formElement, FE_MAX);
self::setIfNotSet($formElement, FE_DECIMAL_FORMAT);
self::setIfNotSet($formElement, F_FE_DATA_PATTERN_ERROR);
$typeSpec = $store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES);
self::adjustFeToColumnDefinition($formElement, $typeSpec);
return $formElement;
}
/**
* Adjusts several FE parameters using smart guesses based on the table column definition and other parameters.
*
* @param array $formElement
* @param $typeSpec
* @throws UserFormException
*/
public static function adjustFeToColumnDefinition(array &$formElement, $typeSpec) {
self::adjustMaxLength($formElement, $typeSpec);
self::adjustDecimalFormat($formElement, $typeSpec);
// Make educated guesses about the desired $min, $max, $checkType, and $inputType
// $typeSpec = 'tinyint(3) UNSIGNED NOT NULL' | 'int(11) NOT NULL'
$arr = explode(' ', $typeSpec, 2);
if (empty($arr[1])) {
$sign = 'signed';
} else {
$arr = explode(' ', $arr[1], 2);
$sign = $arr[0] == 'unsigned' ? 'unsigned' : 'signed';
}
$arr = explode('(', $typeSpec, 2);
$token = $arr[0];
# s: signed, u: unsigned.
# s-min, s-max, s-checktype, u-min, u-max, u-checktype
$control = [
'tinyint' => [-128, 127, SANITIZE_ALLOW_NUMERICAL, 0, 255, SANITIZE_ALLOW_DIGIT],
'smallint' => [-32768, 32767, SANITIZE_ALLOW_NUMERICAL, 0, 65535, SANITIZE_ALLOW_DIGIT],
'mediumint' => [-8388608, 8388607, SANITIZE_ALLOW_NUMERICAL, 0, 16777215, SANITIZE_ALLOW_DIGIT],
'int' => [-2147483648, 2147483647, SANITIZE_ALLOW_NUMERICAL, 0, 4294967295, SANITIZE_ALLOW_DIGIT],
'bigint' => [-9223372036854775808, 9223372036854775807, SANITIZE_ALLOW_NUMERICAL, 0, 18446744073709551615, SANITIZE_ALLOW_DIGIT],
];
$min = '';
$max = '';
$checkType = SANITIZE_ALLOW_ALNUMX;
$inputType = '';
$isANumber = true;
switch ($formElement[FE_TYPE]) {
case FE_TYPE_PASSWORD:
case FE_TYPE_NOTE:
$checkType = SANITIZE_ALLOW_ALL;
break;
// decimalFormat
case FE_TYPE_EDITOR:
case FE_TYPE_TEXT:
if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR)
$checkType = SANITIZE_ALLOW_ALL;
break;
}
switch ($token) {
case 'tinyint':
case 'smallint':
case 'mediumint':
case 'int':
case 'bigint':
$inputType = 'number';
$arr = $control[$token];
if ($sign == 'signed') {
$min = $arr[0];
$max = $arr[1];
$checkType = $arr[2];
} else {
$min = $arr[3];
$max = $arr[4];
$checkType = $arr[5];
}
break;
case 'decimal':
case 'float':
case 'double':
$checkType = SANITIZE_ALLOW_NUMERICAL;
break;
case 'bit':
$inputType = 'number';
$checkType = SANITIZE_ALLOW_DIGIT;
break;
default:
$isANumber = false;
break;
}
// Numbers don't need a maxLength because they are being handled by min/max and/or decimalFormat
if ($isANumber)
$formElement[FE_MAX_LENGTH] = '';
if (!empty($formElement[FE_TYPEAHEAD_SQL]) || !empty($formElement[FE_TYPEAHEAD_LDAP])) {
$inputType = '';
$checkType = SANITIZE_ALLOW_ALNUMX;
}
// Set parameters if not set by user
if ($formElement[FE_CHECK_TYPE] === SANITIZE_ALLOW_AUTO) {
$formElement[FE_CHECK_TYPE] = $checkType;
}
if (empty($formElement[FE_MIN])) $formElement[FE_MIN] = $min;
if (empty($formElement[FE_MAX])) $formElement[FE_MAX] = $max;
if (empty($formElement[FE_INPUT_TYPE])) $formElement[FE_INPUT_TYPE] = $inputType;
}
/**
* Sets the decimalFormat of a FormElement based on the parameter definition and table.field definition
* Affected FormElement fields: FE_DECIMAL_FORMAT
*
* @param array $formElement
* @param $typeSpec
* @throws UserFormException
*/
private function adjustDecimalFormat(array &$formElement, $typeSpec) {
if (isset($formElement[FE_DECIMAL_FORMAT])) {
if ($formElement[FE_DECIMAL_FORMAT] === '') {
// Get decimal format from column definition
$fieldTypeDefinition = $store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES);
if ($fieldTypeDefinition !== false) {
$fieldTypeInfoArray = preg_split("/[()]/", $fieldTypeDefinition);
if ($typeSpec !== false) {
$fieldTypeInfoArray = preg_split("/[()]/", $typeSpec);
if ($fieldTypeInfoArray[0] === 'decimal')
$formElement[FE_DECIMAL_FORMAT] = $fieldTypeInfoArray[1];
}
......@@ -792,15 +918,118 @@ class Support {
$decimalFormatArray = explode(',', $formElement[FE_DECIMAL_FORMAT]);
$isValidDecimalFormat = $decimalFormatArray[0] >= $decimalFormatArray[1];
}
if (!$isValidDecimalFormat)
throw new UserFormException("Invalid decimalFormat.", ERROR_INVALID_DECIMAL_FORMAT);
if (!$isValidDecimalFormat) {
throw new UserFormException("Invalid decimalFormat: '" . $formElement[FE_DECIMAL_FORMAT] . "'", ERROR_INVALID_DECIMAL_FORMAT);
}
}
}
self::setIfNotSet($formElement, FE_DECIMAL_FORMAT);
}
self::setIfNotSet($formElement, F_FE_DATA_PATTERN_ERROR);
/**
* Calculates the maxLength of an input field, based on formElement type, formElement user definition and
* table.field definition.
* Affected formElement fields: FE_MAX_LENGTH
*
* @param array $formElement
* @param $typeSpec
*/
private function adjustMaxLength(array &$formElement, $typeSpec) {
return $formElement;
// MIN( $formElement['maxLength'], table definition)
$maxLength = self::getColumnSize($typeSpec);
$feMaxLength = false;
switch ($formElement[FE_TYPE]) {
case 'date':
$feMaxLength = 10;
break;
case 'datetime':
$feMaxLength = 19;
break;
case 'time':
$feMaxLength = 8;
break;
}
// In case there is no limit of the underlying table column, or a non primary table column, and the FE_TYPE is date/time.
if ($maxLength === false && $feMaxLength !== false) {
$maxLength = $feMaxLength;
}
// In case the underlying table column is not of type date/time, the $maxLength might be too high: correct
if ($feMaxLength !== false && $maxLength !== false && $feMaxLength < $maxLength) {
$maxLength = $feMaxLength;
}
// date/datetime
if ($maxLength !== false) {
if (is_numeric($formElement[FE_MAX_LENGTH]) && $formElement[FE_MAX_LENGTH] != 0) {
if ($formElement[FE_MAX_LENGTH] > $maxLength) {
$formElement[FE_MAX_LENGTH] = $maxLength;
}
} else {
$formElement[FE_MAX_LENGTH] = $maxLength;
}
}
}
/**
* Get column spec from table definition and parse size of it. If nothing defined, return false.
*
* @param $typeSpec
* @return bool|int a) 'false' if there is no length definition, b) length definition, c)
* date|time|datetime|timestamp use hardcoded length
*/
private function getColumnSize($typeSpec) {
$matches = array();
switch ($typeSpec) {
case 'date': // yyyy-mm-dd
return 10;
case 'datetime': // yyyy-mm-dd hh:mm:ss
case 'timestamp': // yyyy-mm-dd hh:mm:ss
return 19;
case 'time': // hh:mm:ss
return 8;
default:
if (substr($typeSpec, 0, 4) === 'set(' || substr($typeSpec, 0, 5) === 'enum(') {
return self::maxLengthSetEnum($typeSpec);
}
break;
}
// e.g.: string(64) >> 64, enum('yes','no') >> false
if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
if (is_numeric($matches[1]))
return $matches[1];
}
return false;
}
/**
* Get the strlen of the longest element in enum('val1','val2',...,'valn') or set('val1','val2',...,'valn')
*
* @param string $typeSpec
*
* @return int
*/
private function maxLengthSetEnum($typeSpec) {
$startPos = (substr($typeSpec, 0, 4) === 'set(') ? 4 : 5;
$max = 0;
$valueList = substr($typeSpec, $startPos, strlen($typeSpec) - $startPos - 1);
$valueArr = explode(',', $valueList);
foreach ($valueArr as $value) {
$value = trim($value, "'");
$len = strlen($value);
if ($len > $max) {
$max = $len;
}
}
return $max;
}
/**
......
......@@ -83,7 +83,7 @@ CREATE TABLE IF NOT EXISTS `FormElement` (
'afterSave', 'afterInsert', 'afterUpdate', 'afterDelete', 'sendMail', 'paste') NOT NULL DEFAULT 'text',
`subrecordOption` SET('edit', 'delete', 'new') NOT NULL DEFAULT '',
`encode` ENUM('none', 'specialchar') NOT NULL DEFAULT 'specialchar',
`checkType` ENUM('alnumx', 'digit', 'numerical', 'email', 'pattern', 'allbut', 'all') NOT NULL DEFAULT 'alnumx',
`checkType` ENUM('auto', 'alnumx', 'digit', 'numerical', 'email', 'pattern', 'allbut', 'all') NOT NULL DEFAULT 'auto',