odac 1.4.12 → 1.4.13
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/CHANGELOG.md +12 -0
- package/client/odac.js +22 -18
- package/package.json +1 -1
- package/src/View/Form.js +314 -514
- package/src/View.js +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
### 🛠️ Fixes & Improvements
|
|
2
|
+
|
|
3
|
+
- **form:** enhance form handling with metadata and improved parsing logic
|
|
4
|
+
- **form:** implement token rotation on successful form submissions
|
|
5
|
+
- **view:** prevent script injection and handle escaped quotes in form configs
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
12
|
+
|
|
1
13
|
### 📚 Documentation
|
|
2
14
|
|
|
3
15
|
- refactor database configuration to a unified structure and document multi-connection support
|
package/client/odac.js
CHANGED
|
@@ -541,6 +541,28 @@ if (typeof window !== 'undefined') {
|
|
|
541
541
|
cache: cache,
|
|
542
542
|
success: data => {
|
|
543
543
|
if (!data.result) return false
|
|
544
|
+
|
|
545
|
+
// Token rotation must always apply on success, independent of messages config.
|
|
546
|
+
// Server has already cleared the old session entry; if we skip this update
|
|
547
|
+
// the next submit sends a stale token and gets "Form session expired".
|
|
548
|
+
if (data.result.success && data.result._token) {
|
|
549
|
+
const tokenInput = formElement.querySelector('input[name="_odac_form_token"]')
|
|
550
|
+
if (tokenInput) tokenInput.value = data.result._token
|
|
551
|
+
|
|
552
|
+
const formTokenAttr = formElement.getAttribute('data-odac-form')
|
|
553
|
+
if (formTokenAttr) {
|
|
554
|
+
formElement.setAttribute('data-odac-form', data.result._token)
|
|
555
|
+
if (!formElement.matches(formSelector)) {
|
|
556
|
+
if (this.#formSubmitHandlers.has(formSelector)) {
|
|
557
|
+
document.removeEventListener('submit', this.#formSubmitHandlers.get(formSelector))
|
|
558
|
+
this.#formSubmitHandlers.delete(formSelector)
|
|
559
|
+
}
|
|
560
|
+
const newObj = {...obj, form: `form[data-odac-form="${data.result._token}"]`}
|
|
561
|
+
this.form(newObj, callback)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
544
566
|
if (obj.messages == undefined || obj.messages) {
|
|
545
567
|
if (data.result.success && (obj.messages == undefined || obj.messages.includes('success') || obj.messages == true)) {
|
|
546
568
|
const successEl = formElement.querySelector('*[odac-form-success]')
|
|
@@ -554,24 +576,6 @@ if (typeof window !== 'undefined') {
|
|
|
554
576
|
formElement.appendChild(span)
|
|
555
577
|
}
|
|
556
578
|
|
|
557
|
-
if (data.result._token) {
|
|
558
|
-
const tokenInput = formElement.querySelector('input[name="_odac_form_token"]')
|
|
559
|
-
if (tokenInput) tokenInput.value = data.result._token
|
|
560
|
-
|
|
561
|
-
const formTokenAttr = formElement.getAttribute('data-odac-form')
|
|
562
|
-
if (formTokenAttr) {
|
|
563
|
-
formElement.setAttribute('data-odac-form', data.result._token)
|
|
564
|
-
if (!formElement.matches(formSelector)) {
|
|
565
|
-
if (this.#formSubmitHandlers.has(formSelector)) {
|
|
566
|
-
document.removeEventListener('submit', this.#formSubmitHandlers.get(formSelector))
|
|
567
|
-
this.#formSubmitHandlers.delete(formSelector)
|
|
568
|
-
}
|
|
569
|
-
const newObj = {...obj, form: `form[data-odac-form="${data.result._token}"]`}
|
|
570
|
-
this.form(newObj, callback)
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
579
|
if (obj.clear !== false && formElement.getAttribute('clear') !== 'false' && !data.result.redirect) {
|
|
576
580
|
formElement
|
|
577
581
|
.querySelectorAll(
|
package/package.json
CHANGED
package/src/View/Form.js
CHANGED
|
@@ -3,6 +3,49 @@ const nodeCrypto = require('crypto')
|
|
|
3
3
|
class Form {
|
|
4
4
|
static FORM_TYPES = ['register', 'login', 'magic-login', 'form']
|
|
5
5
|
|
|
6
|
+
static FORM_META = {
|
|
7
|
+
form: {
|
|
8
|
+
cssClass: 'odac-custom-form',
|
|
9
|
+
dataAttr: 'data-odac-form',
|
|
10
|
+
tokenInputName: '_odac_form_token',
|
|
11
|
+
action: '/_odac/form',
|
|
12
|
+
defaultSubmitText: 'Submit',
|
|
13
|
+
defaultSubmitLoading: 'Processing...',
|
|
14
|
+
storageKey: 'customForms',
|
|
15
|
+
sessionKeyPrefix: '_custom_form_'
|
|
16
|
+
},
|
|
17
|
+
register: {
|
|
18
|
+
cssClass: 'odac-register-form',
|
|
19
|
+
dataAttr: 'data-odac-register',
|
|
20
|
+
tokenInputName: '_odac_register_token',
|
|
21
|
+
action: '/_odac/register',
|
|
22
|
+
defaultSubmitText: 'Register',
|
|
23
|
+
defaultSubmitLoading: 'Processing...',
|
|
24
|
+
storageKey: 'registerForms',
|
|
25
|
+
sessionKeyPrefix: '_register_form_'
|
|
26
|
+
},
|
|
27
|
+
login: {
|
|
28
|
+
cssClass: 'odac-login-form',
|
|
29
|
+
dataAttr: 'data-odac-login',
|
|
30
|
+
tokenInputName: '_odac_login_token',
|
|
31
|
+
action: '/_odac/login',
|
|
32
|
+
defaultSubmitText: 'Login',
|
|
33
|
+
defaultSubmitLoading: 'Logging in...',
|
|
34
|
+
storageKey: 'loginForms',
|
|
35
|
+
sessionKeyPrefix: '_login_form_'
|
|
36
|
+
},
|
|
37
|
+
'magic-login': {
|
|
38
|
+
cssClass: 'odac-magic-login-form',
|
|
39
|
+
dataAttr: 'data-odac-magic-login',
|
|
40
|
+
tokenInputName: '_odac_magic_login_token',
|
|
41
|
+
action: '/_odac/magic-login',
|
|
42
|
+
defaultSubmitText: 'Send Magic Link',
|
|
43
|
+
defaultSubmitLoading: 'Sending...',
|
|
44
|
+
storageKey: 'magicLoginForms',
|
|
45
|
+
sessionKeyPrefix: '_magic_login_form_'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
6
49
|
static escapeHtml(value) {
|
|
7
50
|
if (value === null || value === undefined) return ''
|
|
8
51
|
const map = {
|
|
@@ -22,70 +65,137 @@ class Form {
|
|
|
22
65
|
return content
|
|
23
66
|
}
|
|
24
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Compile-time transform for <odac:{type}>...</odac:{type}> blocks.
|
|
70
|
+
*
|
|
71
|
+
* Emits two <script:odac> runtime hooks (openForm / closeForm) with the
|
|
72
|
+
* form body kept inline between them. This preserves the view engine
|
|
73
|
+
* pipeline for inner content — <odac:if>, <odac:for>, {{ }} interpolation,
|
|
74
|
+
* etc. all keep working inside forms instead of being frozen into a JSON
|
|
75
|
+
* string blob and then mangled by later passes.
|
|
76
|
+
*/
|
|
25
77
|
static parseFormType(content, Odac, type) {
|
|
78
|
+
const meta = this.FORM_META[type]
|
|
26
79
|
const regex = new RegExp(`<odac:${type}[\\s\\S]*?<\\/odac:${type}>`, 'g')
|
|
80
|
+
|
|
27
81
|
return content.replace(regex, match => {
|
|
28
82
|
const formConfig = this.extractConfig(match, null, type)
|
|
29
|
-
let configStr = JSON.stringify(formConfig)
|
|
30
|
-
let matchStr = JSON.stringify(match)
|
|
31
83
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
84
|
+
const openTagRegex = new RegExp(`^<odac:${type}[^>]*>`)
|
|
85
|
+
const closeTagRegex = new RegExp(`</odac:${type}>$`)
|
|
86
|
+
let innerContent = match.replace(openTagRegex, '').replace(closeTagRegex, '')
|
|
87
|
+
|
|
88
|
+
// Pre-render <odac:input> tags into <input>/<textarea>/<label> markup.
|
|
89
|
+
// Field values may contain {{ }} — those stay as-is here and get
|
|
90
|
+
// resolved by the view engine's {{ }} pass on the surrounding HTML.
|
|
91
|
+
innerContent = innerContent.replace(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g, fieldMatch => {
|
|
92
|
+
const field = this.parseInput(fieldMatch)
|
|
93
|
+
if (!field) return fieldMatch
|
|
94
|
+
return this.generateFieldHtml(field)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Pre-render <odac:submit>...</odac:submit> (or self-closing) into <button>
|
|
98
|
+
let submitRendered = false
|
|
99
|
+
innerContent = innerContent.replace(/<odac:submit([^>]*?)(?:\/>|>(.*?)<\/odac:submit>)/g, () => {
|
|
100
|
+
submitRendered = true
|
|
101
|
+
return this.generateSubmitButton(formConfig, meta)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// magic-login convenience: if the author didn't write any <odac:input>,
|
|
105
|
+
// append the default email field that extractConfig pushed into config.
|
|
106
|
+
if (type === 'magic-login' && !match.includes('<odac:input')) {
|
|
107
|
+
const emailField = formConfig.fields.find(f => f.name === 'email')
|
|
108
|
+
if (emailField) innerContent += '\n' + this.generateFieldHtml(emailField)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// magic-login convenience: if no <odac:submit> was rendered, add one.
|
|
112
|
+
if (type === 'magic-login' && !submitRendered) {
|
|
113
|
+
innerContent += '\n' + this.generateSubmitButton(formConfig, meta)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// <odac:set> is server-side only (sent into stored config) — strip from DOM.
|
|
117
|
+
innerContent = innerContent.replace(/<odac:set[^>]*\/?>/g, '')
|
|
118
|
+
|
|
119
|
+
// Serialize config; turn "{{ expr }}" string values into live (await expr)
|
|
120
|
+
// so dynamic config values are evaluated at request time, not compile time.
|
|
121
|
+
let configStr = JSON.stringify(formConfig).replace(/<\/script:odac/gi, '<\\/script:odac')
|
|
122
|
+
configStr = configStr.replace(/"\{\{([\s\S]*?)\}\}"/g, (_, expr) => `(await ${expr.replace(/\\"/g, '"')})`)
|
|
36
123
|
|
|
37
|
-
return
|
|
124
|
+
return (
|
|
125
|
+
`<script:odac>html += await Odac.View.Form.openForm(Odac, '${type}', ${configStr});</script:odac>` +
|
|
126
|
+
innerContent +
|
|
127
|
+
`<script:odac>html += await Odac.View.Form.closeForm();</script:odac>`
|
|
128
|
+
)
|
|
38
129
|
})
|
|
39
130
|
}
|
|
40
131
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
static async runtime(Odac, type, config, originalHtml) {
|
|
132
|
+
// - RUNTIME: emits the opening <form ...> + hidden token input.
|
|
133
|
+
// Generates a fresh CSRF token per render and persists the resolved
|
|
134
|
+
// config in the session under the type-specific key.
|
|
135
|
+
static async openForm(Odac, type, config) {
|
|
136
|
+
const meta = this.FORM_META[type]
|
|
47
137
|
const token = nodeCrypto.randomBytes(32).toString('hex')
|
|
48
138
|
config.token = token
|
|
49
|
-
|
|
50
139
|
this.storeConfig(token, config, Odac, type)
|
|
51
140
|
|
|
52
|
-
|
|
141
|
+
const method = (config.method || 'POST').toUpperCase()
|
|
142
|
+
let classes = meta.cssClass
|
|
143
|
+
if (config.class) classes += ' ' + config.class
|
|
144
|
+
|
|
145
|
+
let attrs = `class="${this.escapeHtml(classes)}"`
|
|
146
|
+
attrs += ` ${meta.dataAttr}="${this.escapeHtml(token)}"`
|
|
147
|
+
attrs += ` method="${this.escapeHtml(method)}"`
|
|
148
|
+
attrs += ` action="${this.escapeHtml(meta.action)}"`
|
|
149
|
+
attrs += ` novalidate`
|
|
150
|
+
if (config.id) attrs += ` id="${this.escapeHtml(config.id)}"`
|
|
151
|
+
if (type === 'form' && config.clear !== undefined) attrs += ` clear="${config.clear}"`
|
|
152
|
+
|
|
153
|
+
let html = `<form ${attrs}>\n`
|
|
154
|
+
html += ` <input type="hidden" name="${meta.tokenInputName}" value="${this.escapeHtml(token)}">\n`
|
|
155
|
+
return html
|
|
53
156
|
}
|
|
54
157
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} else if (type === 'login') {
|
|
59
|
-
return this.extractLoginConfig(html, formToken)
|
|
60
|
-
} else if (type === 'magic-login') {
|
|
61
|
-
return this.extractMagicLoginConfig(html, formToken)
|
|
62
|
-
} else if (type === 'form') {
|
|
63
|
-
return this.extractFormConfig(html, formToken)
|
|
64
|
-
}
|
|
158
|
+
// - RUNTIME: emits the trailing success span + </form>.
|
|
159
|
+
static async closeForm() {
|
|
160
|
+
return `\n <span class="odac-form-success" style="display:none;"></span>\n</form>`
|
|
65
161
|
}
|
|
66
162
|
|
|
67
163
|
static storeConfig(token, config, Odac, type) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
164
|
+
const meta = this.FORM_META[type]
|
|
165
|
+
if (!Odac.View) Odac.View = {}
|
|
166
|
+
if (!Odac.View[meta.storageKey]) Odac.View[meta.storageKey] = {}
|
|
167
|
+
|
|
168
|
+
const formData = {
|
|
169
|
+
config: config,
|
|
170
|
+
created: Date.now(),
|
|
171
|
+
expires: Date.now() + 30 * 60 * 1000,
|
|
172
|
+
sessionId: Odac.Request.session('_client'),
|
|
173
|
+
userAgent: Odac.Request.header('user-agent'),
|
|
174
|
+
ip: Odac.Request.ip
|
|
76
175
|
}
|
|
176
|
+
|
|
177
|
+
Odac.View[meta.storageKey][token] = formData
|
|
178
|
+
Odac.Request.session(meta.sessionKeyPrefix + token, formData)
|
|
77
179
|
}
|
|
78
180
|
|
|
79
|
-
static
|
|
80
|
-
if (type === 'register')
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
181
|
+
static extractConfig(html, formToken, type) {
|
|
182
|
+
if (type === 'register') return this.extractRegisterConfig(html, formToken)
|
|
183
|
+
if (type === 'login') return this.extractLoginConfig(html, formToken)
|
|
184
|
+
if (type === 'magic-login') return this.extractMagicLoginConfig(html, formToken)
|
|
185
|
+
if (type === 'form') return this.extractFormConfig(html, formToken)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static generateSubmitButton(config, meta) {
|
|
189
|
+
const submitText = config.submitText || meta.defaultSubmitText
|
|
190
|
+
const submitLoading = config.submitLoading || meta.defaultSubmitLoading
|
|
191
|
+
|
|
192
|
+
let attrs = `type="submit"`
|
|
193
|
+
attrs += ` data-submit-text="${this.escapeHtml(submitText)}"`
|
|
194
|
+
attrs += ` data-loading-text="${this.escapeHtml(submitLoading)}"`
|
|
195
|
+
if (config.submitClass) attrs += ` class="${this.escapeHtml(config.submitClass)}"`
|
|
196
|
+
if (config.submitStyle) attrs += ` style="${this.escapeHtml(config.submitStyle)}"`
|
|
197
|
+
if (config.submitId) attrs += ` id="${this.escapeHtml(config.submitId)}"`
|
|
198
|
+
return `<button ${attrs}>${this.escapeHtml(submitText)}</button>`
|
|
89
199
|
}
|
|
90
200
|
|
|
91
201
|
static extractRegisterConfig(html, formToken) {
|
|
@@ -109,43 +219,174 @@ class Form {
|
|
|
109
219
|
if (redirectMatch) config.redirect = redirectMatch[1]
|
|
110
220
|
if (autologinMatch) config.autologin = autologinMatch[1] !== 'false'
|
|
111
221
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
222
|
+
this.applySubmitConfig(html, config)
|
|
223
|
+
this.collectFields(html, config)
|
|
224
|
+
this.collectSets(html, config)
|
|
225
|
+
|
|
226
|
+
return config
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
static extractLoginConfig(html, formToken) {
|
|
230
|
+
const config = {
|
|
231
|
+
token: formToken,
|
|
232
|
+
redirect: null,
|
|
233
|
+
submitText: 'Login',
|
|
234
|
+
submitLoading: 'Logging in...',
|
|
235
|
+
fields: []
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const loginMatch = html.match(/<odac:login([^>]*)>/)
|
|
239
|
+
if (!loginMatch) return config
|
|
240
|
+
|
|
241
|
+
const redirectMatch = loginMatch[0].match(/redirect=["']([^"']+)["']/)
|
|
242
|
+
if (redirectMatch) config.redirect = redirectMatch[1]
|
|
243
|
+
|
|
244
|
+
this.applySubmitConfig(html, config)
|
|
245
|
+
this.collectFields(html, config)
|
|
246
|
+
|
|
247
|
+
return config
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
static extractMagicLoginConfig(html, formToken) {
|
|
251
|
+
const config = {
|
|
252
|
+
token: formToken,
|
|
253
|
+
redirect: null,
|
|
254
|
+
submitText: 'Send Magic Link',
|
|
255
|
+
submitLoading: 'Sending...',
|
|
256
|
+
fields: []
|
|
128
257
|
}
|
|
129
258
|
|
|
259
|
+
const tagMatch = html.match(/<odac:magic-login([^>]*)>/)
|
|
260
|
+
if (!tagMatch) return config
|
|
261
|
+
|
|
262
|
+
const tag = tagMatch[0]
|
|
263
|
+
const redirectMatch = tag.match(/redirect=["']([^"']+)["']/)
|
|
264
|
+
const emailLabelMatch = tag.match(/email-label=["']([^"']+)["']/)
|
|
265
|
+
if (redirectMatch) config.redirect = redirectMatch[1]
|
|
266
|
+
|
|
130
267
|
const fieldMatches = html.match(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g)
|
|
131
268
|
if (fieldMatches) {
|
|
132
269
|
for (const fieldHtml of fieldMatches) {
|
|
133
270
|
const field = this.parseInput(fieldHtml)
|
|
134
271
|
if (field) config.fields.push(field)
|
|
135
272
|
}
|
|
273
|
+
} else {
|
|
274
|
+
config.fields.push({
|
|
275
|
+
name: 'email',
|
|
276
|
+
type: 'email',
|
|
277
|
+
placeholder: 'e.g. user@example.com',
|
|
278
|
+
label: emailLabelMatch ? emailLabelMatch[1] : 'Email Address',
|
|
279
|
+
class: '',
|
|
280
|
+
id: null,
|
|
281
|
+
unique: false,
|
|
282
|
+
skip: false,
|
|
283
|
+
value: null,
|
|
284
|
+
validations: [
|
|
285
|
+
{rule: 'required', message: 'Email is required'},
|
|
286
|
+
{rule: 'email', message: 'Invalid email format'}
|
|
287
|
+
]
|
|
288
|
+
})
|
|
136
289
|
}
|
|
137
290
|
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
291
|
+
const applied = this.applySubmitConfig(html, config)
|
|
292
|
+
if (!applied) {
|
|
293
|
+
const submitTextAttr = tag.match(/submit-text=["']([^"']+)["']/)
|
|
294
|
+
if (submitTextAttr) config.submitText = submitTextAttr[1]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return config
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
static extractFormConfig(html, formToken) {
|
|
301
|
+
const config = {
|
|
302
|
+
token: formToken,
|
|
303
|
+
action: null,
|
|
304
|
+
method: 'POST',
|
|
305
|
+
submitText: 'Submit',
|
|
306
|
+
submitLoading: 'Processing...',
|
|
307
|
+
fields: [],
|
|
308
|
+
sets: [],
|
|
309
|
+
class: '',
|
|
310
|
+
id: null,
|
|
311
|
+
table: null,
|
|
312
|
+
redirect: null,
|
|
313
|
+
successMessage: null
|
|
144
314
|
}
|
|
145
315
|
|
|
316
|
+
const formMatch = html.match(/<odac:form([^>]*)>/)
|
|
317
|
+
if (!formMatch) return config
|
|
318
|
+
|
|
319
|
+
const formTag = formMatch[0]
|
|
320
|
+
const extractAttr = name => {
|
|
321
|
+
const m = formTag.match(new RegExp(`${name}=(['"])((?:(?!\\1).)*)\\1`))
|
|
322
|
+
return m ? m[2] : null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const actionMatch = extractAttr('action')
|
|
326
|
+
const methodMatch = extractAttr('method')
|
|
327
|
+
const classMatch = extractAttr('class')
|
|
328
|
+
const idMatch = extractAttr('id')
|
|
329
|
+
const tableMatch = extractAttr('table')
|
|
330
|
+
const redirectMatch = extractAttr('redirect')
|
|
331
|
+
const successMatch = extractAttr('success')
|
|
332
|
+
const clearMatch = extractAttr('clear')
|
|
333
|
+
|
|
334
|
+
if (actionMatch) config.action = actionMatch
|
|
335
|
+
if (methodMatch) config.method = methodMatch.toUpperCase()
|
|
336
|
+
if (classMatch) config.class = classMatch
|
|
337
|
+
if (idMatch) config.id = idMatch
|
|
338
|
+
if (tableMatch) config.table = tableMatch
|
|
339
|
+
if (redirectMatch) config.redirect = redirectMatch
|
|
340
|
+
if (successMatch) config.successMessage = successMatch
|
|
341
|
+
if (clearMatch !== null) config.clear = clearMatch === 'true' || clearMatch === ''
|
|
342
|
+
|
|
343
|
+
this.applySubmitConfig(html, config)
|
|
344
|
+
this.collectFields(html, config)
|
|
345
|
+
this.collectSets(html, config)
|
|
346
|
+
|
|
146
347
|
return config
|
|
147
348
|
}
|
|
148
349
|
|
|
350
|
+
static applySubmitConfig(html, config) {
|
|
351
|
+
const submitMatch = html.match(/<odac:submit([^>]*?)(?:\/?>|>(.*?)<\/odac:submit>)/)
|
|
352
|
+
if (!submitMatch) return false
|
|
353
|
+
|
|
354
|
+
const submitTag = submitMatch[1]
|
|
355
|
+
const textMatch = submitTag.match(/text=["']([^"']+)["']/)
|
|
356
|
+
const loadingMatch = submitTag.match(/loading=["']([^"']+)["']/)
|
|
357
|
+
const classMatch = submitTag.match(/class=["']([^"']+)["']/)
|
|
358
|
+
const styleMatch = submitTag.match(/style=["']([^"']+)["']/)
|
|
359
|
+
const idMatch = submitTag.match(/id=["']([^"']+)["']/)
|
|
360
|
+
|
|
361
|
+
if (textMatch) config.submitText = textMatch[1]
|
|
362
|
+
else if (submitMatch[2]) config.submitText = submitMatch[2].trim()
|
|
363
|
+
|
|
364
|
+
if (loadingMatch) config.submitLoading = loadingMatch[1]
|
|
365
|
+
if (classMatch) config.submitClass = classMatch[1]
|
|
366
|
+
if (styleMatch) config.submitStyle = styleMatch[1]
|
|
367
|
+
if (idMatch) config.submitId = idMatch[1]
|
|
368
|
+
|
|
369
|
+
return true
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
static collectFields(html, config) {
|
|
373
|
+
const fieldMatches = html.match(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g)
|
|
374
|
+
if (!fieldMatches) return
|
|
375
|
+
for (const fieldHtml of fieldMatches) {
|
|
376
|
+
const field = this.parseInput(fieldHtml)
|
|
377
|
+
if (field) config.fields.push(field)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
static collectSets(html, config) {
|
|
382
|
+
const setMatches = html.match(/<odac:set[^>]*\/?>/g)
|
|
383
|
+
if (!setMatches) return
|
|
384
|
+
for (const setTag of setMatches) {
|
|
385
|
+
const set = this.parseSet(setTag)
|
|
386
|
+
if (set) config.sets.push(set)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
149
390
|
static parseInput(html) {
|
|
150
391
|
const fieldTagMatch = html.match(/<odac:input([^>]*?)(?:\/>|>)/)
|
|
151
392
|
if (!fieldTagMatch) return null
|
|
@@ -190,7 +431,6 @@ class Form {
|
|
|
190
431
|
for (const validateTag of validateMatches) {
|
|
191
432
|
const ruleMatch = validateTag.match(/rule=["']([^"']+)["']/)
|
|
192
433
|
const messageMatch = validateTag.match(/message=(["'])(.*?)\1/)
|
|
193
|
-
|
|
194
434
|
if (ruleMatch) {
|
|
195
435
|
field.validations.push({
|
|
196
436
|
rule: ruleMatch[1],
|
|
@@ -200,23 +440,15 @@ class Form {
|
|
|
200
440
|
}
|
|
201
441
|
}
|
|
202
442
|
|
|
203
|
-
// Capture generic attributes
|
|
204
443
|
const extraAttrs = {}
|
|
205
444
|
const knownAttrs = ['name', 'type', 'placeholder', 'label', 'class', 'id', 'unique', 'skip', 'value']
|
|
206
445
|
const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
|
|
207
|
-
let attrMatch
|
|
208
|
-
// Clean tag to just attributes part for safer regex matching if needed,
|
|
209
|
-
// or just run on fieldTag from start
|
|
210
446
|
const attributesString = fieldTag.replace(/^<odac:input/, '').replace(/\/?>$/, '')
|
|
211
|
-
|
|
447
|
+
let attrMatch
|
|
212
448
|
while ((attrMatch = attrRegex.exec(attributesString))) {
|
|
213
449
|
const key = attrMatch[1]
|
|
214
|
-
// If value is undefined, it's a boolean attribute (e.g. required, autofocus) -> set as true (or empty string)
|
|
215
450
|
const value = attrMatch[3] !== undefined ? attrMatch[3] : attrMatch[4] !== undefined ? attrMatch[4] : ''
|
|
216
|
-
|
|
217
|
-
if (!knownAttrs.includes(key)) {
|
|
218
|
-
extraAttrs[key] = value
|
|
219
|
-
}
|
|
451
|
+
if (!knownAttrs.includes(key)) extraAttrs[key] = value
|
|
220
452
|
}
|
|
221
453
|
field.extraAttributes = extraAttrs
|
|
222
454
|
|
|
@@ -248,59 +480,6 @@ class Form {
|
|
|
248
480
|
return set
|
|
249
481
|
}
|
|
250
482
|
|
|
251
|
-
static storeRegisterConfig(token, config, Odac) {
|
|
252
|
-
if (!Odac.View) Odac.View = {}
|
|
253
|
-
if (!Odac.View.registerForms) Odac.View.registerForms = {}
|
|
254
|
-
|
|
255
|
-
const formData = {
|
|
256
|
-
config: config,
|
|
257
|
-
created: Date.now(),
|
|
258
|
-
expires: Date.now() + 30 * 60 * 1000,
|
|
259
|
-
sessionId: Odac.Request.session('_client'),
|
|
260
|
-
userAgent: Odac.Request.header('user-agent'),
|
|
261
|
-
ip: Odac.Request.ip
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
Odac.View.registerForms[token] = formData
|
|
265
|
-
Odac.Request.session(`_register_form_${token}`, formData)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
static generateRegisterForm(originalHtml, config, formToken) {
|
|
269
|
-
const submitText = config.submitText || 'Register'
|
|
270
|
-
const submitLoading = config.submitLoading || 'Processing...'
|
|
271
|
-
|
|
272
|
-
let innerContent = originalHtml.replace(/<odac:register[^>]*>/, '').replace(/<\/odac:register>/, '')
|
|
273
|
-
|
|
274
|
-
innerContent = innerContent.replace(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g, fieldMatch => {
|
|
275
|
-
const field = this.parseInput(fieldMatch)
|
|
276
|
-
if (!field) return fieldMatch
|
|
277
|
-
// Sync with resolved config value if available
|
|
278
|
-
const configField = config.fields.find(f => f.name === field.name)
|
|
279
|
-
if (configField) field.value = configField.value
|
|
280
|
-
return this.generateFieldHtml(field)
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
const submitMatch = innerContent.match(/<odac:submit[\s\S]*?(?:<\/odac:submit>|\/?>)/)
|
|
284
|
-
if (submitMatch) {
|
|
285
|
-
let submitAttrs = `type="submit" data-submit-text="${submitText}" data-loading-text="${submitLoading}"`
|
|
286
|
-
if (config.submitClass) submitAttrs += ` class="${config.submitClass}"`
|
|
287
|
-
if (config.submitStyle) submitAttrs += ` style="${config.submitStyle}"`
|
|
288
|
-
if (config.submitId) submitAttrs += ` id="${config.submitId}"`
|
|
289
|
-
const submitButton = `<button ${submitAttrs}>${submitText}</button>`
|
|
290
|
-
innerContent = innerContent.replace(submitMatch[0], submitButton)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
innerContent = innerContent.replace(/<odac:set[^>]*\/?>/g, '')
|
|
294
|
-
|
|
295
|
-
let html = `<form class="odac-register-form" data-odac-register="${formToken}" method="POST" action="/_odac/register" novalidate>\n`
|
|
296
|
-
html += ` <input type="hidden" name="_odac_register_token" value="${formToken}">\n`
|
|
297
|
-
html += innerContent
|
|
298
|
-
html += `\n <span class="odac-form-success" style="display:none;"></span>\n`
|
|
299
|
-
html += `</form>`
|
|
300
|
-
|
|
301
|
-
return html
|
|
302
|
-
}
|
|
303
|
-
|
|
304
483
|
static generateFieldHtml(field) {
|
|
305
484
|
let html = ''
|
|
306
485
|
const escapedName = this.escapeHtml(field.name)
|
|
@@ -341,18 +520,11 @@ class Form {
|
|
|
341
520
|
}
|
|
342
521
|
|
|
343
522
|
static appendExtraAttributes(attrs, field) {
|
|
344
|
-
if (field.extraAttributes)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// presence is enough.
|
|
350
|
-
if (val === '') {
|
|
351
|
-
attrs += ` ${key}`
|
|
352
|
-
} else {
|
|
353
|
-
attrs += ` ${key}="${this.escapeHtml(val)}"`
|
|
354
|
-
}
|
|
355
|
-
}
|
|
523
|
+
if (!field.extraAttributes) return attrs
|
|
524
|
+
for (const key in field.extraAttributes) {
|
|
525
|
+
const val = field.extraAttributes[key]
|
|
526
|
+
if (val === '') attrs += ` ${key}`
|
|
527
|
+
else attrs += ` ${key}="${this.escapeHtml(val)}"`
|
|
356
528
|
}
|
|
357
529
|
return attrs
|
|
358
530
|
}
|
|
@@ -443,379 +615,7 @@ class Form {
|
|
|
443
615
|
if (errorMessages.pattern) attrs += ` data-error-pattern="${this.escapeHtml(errorMessages.pattern)}"`
|
|
444
616
|
if (errorMessages.email) attrs += ` data-error-email="${this.escapeHtml(errorMessages.email)}"`
|
|
445
617
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return attrs
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
static extractLoginConfig(html, formToken) {
|
|
452
|
-
const config = {
|
|
453
|
-
token: formToken,
|
|
454
|
-
redirect: null,
|
|
455
|
-
submitText: 'Login',
|
|
456
|
-
submitLoading: 'Logging in...',
|
|
457
|
-
fields: []
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const loginMatch = html.match(/<odac:login([^>]*)>/)
|
|
461
|
-
if (!loginMatch) return config
|
|
462
|
-
|
|
463
|
-
const loginTag = loginMatch[0]
|
|
464
|
-
const redirectMatch = loginTag.match(/redirect=["']([^"']+)["']/)
|
|
465
|
-
|
|
466
|
-
if (redirectMatch) config.redirect = redirectMatch[1]
|
|
467
|
-
|
|
468
|
-
const submitMatch = html.match(/<odac:submit([^>]*?)(?:\/?>|>(.*?)<\/odac:submit>)/)
|
|
469
|
-
if (submitMatch) {
|
|
470
|
-
const submitTag = submitMatch[1]
|
|
471
|
-
const textMatch = submitTag.match(/text=["']([^"']+)["']/)
|
|
472
|
-
const loadingMatch = submitTag.match(/loading=["']([^"']+)["']/)
|
|
473
|
-
const classMatch = submitTag.match(/class=["']([^"']+)["']/)
|
|
474
|
-
const styleMatch = submitTag.match(/style=["']([^"']+)["']/)
|
|
475
|
-
const idMatch = submitTag.match(/id=["']([^"']+)["']/)
|
|
476
|
-
|
|
477
|
-
if (textMatch) config.submitText = textMatch[1]
|
|
478
|
-
else if (submitMatch[2]) config.submitText = submitMatch[2].trim()
|
|
479
|
-
|
|
480
|
-
if (loadingMatch) config.submitLoading = loadingMatch[1]
|
|
481
|
-
if (classMatch) config.submitClass = classMatch[1]
|
|
482
|
-
if (styleMatch) config.submitStyle = styleMatch[1]
|
|
483
|
-
if (idMatch) config.submitId = idMatch[1]
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const fieldMatches = html.match(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g)
|
|
487
|
-
if (fieldMatches) {
|
|
488
|
-
for (const fieldHtml of fieldMatches) {
|
|
489
|
-
const field = this.parseInput(fieldHtml)
|
|
490
|
-
if (field) config.fields.push(field)
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return config
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
static storeLoginConfig(token, config, Odac) {
|
|
498
|
-
if (!Odac.View) Odac.View = {}
|
|
499
|
-
if (!Odac.View.loginForms) Odac.View.loginForms = {}
|
|
500
|
-
|
|
501
|
-
const formData = {
|
|
502
|
-
config: config,
|
|
503
|
-
created: Date.now(),
|
|
504
|
-
expires: Date.now() + 30 * 60 * 1000,
|
|
505
|
-
sessionId: Odac.Request.session('_client'),
|
|
506
|
-
userAgent: Odac.Request.header('user-agent'),
|
|
507
|
-
ip: Odac.Request.ip
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
Odac.View.loginForms[token] = formData
|
|
511
|
-
Odac.Request.session(`_login_form_${token}`, formData)
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
static generateLoginForm(originalHtml, config, formToken) {
|
|
515
|
-
const submitText = config.submitText || 'Login'
|
|
516
|
-
const submitLoading = config.submitLoading || 'Logging in...'
|
|
517
|
-
|
|
518
|
-
let innerContent = originalHtml.replace(/<odac:login[^>]*>/, '').replace(/<\/odac:login>/, '')
|
|
519
|
-
|
|
520
|
-
innerContent = innerContent.replace(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g, fieldMatch => {
|
|
521
|
-
const field = this.parseInput(fieldMatch)
|
|
522
|
-
if (!field) return fieldMatch
|
|
523
|
-
// Sync with resolved config value if available
|
|
524
|
-
const configField = config.fields.find(f => f.name === field.name)
|
|
525
|
-
if (configField) field.value = configField.value
|
|
526
|
-
return this.generateFieldHtml(field)
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
const submitMatch = innerContent.match(/<odac:submit[\s\S]*?(?:<\/odac:submit>|\/?>)/)
|
|
530
|
-
if (submitMatch) {
|
|
531
|
-
let submitAttrs = `type="submit" data-submit-text="${submitText}" data-loading-text="${submitLoading}"`
|
|
532
|
-
if (config.submitClass) submitAttrs += ` class="${config.submitClass}"`
|
|
533
|
-
if (config.submitStyle) submitAttrs += ` style="${config.submitStyle}"`
|
|
534
|
-
if (config.submitId) submitAttrs += ` id="${config.submitId}"`
|
|
535
|
-
const submitButton = `<button ${submitAttrs}>${submitText}</button>`
|
|
536
|
-
innerContent = innerContent.replace(submitMatch[0], submitButton)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
let html = `<form class="odac-login-form" data-odac-login="${formToken}" method="POST" action="/_odac/login" novalidate>\n`
|
|
540
|
-
html += ` <input type="hidden" name="_odac_login_token" value="${formToken}">\n`
|
|
541
|
-
html += innerContent
|
|
542
|
-
html += `\n <span class="odac-form-success" style="display:none;"></span>\n`
|
|
543
|
-
html += `</form>`
|
|
544
|
-
|
|
545
|
-
return html
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
static extractFormConfig(html, formToken) {
|
|
549
|
-
const config = {
|
|
550
|
-
token: formToken,
|
|
551
|
-
action: null,
|
|
552
|
-
method: 'POST',
|
|
553
|
-
submitText: 'Submit',
|
|
554
|
-
submitLoading: 'Processing...',
|
|
555
|
-
fields: [],
|
|
556
|
-
sets: [],
|
|
557
|
-
class: '',
|
|
558
|
-
id: null,
|
|
559
|
-
table: null,
|
|
560
|
-
redirect: null,
|
|
561
|
-
successMessage: null
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const formMatch = html.match(/<odac:form([^>]*)>/)
|
|
565
|
-
if (!formMatch) return config
|
|
566
|
-
|
|
567
|
-
const formTag = formMatch[0]
|
|
568
|
-
const extractAttr = name => {
|
|
569
|
-
const match = formTag.match(new RegExp(`${name}=(['"])((?:(?!\\1).)*)\\1`))
|
|
570
|
-
return match ? match[2] : null
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const actionMatch = extractAttr('action')
|
|
574
|
-
const methodMatch = extractAttr('method')
|
|
575
|
-
const classMatch = extractAttr('class')
|
|
576
|
-
const idMatch = extractAttr('id')
|
|
577
|
-
const tableMatch = extractAttr('table')
|
|
578
|
-
const redirectMatch = extractAttr('redirect')
|
|
579
|
-
const successMatch = extractAttr('success')
|
|
580
|
-
|
|
581
|
-
if (actionMatch) config.action = actionMatch
|
|
582
|
-
if (methodMatch) config.method = methodMatch.toUpperCase()
|
|
583
|
-
if (classMatch) config.class = classMatch
|
|
584
|
-
if (idMatch) config.id = idMatch
|
|
585
|
-
if (tableMatch) config.table = tableMatch
|
|
586
|
-
if (redirectMatch) config.redirect = redirectMatch
|
|
587
|
-
if (successMatch) config.successMessage = successMatch
|
|
588
|
-
const clearMatch = extractAttr('clear')
|
|
589
|
-
if (clearMatch !== null) config.clear = clearMatch === 'true' || clearMatch === ''
|
|
590
|
-
|
|
591
|
-
const submitMatch = html.match(/<odac:submit([^>]*?)(?:\/?>|>(.*?)<\/odac:submit>)/)
|
|
592
|
-
if (submitMatch) {
|
|
593
|
-
const submitTag = submitMatch[1]
|
|
594
|
-
const textMatch = submitTag.match(/text=["']([^"']+)["']/)
|
|
595
|
-
const loadingMatch = submitTag.match(/loading=["']([^"']+)["']/)
|
|
596
|
-
const classMatch = submitTag.match(/class=["']([^"']+)["']/)
|
|
597
|
-
const styleMatch = submitTag.match(/style=["']([^"']+)["']/)
|
|
598
|
-
const idMatch = submitTag.match(/id=["']([^"']+)["']/)
|
|
599
|
-
|
|
600
|
-
if (textMatch) config.submitText = textMatch[1]
|
|
601
|
-
else if (submitMatch[2]) config.submitText = submitMatch[2].trim()
|
|
602
|
-
|
|
603
|
-
if (loadingMatch) config.submitLoading = loadingMatch[1]
|
|
604
|
-
if (classMatch) config.submitClass = classMatch[1]
|
|
605
|
-
if (styleMatch) config.submitStyle = styleMatch[1]
|
|
606
|
-
if (idMatch) config.submitId = idMatch[1]
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const fieldMatches = html.match(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g)
|
|
610
|
-
if (fieldMatches) {
|
|
611
|
-
for (const fieldHtml of fieldMatches) {
|
|
612
|
-
const field = this.parseInput(fieldHtml)
|
|
613
|
-
if (field) config.fields.push(field)
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const setMatches = html.match(/<odac:set[^>]*\/?>/g)
|
|
618
|
-
if (setMatches) {
|
|
619
|
-
for (const setTag of setMatches) {
|
|
620
|
-
const set = this.parseSet(setTag)
|
|
621
|
-
if (set) config.sets.push(set)
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
return config
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
static storeFormConfig(token, config, Odac) {
|
|
629
|
-
if (!Odac.View) Odac.View = {}
|
|
630
|
-
if (!Odac.View.customForms) Odac.View.customForms = {}
|
|
631
|
-
|
|
632
|
-
const formData = {
|
|
633
|
-
config: config,
|
|
634
|
-
created: Date.now(),
|
|
635
|
-
expires: Date.now() + 30 * 60 * 1000,
|
|
636
|
-
sessionId: Odac.Request.session('_client'),
|
|
637
|
-
userAgent: Odac.Request.header('user-agent'),
|
|
638
|
-
ip: Odac.Request.ip
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
Odac.View.customForms[token] = formData
|
|
642
|
-
Odac.Request.session(`_custom_form_${token}`, formData)
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
static generateCustomForm(originalHtml, config, formToken) {
|
|
646
|
-
const submitText = config.submitText || 'Submit'
|
|
647
|
-
const submitLoading = config.submitLoading || 'Processing...'
|
|
648
|
-
// Always post to internal handler, real action is in session config
|
|
649
|
-
const formAction = '/_odac/form'
|
|
650
|
-
const method = config.method || 'POST'
|
|
651
|
-
|
|
652
|
-
let innerContent = originalHtml.replace(/<odac:form[^>]*>/, '').replace(/<\/odac:form>/, '')
|
|
653
|
-
|
|
654
|
-
innerContent = innerContent.replace(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g, fieldMatch => {
|
|
655
|
-
const field = this.parseInput(fieldMatch)
|
|
656
|
-
if (!field) return fieldMatch
|
|
657
|
-
// Sync with resolved config value if available
|
|
658
|
-
const configField = config.fields.find(f => f.name === field.name)
|
|
659
|
-
if (configField) field.value = configField.value
|
|
660
|
-
return this.generateFieldHtml(field)
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
const submitMatch = innerContent.match(/<odac:submit[\s\S]*?(?:<\/odac:submit>|\/?>)/)
|
|
664
|
-
if (submitMatch) {
|
|
665
|
-
let submitAttrs = `type="submit" data-submit-text="${this.escapeHtml(submitText)}" data-loading-text="${this.escapeHtml(submitLoading)}"`
|
|
666
|
-
if (config.submitClass) submitAttrs += ` class="${this.escapeHtml(config.submitClass)}"`
|
|
667
|
-
if (config.submitStyle) submitAttrs += ` style="${this.escapeHtml(config.submitStyle)}"`
|
|
668
|
-
if (config.submitId) submitAttrs += ` id="${this.escapeHtml(config.submitId)}"`
|
|
669
|
-
const submitButton = `<button ${submitAttrs}>${this.escapeHtml(submitText)}</button>`
|
|
670
|
-
innerContent = innerContent.replace(submitMatch[0], submitButton)
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
innerContent = innerContent.replace(/<odac:set[^>]*\/?>/g, '')
|
|
674
|
-
|
|
675
|
-
let formAttrs = `class="odac-custom-form${config.class ? ' ' + this.escapeHtml(config.class) : ''}" data-odac-form="${this.escapeHtml(formToken)}" method="${this.escapeHtml(method)}" action="${this.escapeHtml(formAction)}" novalidate`
|
|
676
|
-
if (config.id) formAttrs += ` id="${this.escapeHtml(config.id)}"`
|
|
677
|
-
if (config.clear !== undefined) formAttrs += ` clear="${config.clear}"`
|
|
678
|
-
|
|
679
|
-
let html = `<form ${formAttrs}>\n`
|
|
680
|
-
html += ` <input type="hidden" name="_odac_form_token" value="${this.escapeHtml(formToken)}">\n`
|
|
681
|
-
html += innerContent
|
|
682
|
-
html += `\n <span class="odac-form-success" style="display:none;"></span>\n`
|
|
683
|
-
html += `</form>`
|
|
684
|
-
|
|
685
|
-
return html
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
static extractMagicLoginConfig(html, formToken) {
|
|
689
|
-
const config = {
|
|
690
|
-
token: formToken,
|
|
691
|
-
redirect: null,
|
|
692
|
-
submitText: 'Send Magic Link',
|
|
693
|
-
submitLoading: 'Sending...',
|
|
694
|
-
fields: []
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
const tagMatch = html.match(/<odac:magic-login([^>]*)>/)
|
|
698
|
-
if (!tagMatch) return config
|
|
699
|
-
|
|
700
|
-
const tag = tagMatch[0]
|
|
701
|
-
const redirectMatch = tag.match(/redirect=["']([^"']+)["']/)
|
|
702
|
-
const emailLabelMatch = tag.match(/email-label=["']([^"']+)["']/)
|
|
703
|
-
|
|
704
|
-
if (redirectMatch) config.redirect = redirectMatch[1]
|
|
705
|
-
|
|
706
|
-
// Auto-add email field if not manually specified (simplified usage)
|
|
707
|
-
const fieldMatches = html.match(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g)
|
|
708
|
-
|
|
709
|
-
if (fieldMatches) {
|
|
710
|
-
// Custom fields included
|
|
711
|
-
for (const fieldHtml of fieldMatches) {
|
|
712
|
-
const field = this.parseInput(fieldHtml)
|
|
713
|
-
if (field) config.fields.push(field)
|
|
714
|
-
}
|
|
715
|
-
} else {
|
|
716
|
-
// Default Email Field
|
|
717
|
-
config.fields.push({
|
|
718
|
-
name: 'email',
|
|
719
|
-
type: 'email',
|
|
720
|
-
placeholder: 'e.g. user@example.com',
|
|
721
|
-
label: emailLabelMatch ? emailLabelMatch[1] : 'Email Address',
|
|
722
|
-
class: '',
|
|
723
|
-
id: null,
|
|
724
|
-
unique: false,
|
|
725
|
-
skip: false,
|
|
726
|
-
validations: [
|
|
727
|
-
{rule: 'required', message: 'Email is required'},
|
|
728
|
-
{rule: 'email', message: 'Invalid email format'}
|
|
729
|
-
]
|
|
730
|
-
})
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const submitMatch = html.match(/<odac:submit([^>]*?)(?:\/?>|>(.*?)<\/odac:submit>)/)
|
|
734
|
-
if (submitMatch) {
|
|
735
|
-
const submitTag = submitMatch[1]
|
|
736
|
-
const textMatch = submitTag.match(/text=["']([^"']+)["']/)
|
|
737
|
-
const loadingMatch = submitTag.match(/loading=["']([^"']+)["']/)
|
|
738
|
-
const classMatch = submitTag.match(/class=["']([^"']+)["']/)
|
|
739
|
-
const styleMatch = submitTag.match(/style=["']([^"']+)["']/)
|
|
740
|
-
const idMatch = submitTag.match(/id=["']([^"']+)["']/)
|
|
741
|
-
|
|
742
|
-
if (textMatch) config.submitText = textMatch[1]
|
|
743
|
-
else if (submitMatch[2]) config.submitText = submitMatch[2].trim()
|
|
744
|
-
|
|
745
|
-
if (loadingMatch) config.submitLoading = loadingMatch[1]
|
|
746
|
-
if (classMatch) config.submitClass = classMatch[1]
|
|
747
|
-
if (styleMatch) config.submitStyle = styleMatch[1]
|
|
748
|
-
if (idMatch) config.submitId = idMatch[1]
|
|
749
|
-
} else {
|
|
750
|
-
// Check for submit-text attribute on main tag if no submit tag
|
|
751
|
-
const submitTextAttr = tag.match(/submit-text=["']([^"']+)["']/)
|
|
752
|
-
if (submitTextAttr) config.submitText = submitTextAttr[1]
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
return config
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
static storeMagicLoginConfig(token, config, Odac) {
|
|
759
|
-
if (!Odac.View) Odac.View = {}
|
|
760
|
-
if (!Odac.View.magicLoginForms) Odac.View.magicLoginForms = {}
|
|
761
|
-
|
|
762
|
-
const formData = {
|
|
763
|
-
config: config,
|
|
764
|
-
created: Date.now(),
|
|
765
|
-
expires: Date.now() + 30 * 60 * 1000,
|
|
766
|
-
sessionId: Odac.Request.session('_client'),
|
|
767
|
-
userAgent: Odac.Request.header('user-agent'),
|
|
768
|
-
ip: Odac.Request.ip
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
Odac.View.magicLoginForms[token] = formData
|
|
772
|
-
Odac.Request.session(`_magic_login_form_${token}`, formData)
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
static generateMagicLoginForm(originalHtml, config, formToken) {
|
|
776
|
-
const submitText = config.submitText || 'Send Magic Link'
|
|
777
|
-
const submitLoading = config.submitLoading || 'Sending...'
|
|
778
|
-
|
|
779
|
-
let innerContent = originalHtml.replace(/<odac:magic-login[^>]*>/, '').replace(/<\/odac:magic-login>/, '')
|
|
780
|
-
|
|
781
|
-
// If no custom fields were present in HTML but we added default email in config
|
|
782
|
-
if (!originalHtml.includes('<odac:input')) {
|
|
783
|
-
const emailField = config.fields.find(f => f.name === 'email')
|
|
784
|
-
if (emailField) {
|
|
785
|
-
innerContent += this.generateFieldHtml(emailField)
|
|
786
|
-
}
|
|
787
|
-
} else {
|
|
788
|
-
innerContent = innerContent.replace(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g, fieldMatch => {
|
|
789
|
-
const field = this.parseInput(fieldMatch)
|
|
790
|
-
if (!field) return fieldMatch
|
|
791
|
-
// Sync with resolved config value if available
|
|
792
|
-
const configField = config.fields.find(f => f.name === field.name)
|
|
793
|
-
if (configField) field.value = configField.value
|
|
794
|
-
return this.generateFieldHtml(field)
|
|
795
|
-
})
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
const submitMatch = innerContent.match(/<odac:submit[\s\S]*?(?:<\/odac:submit>|\/?>)/)
|
|
799
|
-
if (submitMatch) {
|
|
800
|
-
let submitAttrs = `type="submit" data-submit-text="${submitText}" data-loading-text="${submitLoading}"`
|
|
801
|
-
if (config.submitClass) submitAttrs += ` class="${config.submitClass}"`
|
|
802
|
-
if (config.submitStyle) submitAttrs += ` style="${config.submitStyle}"`
|
|
803
|
-
if (config.submitId) submitAttrs += ` id="${config.submitId}"`
|
|
804
|
-
const submitButton = `<button ${submitAttrs}>${submitText}</button>`
|
|
805
|
-
innerContent = innerContent.replace(submitMatch[0], submitButton)
|
|
806
|
-
} else if (!innerContent.includes('type="submit"')) {
|
|
807
|
-
// Auto add submit button if missing
|
|
808
|
-
const submitButton = `<button type="submit" data-submit-text="${submitText}" data-loading-text="${submitLoading}">${submitText}</button>`
|
|
809
|
-
innerContent += `\n${submitButton}`
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
let html = `<form class="odac-magic-login-form" data-odac-magic-login="${formToken}" method="POST" action="/_odac/magic-login" novalidate>\n`
|
|
813
|
-
html += ` <input type="hidden" name="_odac_magic_login_token" value="${formToken}">\n`
|
|
814
|
-
html += innerContent
|
|
815
|
-
html += `\n <span class="odac-form-success" style="display:none;"></span>\n`
|
|
816
|
-
html += `</form>`
|
|
817
|
-
|
|
818
|
-
return html
|
|
618
|
+
return this.appendExtraAttributes(attrs, field)
|
|
819
619
|
}
|
|
820
620
|
}
|
|
821
621
|
|
package/src/View.js
CHANGED
|
@@ -443,12 +443,7 @@ class View {
|
|
|
443
443
|
content = this.#parseOdacTag(content)
|
|
444
444
|
content = content.replace(/`/g, '\\\\`').replace(/\$\{/g, '\\\\${')
|
|
445
445
|
|
|
446
|
-
jsBlocks.forEach((jsContent, index) => {
|
|
447
|
-
content = content.replace(`___ODAC_JS_BLOCK_${index}___`, jsContent)
|
|
448
|
-
})
|
|
449
|
-
|
|
450
446
|
let result = 'html += `\n' + content + '\n`'
|
|
451
|
-
content = content.split('\n')
|
|
452
447
|
for (let key in this.#functions) {
|
|
453
448
|
let att = ''
|
|
454
449
|
let func = this.#functions[key]
|
|
@@ -517,6 +512,14 @@ class View {
|
|
|
517
512
|
}
|
|
518
513
|
}
|
|
519
514
|
}
|
|
515
|
+
|
|
516
|
+
// Restore <script:odac> JS bodies after function processing so the regex
|
|
517
|
+
// pipeline above never sees user JS as template syntax. Use a function
|
|
518
|
+
// replacer to keep $-sequences in the JS literal (e.g. $$, $&) intact.
|
|
519
|
+
jsBlocks.forEach((jsContent, index) => {
|
|
520
|
+
result = result.replace(`___ODAC_JS_BLOCK_${index}___`, () => jsContent)
|
|
521
|
+
})
|
|
522
|
+
|
|
520
523
|
let cache = `${nodeCrypto.createHash('md5').update(file).digest('hex')}`
|
|
521
524
|
await fsPromises.mkdir(CACHE_DIR, {recursive: true})
|
|
522
525
|
await fsPromises.writeFile(
|