spec/lib/data/param.cfm
2025-06-02 16:16:51 +03:00

355 lines
19 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!---
bean (child tag)
msyu@mail.ru
2013-08-29 v0.1
2013-09-04 v0.2 tested on CF8 and Railo
2014-01-20 v0.3 added exception for unsupported parameter type
2014-10-24 v0.5 ATTRIBUTES.field became default for ATTRIBUTES.param
2014-11-18 v0.6 rework
2015-07-21 v0.7 EXITTAG moved to the end of file
2015-07-22 v0.8 Changed protocol for skipInsert, skipUpdate, key, readonly, autoincrement, required attributes, now we can set value at runtime like skipInsert="#boolean#" instead of just including or omitting the attribute
2015-12-02 v0.9 Added preprocessor attribute. Fixed timestamp bug ('01.12.2015' treated as 12 JAN 2015)
2015-12-04 v0.12 for dates, submitted value is now back-overwritten
2015-12-04 v0.13 bug fix
v0.94
2017-03-10 v0.15 - scope resolution order changed to CALLER-FORM-URL
2018-05-31 v0.16 - strip whitespace from numbers
2019-02-09 v0.17 - call preprocessor before checking required field
2022-01-06 v0.18 - added BIT type (for PostgreSQL) - в базе представляется boolean!
2023-04-17 v0.19 - форматирование, добавлен other к idstamp (для postgres)
2023-04-18 v0.20 - добавлен атрибут format, для даты
2024-03-22 v0.21 - добавлены синонимы int,boolean,bool. Перенумерованы версии 91->11
2024-10-16 v0.23 - добавлены атрибуты minlength, maxlength, regex, validate
--->
<!--- проверка на неподдерживаемые поля --->
<cfloop collection=#ATTRIBUTES# item="a">
<cfif NOT ListFindNoCase("param,field,type,displayName,default,init,size,scale,skipInsert,skipUpdate,key,readonly,autoincrement,value,null,forNull,eval,required,preprocessor,dtlocale,format,maxlength,minlength,regex,validate", #a#)>
<cfthrow type="UnsupportedAttribute" message='Attribute #a# is unsupported by bean/param' detail='Specified attribute "#a#" is not supported by custom tag bean/param'/>
</cfif>
</cfloop>
<cfparam name="ATTRIBUTES.default" default=""/><!--- подставляется при сохранении, если параметр не передавался. Важно для чекбоксов, они не передаются, если не отмечены --->
<cfparam name="ATTRIBUTES.init" default=#ATTRIBUTES.default#/><!--- передается в форму для новой записи (предзаполнение). Для чекбокса может быть init=1, default=0 - отмеченный по умолчанию --->
<cfparam name="ATTRIBUTES.field"/><!--- имя поля в таблице БД --->
<cfparam name="ATTRIBUTES.param" default="#ATTRIBUTES.field#"/><!--- имя поля в URL/FORM и экспортируемой переменной --->
<cfparam name="ATTRIBUTES.type" default="varchar"/><!--- тип поля в терминах SQL (CF_SQL_*) --->
<cfparam name="ATTRIBUTES.displayName" default=#ATTRIBUTES.param#/><!--- для сообщений об ошибках--->
<cfparam name="ATTRIBUTES.size" default="0"/><!--- for strings, soft limit (causes truncation). 0=unlimited --->
<cfparam name="ATTRIBUTES.scale" default="1"/> <!--- Множитель. Только для чисел, только при записи. --->
<!--- если атрибут записан без значения, он получает значение true! --->
<cfparam name="ATTRIBUTES.skipInsert" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.skipUpdate" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.key" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.autoincrement" type="boolean" default="false"/><!--- правильнее autogenerated, потому что GUID тоже бывает генеренным --->
<cfparam name="ATTRIBUTES.required" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.dtlocale" type="string" default="ru"/><!--- datetime locale--->
<cfparam name="ATTRIBUTES.format" type="string" default=""/><!--- datetime format, normally yyyy-MM-dd, CASE SENSITIVE, NOT YYYY-DD-MM!!!--->
<cfparam name="ATTRIBUTES.maxlength" default="0"/><!--- max length hard limit, throws exception unlike ATTRIBUTES.size. 0=unlimited, специально не указываем тип - то есть это по умолчанию строка - чтобы не ломалось от пустой строки--->
<cfparam name="ATTRIBUTES.minlength" default="0"/><!--- min length hard limit, throws exception unlike ATTRIBUTES.size. 0=unlimited, специально не указываем тип - то есть это по умолчанию строка - чтобы не ломалось от пустой строки--->
<!--- <cfparam name="ATTRIBUTES.regex" type="string" default=""/> мы его закомментировали и оставили, чтобы про него не забыть, а дальше мы проверяем его наличие в скоупе атрибутов ---><!--- validation regex --->
<cfparam name="ATTRIBUTES.validate" type="any" default=#function(x){return ""}#/><!--- non-empty return indicates error --->
<!--- ATTRIBUTES.forNull ---><!--- если forNull не задано, параметр считается обязательным ***?--->
<!--- ATTRIBUTES.preprocessor ---><!--- функция --->
<!--- in-out --->
<cfparam name="ATTRIBUTES.null" type="boolean" default="false"/>
<!--- ATTRIBUTES.value - входной-выходной параметр. --->
<!---/in-out --->
<!--- *** выходные параметры. Странно выглядит. Хоть как-то в имени это отразить, что выходные... --->
<cfparam name="ATTRIBUTES.hasInput" type="boolean" default="false" />
<cfparam name="ATTRIBUTES.submittedValue" type="string" default=""/><!--- необработанное входное значение --->
<cfparam name="ATTRIBUTES.errorState" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.errorMessage" type="string" default=""/>
<cfparam name="ATTRIBUTES.warningState" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.warningMessage" type="string" default=""/>
<!--- Тут намешано 2 стиля, и оба не вызывают восторга
Атрибут декларируется с дефолтом (потому что, в отличие от аргумента функции, мы не можем декларировать необязательный атрибут, так как используем cfparam)
Или атрибут не декларируется, а проверяется его наличие в скоупе атрибутов, что не улучшает читаемость кода --->
<cfassociate basetag="cf_bean" datacollection="params" />
<!--- Поле "только для чтения" не обрабатываем никак, если только оно не ключевое. Зачем может понадобиться readonly ключевое поле - непонятно, но вдруг --->
<cfif NOT (ATTRIBUTES.key)>
<cfif (ATTRIBUTES.readonly)>
<cfexit method="exittag"/>
</cfif>
</cfif>
<!--- Если атрибуту явно указан NULL, дальше ничего не делаем --->
<cfif ATTRIBUTES.null>
<cfset ATTRIBUTES.value="0"/><!--- значение не используется, но переменная должна быть определена --->
<cfexit method="exittag" />
</cfif>
<!--- принимаем значение из передаваемых данных или подставляем значение по умолчанию --->
<!--- можно было бы попробовать cfparam CALLER.#ATTRIBUTES.param# и обрабатывать исключение, но так обходимся без исключений, хотя и не изящно --->
<cfif StructKeyExists(ATTRIBUTES,"value")>
<cfset submittedValue=ATTRIBUTES.value/>
<cfset ATTRIBUTES.hasInput="true"/>
<cfelseif StructKeyExists(CALLER, ATTRIBUTES.param)>
<cfset submittedValue=structFind(CALLER, ATTRIBUTES.param)/>
<cfset ATTRIBUTES.hasInput="true"/>
<cfelseif StructKeyExists(FORM, ATTRIBUTES.param)>
<cfset submittedValue=structFind(FORM, ATTRIBUTES.param)/>
<cfset ATTRIBUTES.hasInput="true"/>
<cfelseif StructKeyExists(URL, ATTRIBUTES.param)>
<cfset submittedValue=structFind(URL, ATTRIBUTES.param)/>
<cfset ATTRIBUTES.hasInput="true"/>
<cfelse>
<!--- если параметр отсутствует в передаваемых данных --->
<cfset submittedValue=ATTRIBUTES.default />
</cfif>
<cfif !StructKeyExists(ATTRIBUTES,"value")>
<cfset ATTRIBUTES.value="" /><!---*** dirty fix--->
</cfif>
<cfset ATTRIBUTES.submittedValue=submittedValue />
<cfif StructKeyExists(ATTRIBUTES,"preprocessor")>
<cfset submittedValue=#ATTRIBUTES.preprocessor(submittedValue)#/>
</cfif>
<!--- check required fields --->
<cfif ATTRIBUTES.required>
<cfif submittedValue EQ "">
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = "Не заполнено обязательное поле #ATTRIBUTES.displayName# "/>
<cfset ATTRIBUTES.value="" />
<cfexit method="exittag" />
</cfif>
</cfif>
<cfif StructKeyExists(ATTRIBUTES, "forNull")>
<cfif #submittedValue# EQ #ATTRIBUTES.forNull#>
<cfset ATTRIBUTES.null="true" />
<cfset ATTRIBUTES.value="" />
<cfexit method="exittag" />
</cfif>
</cfif>
<!--- type validation --->
<cfswitch expression=#ATTRIBUTES.type#>
<!--- для integer, bigint наличие атрибутa autoincrement подавляет сообщение об ошибке --->
<!--- *** можно было сделать для всех, но тогда надо обрабатывать исключения --->
<cfcase value="integer,int,bigint"><!--- *** формат с экспонентой не будет работать --->
<cfset s=reReplace(submittedValue, "[[:space:]]", "", "ALL")/>
<cfif FindOneOf(",.",s)>
<cfset s=ListFirst(s,",.")/>
<cfset ATTRIBUTES.warningState = "true"/>
<cfset ATTRIBUTES.warningMessage = "Number is truncated #ATTRIBUTES.displayName#: #submittedValue#-&gt;#s#"/>
</cfif>
<!---*** не слишком явно - подавление ошибки для автоинкрементного поля --->
<cfif (NOT isNumeric(s))>
<!---<cfif (NOT structKeyExists(ATTRIBUTES, "autoincrement"))>
<cfif (NOT structKeyExists(ATTRIBUTES, "autoincrement")) AND (NOT structKeyExists(ATTRIBUTES, "key"))> --->
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = "Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>
<!---<cfthrow type="custom" message="Invalid number" detail="Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>--->
<!--- по-хорошему, выбрасывать исключение нужно только при попытке сохранения
Но можно и не разбирать в некоторых режимах
--->
<cfelse>
<cfset ATTRIBUTES.value=s*ATTRIBUTES.scale/>
</cfif>
</cfcase>
<cfcase value="guid,uuid,idstamp">
<!--- guid = PostgreSQL hack --->
<cfif NOT isValid("guid", submittedValue) AND len(attributes.submittedvalue) GT 0>
<cfset ATTRIBUTES.errorState = "true" />
<cfset ATTRIBUTES.errorMessage = "[Error]: Value is not a GUID type." />
<!--- <cfthrow type="UnsupportedParameterType"
message="[Error]: Value is not a GUID type."
detail='Value is: #attributes.submittedvalue#'/> --->
<cfelse>
<cfset ATTRIBUTES.value = submittedValue />
</cfif>
</cfcase>
<cfcase value="numeric,decimal,float,real,double">
<cfset s=replace(reReplace(submittedValue, "[[:space:]]", "", "ALL"), ",", ".", "ALL")/>
<cfif NOT isNumeric(s)>
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = "Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>
<!---<cfthrow type="custom" message="Invalid number" detail="Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>--->
<cfelse>
<cfset ATTRIBUTES.value=s*ATTRIBUTES.scale/>
</cfif>
</cfcase>
<cfcase value="bit,boolean,bool"><!--- потому что транслируем примитивно в CF_SQL_BIT--->
<cfset s=trim(submittedValue)/> <!--- нужно ли это? пробелы действительно дают ошибку --->
<cfif NOT isBoolean(s)>
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = "Invalid boolean (#ATTRIBUTES.displayName#: #submittedValue#)"/>
<cfelse>
<!---<cfif s>
<cfset ATTRIBUTES.value=true/>
<cfelse>
<cfset ATTRIBUTES.value=false/>
</cfif>--->
<cfset ATTRIBUTES.value=s/>
</cfif>
</cfcase>
<cfcase value="date,time,timestamp">
<cftry>
<!--- <cfset ATTRIBUTES.value=LSParseDateTime(submittedValue,"#ATTRIBUTES.dtlocale#")/> --->
<cfset ATTRIBUTES.value=LSParseDateTime(submittedValue,"#ATTRIBUTES.dtlocale#","#ATTRIBUTES.format#")/>
<cfset ATTRIBUTES.submittedValue=ATTRIBUTES.value/><!--- *** dirty fix--->
<cfcatch type="ANY">
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = "Invalid datetime (#ATTRIBUTES.displayName#: #submittedValue#)"/>
<!---<cfthrow type="custom" message="Invalid datetime" detail="Invalid datetime (#ATTRIBUTES.displayName#: #submittedValue#)"/>--->
</cfcatch>
</cftry>
<!--- пробрасывать сырое значение при ошибке разбора плохо, потому что функция форматирования даты споткнется--->
</cfcase>
<cfcase value="varchar">
<cfset submittedValue=trim(submittedValue) />
<cfparam name="ATTRIBUTES.default" type="string" default=""/>
<cfset ATTRIBUTES.value=submittedValue/>
<cfif ATTRIBUTES.maxlength GT 0>
<cfif len(submittedValue) GT ATTRIBUTES.maxlength>
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = "Длина #ATTRIBUTES.displayName# превышает #ATTRIBUTES.maxlength#"/>
<cfset ATTRIBUTES.value="" />
<cfexit method="exittag" />
</cfif>
</cfif>
<cfif ATTRIBUTES.minlength GT 0>
<cfif len(submittedValue) LT ATTRIBUTES.minlength>
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = "Длина #ATTRIBUTES.displayName# меньше #ATTRIBUTES.minlength#"/>
<cfset ATTRIBUTES.value="" />
<cfexit method="exittag" />
</cfif>
</cfif>
<cfif ATTRIBUTES.size GT 0>
<cfif len(submittedValue) GT ATTRIBUTES.size>
<!--- truncate input --->
<cfset ATTRIBUTES.value=left(submittedValue, ATTRIBUTES.size)/>
<cfset ATTRIBUTES.warningState = "true"/>
<cfset ATTRIBUTES.warningMessage = "String is truncated (#ATTRIBUTES.displayName#: #len(submittedValue)#-&gt;#ATTRIBUTES.size#)"/>
</cfif>
</cfif>
</cfcase>
<!---<cfcase value="guid,uuid,uniqueidentifier,idstamp">--->
<cfcase value="other">
<!--- other = old PostgreSQL hack (incompatibility) --->
<!--- validation skipped --->
<cfset ATTRIBUTES.value = submittedValue />
</cfcase>
<cfcase value="varbinary">
<cfset ATTRIBUTES.value=submittedValue/>
</cfcase>
<cfdefaultcase>
<cfset ATTRIBUTES.value=""/>
<cfthrow type="UnsupportedParameterType" message="Specified parameter type is unsupported by current version of bean/param" detail='Parameter type "#ATTRIBUTES.type#" is not supported by custom tags bean/param'/>
</cfdefaultcase>
<!---<cfcase value="hex">
<cfset s=submittedValue/>
<cfif reFindNoCase("[^0-9A-F]", s)>
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = "Invalid HEXADECIMAL number (#ATTRIBUTES.displayName#: #submittedValue#)"/>
<cfelse>
<cftry>
<cfset ATTRIBUTES.value=inputBaseN(s, 16)/>
<cfcatch type="any">
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = "Error converting from HEXADECIMAL format (#ATTRIBUTES.displayName#: #submittedValue#)"/>
</cfcatch>
</cftry>
</cfif>
</cfcase>--->
</cfswitch>
<!--- regular expression validation --->
<cfif StructKeyExists(ATTRIBUTES,"regex")>
<cfif reFind(ATTRIBUTES.regex,submittedValue) EQ 0>
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = 'Pattern "#ATTRIBUTES.regex#" does not match "#submittedValue#"'/>
<cfset ATTRIBUTES.value="" />
<cfexit method="exittag" />
</cfif>
</cfif>
<!--- custom validation --->
<cfif StructKeyExists(ATTRIBUTES,"validate")>
<cfset validationMessage=ATTRIBUTES.validate(submittedValue)/>
<cfif len(validationMessage)>
<cfset ATTRIBUTES.errorState = "true"/>
<cfset ATTRIBUTES.errorMessage = validationMessage/>
<cfset ATTRIBUTES.value="" />
<cfexit method="exittag" />
</cfif>
</cfif>
<cfexit method="EXITTAG" />
<!--- это искусственный прием
<cfif ATTRIBUTES.errorState AND NOT structKeyExists(ATTRIBUTES, "autoincrement") AND NOT structKeyExists(ATTRIBUTES, "key")>
<cfset getBaseTagData("cf_bean").validationOk="false"/>
</cfif>
--->
<!---
CF_SQL_BIGINT
CF_SQL_BIT
CF_SQL_CHAR
CF_SQL_BLOB
CF_SQL_CLOB
CF_SQL_DATE
CF_SQL_DECIMAL
CF_SQL_DOUBLE
CF_SQL_FLOAT
CF_SQL_IDSTAMP
CF_SQL_INTEGER
CF_SQL_LONGVARCHAR
CF_SQL_MONEY
CF_SQL_MONEY4
CF_SQL_NUMERIC
CF_SQL_REAL
CF_SQL_REFCURSOR
CF_SQL_SMALLINT
CF_SQL_TIME
CF_SQL_TIMESTAMP
CF_SQL_TINYINT
CF_SQL_VARCHAR
*** CF_SQL_OTHER - используется с Postgre для GUID и jsonb
--->
<!--- при ошибочном вводе, например, некорректном формате, возможно два пути
- сохранить, подставив дефолт (в том числе нулл). Можно генерировать ворнинг, цепляя его прямо к полю. В этом случае надо требовать корректные дефолты
- можно ввести раздельные понятия дефолт и инит.
Разница между дефолтом и инитом. При инициализации новой
Как дефолт, так и инит имеют целевой тип (число, дата...)
Дефолт и инит используются для начального заполнения полей нового объекта. Для строки дефолт-дефолт - пустая, для остальных нулл
Рассмотреть обязательные и необязательные параметры?
Можно
- выгрузить некорректный набор данных для редактирования. При втором варианте нужно все выгружать в стринги? Тогда как будут работать функции форматирования в теле страницы, ожидающе не строку? Скажем, дату? По ближайшей догадке? Пустую строку?
Попытка вставить нулл в колонку NOT NULL должна пресекаться заранее (до ошибки БД)
Нулловые строки можно не рассматривать, приводя к пустой строке.
Тэг принимает параметр от формы, парсит и отдает в атрибуте value. Если не получает, не отдает, можно это специально указывать флагом. Можно ему лишнего не делегировать.
Остальное оставить вызывающему тегу?
--->