docs: detailed comments; fix: var declarations, no DROP, text inputs
This commit is contained in:
+112
-16
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user