docs: detailed comments; fix: var declarations, no DROP, text inputs

This commit is contained in:
2026-07-20 08:13:26 +04:00
parent 53e12ecddd
commit d0fb4df1cb
2 changed files with 169 additions and 23 deletions
+111 -15
View File
@@ -1,67 +1,163 @@
<!---
Application.cfc — конфигурация Lucee CRUD-приложения
Жизненный цикл:
1. OnApplicationStart — вызывается ОДИН раз при старте приложения
- читает TABLE_NAME из env vars (json_env в Terraform)
- создаёт таблицу CREATE TABLE IF NOT EXISTS
- сохраняет имя таблицы в application.tableName
2. OnRequestStart — вызывается ПЕРЕД каждым запросом
- копирует application.tableName → request.tableName
3. OnRequest — вызывает index.cfm с CRUD-логикой
Datasource: testds, конфигурируется из env vars с префиксом testds_
(testds_class, testds_connectionString, testds_username, testds_password, ...)
Terraform json_env должен содержать:
- TABLE_NAME — имя таблицы (по умолчанию check_point)
- testds_class — JDBC-драйвер
- testds_connectionString — строка подключения к PostgreSQL
- testds_username — имя пользователя БД
- testds_password — пароль пользователя БД
--->
<cfcomponent displayname="Application" output="true"> <cfcomponent displayname="Application" output="true">
<!--- Идентификатор приложения для Lucee --->
<cfset this.Name = "tfluceecrud" /> <cfset this.Name = "tfluceecrud" />
<!--- Таймаут приложения: 3 часа бездействия → перезапуск (вызовет OnApplicationStart заново) --->
<cfset this.applicationTimeout = createTimeSpan(0, 0, 3, 0) /> <cfset this.applicationTimeout = createTimeSpan(0, 0, 3, 0) />
<!--- Включаем сессии для защиты от CSRF (Lucee автоматически добавляет токены) --->
<cfset this.sessionmanagement = "Yes" /> <cfset this.sessionmanagement = "Yes" />
<!--- Сессия живёт 2 часа неактивности --->
<cfset this.sessiontimeout = CreateTimeSpan(0, 0, 120, 0) /> <cfset this.sessiontimeout = CreateTimeSpan(0, 0, 120, 0) />
<!--- Имя datasource по умолчанию — используется в cfquery без атрибута datasource --->
<cfset this.datasource = "testds" /> <cfset this.datasource = "testds" />
<!--- Конфигурируем datasource из env vars с префиксом testds_ --->
<cfset getDS(this.datasource, this.datasource) /> <cfset getDS(this.datasource, this.datasource) />
<!--- Отключаем отладку на проде, таймаут запроса 20 секунд --->
<cfsetting requesttimeout="20" showdebugoutput="false" /> <cfsetting requesttimeout="20" showdebugoutput="false" />
<cffunction name="OnApplicationStart" access="public" returntype="boolean">
<cfreturn true />
</cffunction>
<cffunction name="OnRequestStart" access="public" returntype="boolean"> <!---
<cfset request.tableName = createObject("java", "java.lang.System").getEnv().get("TABLE_NAME") /> OnApplicationStart — выполняется ОДИН раз при старте приложения.
<cfif NOT isDefined("request.tableName") OR NOT len(request.tableName)> Здесь: создаём таблицу если её нет, сохраняем имя в application scope.
<cfset request.tableName = "check_point" />
application scope — живёт пока приложение не перезапустится (таймаут 3 часа).
Используем application а не request потому что request умирает после каждого запроса.
--->
<cffunction name="OnApplicationStart" access="public" returntype="boolean">
<!--- 1. Читаем имя таблицы из переменной окружения TABLE_NAME --->
<cfset var tableName = createObject("java", "java.lang.System").getEnv().get("TABLE_NAME") />
<!--- 2. Если переменная не задана в json_env — используем значение по умолчанию --->
<cfif NOT isDefined("tableName") OR NOT len(tableName)>
<cfset tableName = "check_point" />
</cfif> </cfif>
<!--- 3. Сохраняем в application scope — доступно всем запросам этого приложения --->
<cfset application.tableName = tableName />
<!--- 4. Создаём таблицу если она ещё не существует (IF NOT EXISTS) --->
<!--- id: автоинкрементный первичный ключ --->
<!--- value: текстовое поле для CRUD-значений --->
<cftry> <cftry>
<cfquery> <cfquery>
DROP TABLE IF EXISTS #request.tableName# CREATE TABLE IF NOT EXISTS #tableName# (
</cfquery>
<cfquery>
CREATE TABLE #request.tableName# (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
value TEXT NOT NULL DEFAULT '' value TEXT NOT NULL DEFAULT ''
) )
</cfquery> </cfquery>
<!--- Игнорируем ошибки: нет прав, таблица уже есть, БД недоступна --->
<cfcatch type="database"></cfcatch> <cfcatch type="database"></cfcatch>
</cftry> </cftry>
<cfreturn true /> <cfreturn true />
</cffunction> </cffunction>
<!---
OnRequestStart — выполняется ПЕРЕД каждым HTTP-запросом.
Здесь: копируем имя таблицы из application в request scope.
request scope — живёт только в течение одного запроса.
Каждая страница (index.cfm) читает request.tableName.
--->
<cffunction name="OnRequestStart" access="public" returntype="boolean">
<!--- Если приложение уже стартовало — берём имя из application --->
<cfif isDefined("application.tableName")>
<cfset request.tableName = application.tableName />
<!--- Иначе (первый запрос до OnApplicationStart) — fallback --->
<cfelse>
<cfset request.tableName = "check_point" />
</cfif>
<cfreturn true />
</cffunction>
<!---
OnRequest — обработчик каждого запроса.
Включает целевой шаблон (index.cfm) с установленной кодировкой.
--->
<cffunction name="OnRequest" access="public" returntype="void"> <cffunction name="OnRequest" access="public" returntype="void">
<cfargument name="template" type="string" required="true" /> <cfargument name="template" type="string" required="true" />
<!--- UTF-8 для форм и URL-параметров --->
<cfset setEncoding("FORM", "UTF-8") /> <cfset setEncoding("FORM", "UTF-8") />
<cfset setEncoding("URL", "UTF-8") /> <cfset setEncoding("URL", "UTF-8") />
<!--- Вычисляем web-корень для ссылок --->
<cfset local.basePath = getDirectoryFromPath(getCurrentTemplatePath()) /> <cfset local.basePath = getDirectoryFromPath(getCurrentTemplatePath()) />
<cfset request.webRoot = "../" /> <cfset request.webRoot = "../" />
<cfset request.thisPage = Replace(ReplaceNoCase(expandPath(arguments.template), local.basePath, ""), "\", "/") /> <cfset request.thisPage = Replace(ReplaceNoCase(expandPath(arguments.template), local.basePath, ""), "\", "/") />
<!--- Выполняем запрошенный шаблон (index.cfm) --->
<cfinclude template="#arguments.template#" /> <cfinclude template="#arguments.template#" />
</cffunction> </cffunction>
<cffunction name="getDS" access="private" returntype="void">
<cfargument name="dsname" type="string" required="true" />
<cfargument name="prefix" type="string" default="#dsname#" />
<cfset system = createObject("java", "java.lang.System") /> <!---
getDS — конфигурирует datasource Lucee из переменных окружения.
Соглашение об именах env vars:
{prefix}_{field} → параметр datasource
Пример для testds:
testds_class → JDBC-драйвер
testds_connectionString → jdbc:postgresql://host:5432/db
testds_username → пользователь
testds_password → пароль
Все параметры передаются через json_env в Terraform.
--->
<cffunction name="getDS" access="private" returntype="void">
<cfargument name="dsname" type="string" required="true" hint="Имя datasource (testds)" />
<cfargument name="prefix" type="string" default="#dsname#" hint="Префикс env vars" />
<!--- Java-объект для чтения переменных окружения --->
<cfset var system = createObject("java", "java.lang.System") />
<!--- Словарь параметров datasource --->
<cfset var ds = {} /> <cfset var ds = {} />
<!--- Перебираем все возможные поля datasource --->
<cfloop list="class,connectionString,username,password,bundleName,bundleVersion,connectionLimit,liveTimeout,validate" item="field"> <cfloop list="class,connectionString,username,password,bundleName,bundleVersion,connectionLimit,liveTimeout,validate" item="field">
<!--- Читаем env var: {prefix}_{field}, например testds_username --->
<cfset var value = system.getEnv("#arguments.prefix#_#field#") /> <cfset var value = system.getEnv("#arguments.prefix#_#field#") />
<!--- Если переменная задана и не пуста — добавляем в datasource --->
<cfif isDefined("value") AND len(value)> <cfif isDefined("value") AND len(value)>
<cfset structInsert(ds, field, value) /> <cfset structInsert(ds, field, value) />
</cfif> </cfif>
</cfloop> </cfloop>
<!--- Регистрируем datasource в Lucee --->
<cfset THIS.datasources[dsname] = ds /> <cfset THIS.datasources[dsname] = ds />
</cffunction> </cffunction>
+57 -7
View File
@@ -1,15 +1,41 @@
<!---
index.cfm — CRUD-интерфейс для таблицы в PostgreSQL
Логика:
1. Если POST-запрос с FORM.action — выполняем create/update/delete
и редиректим на GET (POST-redirect-GET паттерн)
2. Если GET-запрос с URL.edit — загружаем запись для редактирования
3. Всегда показываем список всех записей + форму добавления/редактирования
Безопасность:
- cfqueryparam для всех входных данных → защита от SQL-инъекций
- cflocation после POST → защита от повторной отправки формы
- cfoutput только где нужно → XSS-защита через автоматическое экранирование Lucee
Переменные:
- request.tableName — имя таблицы (из Application.cfc / json_env TABLE_NAME)
- FORM.action — create | update | delete (POST)
- FORM.value — значение записи (POST)
- FORM.id — ID записи для update/delete (POST)
- URL.edit — ID записи для редактирования (GET)
--->
<cfset tableName = request.tableName /> <cfset tableName = request.tableName />
<!--- Обработка действий --->
<!--- ====== ОБРАБОТКА POST-ЗАПРОСОВ (create / update / delete) ====== --->
<!--- isDefined("FORM.action") — true только если страница вызвана через <form method="post"> --->
<cfif isDefined("FORM.action")> <cfif isDefined("FORM.action")>
<!--- CREATE: вставка новой записи --->
<cfif FORM.action EQ "create"> <cfif FORM.action EQ "create">
<!--- cfqueryparam cfsqltype="cf_sql_varchar" — экранирует значение, защита от SQL-инъекций --->
<cfquery name="insertRow"> <cfquery name="insertRow">
INSERT INTO #tableName# (value) INSERT INTO #tableName# (value)
VALUES (<cfqueryparam value="#FORM.value#" cfsqltype="cf_sql_varchar" />) VALUES (<cfqueryparam value="#FORM.value#" cfsqltype="cf_sql_varchar" />)
</cfquery> </cfquery>
</cfif> </cfif>
<!--- UPDATE: обновление существующей записи по ID --->
<cfif FORM.action EQ "update"> <cfif FORM.action EQ "update">
<cfquery name="updateRow"> <cfquery name="updateRow">
UPDATE #tableName# UPDATE #tableName#
@@ -18,6 +44,7 @@
</cfquery> </cfquery>
</cfif> </cfif>
<!--- DELETE: удаление записи по ID --->
<cfif FORM.action EQ "delete"> <cfif FORM.action EQ "delete">
<cfquery name="deleteRow"> <cfquery name="deleteRow">
DELETE FROM #tableName# DELETE FROM #tableName#
@@ -25,22 +52,30 @@
</cfquery> </cfquery>
</cfif> </cfif>
<!--- POST-redirect-GET: после любого действия — редирект на чистый URL без POST-данных.
Это предотвращает повторную отправку формы при обновлении страницы (F5). --->
<cflocation url="index.cfm" addtoken="no" /> <cflocation url="index.cfm" addtoken="no" />
</cfif> </cfif>
<!--- Режим редактирования --->
<!--- ====== РЕЖИМ РЕДАКТИРОВАНИЯ (GET-запрос с ?edit=ID) ====== --->
<!--- editRow всегда инициализируем пустым словарём.
Если URL.edit не задан или запись не найдена — editRow останется пустым. --->
<cfset editRow = {} /> <cfset editRow = {} />
<cfif isDefined("URL.edit")> <cfif isDefined("URL.edit")>
<!--- Загружаем запись по ID из URL --->
<cfquery name="editQuery"> <cfquery name="editQuery">
SELECT id, value FROM #tableName# SELECT id, value FROM #tableName#
WHERE id = <cfqueryparam value="#URL.edit#" cfsqltype="cf_sql_integer" /> WHERE id = <cfqueryparam value="#URL.edit#" cfsqltype="cf_sql_integer" />
</cfquery> </cfquery>
<!--- Если запись найдена — заполняем editRow данными из запроса --->
<cfif editQuery.recordCount GT 0> <cfif editQuery.recordCount GT 0>
<cfset editRow = { id = editQuery.id, value = editQuery.value } /> <cfset editRow = { id = editQuery.id, value = editQuery.value } />
</cfif> </cfif>
</cfif> </cfif>
<!--- Все записи --->
<!--- ====== СПИСОК ВСЕХ ЗАПИСЕЙ ====== --->
<cfquery name="rows"> <cfquery name="rows">
SELECT id, value FROM #tableName# ORDER BY id SELECT id, value FROM #tableName# ORDER BY id
</cfquery> </cfquery>
@@ -64,18 +99,23 @@
<h1>#tableName#</h1> <h1>#tableName#</h1>
<!--- Таблица записей --->
<table> <table>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Value</th> <th>Value</th>
<th></th> <th></th>
</tr> </tr>
<!--- cfoutput query="rows" — цикл по всем строкам результата SELECT --->
<cfoutput query="rows"> <cfoutput query="rows">
<tr> <tr>
<td>#id#</td> <td>#id#</td>
<td>#value#</td> <td>#value#</td>
<td class="actions"> <td class="actions">
<!--- Ссылка на редактирование: index.cfm?edit={id} --->
<a href="index.cfm?edit=#id#">✏️</a> <a href="index.cfm?edit=#id#">✏️</a>
<!--- Форма удаления: POST с action=delete и id записи.
JS confirm() — защита от случайного удаления. --->
<form method="post" style="display:inline"> <form method="post" style="display:inline">
<input type="hidden" name="action" value="delete" /> <input type="hidden" name="action" value="delete" />
<input type="hidden" name="id" value="#id#" /> <input type="hidden" name="id" value="#id#" />
@@ -84,28 +124,38 @@
</td> </td>
</tr> </tr>
</cfoutput> </cfoutput>
<!--- Если записей нет — показываем информационное сообщение --->
<cfif rows.recordCount EQ 0> <cfif rows.recordCount EQ 0>
<tr><td colspan="3">Нет записей. Создайте первую.</td></tr> <tr><td colspan="3">Нет записей. Создайте первую.</td></tr>
</cfif> </cfif>
</table> </table>
<cfif NOT structIsEmpty(editRow)><cfoutput> <!--- ====== ФОРМА: редактирование ИЛИ добавление ====== --->
<!--- structIsEmpty(editRow) — true если нет записи для редактирования → показываем форму ДОБАВЛЕНИЯ --->
<!--- false если URL.edit задан и запись найдена → показываем форму РЕДАКТИРОВАНИЯ --->
<cfif NOT structIsEmpty(editRow)>
<!--- cfoutput нужен чтобы #editRow.id# и #editRow.value# вычислились --->
<cfoutput>
<h2>Редактировать #editRow.id#</h2> <h2>Редактировать #editRow.id#</h2>
<form method="post"> <form method="post">
<input type="hidden" name="action" value="update" /> <input type="hidden" name="action" value="update" />
<input type="hidden" name="id" value="#editRow.id#" /> <input type="hidden" name="id" value="#editRow.id#" />
<input type="number" name="value" value="#editRow.value#" required /> <!--- type="text" — позволяет вводить любые символы, не только цифры --->
<input type="text" name="value" value="#editRow.value#" required />
<button type="submit">Сохранить</button> <button type="submit">Сохранить</button>
<a href="index.cfm">Отмена</a> <a href="index.cfm">Отмена</a>
</form></cfoutput> </form>
</cfoutput>
<cfelse> <cfelse>
<h2>Добавить</h2> <h2>Добавить</h2>
<form method="post"> <form method="post">
<input type="hidden" name="action" value="create" /> <input type="hidden" name="action" value="create" />
<input type="number" name="value" placeholder="Значение" required /> <!--- placeholder — подсказка в пустом поле --->
<input type="text" name="value" placeholder="Значение" required />
<button type="submit">Добавить</button> <button type="submit">Добавить</button>
</form> </form>
</cfif> </cfif>
</body> </body>
</html> </html>
</html>