odac 0.9.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.
Files changed (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. package/web/view/header/main.html +14 -0
@@ -0,0 +1,600 @@
1
+ const nodeCrypto = require('crypto')
2
+
3
+ class Form {
4
+ static FORM_TYPES = ['register', 'login', 'form']
5
+
6
+ static parse(content, Candy) {
7
+ for (const type of this.FORM_TYPES) {
8
+ content = this.parseFormType(content, Candy, type)
9
+ }
10
+ return content
11
+ }
12
+
13
+ static parseFormType(content, Candy, type) {
14
+ const regex = new RegExp(`<candy:${type}[\\s\\S]*?<\\/candy:${type}>`, 'g')
15
+ const matches = content.match(regex)
16
+ if (!matches) return content
17
+
18
+ for (const match of matches) {
19
+ const formToken = nodeCrypto.randomBytes(32).toString('hex')
20
+ const formConfig = this.extractConfig(match, formToken, type)
21
+
22
+ this.storeConfig(formToken, formConfig, Candy, type)
23
+
24
+ const generatedForm = this.generateForm(match, formConfig, formToken, type)
25
+ content = content.replace(match, generatedForm)
26
+ }
27
+
28
+ return content
29
+ }
30
+
31
+ static extractConfig(html, formToken, type) {
32
+ if (type === 'register') {
33
+ return this.extractRegisterConfig(html, formToken)
34
+ } else if (type === 'login') {
35
+ return this.extractLoginConfig(html, formToken)
36
+ } else if (type === 'form') {
37
+ return this.extractFormConfig(html, formToken)
38
+ }
39
+ }
40
+
41
+ static storeConfig(token, config, Candy, type) {
42
+ if (type === 'register') {
43
+ this.storeRegisterConfig(token, config, Candy)
44
+ } else if (type === 'login') {
45
+ this.storeLoginConfig(token, config, Candy)
46
+ } else if (type === 'form') {
47
+ this.storeFormConfig(token, config, Candy)
48
+ }
49
+ }
50
+
51
+ static generateForm(originalHtml, config, formToken, type) {
52
+ if (type === 'register') {
53
+ return this.generateRegisterForm(originalHtml, config, formToken)
54
+ } else if (type === 'login') {
55
+ return this.generateLoginForm(originalHtml, config, formToken)
56
+ } else if (type === 'form') {
57
+ return this.generateCustomForm(originalHtml, config, formToken)
58
+ }
59
+ }
60
+
61
+ static extractRegisterConfig(html, formToken) {
62
+ const config = {
63
+ token: formToken,
64
+ redirect: null,
65
+ autologin: true,
66
+ submitText: 'Register',
67
+ submitLoading: 'Processing...',
68
+ fields: [],
69
+ sets: []
70
+ }
71
+
72
+ const registerMatch = html.match(/<candy:register([^>]*)>/)
73
+ if (!registerMatch) return config
74
+
75
+ const registerTag = registerMatch[0]
76
+ const redirectMatch = registerTag.match(/redirect=["']([^"']+)["']/)
77
+ const autologinMatch = registerTag.match(/autologin=["']([^"']+)["']/)
78
+
79
+ if (redirectMatch) config.redirect = redirectMatch[1]
80
+ if (autologinMatch) config.autologin = autologinMatch[1] !== 'false'
81
+
82
+ const submitMatch = html.match(/<candy:submit([^>/]*)(?:\/?>|>(.*?)<\/candy:submit>)/)
83
+ if (submitMatch) {
84
+ const submitTag = submitMatch[1]
85
+ const textMatch = submitTag.match(/text=["']([^"']+)["']/)
86
+ const loadingMatch = submitTag.match(/loading=["']([^"']+)["']/)
87
+ const classMatch = submitTag.match(/class=["']([^"']+)["']/)
88
+ const styleMatch = submitTag.match(/style=["']([^"']+)["']/)
89
+ const idMatch = submitTag.match(/id=["']([^"']+)["']/)
90
+
91
+ if (textMatch) config.submitText = textMatch[1]
92
+ else if (submitMatch[2]) config.submitText = submitMatch[2].trim()
93
+
94
+ if (loadingMatch) config.submitLoading = loadingMatch[1]
95
+ if (classMatch) config.submitClass = classMatch[1]
96
+ if (styleMatch) config.submitStyle = styleMatch[1]
97
+ if (idMatch) config.submitId = idMatch[1]
98
+ }
99
+
100
+ const fieldMatches = html.match(/<candy:field[\s\S]*?<\/candy:field>/g)
101
+ if (fieldMatches) {
102
+ for (const fieldHtml of fieldMatches) {
103
+ const field = this.parseField(fieldHtml)
104
+ if (field) config.fields.push(field)
105
+ }
106
+ }
107
+
108
+ const setMatches = html.match(/<candy:set[^>]*\/?>/g)
109
+ if (setMatches) {
110
+ for (const setTag of setMatches) {
111
+ const set = this.parseSet(setTag)
112
+ if (set) config.sets.push(set)
113
+ }
114
+ }
115
+
116
+ return config
117
+ }
118
+
119
+ static parseField(html) {
120
+ const fieldTagMatch = html.match(/<candy:field([^>]*?)(?:\/>|>)/)
121
+ if (!fieldTagMatch) return null
122
+
123
+ const fieldTag = fieldTagMatch[0]
124
+ const nameMatch = fieldTag.match(/name=["']([^"']+)["']/)
125
+ if (!nameMatch) return null
126
+
127
+ const field = {
128
+ name: nameMatch[1],
129
+ type: 'text',
130
+ placeholder: '',
131
+ label: null,
132
+ class: '',
133
+ id: null,
134
+ unique: false,
135
+ skip: false,
136
+ validations: []
137
+ }
138
+
139
+ const typeMatch = fieldTag.match(/type=["']([^"']+)["']/)
140
+ const placeholderMatch = fieldTag.match(/placeholder=["']([^"']+)["']/)
141
+ const labelMatch = fieldTag.match(/label=["']([^"']+)["']/)
142
+ const classMatch = fieldTag.match(/class=["']([^"']+)["']/)
143
+ const idMatch = fieldTag.match(/id=["']([^"']+)["']/)
144
+ const uniqueMatch = fieldTag.match(/unique=["']([^"']+)["']/) || fieldTag.match(/\sunique[\s/>]/)
145
+ const skipMatch = fieldTag.match(/skip=["']([^"']+)["']/) || fieldTag.match(/\sskip[\s/>]/)
146
+
147
+ if (typeMatch) field.type = typeMatch[1]
148
+ if (placeholderMatch) field.placeholder = placeholderMatch[1]
149
+ if (labelMatch) field.label = labelMatch[1]
150
+ if (classMatch) field.class = classMatch[1]
151
+ if (idMatch) field.id = idMatch[1]
152
+ if (uniqueMatch) field.unique = uniqueMatch[1] !== 'false'
153
+ if (skipMatch) field.skip = skipMatch[1] !== 'false'
154
+
155
+ const validateMatches = html.match(/<candy:validate[^>]*>/g)
156
+ if (validateMatches) {
157
+ for (const validateTag of validateMatches) {
158
+ const ruleMatch = validateTag.match(/rule=["']([^"']+)["']/)
159
+ const messageMatch = validateTag.match(/message=(["'])(.*?)\1/)
160
+
161
+ if (ruleMatch) {
162
+ field.validations.push({
163
+ rule: ruleMatch[1],
164
+ message: messageMatch ? messageMatch[2] : null
165
+ })
166
+ }
167
+ }
168
+ }
169
+
170
+ return field
171
+ }
172
+
173
+ static parseSet(html) {
174
+ const nameMatch = html.match(/name=["']([^"']+)["']/)
175
+ if (!nameMatch) return null
176
+
177
+ const set = {
178
+ name: nameMatch[1],
179
+ value: null,
180
+ compute: null,
181
+ callback: null,
182
+ ifEmpty: false
183
+ }
184
+
185
+ const valueMatch = html.match(/value=(["'])(.*?)\1/)
186
+ const computeMatch = html.match(/compute=["']([^"']+)["']/)
187
+ const callbackMatch = html.match(/callback=["']([^"']+)["']/)
188
+ const ifEmptyMatch = html.match(/if-empty=["']([^"']+)["']/) || html.match(/\sif-empty[\s/>]/)
189
+
190
+ if (valueMatch) set.value = valueMatch[2]
191
+ if (computeMatch) set.compute = computeMatch[1]
192
+ if (callbackMatch) set.callback = callbackMatch[1]
193
+ if (ifEmptyMatch) set.ifEmpty = ifEmptyMatch[1] !== 'false'
194
+
195
+ return set
196
+ }
197
+
198
+ static storeRegisterConfig(token, config, Candy) {
199
+ if (!Candy.View) Candy.View = {}
200
+ if (!Candy.View.registerForms) Candy.View.registerForms = {}
201
+
202
+ const formData = {
203
+ config: config,
204
+ created: Date.now(),
205
+ expires: Date.now() + 30 * 60 * 1000,
206
+ sessionId: Candy.Request.session('_client'),
207
+ userAgent: Candy.Request.header('user-agent'),
208
+ ip: Candy.Request.ip
209
+ }
210
+
211
+ Candy.View.registerForms[token] = formData
212
+ Candy.Request.session(`_register_form_${token}`, formData)
213
+ }
214
+
215
+ static generateRegisterForm(originalHtml, config, formToken) {
216
+ const submitText = config.submitText || 'Register'
217
+ const submitLoading = config.submitLoading || 'Processing...'
218
+
219
+ let innerContent = originalHtml.replace(/<candy:register[^>]*>/, '').replace(/<\/candy:register>/, '')
220
+
221
+ innerContent = innerContent.replace(/<candy:field[\s\S]*?<\/candy:field>/g, fieldMatch => {
222
+ const field = this.parseField(fieldMatch)
223
+ if (!field) return fieldMatch
224
+ return this.generateFieldHtml(field)
225
+ })
226
+
227
+ const submitMatch = innerContent.match(/<candy:submit[\s\S]*?(?:<\/candy:submit>|\/?>)/)
228
+ if (submitMatch) {
229
+ let submitAttrs = `type="submit" data-submit-text="${submitText}" data-loading-text="${submitLoading}"`
230
+ if (config.submitClass) submitAttrs += ` class="${config.submitClass}"`
231
+ if (config.submitStyle) submitAttrs += ` style="${config.submitStyle}"`
232
+ if (config.submitId) submitAttrs += ` id="${config.submitId}"`
233
+ const submitButton = `<button ${submitAttrs}>${submitText}</button>`
234
+ innerContent = innerContent.replace(submitMatch[0], submitButton)
235
+ }
236
+
237
+ innerContent = innerContent.replace(/<candy:set[^>]*\/?>/g, '')
238
+
239
+ let html = `<form class="candy-register-form" data-candy-register="${formToken}" method="POST" action="/_candy/register" novalidate>\n`
240
+ html += ` <input type="hidden" name="_candy_register_token" value="${formToken}">\n`
241
+ html += innerContent
242
+ html += `\n <span class="candy-form-success" style="display:none;"></span>\n`
243
+ html += `</form>`
244
+
245
+ return html
246
+ }
247
+
248
+ static generateFieldHtml(field) {
249
+ let html = ''
250
+
251
+ if (field.label && field.type !== 'checkbox') {
252
+ const fieldId = field.id || `candy-${field.name}`
253
+ html += `<label for="${fieldId}">${field.label}</label>\n`
254
+ }
255
+
256
+ const classAttr = field.class ? ` class="${field.class}"` : ''
257
+ const idAttr = field.id ? ` id="${field.id}"` : ` id="candy-${field.name}"`
258
+
259
+ if (field.type === 'checkbox') {
260
+ const attrs = this.buildHtml5Attributes(field)
261
+ if (field.label) {
262
+ html += `<label>\n`
263
+ html += ` <input type="checkbox"${idAttr} name="${field.name}" value="1"${classAttr}${attrs}>\n`
264
+ html += ` ${field.label}\n`
265
+ html += `</label>\n`
266
+ } else {
267
+ html += `<input type="checkbox"${idAttr} name="${field.name}" value="1"${classAttr}${attrs}>\n`
268
+ }
269
+ } else if (field.type === 'textarea') {
270
+ const attrs = this.buildHtml5Attributes(field)
271
+ html += `<textarea${idAttr} name="${field.name}" placeholder="${field.placeholder}"${classAttr}${attrs}></textarea>\n`
272
+ } else {
273
+ const attrs = this.buildHtml5Attributes(field)
274
+ html += `<input type="${field.type}"${idAttr} name="${field.name}" placeholder="${field.placeholder}"${classAttr}${attrs}>\n`
275
+ }
276
+
277
+ return html
278
+ }
279
+
280
+ static buildHtml5Attributes(field) {
281
+ let attrs = ''
282
+ const html5Rules = {
283
+ required: false,
284
+ minlength: null,
285
+ maxlength: null,
286
+ min: null,
287
+ max: null,
288
+ pattern: null
289
+ }
290
+ const errorMessages = {}
291
+
292
+ for (const validation of field.validations) {
293
+ const rules = validation.rule.split('|')
294
+ for (const rule of rules) {
295
+ const [ruleName, ruleValue] = rule.split(':')
296
+
297
+ switch (ruleName) {
298
+ case 'required':
299
+ html5Rules.required = true
300
+ if (validation.message) errorMessages.required = validation.message
301
+ break
302
+ case 'minlen':
303
+ if (field.type !== 'number') {
304
+ html5Rules.minlength = ruleValue
305
+ if (validation.message) errorMessages.minlength = validation.message
306
+ }
307
+ break
308
+ case 'maxlen':
309
+ if (field.type !== 'number') {
310
+ html5Rules.maxlength = ruleValue
311
+ if (validation.message) errorMessages.maxlength = validation.message
312
+ }
313
+ break
314
+ case 'min':
315
+ if (field.type === 'number') html5Rules.min = ruleValue
316
+ break
317
+ case 'max':
318
+ if (field.type === 'number') html5Rules.max = ruleValue
319
+ break
320
+ case 'email':
321
+ if (validation.message) errorMessages.email = validation.message
322
+ break
323
+ case 'url':
324
+ break
325
+ case 'numeric':
326
+ if (field.type === 'text') {
327
+ html5Rules.pattern = '[0-9]+'
328
+ if (validation.message) errorMessages.pattern = validation.message
329
+ }
330
+ break
331
+ case 'alpha':
332
+ if (field.type === 'text') {
333
+ html5Rules.pattern = '[a-zA-Z]+'
334
+ if (validation.message) errorMessages.pattern = validation.message
335
+ }
336
+ break
337
+ case 'alphanumeric':
338
+ if (field.type === 'text') {
339
+ html5Rules.pattern = '[a-zA-Z0-9]+'
340
+ if (validation.message) errorMessages.pattern = validation.message
341
+ }
342
+ break
343
+ case 'accepted':
344
+ if (field.type === 'checkbox') {
345
+ html5Rules.required = true
346
+ if (validation.message) errorMessages.required = validation.message
347
+ }
348
+ break
349
+ }
350
+ }
351
+ }
352
+
353
+ if (html5Rules.required) attrs += ' required'
354
+ if (html5Rules.minlength) attrs += ` minlength="${html5Rules.minlength}"`
355
+ if (html5Rules.maxlength) attrs += ` maxlength="${html5Rules.maxlength}"`
356
+ if (html5Rules.min) attrs += ` min="${html5Rules.min}"`
357
+ if (html5Rules.max) attrs += ` max="${html5Rules.max}"`
358
+ if (html5Rules.pattern) attrs += ` pattern="${html5Rules.pattern}"`
359
+
360
+ if (errorMessages.required) attrs += ` data-error-required="${errorMessages.required.replace(/"/g, '&quot;')}"`
361
+ if (errorMessages.minlength) attrs += ` data-error-minlength="${errorMessages.minlength.replace(/"/g, '&quot;')}"`
362
+ if (errorMessages.maxlength) attrs += ` data-error-maxlength="${errorMessages.maxlength.replace(/"/g, '&quot;')}"`
363
+ if (errorMessages.pattern) attrs += ` data-error-pattern="${errorMessages.pattern.replace(/"/g, '&quot;')}"`
364
+ if (errorMessages.email) attrs += ` data-error-email="${errorMessages.email.replace(/"/g, '&quot;')}"`
365
+
366
+ return attrs
367
+ }
368
+
369
+ static extractLoginConfig(html, formToken) {
370
+ const config = {
371
+ token: formToken,
372
+ redirect: null,
373
+ submitText: 'Login',
374
+ submitLoading: 'Logging in...',
375
+ fields: []
376
+ }
377
+
378
+ const loginMatch = html.match(/<candy:login([^>]*)>/)
379
+ if (!loginMatch) return config
380
+
381
+ const loginTag = loginMatch[0]
382
+ const redirectMatch = loginTag.match(/redirect=["']([^"']+)["']/)
383
+
384
+ if (redirectMatch) config.redirect = redirectMatch[1]
385
+
386
+ const submitMatch = html.match(/<candy:submit([^>/]*)(?:\/?>|>(.*?)<\/candy:submit>)/)
387
+ if (submitMatch) {
388
+ const submitTag = submitMatch[1]
389
+ const textMatch = submitTag.match(/text=["']([^"']+)["']/)
390
+ const loadingMatch = submitTag.match(/loading=["']([^"']+)["']/)
391
+ const classMatch = submitTag.match(/class=["']([^"']+)["']/)
392
+ const styleMatch = submitTag.match(/style=["']([^"']+)["']/)
393
+ const idMatch = submitTag.match(/id=["']([^"']+)["']/)
394
+
395
+ if (textMatch) config.submitText = textMatch[1]
396
+ else if (submitMatch[2]) config.submitText = submitMatch[2].trim()
397
+
398
+ if (loadingMatch) config.submitLoading = loadingMatch[1]
399
+ if (classMatch) config.submitClass = classMatch[1]
400
+ if (styleMatch) config.submitStyle = styleMatch[1]
401
+ if (idMatch) config.submitId = idMatch[1]
402
+ }
403
+
404
+ const fieldMatches = html.match(/<candy:field[\s\S]*?<\/candy:field>/g)
405
+ if (fieldMatches) {
406
+ for (const fieldHtml of fieldMatches) {
407
+ const field = this.parseField(fieldHtml)
408
+ if (field) config.fields.push(field)
409
+ }
410
+ }
411
+
412
+ return config
413
+ }
414
+
415
+ static storeLoginConfig(token, config, Candy) {
416
+ if (!Candy.View) Candy.View = {}
417
+ if (!Candy.View.loginForms) Candy.View.loginForms = {}
418
+
419
+ const formData = {
420
+ config: config,
421
+ created: Date.now(),
422
+ expires: Date.now() + 30 * 60 * 1000,
423
+ sessionId: Candy.Request.session('_client'),
424
+ userAgent: Candy.Request.header('user-agent'),
425
+ ip: Candy.Request.ip
426
+ }
427
+
428
+ Candy.View.loginForms[token] = formData
429
+ Candy.Request.session(`_login_form_${token}`, formData)
430
+ }
431
+
432
+ static generateLoginForm(originalHtml, config, formToken) {
433
+ const submitText = config.submitText || 'Login'
434
+ const submitLoading = config.submitLoading || 'Logging in...'
435
+
436
+ let innerContent = originalHtml.replace(/<candy:login[^>]*>/, '').replace(/<\/candy:login>/, '')
437
+
438
+ innerContent = innerContent.replace(/<candy:field[\s\S]*?<\/candy:field>/g, fieldMatch => {
439
+ const field = this.parseField(fieldMatch)
440
+ if (!field) return fieldMatch
441
+ return this.generateFieldHtml(field)
442
+ })
443
+
444
+ const submitMatch = innerContent.match(/<candy:submit[\s\S]*?(?:<\/candy:submit>|\/?>)/)
445
+ if (submitMatch) {
446
+ let submitAttrs = `type="submit" data-submit-text="${submitText}" data-loading-text="${submitLoading}"`
447
+ if (config.submitClass) submitAttrs += ` class="${config.submitClass}"`
448
+ if (config.submitStyle) submitAttrs += ` style="${config.submitStyle}"`
449
+ if (config.submitId) submitAttrs += ` id="${config.submitId}"`
450
+ const submitButton = `<button ${submitAttrs}>${submitText}</button>`
451
+ innerContent = innerContent.replace(submitMatch[0], submitButton)
452
+ }
453
+
454
+ let html = `<form class="candy-login-form" data-candy-login="${formToken}" method="POST" action="/_candy/login" novalidate>\n`
455
+ html += ` <input type="hidden" name="_candy_login_token" value="${formToken}">\n`
456
+ html += innerContent
457
+ html += `\n <span class="candy-form-success" style="display:none;"></span>\n`
458
+ html += `</form>`
459
+
460
+ return html
461
+ }
462
+
463
+ static extractFormConfig(html, formToken) {
464
+ const config = {
465
+ token: formToken,
466
+ action: null,
467
+ method: 'POST',
468
+ submitText: 'Submit',
469
+ submitLoading: 'Processing...',
470
+ fields: [],
471
+ sets: [],
472
+ class: '',
473
+ id: null,
474
+ table: null,
475
+ redirect: null,
476
+ successMessage: null
477
+ }
478
+
479
+ const formMatch = html.match(/<candy:form([^>]*)>/)
480
+ if (!formMatch) return config
481
+
482
+ const formTag = formMatch[0]
483
+ const extractAttr = name => {
484
+ const match = formTag.match(new RegExp(`${name}=(['"])((?:(?!\\1).)*)\\1`))
485
+ return match ? match[2] : null
486
+ }
487
+
488
+ const actionMatch = extractAttr('action')
489
+ const methodMatch = extractAttr('method')
490
+ const classMatch = extractAttr('class')
491
+ const idMatch = extractAttr('id')
492
+ const tableMatch = extractAttr('table')
493
+ const redirectMatch = extractAttr('redirect')
494
+ const successMatch = extractAttr('success')
495
+
496
+ if (actionMatch) config.action = actionMatch
497
+ if (methodMatch) config.method = methodMatch.toUpperCase()
498
+ if (classMatch) config.class = classMatch
499
+ if (idMatch) config.id = idMatch
500
+ if (tableMatch) config.table = tableMatch
501
+ if (redirectMatch) config.redirect = redirectMatch
502
+ if (successMatch) config.successMessage = successMatch
503
+
504
+ const submitMatch = html.match(/<candy:submit([^>/]*)(?:\/?>|>(.*?)<\/candy:submit>)/)
505
+ if (submitMatch) {
506
+ const submitTag = submitMatch[1]
507
+ const textMatch = submitTag.match(/text=["']([^"']+)["']/)
508
+ const loadingMatch = submitTag.match(/loading=["']([^"']+)["']/)
509
+ const classMatch = submitTag.match(/class=["']([^"']+)["']/)
510
+ const styleMatch = submitTag.match(/style=["']([^"']+)["']/)
511
+ const idMatch = submitTag.match(/id=["']([^"']+)["']/)
512
+
513
+ if (textMatch) config.submitText = textMatch[1]
514
+ else if (submitMatch[2]) config.submitText = submitMatch[2].trim()
515
+
516
+ if (loadingMatch) config.submitLoading = loadingMatch[1]
517
+ if (classMatch) config.submitClass = classMatch[1]
518
+ if (styleMatch) config.submitStyle = styleMatch[1]
519
+ if (idMatch) config.submitId = idMatch[1]
520
+ }
521
+
522
+ const fieldMatches = html.match(/<candy:field[\s\S]*?<\/candy:field>/g)
523
+ if (fieldMatches) {
524
+ for (const fieldHtml of fieldMatches) {
525
+ const field = this.parseField(fieldHtml)
526
+ if (field) config.fields.push(field)
527
+ }
528
+ }
529
+
530
+ const setMatches = html.match(/<candy:set[^>]*\/?>/g)
531
+ if (setMatches) {
532
+ for (const setTag of setMatches) {
533
+ const set = this.parseSet(setTag)
534
+ if (set) config.sets.push(set)
535
+ }
536
+ }
537
+
538
+ return config
539
+ }
540
+
541
+ static storeFormConfig(token, config, Candy) {
542
+ if (!Candy.View) Candy.View = {}
543
+ if (!Candy.View.customForms) Candy.View.customForms = {}
544
+
545
+ const formData = {
546
+ config: config,
547
+ created: Date.now(),
548
+ expires: Date.now() + 30 * 60 * 1000,
549
+ sessionId: Candy.Request.session('_client'),
550
+ userAgent: Candy.Request.header('user-agent'),
551
+ ip: Candy.Request.ip
552
+ }
553
+
554
+ Candy.View.customForms[token] = formData
555
+ Candy.Request.session(`_custom_form_${token}`, formData)
556
+ }
557
+
558
+ static generateCustomForm(originalHtml, config, formToken) {
559
+ const submitText = config.submitText || 'Submit'
560
+ const submitLoading = config.submitLoading || 'Processing...'
561
+ const action = config.action || '/_candy/form'
562
+ const method = config.method || 'POST'
563
+
564
+ let innerContent = originalHtml.replace(/<candy:form[^>]*>/, '').replace(/<\/candy:form>/, '')
565
+
566
+ innerContent = innerContent.replace(/<candy:field[\s\S]*?<\/candy:field>/g, fieldMatch => {
567
+ const field = this.parseField(fieldMatch)
568
+ if (!field) return fieldMatch
569
+ return this.generateFieldHtml(field)
570
+ })
571
+
572
+ const escapeHtml = str =>
573
+ String(str).replace(/[&<>"']/g, m => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'})[m])
574
+
575
+ const submitMatch = innerContent.match(/<candy:submit[\s\S]*?(?:<\/candy:submit>|\/?>)/)
576
+ if (submitMatch) {
577
+ let submitAttrs = `type="submit" data-submit-text="${escapeHtml(submitText)}" data-loading-text="${escapeHtml(submitLoading)}"`
578
+ if (config.submitClass) submitAttrs += ` class="${escapeHtml(config.submitClass)}"`
579
+ if (config.submitStyle) submitAttrs += ` style="${escapeHtml(config.submitStyle)}"`
580
+ if (config.submitId) submitAttrs += ` id="${escapeHtml(config.submitId)}"`
581
+ const submitButton = `<button ${submitAttrs}>${escapeHtml(submitText)}</button>`
582
+ innerContent = innerContent.replace(submitMatch[0], submitButton)
583
+ }
584
+
585
+ innerContent = innerContent.replace(/<candy:set[^>]*\/?>/g, '')
586
+
587
+ let formAttrs = `class="candy-custom-form${config.class ? ' ' + escapeHtml(config.class) : ''}" data-candy-form="${escapeHtml(formToken)}" method="${escapeHtml(method)}" action="${escapeHtml(action)}" novalidate`
588
+ if (config.id) formAttrs += ` id="${escapeHtml(config.id)}"`
589
+
590
+ let html = `<form ${formAttrs}>\n`
591
+ html += ` <input type="hidden" name="_candy_form_token" value="${escapeHtml(formToken)}">\n`
592
+ html += innerContent
593
+ html += `\n <span class="candy-form-success" style="display:none;"></span>\n`
594
+ html += `</form>`
595
+
596
+ return html
597
+ }
598
+ }
599
+
600
+ module.exports = Form