research: payg-report полная копия репы (все файлы, без .git)

This commit is contained in:
2026-05-30 14:23:10 +03:00
parent 032b41b3f5
commit 876e03b78b
180 changed files with 13907 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
<!---
Access check aka проверка доступа.
Если доступа нет, код, заключенный внутри тега, не выполняется, текст не выводится.
Два варианта использования.
1. При заданном значении действия проверяется наличие этого доступа к объекту, ответ да-нет
2. Проверяется уровень доступа к заданному объекту
Для пустого объекта
Если задать строковый атрибут output, в переменную с таким именем запишется
TRUE(доступ разрешен)/FALSE(доступ запрещен).
Тег обязательно должен быть закрыт.
---><cfsilent>
<!--- проверка на закрытость тега - без закрытия возможна неоднозначность со вложенными тегами --->
<cfif NOT thisTag.hasEndTag>
<cfthrow
type="custom" message="Unclosed AC.CFM tag found."
detail="AC.CFM tag must be closed. Self-closed tag is acceptable, e.g.: &lt;cfmodule template=""mod/ac.cfm"" /&gt;.">
</cfif>
<cfif thisTag.ExecutionMode is 'start'>
<cfparam name="ATTRIBUTES.obj" type="string" default=""/><!---
объект, к которому проверяется доступ --->
<cfparam name="ATTRIBUTES.act" type="numeric" default="#request.PERMISSION_READ#"/><!---
действие, которое проверяется --->
<cfparam name="ATTRIBUTES.output" type="string" default=""/>
<cfparam name="ATTRIBUTES.permissionOut" type="string" default=""/>
<cfset permission=request.PERMISSION_NONE/>
<cfif ATTRIBUTES.obj EQ "">
<cfif #request.usrAuthenticated()#>
<cfset permission=request.PERMISSION_WRITE/>
<cfelse>
<cfset permission=request.PERMISSION_READ/>
</cfif>
<cfset permitted=(ATTRIBUTES.act EQ #request.PERMISSION_READ# OR #request.usrAuthenticated()#)/>
<cfelse>
<cftry>
<cfset permitted=(len(ATTRIBUTES.obj) GT 0)>
<!---*** Максимальный доступ для перечисленных объектов и ролей
Можно подумать о минимальном--->
<cfquery name="qACL" datasource="#request.DS#">
select coalesce(max(permission),0) as permission
from usr_role ur
join acl on acl.role_id=ur.role_id
where ur.usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value="#request.usr_id#">
and acl.obj in (<cfqueryparam cfsqltype="cf_sql_varchar" value="#ATTRIBUTES.obj#" list="true">)
</cfquery>
<cfset permitted=(qACL.permission GE ATTRIBUTES.act)>
<cfset permission=#qACL.permission#/>
<cfcatch type="database">
<cfset permitted="Yes"/><!---***--->
<cfrethrow/>
</cfcatch>
</cftry>
</cfif>
<cfif len(ATTRIBUTES.output)>
<cfset setVariable("CALLER.#ATTRIBUTES.output#", permitted)><!---boolean--->
</cfif>
<cfif len(ATTRIBUTES.permissionOut)>
<cfset setVariable("CALLER.#ATTRIBUTES.permissionOut#", permission)><!---integer 0 1 2--->
</cfif>
<cfif NOT permitted><cfexit method="EXITTAG"></cfif>
</cfif><!---thisTag.ExecutionMode is 'start'--->
</cfsilent>
@@ -0,0 +1,12 @@
<!---v0.2 19:01 06.10.2020
---><cfparam name="ATTRIBUTES.contenttype"/>
<cfswitch expression=#ATTRIBUTES.contenttype#>
<cfcase value="text/plain"><img src="img/mimetype/text.png"/></cfcase>
<cfcase value="text/html"><img src="img/mimetype/html.png"/></cfcase>
<cfcase value="application/pdf"><img src="img/mimetype/pdf.png"/></cfcase>
<cfcase value="image/gif,image/png,image/jpeg"><img src="img/mimetype/image.png"/></cfcase>
<cfcase value="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"><img src="img/mimetype/excel.png"/></cfcase>
<cfcase value="application/vnd.ms-powerpoint"><img src="img/mimetype/powerpoint.png"/></cfcase>
<cfcase value="application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"><img src="img/mimetype/word.png"/></cfcase>
<cfdefaultcase><img src="img/mimetype/document.png"/></cfdefaultcase>
</cfswitch><cfexit method="exittag"/>
@@ -0,0 +1,23 @@
<cfsilent>
<cfparam name="ATTRIBUTES.name"/>
<cfparam name="ATTRIBUTES.value" default="1"/>
<cfif structKeyExists(ATTRIBUTES,"checked")>
<cfset checked=!(ATTRIBUTES.checked EQ "0" OR ATTRIBUTES.checked EQ "")/>
<cfelse>
<cfset checked=!(ATTRIBUTES.container[ATTRIBUTES.name] EQ "0" OR ATTRIBUTES.container[ATTRIBUTES.name] EQ "")/>
</cfif>
<cfparam name="ATTRIBUTES.suffix" default=""/>
<cfparam name="ATTRIBUTES.id" default="#ATTRIBUTES.name##ATTRIBUTES.suffix#"/>
<cfparam name="ATTRIBUTES.disabled" type="boolean" default="No"/>
<cfset attributeList="name,value,checked,container,suffix,id,disabled"/>
</cfsilent><cfoutput><input type="checkbox" name="#ATTRIBUTES.name#"<!---
---><cfif len(ATTRIBUTES.id)> id=#ATTRIBUTES.id#</cfif> value="#ATTRIBUTES.value#"<!---
---><cfif checked> checked="checked"</cfif><!---
---><cfif ATTRIBUTES.disabled> disabled="disabled"</cfif><!---
passthrough additional attributes
---><cfloop collection=#ATTRIBUTES# item="attr"><!---
---><cfif NOT listFindNoCase(attributeList,attr)> #attr#="#structFind(ATTRIBUTES,attr)#"</cfif><!---
---></cfloop>/></cfoutput><cfexit method="exittag"/>
@@ -0,0 +1,30 @@
<cfparam name="ATTRIBUTES.lang" type="string" default=""/>
<cfparam name="request.language" type="string" default=""/>
<cfif len(ATTRIBUTES.lang) AND #ATTRIBUTES.lang# NEQ #request.language#>
<cfexit method="exittag"/>
</cfif>
<cfif thisTag.executionMode IS "end">
<!--- <cfloop collection=#CALLER# item="item">
<cfset structInsert(variables, item, CALLER[item], false)/>
</cfloop> --->
<cfassociate basetag="cf_table" datacollection="columns"/>
<cfset attributeList=""/> <!--- *** сделать проброс атрибутов --->
<cfparam name="thisTag.th" type="array" default=#[{content="",field=""}]#/><!---контейнер для данных вложенного элемента TH. Предусмотрен только один элемент массива, остальные игнорируются--->
<!---<cfparam name="thisTag.td" type="array" default=#[{content="",field=""}]#/>--->
<cfparam name="thisTag.td" type="array" default=#thisTag.th#/><!---контейнер для данных вложенного элемента TD. Предусмотрен только один элемент массива, остальные игнорируются--->
<!---Зачем массив? - потому что массив вложенных тегов, а у нас считается только первый вложенный тег TD--->
<cfset ATTRIBUTES.td=#thisTag.td#/><!---поле, видное включающему тегу table в коллекции columns--->
<cfset ATTRIBUTES.th=#thisTag.th#/><!---поле, видное включающему тегу table в коллекции columns--->
<cfset ATTRIBUTES.content=thisTag.generatedContent/>
<cfparam name="ATTRIBUTES.field" default="#thisTag.td[1].field#"/><!---***---><!---настоящий атрибут - имя поля запроса (query) для ВЫВОДА, может задаваться как атрибут тега--->
<cfparam name="ATTRIBUTES.formatter" type="function" default=#function (x) {return x;}#/>
<cfparam name="ATTRIBUTES.sortable" type="boolean" default=true/>
<cfparam name="ATTRIBUTES.visible" type="boolean" default=true/>
<cfparam name="ATTRIBUTES.columnOrder" type="integer" default=0/>
<!--- Желательное поведение сортировки колонок: для некоторых колонок указан порядок, на этом месте они и оказываются. Не указан - идут в порядке появления в коде. Если 2 колонки с одним порядком сортировки, первой выводится та, которая первая по тексту. --->
</cfif>
@@ -0,0 +1,46 @@
<cfsilent>
<cfimport prefix="c" taglib="."/>
<!---*** идея включить титул в метаданные была не совсем пустой, но когда титул определяется в селекте, это не слишком удобно --->
<cfassociate basetag="cf_grid_head" datacollection="headersArray"/>
<cfset envelope=getBaseTagData('cf_grid_head',1)/>
<cfparam name="ATTRIBUTES.title" type="string" default="#envelope.ATTRIBUTES.titleMap[ATTRIBUTES.name].title#"/>
<cfparam name="ATTRIBUTES.name" type="string"/><!--- Заголовок указвается на том поле, по которому будет сортировка, а не на том, которе отображается --->
<cfparam name="ATTRIBUTES.arrows" type="boolean" default="Yes"/>
<cfset attributeList="name,arrows,title"/>
<!--- В данном случае реализован подход: дополнительные атрибуты передаются вниз.
Другой вариант - создавать атрибут extra, в который писать суффикс QueryString.
Первый подход, использованный здесь, позволяет иметь доступ к дополнительным атрибутам, но непонятно, зачем - ведь их имена все равно неизвестны.
Очевидный минус этого подхода - возможный конфликт имен с именами существующих атрибутов --->
<cfset extraAttributes=structNew()/>
<!--- дополнительные атрибуты родительского тега--->
<cfif StructKeyExists(envelope, "extraAttributes") AND isStruct(envelope.extraAttributes)>
<cfloop collection=#envelope.extraAttributes# item="attr">
<cfif NOT listFindNoCase(attributeList, attr)><!--- проверка относительно локальных атрибутов, чтобы их не перетереть--->
<cfset val="#structFind(envelope.extraAttributes, attr)#"/>
<cfif IsSimpleValue(val)>
<cfset structInsert(extraAttributes, attr, val)/>
</cfif>
</cfif>
</cfloop>
</cfif>
<cfloop collection=#ATTRIBUTES# item="attr">
<cfif NOT listFindNoCase(attributeList, attr)>
<cfset val="#structFind(ATTRIBUTES, attr)#"/>
<cfif IsSimpleValue(val)>
<cfset structInsert(extraAttributes, attr, val, "Yes")/><!--- локальные атрибуты перетрут родительские --->
</cfif>
</cfif>
</cfloop>
</cfsilent><cfif thisTag.executionMode is "end"><cfoutput>#ATTRIBUTES.title#
<cfif ATTRIBUTES.arrows><c:order_arrows sortArray=#envelope.ATTRIBUTES.sortArray# n=#envelope.ATTRIBUTES.titleMap[ATTRIBUTES.name].ordinal# attributeCollection=#extraAttributes#/></cfif></cfoutput></cfif>
@@ -0,0 +1,178 @@
<!--- combobox
v3.1 23.10.2020 support of list in multiple selection
v3.0 12.08.2016 ATTRIBUTES.readonly
v2.1 04.05.2016 ATTRIBUTES.disabled
v2 с форматным выводом
---><cfprocessingdirective suppresswhitespace="true">
<!---
<cfparam name="ATTRIBUTES.query" type="query"/>
<cfparam name="ATTRIBUTES.queryString" type="string"/>
<cfparam name="ATTRIBUTES.list" type="string"/>
--->
<cfparam name="ATTRIBUTES.DS" type="string" default="#request.DS#"/>
<cfparam name="ATTRIBUTES.combo"/><!--- имя контрола (name)--->
<cfparam name="ATTRIBUTES.key"/><!--- Имя ключевого поля---><!--- ограничение: не поддерживаем составные ключи --->
<cfparam name="ATTRIBUTES.display" default=""/><!--- имя поля резалтсета --->
<cfparam name="ATTRIBUTES.displayf" default=""/><!--- форматный вывод, поля резалтсета писать: ##field##, например "*##field1##:##field2##*" --->
<cfparam name="ATTRIBUTES.selected" default=""/><!--- в случае base64 - завернуто в base64--->
<cfparam name="ATTRIBUTES.base64" default="No"/><!---*** это архитектурная ошибка. Base64 is url unsafe. Но поскольку все передается POST-ом, обычно не проявляется --->
<cfparam name="ATTRIBUTES.disabled" type="boolean" default="No"/>
<cfparam name="ATTRIBUTES.style_selector" default=""/>
<cfparam name="ATTRIBUTES.style_spec" type="struct" default=#structNew()#/>
<cfparam name="ATTRIBUTES.class_selector" default=""/>
<cfparam name="ATTRIBUTES.class_spec" type="struct" default=#structNew()#/>
<cfparam name="ATTRIBUTES.flag_disabled" type="string" default=""/><!--- проверяется на > 0. Возможно, не следует отключать выбранное значение --->
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="false"/><!--- в случае readonly выводится текст и скрытое поле с ID, если множественное выделение, то надо наверно выводить все выделенные элементы? --->
<cfparam name="ATTRIBUTES.extra" type="string" default=""/><!--- --->
<cfset attributeList="template,query,queryString,combo,key,display,displayf,selected,empty,DS,base64,disabled,style_selector,style_spec,class_selector,class_spec,flag_disabled,readonly,extra"/>
<cffunction name="escape64" returnType="string">
<cfargument name="val" type="string">
<cfif #ATTRIBUTES.base64#>
<cfreturn ToBase64(val)>
<cfelse>
<cfreturn val>
</cfif>
</cffunction>
<cffunction name="isSelected" returnType="boolean">
<!--- tests for presense of key (second arg) in passed array (first arg) or, if sel is simple value just compares key and sel.
Used for checking against multiple alternatives like "save" OR "saveAndClose"
--->
<cfargument name="sel">
<cfargument name="key">
<cfscript>
if (isArray(sel)) {
for(i=1; i <= ArrayLen(sel); i++){
if (sel[i]==key) {return true;}
}
return false;
} else { //считаем, что из сложных типов тут бывает только массив
if (!isSimpleValue(sel)) {return false;}
/*return (sel == key);*/
return listFind(sel,key);
}
</cfscript>
</cffunction>
<cffunction name="strSelected" returnType="string">
<cfargument name="sel">
<cfargument name="key">
<!---unsupported in CF8: <cfreturn (isSelected(sel,key)? " selected" : "")>--->
<cfif isSelected(sel,key)>
<cfreturn " selected">
<cfelse>
<cfreturn "">
</cfif>
</cffunction>
<!--- функция условного обертывания --->
<cffunction name="w" returnType="string">
<cfargument name="prefix"/>
<cfargument name="input"/>
<cfargument name="suffix" default=""/>
<cfif len(input) GT 0>
<cfreturn "#prefix##input##suffix#"/>
<cfelse>
<cfreturn ""/>
</cfif>
</cffunction>
<cffunction name="strAttribute" returnType="string">
<!---returns formatted string ' attribute="value"',
where attribute name is the first argument,
and value is taken from passed structure(third argument) by key (second argument)--->
<cfargument name="name"/>
<cfargument name="selector"/>
<cfargument name="spec" type="struct"/>
<cfif StructKeyExists(spec, selector)>
<cfreturn ' #name#="#structFind(spec, selector)#"'/>
<cfelse>
<cfreturn "">
</cfif>
</cffunction>
<!--- ---------------------------------------------------------------------------------- --->
<cfif structKeyExists(ATTRIBUTES,"query")>
<cfset qSel=ATTRIBUTES.query>
<cfelseif structKeyExists(ATTRIBUTES,"queryString")>
<cfquery name="qSel" datasource="#ATTRIBUTES.DS#">
#preserveSingleQuotes(ATTRIBUTES.queryString)#
</cfquery>
<cfelseif structKeyExists(ATTRIBUTES,"list")>
<cfset qSel=#queryNew("item")#/>
<cfloop list=#ATTRIBUTES.list# item="item">
<cfset queryAddRow(qSel,{"item":"#item#"})/>
</cfloop>
</cfif>
<!--- parse class specifications--->
<cfscript>
</cfscript>
<cfif ATTRIBUTES.readonly>
<cfif isArray(ATTRIBUTES.selected)>
<cfset selectedList=ArrayToList(ATTRIBUTES.selected)/>
<cfelse>
<cfset selectedList=ATTRIBUTES.selected/>
</cfif>
<cfquery name="qSelFiltered" dbtype="query">
select * from qSel where <cfif len(selectedList)>#ATTRIBUTES.key# in (#selectedList#)<cfelse>1=0</cfif>
</cfquery>
<cfoutput>
<div class="textbox-disabled" style="display:inline-block;"<cfloop collection=#ATTRIBUTES# item="attr"><!---
---><cfif NOT listFindNoCase(attributeList,attr)> #attr#="#structFind(ATTRIBUTES,attr)#"</cfif><!---
---></cfloop>>
<cfif qSelFiltered.recordCount GT 0>
<cfloop query="qSelFiltered"><!---
---><cfset key=evaluate("#ATTRIBUTES.key#")/><!---
---><cfset key=trim(escape64(key))/><!---
---><div><input type="hidden" name="#ATTRIBUTES.combo#" value="#key#"/>#Evaluate(ATTRIBUTES.display)#<cfset Evaluate("WriteOutput('#ATTRIBUTES.displayf#')")/></div>
</cfloop>
<cfelseif isDefined("ATTRIBUTES.empty")>
<input type="hidden" name="#ATTRIBUTES.combo#" value="#ATTRIBUTES.empty#"/><!-- #ATTRIBUTES.combo#: nothing selected -->
&nbsp;&nbsp;&nbsp;
<cfelse>
&nbsp;&nbsp;&nbsp;
</cfif>
</div>
</cfoutput>
<cfelse><!--- ATTRIBUTES.readonly --->
<cfoutput>
<select name="#ATTRIBUTES.combo#"<!---
passthrough additional attributes
---><cfloop collection=#ATTRIBUTES# item="attr"><!---
---><cfif NOT listFindNoCase(attributeList,attr)> #attr#="#structFind(ATTRIBUTES,attr)#"</cfif><!---
---></cfloop><cfif ATTRIBUTES.disabled> disabled="disabled"</cfif><cfif len(ATTRIBUTES.extra)>#ATTRIBUTES.extra#</cfif>>
<cfif isDefined("ATTRIBUTES.empty")>
<option value="#ATTRIBUTES.empty#"></option><!--- *** Надо было делать не пустое, а дефолтное значение, чтобы можно было ввести, например, {N/A, -1} --->
</cfif>
<cfloop query=qSel>
<cfset key=evaluate("#ATTRIBUTES.key#")/><!--- в контексте запроса, поэтому находится в цикле! *** не самый лучший способ в смысле производительности --->
<cfset key=trim(escape64(key))/>
<cfset str_sel=strSelected(ATTRIBUTES.selected, key)/>
<option value="#key#"#str_sel#<!---
---><cfif len(ATTRIBUTES.style_selector) GT 0>#strAttribute("style",
Evaluate(ATTRIBUTES.style_selector),ATTRIBUTES.style_spec)#</cfif><!---
---><cfif len(ATTRIBUTES.class_selector) GT 0>#strAttribute("class", Evaluate(ATTRIBUTES.class_selector),ATTRIBUTES.class_spec)#</cfif><!---
---><cfif len(ATTRIBUTES.flag_disabled) GT 0><cfif Evaluate(ATTRIBUTES.flag_disabled) GT 0> disabled="1"</cfif></cfif><!---
--->>#Evaluate(ATTRIBUTES.display)#<cfset Evaluate("WriteOutput('#ATTRIBUTES.displayf#')")/></option>
</cfloop>
</select>
</cfoutput>
</cfif>
</cfprocessingdirective><cfexit method="EXITTAG"/>
@@ -0,0 +1,21 @@
<cfsilent>
<cfparam name="ATTRIBUTES.name"/>
<cfif structKeyExists(ATTRIBUTES,"value")>
<cfset value=#ATTRIBUTES.value#/>
<cfelse>
<cfset value=#ATTRIBUTES.container[ATTRIBUTES.name]#/>
</cfif>
<cfparam name="ATTRIBUTES.suffix" default=""/>
<cfparam name="ATTRIBUTES.id" default="#ATTRIBUTES.name##ATTRIBUTES.suffix#"/>
<cfparam name="ATTRIBUTES.format" default="YYYY-MM-DD"/>
<cfparam name="ATTRIBUTES.size" default="#len(ATTRIBUTES.format)#"/>
<cfparam name="ATTRIBUTES.disabled" type="boolean" default="No"/>
<cfset attributeList="name,value,container,suffix,id,format,size,disabled"/>
</cfsilent><cfoutput><input type="text" name="#ATTRIBUTES.name#"<cfif len(ATTRIBUTES.id)> id=#ATTRIBUTES.id#</cfif> value="#dateFormat(value, ATTRIBUTES.format)#" size="#ATTRIBUTES.size#"<cfif ATTRIBUTES.disabled> disabled="disabled"</cfif><!---
passthrough additional attributes
---><cfloop collection=#ATTRIBUTES# item="attr"><!---
---><cfif NOT listFindNoCase(attributeList,attr)> #attr#="#structFind(ATTRIBUTES,attr)#"</cfif><!---
---></cfloop>/></cfoutput><cfexit method="exittag"/>
@@ -0,0 +1,29 @@
<cfsilent>
<cfparam name="ATTRIBUTES.name"/>
<cfif structKeyExists(ATTRIBUTES,"value")>
<cfset value=#ATTRIBUTES.value#/>
<cfelse>
<cfset value=#ATTRIBUTES.container[ATTRIBUTES.name]#/>
</cfif>
<cfparam name="ATTRIBUTES.suffix" default=""/>
<cfparam name="ATTRIBUTES.id" default="#ATTRIBUTES.name##ATTRIBUTES.suffix#"/>
<cfparam name="ATTRIBUTES.dformat" default="YYYY-MM-DD"/>
<cfparam name="ATTRIBUTES.tformat" default="HH:MM"/>
<cfparam name="ATTRIBUTES.size" default="#len(ATTRIBUTES.dformat)+len(ATTRIBUTES.tformat)+1#"/>
<cfparam name="ATTRIBUTES.disabled" type="boolean" default="No"/>
<cfparam name="ATTRIBUTES.timeFirst" type="boolean" default="No"/>
<cfif ATTRIBUTES.timeFirst>
<cfset value="#timeFormat(value, ATTRIBUTES.tformat)# #dateFormat(value, ATTRIBUTES.dformat)#"/>
<cfelse>
<cfset value="#dateFormat(value, ATTRIBUTES.dformat)# #timeFormat(value, ATTRIBUTES.tformat)#"/>
</cfif>
<cfset attributeList="name,value,container,suffix,id,dformat,tformat,size,disabled"/>
</cfsilent><cfoutput><input type="text" name="#ATTRIBUTES.name#"<cfif len(ATTRIBUTES.id)> id=#ATTRIBUTES.id#</cfif> value="#value#" size="#ATTRIBUTES.size#"<cfif ATTRIBUTES.disabled> disabled="disabled"</cfif><!---
passthrough additional attributes
---><cfloop collection=#ATTRIBUTES# item="attr"><!---
---><cfif NOT listFindNoCase(attributeList,attr)> #attr#="#structFind(ATTRIBUTES,attr)#"</cfif><!---
---></cfloop>/></cfoutput><cfexit method="exittag"/>
@@ -0,0 +1,18 @@
<!---moved to layout--->
<!---<cfif thisTag.executionMode IS "end" OR NOT thisTag.hasEndTag>
<cfparam name="ATTRIBUTES.backUrl" type="string" default="."/>
<cfparam name="ATTRIBUTES.enable" type="boolean" default="No"/>
<cfoutput>
<div class="row" style="margin:0px;">
<div class="col-sm-3" style="padding: 0px;">
<button type="submit" name="save" class="btn btn-secondary" style="width: 100%; padding: 6px 0px;"<cfif NOT ATTRIBUTES.enable> disabled</cfif>>#request.i18("Сохранить","Save")#</button>
</div>
<div class="col-sm-6" style="padding: 0px 5px;">
<button type="submit" name="saveAndClose" class="btn btn-secondary" style="width: 100%; padding: 6px 0px;"<cfif NOT ATTRIBUTES.enable> disabled</cfif>>#request.i18("Сохранить и закрыть","Save and Close")#</button>
</div>
<div class="col-sm-3" style="padding: 0px;">
<button type="button" name="close" class="btn btn-secondary" style="width: 100%; padding: 6px 0px;" onClick="document.location.href='#ATTRIBUTES.backUrl#'">#request.i18("Закрыть","Close")#</button>
</div>
</div>
</cfoutput>
</cfif>--->
@@ -0,0 +1,18 @@
<cfsilent>
<cfparam name="ATTRIBUTES.titleMap" default=#structNew()#/>
<cfparam name="ATTRIBUTES.sortArray" default=#arrayNew(1)#/>
<cfset attributeList="titleMap,sortArray"/>
<cfset extraAttributes=structNew()/>
<cfloop collection=#ATTRIBUTES# item="attr">
<cfif NOT listFindNoCase(attributeList, attr)>
<cfset val="#structFind(ATTRIBUTES, attr)#"/>
<cfif IsSimpleValue(val)>
<cfset structInsert(extraAttributes, attr, val)/>
</cfif>
</cfif>
</cfloop>
</cfsilent><!---
---><cfif thisTag.executionMode is "start"><tr><cfelseif thisTag.executionMode is "end"></tr></cfif>
@@ -0,0 +1,11 @@
<cfsilent>
<cfparam name="ATTRIBUTES.canWrite" type="boolean" default="Yes"/>
<cfif NOT ATTRIBUTES.canWrite>
<cfexit method="exitTag"/>
</cfif>
<cfparam name="ATTRIBUTES.entity"/>
<cfparam name="ATTRIBUTES.key" default="#ATTRIBUTES.entity#_id"/>
<cfparam name="ATTRIBUTES.extra" default=""/>
<cfparam name="ATTRIBUTES.fwx"/>
<cfparam name="ATTRIBUTES.title" default="создать"/>
</cfsilent><cfoutput><a href="#ATTRIBUTES.entity#.cfm?#ATTRIBUTES.key#=-1<cfif len(ATTRIBUTES.extra)>&#ATTRIBUTES.extra#</cfif>&#ATTRIBUTES.fwx#" title="#ATTRIBUTES.title#" class="add"></a></cfoutput><cfexit method="exitTag"/>
@@ -0,0 +1,10 @@
<cfsilent>
<cfparam name="ATTRIBUTES.canWrite" type="boolean" default="Yes"/>
<cfif NOT ATTRIBUTES.canWrite>
<cfexit method="exitTag"/>
</cfif>
<cfparam name="ATTRIBUTES.entity"/>
<cfparam name="ATTRIBUTES.key" default="#ATTRIBUTES.entity#_id"/>
<cfparam name="ATTRIBUTES.id"/>
<cfparam name="ATTRIBUTES.fwx"/>
</cfsilent><cfoutput><a href="#ATTRIBUTES.entity#_del.cfm?#ATTRIBUTES.key#=#ATTRIBUTES.id#&#ATTRIBUTES.fwx#" title="удалить" name="#ATTRIBUTES.id#" class="del"></a></cfoutput><cfexit method="exitTag"/>
@@ -0,0 +1,9 @@
<!---***deprecated. Use link_view_edit---><cfsilent>
<cfparam name="ATTRIBUTES.canWrite" type="boolean" default="Yes"/>
<cfparam name="ATTRIBUTES.entity"/>
<cfparam name="ATTRIBUTES.key" default="#ATTRIBUTES.entity#_id"/>
<cfparam name="ATTRIBUTES.id"/>
<cfparam name="ATTRIBUTES.fwx"/>
<cfparam name="ATTRIBUTES.title_edit" default="редактировать"/>
<cfparam name="ATTRIBUTES.title_view" default="просмотреть"/>
</cfsilent><cfoutput><a href="#ATTRIBUTES.entity#.cfm?#ATTRIBUTES.key#=#ATTRIBUTES.id#&#ATTRIBUTES.fwx#" name="#ATTRIBUTES.id#" <cfif ATTRIBUTES.canWrite>title="#ATTRIBUTES.title_edit#" class="edit"<cfelse>title="#ATTRIBUTES.title_view#" class="view"</cfif>></a></cfoutput><cfexit method="exitTag"/>
@@ -0,0 +1,6 @@
<cfsilent>
<cfparam name="ATTRIBUTES.entity"/>
<cfparam name="ATTRIBUTES.key" default="#ATTRIBUTES.entity#_id"/>
<cfparam name="ATTRIBUTES.id"/>
<cfparam name="ATTRIBUTES.fwx"/>
</cfsilent><cfoutput><a href="#ATTRIBUTES.entity#.cfm?#ATTRIBUTES.key#=#ATTRIBUTES.id#&#ATTRIBUTES.fwx#" title="просмотреть" name="#ATTRIBUTES.id#" class="view"></a></cfoutput><cfexit method="exitTag"/>
@@ -0,0 +1,9 @@
<cfsilent>
<cfparam name="ATTRIBUTES.canWrite" type="boolean" default="Yes"/>
<cfparam name="ATTRIBUTES.entity"/>
<cfparam name="ATTRIBUTES.key" default="#ATTRIBUTES.entity#_id"/>
<cfparam name="ATTRIBUTES.id"/>
<cfparam name="ATTRIBUTES.fwx"/>
<cfparam name="ATTRIBUTES.title_edit" default="редактировать"/>
<cfparam name="ATTRIBUTES.title_view" default="просмотреть"/>
</cfsilent><cfoutput><a href="#ATTRIBUTES.entity#.cfm?#ATTRIBUTES.key#=#ATTRIBUTES.id#&#ATTRIBUTES.fwx#" name="#ATTRIBUTES.id#" <cfif ATTRIBUTES.canWrite>title="#ATTRIBUTES.title_edit#" class="edit"<cfelse>title="#ATTRIBUTES.title_view#" class="view"</cfif>></a></cfoutput><cfexit method="exitTag"/>
@@ -0,0 +1,25 @@
<cfprocessingdirective suppressWhitespace="YES"><cfsilent>
<cfparam name="ATTRIBUTES.page" type="string"/>
<cfparam name="ATTRIBUTES.queryString" type="string" default=""/>
<cfparam name="ATTRIBUTES.acl" type="string" default=""/>
<cfparam name="ATTRIBUTES.label" type="string" default=""/>
<cfif structKeyExists(#ATTRIBUTES#,"external")>
<cfset class="menu-item-ext"/>
<cfset target=' target="_blank"'/>
<cfelse>
<cfset class="menu-item"/>
<cfset target=''/>
</cfif>
</cfsilent><!---
---><cfmodule template="../ac.cfm" obj="#ATTRIBUTES.acl#"><cfif NOT (request.thisPage EQ ATTRIBUTES.page)><cfsilent>
<cfif len(ATTRIBUTES.queryString)>
<cfset pageUrl="#ATTRIBUTES.page#?#ATTRIBUTES.queryString#"/>
<cfelse>
<cfset pageUrl="#ATTRIBUTES.page#"/>
</cfif>
</cfsilent><cfoutput><li class="#class#"><a href="#pageUrl#"#target#>#ATTRIBUTES.label#</a></li></cfoutput>
<cfelse><cfoutput><li class="selected">#ATTRIBUTES.label#</li></cfoutput></cfif><!---
---></cfmodule></cfprocessingdirective><cfexit method="EXITTAG"/>
@@ -0,0 +1,6 @@
<cfsilent>
<cfparam name="ATTRIBUTES.targetid"/>
<cfparam name="ATTRIBUTES.enabled" default="Yes"/>
<cfparam name="ATTRIBUTES.suffix" default=""/>
<cfparam name="ATTRIBUTES.title" default="Сейчас"/>
</cfsilent><cfoutput><button onclick="document.getElementById(#ATTRIBUTES.targetid#).value=""></</cfoutput><cfexit method="exittag"/>
@@ -0,0 +1,36 @@
<cfsilent>
<cfparam name="ATTRIBUTES.n" type="numeric"/>
<cfset attributeList="n,sortArray"/>
<cfset img_up="arr_up.png"/>
<cfset img_dn="arr_dn.png"/>
<cfset HIGHLIGHTED_CNT=2/>
<cfif isDefined("ATTRIBUTES.sortArray")>
<cfset i=1>
<cfloop condition="i LE HIGHLIGHTED_CNT AND i LE arrayLen(ATTRIBUTES.sortArray)">
<cftry>
<cfif ATTRIBUTES.sortArray[i].fld EQ ATTRIBUTES.n>
<cfif ATTRIBUTES.sortArray[i].asc>
<cfset img_up="arr_up#i#.png">
<cfelse>
<cfset img_dn="arr_dn#i#.png">
</cfif>
</cfif>
<cfcatch type="Any"></cfcatch>
</cftry>
<cfset i=i+1>
</cfloop>
</cfif>
<cfset extra_query_string=""/>
<cfloop collection=#ATTRIBUTES# item="attr">
<cfif NOT listFindNoCase(attributeList,attr)>
<cfset val="#structFind(ATTRIBUTES,attr)#"/>
<cfif IsSimpleValue(val)>
<cfset extra_query_string="#extra_query_string#&#attr#=#val#"/>
</cfif>
</cfif>
</cfloop>
</cfsilent><cfoutput><a href="?sort=#ATTRIBUTES.n#a#extra_query_string#" class="noprint" style="padding:2px;" title="по возрастанию"><img src="img/#img_up#" alt="по возрастанию"></a><a href="?sort=#ATTRIBUTES.n#d#extra_query_string#" title="по убыванию"><img src="img/#img_dn#" alt="по убыванию"></a></cfoutput><cfexit method="exittag"/>
@@ -0,0 +1,80 @@
<cfparam name="nStart" type="integer" default="1"/>
<cfparam name="ATTRIBUTES.recordCount" type="integer"/>
<cfparam name="ATTRIBUTES.recordsPerPage" type="integer"/>
<cfparam name="ATTRIBUTES.thisPage" type="string" default=""/>
<cfparam name="ATTRIBUTES.addtionalUrlParams" type="string" default=""/>
<cfparam name="ATTRIBUTES.self" type="string" default=""/>
<!---<cfparam name="ATTRIBUTES.fwx" type="string" default=""/>--->
<cfparam name="ATTRIBUTES.output" type="string" default="paginator"/>
<!--- Исключение: рассчитанный индекс страницы превышает количество существующих страниц - надо возвращаться на первую --->
<cfset nPages = Int((ATTRIBUTES.recordCount-1)/ATTRIBUTES.recordsPerPage)+1/>
<cfset nCurrentPage = Int((nStart-1)/ATTRIBUTES.recordsPerPage)+1/>
<cfif nStart GT ATTRIBUTES.recordCount>
<cfset nStart=1/>
</cfif>
<cfset UrlSuffix=""/>
<cfif len(ATTRIBUTES.addtionalUrlParams) GT 0>
<cfset UrlSuffix="&#ATTRIBUTES.addtionalUrlParams#"/>
</cfif>
<cfif len(ATTRIBUTES.self) GT 0>
<cfset UrlSuffix="&track=#ATTRIBUTES.self#"/>
</cfif>
<cfset somePages=3/>
<cfsavecontent variable="pages">
<cfif ATTRIBUTES.recordCount GT ATTRIBUTES.recordsPerPage>
<cfset doMerge = false/>
<cfloop index="i" from="1" to="#nPages#">
<cfset nPageStartRecordNum=(i-1)*ATTRIBUTES.recordsPerPage+1>
<!---
схема, которая обеспечивает дихотомический поиск
первые несколько страниц (не менее 2)
если есть разрыв - многоточие
посередине
если есть разрыв - многоточие
несколько страниц до текушей
текущую без ссылки
несколько страниц после текущей
если есть разрыв - многоточие
посередине
если есть разрыв - многоточие
последние несколько страниц
--->
<!---Можно сделать на многоточии ссылку на середину интервала!--->
<!---Хочется добиться, чтобы при хождении вперед-назад значения повторялись--->
<!---забавный эффект, когда остается разрыв в 1 страницу, а там все равно многоточие--->
<cfoutput>
<cfif i NEQ nCurrentPage>
<cfif i GT somePages AND (nCurrentPage-i) GT somePages>
<cfif NOT doMerge><!---*** неуклюже--->
<cfset nMidPage = ceiling(nCurrentPage/2)/>
<cfset nMidStartRecord = (nMidPage-1)*ATTRIBUTES.recordsPerPage+1/>
<a href="#ATTRIBUTES.thisPage#?nStart=#nMidStartRecord##UrlSuffix#" title="#nMidPage#">...</a>&nbsp;<cfset doMerge=true/>
</cfif>
<cfelseif (i-nCurrentPage) GT somePages AND (nPages-i) GT somePages>
<cfif NOT doMerge>
<cfset nMidPage = ceiling((nCurrentPage+nPages)/2)/>
<cfset nMidStartRecord = (nMidPage-1)*ATTRIBUTES.recordsPerPage+1/>
<a href="#ATTRIBUTES.thisPage#?nStart=#nMidStartRecord##UrlSuffix#" title="#nMidPage#">...</a>&nbsp;<cfset doMerge=true/>
</cfif>
<cfelse>
<a href="#ATTRIBUTES.thisPage#?nStart=#nPageStartRecordNum##UrlSuffix#">#i#</a>&nbsp;
<cfset doMerge=false/>
</cfif>
<cfelse>
<b>#i#</b>&nbsp;
</cfif>
</cfoutput>
</cfloop>
</cfif>
</cfsavecontent>
<cfset "CALLER.#ATTRIBUTES.output#.links"=#pages#/>
<cfset "CALLER.#ATTRIBUTES.output#.recordsPerPage"=#ATTRIBUTES.recordsPerPage#/>
<cfexit method="exittag"/>
@@ -0,0 +1,154 @@
<cfimport prefix="c" taglib="."/>
<cfif thisTag.executionMode IS "end">
<!--- <cfloop collection=#CALLER# item="item">
<cfset structInsert(variables, item, CALLER[item], false)/>
</cfloop> --->
<cfparam name="ATTRIBUTES.query" type="query"/>
<cfparam name="ATTRIBUTES.recordsPerPage" type="integer" default="500"/>
<cfparam name="ATTRIBUTES.nStart" type="integer" default="1"/>
<cfparam name="ATTRIBUTES.titleMap" type="struct" default=srtuctNew()/><!---for definition see: field_set.cfm--->
<cfparam name="ATTRIBUTES.sortArray" type="array" default=arrayNew(1)/>
<cfset tableAttributeList="query,recordsPerPage,nStart,titleMap,sortArray"/>
<cfset colAttributeList="lang,th,td,content,field,formatter,sortable,visible,columnOrder"/>
<cfset tdAttributeList="content,field"/>
<cfparam name="thisTag.columns" type="array"/>
<cffunction name=reorderColumns output="No">
<!--- Вариант сортировки. Вынимаем все элементы с указанным порядком в отдельную коллекцию,
после чего вставляем их в оставшуюся коллекцию на указанные позиции.
нет смысла делать вид, что мы реализуем что-то универсальное
(поле для сортировки в совершенно конкретном поле структуры в массиве),
поэтому ничего не передаем в аргументах, спокойно ссылаемся на окружение--->
<cfset var tmpOrderedIndexes=ArrayNew(1)/>
<cfset var tmpUnorderedIndexes=ArrayNew(1)/>
<cfloop from=1 to=#arrayLen(thisTag.columns)# index="i">
<cfif thisTag.columns[i].columnOrder GT 0>
<cfset arrayAppend(tmpOrderedIndexes,i)/>
<cfelse>
<cfset arrayAppend(tmpUnorderedIndexes,i)/>
</cfif>
</cfloop>
<cfloop from=#arrayLen(tmpOrderedIndexes)# to=1 step=-1 index="i">
<!--- counting up-down preserves entry order for equal columnOrders --->
<cfset var pos=max(min(thisTag.columns[tmpOrderedIndexes[i]].columnOrder, len(tmpUnorderedIndexes)+1),1)/>
<cfset arrayInsertAt(tmpUnorderedIndexes,pos,tmpOrderedIndexes[i])/>
</cfloop>
<cfset var sortedColumns=ArrayNew(1)/>
<cfloop from=1 to=#arrayLen(tmpUnorderedIndexes)# index="i">
<cfset sortedColumns[i]=thisTag.columns[tmpUnorderedIndexes[i]]/>
</cfloop>
<cfreturn sortedColumns/>
</cffunction>
<cfset sortedColumns=reorderColumns()/>
<cfset thisTag.generatedContent=""/>
<table<!--- passthrough additional attributes
---><cfoutput><cfloop collection=#ATTRIBUTES# item="attr"><!---
---><cfif NOT listFindNoCase(tableAttributeList,attr)<!--- AND isSimpleValue(structFind(ATTRIBUTES,attr))--->> #attr#="#structFind(ATTRIBUTES,attr)#"</cfif><!---
---></cfloop></cfoutput>>
<thead>
<tr>
<cfloop array=#sortedColumns# index="column">
<cfset th=column.th[1]/>
<th<!--- атрибуты колонки передаются в TH, хотя это выглядит не очень нужным --->
<cfloop collection=#column# item="colAttr">
<cfif NOT listFindNoCase(colAttributeList,colAttr)><cfoutput>#colAttr#="#structFind(column,colAttr)#" </cfoutput></cfif>
</cfloop>>
<cfif len(column.field) AND column.sortable>
<c:order_arrows sortArray=#ATTRIBUTES.sortArray# n=#ATTRIBUTES.titleMap[column.field].ordinal#<!--- attributeCollection=#extraAttributes#--->/>
</cfif>
<cfif len(th.content)>
<cfset Evaluate("WriteOutput('#th.content#')")/>
<cfelseif len(column.field)>
<cfif structKeyExists(ATTRIBUTES.titleMap[column.field],"title")>
<cfoutput>#ATTRIBUTES.titleMap[column.field].title#</cfoutput>
</cfif>
</cfif>
</th>
</cfloop>
</tr>
</thead>
<tbody>
<cfoutput query=#ATTRIBUTES.query# maxrows=#ATTRIBUTES.recordsPerPage# startrow=#ATTRIBUTES.nStart#>
<tr>
<cfloop array=#sortedColumns# index="column">
<cfset td=column.td[1]/>
<td<!--- Если нужно передать атрибут в TD, можно ввести тег <C:TD class="r"/> только как плейсхолдер для атрибутов, а поле будет получено от колонки --->
<cfloop collection=#td# item="tdAttr">
<cfif NOT listFindNoCase(tdAttributeList,tdAttr)>#tdAttr#="#structFind(td,tdAttr)#" </cfif>
</cfloop>
><cfif len(td.content)><cfset Evaluate("WriteOutput('#td.content#')")/><!---#td.content#--->
<cfelseif len(td.field)>#ATTRIBUTES.query[td.field][currentRow]#
<cfelseif len(column.field)>#column.formatter(ATTRIBUTES.query[column.field][currentRow])#
</cfif></td>
</cfloop>
</tr>
</cfoutput>
</tbody>
</table>
<!--- <cfdump var=#variables#/> --->
</cfif>
<!--- <cfexit method="exitTag"/> --->
<cfscript>
/**
* Sorts an array of structures based on a key in the structures.
*
* @param aofS Array of structures.
* @param key Key to sort by.
* @param sortOrder Order to sort by, asc or desc.
* @param sortType Text, textnocase, or numeric.
* @param delim Delimiter used for temporary data storage. Must not exist in data. Defaults to a period.
* @return Returns a sorted array.
* @author Nathan Dintenfass (nathan@changemedia.com)
* @version 1, December 10, 2001
*/
function arrayOfStructsSort(aOfS,key){
//by default we'll use an ascending sort
var sortOrder = "asc";
//by default, we'll use a textnocase sort
var sortType = "textnocase";
//by default, use ascii character 30 as the delim
var delim = ".";
//make an array to hold the sort stuff
var sortArray = arraynew(1);
//make an array to return
var returnArray = arraynew(1);
//grab the number of elements in the array (used in the loops)
var count = arrayLen(aOfS);
//make a variable to use in the loop
var ii = 1;
//if there is a 3rd argument, set the sortOrder
if(arraylen(arguments) GT 2)
sortOrder = arguments[3];
//if there is a 4th argument, set the sortType
if(arraylen(arguments) GT 3)
sortType = arguments[4];
//if there is a 5th argument, set the delim
if(arraylen(arguments) GT 4)
delim = arguments[5];
//loop over the array of structs, building the sortArray
for(ii = 1; ii lte count; ii = ii + 1)
sortArray[ii] = aOfS[ii][key] & delim & ii;
//now sort the array
arraySort(sortArray,sortType,sortOrder);
//now build the return array
for(ii = 1; ii lte count; ii = ii + 1)
returnArray[ii] = aOfS[listLast(sortArray[ii],delim)];
//return the array
return returnArray;
}
</cfscript>
@@ -0,0 +1,15 @@
<cfif thisTag.executionMode IS "end">
<!--- <cfloop collection=#CALLER# item="item">
<cfset structInsert(variables, item, CALLER[item], false)/>
</cfloop> --->
<cfassociate basetag="cf_column" datacollection="td"/>
<cfset attributeList="field"/>
<cfparam name="ATTRIBUTES.field" default=""/>
<cfset ATTRIBUTES.content=thisTag.generatedContent/>
</cfif>
<!---<cfparam name="ATTRIBUTES.lang" type="string" default=""/>
<cfparam name="request.language" type="string" default=""/>
<cfif len(ATTRIBUTES.lang) AND #ATTRIBUTES.lang# NEQ #request.language#>
<cfexit method="exittag"/>
</cfif>--->
@@ -0,0 +1,20 @@
<cfsilent>
<cfparam name="ATTRIBUTES.name"/>
<cfif structKeyExists(ATTRIBUTES,"value")>
<cfset value=#ATTRIBUTES.value#/>
<cfelse>
<cfset value=#ATTRIBUTES.container[ATTRIBUTES.name]#/>
</cfif>
<cfparam name="ATTRIBUTES.suffix" default=""/>
<cfparam name="ATTRIBUTES.id" default="#ATTRIBUTES.name##ATTRIBUTES.suffix#"/>
<cfparam name="ATTRIBUTES.size" default=""/>
<cfparam name="ATTRIBUTES.disabled" type="boolean" default="No"/>
<cfset attributeList="name,value,container,suffix,id,size,disabled"/>
</cfsilent><cfoutput><input type="text" name="#ATTRIBUTES.name#"<cfif len(ATTRIBUTES.id)> id=#ATTRIBUTES.id#</cfif> value="#value#"<cfif len(ATTRIBUTES.size)> size="#ATTRIBUTES.size#"</cfif><cfif ATTRIBUTES.disabled> disabled="disabled"</cfif><!---
passthrough additional attributes
---><cfloop collection=#ATTRIBUTES# item="attr"><!---
---><cfif NOT listFindNoCase(attributeList,attr)> #attr#="#structFind(ATTRIBUTES,attr)#"</cfif><!---
---></cfloop>/></cfoutput><cfexit method="exittag"/>
@@ -0,0 +1,6 @@
<cfif thisTag.executionMode IS "end">
<cfassociate basetag="cf_column" datacollection="th"/>
<cfset attributeList="field"/>
<cfparam name="ATTRIBUTES.field" default=""/>
<cfset ATTRIBUTES.content=thisTag.generatedContent/>
</cfif>
@@ -0,0 +1,21 @@
<cfsilent>
<cfparam name="ATTRIBUTES.name"/>
<cfif structKeyExists(ATTRIBUTES,"value")>
<cfset value=#ATTRIBUTES.value#/>
<cfelse>
<cfset value=#ATTRIBUTES.container[ATTRIBUTES.name]#/>
</cfif>
<cfparam name="ATTRIBUTES.suffix" default=""/>
<cfparam name="ATTRIBUTES.id" default="#ATTRIBUTES.name##ATTRIBUTES.suffix#"/>
<cfparam name="ATTRIBUTES.format" default="HH:MM"/>
<cfparam name="ATTRIBUTES.size" default="#len(ATTRIBUTES.format)#"/>
<cfparam name="ATTRIBUTES.disabled" type="boolean" default="No"/>
<cfset attributeList="name,value,container,suffix,id,format,size,disabled"/>
</cfsilent><cfoutput><input type="text" name="#ATTRIBUTES.name#"<cfif len(ATTRIBUTES.id)> id=#ATTRIBUTES.id#</cfif> value="#timeFormat(value, ATTRIBUTES.format)#" size="#ATTRIBUTES.size#"<cfif ATTRIBUTES.disabled> disabled="disabled"</cfif><!---
passthrough additional attributes
---><cfloop collection=#ATTRIBUTES# item="attr"><!---
---><cfif NOT listFindNoCase(attributeList,attr)> #attr#="#structFind(ATTRIBUTES,attr)#"</cfif><!---
---></cfloop>/></cfoutput><cfexit method="exittag"/>
+401
View File
@@ -0,0 +1,401 @@
<!---
bean v0.96
msyu@mail.ru
2013-08-29 v0.1
2013-09-04 v0.2 tested with CF8 and Railo
2013-10-14 added meta attribute - can now export each parameter metadata
2014-12-26 POST-GET read-pass-save
2015-01-19 read-pass-save, добавлен formmarker, убрано определение режима по POST-GET
2015-01-19 убран formmarker, упразднен режим init (используется pass)
2015-07-16 добавлен passmarker для перерисовки формы с введенными значениями
2015-10-02 поддержка составных ключей
2015-10-09 v0.9 если задано value параметра, считается, что он hasInput (корректно работает в составе ключа)
2015-11-25 добавлен readmarker, mode (принудительная установка режима)
2015-11-26 добавлен mode=skip
2016-09-01 изменена трактовка processingMode. Теперь понимается как исходная команда, фиксируется до начала действий. Можно использовать для управления
2018-09-16 добавлена ограниченная поддержка нецелых первичных ключей (idstamp)
child tag: param
to save - should send parameter with name from list ATTRIBUTES.savemarker (value doesn't matter) in FORM, e.g. URL.save="trtrtr", while ATTRIBUTES.savemarker="save,saveAndClose"; for multiple forms on a page (and bean tags) different savemarkers should be used
2022-01-06 PostgreSQL (MSSQL incompatibility because of scope_identity)
2024-04-13 PostgreSQL GUID, autoincrement by RETURNING
2024-09-05 added status.recordExists (*** при вставке принудительно устанавливаем в true, веря, что если insert прошел, то запись появилась)
--->
<!---
Автогенерируемое поле GUID можно заполнять несколькими способами
- как default или init bean/param, но это не очень удобно, потому что может потеряться удобный признак новой записи - инвалидный ключ, и сейчас init-default взаимодействуют некорректно, по крайней мере, неочевидно
- генерируемое в БД (например, как default). Для PostgreSQL значение возвращается через Returning, как это делать для других БД, непонятно.
- можно генерировать при сохранении в bean, но для этого нужно расширение синтаксиса, зато не зависит от БД (*** TODO)
--->
<cfif thisTag.ExecutionMode is 'end'>
<cfparam name="ATTRIBUTES.readmarker" type="string" default="read"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда перечитывания. Имеет приоритет перед savemarker, passmarker. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.savemarker" type="string" default="save,saveAndClose"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда сохранения. Обычно это имя кнопки. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.passmarker" type="string" default="pass"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда заполнить форму отправленными данными без перечитывания. В форме такое поле обычно есть. Команда сохранения имеет приоритет перед пробросом. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.datasource" type="string"/>
<cfparam name="ATTRIBUTES.table" type="string"/>
<cfparam name="ATTRIBUTES.output" type="string"/><!--- имя переменной (структуры) области CALLER, в которую возвращаются данные. Обычно "d" --->
<cfparam name="ATTRIBUTES.info" type="string" default=""/>
<cfparam name="ATTRIBUTES.meta" type="string" default=""/>
<cfparam name="ATTRIBUTES.status" type="string"/><!--- имя переменной (структуры) области CALLER, в которую возвращается статуc. Обычно "status" --->
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
<cfparam name="ATTRIBUTES.mode" type="string" default=""/> <!--- Принудительный режим read|pass|save|skip. Имеет приоритет перед маркерами --->
<cfif ATTRIBUTES.mode EQ "skip">
<!---<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorState", false) />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorMessage", "") />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.processingMode", "skip") />--->
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(false,"","skip",false))/><!--- *** можно было не создавать компонент, он все равно не имеет логики, это map из нескольких полей. С другой стороны, хоть поля декларированы. Не очень удобно, что в отдельном файле --->
<cfexit method="exittag"/>
</cfif>
<cfset validationOk="true" />
<cfset errorState="false" />
<cfset errorMessage="" />
<cfset warningState="false" />
<cfset warningMessage="" />
<!---<cfset keyField=""/>
<cfset keyValue=""/>--->
<cfset hasAutoIncrement="false"/>
<cfset autoIncrementKeyIndex=0/><!--- сгенерированное значение присваивается первому полю, отмеченному как autoincrement --->
<cfset recordExists = "false"/>
<cfset keyInvalid = "false"/>
<!--- разбор метаданных - поиск ключа --->
<!--- поддерживаются только простые целочисленные ключи --->
<cfset keys=arrayNew(1)/>
<!--- *** почему где-то копируем поля param в key, а где-то прямо цикл по параметрам --->
<!--- коллекция thistag.params формируется вложенными тегами param --->
<cfloop array=#thistag.params# index="param">
<!---<cfdump var=#param#/>--->
<cfif structFind(param,"key")><!--- not just structKeyExists! Not exisis, OR exists, but FALSE! --->
<cfset key = structNew()/>
<cfset ArrayAppend(keys, key)/>
<cfset key.name = param.param />
<cfset key.field = param.field />
<cfset key.type = param.type />
<cfset key.value = param.value /><!--- должно уже быть присвоено --->
<cfset key.null = param.null />
<cfif param.errorState OR !param.hasInput>
<cfset keyInvalid="true"/>
<cfelse>
<cfset keyInvalid=!checkDataFormat(param.type, param.value)/>
</cfif>
<cfif !hasAutoIncrement><!--- take only first field with "autoincrement" attribute--->
<cfif structFind(param, "autoincrement")>
<cfset hasAutoIncrement = true/>
<cfset autoIncrementKeyIndex = arrayLen(keys)/>
</cfif>
</cfif>
</cfif>
</cfloop>
<!---<cfdump var=#keys#/>--->
<cfif !keyInvalid>
<!--- *** дублируется похожий cfqueryparam, перепутал один раз --->
<cfquery name="qCheckExistence" datasource="#ATTRIBUTES.datasource#">
select count(*) as cnt
from #ATTRIBUTES.table#
where 1=1
<cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop>
</cfquery>
<cfset recordExists = (qCheckExistence.cnt GT 0)/>
<!--- <cfdump var=#qCheckExistence#/> --->
<cfelse>
<cfset recordExists = "false"/>
</cfif>
<cfif listFindNoCase("read,pass,save", ATTRIBUTES.mode)>
<cfset processingMode=ATTRIBUTES.mode/>
<cfelse>
<!--- ищем команду принудительного перечитывания --->
<cfset doRead = false/>
<cfloop list=#ATTRIBUTES.readmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doRead = true/>
<cfbreak/>
</cfif>
</cfloop>
<!--- ищем команду сохранения --->
<cfset doSave = false/>
<cfif !doRead AND !ATTRIBUTES.readonly>
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doSave = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- ищем команду проброса --->
<cfset doPass = false/>
<cfif !doRead AND !doSave>
<cfloop list=#ATTRIBUTES.passmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doPass = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- определяем режим --->
<cfif doRead>
<cfset processingMode="read"/>
<cfelseif doSave>
<cfset processingMode="save"/>
<cfelse>
<cfif doPass OR !recordExists>
<cfset processingMode="pass"/>
<cfelse>
<cfset processingMode="read"/>
</cfif>
</cfif>
</cfif>
<cfset initialProcessingMode = processingMode/>
<!---debug <cfoutput>processingMode:#processingMode#</cfoutput>--->
<!--- ---------------------------------------------------------- --->
<!---*** Необходимость вставки (против апдейта) определяется по существованию записи. Это небезопасно, поскольку наличие записи с ключом -1 может прекратить создание новых записей. Конечно, можно добавить констрейнт на ключевое поле, но это лишняя зависимость.
Можно явно передавать команду вставки, а не заморачиваться с анализом ключа. Просто традиционно мы сигнализировали необходимость вставки, передавая невалидное значение ключа, обычно -1. Для целочисленных ключей можно было бы просто проверять положительность (что всю жизнь отлично работало), но хочется более общего.
--->
<cfif processingMode IS "save"> <!--- validation lookup --->
<cfloop array=#thistag.params# index="param">
<cfif !param.key>
<cfif param.warningState>
<cfif NOT warningState><!--- catch the first fault --->
<cfset warningMessage=param.warningMessage />
</cfif>
<cfset warningState="true" />
</cfif>
<cfif param.errorState>
<cfif NOT errorState><!--- catch the first fault --->
<cfset errorMessage=param.errorMessage />
</cfif>
<cfset errorState="true" />
<cfset validationOk="false"/>
</cfif>
</cfif>
<cfif errorState>
<!--- clear warning state --->
<cfset warningMessage="" />
<cfset warningState=false />
<!--- finish lookup --->
<cfbreak/>
</cfif>
</cfloop>
<cfif validationOk>
<!---<cfdump var=#param#/> --->
<cftry>
<cfif recordExists><!--- update. Наличие keyValue гарантировано (уверен) --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#" result="qSaveResult">
update #ATTRIBUTES.table# set
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<!--- не обновляем ключевые поля --->
<cfif NOT (StructFind(param, "key")
OR StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipUpdate")
)><cfif i++ GT 0>,</cfif>
#param.field#=<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
where 1=1
<cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#"/>
</cfloop>
</cfquery>
<!---<cfdump var=#qSaveResult#/>--->
<cfelse><!--- insert --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#">
insert into #ATTRIBUTES.table#
(
<cfset i=0 />
<!--- ключевые поля без автоинкремента попадают в insert! --->
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
#param.field#
</cfif>
</cfloop>
)
values
(
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
)
<!--- /*MSSQL syntax*/
<cfif hasAutoIncrement>
select scope_identity() as id;
</cfif>
--->
<!---*** PostgreSQL incompatible hack follows--->
<cfif hasAutoIncrement>
returning #thistag.params[autoIncrementKeyIndex].field# as id;
</cfif>;
</cfquery>
<!--- <cfdump var=#qSave#/> --->
<cfif hasAutoIncrement>
<cfset keys[autoIncrementKeyIndex].value=qSave.id/>
</cfif>
<!--- здесь можно установить recordExists=true, если твердо верить, что вставка прошла, если нет исключения --->
<cfset recordExists=true/>
</cfif>
<!--- recordExists --->
<cfset processingMode="read"/>
<cfcatch type="database">
<cfset errorState="true" />
<cfset errorMessage="Ошибка при записи в базу данных. #cfcatch.message# #cfcatch.detail#" />
<cfset processingMode="pass" />
</cfcatch>
</cftry>
<cfelse><!--- (not) validationOk --->
<cfset processingMode="pass"/>
</cfif><!--- validationOk --->
</cfif><!--- processingMode EQ "save" --->
<!--- экспортируем данные --->
<cfswitch expression=#processingMode#>
<cfcase value="read">
<cfquery name="qRead" datasource="#ATTRIBUTES.datasource#">
select
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfset i=i+1/>
#param.field#<cfif i LT ArrayLen(thistag.params)>,</cfif>
</cfloop>
from #ATTRIBUTES.table#
where 1=1
<cfloop array=#keys# index="key">
/*#sqlType(key.type)#*/
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop>
</cfquery>
<cfloop array=#thistag.params# index="param"><!--- *** --->
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", structFind(qRead, #param.field#)) />
</cfloop>
</cfcase>
<cfcase value="pass"><!--- заполнить все поля. Если значения переданы в форме или урле, тег параметра обязан их разобрать и подставить, в противном случае подставить инит-значения. Инит-значение по умолчанию пустая строка, дефолт по умолчанию тоже пустая строка --->
<cfloop array=#thistag.params# index="param">
<!--- <cfdump var=#param#/>--->
<cfif param.hasInput>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.submittedValue) /><!---*** дефект: 03.12.2015, передаваемая строкой, при обработке dateFormat неправильно парсится --->
<cfelse>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.init) />
</cfif>
</cfloop>
</cfcase>
<cfdefaultcase>
</cfdefaultcase>
</cfswitch>
<cfif len(ATTRIBUTES.info)><!--- экспортируем дополнительные сведения--->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.submittedValue", #param.submittedValue#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorState", #param.errorState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorMessage", #param.errorMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningState", #param.warningState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningMessage", #param.warningMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.null", #param.null#) />
</cfloop>
</cfif>
<cfif len(ATTRIBUTES.meta)><!--- экспортируем метаданные параметра - для инвариантности вызовов --->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.param", #param.param#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.field", #param.field#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.type", #param.type#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.displayName", #param.displayName#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.default", #param.default#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.size", #param.size#) />
</cfloop>
</cfif>
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(errorState,errorMessage,initialProcessingMode,recordExists))/>
</cfif><!--- thisTag.ExecutionMode is 'end'--->
<!--- quick and dirty --->
<cffunction name="sqlType">
<cfargument name="type"/>
<cfswitch expression=#type#>
<cfcase value="guid,uuid,idstamp"><cfreturn "CF_SQL_OTHER"/></cfcase><!--- hack for PostgreSQL uuid --->
<cfdefaultcase><cfreturn uCase("CF_SQL_#type#")/></cfdefaultcase>
</cfswitch>
</cffunction>
<cffunction name="checkDataFormat">
<cfargument name="type"/>
<cfargument name="value"/>
<cfswitch expression=#type#>
<cfcase value="integer"><cfreturn isValid("integer",value)/></cfcase>
<cfcase value="guid,uuid,idstamp"><cfreturn isValid("guid",value)/></cfcase>
<cfdefaultcase><cfreturn true/></cfdefaultcase>
</cfswitch>
</cffunction>
<!--- //quick and dirty
function sqlType(type) {
switch (type) {
case "guid,idstamp": return "CF_SQL_OTHER"; /*hack for PostgreSQL uuid*/ /*break not needed!*/
default: return uCase("CF_SQL_#type#");
}
}
function checkDataFormat(type,value) {
switch (type) {
case "integer": return isValid("integer",value); /*break not needed!*/
case "guid,uuid,idstamp": return isValid("guid",value); /*break not needed!*/ /*INCORRECT cfscript switch-case does not support lists*/
default: return true;
}
} --->
<cfscript>
//readonly может транслироваться в pass!
function translateMode(readonly) {
if (readonly) {
return "read";
} else {
return "";
}
}
</cfscript>
@@ -0,0 +1,400 @@
<!---
bean v0.95
msyu@mail.ru
2013-08-29 v0.1
2013-09-04 v0.2 tested with CF8 and Railo
2013-10-14 added meta attribute - can now export each parameter metadata
2014-12-26 POST-GET read-pass-save
2015-01-19 read-pass-save, добавлен formmarker, убрано определение режима по POST-GET
2015-01-19 убран formmarker, упразднен режим init (используется pass)
2015-07-16 добавлен passmarker для перерисовки формы с введенными значениями
2015-10-02 поддержка составных ключей
2015-10-09 v0.9 если задано value параметра, считается, что он hasInput (корректно работает в составе ключа)
2015-11-25 добавлен readmarker, mode (принудительная установка режима)
2015-11-26 добавлен mode=skip
2016-09-01 изменена трактовка processingMode. Теперь понимается как исходная команда, фиксируется до начала действий. Можно использовать для управления
2018-09-16 добавлена ограниченная поддержка нецелых первичных ключей (idstamp)
child tag: param
to save - should send parameter with name from list ATTRIBUTES.savemarker (value doesn't matter) in FORM, e.g. URL.save="trtrtr", while ATTRIBUTES.savemarker="save,saveAndClose"; for multiple forms on a page (and bean tags) different savemarkers should be used
2022-01-06 PostgreSQL (MSSQL incompatibility because of scope_identity)
2024-04-13 PostgreSQL GUID, autoincrement by RETURNING
--->
<!---
Автогенерируемое поле GUID можно заполнять несколькими способами
- как default или init bean/param, но это не очень удобно, потому что может потеряться удобный признак новой записи - инвалидный ключ, и сейчас init-default взаимодействуют некорректно, по крайней мере, неочевидно
- генерируемое в БД (например, как default). Для PostgreSQL значение возвращается через Returning, как это делать для других БД, непонятно.
- можно генерировать при сохранении в bean, но для этого нужно расширение синтаксиса, зато не зависит от БД (*** TODO)
--->
<cfif thisTag.ExecutionMode is 'end'>
<cfparam name="ATTRIBUTES.readmarker" type="string" default="read"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда перечитывания. Имеет приоритет перед savemarker, passmarker. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.savemarker" type="string" default="save,saveAndClose"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда сохранения. Обычно это имя кнопки. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.passmarker" type="string" default="pass"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда заполнить форму отправленными данными без перечитывания. В форме такое поле обычно есть. Команда сохранения имеет приоритет перед пробросом. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.datasource" type="string"/>
<cfparam name="ATTRIBUTES.table" type="string"/>
<cfparam name="ATTRIBUTES.output" type="string"/>
<cfparam name="ATTRIBUTES.info" type="string" default=""/>
<cfparam name="ATTRIBUTES.meta" type="string" default=""/>
<cfparam name="ATTRIBUTES.status" type="string"/>
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
<cfparam name="ATTRIBUTES.mode" type="string" default=""/> <!--- Принудительный режим read|pass|save|skip. Имеет приоритет перед маркерами --->
<cfif ATTRIBUTES.mode EQ "skip">
<!---<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorState", false) />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorMessage", "") />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.processingMode", "skip") />--->
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(false,"","skip"))/>
<cfexit method="exittag"/>
</cfif>
<cfset validationOk="true" />
<cfset errorState="false" />
<cfset errorMessage="" />
<cfset warningState="false" />
<cfset warningMessage="" />
<!---<cfset keyField=""/>
<cfset keyValue=""/>--->
<cfset hasAutoIncrement="false"/>
<cfset autoIncrementKeyIndex=0/><!--- сгенерированное значение присваивается первому полю, отмеченному как autoincrement --->
<cfset recordExists = "false"/>
<cfset keyInvalid = "false"/>
<!--- разбор метаданных - поиск ключа --->
<!--- поддерживаются только простые целочисленные ключи --->
<cfset keys=arrayNew(1)/>
<!--- *** почему где-то копируем поля param в key, а где-то прямо цикл по параметрам --->
<!--- коллекция thistag.params формируется вложенными тегами param --->
<cfloop array=#thistag.params# index="param">
<!---<cfdump var=#param#/>--->
<cfif structFind(param,"key")><!--- not just structKeyExists! Not exisis, OR exists, but FALSE! --->
<cfset key = structNew()/>
<cfset ArrayAppend(keys, key)/>
<cfset key.name = param.param />
<cfset key.field = param.field />
<cfset key.type = param.type />
<cfset key.value = param.value /><!--- должно уже быть присвоено --->
<cfset key.null = param.null />
<cfif param.errorState OR !param.hasInput>
<cfset keyInvalid="true"/>
<cfelse>
<cfset keyInvalid=!checkDataFormat(param.type, param.value)/>
</cfif>
<cfif !hasAutoIncrement><!--- take only first field with "autoincrement" attribute--->
<cfif structFind(param, "autoincrement")>
<cfset hasAutoIncrement = true/>
<cfset autoIncrementKeyIndex = arrayLen(keys)/>
</cfif>
</cfif>
</cfif>
</cfloop>
<!---<cfdump var=#keys#/>--->
<cfif !keyInvalid>
<!--- *** дублируется похожий cfqueryparam, перепутал один раз --->
<cfquery name="qCheckExistence" datasource="#ATTRIBUTES.datasource#">
select count(*) as cnt
from #ATTRIBUTES.table#
where 1=1
<cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop>
</cfquery>
<cfset recordExists = (qCheckExistence.cnt GT 0)/>
<!--- <cfdump var=#qCheckExistence#/> --->
<cfelse>
<cfset recordExists = "false"/>
</cfif>
<cfif listFindNoCase("read,pass,save", ATTRIBUTES.mode)>
<cfset processingMode=ATTRIBUTES.mode/>
<cfelse>
<!--- ищем команду принудительного перечитывания --->
<cfset doRead = false/>
<cfloop list=#ATTRIBUTES.readmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doRead = true/>
<cfbreak/>
</cfif>
</cfloop>
<!--- ищем команду сохранения --->
<cfset doSave = false/>
<cfif !doRead AND !ATTRIBUTES.readonly>
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doSave = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- ищем команду проброса --->
<cfset doPass = false/>
<cfif !doRead AND !doSave>
<cfloop list=#ATTRIBUTES.passmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doPass = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- определяем режим --->
<cfif doRead>
<cfset processingMode="read"/>
<cfelseif doSave>
<cfset processingMode="save"/>
<cfelse>
<cfif doPass OR !recordExists>
<cfset processingMode="pass"/>
<cfelse>
<cfset processingMode="read"/>
</cfif>
</cfif>
</cfif>
<cfset initialProcessingMode = processingMode/>
<!---debug <cfoutput>processingMode:#processingMode#</cfoutput>--->
<!--- ---------------------------------------------------------- --->
<!---*** Необходимость вставки (против апдейта) определяется по существованию записи. Это небезопасно, поскольку наличие записи с ключом -1 может прекратить создание новых записей. Конечно, можно добавить констрейнт на ключевое поле, но это лишняя зависимость.
Можно явно передавать команду вставки, а не заморачиваться с анализом ключа. Просто традиционно мы сигнализировали необходимость вставки, передавая невалидное значение ключа, обычно -1. Для целочисленных ключей можно было бы просто проверять положительность (что всю жизнь отлично работало), но хочется более общего.
--->
<cfif processingMode IS "save"> <!--- validation lookup --->
<cfloop array=#thistag.params# index="param">
<cfif !param.key>
<cfif param.warningState>
<cfif NOT warningState><!--- catch the first fault --->
<cfset warningMessage=param.warningMessage />
</cfif>
<cfset warningState="true" />
</cfif>
<cfif param.errorState>
<cfif NOT errorState><!--- catch the first fault --->
<cfset errorMessage=param.errorMessage />
</cfif>
<cfset errorState="true" />
<cfset validationOk="false"/>
</cfif>
</cfif>
<cfif errorState>
<!--- clear warning state --->
<cfset warningMessage="" />
<cfset warningState=false />
<!--- finish lookup --->
<cfbreak/>
</cfif>
</cfloop>
<cfif validationOk>
<!---<cfdump var=#param#/> --->
<cftry>
<cfif recordExists><!--- update. Наличие keyValue гарантировано (уверен) --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#" result="qSaveResult">
update #ATTRIBUTES.table# set
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<!--- не обновляем ключевые поля --->
<cfif NOT (StructFind(param, "key")
OR StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipUpdate")
)><cfif i++ GT 0>,</cfif>
#param.field#=<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
where 1=1
<cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#"/>
</cfloop>
</cfquery>
<!---<cfdump var=#qSaveResult#/>--->
<cfelse><!--- insert --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#">
insert into #ATTRIBUTES.table#
(
<cfset i=0 />
<!--- ключевые поля без автоинкремента попадают в insert! --->
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
#param.field#
</cfif>
</cfloop>
)
values
(
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
);
<!---
<cfif hasAutoIncrement>
select scope_identity() as id;
</cfif>
--->
<!---
*** PostgreSQL incompatible hack
--->
<cfif hasAutoIncrement>
select LASTVAL() as id;
</cfif>
</cfquery>
<cfif hasAutoIncrement>
<cfset keys[autoIncrementKeyIndex].value=qSave.id/>
</cfif>
</cfif>
<!--- recordExists --->
<cfset processingMode="read"/>
<cfcatch type="database">
<cfset errorState="true" />
<cfset errorMessage="Ошибка при записи в базу данных. #cfcatch.message# #cfcatch.detail#" />
<cfset processingMode="pass" />
</cfcatch>
</cftry>
<cfelse><!--- (not) validationOk --->
<cfset processingMode="pass"/>
</cfif><!--- validationOk --->
</cfif><!--- processingMode EQ "save" --->
<!--- экспортируем данные --->
<cfswitch expression=#processingMode#>
<cfcase value="read">
<cfquery name="qRead" datasource="#ATTRIBUTES.datasource#">
select
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfset i=i+1/>
#param.field#<cfif i LT ArrayLen(thistag.params)>,</cfif>
</cfloop>
from #ATTRIBUTES.table#
where 1=1
<cfloop array=#keys# index="key">
/*#sqlType(key.type)#*/
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop>
</cfquery>
<cfloop array=#thistag.params# index="param"><!--- *** --->
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", structFind(qRead, #param.field#)) />
</cfloop>
</cfcase>
<cfcase value="pass"><!--- заполнить все поля. Если значения переданы в форме или урле, тег параметра обязан их разобрать и подставить, в противном случае подставить инит-значения. Инит-значение по умолчанию пустая строка, дефолт по умолчанию тоже пустая строка --->
<cfloop array=#thistag.params# index="param">
<!--- <cfdump var=#param#/>--->
<cfif param.hasInput>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.submittedValue) /><!---*** дефект: 03.12.2015, передаваемая строкой, при обработке dateFormat неправильно парсится --->
<cfelse>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.init) />
</cfif>
</cfloop>
</cfcase>
<cfdefaultcase>
</cfdefaultcase>
</cfswitch>
<cfif len(ATTRIBUTES.info)><!--- экспортируем дополнительные сведения--->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.submittedValue", #param.submittedValue#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorState", #param.errorState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorMessage", #param.errorMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningState", #param.warningState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningMessage", #param.warningMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.null", #param.null#) />
</cfloop>
</cfif>
<cfif len(ATTRIBUTES.meta)><!--- экспортируем метаданные параметра - для инвариантности вызовов --->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.param", #param.param#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.field", #param.field#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.type", #param.type#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.displayName", #param.displayName#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.default", #param.default#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.size", #param.size#) />
</cfloop>
</cfif>
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(errorState,errorMessage,initialProcessingMode))/>
</cfif><!--- thisTag.ExecutionMode is 'end'--->
<!--- quick and dirty --->
<cffunction name="sqlType">
<cfargument name="type"/>
<cfswitch expression=#type#>
<cfcase value="guid,uuid,idstamp"><cfreturn "CF_SQL_OTHER"/></cfcase><!--- hack for PostgreSQL uuid --->
<cfdefaultcase><cfreturn uCase("CF_SQL_#type#")/></cfdefaultcase>
</cfswitch>
</cffunction>
<cffunction name="checkDataFormat">
<cfargument name="type"/>
<cfargument name="value"/>
<cfswitch expression=#type#>
<cfcase value="integer"><cfreturn isValid("integer",value)/></cfcase>
<cfcase value="guid,uuid,idstamp"><cfreturn isValid("guid",value)/></cfcase>
<cfdefaultcase><cfreturn true/></cfdefaultcase>
</cfswitch>
</cffunction>
<!--- //quick and dirty
function sqlType(type) {
switch (type) {
case "guid,idstamp": return "CF_SQL_OTHER"; /*hack for PostgreSQL uuid*/ /*break not needed!*/
default: return uCase("CF_SQL_#type#");
}
}
function checkDataFormat(type,value) {
switch (type) {
case "integer": return isValid("integer",value); /*break not needed!*/
case "guid,uuid,idstamp": return isValid("guid",value); /*break not needed!*/ /*INCORRECT cfscript switch-case does not support lists*/
default: return true;
}
} --->
<cfscript>
//readonly может транслироваться в pass!
function translateMode(readonly) {
if (readonly) {
return "read";
} else {
return "";
}
}
</cfscript>
+398
View File
@@ -0,0 +1,398 @@
<!---
bean v0.95
msyu@mail.ru
2013-08-29 v0.1
2013-09-04 v0.2 tested with CF8 and Railo
2013-10-14 added meta attribute - can now export each parameter metadata
2014-12-26 POST-GET read-pass-save
2015-01-19 read-pass-save, добавлен formmarker, убрано определение режима по POST-GET
2015-01-19 убран formmarker, упразднен режим init (используется pass)
2015-07-16 добавлен passmarker для перерисовки формы с введенными значениями
2015-10-02 поддержка составных ключей
2015-10-09 v0.9 если задано value параметра, считается, что он hasInput (корректно работает в составе ключа)
2015-11-25 добавлен readmarker, mode (принудительная установка режима)
2015-11-26 добавлен mode=skip
2016-09-01 изменена трактовка processingMode. Теперь понимается как исходная команда, фиксируется до начала действий. Можно использовать для управления
2018-09-16 добавлена ограниченная поддержка нецелых первичных ключей (idstamp)
child tag: param
to save - should send parameter with name from list ATTRIBUTES.savemarker (value doesn't matter) in FORM, e.g. URL.save="trtrtr", while ATTRIBUTES.savemarker="save,saveAndClose"; for multiple forms on a page (and bean tags) different savemarkers should be used
2022-01-06 PostgreSQL (MSSQL incompatibility because of scope_identity)
2024-04-13 PostgreSQL GUID, autoincrement by RETURNING
--->
<!---
Автогенерируемое поле GUID можно заполнять несколькими способами
- как default или init bean/param, но это не очень удобно, потому что может потеряться удобный признак новой записи - инвалидный ключ, и сейчас init-default взаимодействуют некорректно, по крайней мере, неочевидно
- генерируемое в БД (например, как default). Для PostgreSQL значение возвращается через Returning, как это делать для других БД, непонятно.
- можно генерировать при сохранении в bean, но для этого нужно расширение синтаксиса, зато не зависит от БД (*** TODO)
--->
<cfif thisTag.ExecutionMode is 'end'>
<cfparam name="ATTRIBUTES.readmarker" type="string" default="read"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда перечитывания. Имеет приоритет перед savemarker, passmarker. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.savemarker" type="string" default="save,saveAndClose"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда сохранения. Обычно это имя кнопки. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.passmarker" type="string" default="pass"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда заполнить форму отправленными данными без перечитывания. В форме такое поле обычно есть. Команда сохранения имеет приоритет перед пробросом. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.datasource" type="string"/>
<cfparam name="ATTRIBUTES.table" type="string"/>
<cfparam name="ATTRIBUTES.output" type="string"/>
<cfparam name="ATTRIBUTES.info" type="string" default=""/>
<cfparam name="ATTRIBUTES.meta" type="string" default=""/>
<cfparam name="ATTRIBUTES.status" type="string"/>
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
<cfparam name="ATTRIBUTES.mode" type="string" default=""/> <!--- Принудительный режим read|pass|save|skip. Имеет приоритет перед маркерами --->
<cfif ATTRIBUTES.mode EQ "skip">
<!---<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorState", false) />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorMessage", "") />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.processingMode", "skip") />--->
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(false,"","skip"))/>
<cfexit method="exittag"/>
</cfif>
<cfset validationOk="true" />
<cfset errorState="false" />
<cfset errorMessage="" />
<cfset warningState="false" />
<cfset warningMessage="" />
<!---<cfset keyField=""/>
<cfset keyValue=""/>--->
<cfset hasAutoIncrement="false"/>
<cfset autoIncrementKeyIndex=0/><!--- сгенерированное значение присваивается первому полю, отмеченному как autoincrement --->
<cfset recordExists = "false"/>
<cfset keyInvalid = "false"/>
<!--- разбор метаданных - поиск ключа --->
<!--- поддерживаются только простые целочисленные ключи --->
<cfset keys=arrayNew(1)/>
<!--- *** почему где-то копируем поля param в key, а где-то прямо цикл по параметрам --->
<!--- коллекция thistag.params формируется вложенными тегами param --->
<cfloop array=#thistag.params# index="param">
<!---<cfdump var=#param#/>--->
<cfif structFind(param,"key")><!--- not just structKeyExists! Not exisis, OR exists, but FALSE! --->
<cfset key = structNew()/>
<cfset ArrayAppend(keys, key)/>
<cfset key.name = param.param />
<cfset key.field = param.field />
<cfset key.type = param.type />
<cfset key.value = param.value /><!--- должно уже быть присвоено --->
<cfset key.null = param.null />
<cfif param.errorState OR !param.hasInput>
<cfset keyInvalid="true"/>
<cfelse>
<cfset keyInvalid=!checkDataFormat(param.type, param.value)/>
</cfif>
<cfif !hasAutoIncrement><!--- take only first field with "autoincrement" attribute--->
<cfif structFind(param, "autoincrement")>
<cfset hasAutoIncrement = true/>
<cfset autoIncrementKeyIndex = arrayLen(keys)/>
</cfif>
</cfif>
</cfif>
</cfloop>
<!---<cfdump var=#keys#/>--->
<cfif !keyInvalid>
<!--- *** дублируется похожий cfqueryparam, перепутал один раз --->
<cfquery name="qCheckExistence" datasource="#ATTRIBUTES.datasource#">
select count(*) as cnt
from #ATTRIBUTES.table#
where 1=1
<cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop>
</cfquery>
<cfset recordExists = (qCheckExistence.cnt GT 0)/>
<!--- <cfdump var=#qCheckExistence#/> --->
<cfelse>
<cfset recordExists = "false"/>
</cfif>
<cfif listFindNoCase("read,pass,save", ATTRIBUTES.mode)>
<cfset processingMode=ATTRIBUTES.mode/>
<cfelse>
<!--- ищем команду принудительного перечитывания --->
<cfset doRead = false/>
<cfloop list=#ATTRIBUTES.readmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doRead = true/>
<cfbreak/>
</cfif>
</cfloop>
<!--- ищем команду сохранения --->
<cfset doSave = false/>
<cfif !doRead AND !ATTRIBUTES.readonly>
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doSave = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- ищем команду проброса --->
<cfset doPass = false/>
<cfif !doRead AND !doSave>
<cfloop list=#ATTRIBUTES.passmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doPass = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- определяем режим --->
<cfif doRead>
<cfset processingMode="read"/>
<cfelseif doSave>
<cfset processingMode="save"/>
<cfelse>
<cfif doPass OR !recordExists>
<cfset processingMode="pass"/>
<cfelse>
<cfset processingMode="read"/>
</cfif>
</cfif>
</cfif>
<cfset initialProcessingMode = processingMode/>
<!---debug <cfoutput>processingMode:#processingMode#</cfoutput>--->
<!--- ---------------------------------------------------------- --->
<!---*** Необходимость вставки (против апдейта) определяется по существованию записи. Это небезопасно, поскольку наличие записи с ключом -1 может прекратить создание новых записей. Конечно, можно добавить констрейнт на ключевое поле, но это лишняя зависимость.
Можно явно передавать команду вставки, а не заморачиваться с анализом ключа. Просто традиционно мы сигнализировали необходимость вставки, передавая невалидное значение ключа, обычно -1. Для целочисленных ключей можно было бы просто проверять положительность (что всю жизнь отлично работало), но хочется более общего.
--->
<cfif processingMode IS "save"> <!--- validation lookup --->
<cfloop array=#thistag.params# index="param">
<cfif !param.key>
<cfif param.warningState>
<cfif NOT warningState><!--- catch the first fault --->
<cfset warningMessage=param.warningMessage />
</cfif>
<cfset warningState="true" />
</cfif>
<cfif param.errorState>
<cfif NOT errorState><!--- catch the first fault --->
<cfset errorMessage=param.errorMessage />
</cfif>
<cfset errorState="true" />
<cfset validationOk="false"/>
</cfif>
</cfif>
<cfif errorState>
<!--- clear warning state --->
<cfset warningMessage="" />
<cfset warningState=false />
<!--- finish lookup --->
<cfbreak/>
</cfif>
</cfloop>
<cfif validationOk>
<!---<cfdump var=#param#/> --->
<cftry>
<cfif recordExists><!--- update. Наличие keyValue гарантировано (уверен) --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#" result="qSaveResult">
update #ATTRIBUTES.table# set
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<!--- не обновляем ключевые поля --->
<cfif NOT (StructFind(param, "key")
OR StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipUpdate")
)><cfif i++ GT 0>,</cfif>
#param.field#=<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
where 1=1
<cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#"/>
</cfloop>
</cfquery>
<!---<cfdump var=#qSaveResult#/>--->
<cfelse><!--- insert --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#">
insert into #ATTRIBUTES.table#
(
<cfset i=0 />
<!--- ключевые поля без автоинкремента попадают в insert! --->
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
#param.field#
</cfif>
</cfloop>
)
values
(
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
)
<!---
<cfif hasAutoIncrement>
select scope_identity() as id;
</cfif>
--->
<!---*** PostgreSQL incompatible hack follows--->
<cfif hasAutoIncrement>
returning #thistag.params[autoIncrementKeyIndex].field# as id;
</cfif>;
</cfquery>
<!--- <cfdump var=#qSave#/> --->
<cfif hasAutoIncrement>
<cfset keys[autoIncrementKeyIndex].value=qSave.id/>
</cfif>
</cfif>
<!--- recordExists --->
<cfset processingMode="read"/>
<cfcatch type="database">
<cfset errorState="true" />
<cfset errorMessage="Ошибка при записи в базу данных. #cfcatch.message# #cfcatch.detail#" />
<cfset processingMode="pass" />
</cfcatch>
</cftry>
<cfelse><!--- (not) validationOk --->
<cfset processingMode="pass"/>
</cfif><!--- validationOk --->
</cfif><!--- processingMode EQ "save" --->
<!--- экспортируем данные --->
<cfswitch expression=#processingMode#>
<cfcase value="read">
<cfquery name="qRead" datasource="#ATTRIBUTES.datasource#">
select
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfset i=i+1/>
#param.field#<cfif i LT ArrayLen(thistag.params)>,</cfif>
</cfloop>
from #ATTRIBUTES.table#
where 1=1
<cfloop array=#keys# index="key">
/*#sqlType(key.type)#*/
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop>
</cfquery>
<cfloop array=#thistag.params# index="param"><!--- *** --->
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", structFind(qRead, #param.field#)) />
</cfloop>
</cfcase>
<cfcase value="pass"><!--- заполнить все поля. Если значения переданы в форме или урле, тег параметра обязан их разобрать и подставить, в противном случае подставить инит-значения. Инит-значение по умолчанию пустая строка, дефолт по умолчанию тоже пустая строка --->
<cfloop array=#thistag.params# index="param">
<!--- <cfdump var=#param#/>--->
<cfif param.hasInput>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.submittedValue) /><!---*** дефект: 03.12.2015, передаваемая строкой, при обработке dateFormat неправильно парсится --->
<cfelse>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.init) />
</cfif>
</cfloop>
</cfcase>
<cfdefaultcase>
</cfdefaultcase>
</cfswitch>
<cfif len(ATTRIBUTES.info)><!--- экспортируем дополнительные сведения--->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.submittedValue", #param.submittedValue#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorState", #param.errorState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorMessage", #param.errorMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningState", #param.warningState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningMessage", #param.warningMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.null", #param.null#) />
</cfloop>
</cfif>
<cfif len(ATTRIBUTES.meta)><!--- экспортируем метаданные параметра - для инвариантности вызовов --->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.param", #param.param#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.field", #param.field#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.type", #param.type#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.displayName", #param.displayName#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.default", #param.default#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.size", #param.size#) />
</cfloop>
</cfif>
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(errorState,errorMessage,initialProcessingMode))/>
</cfif><!--- thisTag.ExecutionMode is 'end'--->
<!--- quick and dirty --->
<cffunction name="sqlType">
<cfargument name="type"/>
<cfswitch expression=#type#>
<cfcase value="guid,uuid,idstamp"><cfreturn "CF_SQL_OTHER"/></cfcase><!--- hack for PostgreSQL uuid --->
<cfdefaultcase><cfreturn uCase("CF_SQL_#type#")/></cfdefaultcase>
</cfswitch>
</cffunction>
<cffunction name="checkDataFormat">
<cfargument name="type"/>
<cfargument name="value"/>
<cfswitch expression=#type#>
<cfcase value="integer"><cfreturn isValid("integer",value)/></cfcase>
<cfcase value="guid,uuid,idstamp"><cfreturn isValid("guid",value)/></cfcase>
<cfdefaultcase><cfreturn true/></cfdefaultcase>
</cfswitch>
</cffunction>
<!--- //quick and dirty
function sqlType(type) {
switch (type) {
case "guid,idstamp": return "CF_SQL_OTHER"; /*hack for PostgreSQL uuid*/ /*break not needed!*/
default: return uCase("CF_SQL_#type#");
}
}
function checkDataFormat(type,value) {
switch (type) {
case "integer": return isValid("integer",value); /*break not needed!*/
case "guid,uuid,idstamp": return isValid("guid",value); /*break not needed!*/ /*INCORRECT cfscript switch-case does not support lists*/
default: return true;
}
} --->
<cfscript>
//readonly может транслироваться в pass!
function translateMode(readonly) {
if (readonly) {
return "read";
} else {
return "";
}
}
</cfscript>
@@ -0,0 +1,398 @@
<!---
bean v0.95
msyu@mail.ru
2013-08-29 v0.1
2013-09-04 v0.2 tested with CF8 and Railo
2013-10-14 added meta attribute - can now export each parameter metadata
2014-12-26 POST-GET read-pass-save
2015-01-19 read-pass-save, добавлен formmarker, убрано определение режима по POST-GET
2015-01-19 убран formmarker, упразднен режим init (используется pass)
2015-07-16 добавлен passmarker для перерисовки формы с введенными значениями
2015-10-02 поддержка составных ключей
2015-10-09 v0.9 если задано value параметра, считается, что он hasInput (корректно работает в составе ключа)
2015-11-25 добавлен readmarker, mode (принудительная установка режима)
2015-11-26 добавлен mode=skip
2016-09-01 изменена трактовка processingMode. Теперь понимается как исходная команда, фиксируется до начала действий. Можно использовать для управления
2018-09-16 добавлена ограниченная поддержка нецелых первичных ключей (idstamp)
child tag: param
to save - should send parameter with name from list ATTRIBUTES.savemarker (value doesn't matter) in FORM, e.g. URL.save="trtrtr", while ATTRIBUTES.savemarker="save,saveAndClose"; for multiple forms on a page (and bean tags) different savemarkers should be used
2022-01-06 PostgreSQL (MSSQL incompatibility because of scope_identity)
2024-04-13 PostgreSQL GUID, autoincrement by RETURNING
--->
<!---
Автогенерируемое поле GUID можно заполнять несколькими способами
- как default или init bean/param, но это не очень удобно, потому что может потеряться удобный признак новой записи - инвалидный ключ, и сейчас init-default взаимодействуют некорректно, по крайней мере, неочевидно
- генерируемое в БД (например, как default). Для PostgreSQL значение возвращается через Returning, как это делать для других БД, непонятно.
- можно генерировать при сохранении в bean, но для этого нужно расширение синтаксиса, зато не зависит от БД (*** TODO)
--->
<cfif thisTag.ExecutionMode is 'end'>
<cfparam name="ATTRIBUTES.readmarker" type="string" default="read"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда перечитывания. Имеет приоритет перед savemarker, passmarker. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.savemarker" type="string" default="save,saveAndClose"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда сохранения. Обычно это имя кнопки. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.passmarker" type="string" default="pass"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда заполнить форму отправленными данными без перечитывания. В форме такое поле обычно есть. Команда сохранения имеет приоритет перед пробросом. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.datasource" type="string"/>
<cfparam name="ATTRIBUTES.table" type="string"/>
<cfparam name="ATTRIBUTES.output" type="string"/>
<cfparam name="ATTRIBUTES.info" type="string" default=""/>
<cfparam name="ATTRIBUTES.meta" type="string" default=""/>
<cfparam name="ATTRIBUTES.status" type="string"/>
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
<cfparam name="ATTRIBUTES.mode" type="string" default=""/> <!--- Принудительный режим read|pass|save|skip. Имеет приоритет перед маркерами --->
<cfif ATTRIBUTES.mode EQ "skip">
<!---<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorState", false) />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorMessage", "") />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.processingMode", "skip") />--->
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(false,"","skip"))/>
<cfexit method="exittag"/>
</cfif>
<cfset validationOk="true" />
<cfset errorState="false" />
<cfset errorMessage="" />
<cfset warningState="false" />
<cfset warningMessage="" />
<!---<cfset keyField=""/>
<cfset keyValue=""/>--->
<cfset hasAutoIncrement="false"/>
<cfset autoIncrementKeyIndex=0/><!--- сгенерированное значение присваивается первому полю, отмеченному как autoincrement --->
<cfset recordExists = "false"/>
<cfset keyInvalid = "false"/>
<!--- разбор метаданных - поиск ключа --->
<!--- поддерживаются только простые целочисленные ключи --->
<cfset keys=arrayNew(1)/>
<!--- *** почему где-то копируем поля param в key, а где-то прямо цикл по параметрам --->
<!--- коллекция thistag.params формируется вложенными тегами param --->
<cfloop array=#thistag.params# index="param">
<!---<cfdump var=#param#/>--->
<cfif structFind(param,"key")><!--- not just structKeyExists! Not exisis, OR exists, but FALSE! --->
<cfset key = structNew()/>
<cfset ArrayAppend(keys, key)/>
<cfset key.name = param.param />
<cfset key.field = param.field />
<cfset key.type = param.type />
<cfset key.value = param.value /><!--- должно уже быть присвоено --->
<cfset key.null = param.null />
<cfif param.errorState OR !param.hasInput>
<cfset keyInvalid="true"/>
<cfelse>
<cfset keyInvalid=!checkDataFormat(param.type, param.value)/>
</cfif>
<cfif !hasAutoIncrement><!--- take only first field with "autoincrement" attribute--->
<cfif structFind(param, "autoincrement")>
<cfset hasAutoIncrement = true/>
<cfset autoIncrementKeyIndex = arrayLen(keys)/>
</cfif>
</cfif>
</cfif>
</cfloop>
<!---<cfdump var=#keys#/>--->
<cfif !keyInvalid>
<!--- *** дублируется похожий cfqueryparam, перепутал один раз --->
<cfquery name="qCheckExistence" datasource="#ATTRIBUTES.datasource#">
select count(*) as cnt
from #ATTRIBUTES.table#
where 1=1
<!--- <cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop> --->
</cfquery>
<cfset recordExists = (qCheckExistence.cnt GT 0)/>
<!--- <cfdump var=#qCheckExistence#/> --->
<cfelse>
<cfset recordExists = "false"/>
</cfif>
<cfif listFindNoCase("read,pass,save", ATTRIBUTES.mode)>
<cfset processingMode=ATTRIBUTES.mode/>
<cfelse>
<!--- ищем команду принудительного перечитывания --->
<cfset doRead = false/>
<cfloop list=#ATTRIBUTES.readmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doRead = true/>
<cfbreak/>
</cfif>
</cfloop>
<!--- ищем команду сохранения --->
<cfset doSave = false/>
<cfif !doRead AND !ATTRIBUTES.readonly>
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doSave = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- ищем команду проброса --->
<cfset doPass = false/>
<cfif !doRead AND !doSave>
<cfloop list=#ATTRIBUTES.passmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doPass = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- определяем режим --->
<cfif doRead>
<cfset processingMode="read"/>
<cfelseif doSave>
<cfset processingMode="save"/>
<cfelse>
<cfif doPass OR !recordExists>
<cfset processingMode="pass"/>
<cfelse>
<cfset processingMode="read"/>
</cfif>
</cfif>
</cfif>
<cfset initialProcessingMode = processingMode/>
<!---debug <cfoutput>processingMode:#processingMode#</cfoutput>--->
<!--- ---------------------------------------------------------- --->
<!---*** Необходимость вставки (против апдейта) определяется по существованию записи. Это небезопасно, поскольку наличие записи с ключом -1 может прекратить создание новых записей. Конечно, можно добавить констрейнт на ключевое поле, но это лишняя зависимость.
Можно явно передавать команду вставки, а не заморачиваться с анализом ключа. Просто традиционно мы сигнализировали необходимость вставки, передавая невалидное значение ключа, обычно -1. Для целочисленных ключей можно было бы просто проверять положительность (что всю жизнь отлично работало), но хочется более общего.
--->
<cfif processingMode IS "save"> <!--- validation lookup --->
<cfloop array=#thistag.params# index="param">
<cfif !param.key>
<cfif param.warningState>
<cfif NOT warningState><!--- catch the first fault --->
<cfset warningMessage=param.warningMessage />
</cfif>
<cfset warningState="true" />
</cfif>
<cfif param.errorState>
<cfif NOT errorState><!--- catch the first fault --->
<cfset errorMessage=param.errorMessage />
</cfif>
<cfset errorState="true" />
<cfset validationOk="false"/>
</cfif>
</cfif>
<cfif errorState>
<!--- clear warning state --->
<cfset warningMessage="" />
<cfset warningState=false />
<!--- finish lookup --->
<cfbreak/>
</cfif>
</cfloop>
<cfif validationOk>
<!---<cfdump var=#param#/> --->
<cftry>
<cfif recordExists><!--- update. Наличие keyValue гарантировано (уверен) --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#" result="qSaveResult">
update #ATTRIBUTES.table# set
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<!--- не обновляем ключевые поля --->
<cfif NOT (StructFind(param, "key")
OR StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipUpdate")
)><cfif i++ GT 0>,</cfif>
#param.field#=<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
where 1=1
<cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#"/>
</cfloop>
</cfquery>
<!---<cfdump var=#qSaveResult#/>--->
<cfelse><!--- insert --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#">
insert into #ATTRIBUTES.table#
(
<cfset i=0 />
<!--- ключевые поля без автоинкремента попадают в insert! --->
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
#param.field#
</cfif>
</cfloop>
)
values
(
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
)
<!---
<cfif hasAutoIncrement>
select scope_identity() as id;
</cfif>
--->
<!---*** PostgreSQL incompatible hack follows--->
<cfif hasAutoIncrement>
returning #thistag.params[autoIncrementKeyIndex].field# as id;
</cfif>;
</cfquery>
<!--- <cfdump var=#qSave#/> --->
<cfif hasAutoIncrement>
<cfset keys[autoIncrementKeyIndex].value=qSave.id/>
</cfif>
</cfif>
<!--- recordExists --->
<cfset processingMode="read"/>
<cfcatch type="database">
<cfset errorState="true" />
<cfset errorMessage="Ошибка при записи в базу данных. #cfcatch.message# #cfcatch.detail#" />
<cfset processingMode="pass" />
</cfcatch>
</cftry>
<cfelse><!--- (not) validationOk --->
<cfset processingMode="pass"/>
</cfif><!--- validationOk --->
</cfif><!--- processingMode EQ "save" --->
<!--- экспортируем данные --->
<cfswitch expression=#processingMode#>
<cfcase value="read">
<cfquery name="qRead" datasource="#ATTRIBUTES.datasource#">
select
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfset i=i+1/>
#param.field#<cfif i LT ArrayLen(thistag.params)>,</cfif>
</cfloop>
from #ATTRIBUTES.table#
where 1=1
<cfloop array=#keys# index="key">
/*#sqlType(key.type)#*/
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop>
</cfquery>
<cfloop array=#thistag.params# index="param"><!--- *** --->
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", structFind(qRead, #param.field#)) />
</cfloop>
</cfcase>
<cfcase value="pass"><!--- заполнить все поля. Если значения переданы в форме или урле, тег параметра обязан их разобрать и подставить, в противном случае подставить инит-значения. Инит-значение по умолчанию пустая строка, дефолт по умолчанию тоже пустая строка --->
<cfloop array=#thistag.params# index="param">
<!--- <cfdump var=#param#/>--->
<cfif param.hasInput>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.submittedValue) /><!---*** дефект: 03.12.2015, передаваемая строкой, при обработке dateFormat неправильно парсится --->
<cfelse>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.init) />
</cfif>
</cfloop>
</cfcase>
<cfdefaultcase>
</cfdefaultcase>
</cfswitch>
<cfif len(ATTRIBUTES.info)><!--- экспортируем дополнительные сведения--->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.submittedValue", #param.submittedValue#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorState", #param.errorState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorMessage", #param.errorMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningState", #param.warningState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningMessage", #param.warningMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.null", #param.null#) />
</cfloop>
</cfif>
<cfif len(ATTRIBUTES.meta)><!--- экспортируем метаданные параметра - для инвариантности вызовов --->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.param", #param.param#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.field", #param.field#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.type", #param.type#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.displayName", #param.displayName#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.default", #param.default#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.size", #param.size#) />
</cfloop>
</cfif>
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(errorState,errorMessage,initialProcessingMode))/>
</cfif><!--- thisTag.ExecutionMode is 'end'--->
<!--- quick and dirty --->
<cffunction name="sqlType">
<cfargument name="type"/>
<cfswitch expression=#type#>
<cfcase value="guid,uuid,idstamp"><cfreturn "CF_SQL_OTHER"/></cfcase><!--- hack for PostgreSQL uuid --->
<cfdefaultcase><cfreturn uCase("CF_SQL_#type#")/></cfdefaultcase>
</cfswitch>
</cffunction>
<cffunction name="checkDataFormat">
<cfargument name="type"/>
<cfargument name="value"/>
<cfswitch expression=#type#>
<cfcase value="integer"><cfreturn isValid("integer",value)/></cfcase>
<cfcase value="guid,uuid,idstamp"><cfreturn isValid("guid",value)/></cfcase>
<cfdefaultcase><cfreturn true/></cfdefaultcase>
</cfswitch>
</cffunction>
<!--- //quick and dirty
function sqlType(type) {
switch (type) {
case "guid,idstamp": return "CF_SQL_OTHER"; /*hack for PostgreSQL uuid*/ /*break not needed!*/
default: return uCase("CF_SQL_#type#");
}
}
function checkDataFormat(type,value) {
switch (type) {
case "integer": return isValid("integer",value); /*break not needed!*/
case "guid,uuid,idstamp": return isValid("guid",value); /*break not needed!*/ /*INCORRECT cfscript switch-case does not support lists*/
default: return true;
}
} --->
<cfscript>
//readonly может транслироваться в pass!
function translateMode(readonly) {
if (readonly) {
return "read";
} else {
return "";
}
}
</cfscript>
@@ -0,0 +1,24 @@
<cfsilent>
<!--- 20181209 v0.3 --->
<!---v0.3 добавлен атрибут cfSqlType--->
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_field_set" datacollection="fieldsArray"/>
<cfparam name="ATTRIBUTES.expression" default=""/>
<cfparam name="ATTRIBUTES.title" default=""/>
<cfparam name="ATTRIBUTES.name" default=""/>
<cfparam name="ATTRIBUTES.cfSqlType" default="CF_SQL_VARCHAR"/>
<cfif ATTRIBUTES.expression IS "">
<cfset ATTRIBUTES.expression=thisTag.generatedContent/>
</cfif>
<cfif ATTRIBUTES.name IS "">
<cfset regex="([[:word:]]+)[[:space:]]*$"/>
<cfset matches=REFindNoCase(regex, ATTRIBUTES.expression, 1, true)/>
<cfif arrayLen(matches.pos) GT 1>
<cfset ATTRIBUTES.name=mid(ATTRIBUTES.expression, matches.pos[2], matches.len[2])/>
</cfif>
</cfif>
</cfif>
</cfsilent>
@@ -0,0 +1,41 @@
<cfsilent>
<!--- 20181209 v0.3 --->
<!--- Используется в селекте для структурирования списка полей. Выводит вместо себя список через запятую, экспортирует Map со структурами выражение+заголовок, с ключом, соответствующим имени колонки в селекте (ключ вычисляется регулярным выражением) --->
<!---v0.3 добавлено поле cfSqlType--->
<cfif thisTag.executionMode is "end">
<cfparam name="ATTRIBUTES.titleMapOut"/>
<cfparam name="ATTRIBUTES.lengthOut"/>
<cfparam name="ATTRIBUTES.listOut" default=""/>
<cfparam name="thisTag.fieldsArray" type="array"/><!--- Вложенные теги field отдают сюда свои данные --->
<cfset titleMap=structNew()/>
<cfset list=""/>
<cfset i=0/>
<cfloop array=#thisTag.fieldsArray# index="field">
<cfset i=i+1/>
<cfset list=listAppend(#list#, #field.expression#)/>
<cfif len(#field.name#)>
<cfset structInsert(#titleMap#, #field.name#, structNew())/>
<cfset "titleMap.#field.name#.ordinal"=#i#/>
<cfset "titleMap.#field.name#.title"=#field.title#/>
<cfset "titleMap.#field.name#.cfSqlType"=#field.cfSqlType#/>
</cfif>
</cfloop>
<cfif len(ATTRIBUTES.lengthOut)>
<cfset "CALLER.#ATTRIBUTES.lengthOut#"=#arrayLen(thisTag.fieldsArray)#/>
</cfif>
<cfif len(ATTRIBUTES.titleMapOut)>
<cfset "CALLER.#ATTRIBUTES.titleMapOut#"=#titleMap#/>
</cfif>
<cfif len(ATTRIBUTES.listOut)>
<cfset "CALLER.#ATTRIBUTES.listOut#"=preserveSingleQuotes(list)/>
<cfelse>
<cfset thisTag.generatedContent=preserveSingleQuotes(list)/>
</cfif>
</cfif>
</cfsilent>
@@ -0,0 +1,305 @@
<!---
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
--->
<!--- проверка на неподдерживаемые поля --->
<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", #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. 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!!!--->
<!--- 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=""/>
<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 isDefined("ATTRIBUTES.preprocessor")>
<cfset submittedValue=#ATTRIBUTES.preprocessor(submittedValue)#/>
</cfif>
<!--- check required fields --->
<cfif StructFind(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>
<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.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>
<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. Если не получает, не отдает, можно это специально указывать флагом. Можно ему лишнего не делегировать.
Остальное оставить вызывающему тегу?
--->
+297
View File
@@ -0,0 +1,297 @@
<cfsilent>
<!--- v0.4---><!---9:50 06.10.2020--->
<!--- v0.5---><!---2024-12-11 15:10:07--->
<!--- DEL.CFM - унифицированный шаблон операции удаления записи --->
<cfimport prefix="m" taglib="../../lib"/>
<cfimport prefix="m" taglib="../../lib/controls"/>
</cfsilent><cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag><m:silent silent="No">
<cfparam name="ATTRIBUTES.savemarker" type="string" default="delete"/>
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
<cfparam name="ATTRIBUTES.checkQuery" type="string" default="select 0 as chk"/> <!--- запрос проверки целостности.
Простая строка (параметр преобразуется до вызова тега).
Если можно удалять, должен вернуть 0 в поле chk --->
<cfparam name="ATTRIBUTES.displayDependencies" type="boolean" default="Yes"/>
<cfparam name="ATTRIBUTES.ignoreDependencies" type="boolean" default="No"/><!--- будем удалять, несмотря на наличие зависимостей Предположительно, мы примем меры в запросе удаления, или расчитываем на каскадное удаление. Имеет смысл, видимо, только в комбинации с displayDependencies=TRUE, иначе зачем описывать зависимости и потом их игнорировать. --->
<cfparam name="ATTRIBUTES.entity" type="string"/>
<cfparam name="ATTRIBUTES.accessObj" type="string" default="#ATTRIBUTES.entity#"/> <!--- объект доступа, права к которому проверяем --->
<cfparam name="ATTRIBUTES.deleteQuery" type="string" default=""/> <!--- запрос удаления *** надо бы параметризовать --->
<cfparam name="ATTRIBUTES.confirmMessage" type="string"/> <!--- текст для javascript:confirm() --->
<cfparam name="ATTRIBUTES.denyMessage" type="string" default="Удаление невозможно, существуют зависимые записи."/> <!---
сообщение о невозможности удаления записи (если проверка целостности говорит нельзя) --->
<cfparam name="ATTRIBUTES.queryString" type="string" default=""/>
<cfparam name="ATTRIBUTES.defaultBackUrl" type="string" default="#ATTRIBUTES.entity#_ls.cfm"/>
<cfparam name="ATTRIBUTES.output" type="string" default=""/>
<cfparam name="ATTRIBUTES.status" type="string" default=""/>
<cfparam name="thisTag.dependencies" type="array" default=#arrayNew(1)#/>
<cfparam name="thisTag.conditions" type="array"/>
<cfset "CALLER.#ATTRIBUTES.status#"=CreateObject("component","status").init(false,"","")/>
<cfif len(ATTRIBUTES.queryString) EQ 0>
<cfset i = 0 />
<cfloop array=#thisTag.conditions# index="condition">
<cfif i GT 0>
<cfset ATTRIBUTES.queryString="#ATTRIBUTES.queryString#&"/>
</cfif>
<cfset i = i+1/>
<cfset ATTRIBUTES.queryString="#ATTRIBUTES.queryString##condition.field#=#condition.value#"/>
</cfloop>
</cfif>
<m:track
thisURL="#request.thisPage#?#ATTRIBUTES.queryString#"
defaultBackUrl="#ATTRIBUTES.defaultBackUrl#"
output="tr"
/>
<cfquery name="qCheck" datasource="#request.DS#">
#preserveSingleQuotes(ATTRIBUTES.checkQuery)#
</cfquery>
<cfset sDenyMessage=""/>
<cfoutput query="qCheck">
<cfset sDenyMessage=evaluate("""#ATTRIBUTES.denyMessage#""")/>
</cfoutput>
<!--- быстрая проверка --->
<cfset dependentRecordsExist = false/>
<cfloop array=#thisTag.dependencies# index="dep">
<cfif dep.query.recordCount GT 0>
<cfset dependentRecordsExist = true/>
<cfbreak/>
</cfif>
</cfloop>
<!--- do delete --->
<m:ac obj="#ATTRIBUTES.accessObj#" act="#request.PERMISSION_WRITE#" output="writePermitted">
<cfset doSave = false/>
<cfif !ATTRIBUTES.readonly>
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
<cfif structKeyExists(FORM,"#marker#")><!--- удаление можно делать только постом --->
<cfset doSave = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<cfif doSave AND qCheck.chk EQ 0>
<cftry> <!--- <cfoutput> <cfset i = 0 />
<cfloop list=#ATTRIBUTES.deleteQuery# delimiters="?" index="queryPart">
<cfif i GT 0>
cfqueryparam
cfsqltype='#condition.cfsqltype#'
value='#condition.value#'
null='#condition.null#'/
</cfif><cfset i = i + 1/>*#queryPart#* #i#
</cfloop>
</cfoutput> --->
<cfquery name="qDel" datasource="#request.DS#" result="result">
<cfif len(ATTRIBUTES.deleteQuery)>
<!--- запрос удаления должен быть сформулирован так,
чтобы условия удаления были в конце --->
<cfset i = 0 />
<cfloop list=#ATTRIBUTES.deleteQuery# delimiters="?" index="queryPart">
<cfif i GT 0>
<cfqueryparam
cfsqltype='#condition.cfsqltype#'
value='#condition.value#'
null='#condition.null#'/>
</cfif><cfset i = i + 1/>#queryPart#
</cfloop>
<cfelse>
delete from #ATTRIBUTES.entity# where /**/
<cfset i = 0 />
<cfloop array=#thisTag.conditions# index="condition">
<cfif i GT 0> AND </cfif>
<cfset i = i + 1/>
#condition.expression#=<cfqueryparam
cfsqltype='#condition.cfsqltype#'
value='#condition.value#'
null='#condition.null#'/>
</cfloop>
</cfif>
</cfquery>
<!--- *** неуклюжий костыль --->
<cfset CALLER.saveAndClose = true/><!--- сигнал для закрытия экрана --->
<cfcatch type="database">
<cfset "CALLER.#ATTRIBUTES.status#.errorState"=true/>
<cfset "CALLER.#ATTRIBUTES.status#.errorMessage"="Ошибка при обращении к базе данных. #cfcatch.message# #cfcatch.detail# #cfcatch.tagcontext[1].template# line #cfcatch.tagcontext[1].line#"/>
<!--- <cfrethrow/> --->
</cfcatch>
</cftry>
</cfif>
</m:ac>
<cfif !writePermitted><!--- перетирания cfcatch не будет --->
<cfset "CALLER.#ATTRIBUTES.status#.errorState"=true/>
<cfset "CALLER.#ATTRIBUTES.status#.errorMessage"="Ваших полномочий недостаточно для выполнения данной операции."/>
</cfif>
<!--- print output --->
<cfsavecontent variable="markup">
<cfoutput>
<div class="container-fluid">
<div class="row">
<br>
<form name="frmDel" action="#request.thisPage#" method="POST">
<cfif NOT writePermitted>
<!--- ситуация возможна при прямом заходе по URL. Надо высказать юзарю свое неодобрение. --->
<!--- <h4 class="text-danger">Ваших полномочий недостаточно для выполнения данной операции.</h4>
<br/> --->
<cfelseif qCheck.chk GT 0>
<h4 class="text-danger">#ATTRIBUTES.denyMessage#</h4>
<br/>
<cfelseif dependentRecordsExist>
<cfif ATTRIBUTES.displayDependencies>
<h4 class="text-danger">#ATTRIBUTES.denyMessage#</h4>
<br/>
<cftry>
<cfloop array=#thisTag.dependencies# index="dep">
<cfif dep.query.recordCount GT 0>
<h5>#dep.title#</h5>
<cfif dep.showDetails>
<table class="worktable">
<thead>
<tr>
<th width="1%" class="c"></th>
<cfloop array=#dep.fields# item="fld">
<cfif len(fld.title)>
<th>#fld.title#</th>
</cfif>
</cfloop>
<th width="1%"></th>
</tr>
</thead>
<cfoutput query=#dep.query#>
<cfset urlQueryString=""/>
<cfloop array=#dep.keys# index="keyName">
<cfif len(urlQueryString)>
<cfset urlQueryString="#urlQueryString#&"/><!---***--->
</cfif>
<cfset urlQueryString="#urlQueryString##keyName#=#dep.query[keyName][currentRow]#"/>
</cfloop>
<tr>
<td>
<a href="#dep.page#?#urlQueryString#&#tr.fwx#" <!---class="far fa-edit"--->><img src="img/edit.gif"/></a>
</td>
<cfloop array=#dep.fields# item="fld">
<cfif len(fld.title)>
<td>
<cfif isDefined("fld.formatter")>
#fld.formatter(dep.query[fld.name][currentRow])#
<cfelse>
#dep.query[fld.name][currentRow]#
</cfif>
</td>
</cfif>
</cfloop>
<td>
<a href="#dep.delete_page#?#urlQueryString#&#tr.fwx#" <!---class="far fa-trash-alt text-danger"--->><img src="img/del.gif"/></a>
</td>
</tr>
</cfoutput>
</table>
<cfelse><!---dep.showDetails--->
<cfoutput><p>#dep.denyMessage# (#dep.query.recordCount#)</p></cfoutput>
</cfif><!---dep.showDetails--->
</cfif>
</cfloop>
<cfcatch type="any">
<cfoutput>#cfcatch.message# #cfcatch.detail# #cfcatch.tagcontext[1].template# line #cfcatch.tagcontext[1].line#/></cfoutput><cfrethrow/>
</cfcatch>
</cftry>
<cfelse><!---ATTRIBUTES.displayDependencies--->
<cfoutput>#ATTRIBUTES.denyMessage#</cfoutput>
</cfif><!---ATTRIBUTES.displayDependencies--->
</cfif>
<cfif NOT dependentRecordsExist OR ATTRIBUTES.ignoreDependencies><!--- qCheck.chk GT 0 | dependentRecordsExist --->
<cfif writePermitted>
<input type="hidden" name="track" value="#tr.self#">
<cfloop array=#thisTag.conditions# index="condition">
<!---<input type="hidden" name="#condition.expression#" value="#evaluate('#condition.expression#')#"/>--->
<input type="hidden" name="#condition.field#" value="#evaluate('#condition.field#')#"/><!--- по замыслу, берется из скоупа FORM (*** слишком неявно)--->
</cfloop>
<button type="submit" class="maincontrol btn btn-secondary" name="delete" onclick="if(!confirm('#evaluate("""#ATTRIBUTES.confirmMessage#""")#')) return false;">Удалить</button>
</cfif>
</cfif>
<button type="button" class="maincontrol btn btn-secondary" name="close" onclick="document.location.href='#tr.backURL#'">
Закрыть
</button>
<!--- <cfdump var=#dependentRecordsExist#/>
<cfdump var=#ATTRIBUTES.ignoreDependencies#/>
<cfdump var=#NOT dependentRecordsExist OR ATTRIBUTES.ignoreDependencies#/> --->
</form>
</div>
</div>
</cfoutput>
</cfsavecontent>
</m:silent><cfif len(ATTRIBUTES.output)><cfset "CALLER.#ATTRIBUTES.output#" = #markup#/><cfelse>
<cfoutput>#markup#</cfoutput>
</cfif></cfif>
<!--- пример монструозной конструкции Postgre для каскадного удаления --->
<!--- with
inst as (
delete from instance
where instance_uid='e283e337-0ddc-459c-88b8-57d1963b4a7d'
returning instance_uid
),
st as (
delete from instance_state
where instance_uid in (select instance_uid from inst)
returning instance_state_uid
),
op as (
delete from instance_operation
where instance_uid in (select instance_uid from inst)
returning instance_operation_uid
),
par as (
delete from instance_operation_param
where instance_operation_uid in (select instance_operation_uid from op)
returning instance_operation_param_uid
),
cfs as (
delete from instance_operation_cfs_param
where instance_operation_uid in (select instance_operation_uid from op)
returning instance_operation_cfs_param_uid
)
select * from inst
left outer join st on (inst.instance_uid=st.instance_uid)
left outer join op on (inst.instance_uid=op.instance_uid)
left outer join par on (op.instance_operation_uid=par.instance_operation_uid)
left outer join cfs on (op.instance_operation_uid=cfs.instance_operation_uid)
; --->
<!--- такой вопрос: точно ли будут задействованы все CTE, если я на них не сошлюсь? На месте оптимизатора я бы проигнорил незадействованные. Наверное, надо все включить в джойн для уверенности? --->
@@ -0,0 +1,19 @@
<!---v0.2 9:50 06.10.2020
---><cfsilent>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_del" datacollection="conditions" />
<cfparam name="ATTRIBUTES.field"/><!---имя параметра формы --->
<cfparam name="ATTRIBUTES.value"/>
<cfparam name="ATTRIBUTES.expression" default=""/><!---левая сторона выражения во where, обычно имя поля--->
<cfparam name="ATTRIBUTES.cfsqltype" default="cf_sql_integer"/>
<cfparam name="ATTRIBUTES.null" type="boolean" default="false"/>
<cfif ATTRIBUTES.expression IS "">
<cfif len(thisTag.generatedContent)>
<cfset ATTRIBUTES.expression=#thisTag.generatedContent#/>
<cfelse>
<cfset ATTRIBUTES.expression=#ATTRIBUTES.field#/>
</cfif>
</cfif>
</cfif>
</cfsilent>
@@ -0,0 +1,26 @@
<cfsilent>
<cfset out=""/>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_dependency" datacollection="fieldsArray" />
<cfparam name="ATTRIBUTES.expression" default=""/>
<cfparam name="ATTRIBUTES.title" default=""/>
<cfparam name="ATTRIBUTES.name" default=""/>
<cfif ATTRIBUTES.expression IS "">
<cfset ATTRIBUTES.expression=thisTag.generatedContent/>
</cfif>
<cfif ATTRIBUTES.name IS "">
<cfset regex="([[:word:]]+)[[:space:]]*$"/>
<cfset matches=REFindNoCase(regex, ATTRIBUTES.expression, 1, true)/>
<cfif arrayLen(matches.pos) GT 1>
<cfset ATTRIBUTES.name=mid(ATTRIBUTES.expression, matches.pos[2], matches.len[2])/>
</cfif>
</cfif>
<cfif arrayLen(getBaseTagData("cf_dependency",1).thisTag.fieldsArray) GT 0>
<cfset thisTag.generatedContent=", #thisTag.generatedContent#"/>
</cfif>
</cfif>
</cfsilent>
@@ -0,0 +1,9 @@
<cfsilent>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_dependency" datacollection="keys" />
<cfparam name="ATTRIBUTES.field"/>
<cfparam name="ATTRIBUTES.cfsqltype default="cf_sql_integer"/>
</cfif>
</cfsilent>
@@ -0,0 +1,56 @@
<cfsilent>
<cfif thisTag.executionMode IS "end">
<cfassociate basetag="cf_del" datacollection="dependencies" />
<!--- following are tag attribute placeholders. Coorresponding data are used by parent tags!--->
<cfparam name="ATTRIBUTES.entity" default=""/>
<cfparam name="ATTRIBUTES.title" default=""/>
<cfparam name="ATTRIBUTES.page" default="#ATTRIBUTES.entity#.cfm"/>
<cfparam name="ATTRIBUTES.delete_page" default="#ATTRIBUTES.entity#_del.cfm"/>
<cfparam name="ATTRIBUTES.showDetails" type="boolean" default="Yes"/>
<cfparam name="ATTRIBUTES.denyMessage" type="string" default="Необходимо удалить соответствующие записи"/><!--- displayed instead of details when ATTRIBUTES.showDetails IS "No"--->
<!---<cfparam name="ATTRIBUTES.ondelete" default="restrict"/> implement 1-level Cascade? --->
<cfparam name="thisTag.fields" type="array"/>
<cfparam name="thisTag.from" type="array"/>
<cfparam name="thisTag.conditions" type="array"/>
<cfparam name="thisTag.order_by" type="array"/>
<cfset ATTRIBUTES.fields=thisTag.fields/>
<cfset ATTRIBUTES.keys=arrayNew(1)/>
<cfset i=0/>
<cfloop array=#thisTag.fields# index="field">
<cfif len(field.name)>
<cfif structKeyExists(field, "key")>
<cfset arrayAppend(ATTRIBUTES.keys, #field.name#)/>
</cfif>
</cfif>
</cfloop>
<cfquery name="qDependency" datasource="#request.DS#">
select
<cfset i = 0 />
<cfloop array=#thisTag.fields# index="field">
<cfif i GT 0>, </cfif><cfset i = i+1/>#field.expression#
</cfloop>
from
<cfloop array=#thisTag.from# index="from">
#from.expression#
</cfloop>
where
<cfset i = 0 />
<cfloop array=#thisTag.conditions# index="condition">
<cfif i GT 0> AND </cfif><cfset i = i+1/>
#condition.expression#=<cfqueryparam cfsqltype='#condition.cfsqltype#' value='#condition.value#' null='#condition.null#'/>
</cfloop>
order by
<cfset i = 0 />
<cfloop array=#thisTag.order_by# index="order">
<cfif i GT 0>, </cfif><cfset i = i+1/>#order.expression#
</cfloop>
</cfquery>
<cfset ATTRIBUTES.query=#qDependency#/>
</cfif>
<cfset thisTag.generatedContent=""/>
</cfsilent>
@@ -0,0 +1,9 @@
<cfsilent>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_dependency" datacollection="conditions" />
<cfparam name="ATTRIBUTES.expression" default=#thisTag.generatedContent#/>
<cfparam name="ATTRIBUTES.value"/>
<cfparam name="ATTRIBUTES.cfsqltype" default="cf_sql_integer"/>
<cfparam name="ATTRIBUTES.null" type="boolean" default="No"/>
</cfif>
</cfsilent>
@@ -0,0 +1,22 @@
<cfsilent></cfsilent>
<cfset out=""/>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_dependency" datacollection="fields" />
<cfparam name="ATTRIBUTES.expression" default=#thisTag.generatedContent#/>
<cfparam name="ATTRIBUTES.title" default=""/>
<cfparam name="ATTRIBUTES.name" default=""/>
<cfif ATTRIBUTES.name IS "">
<cfset regex="([[:word:]]+)[[:space:]]*$"/>
<cfset matches=REFindNoCase(regex, ATTRIBUTES.expression, 1, true)/>
<cfif arrayLen(matches.pos) GT 1>
<cfset ATTRIBUTES.name=mid(ATTRIBUTES.expression, matches.pos[2], matches.len[2])/>
</cfif>
</cfif>
<cfif arrayLen(getBaseTagData("cf_dependency",1).thisTag.fields) GT 1>
<cfset thisTag.generatedContent=", #thisTag.generatedContent#"/>
</cfif>
</cfif>
@@ -0,0 +1,6 @@
<cfsilent>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_dependency" datacollection="from" />
<cfparam name="ATTRIBUTES.expression" default=#thisTag.generatedContent# />
</cfif>
</cfsilent>
@@ -0,0 +1,6 @@
<cfsilent>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_dependency" datacollection="order_by" />
<cfparam name="ATTRIBUTES.expression" default=#thisTag.generatedContent# />
</cfif>
</cfsilent>
+66
View File
@@ -0,0 +1,66 @@
<cfsilent>
<!--- 20181209 v0.3 --->
<!---v0.3 добавлен атрибут cfSqlType--->
<!---v0.4 добавлен атрибут container for json--->
<!---v0.5 добавлен атрибут type и трансляция в CF_SQL_--->
<!--- 20210326 v0.8 formatter --->
<cffunction name="passThrough"
returntype="any"
output="false"
hint="just return argument">
<cfargument name="x" type="ANY" required="true"/>
<cfreturn #ARGUMENTS.x#/>
</cffunction>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_field_set" datacollection="fieldsArray"/>
<cfparam name="ATTRIBUTES.expression" default=""/>
<cfparam name="ATTRIBUTES.title" default=""/>
<cfparam name="ATTRIBUTES.name" default=""/>
<cfparam name="ATTRIBUTES.type" default="string"/>
<cfparam name="ATTRIBUTES.cfSqlType" default="#getCfSqlType(ATTRIBUTES.type)#"/>
<cfparam name="ATTRIBUTES.container" default=""/>
<cfparam name="ATTRIBUTES.formatter" default=#passThrough#/>
<cfif ATTRIBUTES.expression IS "">
<cfset ATTRIBUTES.expression=thisTag.generatedContent/>
</cfif>
<cfif ATTRIBUTES.name IS "">
<cfset regex="([[:word:]]+)[[:space:]]*$"/>
<cfset matches=REFindNoCase(regex, ATTRIBUTES.expression, 1, true)/>
<cfif arrayLen(matches.pos) GT 1>
<cfset ATTRIBUTES.name=mid(ATTRIBUTES.expression, matches.pos[2], matches.len[2])/>
</cfif>
</cfif>
</cfif>
<cffunction name="getCfSqlType">
<!---Translate CF type understandable by isValid to CF_SQL_*--->
<cfargument name="type"/>
<cfswitch expression=#ARGUMENTS.type#>
<cfcase value="string">
<cfreturn "CF_SQL_VARCHAR"/>
</cfcase>
<cfcase value="integer">
<cfreturn "CF_SQL_INTEGER"/>
</cfcase>
<cfcase value="boolean">
<cfreturn "CF_SQL_BIT"/>
</cfcase>
<cfcase value="numeric">
<cfreturn "CF_SQL_NUMERIC"/>
</cfcase>
<cfcase value="date,time">
<cfreturn "CF_SQL_TIMESTAMP"/>
</cfcase>
<cfdefaultcase>
<cfthrow type="custom" message="Unsupported type" detail="Type #ARGUMENTS.type# not supported"/>
</cfdefaultcase>
</cfswitch>
</cffunction>
</cfsilent>
@@ -0,0 +1,68 @@
<cfsilent>
<!--- 20181209 v0.3 добавлено поле cfSqlType--->
<!--- 20201029 v0.5 все атрибуты необязательные--->
<!--- 20201114 v0.6 добавлен атрибут "container" for json--->
<!--- 20201201 v0.7 исправлен listOut--->
<!--- 20210326 v0.8 formatter --->
<!--- 20210326 v0.9 listOutKeepContent --->
<!--- Используется в селекте для структурирования списка полей. Выводит вместо себя список через запятую, экспортирует Map со структурами выражение+заголовок, с ключом, соответствующим имени колонки в селекте (ключ вычисляется регулярным выражением) --->
<cfif thisTag.executionMode is "end">
<cfparam name="ATTRIBUTES.titleMapOut" default=""/>
<cfparam name="ATTRIBUTES.lengthOut" default=""/>
<cfparam name="ATTRIBUTES.listOut" default=""/><!---возвращаемый список полей через запятую для селекта. Если не задан (чаще всего так), этот список возвращается в виде контента тега --->
<cfparam name="ATTRIBUTES.listOutKeepContent" default=false/><!--- указывает listOut не очищать контент --->
<cfparam name="ATTRIBUTES.fieldsToInclude" default=""/><!--- список нужных полей, если не пустой - выбрасывать все поля, кроме тех, что в списке --->
<cfparam name="thisTag.fieldsArray" type="array"/><!--- Вложенные теги field отдают сюда свои данные --->
<cfset titleMap=structNew("linked")/><!---railo syntax--->
<cfset list=""/>
<cfset i=0/>
<cfloop array=#thisTag.fieldsArray# index="field">
<cfif len(#field.name#) EQ 0
OR listLen(ATTRIBUTES.fieldsToInclude) EQ 0
OR listFind(ATTRIBUTES.fieldsToInclude,field.name)>
<!--- пытаемся сохранить прежнюю логику - поля без имени не выбрасываются, но не попадают в titleMap --->
<cfset i=i+1/>
<cfset list=listAppend(#list#, #field.expression#)/>
<cfif len(#field.name#)>
<cfset structInsert(#titleMap#, #field.name#, structNew())/>
<cfset "titleMap.#field.name#.ordinal"=#i#/>
<cfset "titleMap.#field.name#.title"=#field.title#/>
<cfset "titleMap.#field.name#.type"=#field.type#/>
<cfset "titleMap.#field.name#.cfSqlType"=#field.cfSqlType#/>
<cfset "titleMap.#field.name#.container"=#field.container#/>
<cfset "titleMap.#field.name#.formatter"=#field.formatter#/>
</cfif>
</cfif>
</cfloop>
<cfif len(ATTRIBUTES.lengthOut)>
<cfset "CALLER.#ATTRIBUTES.lengthOut#"= i/>
</cfif>
<cfif len(ATTRIBUTES.titleMapOut)>
<cfset "CALLER.#ATTRIBUTES.titleMapOut#"=#titleMap#/>
</cfif>
<!--- <cfif len(ATTRIBUTES.listOut)>
<cfset "CALLER.#ATTRIBUTES.listOut#"=preserveSingleQuotes(list)/>
<cfset thisTag.generatedContent=""/>
</cfif> --->
<cfif len(ATTRIBUTES.listOut)><!--- *** некрасиво --->
<cfset "CALLER.#ATTRIBUTES.listOut#"=list/>
<cfif ATTRIBUTES.listOutKeepContent>
<cfset thisTag.generatedContent=preserveSingleQuotes(list)/>
<cfelse>
<cfset thisTag.generatedContent=""/>
</cfif>
<cfelse>
<cfset thisTag.generatedContent=preserveSingleQuotes(list)/>
</cfif>
</cfif>
</cfsilent>
+354
View File
@@ -0,0 +1,354 @@
<!---
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. Если не получает, не отдает, можно это специально указывать флагом. Можно ему лишнего не делегировать.
Остальное оставить вызывающему тегу?
--->
@@ -0,0 +1,43 @@
<cfcomponent displayname="Bean Status" output="false">
<!--- v 0.03 2024-09-05 12:36:40 added recordExists --->
<cfproperty name="errorState" type="boolean"/>
<cfproperty name="errorMessage" type="string"/>
<cfproperty name="processingMode" type="string"/>
<cfproperty name="recordExists" type="boolean"/>
<cffunction name="init">
<cfset this.errorState=false/>
<cfset this.errorMessage=""/>
<cfset this.processingMode=""/>
<cfset this.recordExists=false/>
<cfreturn this>
</cffunction>
<cffunction name="init">
<cfargument name="errorState" type="boolean"/>
<cfargument name="errorMessage" type="string"/>
<cfargument name="processingMode" type="string"/>
<cfset this.errorState=arguments.errorState/>
<cfset this.errorMessage=arguments.errorMessage/>
<cfset this.processingMode=arguments.processingMode/>
<cfreturn this>
</cffunction>
<cffunction name="init">
<cfargument name="errorState" type="boolean"/>
<cfargument name="errorMessage" type="string"/>
<cfargument name="processingMode" type="string"/>
<cfargument name="recordExists" type="boolean"/>
<cfset this.errorState=arguments.errorState/>
<cfset this.errorMessage=arguments.errorMessage/>
<cfset this.processingMode=arguments.processingMode/>
<cfset this.recordExists=arguments.recordExists/>
<cfreturn this>
</cffunction>
</cfcomponent>
@@ -0,0 +1,59 @@
<cfif thisTag.ExecutionMode is 'end'>
<cfparam name="ATTRIBUTES.output" type="string">
<cfparam name="ATTRIBUTES.outputType" type="string" default="map">
<cfparam name="thisTag.params" type="array"/>
<cffunction name="paramTypeToColumnType">
<cfargument name="paramType" type="string"/>
<cfswitch expression=#ARGUMENTS.paramType#>
<cfcase value="integer">
<cfreturn "integer"/>
</cfcase>
<cfcase value="numeric">
<cfreturn "decimal"/><!---*** --->
</cfcase>
<cfcase value="datetime">
<cfreturn "timestamp"/>
</cfcase>
<cfcase value="string">
<cfreturn "varchar"/>
</cfcase>
<cfdefaultcase>
<cfreturn "varchar"/>
</cfdefaultcase>
</cfswitch>
</cffunction>
<cfswitch expression=#ATTRIBUTES.outputType#>
<cfcase value="map">
<cfloop from=1 to=#arrayLen(thisTag.params)# index="i">
<cfset "CALLER.#ATTRIBUTES.output#.#thisTag.params[i].name#"=thisTag.params[i].value/>
</cfloop>
</cfcase>
<cfcase value="query">
<cfset columnList=""/>
<cfset columnTypeList=""/>
<cfloop from=1 to=#arrayLen(thisTag.params)# index="i">
<cfset columnList=listAppend(columnList, thisTag.params[i].name)/>
<cfset columnTypeList=listAppend(columnTypeList, paramTypeToColumnType(thisTag.params[i].type))/>
</cfloop>
<cfset qOut=queryNew(columnList,columnTypeList)/>
<cfset queryAddRow(qOut)/>
<cfloop from=1 to=#arrayLen(thisTag.params)# index="i">
<cfset querySetCell(qOut, thisTag.params[i].name, thisTag.params[i].value)/>
</cfloop>
<cfset "CALLER.#ATTRIBUTES.output#"=#qOut#/>
</cfcase>
<cfdefaultcase>
<cfthrow type="custom" message="Unsupported output type '#ATTRIBUTES.outputType#'" detail=""/>
</cfdefaultcase>
</cfswitch>
</cfif>
@@ -0,0 +1,69 @@
<cfsilent></cfsilent>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfassociate basetag="cf_submit_collection" datacollection="params" />
<cfparam name="ATTRIBUTES.name"/>
<cfparam name="ATTRIBUTES.type" default="string"/>
<cfparam name="ATTRIBUTES.default" default=""/><!--- не валидируется, что позволяет использовать, например, пустую строку для индикации нулла в числовой колонке --->
<!--- output params --->
<cfparam name="ATTRIBUTES.submittedValue" type="string" default=""/>
<cfparam name="ATTRIBUTES.value" type="any" default=""/>
<cfif structKeyExists(CALLER.VARIABLES, "#ATTRIBUTES.name#")>
<cfset ATTRIBUTES.submittedValue=CALLER.VARIABLES["#ATTRIBUTES.name#"]/>
<cfelseif structKeyExists(FORM, "#ATTRIBUTES.name#")>
<cfset ATTRIBUTES.submittedValue=FORM["#ATTRIBUTES.name#"]/>
<cfelseif structKeyExists(URL, "#ATTRIBUTES.name#")>
<cfset ATTRIBUTES.submittedValue=URL["#ATTRIBUTES.name#"]/>
<cfelse>
<cfset ATTRIBUTES.submittedValue=#ATTRIBUTES.default#/>
</cfif>
<cfif structKeyExists(ATTRIBUTES,"preprocessor")>
<cfset preprocessedValue=#ATTRIBUTES.preprocessor(ATTRIBUTES.submittedValue)#/>
<cfelse>
<cfset preprocessedValue=#ATTRIBUTES.submittedValue#/>
</cfif>
<cfswitch expression=#ATTRIBUTES.type#>
<cfcase value="integer">
<cfif isValid('integer', preprocessedValue)>
<cfset ATTRIBUTES.value=preprocessedValue/>
<cfelseif isValid('number', preprocessedValue)>
<cfset ATTRIBUTES.value=round(preprocessedValue)/>
<cfelse>
<cfset ATTRIBUTES.value=#ATTRIBUTES.default#/>
</cfif>
</cfcase>
<cfcase value="numeric">
<cfif isValid('numeric', preprocessedValue)>
<cfset ATTRIBUTES.value=preprocessedValue/>
<cfelse>
<cfset ATTRIBUTES.value=#ATTRIBUTES.default#/>
</cfif>
</cfcase>
<cfcase value="datetime">
<cfparam name="ATTRIBUTES.locale" default="ru"/>
<cfparam name="ATTRIBUTES.format" default=""/>
<cftry>
<cfset ATTRIBUTES.value=LSParseDateTime(preprocessedValue, #ATTRIBUTES.locale#, #ATTRIBUTES.format#)/>
<cfcatch type="ANY">
<cfset ATTRIBUTES.value=#ATTRIBUTES.default#/>
</cfcatch>
</cftry>
</cfcase>
<cfcase value="string">
<cfset ATTRIBUTES.value=preprocessedValue/>
</cfcase>
</cfswitch>
</cfif>
@@ -0,0 +1,33 @@
<cfcomponent extends="form_page_info" displayname="information container for detail page" output="true">
<cfproperty name="formName" type="string"/>
<cfproperty name="formAction" type="string"/>
<cfproperty name="formMethod" type="string"/>
<cfproperty name="formEncType" type="string"/>
<cffunction name="init">
<cfargument name="entity" type="string" required="true"/>
<cfargument name="key" type="string" default="#arguments.entity#_id"/>
<cfargument name="track" type="struct" required="true"/>
<cfargument name="permission" type="number" required="true"/>
<cfargument name="formName" type="string" default=""/>
<cfargument name="formAction" type="string" default=""/>
<cfargument name="formMethod" type="string" default=""/>
<cfargument name="formEncType" type="string" default=""/>
<cfset super.init(arguments.entity, arguments.key, arguments.track, arguments.formName, arguments.formAction, arguments.formMethod,arguments.formEncType)/>
<cfset this.permission=arguments.permission/>
<cfset this.formName=arguments.formName/>
<cfset this.formAction=arguments.formAction/>
<cfset this.formMethod=arguments.formMethod/>
<cfset this.formEncType=arguments.formEncType/>
<cfreturn this>
</cffunction>
<cffunction name="getType" returntype="string">
<cfreturn "detail"/>
</cffunction>
</cfcomponent>
@@ -0,0 +1,59 @@
<cfsilent><!--- dispatch --->
<cfimport prefix="m" taglib="../lib"/>
<cfimport prefix="c" taglib="../lib/controls"/>
<cfparam name="ATTRIBUTES.usePRG" type="boolean" default="Yes"/>
<cfparam name="ATTRIBUTES.pageInfo" type="struct"/>
<cfparam name="ATTRIBUTES.status" type="struct"/>
<cfif structKeyExists(ATTRIBUTES,"id")>
<cfset id=#ATTRIBUTES.id#/>
<cfelse>
<cfparam name="ATTRIBUTES.data" type="struct"/>
<cfset id=#ATTRIBUTES.data[ATTRIBUTES.pageInfo.entity &"_id"]#/>
</cfif>
<cfparam name="ATTRIBUTES.defaultBackUrl" type="string" default="#ATTRIBUTES.pageInfo.entity#_ls.cfm"/>
<cfparam name="ATTRIBUTES.queryString" type="string" default="#ATTRIBUTES.pageInfo.key#=#id#"/><!---works only for entities with simple integer primary key--->
<cfparam name="ATTRIBUTES.trackOut" type="string" default=""/>
<cfparam name="ATTRIBUTES.idAttributesOut" type="string" default=""/><!---works only for entities with simple integer primary key--->
<m:track
thisURL="#request.thisPage#?#ATTRIBUTES.queryString#"
defaultBackUrl="#ATTRIBUTES.defaultBackUrl#"
output="tr"
/>
<!--- route --->
<cfif ATTRIBUTES.usePRG AND !ATTRIBUTES.status.errorState>
<!--- PRG pattern--->
<cfif structKeyExists(CALLER, "save")>
<m:location url="#tr.selfUrl#"/>
</cfif>
</cfif>
<cfif structKeyExists(CALLER, "saveAndClose") AND !ATTRIBUTES.status.errorState>
<m:location url="#tr.backUrl#"/>
</cfif>
<cfif structKeyExists(CALLER, "saveAndNew") AND !ATTRIBUTES.status.errorState>
<m:location url="#tr.selfUrl#&doClone"/><!--- должен содержать Query String, чтобы указать ключ, иначе смысла нет--->
</cfif>
<!---/route --->
<cfset ATTRIBUTES.pageInfo.track=#tr#/>
<cfset ATTRIBUTES.pageInfo.status=#ATTRIBUTES.status#/><!---Передаем статус в pageInfo--->
<cfif len(ATTRIBUTES.trackOut)>
<cfset "CALLER.#ATTRIBUTES.trackOut#"=#tr#/>
</cfif>
<cfif len(ATTRIBUTES.idAttributesOut)>
<cfset "CALLER.#ATTRIBUTES.idAttributesOut#.cfsqltype"="cf_sql_integer"/>
<cfset "CALLER.#ATTRIBUTES.idAttributesOut#.value"=#id#/>
<cfset "CALLER.#ATTRIBUTES.idAttributesOut#.null"=#!isNumeric(id)#/>
</cfif>
<cfexit method="exittag"/>
</cfsilent>
@@ -0,0 +1,283 @@
component {
public any function init() {
variables.utcBaseDate = createObject( 'java', 'java.util.Date' ).init( javacast( 'int', 0 ) );
variables.ECParameterSpecCache = { };
}
function convertDateToUnixTimestamp( required date dateToConvert ) {
return dateDiff( 's', utcBaseDate, parseDateTime( dateToConvert ) );
}
function convertUnixTimestampToDate( required numeric timestamp ) {
return dateAdd( 's', timestamp, utcBaseDate );
}
function base64UrlToBinary( base64url ) {
var base64 = base64url.replace( '-', '+', 'all' ).replace( '_', '/', 'all' );
var padded = base64 & repeatString( '=', 4 - ( len( base64 ) % 4 ) );
return binaryDecode( padded, 'base64' );
}
function binaryToBase64Url( source ) {
return binaryEncode( source, 'base64' )
.replace( '+', '-', 'all' )
.replace( '/', '_', 'all' )
.replace( '=', '', 'all' );
}
/**
* The INTEGER encoding for DER consists of a 02 tag a length encoding of the value
* and then a signed, minimum sized, big endian encoding of the encoded number.
*
* - https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature
*/
function derEncodeIntegerBytes( byteArray ) {
// first remove any padding
for ( var i = 1; i <= arrayLen( byteArray ); i++ ) {
if ( byteArray[ i ] != 0 ) break;
}
var unpadded = arraySlice( byteArray, i );
// add sign if negative
if ( unpadded[ 1 ] < 0 ) {
unpadded.prepend( 0 );
}
// if len > 127 the length encoding will be wrong, but that won't happen for the supported signature sizes
var derEncoded = [ 2, unpadded.len() ];
derEncoded.append( unpadded, true );
return derEncoded;
}
/**
* The SEQUENCE encoding is simply a tag set to the byte value 30, the
* length encoding and then the concatenation of the two INTEGER
* structures.
*
* https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature
*
* Also see:
* https://crypto.stackexchange.com/questions/57731/ecdsa-signature-rs-to-asn1-der-encoding-question
*/
function convertP1363ToDER( signature ) {
var split = len( signature ) / 2;
var r = derEncodeIntegerBytes( arraySlice( signature, 1, split ) );
var s = derEncodeIntegerBytes( arraySlice( signature, split + 1, split ) );
var DERSignature = [ 48 ];
var length = r.len() + s.len();
if ( length > 255 ) {
throw(
type = 'jwtcfml.InvalidSignature',
message = 'Invalid P1363 key.',
detail = 'The P1363 signature is too long.'
);
}
/*
The length is simply a single byte if it is smaller than 128 (or hex 80)
of the size. If it is larger then it is two byte: one byte set to 81,
which indicates that one length byte will follow, and one byte
containing the actual value.
https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature
*/
if ( length > 127 ) {
DERSignature.append( -127 );
length -= 256;
}
DERSignature.append( length );
DERSignature.append( r, true );
DERSignature.append( s, true );
return javacast( 'byte[]', DERSignature );
}
function convertDERtoP1363( required any signature, required string algorithm ) {
// extract the two integers from the DER signature
// assuming a 02 tag byte followed by a single length byte since we should not see
// anything larger in the supported algorithms
var start = 3;
while ( signature[ start ] != 2 ) start++;
var r = arraySlice( signature, start + 2, signature[ start + 1 ] );
var s = arraySlice( signature, start + 2 + r.len() + 2 );
if ( r[ 1 ] == 0 ) r = arraySlice( r, 2 );
if ( s[ 1 ] == 0 ) s = arraySlice( s, 2 );
var lengthMap = {
ES256: 32,
ES384: 48,
ES512: 64
};
var P1363Signature = [ ];
for ( var i = 1; i <= lengthMap[ algorithm ] - r.len(); i++ ) P1363Signature.append( 0 );
P1363Signature.append( r, true );
for ( var i = 1; i <= lengthMap[ algorithm ] - s.len(); i++ ) P1363Signature.append( 0 );
P1363Signature.append( s, true );
return javacast( 'byte[]', P1363Signature );
}
function parsePEMEncodedKey( required string pemKey ) {
if ( reFind( '^-----BEGIN (RSA|EC) (PARAMETERS|PRIVATE)', pemKey ) ) {
throw(
type = 'jwtcfml.InvalidPrivateKey',
message = 'Invalid private key format.',
detail = 'Please encode your private key in PKCS8 format, e.g.: `openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pk8'
)
}
var binaryKey = binaryDecode(
trim( pemKey ).reReplace( '-----[A-Z\s]+-----', '', 'all' ).reReplace( '[\r\n]', '', 'all' ),
'base64'
);
if ( find( '-----BEGIN CERTIFICATE-----', pemKey ) ) {
var bis = createObject( 'java', 'java.io.ByteArrayInputStream' ).init( binaryKey );
return createObject( 'java', 'java.security.cert.CertificateFactory' )
.getInstance( 'X.509' )
.generateCertificate( bis )
.getPublicKey();
}
if ( find( '-----BEGIN PUBLIC KEY-----', pemKey ) ) {
var publicKeySpec = createObject( 'java', 'java.security.spec.X509EncodedKeySpec' ).init( binaryKey );
try {
return createObject( 'java', 'java.security.KeyFactory' )
.getInstance( 'RSA' )
.generatePublic( publicKeySpec );
} catch ( any e ) {
}
try {
return createObject( 'java', 'java.security.KeyFactory' )
.getInstance( 'EC' )
.generatePublic( publicKeySpec );
} catch ( any e ) {
}
}
if ( find( '-----BEGIN PRIVATE KEY-----', pemKey ) ) {
var privateKeySpec = createObject( 'java', 'java.security.spec.PKCS8EncodedKeySpec' ).init( binaryKey );
try {
return createObject( 'java', 'java.security.KeyFactory' )
.getInstance( 'RSA' )
.generatePrivate( privateKeySpec );
} catch ( any e ) {
}
try {
return createObject( 'java', 'java.security.KeyFactory' )
.getInstance( 'EC' )
.generatePrivate( privateKeySpec );
} catch ( any e ) {
}
}
throw(
type = 'jwtcfml.InvalidPEMKey',
message = 'Invalid PEM key.',
detail = 'Please ensure you are using an RSA or EC public or private key or certificate.'
)
}
function parseJWK( required struct jwk ) {
if ( jwk.kty == 'RSA' ) {
if ( jwk.keyExists( 'd' ) ) {
try {
var bigInts = bigIntegers( jwk, [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi' ] );
var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateCrtKeySpec' ).init(
bigInts.n,
bigInts.e,
bigInts.d,
bigInts.p,
bigInts.q,
bigInts.dp,
bigInts.dq,
bigInts.qi
);
var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' );
return kf.generatePrivate( keySpec );
} catch ( any e ) {
}
try {
var bigInts = bigIntegers( jwk, [ 'n', 'd' ] );
var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateKeySpec' ).init(
bigInts.n,
bigInts.d
);
var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' );
return kf.generatePrivate( keySpec );
} catch ( any e ) {
}
} else {
try {
var bigInts = bigIntegers( jwk, [ 'n', 'e' ] );
var ks = createObject( 'java', 'java.security.spec.RSAPublicKeySpec' ).init( bigInts.n, bigInts.e );
var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' );
return kf.generatePublic( ks );
} catch ( any e ) {
}
}
}
if ( jwk.kty == 'EC' ) {
var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'EC' );
var ECParameterSpec = getECParameterSpec( jwk.crv );
if ( jwk.keyExists( 'd' ) ) {
var bigInts = bigIntegers( jwk, [ 'd' ] );
var ks = createObject( 'java', 'java.security.spec.ECPrivateKeySpec' ).init(
bigInts.d,
ECParameterSpec
);
return kf.generatePrivate( ks );
} else {
var bigInts = bigIntegers( jwk, [ 'x', 'y' ] );
var ECPoint = createObject( 'java', 'java.security.spec.ECPoint' ).init( bigInts.x, bigInts.y );
var ks = createObject( 'java', 'java.security.spec.ECPublicKeySpec' ).init( ECPoint, ECParameterSpec );
return kf.generatePublic( ks );
}
}
throw(
type = 'jwtcfml.InvalidJWK',
message = 'Invalid JWK key.',
detail = 'Please ensure you are using an valid JWK RSA or EC public or private key.'
)
}
private function bigIntegers( jwk, keys ) {
var bigInts = { };
for ( var key in keys ) {
bigInts[ key ] = createObject( 'java', 'java.math.BigInteger' ).init( 1, base64UrlToBinary( jwk[ key ] ) );
}
return bigInts;
}
private function getECParameterSpec( crv ) {
if ( !variables.ECParameterSpecCache.keyExists( crv ) ) {
var kpg = createObject( 'java', 'java.security.KeyPairGenerator' ).getInstance( 'EC' );
var ecgp = createObject( 'java', 'java.security.spec.ECGenParameterSpec' ).init(
'secp#crv.listLast( '-' )#r1'
);
kpg.initialize( ecgp );
variables.ECParameterSpecCache[ crv ] = kpg
.generateKeyPair()
.getPublic()
.getParams();
}
return variables.ECParameterSpecCache[ crv ];
}
}
+131
View File
@@ -0,0 +1,131 @@
<cfsilent>
<!--- build query filter string --->
<!--- пустые и некорректные значения параметров вызвыают пропуск параметра --->
<cfparam name="ATTRIBUTES.filter" type="struct"/>
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfset queryFilter="">
<cfloop collection=#ATTRIBUTES.filter# item="item">
<cftry>
<cfset fltr=structFind(ATTRIBUTES.filter,item)>
<!--- checks and preformatting --->
<cfset skip=false><!--- *** неуклюжая имитация continue --->
<cfswitch expression=#fltr.ftype#>
<cfcase value="numeric">
<cfif NOT isNumeric(fltr.val)><cfset skip=true></cfif>
<cfset val=fltr.val>
</cfcase>
<cfcase value="date">
<cfif NOT isDate(fltr.val)><cfset skip=true></cfif>
<cfset val=fltr.val>
</cfcase>
<cfcase value="string">
<!--- special case --->
<!--- escape single quotes for security--->
<cfset val=replace(fltr.val,"'","''", "ALL")>
<!--- elclose in single quotes --->
<cfif NOT fltr.compare EQ "LIKE%"><!---*** криво --->
<cfset val="'#val#'">
</cfif>
</cfcase>
<cfcase value="list"><!--- *** для простоты листы разных типов не различаются, что не есть хорошо, повышается риск инъекциии --->
<!--- данные списка хранятся в виде массива, чтобы исключить проблемы с запятыми--->
<cfif arrayLen(fltr.val) EQ 0>
<cfset skip=true>
<cfelse>
<cfset isNumericList=true>
<!--- test: maybe list is of numbers--->
<cfloop array=#fltr.val# index="item">
<cfif NOT isNumeric(#item#)>
<cfset isNumericList=false/>
<cfbreak/>
</cfif>
</cfloop>
<!---<cfoutput>#isNumericList#</cfoutput>--->
<cfset lst="">
<cfif isNumericList>
<cfloop array=#fltr.val# index="item">
<cfset lst=listAppend(lst, item)><!---<cfoutput>**#item#</cfoutput> --->
</cfloop>
<cfelse>
<cfloop array=#fltr.val# index="item"><!--- escape single quotes to prevent SQL injection --->
<cfset cleanItem=replace(item, "'", "''", "ALL")>
<cfset lst=listAppend(lst, "'#cleanItem#'")><cfoutput>#item#</cfoutput>
</cfloop>
</cfif>
<cfset val=lst>
<!---<cfoutput>#val#</cfoutput>--->
</cfif>
</cfcase>
<cfdefaultcase>
<cfset skip=true>
</cfdefaultcase>
</cfswitch>
<!--- query building --->
<!--- потенциально опасно --->
<!--- *** протестировать на sql injection --->
<cfif NOT skip>
<cfif structKeyExists(fltr,"expression") AND len(fltr.expression)>
<cfset cmpExpr=replace(fltr.expression,"?","#val#","ALL")/>
<cfelse>
<cfswitch expression=#fltr.compare#>
<cfcase value="EQ">
<cfset cmpExpr="#fltr.field#=#val#">
</cfcase>
<cfcase value="NE,NEQ">
<cfset cmpExpr="NOT #fltr.field#=#val#">
</cfcase>
<cfcase value="LE">
<cfset cmpExpr="#fltr.field#<=#val#">
</cfcase>
<cfcase value="LT">
<cfset cmpExpr="#fltr.field#<#val#">
</cfcase>
<cfcase value="GE">
<cfset cmpExpr="#fltr.field#>=#val#">
</cfcase>
<cfcase value="GT">
<cfset cmpExpr="#fltr.field#>#val#">
</cfcase>
<cfcase value="IN">
<cfset cmpExpr="#fltr.field# IN (#val#)">
</cfcase>
<cfcase value="LIKE">
<cfset cmpExpr="#fltr.field# LIKE #val#">
</cfcase>
<cfcase value="LIKE%">
<cfset cmpExpr="#fltr.field# LIKE '%#val#%'">
</cfcase>
<cfdefaultcase>
<cfset skip=true>
</cfdefaultcase>
</cfswitch>
</cfif>
</cfif>
<cfif NOT skip>
<cfset queryFilter="#queryFilter# AND #cmpExpr#">
</cfif>
<cfcatch type="ANY"></cfcatch>
</cftry>
</cfloop>
<cfif isDefined("ATTRIBUTES.output")>
<cfset "CALLER.#ATTRIBUTES.output#"=preserveSingleQuotes(queryFilter)/>
<cfelse>
<cfset thisTag.generatedContent=preserveSingleQuotes(queryFilter)/>
</cfif>
</cfif>
</cfsilent>
@@ -0,0 +1,50 @@
<!--- version 2.01---><!---15:25 31.01.2019--->
<!--- version 2.02---><!---2025-08-04 15:19:34 postgre specific ilike instead of like--->
<!--- build query filter string --->
<!--- пустые и некорректные значения параметров вызвыают пропуск параметра --->
<cfparam name="ATTRIBUTES.filter" type="struct"/>
<cfloop collection=#ATTRIBUTES.filter# item="item">
<cfsilent>
<cfset fltr=structFind(ATTRIBUTES.filter,item)>
<cfparam name="fltr.val" default=""/>
<cfparam name="fltr.prefix" default=""/>
<cfparam name="fltr.suffix" default=""/>
<cfparam name="fltr.list" default="No"/>
</cfsilent> <!---
---> AND <cfif structKeyExists(fltr,"expression") AND len(fltr.expression)><!---
---><cfset len=listLen(#fltr.expression#,"?")><!---
---><cfloop from=1 to=#len# index="i"><!---
---><cfoutput>#listGetAt(fltr.expression,i,"?")#</cfoutput><!---
---><cfif i LT #len#><cfqueryparam cfsqltype="#getCfSqLType(fltr.ftype)#" list="#fltr.list#" value="#fltr.prefix##fltr.val##fltr.suffix#"/></cfif><!---
---></cfloop><!---
---><cfelse><!---
---><cfoutput>#fltr.field#</cfoutput><cfswitch expression=#fltr.compare#><cfcase value="EQ"> = </cfcase><cfcase value="NE,NEQ"> <> </cfcase><cfcase value="LE"> <= </cfcase><cfcase value="LT"> < </cfcase><cfcase value="GE"> >= </cfcase><cfcase value="GT"> > </cfcase><cfcase value="LIKE"> ILIKE </cfcase><cfcase value="NOTLIKE"> NOT ILIKE </cfcase><cfcase value="LIKE%"> ILIKE <cfqueryparam cfsqltype=#getCfSqLType(fltr.ftype)# value="%#fltr.val#%"/><cfcontinue/></cfcase><cfcase value="NOTLIKE%"> NOT ILIKE <cfqueryparam cfsqltype=#getCfSqLType(fltr.ftype)# value="%#fltr.val#%"/><cfcontinue/></cfcase><cfcase value="IN"> IN (<cfqueryparam cfsqltype=#getCfSqLType(fltr.ftype)# list="#fltr.list#" value="#fltr.val#"/>)<cfcontinue/></cfcase><cfdefaultcase> = <cfoutput> #fltr.field#</cfoutput><cfcontinue/><!---***криво---></cfdefaultcase></cfswitch><cfqueryparam cfsqltype=#getCfSqLType(fltr.ftype)# value="#fltr.val#"/><cfcontinue/>
</cfif>
</cfloop>
<cfexit method="exittag"/>
<cffunction name="getCfSqlType" output="No">
<cfargument name="fieldType" default=""/>
<cfswitch expression=#ARGUMENTS.fieldType#>
<cfcase value="numeric">
<cfreturn "CF_SQL_NUMERIC"/>
</cfcase>
<cfcase value="date,time,datetime">
<cfreturn "CF_SQL_TIMESTAMP"/>
</cfcase>
<cfcase value="guid">
<cfreturn "CF_SQL_OTHER"/>
</cfcase>
<cfcase value="string">
<cfreturn "CF_SQL_VARCHAR"/>
</cfcase>
<cfdefaultcase>
<cfreturn "CF_SQL_VARCHAR"/>
</cfdefaultcase>
</cfswitch>
</cffunction>
@@ -0,0 +1,9 @@
<cfcomponent extends="form_page_info" displayname="information container for form page" output="false">
<cfproperty name="settings" type="struct"/>
<cffunction name="getType" returntype="string">
<cfreturn "filter"/>
</cffunction>
</cfcomponent>
@@ -0,0 +1,26 @@
<cfsilent></cfsilent>
<!--- version 2.01--->
<cfimport prefix="m" taglib="."/>
<cfparam name="ATTRIBUTES.target" type="string"/>
<cfparam name="ATTRIBUTES.scopeName" type="string" default="CALLER"/>
<cfparam name="ATTRIBUTES.structName" type="string" default="filter"/>
<cfparam name="ATTRIBUTES.markerList" type="string" default="applyFilter,saveAndClose"/>
<cfif thisTag.executionMode IS "start">
<cfset key="#ATTRIBUTES.target#.filter"/>
<cfif isDefined("reset") or isDefined("resetAndClose")>
<m:settings action="clear" key="#key#"/>
</cfif>
<m:settings action="get" key="#key#" output="filter"/>
<cfset "#ATTRIBUTES.scopeName#.#ATTRIBUTES.structName#"=#filter#/>
<cfelse>
<!--- in fact, for any action --->
<cfloop list=#ATTRIBUTES.markerList# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<m:settings action="sync" key="#key#" obj=#CALLER.filter#>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
+180
View File
@@ -0,0 +1,180 @@
<!--- version 2.01---><!---15:25 31.01.2019--->
<!--- 2017.06.16 исправлен баг с очисткой при числовом значении по умолчанию --->
<!---v0.5 10:02 02.01.2019--->
<cfassociate basetag="cf_filter_settings"/>
<cfparam name="ATTRIBUTES.filter" type="struct" default=#getBaseTagData("CF_FILTER_SETTINGS",1).filter#/><!--- *** possible Railo bug, getBaseTagData works only with second parameter --->
<cfparam name="ATTRIBUTES.param" type="string"/>
<cfparam name="ATTRIBUTES.default" type="string" default=""/><!--- список: значения, установленные по умолчанию. Обычно состоит из 1 значения даже для списка. В случае кодирования base64 этот список НЕ кодируется (проблемы с запятыми остаются на совести разработчика)--->
<!---*** И вообще Base64 нельзя использовать непосредственно, он не URLsafe --->
<cfparam name="ATTRIBUTES.ftype" type="string" default="string"/>
<cfparam name="ATTRIBUTES.list" type="boolean" default="No"/>
<cfparam name="ATTRIBUTES.compare" type="string" default="EQ"/>
<cfparam name="ATTRIBUTES.field" type="string" default=""/>
<cfparam name="ATTRIBUTES.base64" type="boolean" default="No"/><!--- означает, что параметр нужно раскодировать из base64---><!---*** Переделать на URL-safe base64--->
<!---
CFQUERYPARAM CFSQLTYPE="CF_SQL_VARCHAR" ставит апострофы вокруг значения параметра.
Поэтому приходится любые wildcards засовывать в значение bind variable. Запрос должен иметь вид
SELECT A,B FROM C WHERE A LIKE ?
(без всяких апострофов), а значение параметра будет, например,
"%#submitted_string#%"
--->
<cfparam name="ATTRIBUTES.prefix" type="string" default=""/>
<cfparam name="ATTRIBUTES.suffix" type="string" default=""/>
<cfparam name="ATTRIBUTES.expression" type="string" default=""/>
<!---
Хранение данных для списков
переделано обратно с массива на лист.
При этом случилась забавная, но неприятная и трудноуловимая ошибка
arrayAppend(array,item) механически заменил на listAppend(list,item)
а надо было на list=listAppend(list,item)
--->
<!---<cfif ATTRIBUTES.list>
<cfset def = "">
<cfloop list=#ATTRIBUTES.default# index="item">
<cfset listAppend(def, item)>Список значений по умолчанию НЕ должен быть кодирован в base64 при ATTRIBUTES.base64
</cfloop>
<cfelse>
</cfif>--->
<cfset def=ATTRIBUTES.default/>
<!---<cfoutput>#ATTRIBUTES.param#:</cfoutput>--->
<!---<cfif evaluate("isDefined('CALLER.#ATTRIBUTES.param#')")>
<cfset evaluate("val=(CALLER.#ATTRIBUTES.param#)")>--->
<cfif structKeyExists(CALLER, '#ATTRIBUTES.param#')>
<cfset val=structFind(CALLER, '#ATTRIBUTES.param#')/>
<!--- parse --->
<cfset to=1/>
<cfset values = ""/>
<cfif ATTRIBUTES.list>
<cfset to=listLen(val)/>
</cfif>
<cfloop from="1" to=#to# index="i">
<cfif ATTRIBUTES.list>
<cfset element=listGetAt(val,i)/>
<cfelse>
<cfset element=val/>
</cfif>
<cfswitch expression=#ATTRIBUTES.ftype#>
<cfcase value="numeric">
<cfset element=trim(element)>
<cfif NOT isNumeric(element)>
<cfset element=def/>
</cfif>
</cfcase>
<cfcase value="guid">
<cfset element=trim(element)>
<cfif NOT isValid('guid',element)>
<cfset element=def/>
</cfif>
</cfcase>
<cfcase value="date">
<cftry>
<cfset element=CreateDateTime(ListGetAt(element,3,'.'), ListGetAt(element,2,'.'), ListGetAt(element,1,'.'),0,0,0)>
<cfcatch type="Any">
<!--- <cfoutput>#cfcatch.message# #cfcatch.detail#</cfoutput> --->
</cfcatch>
</cftry>
</cfcase>
<cfcase value="string">
<cfif ATTRIBUTES.base64>
<cfset element=ToString(BinaryDecode(element , "base64"))>
</cfif>
<cfset element=safeStr(element)/>
</cfcase>
<cfdefaultcase>
<cfthrow message="Filterparam: unsupported ftype value '#ATTRIBUTES.ftype#'" detail="Supported values are 'numeric', 'date', 'string'">
</cfdefaultcase>
</cfswitch>
<cfif ATTRIBUTES.list>
<cfset values=listAppend(values, element)/>
<cfelse>
<cfset val=element/>
<cfbreak/>
</cfif>
</cfloop>
<cfif ATTRIBUTES.list>
<cfset val=values/><!---*** все это выглядит ужасно. Переиспользование переменных вперед-назад, IF вокруг листа--->
</cfif>
<!---/parse --->
<cfelseif structKeyExists(ATTRIBUTES.filter, ATTRIBUTES.param)><!--- если параметр есть в сохраненном фильтре, взять оттуда --->
<cfset filterItem = structFind(ATTRIBUTES.filter, ATTRIBUTES.param)>
<cfif isStruct(filterItem)>
<cfset val=filterItem.val>
<cfelse><!---not struct <cfoutput>#ATTRIBUTES.param#</cfoutput>--->
<cfset val=def>
</cfif>
<cfelse>
<cfset val=def>
</cfif>
<cfset setVariable("CALLER.#ATTRIBUTES.param#", "")/>
<cftry>
<cfset setVariable("CALLER.#ATTRIBUTES.param#",#val#)/>
<cfcatch type="Any"></cfcatch>
</cftry>
<cfif shouldRemoveValue(val, def)>
<cfset structDelete(ATTRIBUTES.filter, ATTRIBUTES.param)/>
<cfelse>
<cfset fltItem=structNew()>
<cfset fltItem.ftype=ATTRIBUTES.ftype>
<cfset fltItem.list=ATTRIBUTES.list>
<cfset fltItem.field=ATTRIBUTES.field>
<cfset fltItem.compare=ATTRIBUTES.compare>
<cfset fltItem.val=val>
<cfset fltItem.prefix=ATTRIBUTES.prefix/>
<cfset fltItem.suffix=ATTRIBUTES.suffix/>
<cfset fltItem.expression=ATTRIBUTES.expression/>
<cfset structInsert(ATTRIBUTES.filter,ATTRIBUTES.param,fltItem, "TRUE")>
</cfif>
<cfexit method="exittag"/>
<cffunction name="shouldRemoveValue" returnType="boolean">
<cfargument name="val"/>
<cfargument name="def"/>
<!---<cfdump var=#val#> <cfdump var=#def#>--->
<cfscript>
if (!IsSimpleValue(val)){
if (isArray(val)) {
return (arrayLen(val) == 0);
}
return false;
}
return ((val==def) OR (val==""));
</cfscript>
</cffunction>
<cffunction name="safeStr" returnType="string">
<cfargument name="val" type="string"/>
<cfreturn replaceList(val, "',"",<,>", ",,,")/>
</cffunction>
@@ -0,0 +1,31 @@
<cfcomponent extends="generic_page_info" displayname="information container for form page" output="true">
<cfproperty name="formName" type="string"/>
<cfproperty name="formAction" type="string"/>
<cfproperty name="formMethod" type="string"/>
<cfproperty name="formEncType" type="string"/>
<cffunction name="init">
<cfargument name="entity" type="string" required="true"/>
<cfargument name="key" type="string" default="#arguments.entity#_id"/><!--- *** некрасиво как выполнен проброс --->
<cfargument name="track" type="struct" required="true"/>
<cfargument name="formName" type="string" default=""/>
<cfargument name="formAction" type="string" default=""/>
<cfargument name="formMethod" type="string" default=""/>
<cfargument name="formEncType" type="string" default=""/>
<cfset super.init(arguments.entity, arguments.key, arguments.track)/>
<cfset this.formName=arguments.formName/>
<cfset this.formAction=arguments.formAction/>
<cfset this.formMethod=arguments.formMethod/>
<cfset this.formEncType=arguments.formEncType/>
<cfreturn this>
</cffunction>
<cffunction name="getType" returntype="string">
<cfreturn "form"/>
</cffunction>
</cfcomponent>
@@ -0,0 +1,39 @@
<cfcomponent displayname="Generic Page Info" output="true">
<cfproperty name="entity" type="string"/>
<cfproperty name="key" type="string"/>
<cfproperty name="status" type="struct"/>
<cfproperty name="track" type="struct"/>
<cfproperty name="permission" type="integer"/>
<cfset this.PERMISSION_NONE=0/><!---duplicated in request scope--->
<cfset this.PERMISSION_READ=1/><!---duplicated in request scope--->
<cfset this.PERMISSION_WRITE=2/><!---duplicated in request scope--->
<cffunction name="init" access="public">
<cfargument name="entity" type="string" required="true"/>
<cfargument name="key" type="string" default="#arguments.entity#_id"/>
<cfargument name="track" type="struct" required="true"/>
<cfset this.entity=arguments.entity/>
<cfset this.key=arguments.key/>
<cfset this.track=arguments.track/>
<cfset this.permission=this.PERMISSION_NONE/>
<cfset this.status.errorState=false/>
<cfset this.status.errorMessage=""/>
<cfreturn this>
</cffunction>
<cffunction name="readPermitted" returntype="boolean">
<cfreturn (this.permission GE this.PERMISSION_READ)/>
</cffunction>
<cffunction name="writePermitted" returntype="boolean">
<cfreturn (this.permission GE this.PERMISSION_WRITE)/>
</cffunction>
<cffunction name="getType" returntype="string">
<cfreturn "generic"/>
</cffunction>
</cfcomponent>
@@ -0,0 +1,25 @@
<cfparam name="ATTRIBUTES.container" type="struct">
<cfparam name="ATTRIBUTES.objName" type="string">
<cfif NOT structKeyExists(ATTRIBUTES.container, ATTRIBUTES.objName)>
<cfset structInsert(ATTRIBUTES.container,ATTRIBUTES.objName,structNew())>
</cfif>
<cfset obj=structFind(ATTRIBUTES.container,ATTRIBUTES.objName)>
<!---
Раздельная инициализация позволяет "чинить" поврежденный объект,
но
*** при этом факт повреждения не обнаруживается
--->
<cfif NOT structKeyExists(obj, "sort")>
<cfset structInsert(obj,"sort", structNew())>
</cfif>
<cfif NOT structKeyExists(obj, "filter")>
<cfset structInsert(obj,"filter", structNew())>
</cfif>
<cfexit method="EXITTAG" />
+262
View File
@@ -0,0 +1,262 @@
component {
variables.algorithmMap = {
HS256: 'HmacSHA256',
HS384: 'HmacSHA384',
HS512: 'HmacSHA512',
RS256: 'SHA256withRSA',
RS384: 'SHA384withRSA',
RS512: 'SHA512withRSA',
ES256: 'SHA256withECDSA',
ES384: 'SHA384withECDSA',
ES512: 'SHA512withECDSA'
};
public any function init() {
variables.encodingUtils = new encodingUtils();
variables.jss = createObject( 'java', 'java.security.Signature' );
variables.messageDigest = createObject( 'java', 'java.security.MessageDigest' );
return this;
}
public string function encode(
required struct payload,
required any key,
required string algorithm,
struct headers = { }
) {
if ( !algorithmMap.keyExists( algorithm ) ) {
throw(
type = 'jwtcfml.InvalidAlgorithm',
message = 'Invalid JWT Algorithm.',
detail = 'The passed in algorithm is not supported.'
);
}
var header = { };
header.append( headers );
header.append( {
'typ': 'JWT',
'alg': algorithm
} );
var duplicatedPayload = duplicate( payload );
for ( var claim in [ 'iat', 'exp', 'nbf' ] ) {
if ( duplicatedPayload.keyExists( claim ) && isDate( duplicatedPayload[ claim ] ) ) {
duplicatedPayload[ claim ] = encodingUtils.convertDateToUnixTimestamp( duplicatedPayload[ claim ] );
}
}
var stringToSignParts = [
encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( header ), 'utf-8' ) ),
encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( duplicatedPayload ), 'utf-8' ) )
];
var stringToSign = stringToSignParts.toList( '.' );
return stringToSign & '.' & encodingUtils.binaryToBase64Url( sign( stringToSign, key, algorithm ) );
}
public struct function decode(
required string token,
any key,
any algorithms = [ ],
struct claims = { },
boolean verify = true
) {
var parts = listToArray( token, '.' );
if ( arrayLen( parts ) != 3 ) {
throw(
type = 'jwtcfml.InvalidToken',
message = 'Invalid JWT.',
detail = 'The passed in token does not have three `.` delimited parts.'
);
}
algorithms = isArray( algorithms ) ? algorithms : [ algorithms ];
var decoded = {
header: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 1 ] ), 'utf-8' ) ),
payload: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 2 ] ), 'utf-8' ) )
};
if ( verify ) {
if (
!algorithms.find( decoded.header.alg ) ||
!algorithmMap.keyExists( decoded.header.alg )
) {
throw(
type = 'jwtcfml.InvalidAlgorithm',
message = 'Unsupported or invalid algorithm',
detail = 'The passed in token does not have an algorithm declaration or its declared algorithm (#decoded.header.alg#) does not match the specified algorithms of #serializeJSON( algorithms )#.'
);
}
var stringToSign = parts[ 1 ] & '.' & parts[ 2 ];
var signature = encodingUtils.base64UrlToBinary( parts[ 3 ] );
if (
!verifySignature(
stringToSign,
key,
signature,
decoded.header.alg
)
) {
throw(
type = 'jwtcfml.InvalidSignature',
message = 'Signature is Invalid',
detail = 'The signature of the passed in token is invalid.'
);
}
var baseClaims = {
'exp': true,
'nbf': true
};
baseClaims.append( claims );
verifyClaims( decoded.payload, baseClaims );
}
for ( var claim in [ 'iat', 'exp', 'nbf' ] ) {
if ( decoded.payload.keyExists( claim ) ) {
decoded.payload[ claim ] = encodingUtils.convertUnixTimestampToDate( decoded.payload[ claim ] );
}
}
return decoded.payload;
}
public struct function getHeader( required string token ) {
return deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( listFirst( token, '.' ) ), 'utf-8' ) );
}
public function parsePEMEncodedKey( required string pemKey ) {
return encodingUtils.parsePEMEncodedKey( pemKey );
}
public function parseJWK( required struct jwk ) {
return encodingUtils.parseJWK( jwk );
}
private function sign( message, key, algorithm ) {
if ( left( algorithm, 1 ) == 'H' ) {
var sig = binaryDecode(
hmac(
message,
key,
algorithmMap[ algorithm ],
'utf-8'
),
'hex'
);
} else {
if ( isSimpleValue( key ) ) {
key = encodingUtils.parsePEMEncodedKey( key );
} else if ( isStruct( key ) ) {
key = encodingUtils.parseJWK( key );
}
var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] );
jssInstance.initSign( key );
jssInstance.update( charsetDecode( message, 'utf-8' ) );
var sig = jssInstance.sign();
if ( left( algorithm, 1 ) == 'E' ) {
sig = encodingUtils.convertDERtoP1363( sig, algorithm );
}
}
return sig;
}
private function verifySignature( message, key, signature, algorithm ) {
if ( left( algorithm, 1 ) == 'H' ) {
var sig = binaryDecode(
hmac(
message,
key,
algorithmMap[ algorithm ],
'utf-8'
),
'hex'
);
return MessageDigest.isEqual( signature, sig );
}
if ( left( algorithm, 1 ) == 'E' ) {
signature = encodingUtils.convertP1363ToDER( signature );
}
if ( isSimpleValue( key ) ) {
key = encodingUtils.parsePEMEncodedKey( key );
} else if ( isStruct( key ) ) {
key = encodingUtils.parseJWK( key );
}
var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] );
jssInstance.initVerify( key );
jssInstance.update( charsetDecode( message, 'utf-8' ) );
return jssInstance.verify( signature );
}
private function verifyClaims( payload, claims ) {
if (
structKeyExists( payload, 'exp' )
&& !verifyDateClaim( payload.exp, claims.exp, -1 )
) {
throw(
type = 'jwtcfml.ExpiredSignature',
message = 'Token has expired',
detail = 'The passed in token has expired.'
);
}
if (
structKeyExists( payload, 'nbf' )
&& !verifyDateClaim( payload.nbf, claims.nbf, 1 )
) {
throw(
type = 'jwtcfml.NotBeforeException',
message = 'Token is not valid',
detail = 'The passed in token has not yet become valid.'
);
}
if ( structKeyExists( claims, 'iss' ) ) {
if ( !structKeyExists( payload, 'iss' ) || compare( payload.iss, claims.iss ) != 0 ) {
throw(
type = 'jwtcfml.InvalidIssuer',
message = 'Token has an invalid issuer',
detail = 'The passed in token either does not specify an issuer or the claimed issuer is not valid.'
);
}
}
if ( structKeyExists( claims, 'aud' ) ) {
var audArray = isArray( claims.aud ) ? claims.aud : [ claims.aud ];
if ( !structKeyExists( payload, 'aud' ) || !audArray.find( payload.aud ) ) {
throw(
type = 'jwtcfml.InvalidAudience',
message = 'Token has an invalid audience',
detail = 'The passed in token either does not specify an audience or the claimed audience is not valid.'
);
}
}
}
private function verifyDateClaim( payloadDate, claim, failState ) {
var pd = encodingUtils.convertUnixTimestampToDate( payloadDate );
var cd = claim;
if ( !isBoolean( cd ) || cd ) {
if ( isNumeric( cd ) ) {
cd = encodingUtils.convertUnixTimestampToDate( cd );
} else if ( !isDate( cd ) ) {
cd = now();
}
return dateCompare( pd, cd ) != failState;
}
return true;
}
}
+5
View File
@@ -0,0 +1,5 @@
<!--- used instead cflocation which, at least for railo, makes cyrillics in url corrupt --->
<cfparam name="ATTRIBUTES.url" type="string"/>
<cfheader statuscode="302" statustext="Moved Temporarily"/>
<cfheader charset="#GetEncoding('URL')#" name="Location" value="#ATTRIBUTES.url#"/>
<cfabort/>
+24
View File
@@ -0,0 +1,24 @@
<cfsilent>
<cfparam name="ATTRIBUTES.catch"/>
<cfparam name="ATTRIBUTES.output" default=""/><!--- deprecated, use instead ATTRIBUTES.status --->
<cfsavecontent variable="message">
<cfoutput>
Ошибка при обращении к базе данных. Возможно, программа некорректно сформировала условия фильтра или сортировки. В качестве временной меры попробуйте <a href="#request.thisPage#?reset_filter=yes&sort=reset">сбросить условия фильтра и сортировки</a>. Просьба сообщить об ошибке администратору.<br/>
#ATTRIBUTES.catch.message#<br/>
#ATTRIBUTES.catch.detail#<br/>
</cfoutput>
</cfsavecontent>
<!--- deprecated behavior --->
<cfif len(ATTRIBUTES.output)>
<cfset "CALLER.#ATTRIBUTES.output#"=#message#/>
</cfif>
<!--- modern behavior --->
<cfif structKeyExists(ATTRIBUTES,"status")>
<cfset ATTRIBUTES.status.errorState="Yes"/>
<cfset ATTRIBUTES.status.errorMessage="#message#"/>
</cfif>
</cfsilent>
@@ -0,0 +1,10 @@
<cfcomponent extends="generic_page_info" displayname="information container for page with form (detail, filter)" output="false">
<cfproperty name="recordsPerPage" type="integer"/>
<cfproperty name="nStart" type="integer"/>
<cfproperty name="settings" type="struct"/>
<cffunction name="getType" returntype="string">
<cfreturn "ls"/>
</cffunction>
</cfcomponent>
+24
View File
@@ -0,0 +1,24 @@
<cfprocessingdirective suppressWhitespace="YES"><cfsilent>
<cfparam name="ATTRIBUTES.page" type="string"/>
<cfparam name="ATTRIBUTES.queryString" type="string" default=""/>
<cfparam name="ATTRIBUTES.acl" type="string" default=""/>
<cfparam name="ATTRIBUTES.label" type="string" default=""/>
<cfif structKeyExists(#ATTRIBUTES#,"external")>
<!--- <cfset class="menu-item-ext"/>--->
<cfset target=' target="_blank"'/>
<cfelse>
<!--- <cfset class="menu-item"/>--->
<cfset target=''/>
</cfif>
<cfif len(ATTRIBUTES.queryString)>
<cfset pageUrl="#ATTRIBUTES.page#?#ATTRIBUTES.queryString#"/>
<cfelse>
<cfset pageUrl="#ATTRIBUTES.page#"/>
</cfif>
</cfsilent><!---
---><cfmodule template="ac.cfm" obj="#ATTRIBUTES.acl#"><cfoutput><li class="nav-item"><a class="nav-link<cfif (request.thisPage EQ ATTRIBUTES.page)> active</cfif>" href="#pageUrl#"#target#>#ATTRIBUTES.label#</a></li></cfoutput><!---
---></cfmodule></cfprocessingdirective><cfexit method="EXITTAG"/>
@@ -0,0 +1,11 @@
<cfparam name="ATTRIBUTES.mimetype"/>
<cfswitch expression=#ATTRIBUTES.mimetype#>
<cfcase value="text/plain"><img src="img/mimetype/text.png"/></cfcase>
<cfcase value="text/html"><img src="img/mimetype/html.png"/></cfcase>
<cfcase value="application/pdf"><img src="img/mimetype/pdf.png"/></cfcase>
<cfcase value="image/gif,image/png,image/jpeg"><img src="img/mimetype/image.png"/></cfcase>
<cfcase value="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"><img src="img/mimetype/excel.png"/></cfcase>
<cfcase value="application/vnd.ms-powerpoint"><img src="img/mimetype/powerpoint.png"/></cfcase>
<cfcase value="application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"><img src="img/mimetype/word.png"/></cfcase>
<cfdefaultcase><img src="img/mimetype/document.png"/></cfdefaultcase>
</cfswitch><cfexit method="exittag"/>
+193
View File
@@ -0,0 +1,193 @@
/**
* @displayname oauth2
* @output false
* @hint The oauth2 object.
* @author Matt Gifford
* @website https://www.monkehworks.com
* @purpose A ColdFusion Component to manage authentication using the OAuth2 protocol.
**/
component accessors="true"{
property name="client_id" type="string";
property name="client_secret" type="string";
property name="authEndpoint" type="string";
property name="accessTokenEndpoint" type="string";
property name="redirect_uri" type="string";
property name="PKCE" type="struct";
/**
* I return an initialized oauth2 object instance.
* @client_id The client ID for your application.
* @client_secret The client secret for your application.
* @authEndpoint The URL endpoint that handles the authorisation.
* @accessTokenEndpoint The URL endpoint that handles retrieving the access token.
* @redirect_uri The URL to redirect the user back to following authentication.
**/
public oauth2 function init(
required string client_id,
required string client_secret,
required string authEndpoint,
required string accessTokenEndpoint,
required string redirect_uri
){
setClient_id( arguments.client_id );
setClient_secret( arguments.client_secret );
setAuthEndpoint( arguments.authEndpoint );
setAccessTokenEndpoint( arguments.accessTokenEndpoint );
setRedirect_uri( arguments.redirect_uri );
return this;
}
/**
* I return the URL as a string which we use to redirect the user for authentication. |
* The method will handle the client_id and redirect_uri values for you. Anything else you need to send to the provider in the URL can be sent via the parameters argument.
* @parameters A structure containing key / value pairs of data to be included in the URL string.
**/
public string function buildRedirectToAuthURL( struct parameters={} ) {
var objAuthBuilder = new utils.authStringBuilder(
authEndpoint = getAuthEndpoint(),
client_id = getClient_id(),
redirect_uri = getRedirect_uri()
);
return objAuthBuilder.withParams( arguments.parameters ).get();
}
/**
* I return an instance of the authStringBuilder class. The actual link string is generated by then calling the .get() method on the class object.
* The method will handle the client_id and redirect_uri values for you. Anything else you need to send to the provider in the URL can be sent via the parameters argument.
* @parameters A structure containing key / value pairs of data to be included in the URL string.
**/
public authStringBuilder function buildRedirectToAuthURLWithBuilder( struct parameters={} ) {
var objAuthBuilder = new utils.authStringBuilder(
authEndpoint = getAuthEndpoint(),
client_id = getClient_id(),
redirect_uri = getRedirect_uri()
);
return objAuthBuilder.withParams( arguments.parameters );
}
/**
* I make the HTTP request to obtain the access token.
* @code The code returned from the authentication request.
* @formfields An optional array of structs for the provider requirements to add new form fields.
* @headers An optional array of structs to add custom headers to the request if required.
**/
public struct function makeAccessTokenRequest(
required string code,
array formfields = [],
array headers = []
){
var stuResponse = {};
var httpService = new http();
httpService.setMethod( "post" );
httpService.setCharset( "utf-8" );
httpService.setUrl( getAccessTokenEndpoint() );
if( arrayLen( arguments.headers ) ){
for( var item in arguments.headers ){
httpService.addParam( type="header", name=item[ 'name' ], value=item[ 'value' ] );
}
}
httpService.addParam( type="formfield", name="client_id", value=getClient_id() );
httpService.addParam( type="formfield", name="client_secret", value=getClient_secret() );
httpService.addParam( type="formfield", name="code", value=arguments.code );
httpService.addParam( type="formfield", name="redirect_uri", value=getRedirect_uri() );
httpService.addParam( type="formfield", name="grant_type", value='authorization_code' );
if( arrayLen( arguments.formfields ) ){
for( var item in arguments.formfields ){
httpService.addParam( type="formfield", name=item[ 'name' ], value=item[ 'value' ] );
}
}
var result = httpService.send().getPrefix();
if( '200' == result.ResponseHeader[ 'Status_Code' ] ) {
stuResponse.success = true;
stuResponse.content = result.FileContent;
} else {
stuResponse.success = false;
stuResponse.content = result.Statuscode;
}
//dump(var=formfields,label="form fields");
return stuResponse;
}
/**
* I make the HTTP request to refresh the access token.
* @refresh_token The refresh_token returned from the accessTokenRequest request.
**/
public struct function refreshAccessTokenRequest(
required string refresh_token,
array formfields = [],
array headers = []
){
var stuResponse = {};
var httpService = new http();
httpService.setMethod( "post" );
httpService.setCharset( "utf-8" );
httpService.setUrl( getAccessTokenEndpoint() );
httpService.addParam( type="header", name="Content-Type", value="application/x-www-form-urlencoded" );
if( arrayLen( arguments.headers ) ){
for( var item in arguments.headers ){
httpService.addParam( type="header", name=item[ 'name' ], value=item[ 'value' ] );
}
}
httpService.addParam( type="formfield", name="client_id", value=getClient_id() );
httpService.addParam( type="formfield", name="client_secret", value=getClient_secret() );
httpService.addParam( type="formfield", name="refresh_token", value=arguments.refresh_token );
httpService.addParam( type="formfield", name="grant_type", value="refresh_token" );
if( arrayLen( arguments.formfields ) ){
for( var item in arguments.formfields ){
httpService.addParam( type="formfield", name=item[ 'name' ], value=item[ 'value' ] );
}
}
var result = httpService.send().getPrefix();
if( '200' == result.ResponseHeader[ 'Status_Code' ] ) {
stuResponse.success = true;
stuResponse.content = result.FileContent;
} else {
stuResponse.success = false;
stuResponse.content = result.Statuscode;
}
return stuResponse;
}
/**
* I return a string containing any extra URL parameters to concatenate and pass through when authenticating.
* @argScope A structure containing key / value pairs of data to be included in the URL string.
**/
public string function buildParamString( struct argScope={} ) {
var strURLParam = '?';
if( structCount( arguments.argScope ) ) {
var intCount = 1;
for( var key in arguments.argScope ) {
if( listLen( strURLParam ) && intCount > 1 ) {
strURLParam = strURLParam & '&';
}
strURLParam = strURLParam & lcase( key ) & '=' & trim( arguments.argScope[ key ] );
intCount++;
}
}
return strURLParam;
}
/**
* Returns the properties as a struct
*/
public struct function getMemento(){
var result = {};
for( var thisProp in getMetaData( this ).properties ){
if( structKeyExists( variables, thisProp[ 'name' ] ) ){
result[ thisProp[ 'name' ] ] = variables[ thisProp[ 'name' ] ];
}
}
return result;
}
/**
* Generates a struct containing the code verifier and code challenge
* values for use with the PKCE extension.
*/
public struct function generatePKCE(){
var objPKCE = new utils.deps.pkce.pkce();
return objPKCE.generatePKCE();
}
}
@@ -0,0 +1,44 @@
<cfsilent><!--- build query sort string --->
<!--- v0.3 ---><!--- 2025-04-29 09:13:19 --->
<cfparam name="ATTRIBUTES.sortArray" type="array"/>
<!--- <cfparam name="ATTRIBUTES.defaultSortArray" type="array" default=#[]#/> оказывается, это уже было --->
<cfparam name="ATTRIBUTES.fieldCount" type="integer" default=0/>
<cfparam name="ATTRIBUTES.defaultSortArray" type="array" default=#[]#/><!--- #[{"fld":1,"asc":0}]# --->
<cfparam name="ATTRIBUTES.defaultOrder" default="1 desc"/><!--- оба варианта не отображаются в реестре, чтобы отображались - надо работать с массивом вне этого модуля, модуль не модифицирует массив сортировки. В общем, оба варианта дефолтной сортировки не вызывают восторга, но пусть остаются - в конце концов, есть не только реестры --->
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
<cfif ArrayLen(ATTRIBUTES.sortArray) EQ 0>
<cfset ATTRIBUTES.sortArray=ATTRIBUTES.defaultSortArray/>
</cfif>
<cfset querySort="">
<cfset i=1>
<cfloop array=#ATTRIBUTES.sortArray# index="sort"><!--- index is not an ordinal, but the element of array--->
<cftry>
<cfif (ATTRIBUTES.fieldCount LE 0) OR (sort.fld LE ATTRIBUTES.fieldCount)>
<cfif sort.asc>
<cfset querySort=listAppend(querySort," #sort.fld# asc")>
<cfelse>
<cfset querySort=listAppend(querySort," #sort.fld# desc")>
</cfif>
</cfif>
<cfcatch type="Any"></cfcatch>
</cftry>
<cfset i=i+1>
</cfloop>
<cfif len(trim(querySort)) EQ 0>
<cfset querySort=#ATTRIBUTES.defaultOrder#>
</cfif>
<cfset querySort=trim(querySort)>
<cfif isDefined("ATTRIBUTES.output")>
<cfset "CALLER.#ATTRIBUTES.output#"=querySort/>
<cfelse>
<cfset thisTag.generatedContent=querySort/>
</cfif>
</cfif>
</cfsilent>
@@ -0,0 +1,26 @@
<cfsilent></cfsilent><!--- prepare environment ---><!--- v02 17:35 28.06.2018---><!---14:55 29.06.2018 bug fixed--->
<cfimport prefix="m" taglib="../lib"/>
<cfparam name="ATTRIBUTES.entity" type="string"/>
<cfparam name="ATTRIBUTES.accessObject" type="string" default=#ATTRIBUTES.entity#/>
<cfparam name="ATTRIBUTES.key" type="string" default="#ATTRIBUTES.entity#_id"/>
<cfparam name="ATTRIBUTES.pageInfoOut" type="string"/>
<cfparam name="ATTRIBUTES.formEncType" type="string" default=""/>
<cfparam name="ATTRIBUTES.trackOut" type="string" default="tr"/>
<cfparam name="ATTRIBUTES.thisUrl" default=""/>
<cfif len(ATTRIBUTES.thisUrl) EQ 0><!--- наугад используем типовое соглашение--->
<cfparam name="#ATTRIBUTES.key#" default="-1"/>
<cfset id=evaluate("#ATTRIBUTES.key#")/><!---*** неизящно, зато безопасно---><!--- *** как-то не слишком безопасно --->
<cfset ATTRIBUTES.thisUrl="#request.thisPage#?#ATTRIBUTES.key#=#id#"/>
</cfif>
<m:ac obj="#ATTRIBUTES.accessObject#" permissionOut="permission"/>
<m:track thisUrl="#ATTRIBUTES.thisUrl#" defaultBackUrl="#ATTRIBUTES.entity#_ls.cfm" output="track"/>
<cfset pageInfo=CreateObject("component","detail_page_info").init("#ATTRIBUTES.entity#","#ATTRIBUTES.key#",#track#,"#permission#","frm",#request.thisPage#,"POST","#ATTRIBUTES.formEncType#")/>
<cfset "CALLER.#ATTRIBUTES.trackOut#"=#track#/>
<cfset "CALLER.#ATTRIBUTES.pageInfoOut#"=#pageInfo#/>
<cfexit method="exittag"/>
@@ -0,0 +1,26 @@
<cfsilent><!--- prepare environment --->
<cfimport prefix="m" taglib="../lib"/>
<cfparam name="ATTRIBUTES.entity" type="string"/>
<cfparam name="ATTRIBUTES.accessObject" type="string" default=#ATTRIBUTES.entity#/>
<cfparam name="ATTRIBUTES.key" type="string" default="#ATTRIBUTES.entity#_id"/>
<cfparam name="ATTRIBUTES.pageInfoOut" type="string"/>
<cfparam name="ATTRIBUTES.trackOut" type="string"/>
<cfparam name="ATTRIBUTES.lsPage" type="string" default="#ATTRIBUTES.entity#_ls"/>
<m:track thisUrl="#request.thisPage#" defaultBackUrl="#ATTRIBUTES.lsPage#.cfm" output="tr"/>
<cfset pageInfo=CreateObject("component","filter_page_info").init(#ATTRIBUTES.entity#,"#ATTRIBUTES.key#",#tr#,"frm",#request.thisPage#,"POST","")/>
<m:settings key="#ATTRIBUTES.lsPage#" output="settings" action="get"/>
<cfset pageInfo.settings=#settings#/>
<m:ac obj="#ATTRIBUTES.accessObject#" act="#request.PERMISSION_READ#" output="readPermitted"/>
<cfset pageInfo.readPermitted=#readPermitted#/>
<cfset "CALLER.#ATTRIBUTES.trackOut#"=#tr#/>
<cfset "CALLER.#ATTRIBUTES.pageInfoOut#"=#pageInfo#/>
<cfexit method="exittag"/>
</cfsilent>
@@ -0,0 +1,16 @@
<cfsilent><!--- prepare environment --->
<cfimport prefix="m" taglib="../lib"/>
<cfparam name="ATTRIBUTES.pageInfoOut" type="string"/>
<cfparam name="ATTRIBUTES.trackOut" type="string"/>
<cfparam name="ATTRIBUTES.action" type="string" default="#request.thisPage#"/><!--- Если подстановка шаблона делается в application.cfc, то request.thisPage будет соответствовать URL, а не шаблону --->
<m:track thisUrl="#request.thisPage#" defaultBackUrl="." output="tr"/>
<cfset pageInfo=CreateObject("component","form_page_info").init("","",#tr#,"frm",#ATTRIBUTES.action#,"POST","")/>
<cfset "CALLER.#ATTRIBUTES.trackOut#"=#tr#/>
<cfset "CALLER.#ATTRIBUTES.pageInfoOut#"=#pageInfo#/>
<cfexit method="exittag"/>
</cfsilent>
+43
View File
@@ -0,0 +1,43 @@
<cfsilent><!--- prepare environment --->
<!---v0.2 accessObject--->
<cfimport prefix="m" taglib="../lib"/></cfsilent><m:silent silent="No">
<cfparam name="ATTRIBUTES.entity" type="string"/>
<cfparam name="ATTRIBUTES.accessObject" type="string" default=#ATTRIBUTES.entity#/>
<cfparam name="ATTRIBUTES.key" type="string" default="#ATTRIBUTES.entity#_id"/>
<cfparam name="ATTRIBUTES.settingsKey" type="string" default="#ATTRIBUTES.entity#_ls"/>
<cfparam name="ATTRIBUTES.pageInfoOut" type="string"/>
<cfparam name="ATTRIBUTES.trackOut" type="string"/>
<cfparam name="ATTRIBUTES.recordsPerPage" type="integer" default="500"/>
<cfparam name="nStart" default="1"/>
<cfif nStart EQ 1>
<cfset thisUrl="#request.thisPage#"/>
<cfelse>
<cfset thisUrl="#request.thisPage#?nStart=#nStart#"/>
</cfif>
<m:track thisUrl="#thisUrl#" defaultBackUrl="." output="tr"/>
<cfset pageInfo=CreateObject("component","ls_page_info").init("#ATTRIBUTES.entity#","#ATTRIBUTES.key#",#tr#)/>
<cfset pageInfo.recordsPerPage=#ATTRIBUTES.recordsPerPage#/>
<cfset pageInfo.nStart=#nStart#/>
<m:ac obj="#ATTRIBUTES.accessObject#" permissionOut="permission"/>
<cfset pageInfo.permission=#permission#/>
<cfif isDefined("CALLER.reset_filter")> <!---*** --->
<m:settings action="clear" key="#ATTRIBUTES.settingsKey#.filter"/>
</cfif>
<m:settings key="#ATTRIBUTES.settingsKey#" output="settings"><!---*** грязно: мы работаем с сортировкой, а синхронизируем весь контейнер и получаем его побочным эффектом для дальнейшего использования --->
<m:sortparam sort=#settings.sort# sortlength=2/><!---*** вот где устанавливается длина массива сортировки--->
</m:settings>
<cfset pageInfo.settings=settings/><!--- unlike m:track, output="CALLER.settings" wouldnt't work for m:settings because of some StructFind --->
<cfset "CALLER.#ATTRIBUTES.trackOut#"=#tr#/>
<cfset "CALLER.#ATTRIBUTES.pageInfoOut#"=#pageInfo#/>
<cfexit method="exittag"/>
</m:silent>
@@ -0,0 +1,396 @@
<cfcomponent
displayname="ReST API helper"
output="true"
hint="Static helper methods for ReST API">
<!--- v0.02 2024-10-15 --->
<!--- v0.03 2024-11-27 --->
<cffunction name="empty2null"
access="public"
returntype="any"
output="false"
hint="replace empty field with null">
<cfargument name="value" type="any" required="true" />
<cfif isEmpty(ARGUMENTS.value)><!---*** некорректно, но просто, пустые строки превращает в нулл--->
<cfreturn javacast('null','')/>
<cfelse>
<cfreturn ARGUMENTS.value />
</cfif>
</cffunction>
<cffunction name="passThrough"
returntype="any"
output="false"
hint="just return argument">
<cfargument name="x" type="ANY" required="true"/>
<cfreturn #ARGUMENTS.x#/>
</cffunction>
<cffunction name="appendRecord"
access="public"
returntype="any"
output="false"
hint="put data into struct, replace empty fields with null">
<cfargument name="struct" type="struct" required="true" />
<cfargument name="key" type="string" required="true" />
<cfargument name="fieldMap" type="struct" required="true" />
<cfargument name="query" type="query" required="true" />
<cfargument name="fieldNameDecorator" type="function" required="false" default=#passThrough#/>
<cfif len(ARGUMENTS.key)>
<cfset ARGUMENTS.struct[ARGUMENTS.key]=structNew()/>
<cfset var container=ARGUMENTS.struct[ARGUMENTS.key]/>
<cfelse>
<cfset var container=ARGUMENTS.struct/>
</cfif>
<cfloop list=#structKeyList(ARGUMENTS.fieldMap)# index="local.col">
<cfset var obj=container/>
<cfset var formatter = structFind(ARGUMENTS.fieldMap[local.col], "formatter")/>
<cfif structKeyExists(ARGUMENTS.fieldMap[#local.col#],"container")>
<cfset var subContainerName = structFind(ARGUMENTS.fieldMap[local.col], "container")/>
<cfif len(subContainerName)>
<cfif !structKeyExists(container, subContainerName)>
<cfset structInsert(container, subContainerName, structNew())/>
</cfif>
<cfset obj=container[subContainerName]/>
</cfif>
</cfif>
<cfif queryColumnExists(ARGUMENTS.query, local.col)>
<cfset obj[ARGUMENTS.fieldNameDecorator(local.col)]=empty2null(formatter(ARGUMENTS.query[local.col][ARGUMENTS.query.currentRow]))/>
</cfif>
<!---*** не уверен в надежности конструкции во всех реализациях CF, контекст query передастся ли--->
</cfloop>
<cfreturn ARGUMENTS.struct/>
</cffunction>
<cffunction name="parseFilterParams"
returntype="struct"
output="true"
hint="parse and collect filter params, for operators see filter_build">
<!---e.g. duration=NEQ:2--->
<!---*** add test for multiple values--->
<cfargument name="params" type="struct" required="true" />
<cfset var out=structNew()/>
<cfloop collection=#params# index="local.item">
<cfset var urlParamName=snake2camel(lCase(local.item))/>
<cfif structKeyExists(URL,urlParamName)>
<cfset var rawValue=URL[urlParamName]/>
<cfset var operator="EQ"/>
<cfset var value=#rawValue#/>
<cfif listLen(rawValue,":") GT 1>
<cfset var operator=listGetAt(rawValue,1,":")/>
<cfset var value=listGetAt(rawValue,2,":")/>
</cfif>
<cfif !isValid(ARGUMENTS.params[local.item].type,value)>
<cfthrow type="invalidParamValue" message="Filter parameter #urlParamName# value is not a valid #ARGUMENTS.params[local.item].type#"/><!---не выводим значение, чтобы исключить XSS--->
</cfif>
<cfif structKeyExists(ARGUMENTS.params[local.item], "prefix") AND len(ARGUMENTS.params[local.item].prefix)>
<cfset var fieldName="#ARGUMENTS.params[local.item].prefix#.#local.item#"/>
<cfelse>
<cfset var fieldName=#local.item#/>
</cfif>
<cfset var rec={field=#fieldName#, val=#value#, ftype=#ARGUMENTS.params[local.item].type#, compare=#operator#}/><!---*** add list--->
<cfset structInsert(out, local.item, rec, true)/>
</cfif>
</cfloop>
<cfreturn out/>
</cffunction>
<cffunction name="parseOrderBy"
returntype="struct"
output="false"
hint="parse and collect sort order param"><!--- deprecated --->
<cfargument name="params" type="struct" required="true" />
<cfargument name="orderBy" type="string" required="true" />
<cfset var out={}/>
<cfloop list=#ARGUMENTS.orderBy# index="local.item">
<cfif listLen(local.item,".") GT 1>
<cfset var fld=camel2snake(listGetAt(local.item,1,"."))/>
<cfswitch expression=#uCase(listGetAt(local.item,2,"."))#>
<cfcase value="ASC"><cfset var asc=true/></cfcase>
<cfcase value="DESC"><cfset var asc=false/></cfcase>
<cfdefaultcase>
<cfthrow type="InvalidParamValue" message="Invalid orderBy format" detail="orderBy suffix should be '.asc' or '.desc'" /><!---*** XSS vulnerability, so we would not show the suffix--->
</cfdefaultcase>
</cfswitch>
<cfelse>
<cfset var fld=camel2snake(local.item)/>
<cfset var asc=true/>
</cfif>
<cfif structKeyExists(ARGUMENTS.params, fld)>
<cfset structInsert(out,"#ARGUMENTS.params[fld].prefix#.#fld#", {fld="#ARGUMENTS.params[fld].prefix#.#fld#",asc=#asc#}, true)/>
<cfelse>
<cfthrow type="InvalidParamValue" message="Invalid orderBy field"/>
</cfif>
</cfloop>
<cfreturn out/>
</cffunction>
<cffunction name="parseNumericOrder"
returntype="array"
output="false"
hint="parse and collect sort order param with numeric notation">
<cfargument name="fieldSet" type="struct" required="true" />
<cfargument name="orderBy" type="string" required="true" />
<cfset var out=[]/>
<cfloop list=#ARGUMENTS.orderBy# index="local.item">
<cfif listLen(local.item,".") GT 1>
<cfset var fld=camel2snake(listGetAt(local.item,1,"."))/>
<cfswitch expression=#uCase(listGetAt(local.item,2,"."))#>
<cfcase value="ASC"><cfset var asc=true/></cfcase>
<cfcase value="DESC"><cfset var asc=false/></cfcase>
<cfdefaultcase>
<cfthrow type="InvalidParamValue" message="Invalid orderBy format" detail="orderBy suffix should be '.asc' or '.desc'" /><!---*** XSS vulnerability, so we would not show the suffix--->
</cfdefaultcase>
</cfswitch>
<cfelse>
<cfset var fld=camel2snake(local.item)/>
<cfset var asc=true/>
</cfif>
<cfif structKeyExists(ARGUMENTS.fieldSet, fld)>
<cfset arrayAppend(out,{fld="#ARGUMENTS.fieldSet[fld].ordinal#",asc=#asc#})/>
<cfelse>
<cfthrow type="InvalidParamValue" message="Invalid orderBy field"/>
</cfif>
</cfloop>
<cfreturn out/>
</cffunction>
<!---<cffunction name="snake2camel"
access="public"
returntype="any"
output="false"
hint="convert snake style name to camel style name">
<cfargument name="snake" type="string" required="true" />
<cfset var snake=ARGUMENTS.snake/>
<cfset var camel=""/>
<cfset var pos=1/>
<cfloop condition="true">
<cfset next=reFind("(_[a-z])", snake, pos, false)/>
<cfif NOT next GT 0>
<cfbreak/>
</cfif>
<cfset camel="#camel##mid(snake,pos,next-pos)##uCase(mid(snake,next+1,1))#"/>
<cfset pos=next+2/>
</cfloop>
<cfset camel="#camel##mid(snake,pos,len(snake))#"/>
<cfreturn #camel#/>
</cffunction>--->
<!---not used any more--->
<cffunction name="query4json" access="public" returntype="any" output="false"
hint="Converts query to an array of structs. Key names are lowercase. Empty fields are treated as nulls">
<cfargument name="Query" type="query" required="true" />
<cfset var out=arrayNew(1)/>
<cfset var i=0/>
<cfloop query=ARGUMENTS.Query>
<cfset i=i+1/>
<cfset out[i]=structNew()/>
<cfloop list=#ARGUMENTS.Query.ColumnList# index="local.col">
<cfset var value=ARGUMENTS.Query[local.col][currentRow]/>
<cfif isEmpty(value)><!---*** некорректно, но просто, пустые строки превращает в нулл--->
<cfset out[i][local.col]=javacast('null','')/>
<cfelse>
<cfset out[i][local.col]=ARGUMENTS.Query[local.col][currentRow]/>
</cfif>
</cfloop>
</cfloop>
<cfreturn(out)/>
</cffunction>
<cffunction name="snake2camel"
access="public"
returntype="any"
output="false"
hint="convert snake style name to camel style name">
<cfargument name="snake" type="string" required="true" />
<cfreturn #reReplace(ARGUMENTS.snake,"_([a-z])","\u\1","ALL")#/>
</cffunction>
<cffunction name="camel2snake"
access="public"
returntype="any"
output="false"
hint="convert camel style name to snake style name">
<cfargument name="snake" type="string" required="true" />
<cfreturn #reReplace(ARGUMENTS.snake,"([A-Z])","_\l\1","ALL")#/>
</cffunction>
<cffunction name="formatMessage"
access="public"
returntype="any"
output="true"
hint="formats Exception for display">
<cfargument name="message" type="string" required="true" />
<cfargument name="title" type="string" required="false" />
<cfset var title=structKeyExists(ARGUMENTS,"title") ? #ARGUMENTS.title# : #ARGUMENTS.message# />
<cfreturn {type="about:blank", title="#title#", detail="#ARGUMENTS.message#"}/>
</cffunction>
<cffunction name="formatException"
access="public"
returntype="any"
output="true"
hint="formats Exception for display">
<cfargument name="ex" type="struct" required="true" />
<cfargument name="title" type="string" required="false" />
<cfset var detail = listAppend(#ARGUMENTS.ex.message#,#ARGUMENTS.ex.detail#,": " )/>
<cfset var title = structKeyExists(ARGUMENTS,"title") ? #ARGUMENTS.title# : #ARGUMENTS.ex.message# />
<cfreturn formatMessage(detail, title) />
</cffunction>
<cffunction name="formatBadRequestError"
access="public"
returntype="any"
output="true"
hint="formats Bad Request Exception">
<cfargument name="ex" type="struct" required="true" />
<cfset var detail=listAppend(#ARGUMENTS.ex.message#,#ARGUMENTS.ex.detail#,":" )/>
<cfreturn {type="about:blank", title="Bad Request", detail="#detail#"}/>
</cffunction>
<!---<cffunction name="wrapResultSet"
access="public"
returntype="any"
output="true"
hint="format resultset as json">
<cfargument name="qRead" type="query" required="true" />
<cfargument name="titleMap" type="struct" required="true" />
<cfargument name="startrow" type="integer" required="false" default="1" />
<cfset var resultSet=[]/>
<cfloop query=#ARGUMENTS.qRead# startRow=#ARGUMENTS.startrow#>
<cfset var rec={}/>
<cfset appendRecord(rec, "", ARGUMENTS.titleMap, ARGUMENTS.qRead, snake2camel)/>
<cfset arrayAppend(resultSet, rec)/>
</cfloop>
<cfset var out={
pageSize=
}/>
<cfcontent type="application/json"/>
<cfoutput>{
"pageSize":#pageSize#,
"page":#page#,
"orderBy":"#orderBy#",
"size":"#arrayLen(resultSet)#",
"total":"#qTotal.cnt#",
"results":#serializeJson(resultSet)#,
"queryDurationMs":#queryDurationMs#, <cfset runDurationMs=getTickCount()-request.startTickCount/>
"runDurationMs":#runDurationMs#
}</cfoutput>--->
<cffunction name="validateField"
access="public"
returntype="any"
output="false"
hint="validate field of a structure, if exisits">
<cfargument name="struct" type="struct" required=true/>
<cfargument name="name" required=true/>
<cfargument name="type" default="string"/>
<cfargument name="required" type="boolean" default="false"/>
<cfif structKeyExists(ARGUMENTS.struct, ARGUMENTS.name)>
<cfif NOT isValid(ARGUMENTS.type, ARGUMENTS.struct[ARGUMENTS.name])>
<cfthrow type="invalidParamValue" message="Parameter validation failed" detail="Supplied value of '#ARGUMENTS.name#' is not a valid #ARGUMENTS.type#"/>
</cfif>
<cfelse>
<cfif ARGUMENTS.required>
<cfthrow type="invalidParamValue" message="Required field not supplied" detail="Required field #ARGUMENTS.name# not found"/>
</cfif>
</cfif>
</cffunction>
<!--- returns false if the key does not exist and throws exception on invalid format--->
<cffunction name="keyExistsAndValid" access="public" returntype="boolean">
<cfargument name="struct" type="struct" required=true/>
<cfargument name="key" type="string" required=true/>
<cfargument name="type" type="string" required=false/>
<cfargument name="maxLength" type="numeric" required=false hint="Checked for strings only, negative value means no limit"/>
<cfif structKeyExists(arguments.struct, arguments.key)>
<cfif structKeyExists(arguments, "type")>
<cfset var x=arguments.struct[arguments.key]/>
<cfif isValid(arguments.type, x)>
<cfif lCase(arguments.type) EQ "string">
<cfif structKeyExists(arguments, "maxLength")>
<cfif ARGUMENTS.maxLength GE 0>
<cfif len(x) GT #arguments.maxLength#>
<cfthrow type="invalidParamValue" message="Invalid parameter value (string too long)" detail="Parameter #arguments.key# length #len(x)# is greater than #arguments.maxLength#" errorcode=400/>
</cfif>
</cfif>
</cfif>
</cfif>
<cfelse>
<cfthrow type="invalidParamValue" message="Invalid parameter value" detail="Parameter #arguments.key# is not a valid #arguments.type#" errorcode=400/>
</cfif>
</cfif>
<cfreturn true/>
<cfelse>
<cfreturn false/>
</cfif>
</cffunction>
<!--- returns false if the key does not exist or has invalid format--->
<cffunction name="keyExistsAndValidGraceful" access="public" returntype="boolean">
<cfargument name="struct" type="struct" required=true/>
<cfargument name="key" type="string" required=true/>
<cfargument name="type" type="string" required=false/>
<cfargument name="maxLength" type="numeric" required=false default="-1" hint="Checked for strings only"/>
<cftry>
<cfreturn keyExistsAndValid(ARGUMENTS.struct, ARGUMENTS.key, ARGUMENTS.type, ARGUMENTS.maxLength)/>
<cfcatch type="ANY"><!---<cfrethrow/>---></cfcatch>
</cftry>
<cfreturn false/>
</cffunction>
</cfcomponent>
+213
View File
@@ -0,0 +1,213 @@
<!--- *** Refactoring required --->
<!--- v0.3
2017-08-30
Настройки вынесены в таблицу usr_settings в связи с миграцией репозитория пользователей в мастер справочники
Заодно cfqueryparam
модуль должен инкапсулировать работу с настройками
- хранение в сессии и в БД, синхронность с базой
- чтение и модификацию
- очистку(сброс)
При этом модуль ничего не должен знать о структуре самих настроек (фильтрах, сортировках и т.д.)
Жизненный цикл контейнера settings
1. Живет в сессии
2. Если в сессии нет settings - вычитывается из БД
3. Если и в БД нет - инициализируется и сразу сохраняется в БД (пустой)
4. При модификации синхронизируется с базой (пишет в нее) - нужно либо снаружи вызывать сохранение, либо инкапсулировать запись
(инкапсулировать запись - сильно усложнит код)
5. Для работы выдает КОПИЮ данных, соответственно, при сохранении должен ее синхронизировать???
Пока решили, что будем выдавать ссылку на объект в сессии, при этом нужно блок кода синхронизировать CFLOCK
При этом 2 проблемы:
- некрасивый код (лок на сессию руками, связь совершенно неявная)
- избыточная блокировка
- избыточное сохранение (можно было бы проверять на изменение)
чтобы действовать без блокировки, можно экспортировать объект в безопасный scope - переменные или реквест
Альтернативный синтаксис вызова
<m:settings key="page_name" output="settings"><!--- implicit get --->
<m:sortparam sort=#settings.sort# fieldcount=9 sortlength=2/><!--- any modification --->
</m:settings><!--- implicit sync --->
Разрешение конфликта версий при обновлении софта
Конфликт версий: код требует параметр, отсутствующий в сохраненной версии. Например, одна версия фильтра сохранила свои настройки,
а другая, более новая, пытается прочесть несуществующий ключ.
Другой вариант - несуществующее поле (убранное в очередной версии) - этот конфликт проявится только при запросе к БД.
Варианты решения
- создание параметра с дефолтными значениями при запросе (во все запросы добавить значения по умолчанию). Недостаток - возможно создание помойки
- контроль номера версии. Выглядит сложным в сопровождении
*** Возможно, это была плохая идея - разрешить доступ по длинному ключу. Когда доставали и записывали целиком объект, было меньше возможностей для ошибок
--->
<!---*** обработка вызова get-sync c закрывающим тегом --->
<cfparam name="ATTRIBUTES.action" type="string" default=""/>
<cfparam name="obj" type="struct" default=#structNew()#/>
<cfparam name="doSyncOnClose" type="boolean" default="No"/>
<cfif thisTag.executionMode IS "start">
<cfif ATTRIBUTES.action IS "">
<cfset ATTRIBUTES.action="get"/>
<cfset doSyncOnClose="Yes"/>
</cfif>
<cfelse>
<cfif doSyncOnClose>
<cfset ATTRIBUTES.action="sync"/>
<cfelse>
<cfexit method="exittag"/>
</cfif>
</cfif>
<cfset useDB="false"/><!--- --->
<cfswitch expression=#lCase(ATTRIBUTES.action)#>
<cfcase value="create"> <!--- конструктор --->
<cflock scope="SESSION" type="EXCLUSIVE" timeout="3">
<cfset session.settings = retrieveSettings()>
</cflock>
</cfcase>
<cfcase value="sync">
<!--- Синхронизировать измененный объект (с сессией и персистенсом) --->
<cfparam name="ATTRIBUTES.key" type="string">
<cfif doSyncOnClose>
<cfif thisTag.executionMode IS "end">
<cfset obj=StructFind(CALLER, "#ATTRIBUTES.output#")/>
</cfif>
<cfelse>
<cfparam name="ATTRIBUTES.obj" type="struct">
<cfset obj=#ATTRIBUTES.obj#/>
</cfif>
<!---<cfdump var=#obj#/>--->
<cflock scope="SESSION" type="EXCLUSIVE" timeout="3">
<!--- помещаем объект на свое место в структуре --->
<cfset container=#session.settings#>
<cfset cnt = listLen("#ATTRIBUTES.key#", ".")>
<cfloop index="i" from=1 to=#cnt#>
<cfset item = listGetAt(#ATTRIBUTES.key#, i, ".")>
<cfif i EQ cnt><!--- last element --->
<cfset structInsert(#container#, #item#, #obj#, "YES")>
<cfelse>
<!--- попытка перебдеть--->
<cfif NOT structKeyExists(#container#,#item#)>
<cfset structInsert(#container#,#item#,#structNew()#)>
</cfif>
<cfset container=structFind(container,item)>
</cfif>
</cfloop>
</cflock>
<!--- сохраняем настройки в персистенс --->
<cfset saveSettings(container)>
</cfcase>
<cfcase value="reset"><!--- *** надо было назвать reset_all или как-то так --->
<!--- Сбросить все пользовательские настройки --->
<cfset session.settings=structNew()>
<cfset saveSettings(session.settings)>
</cfcase>
<cfcase value="clear">
<!--- Очистить секцию пользовательских настроек (sort или filter)--->
<cfparam name="ATTRIBUTES.key" type="string">
<cfset targetFullName = "session.settings.#ATTRIBUTES.key#">
<cfset objName = listGetAt(ATTRIBUTES.key,1,".")>
<cfif isDefined(targetFullName)>
<cflock scope="SESSION" type="EXCLUSIVE" timeout="3">
<cfset evaluate("structClear(#targetFullName#)")>
<cfset obj = evaluate(#targetFullName#)>
<cfmodule template="settings.cfm" action="sync" key=#ATTRIBUTES.key# obj=#obj#><!--- синхронизация при очистке используется ---><!--- хорошо ли это? А вообще надо?--->
</cflock>
</cfif>
</cfcase>
<cfcase value="get">
<!---
Получить объект или секцию
Обязательный параметр key - имя ключа в структуре, начинающееся с имени объекта, через точку
В параметре output имя переменной, принимающей результат
--->
<cfparam name="ATTRIBUTES.key" type="string">
<cfparam name="ATTRIBUTES.output" type="string">
<cfif NOT isDefined("session.settings")><!--- *** container--->
<cfmodule template="settings.cfm" action="create"/>
</cfif>
<cfset targetFullName = "session.settings.#ATTRIBUTES.key#">
<cfset objName = listGetAt(ATTRIBUTES.key, 1, ".")>
<cfif (NOT structKeyExists(#session.settings#, objName))
OR structIsEmpty(structFind(#session.settings#, objName))>
<cfmodule template="init_settings.cfm" container=#session.settings# objName=#objName#/>
<!--- несколько неуклюже, лучше просто возвращать объект и потом класть в сессию --->
</cfif>
<cftry>
<cfset obj=evaluate(targetFullName)>
<cfcatch type="ANY">
<cfmodule template="init_settings.cfm" container=#session.settings# objName=#objName#/><!---*** грубо --->
<cfset obj=evaluate(targetFullName)>
</cfcatch>
</cftry>
<cfset setVariable("CALLER.#ATTRIBUTES.output#", #duplicate(obj)#)>
</cfcase>
<cfdefaultcase>
<cfthrow message="Unsupported action = #ATTRIBUTES.action# in settings.cfm">
</cfdefaultcase>
</cfswitch>
<!---***debug <cfoutput>#ATTRIBUTES.action# #thisTag.executionMode# #thisTag.hasendtag#</cfoutput>--->
<!--- --------------------------------------------------------------------------------- --->
<!--- private methods --->
<cffunction name="retrieveSettings" access="private">
<cfif useDB><!--- можно перегрузить методы для разных провайдеров персистенса, но пока смысла в этом не видно, код достаточно примитивен и управляем --->
<cfquery name="qGet" datasource="#request.DS#">
select settings from usr where usr_id='#request.usr_id#'
</cfquery>
<cfif len(qGet.settings) GT 0>
<cftry>
<CFWDDX ACTION='wddx2cfml' input=#qGet.settings# output='settings'/>
<cfreturn #settings#/>
<cfcatch type="Any">Err:
<cfoutput>#cfcatch.message#</cfoutput>
<cfabort/>
</cfcatch>
</cftry>
</cfif>
</cfif>
<cfreturn #structNew()#/>
</cffunction>
<cffunction name="saveSettings" access="private">
<cfargument name=settings>
<cfif NOT request.usrAuthenticated()>
<cfreturn />
</cfif>
<cfif useDB>
<cfquery name="qCheckExistence" datasource="#request.DS#">
select count(*) as cnt from usr_serrings where usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value='#request.usr_id#'/>
</cfquery>
<CFWDDX ACTION='cfml2wddx' input=#session.settings# output='strSettings'/>
<cfif qCheckExistence.cnt EQ 0>
<cfquery name="qSave" datasource="#request.DS#">
insert int usr_settings (usr_id, settings)
values(<cfqueryparam cfsqltype="cf_sql_integer" value='#request.usr_id#'/>, <cfqueryparam cfsqltype="cf_sql_varchar" value='#strSettings#'/>)
</cfquery>
<cfelse>
<cfquery name="qSave" datasource="#request.DS#">
update usr_settings
set settings=<cfqueryparam cfsqltype="cf_sql_varchar" value='#strSettings#'/>
where usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value='#request.usr_id#'/>
</cfquery>
</cfif>
<cfelse>
<!--- do nothing --->
</cfif>
</cffunction>
+9
View File
@@ -0,0 +1,9 @@
<cfsilent>
<!--- Управляемая имитация cfsilent (с возможностью отключения для отладки) --->
<cfif thisTag.executionMode IS "end">
<cfparam name="ATTRIBUTES.silent" type="boolean" default="Yes"/>
<cfif ATTRIBUTES.silent>
<cfset thisTag.generatedContent=""/>
</cfif>
</cfif>
</cfsilent>
+76
View File
@@ -0,0 +1,76 @@
<cfsilent>
<cfparam name="ATTRIBUTES.sort" type="struct">
<cfparam name="ATTRIBUTES.fieldcount" type="numeric" default="0">
<cfparam name="ATTRIBUTES.sortlength" type="numeric" default="1">
<cfif NOT isDefined("ATTRIBUTES.sort.sortArray")>
<cfset structInsert(ATTRIBUTES.sort,"sortArray",arrayNew(1),"TRUE")>
</cfif>
<cfset sortArray=ATTRIBUTES.sort.sortArray>
<cfif evaluate("isDefined('CALLER.sort')")>
<cfset evaluate("val=(CALLER.sort)")>
<!--- parse --->
<cfif val EQ "reset">
<cfset ATTRIBUTES.sort.sortArray=arrayNew(1)>
<cfexit method="exittag"/>
</cfif>
<cfset pos=find("a",val)>
<cfif pos LE 0>
<cfset pos=find("d",val)>
</cfif>
<cfif pos LE 1>
<cfexit>
<cfelseif pos GT 1>
<cfset fldNum=left(val,pos-1)>
<cfset fldAsc=mid(val,pos,1)>
<cfelse>
<cfset fldNum=val>
<cfset fldAsc="a">
</cfif>
<!---/parse --->
<!--- validate --->
<cfif NOT isNumeric(fldNum)>
<cfexit>
</cfif>
<cfif (ATTRIBUTES.fieldcount GT 0) AND (fldNum GT ATTRIBUTES.fieldcount)>
<cfexit>
</cfif>
<!---/validate --->
<cfelse>
<cfexit method="exittag"/>
</cfif>
<cfset sortItem=structNew()>
<cfset structInsert(sortItem,"fld",fldNum)>
<cfset structInsert(sortItem,"asc",(fldAsc EQ "a"))>
<!--- drop all occurences of this field --->
<cfset i=1>
<cfloop condition="i LE arrayLen(sortArray)">
<cftry>
<cfif sortArray[i].fld EQ sortItem.fld>
<cfset arrayDeleteAt(sortArray,i)>
</cfif>
<cfcatch type="Any">
<cfset arrayDeleteAt(sortArray,i)>
</cfcatch>
</cftry>
<cfset i=i+1>
</cfloop>
<!--- insert item --->
<cfset arrayPrepend(sortArray, sortItem)>
<!--- truncate array--->
<cfloop condition="arrayLen(sortArray) GT ATTRIBUTES.sortlength">
<cfset arrayDeleteAt(sortArray,arrayLen(sortArray))>
</cfloop>
<!--- save --->
<cfset ATTRIBUTES.sort.sortArray=sortArray>
<!---/save --->
<cfexit method="exittag"/>
</cfsilent>
+103
View File
@@ -0,0 +1,103 @@
<cfsilent><!--- утилита для передачи стека вызовов через URL --->
<!---v1.01 14:00 03.04.2019 added output param thisURL--->
<!---v1.02 06:16 26.12.2019 symbols -._ are not encoded --->
<cfparam name="ATTRIBUTES.thisUrl" type="string" default=""><!--- текущий URL без трэка --->
<cfparam name="ATTRIBUTES.output" type="string" default="tr">
<cfparam name="ATTRIBUTES.input" type="string" default="track"><!--- имя параметра в URL или FORM, в которой лежит текущий трэк --->
<cfparam name="ATTRIBUTES.urlParamName" type="string" default="#ATTRIBUTES.input#">
<cfparam name="ATTRIBUTES.defaultBackUrl" type="string">
<!---<cffunction name="urlSafeWrap">
<cfargument name="s" type="string"/>
<cfreturn replace(URLEncodedFormat(s), "%", "!", "ALL")/>
</cffunction>--->
<!---<cffunction name="urlSafeWrap">
<cfargument name="s" type="string"/>
<cfreturn
replace(
replaceList(
URLEncodedFormat(s),
"#URLEncodedFormat('_')#,#URLEncodedFormat('.')#,#URLEncodedFormat('-')#,#URLEncodedFormat('~')#,#URLEncodedFormat('=')#",
"_,.,-,~,="
),
"%", "!", "ALL"
)/>
</cffunction>--->
<cffunction name="urlSafeWrap">
<cfargument name="s" type="string"/>
<cfreturn
replace(
replaceList(
URLEncodedFormat(s),
"#URLEncodedFormat('_')#,#URLEncodedFormat('.')#,#URLEncodedFormat('-')#",
"_,.,-"
),
"%", "!", "ALL"
)/><!---unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" https://www.ietf.org/rfc/rfc3986.txt--->
</cffunction>
<cffunction name="urlSafeUnWrap">
<cfargument name="s" type="string"/>
<cfreturn URLDecode(replace(s, "!", "%","ALL"))/>
</cffunction>
<cfif StructKeyExists(form, ATTRIBUTES.input)>
<cfset track=structFind(form, ATTRIBUTES.input)/>
<cfelseif StructKeyExists(url, ATTRIBUTES.input)>
<cfset track=structFind(url, ATTRIBUTES.input)/>
<cfelse>
<cfset track=""/>
</cfif>
<!--- для предохранения спецсимволов (особенно запятых) в урле используется заворачивание в URLEncodedFormat. ColdFusion (по крайней мере старые версии) не умеет возвращать множественные значения иначе, как списком через запятую. Чтобы избежать непрошенного разворачивания URLEncodedFormat при отправке GETом, мы заменяем % на !.
Реализовал вариант с заворачиванием в GZIP, работает неплохо, но вариант с URLEncodedFormat сохраняет хоть какую-то читабельность.
Можно было бы кодировать в base64. Или вообще хранить на сервере в сессии, а передавать хэш - но непонятно, когда можно забыть этот урл, а накапливать их зря в сессии не хочется.
Попробовал Base64 - очень плохо, все размножается при повторном кодировании и начинает виснуть.
--->
<cfset trackBack="">
<cfif listLen(track) GT 0>
<cfset trackBack=ListDeleteAt(track, listLen(track))>
</cfif>
<cfset trackForward=ListAppend(track, urlSafeWrap(ATTRIBUTES.thisUrl))/>
<cfset backUrlBase=urlSafeUnWrap(listLast(track))/>
<cfif len(backUrlBase) GT 0>
<cfif len(trackBack) GT 0>
<cfif find("?", backUrlBase) GT 0>
<!--- has query string --->
<cfset backUrl="#backUrlBase#&#ATTRIBUTES.urlParamName#=#trackBack#"/>
<cfelse>
<!--- no query string --->
<cfset backUrl="#backUrlBase#?#ATTRIBUTES.urlParamName#=#trackBack#"/>
</cfif>
<cfelse>
<cfset backUrl="#backUrlBase#"/>
</cfif>
<cfelse>
<cfset backUrl="#ATTRIBUTES.defaultBackUrl#"/>
</cfif>
<cfif NOT (find("?", ATTRIBUTES.thisUrl) GT 0)>
<cfset selfUrl="#ATTRIBUTES.thisUrl#?#ATTRIBUTES.urlParamName#=#track#"/>
<cfelse>
<cfset selfUrl="#ATTRIBUTES.thisUrl#&#ATTRIBUTES.urlParamName#=#track#"/>
</cfif>
<cfset setVariable("CALLER.#ATTRIBUTES.output#", structNew())/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.fw", trackForward)/><!--- значение стэка: &track=#tr.fw# --->
<cfset setVariable("CALLER.#ATTRIBUTES.output#.fwx", "#ATTRIBUTES.urlParamName#=#trackForward#")/><!--- готовый суффикс для URL: &#tr.fwx#--->
<cfset setVariable("CALLER.#ATTRIBUTES.output#.bk", trackBack)/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.backUrl", backUrl)/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.self", track)/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.selfUrl", selfUrl)/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.thisUrl", ATTRIBUTES.thisUrl)/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.len", listLen(track))/>
<cfexit method="exitTag"/>
</cfsilent>
+40
View File
@@ -0,0 +1,40 @@
<cfparam name="ATTRIBUTES.field"/>
<cfparam name="ATTRIBUTES.output"/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.ok", "No")/>
<cfif !structKeyExists(FORM, ATTRIBUTES.field)>
<cfexit method="exittag"/>
</cfif>
<cftry>
<cffile
action="upload"
filefield="#ATTRIBUTES.field#"
destination="#GetTempDirectory()#/"
nameconflict="MAKEUNIQUE"
/>
<cfcatch>
<!---<cfdump var=#cfcatch#/><cfrethrow/>--->
<cfexit method="exittag"/>
</cfcatch>
</cftry>
<cfset pathOnServer="#cffile.ServerDirectory#/#cffile.ServerFile#"/>
<cfset contentType="#cffile.ContentType#/#cffile.ContentSubType#"/>
<cfset fileName="#cffile.ClientFile#"/>
<cffile
action="ReadBinary"
file="#pathOnServer#"
variable="binaryData"
/>
<cffile action="DELETE" file="#pathOnServer#"/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.fileName", "#fileName#")/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.contentType", "#contentType#")/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.binaryData", #binaryData#)/>
<cfset setVariable("CALLER.#ATTRIBUTES.output#.ok", "Yes")/>
<cfexit method="exittag"/>
@@ -0,0 +1,142 @@
component {
property name="authEndpoint" type="string" default="";
property name="client_id" type="string" default="";
property name="redirect_uri" type="string" default="";
property name="response_type" type="string" default="code";
property name="state" type="string" default="";
property name="PKCECodeChallenge" type="string" default="";
property name="PKCECodeChallengeMethod" type="string" default="S256";
property name="PKCECodeVerifier" type="string" default="";
public authStringBuilder function init(
required string authEndpoint,
required string client_id,
required string redirect_uri,
string response_type = 'code'
){
setAuthEndpoint( arguments.authEndpoint );
setClientId( arguments.client_id );
setRedirectURI( arguments.redirect_uri );
setResponseType( arguments.response_type );
return this;
}
public void function setAuthEndpoint( required string authEndpoint ){
variables.authEndpoint = arguments.authEndpoint;
}
public void function setClientId( required string client_id ){
variables.params[ 'client_id' ] = arguments.client_id;
}
public void function setRedirectURI( required string redirect_uri ){
variables.params[ 'redirect_uri' ] = arguments.redirect_uri;
}
public void function setResponseType( required string response_type ){
variables.params[ 'response_type' ] = arguments.response_type;
}
public void function setPKCECodeChallenge( required string code_challenge ){
variables.params[ 'code_challenge' ] = arguments.code_challenge;
}
public void function setPKCECodeChallengeMethod( required string code_challenge_method ){
variables.params[ 'code_challenge_method' ] = arguments.code_challenge_method;
}
public void function setPKCECodeVerifier( required string code_verifier ){
variables.params[ 'code_verifier' ] = arguments.code_verifier;
}
public authStringBuilder function withAuthEndpoint(
required string authEndpoint
){
setAuthEndpoint( arguments.authEndpoint );
return this;
}
public authStringBuilder function withClientId(
required string client_id
){
setClientId( arguments.client_id );
return this;
}
public authStringBuilder function withResponseType(
required string response_type
){
setResponseType( arguments.response_type );
return this;
}
public authStringBuilder function withRedirectURI(
required string redirect_uri
){
setRedirectURI( arguments.redirect_uri );
return this;
}
public authStringBuilder function withState(
required string state
){
variables.params[ 'state' ] = arguments.state;
return this;
}
public authStringBuilder function withPKCE(
required string code_challenge,
required string code_challenge_method,
required string code_verifier
){
setPKCECodeChallenge( arguments.code_challenge );
setPKCECodeChallengeMethod( arguments.code_challenge_method );
setPKCECodeVerifier( arguments.code_verifier );
return this;
}
public authStringBuilder function withParams( required struct parameters ){
for( var item in arguments.parameters ){
variables.params[ item ] = arguments.parameters[ item ];
}
return this;
}
public string function get(){
var stuParams = duplicate( variables.params );
structDelete( stuParams, 'code_verifier' );
return variables.authEndpoint & buildParamString( argScope = stuParams );
}
/**
* Returns a struct representation of the params values stored in variables.params.
*/
public struct function paramsMemento(){
return variables.params;
}
/**
* I return a string containing any extra URL parameters to concatenate and pass through when authenticating.
* @argScope A structure containing key / value pairs of data to be included in the URL string.
**/
public string function buildParamString( struct argScope={} ) {
var strURLParam = '?';
if( structCount( arguments.argScope ) ) {
var intCount = 1;
for( var key in arguments.argScope ) {
if( listLen( strURLParam ) && intCount > 1 ) {
strURLParam = strURLParam & '&';
}
strURLParam = strURLParam & lcase( key ) & '=' & trim( arguments.argScope[ key ] );
intCount++;
}
}
return strURLParam;
}
}