@reldens/cms 0.20.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 +648 -12
- package/admin/reldens-admin-client.css +77 -141
- package/admin/reldens-admin-client.js +108 -133
- package/admin/templates/clear-all-cache-button.html +7 -7
- package/admin/templates/edit.html +7 -0
- package/admin/templates/fields/view/audio.html +7 -0
- package/admin/templates/fields/view/audios.html +8 -0
- package/admin/templates/layout.html +15 -9
- package/admin/templates/list-content.html +4 -2
- package/admin/templates/list.html +24 -8
- package/admin/templates/view.html +21 -0
- package/install/index.html +4 -0
- package/lib/admin-manager/admin-filters-manager.js +177 -0
- package/lib/admin-manager/contents-builder.js +1 -0
- package/lib/admin-manager/default-translations.js +38 -0
- package/lib/admin-manager/router-contents.js +64 -52
- package/lib/admin-manager/router.js +19 -0
- 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/content-renderer.js +178 -0
- package/lib/frontend/entity-access-manager.js +63 -0
- package/lib/frontend/request-processor.js +128 -0
- package/lib/frontend/response-manager.js +54 -0
- package/lib/frontend/template-cache.js +102 -0
- package/lib/frontend/template-resolver.js +111 -0
- package/lib/frontend.js +122 -629
- package/lib/installer.js +2 -1
- package/lib/manager.js +25 -12
- package/lib/search-renderer.js +15 -7
- package/lib/search-request-handler.js +67 -0
- package/lib/search.js +13 -1
- package/lib/template-engine/collections-single-transformer.js +11 -5
- package/lib/template-engine/collections-transformer.js +47 -34
- package/lib/template-engine/entities-transformer.js +3 -2
- package/lib/template-engine/forms-transformer.js +187 -0
- package/lib/template-engine/partials-transformer.js +5 -6
- package/lib/template-engine/system-variables-provider.js +4 -1
- package/lib/template-engine.js +28 -5
- package/lib/template-reloader.js +307 -0
- package/lib/templates-list.js +2 -0
- package/migrations/default-forms.sql +22 -0
- package/package.json +5 -5
- package/templates/{browserconfig.xml → assets/favicons/default/browserconfig.xml} +1 -1
- package/templates/assets/favicons/default/favicon.ico +0 -0
- package/templates/{site.webmanifest → assets/favicons/default/site.webmanifest} +3 -3
- 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
- package/templates/js/functions.js +144 -0
- package/templates/js/scripts.js +5 -0
- package/templates/page.html +11 -5
- package/templates/partials/pagedCollection.html +1 -1
- package/lib/admin-translations.js +0 -56
- package/templates/favicon.ico +0 -0
- /package/templates/assets/favicons/{android-icon-144x144.png → default/android-icon-144x144.png} +0 -0
- /package/templates/assets/favicons/{android-icon-192x192.png → default/android-icon-192x192.png} +0 -0
- /package/templates/assets/favicons/{android-icon-512x512.png → default/android-icon-512x512.png} +0 -0
- /package/templates/assets/favicons/{apple-touch-icon.png → default/apple-touch-icon.png} +0 -0
- /package/templates/assets/favicons/{favicon-16x16.png → default/favicon-16x16.png} +0 -0
- /package/templates/assets/favicons/{favicon-32x32.png → default/favicon-32x32.png} +0 -0
- /package/templates/assets/favicons/{mstile-150x150.png → default/mstile-150x150.png} +0 -0
- /package/templates/assets/favicons/{safari-pinned-tab.svg → default/safari-pinned-tab.svg} +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - CMS - DynamicFormRenderer
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
8
|
+
const { FileHandler } = require('@reldens/server-utils');
|
|
9
|
+
|
|
10
|
+
class DynamicFormRenderer
|
|
11
|
+
{
|
|
12
|
+
|
|
13
|
+
constructor(props)
|
|
14
|
+
{
|
|
15
|
+
this.renderEngine = sc.get(props, 'renderEngine', false);
|
|
16
|
+
this.getPartials = sc.get(props, 'getPartials', false);
|
|
17
|
+
this.projectRoot = sc.get(props, 'projectRoot', './');
|
|
18
|
+
this.templatesPath = FileHandler.joinPaths(this.projectRoot, 'templates');
|
|
19
|
+
this.defaultDomain = sc.get(props, 'defaultDomain', 'default');
|
|
20
|
+
this.loadedTemplates = {};
|
|
21
|
+
this.events = sc.get(props, 'events', false);
|
|
22
|
+
if(!this.events){
|
|
23
|
+
Logger.error('EventsManager not provided to DynamicFormRenderer - forms functionality disabled');
|
|
24
|
+
this.isDisabled = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async renderForm(formConfig, fieldsToRender, domain, req, attributes = {}, systemVariables = {}, enhancedData = {})
|
|
29
|
+
{
|
|
30
|
+
if(this.isDisabled){
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
await this.events.emit('reldens.dynamicFormRenderer.beforeFieldsRender', {
|
|
34
|
+
formConfig,
|
|
35
|
+
fieldsToRender,
|
|
36
|
+
domain,
|
|
37
|
+
req,
|
|
38
|
+
attributes,
|
|
39
|
+
systemVariables,
|
|
40
|
+
enhancedData
|
|
41
|
+
});
|
|
42
|
+
let formFields = await this.renderFormFields(fieldsToRender, domain, req);
|
|
43
|
+
await this.events.emit('reldens.dynamicFormRenderer.afterFieldsRender', {
|
|
44
|
+
formConfig,
|
|
45
|
+
fieldsToRender,
|
|
46
|
+
formFields,
|
|
47
|
+
domain,
|
|
48
|
+
req,
|
|
49
|
+
attributes,
|
|
50
|
+
systemVariables,
|
|
51
|
+
enhancedData
|
|
52
|
+
});
|
|
53
|
+
let formTemplate = await this.loadFormTemplate('form', domain);
|
|
54
|
+
if(!formTemplate){
|
|
55
|
+
Logger.error('Form template not found');
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
let messageData = this.parseFormMessages(req, attributes);
|
|
59
|
+
return this.renderEngine.render(formTemplate, Object.assign({}, enhancedData, {
|
|
60
|
+
formKey: formConfig.form_key,
|
|
61
|
+
formFields,
|
|
62
|
+
submitUrl: sc.get(attributes, 'submitUrl', '/dynamic-form'),
|
|
63
|
+
successRedirect: sc.get(attributes, 'successRedirect', '/'),
|
|
64
|
+
errorRedirect: sc.get(attributes, 'errorRedirect', '/'),
|
|
65
|
+
honeypotFieldName: sc.get(attributes, 'honeypotFieldName', 'website_url'),
|
|
66
|
+
submitButtonText: sc.get(attributes, 'submitButtonText', 'Submit'),
|
|
67
|
+
cssClass: sc.get(attributes, 'cssClass', 'dynamic-form'),
|
|
68
|
+
showSuccessMessage: messageData.showSuccessMessage,
|
|
69
|
+
successMessage: messageData.successMessage,
|
|
70
|
+
showErrorMessage: messageData.showErrorMessage,
|
|
71
|
+
errorMessage: messageData.errorMessage,
|
|
72
|
+
systemVariables
|
|
73
|
+
}), this.getPartialsForDomain(domain));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
parseFormMessages(req, attributes)
|
|
77
|
+
{
|
|
78
|
+
let queryParams = sc.get(req, 'query', {});
|
|
79
|
+
let formSuccess = sc.get(queryParams, 'form-success', '');
|
|
80
|
+
let formError = sc.get(queryParams, 'form-error', '');
|
|
81
|
+
let formKey = sc.get(queryParams, 'form-key', '');
|
|
82
|
+
let showSuccessMessage = '1' === formSuccess && '' !== formKey;
|
|
83
|
+
let showErrorMessage = '' !== formError && '' !== formKey;
|
|
84
|
+
let successMessage = sc.get(attributes, 'successMessage', 'Form submitted successfully!');
|
|
85
|
+
let errorMessage = sc.get(attributes, 'errorMessage', formError || 'There was an error submitting the form.');
|
|
86
|
+
return {
|
|
87
|
+
showSuccessMessage,
|
|
88
|
+
successMessage,
|
|
89
|
+
showErrorMessage,
|
|
90
|
+
errorMessage
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async renderFormFields(fieldsToRender, domain, req, submittedValues = {}, errors = {})
|
|
95
|
+
{
|
|
96
|
+
if(!sc.isArray(fieldsToRender) || 0 === fieldsToRender.length){
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
let renderedFields = '';
|
|
100
|
+
for(let field of fieldsToRender){
|
|
101
|
+
if(!sc.isObject(field)){
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let fieldHtml = await this.renderFormField(field, domain, submittedValues, errors);
|
|
105
|
+
if(fieldHtml){
|
|
106
|
+
renderedFields += fieldHtml;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return renderedFields;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async renderFormField(field, domain, submittedValues = {}, errors = {})
|
|
113
|
+
{
|
|
114
|
+
let fieldType = sc.get(field, 'type', 'text');
|
|
115
|
+
let fieldTemplate = await this.loadFormTemplate('field_'+fieldType, domain);
|
|
116
|
+
if(!fieldTemplate){
|
|
117
|
+
fieldTemplate = await this.loadFormTemplate('field_text', domain);
|
|
118
|
+
}
|
|
119
|
+
if(!fieldTemplate){
|
|
120
|
+
Logger.error('Field template not found for type: '+fieldType);
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
return this.renderEngine.render(
|
|
124
|
+
fieldTemplate,
|
|
125
|
+
this.buildFieldTemplateData(field, submittedValues, errors),
|
|
126
|
+
this.getPartialsForDomain(domain)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
buildFieldTemplateData(field, submittedValues = {}, errors = {})
|
|
131
|
+
{
|
|
132
|
+
let fieldName = sc.get(field, 'name', '');
|
|
133
|
+
let fieldType = sc.get(field, 'type', 'text');
|
|
134
|
+
let fieldLabel = sc.get(field, 'label', fieldName);
|
|
135
|
+
let fieldValue = sc.get(submittedValues, fieldName, sc.get(field, 'defaultValue', ''));
|
|
136
|
+
let isRequired = sc.get(field, 'required', false);
|
|
137
|
+
let fieldError = sc.get(errors, fieldName, '');
|
|
138
|
+
let options = sc.get(field, 'options', []);
|
|
139
|
+
if(sc.isArray(options)){
|
|
140
|
+
options = options.map(option => {
|
|
141
|
+
if(sc.isObject(option)){
|
|
142
|
+
return {
|
|
143
|
+
value: sc.get(option, 'value', ''),
|
|
144
|
+
label: sc.get(option, 'label', sc.get(option, 'value', '')),
|
|
145
|
+
selected: sc.get(option, 'value', '') === fieldValue
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
value: option,
|
|
150
|
+
label: option,
|
|
151
|
+
selected: option === fieldValue
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
field,
|
|
157
|
+
fieldName,
|
|
158
|
+
fieldType,
|
|
159
|
+
fieldLabel,
|
|
160
|
+
fieldValue,
|
|
161
|
+
isRequired,
|
|
162
|
+
fieldError,
|
|
163
|
+
hasError: '' !== fieldError,
|
|
164
|
+
requiredClass: isRequired ? 'required' : '',
|
|
165
|
+
errorClass: '' !== fieldError ? 'error' : '',
|
|
166
|
+
options,
|
|
167
|
+
placeholder: sc.get(field, 'placeholder', ''),
|
|
168
|
+
helpText: sc.get(field, 'helpText', ''),
|
|
169
|
+
maxLength: sc.get(field, 'maxLength', ''),
|
|
170
|
+
pattern: sc.get(field, 'pattern', ''),
|
|
171
|
+
min: sc.get(field, 'min', ''),
|
|
172
|
+
max: sc.get(field, 'max', ''),
|
|
173
|
+
step: sc.get(field, 'step', '')
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async loadFormTemplate(templateName, domain)
|
|
178
|
+
{
|
|
179
|
+
let cacheKey = templateName+'_'+domain;
|
|
180
|
+
if(sc.hasOwn(this.loadedTemplates, cacheKey)){
|
|
181
|
+
return this.loadedTemplates[cacheKey];
|
|
182
|
+
}
|
|
183
|
+
let templatePath = this.findFormTemplate(templateName, domain);
|
|
184
|
+
if(!templatePath){
|
|
185
|
+
Logger.warning('Form template not found: '+templateName+' for domain: '+domain);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
let templateContent = await FileHandler.readFile(templatePath);
|
|
189
|
+
if(!templateContent){
|
|
190
|
+
Logger.error('Failed to read form template: '+templatePath);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
this.loadedTemplates[cacheKey] = templateContent;
|
|
194
|
+
return templateContent;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
findFormTemplate(templateName, domain)
|
|
198
|
+
{
|
|
199
|
+
let filename = templateName+'.html';
|
|
200
|
+
let domainTemplatePath = FileHandler.joinPaths(this.templatesPath, 'domains', domain, 'cms_forms', filename);
|
|
201
|
+
if(FileHandler.exists(domainTemplatePath)){
|
|
202
|
+
return domainTemplatePath;
|
|
203
|
+
}
|
|
204
|
+
if(this.defaultDomain && domain !== this.defaultDomain){
|
|
205
|
+
let defaultDomainPath = FileHandler.joinPaths(this.templatesPath, 'domains', this.defaultDomain, 'cms_forms', filename);
|
|
206
|
+
if(FileHandler.exists(defaultDomainPath)){
|
|
207
|
+
return defaultDomainPath;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let rootTemplatePath = FileHandler.joinPaths(this.templatesPath, 'cms_forms', filename);
|
|
211
|
+
if(FileHandler.exists(rootTemplatePath)){
|
|
212
|
+
return rootTemplatePath;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getPartialsForDomain(domain)
|
|
218
|
+
{
|
|
219
|
+
if(!this.getPartials){
|
|
220
|
+
Logger.error('getPartials function not provided to DynamicFormRenderer');
|
|
221
|
+
return {};
|
|
222
|
+
}
|
|
223
|
+
return this.getPartials(domain);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports.DynamicFormRenderer = DynamicFormRenderer;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - CMS - DynamicFormRequestHandler
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
8
|
+
|
|
9
|
+
class DynamicFormRequestHandler
|
|
10
|
+
{
|
|
11
|
+
|
|
12
|
+
constructor(props)
|
|
13
|
+
{
|
|
14
|
+
this.dynamicForm = sc.get(props, 'dynamicForm', false);
|
|
15
|
+
this.contentRenderer = sc.get(props, 'contentRenderer', false);
|
|
16
|
+
this.requestProcessor = sc.get(props, 'requestProcessor', false);
|
|
17
|
+
this.cacheManager = sc.get(props, 'cacheManager', false);
|
|
18
|
+
this.enableJsonResponse = sc.get(props, 'enableJsonResponse', false);
|
|
19
|
+
this.events = sc.get(props, 'events', false);
|
|
20
|
+
if(!this.events){
|
|
21
|
+
Logger.error('EventsManager not provided to DynamicFormRequestHandler - forms functionality disabled');
|
|
22
|
+
this.isDisabled = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async handleFormSubmission(req, res)
|
|
27
|
+
{
|
|
28
|
+
if(this.isDisabled){
|
|
29
|
+
return this.handleBadRequest(res, 'Forms functionality disabled');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
let formKey = sc.get(req.body, 'formKey', '');
|
|
33
|
+
let submittedValues = sc.get(req.body, 'submittedValues', {});
|
|
34
|
+
if(!formKey || !submittedValues){
|
|
35
|
+
return this.handleBadRequest(res, 'Missing formKey or submittedValues');
|
|
36
|
+
}
|
|
37
|
+
await this.events.emit('reldens.dynamicFormRequestHandler.beforeValidation', {
|
|
38
|
+
formKey,
|
|
39
|
+
submittedValues,
|
|
40
|
+
req,
|
|
41
|
+
res
|
|
42
|
+
});
|
|
43
|
+
let validation = await this.dynamicForm.validateFormSubmission(formKey, submittedValues, req);
|
|
44
|
+
if(!validation.isValid){
|
|
45
|
+
return this.handleValidationError(req, res, validation.error, formKey);
|
|
46
|
+
}
|
|
47
|
+
let preparedValues = this.dynamicForm.prepareSubmittedValues(
|
|
48
|
+
submittedValues,
|
|
49
|
+
validation.formConfig.fields_schema
|
|
50
|
+
);
|
|
51
|
+
await this.events.emit('reldens.dynamicFormRequestHandler.beforeSave', {
|
|
52
|
+
formKey,
|
|
53
|
+
preparedValues,
|
|
54
|
+
formConfig: validation.formConfig,
|
|
55
|
+
req,
|
|
56
|
+
res
|
|
57
|
+
});
|
|
58
|
+
let submissionResult = await this.dynamicForm.saveFormSubmission(
|
|
59
|
+
validation.formConfig,
|
|
60
|
+
preparedValues
|
|
61
|
+
);
|
|
62
|
+
if(!submissionResult){
|
|
63
|
+
return this.handleValidationError(req, res, 'Failed to save form submission', formKey);
|
|
64
|
+
}
|
|
65
|
+
await this.events.emit('reldens.dynamicFormRequestHandler.afterSave', {
|
|
66
|
+
formKey,
|
|
67
|
+
submissionResult,
|
|
68
|
+
formConfig: validation.formConfig,
|
|
69
|
+
req,
|
|
70
|
+
res
|
|
71
|
+
});
|
|
72
|
+
return this.handleSuccessResponse(req, res, formKey, submissionResult);
|
|
73
|
+
} catch(error) {
|
|
74
|
+
Logger.error('Form submission handling error: '+error.message);
|
|
75
|
+
return this.handleBadRequest(res, 'Internal server error');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
handleBadRequest(res, message)
|
|
80
|
+
{
|
|
81
|
+
if(this.enableJsonResponse){
|
|
82
|
+
return res.status(400).json({
|
|
83
|
+
success: false,
|
|
84
|
+
error: message
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return res.status(400).send('Bad Request: '+message);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
handleValidationError(req, res, error, formKey)
|
|
91
|
+
{
|
|
92
|
+
if(this.enableJsonResponse){
|
|
93
|
+
return res.status(400).json({
|
|
94
|
+
success: false,
|
|
95
|
+
error: error,
|
|
96
|
+
formKey: formKey
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return res.redirect(this.buildErrorRedirectPath(req, error, formKey));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
handleSuccessResponse(req, res, formKey, submissionResult)
|
|
103
|
+
{
|
|
104
|
+
if(this.enableJsonResponse){
|
|
105
|
+
return res.json({
|
|
106
|
+
success: true,
|
|
107
|
+
message: 'Form submitted successfully',
|
|
108
|
+
formKey: formKey,
|
|
109
|
+
submissionId: submissionResult.id
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return res.redirect(this.buildSuccessRedirectPath(sc.get(req.body, 'successRedirect', '/'), formKey));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
buildErrorRedirectPath(req, error, formKey)
|
|
116
|
+
{
|
|
117
|
+
let baseRedirect = sc.get(req.body, 'errorRedirect', '/');
|
|
118
|
+
let finalRedirect = sc.get(req.headers, 'referer', baseRedirect) || baseRedirect;
|
|
119
|
+
return finalRedirect
|
|
120
|
+
+(finalRedirect.includes('?') ? '&' : '?')
|
|
121
|
+
+'form-error='+encodeURIComponent(error)
|
|
122
|
+
+'&form-key='+encodeURIComponent(formKey);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
buildSuccessRedirectPath(successRedirect, formKey)
|
|
126
|
+
{
|
|
127
|
+
return successRedirect
|
|
128
|
+
+(successRedirect.includes('?') ? '&' : '?')
|
|
129
|
+
+'form-success=1'
|
|
130
|
+
+'&form-key='+encodeURIComponent(formKey);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports.DynamicFormRequestHandler = DynamicFormRequestHandler;
|
|
@@ -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;
|