@reldens/cms 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +413 -2
- package/admin/reldens-admin-client.css +10 -128
- package/admin/templates/fields/view/audio.html +7 -0
- package/admin/templates/fields/view/audios.html +8 -0
- package/install/index.html +4 -0
- package/lib/admin-manager/router-contents.js +14 -7
- package/lib/dynamic-form-renderer.js +228 -0
- package/lib/dynamic-form-request-handler.js +135 -0
- package/lib/dynamic-form.js +310 -0
- package/lib/frontend.js +35 -1
- package/lib/installer.js +2 -1
- package/lib/template-engine/forms-transformer.js +187 -0
- package/lib/template-engine.js +17 -0
- package/lib/templates-list.js +2 -0
- package/migrations/default-forms.sql +22 -0
- package/package.json +5 -5
- package/templates/cms_forms/field_email.html +14 -0
- package/templates/cms_forms/field_number.html +17 -0
- package/templates/cms_forms/field_select.html +15 -0
- package/templates/cms_forms/field_text.html +16 -0
- package/templates/cms_forms/field_textarea.html +13 -0
- package/templates/cms_forms/form.html +22 -0
- package/templates/css/styles.css +4 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - CMS - DynamicForm
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { SchemaValidator, Logger, sc } = require('@reldens/utils');
|
|
8
|
+
|
|
9
|
+
class DynamicForm
|
|
10
|
+
{
|
|
11
|
+
|
|
12
|
+
constructor(props)
|
|
13
|
+
{
|
|
14
|
+
this.dataServer = sc.get(props, 'dataServer', false);
|
|
15
|
+
this.honeypotFieldName = sc.get(props, 'honeypotFieldName', 'website_url');
|
|
16
|
+
this.allowedOrigins = sc.get(props, 'allowedOrigins', []);
|
|
17
|
+
this.rateLimitCache = new Map();
|
|
18
|
+
this.rateLimitWindow = sc.get(props, 'rateLimitWindow', 300000);
|
|
19
|
+
this.rateLimitMax = sc.get(props, 'rateLimitMax', 5);
|
|
20
|
+
this.events = sc.get(props, 'events', false);
|
|
21
|
+
if(!this.events){
|
|
22
|
+
Logger.error('EventsManager not provided to DynamicForm - forms functionality disabled');
|
|
23
|
+
this.isDisabled = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async validateFormSubmission(formKey, submittedValues, req)
|
|
28
|
+
{
|
|
29
|
+
if(this.isDisabled){
|
|
30
|
+
return {isValid: false, error: 'Forms functionality disabled'};
|
|
31
|
+
}
|
|
32
|
+
if(!formKey || !submittedValues){
|
|
33
|
+
return {isValid: false, error: 'Missing formKey or submittedValues'};
|
|
34
|
+
}
|
|
35
|
+
if(!sc.isObject(submittedValues)){
|
|
36
|
+
return {isValid: false, error: 'Invalid submittedValues format'};
|
|
37
|
+
}
|
|
38
|
+
let formConfig = await this.getFormConfig(formKey);
|
|
39
|
+
if(!formConfig){
|
|
40
|
+
return {isValid: false, error: 'Form not found or disabled'};
|
|
41
|
+
}
|
|
42
|
+
await this.events.emit('reldens.dynamicForm.beforeValidation', {
|
|
43
|
+
formKey,
|
|
44
|
+
formConfig,
|
|
45
|
+
submittedValues,
|
|
46
|
+
req
|
|
47
|
+
});
|
|
48
|
+
let originValidation = this.validateOrigin(req);
|
|
49
|
+
if(!originValidation.isValid){
|
|
50
|
+
return originValidation;
|
|
51
|
+
}
|
|
52
|
+
let honeypotValidation = this.validateHoneypot(submittedValues);
|
|
53
|
+
if(!honeypotValidation.isValid){
|
|
54
|
+
return honeypotValidation;
|
|
55
|
+
}
|
|
56
|
+
let rateLimitValidation = this.validateRateLimit(req);
|
|
57
|
+
if(!rateLimitValidation.isValid){
|
|
58
|
+
return rateLimitValidation;
|
|
59
|
+
}
|
|
60
|
+
let fieldsValidation = this.validateFields(formConfig.fields_schema, submittedValues);
|
|
61
|
+
if(!fieldsValidation.isValid){
|
|
62
|
+
return fieldsValidation;
|
|
63
|
+
}
|
|
64
|
+
await this.events.emit('reldens.dynamicForm.afterValidation', {
|
|
65
|
+
formKey,
|
|
66
|
+
formConfig,
|
|
67
|
+
submittedValues,
|
|
68
|
+
req,
|
|
69
|
+
validationResult: {isValid: true, formConfig}
|
|
70
|
+
});
|
|
71
|
+
return {isValid: true, formConfig};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getFormConfig(formKey)
|
|
75
|
+
{
|
|
76
|
+
let formsEntity = this.dataServer.getEntity('cmsForms');
|
|
77
|
+
if(!formsEntity){
|
|
78
|
+
Logger.error('Forms entity not found');
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
let form = await formsEntity.loadOneBy('form_key', formKey);
|
|
82
|
+
if(!form || !form.enabled){
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return form;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
validateHoneypot(submittedValues)
|
|
89
|
+
{
|
|
90
|
+
let honeypotValue = sc.get(submittedValues, this.honeypotFieldName, '');
|
|
91
|
+
if('' !== honeypotValue){
|
|
92
|
+
Logger.warning('Honeypot field filled, potential bot submission');
|
|
93
|
+
return {isValid: false, error: 'Invalid submission'};
|
|
94
|
+
}
|
|
95
|
+
return {isValid: true};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
validateOrigin(req)
|
|
99
|
+
{
|
|
100
|
+
if(0 === this.allowedOrigins.length){
|
|
101
|
+
return {isValid: true};
|
|
102
|
+
}
|
|
103
|
+
let origin = sc.get(req.headers, 'origin', '');
|
|
104
|
+
let referer = sc.get(req.headers, 'referer', '');
|
|
105
|
+
let host = sc.get(req.headers, 'host', '');
|
|
106
|
+
let isValidOrigin = false;
|
|
107
|
+
for(let allowedOrigin of this.allowedOrigins){
|
|
108
|
+
if(origin.includes(allowedOrigin) || referer.includes(allowedOrigin) || host.includes(allowedOrigin)){
|
|
109
|
+
isValidOrigin = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if(!isValidOrigin){
|
|
114
|
+
Logger.warning('Invalid form submission origin: '+origin+' / '+referer+' / '+host);
|
|
115
|
+
return {isValid: false, error: 'Invalid request origin'};
|
|
116
|
+
}
|
|
117
|
+
return {isValid: true};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
validateRateLimit(req)
|
|
121
|
+
{
|
|
122
|
+
let clientIp = this.getClientIp(req);
|
|
123
|
+
let now = Date.now();
|
|
124
|
+
let clientLimits = this.rateLimitCache.get(clientIp) || {submissions: [], firstSubmission: now};
|
|
125
|
+
clientLimits.submissions = clientLimits.submissions.filter(time => now - time < this.rateLimitWindow);
|
|
126
|
+
if(clientLimits.submissions.length >= this.rateLimitMax){
|
|
127
|
+
Logger.warning('Rate limit exceeded for IP: '+clientIp);
|
|
128
|
+
return {isValid: false, error: 'Too many submissions, please try again later'};
|
|
129
|
+
}
|
|
130
|
+
clientLimits.submissions.push(now);
|
|
131
|
+
this.rateLimitCache.set(clientIp, clientLimits);
|
|
132
|
+
return {isValid: true};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getClientIp(req)
|
|
136
|
+
{
|
|
137
|
+
return sc.get(req.headers, 'x-forwarded-for', sc.get(req.connection, 'remoteAddress', 'unknown')).split(',')[0].trim();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
validateFields(fieldsSchema, submittedValues)
|
|
141
|
+
{
|
|
142
|
+
let parsedSchema = sc.isString(fieldsSchema) ? sc.toJson(fieldsSchema) : fieldsSchema;
|
|
143
|
+
if(!sc.isArray(parsedSchema)){
|
|
144
|
+
Logger.error('Invalid fields schema format');
|
|
145
|
+
return {isValid: false, error: 'Invalid form configuration'};
|
|
146
|
+
}
|
|
147
|
+
let schemaForValidator = {};
|
|
148
|
+
let missingFields = [];
|
|
149
|
+
for(let field of parsedSchema){
|
|
150
|
+
if(!sc.isObject(field)){
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
let fieldName = sc.get(field, 'name', '');
|
|
154
|
+
let fieldType = sc.get(field, 'type', 'text');
|
|
155
|
+
let isRequired = sc.get(field, 'required', false);
|
|
156
|
+
if('' === fieldName){
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
let validationSchema = this.buildValidationSchema(field, fieldType);
|
|
160
|
+
validationSchema.required = isRequired;
|
|
161
|
+
schemaForValidator[fieldName] = validationSchema;
|
|
162
|
+
if(isRequired){
|
|
163
|
+
let fieldValue = sc.get(submittedValues, fieldName, '');
|
|
164
|
+
if('' === fieldValue || null === fieldValue || undefined === fieldValue){
|
|
165
|
+
let fieldLabel = sc.get(field, 'label', fieldName);
|
|
166
|
+
missingFields.push(fieldLabel);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if(0 < missingFields.length){
|
|
171
|
+
return {
|
|
172
|
+
isValid: false,
|
|
173
|
+
error: 'Required fields missing: '+missingFields.join(', '),
|
|
174
|
+
missingFields
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
let validator = new SchemaValidator(schemaForValidator);
|
|
178
|
+
if(!validator.validate(submittedValues)){
|
|
179
|
+
return {isValid: false, error: 'Invalid field values provided'};
|
|
180
|
+
}
|
|
181
|
+
return {isValid: true};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
buildValidationSchema(field, fieldType)
|
|
185
|
+
{
|
|
186
|
+
let schema = {type: 'string'};
|
|
187
|
+
let minLength = sc.get(field, 'minLength', 0);
|
|
188
|
+
let maxLength = sc.get(field, 'maxLength', 0);
|
|
189
|
+
let pattern = sc.get(field, 'pattern', '');
|
|
190
|
+
let min = sc.get(field, 'min', '');
|
|
191
|
+
let max = sc.get(field, 'max', '');
|
|
192
|
+
if('number' === fieldType){
|
|
193
|
+
schema.type = 'number';
|
|
194
|
+
if('' !== min && sc.isNumber(Number(min))){
|
|
195
|
+
schema.min = Number(min);
|
|
196
|
+
}
|
|
197
|
+
if('' !== max && sc.isNumber(Number(max))){
|
|
198
|
+
schema.max = Number(max);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if('email' === fieldType){
|
|
202
|
+
schema.custom = (value) => {
|
|
203
|
+
if(!value){
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
let emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
207
|
+
return emailPattern.test(value);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if(0 < minLength){
|
|
211
|
+
schema.min = minLength;
|
|
212
|
+
}
|
|
213
|
+
if(0 < maxLength){
|
|
214
|
+
schema.max = maxLength;
|
|
215
|
+
}
|
|
216
|
+
if('' !== pattern){
|
|
217
|
+
try {
|
|
218
|
+
schema.pattern = new RegExp(pattern);
|
|
219
|
+
} catch(error) {
|
|
220
|
+
Logger.warning('Invalid field pattern: '+pattern+' for field: '+sc.get(field, 'name', ''));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
let options = sc.get(field, 'options', []);
|
|
224
|
+
if(sc.isArray(options) && 0 < options.length){
|
|
225
|
+
schema.enum = options.map(option => {
|
|
226
|
+
return sc.isObject(option) ? sc.get(option, 'value', '') : option;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return schema;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
prepareSubmittedValues(submittedValues, fieldsSchema)
|
|
233
|
+
{
|
|
234
|
+
let parsedSchema = sc.isString(fieldsSchema) ? sc.toJson(fieldsSchema) : fieldsSchema;
|
|
235
|
+
if(!sc.isArray(parsedSchema)){
|
|
236
|
+
return submittedValues;
|
|
237
|
+
}
|
|
238
|
+
let prepared = {};
|
|
239
|
+
for(let field of parsedSchema){
|
|
240
|
+
if(!sc.isObject(field)){
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
let fieldName = sc.get(field, 'name', '');
|
|
244
|
+
let fieldType = sc.get(field, 'type', 'text');
|
|
245
|
+
let fieldValue = sc.get(submittedValues, fieldName, '');
|
|
246
|
+
if('' === fieldName){
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
prepared[fieldName] = this.normalizeFieldValue(fieldValue, fieldType, field);
|
|
250
|
+
}
|
|
251
|
+
return prepared;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
normalizeFieldValue(value, type, field = {})
|
|
255
|
+
{
|
|
256
|
+
if(!value){
|
|
257
|
+
return '';
|
|
258
|
+
}
|
|
259
|
+
let stringValue = String(value);
|
|
260
|
+
if('email' === type){
|
|
261
|
+
stringValue = stringValue.toLowerCase().trim();
|
|
262
|
+
}
|
|
263
|
+
if('number' === type){
|
|
264
|
+
let numberValue = parseFloat(stringValue);
|
|
265
|
+
return isNaN(numberValue) ? '' : numberValue;
|
|
266
|
+
}
|
|
267
|
+
let maxLength = sc.get(field, 'maxLength', 0);
|
|
268
|
+
if(0 < maxLength){
|
|
269
|
+
stringValue = stringValue.substring(0, maxLength);
|
|
270
|
+
}
|
|
271
|
+
return stringValue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async saveFormSubmission(formConfig, preparedValues)
|
|
275
|
+
{
|
|
276
|
+
let submissionsEntity = this.dataServer.getEntity('cmsFormsSubmitted');
|
|
277
|
+
if(!submissionsEntity){
|
|
278
|
+
Logger.error('Forms submissions entity not found');
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
await this.events.emit('reldens.dynamicForm.beforeSave', {
|
|
282
|
+
formConfig,
|
|
283
|
+
preparedValues
|
|
284
|
+
});
|
|
285
|
+
try {
|
|
286
|
+
let submissionData = {
|
|
287
|
+
form_id: formConfig.id,
|
|
288
|
+
submitted_values: JSON.stringify(preparedValues)
|
|
289
|
+
};
|
|
290
|
+
let result = await submissionsEntity.create(submissionData);
|
|
291
|
+
if(!result){
|
|
292
|
+
Logger.error('Failed to save form submission');
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
await this.events.emit('reldens.dynamicForm.afterSave', {
|
|
296
|
+
formConfig,
|
|
297
|
+
preparedValues,
|
|
298
|
+
result
|
|
299
|
+
});
|
|
300
|
+
Logger.info('Form submission saved successfully for form: '+formConfig.form_key);
|
|
301
|
+
return result;
|
|
302
|
+
} catch(error) {
|
|
303
|
+
Logger.error('Error saving form submission: '+error.message);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports.DynamicForm = DynamicForm;
|
package/lib/frontend.js
CHANGED
|
@@ -8,6 +8,9 @@ const { TemplateEngine } = require('./template-engine');
|
|
|
8
8
|
const { Search } = require('./search');
|
|
9
9
|
const { SearchRenderer } = require('./search-renderer');
|
|
10
10
|
const { SearchRequestHandler } = require('./search-request-handler');
|
|
11
|
+
const { DynamicForm } = require('./dynamic-form');
|
|
12
|
+
const { DynamicFormRenderer } = require('./dynamic-form-renderer');
|
|
13
|
+
const { DynamicFormRequestHandler } = require('./dynamic-form-request-handler');
|
|
11
14
|
const { TemplateResolver } = require('./frontend/template-resolver');
|
|
12
15
|
const { TemplateCache } = require('./frontend/template-cache');
|
|
13
16
|
const { RequestProcessor } = require('./frontend/request-processor');
|
|
@@ -39,12 +42,23 @@ class Frontend
|
|
|
39
42
|
this.cacheManager = sc.get(props, 'cacheManager', false);
|
|
40
43
|
this.handleFrontendTemplateReload = sc.get(props, 'handleFrontendTemplateReload', false);
|
|
41
44
|
this.searchPath = sc.get(props, 'searchPath', '/search');
|
|
45
|
+
this.dynamicFormPath = sc.get(props, 'dynamicFormPath', '/dynamic-form');
|
|
46
|
+
this.dynamicFormDisplayPath = sc.get(props, 'dynamicFormDisplayPath', '/form');
|
|
42
47
|
this.searchSets = sc.get(props, 'searchSets', false);
|
|
43
48
|
this.searchConfig = {dataServer: this.dataServer};
|
|
44
49
|
if(this.searchSets){
|
|
45
50
|
this.searchConfig.searchSets = this.searchSets;
|
|
46
51
|
}
|
|
47
52
|
this.search = new Search(this.searchConfig);
|
|
53
|
+
this.dynamicFormConfig = {
|
|
54
|
+
dataServer: this.dataServer,
|
|
55
|
+
allowedOrigins: sc.get(props, 'allowedOrigins', []),
|
|
56
|
+
honeypotFieldName: sc.get(props, 'honeypotFieldName', 'website_url'),
|
|
57
|
+
rateLimitWindow: sc.get(props, 'rateLimitWindow', 300000),
|
|
58
|
+
rateLimitMax: sc.get(props, 'rateLimitMax', 5),
|
|
59
|
+
events: this.events
|
|
60
|
+
};
|
|
61
|
+
this.dynamicForm = new DynamicForm(this.dynamicFormConfig);
|
|
48
62
|
this.metaDefaults = sc.get(props, 'metaDefaults', {
|
|
49
63
|
locale: 'en',
|
|
50
64
|
viewport: 'width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover',
|
|
@@ -62,7 +76,22 @@ class Frontend
|
|
|
62
76
|
renderEngine: this.renderEngine,
|
|
63
77
|
getPartials: this.templateCache.getPartialsForDomain.bind(this.templateCache)
|
|
64
78
|
});
|
|
79
|
+
this.dynamicFormRenderer = new DynamicFormRenderer({
|
|
80
|
+
renderEngine: this.renderEngine,
|
|
81
|
+
getPartials: this.templateCache.getPartialsForDomain.bind(this.templateCache),
|
|
82
|
+
projectRoot: this.projectRoot,
|
|
83
|
+
defaultDomain: this.defaultDomain,
|
|
84
|
+
events: this.events
|
|
85
|
+
});
|
|
65
86
|
this.searchRequestHandler = new SearchRequestHandler(this);
|
|
87
|
+
this.dynamicFormRequestHandler = new DynamicFormRequestHandler({
|
|
88
|
+
dynamicForm: this.dynamicForm,
|
|
89
|
+
contentRenderer: this.contentRenderer,
|
|
90
|
+
requestProcessor: this.requestProcessor,
|
|
91
|
+
cacheManager: this.cacheManager,
|
|
92
|
+
enableJsonResponse: sc.get(props, 'enableJsonResponse', false),
|
|
93
|
+
events: this.events
|
|
94
|
+
});
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
async initialize()
|
|
@@ -96,7 +125,9 @@ class Frontend
|
|
|
96
125
|
events: this.events,
|
|
97
126
|
defaultDomain: this.defaultDomain,
|
|
98
127
|
projectRoot: this.projectRoot,
|
|
99
|
-
publicPath: this.publicPath
|
|
128
|
+
publicPath: this.publicPath,
|
|
129
|
+
dynamicForm: this.dynamicForm,
|
|
130
|
+
dynamicFormRenderer: this.dynamicFormRenderer
|
|
100
131
|
});
|
|
101
132
|
this.contentRenderer.templateEngine = this.templateEngine;
|
|
102
133
|
this.searchConfig.jsonFieldsParser = this.templateEngine.jsonFieldsParser;
|
|
@@ -107,6 +138,9 @@ class Frontend
|
|
|
107
138
|
this.app.get(this.searchPath, async (req, res) => {
|
|
108
139
|
return await this.searchRequestHandler.handleSearchRequest(req, res);
|
|
109
140
|
});
|
|
141
|
+
this.app.post(this.dynamicFormPath, async (req, res) => {
|
|
142
|
+
return await this.dynamicFormRequestHandler.handleFormSubmission(req, res);
|
|
143
|
+
});
|
|
110
144
|
this.app.get('*', async (req, res) => {
|
|
111
145
|
return await this.handleRequest(req, res);
|
|
112
146
|
});
|
package/lib/installer.js
CHANGED
|
@@ -168,7 +168,8 @@ class Installer
|
|
|
168
168
|
'install-default-user': 'default-user.sql',
|
|
169
169
|
'install-default-homepage': 'default-homepage.sql',
|
|
170
170
|
'install-default-blocks': 'default-blocks.sql',
|
|
171
|
-
'install-entity-access': 'default-entity-access.sql'
|
|
171
|
+
'install-entity-access': 'default-entity-access.sql',
|
|
172
|
+
'install-dynamic-forms': 'default-forms.sql'
|
|
172
173
|
};
|
|
173
174
|
for(let checkboxName of Object.keys(executeFiles)){
|
|
174
175
|
let fileName = executeFiles[checkboxName];
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - CMS - FormsTransformer
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
8
|
+
|
|
9
|
+
class FormsTransformer
|
|
10
|
+
{
|
|
11
|
+
|
|
12
|
+
constructor(props)
|
|
13
|
+
{
|
|
14
|
+
this.renderEngine = sc.get(props, 'renderEngine', false);
|
|
15
|
+
this.getPartials = sc.get(props, 'getPartials', false);
|
|
16
|
+
this.processAllTemplateFunctions = sc.get(props, 'processAllTemplateFunctions', false);
|
|
17
|
+
this.dynamicForm = sc.get(props, 'dynamicForm', false);
|
|
18
|
+
this.dynamicFormRenderer = sc.get(props, 'dynamicFormRenderer', false);
|
|
19
|
+
this.events = sc.get(props, 'events', false);
|
|
20
|
+
if(!this.events){
|
|
21
|
+
Logger.error('EventsManager not provided to FormsTransformer - forms functionality disabled');
|
|
22
|
+
this.isDisabled = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async transform(template, domain, req, systemVariables, enhancedData = {})
|
|
27
|
+
{
|
|
28
|
+
if(this.isDisabled){
|
|
29
|
+
return template;
|
|
30
|
+
}
|
|
31
|
+
let processedTemplate = template;
|
|
32
|
+
let formTags = this.findAllFormTags(template);
|
|
33
|
+
for(let i = formTags.length - 1; i >= 0; i--){
|
|
34
|
+
let tag = formTags[i];
|
|
35
|
+
let formKey = sc.get(tag.attributes, 'key', '');
|
|
36
|
+
if(!formKey){
|
|
37
|
+
Logger.warning('cmsForm tag missing key attribute');
|
|
38
|
+
processedTemplate = processedTemplate.substring(0, tag.start)+''+processedTemplate.substring(tag.end);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
let formConfig = await this.dynamicForm.getFormConfig(formKey);
|
|
42
|
+
if(!formConfig){
|
|
43
|
+
Logger.warning('Form not found or disabled: '+formKey);
|
|
44
|
+
processedTemplate = processedTemplate.substring(0, tag.start)+''+processedTemplate.substring(tag.end);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
let fieldsToRender = this.parseFieldsFilter(tag.attributes, formConfig);
|
|
48
|
+
if(!sc.isArray(fieldsToRender) || 0 === fieldsToRender.length){
|
|
49
|
+
processedTemplate = processedTemplate.substring(0, tag.start)+''+processedTemplate.substring(tag.end);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
await this.events.emit('reldens.formsTransformer.beforeRender', {
|
|
53
|
+
formKey,
|
|
54
|
+
formConfig,
|
|
55
|
+
fieldsToRender,
|
|
56
|
+
formAttributes: tag.attributes,
|
|
57
|
+
domain,
|
|
58
|
+
req,
|
|
59
|
+
systemVariables,
|
|
60
|
+
enhancedData
|
|
61
|
+
});
|
|
62
|
+
let formContent = await this.dynamicFormRenderer.renderForm(
|
|
63
|
+
formConfig,
|
|
64
|
+
fieldsToRender,
|
|
65
|
+
domain,
|
|
66
|
+
req,
|
|
67
|
+
Object.assign({}, tag.attributes, {
|
|
68
|
+
successRedirect: sc.get(tag.attributes, 'successRedirect', req.path),
|
|
69
|
+
errorRedirect: sc.get(tag.attributes, 'errorRedirect', req.path)
|
|
70
|
+
}),
|
|
71
|
+
systemVariables,
|
|
72
|
+
enhancedData
|
|
73
|
+
);
|
|
74
|
+
await this.events.emit('reldens.formsTransformer.afterRender', {
|
|
75
|
+
formKey,
|
|
76
|
+
formConfig,
|
|
77
|
+
formContent,
|
|
78
|
+
domain,
|
|
79
|
+
req,
|
|
80
|
+
systemVariables,
|
|
81
|
+
enhancedData
|
|
82
|
+
});
|
|
83
|
+
processedTemplate = processedTemplate.substring(0, tag.start) +
|
|
84
|
+
formContent +
|
|
85
|
+
processedTemplate.substring(tag.end);
|
|
86
|
+
}
|
|
87
|
+
return processedTemplate;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
findAllFormTags(template)
|
|
91
|
+
{
|
|
92
|
+
let formTags = [];
|
|
93
|
+
let pos = 0;
|
|
94
|
+
let cmsForm = '<cmsForm';
|
|
95
|
+
for(let tagStart = template.indexOf(cmsForm, pos); -1 !== tagStart; tagStart=template.indexOf(cmsForm, pos)){
|
|
96
|
+
let tagEnd = this.findFormTagEnd(template, tagStart);
|
|
97
|
+
if(-1 === tagEnd){
|
|
98
|
+
pos = tagStart + cmsForm.length;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
let fullTag = template.substring(tagStart, tagEnd);
|
|
102
|
+
let attributes = this.parseFormAttributes(fullTag);
|
|
103
|
+
formTags.push({
|
|
104
|
+
start: tagStart,
|
|
105
|
+
end: tagEnd,
|
|
106
|
+
attributes: attributes,
|
|
107
|
+
fullTag: fullTag
|
|
108
|
+
});
|
|
109
|
+
pos = tagEnd;
|
|
110
|
+
}
|
|
111
|
+
return formTags;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
findFormTagEnd(template, tagStart)
|
|
115
|
+
{
|
|
116
|
+
let inQuotes = false;
|
|
117
|
+
let quoteChar = '';
|
|
118
|
+
let selfCloseTag = '/>';
|
|
119
|
+
let openCloseTag = '</cmsForm>';
|
|
120
|
+
for(let i = tagStart; i < template.length; i++){
|
|
121
|
+
let char = template[i];
|
|
122
|
+
if(!inQuotes && ('"' === char || "'" === char)){
|
|
123
|
+
inQuotes = true;
|
|
124
|
+
quoteChar = char;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if(inQuotes && char === quoteChar && '\\' !== template[i - 1]){
|
|
128
|
+
inQuotes = false;
|
|
129
|
+
quoteChar = '';
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if(!inQuotes){
|
|
133
|
+
if(template.substring(i, i + selfCloseTag.length) === selfCloseTag){
|
|
134
|
+
return i + selfCloseTag.length;
|
|
135
|
+
}
|
|
136
|
+
if('>' === char){
|
|
137
|
+
let closeIndex = template.indexOf(openCloseTag, i);
|
|
138
|
+
if(-1 !== closeIndex){
|
|
139
|
+
return closeIndex + openCloseTag.length;
|
|
140
|
+
}
|
|
141
|
+
return i + 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
parseFormAttributes(fullTag)
|
|
149
|
+
{
|
|
150
|
+
let attributes = {};
|
|
151
|
+
let valueRegex = /(\w+)=(['"])((?:(?!\2)[^\\]|\\.)*)(\2)/g;
|
|
152
|
+
for(let match of fullTag.matchAll(valueRegex)){
|
|
153
|
+
attributes[match[1]] = match[3];
|
|
154
|
+
}
|
|
155
|
+
let booleanRegex = /\b(\w+)(?!\s*=)/g;
|
|
156
|
+
for(let match of fullTag.matchAll(booleanRegex)){
|
|
157
|
+
if(!sc.hasOwn(attributes, match[1]) && 'cmsForm' !== match[1]){
|
|
158
|
+
attributes[match[1]] = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return attributes;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
parseFieldsFilter(attributes, formConfig)
|
|
165
|
+
{
|
|
166
|
+
let fieldsFilter = sc.get(attributes, 'fields', '');
|
|
167
|
+
if(!fieldsFilter){
|
|
168
|
+
return formConfig.fields_schema;
|
|
169
|
+
}
|
|
170
|
+
let fieldsSchema = sc.isString(formConfig.fields_schema) ? sc.toJson(formConfig.fields_schema) : formConfig.fields_schema;
|
|
171
|
+
if(!sc.isArray(fieldsSchema)){
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
let requestedFields = fieldsFilter.split(',').map(f => f.trim()).filter(f => '' !== f);
|
|
175
|
+
let filteredFields = [];
|
|
176
|
+
for(let fieldName of requestedFields){
|
|
177
|
+
let field = sc.fetchByProperty(fieldsSchema, 'name', fieldName);
|
|
178
|
+
if(field){
|
|
179
|
+
filteredFields.push(field);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return filteredFields;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports.FormsTransformer = FormsTransformer;
|
package/lib/template-engine.js
CHANGED
|
@@ -9,6 +9,7 @@ const { EntitiesTransformer } = require('./template-engine/entities-transformer'
|
|
|
9
9
|
const { CollectionsTransformer } = require('./template-engine/collections-transformer');
|
|
10
10
|
const { CollectionsSingleTransformer } = require('./template-engine/collections-single-transformer');
|
|
11
11
|
const { PartialsTransformer } = require('./template-engine/partials-transformer');
|
|
12
|
+
const { FormsTransformer } = require('./template-engine/forms-transformer');
|
|
12
13
|
const { UrlTransformer } = require('./template-engine/url-transformer');
|
|
13
14
|
const { AssetTransformer } = require('./template-engine/asset-transformer');
|
|
14
15
|
const { DateTransformer } = require('./template-engine/date-transformer');
|
|
@@ -28,6 +29,8 @@ class TemplateEngine
|
|
|
28
29
|
this.defaultDomain = sc.get(props, 'defaultDomain', 'default');
|
|
29
30
|
this.projectRoot = sc.get(props, 'projectRoot', './');
|
|
30
31
|
this.publicPath = sc.get(props, 'publicPath', './public');
|
|
32
|
+
this.dynamicForm = sc.get(props, 'dynamicForm', false);
|
|
33
|
+
this.dynamicFormRenderer = sc.get(props, 'dynamicFormRenderer', false);
|
|
31
34
|
this.jsonFieldsParser = new JsonFieldsParser({entitiesConfig: sc.get(props, 'entitiesConfig', {})});
|
|
32
35
|
this.systemVariablesProvider = new SystemVariablesProvider({
|
|
33
36
|
defaultDomain: this.defaultDomain,
|
|
@@ -62,6 +65,17 @@ class TemplateEngine
|
|
|
62
65
|
getPartials: this.getPartials,
|
|
63
66
|
processAllTemplateFunctions: this.processAllTemplateFunctions.bind(this)
|
|
64
67
|
});
|
|
68
|
+
this.formsTransformer = false;
|
|
69
|
+
if(this.dynamicForm && this.dynamicFormRenderer){
|
|
70
|
+
this.formsTransformer = new FormsTransformer({
|
|
71
|
+
renderEngine: this.renderEngine,
|
|
72
|
+
getPartials: this.getPartials,
|
|
73
|
+
processAllTemplateFunctions: this.processAllTemplateFunctions.bind(this),
|
|
74
|
+
dynamicForm: this.dynamicForm,
|
|
75
|
+
dynamicFormRenderer: this.dynamicFormRenderer,
|
|
76
|
+
events: this.events
|
|
77
|
+
});
|
|
78
|
+
}
|
|
65
79
|
this.urlTransformer = new UrlTransformer();
|
|
66
80
|
this.assetTransformer = new AssetTransformer();
|
|
67
81
|
this.dateTransformer = new DateTransformer({
|
|
@@ -82,6 +96,9 @@ class TemplateEngine
|
|
|
82
96
|
this.dateTransformer,
|
|
83
97
|
this.translateTransformer
|
|
84
98
|
];
|
|
99
|
+
if(this.formsTransformer){
|
|
100
|
+
this.transformers.push(this.formsTransformer);
|
|
101
|
+
}
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
async processAllTemplateFunctions(template, domain, req, systemVariables, enhancedData = {})
|
package/lib/templates-list.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
-- Default CMS Forms
|
|
3
|
+
|
|
4
|
+
CREATE TABLE `cms_forms` (
|
|
5
|
+
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
6
|
+
`form_key` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_0900_ai_ci',
|
|
7
|
+
`fields_schema` JSON NOT NULL,
|
|
8
|
+
`enabled` TINYINT UNSIGNED NOT NULL DEFAULT '0',
|
|
9
|
+
`created_at` TIMESTAMP NOT NULL DEFAULT (NOW()),
|
|
10
|
+
`updated_at` TIMESTAMP NOT NULL DEFAULT (NOW()) ON UPDATE CURRENT_TIMESTAMP,
|
|
11
|
+
PRIMARY KEY (`id`) USING BTREE,
|
|
12
|
+
UNIQUE INDEX `form_key` (`form_key`) USING BTREE
|
|
13
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
CREATE TABLE `cms_forms_submitted` (
|
|
17
|
+
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
18
|
+
`form_id` INT UNSIGNED NOT NULL,
|
|
19
|
+
`submitted_values` JSON NOT NULL,
|
|
20
|
+
PRIMARY KEY (`id`) USING BTREE,
|
|
21
|
+
INDEX `FK__cms_forms` (`form_id`) USING BTREE
|
|
22
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reldens/cms",
|
|
3
3
|
"scope": "@reldens",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.23.0",
|
|
5
5
|
"description": "Reldens - CMS",
|
|
6
6
|
"author": "Damian A. Pastorini",
|
|
7
7
|
"license": "MIT",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"url": "https://github.com/damian-pastorini/reldens-cms/issues"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@reldens/server-utils": "^0.
|
|
37
|
-
"@reldens/storage": "^0.
|
|
38
|
-
"@reldens/utils": "^0.
|
|
39
|
-
"dotenv": "^17.2.
|
|
36
|
+
"@reldens/server-utils": "^0.24.0",
|
|
37
|
+
"@reldens/storage": "^0.65.0",
|
|
38
|
+
"@reldens/utils": "^0.52.0",
|
|
39
|
+
"dotenv": "^17.2.1",
|
|
40
40
|
"mustache": "^4.2.0"
|
|
41
41
|
}
|
|
42
42
|
}
|