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,271 @@
1
+ class Validator {
2
+ #checklist = {}
3
+ #completed = false
4
+ #message = {}
5
+ #method = 'POST'
6
+ #name = ''
7
+ #request
8
+
9
+ constructor(Request) {
10
+ this.#request = Request
11
+ }
12
+
13
+ check(rules) {
14
+ if (!this.#checklist[this.#method]) this.#checklist[this.#method] = {}
15
+ if (!this.#checklist[this.#method][this.#name]) this.#checklist[this.#method][this.#name] = []
16
+ this.#checklist[this.#method][this.#name].push({rules: rules, message: null})
17
+ return this
18
+ }
19
+
20
+ async error() {
21
+ if (!this.#completed) await this.#validate()
22
+ return Object.keys(this.#message).length > 0
23
+ }
24
+
25
+ get(key) {
26
+ if (this.#completed) this.#completed = false
27
+ this.#method = 'GET'
28
+ this.#name = key
29
+ return this
30
+ }
31
+
32
+ message(value) {
33
+ const checks = this.#checklist[this.#method][this.#name]
34
+ if (checks && checks.length > 0) {
35
+ checks[checks.length - 1].message = value
36
+ }
37
+ return this
38
+ }
39
+
40
+ post(key) {
41
+ if (this.#completed) this.#completed = false
42
+ this.#method = 'POST'
43
+ this.#name = key
44
+ return this
45
+ }
46
+
47
+ async result(message, data) {
48
+ if (!this.#completed) await this.#validate()
49
+ let result = {}
50
+ result.result = {}
51
+ result.result.success = Object.keys(this.#message).length === 0
52
+ if (result.result.success) {
53
+ result.result.message = message ?? ''
54
+ result.data = data ?? null
55
+ } else {
56
+ result.errors = this.#message['_candy_form'] ? {_candy_form: this.#message['_candy_form']} : this.#message
57
+ }
58
+ return result
59
+ }
60
+
61
+ success(callback) {
62
+ if (typeof callback === 'string') return this.result(callback)
63
+ else return this.result(null, callback)
64
+ }
65
+
66
+ async #validate() {
67
+ for (const method of Object.keys(this.#checklist)) {
68
+ for (const key of Object.keys(this.#checklist[method])) {
69
+ const checks = this.#checklist[method][key]
70
+ let value
71
+
72
+ if (method === 'VAR') {
73
+ value = checks.customValue
74
+ } else if (method === 'FILES') {
75
+ value = this.#request.file ? await this.#request.file(key) : null
76
+ } else {
77
+ value = await this.#request.request(key, method)
78
+ }
79
+
80
+ for (const checkItem of checks) {
81
+ if (this.#message[key]) break
82
+
83
+ let error = false
84
+ let rules = checkItem.rules
85
+
86
+ if (typeof rules === 'boolean') {
87
+ error = rules === false
88
+ } else {
89
+ for (const rule of rules.includes('|') ? rules.split('|') : [rules]) {
90
+ let vars = rule.split(':')
91
+ let inverse = vars[0].startsWith('!')
92
+ if (!error) {
93
+ switch (inverse ? vars[0].substr(1) : vars[0]) {
94
+ case 'required':
95
+ error = value === undefined || value === '' || value === null
96
+ break
97
+ case 'accepted':
98
+ error = !value || (value !== 1 && value !== '1' && value !== 'on' && value !== 'yes' && value !== true)
99
+ break
100
+ case 'numeric':
101
+ error = value && value !== '' && !Candy.Var(value).is('numeric')
102
+ break
103
+ case 'alpha':
104
+ error = value && value !== '' && !Candy.Var(value).is('alpha')
105
+ break
106
+ case 'alphaspace':
107
+ error = value && value !== '' && !Candy.Var(value).is('alphaspace')
108
+ break
109
+ case 'alphanumeric':
110
+ error = value && value !== '' && !Candy.Var(value).is('alphanumeric')
111
+ break
112
+ case 'alphanumericspace':
113
+ error = value && value !== '' && !Candy.Var(value).is('alphanumericspace')
114
+ break
115
+ case 'email':
116
+ error = value && value !== '' && !Candy.Var(value).is('email')
117
+ break
118
+ case 'ip':
119
+ error = value && value !== '' && !Candy.Var(value).is('ip')
120
+ break
121
+ case 'float':
122
+ error = value && value !== '' && !Candy.Var(value).is('float')
123
+ break
124
+ case 'mac':
125
+ error = value && value !== '' && !Candy.Var(value).is('mac')
126
+ break
127
+ case 'domain':
128
+ error = value && value !== '' && !Candy.Var(value).is('domain')
129
+ break
130
+ case 'url':
131
+ error = value && value !== '' && !Candy.Var(value).is('url')
132
+ break
133
+ case 'username':
134
+ error = value && value !== '' && !/^[a-zA-Z0-9]+$/.test(value)
135
+ break
136
+ case 'xss':
137
+ error = value && value !== '' && /<[^>]*>/g.test(value)
138
+ break
139
+ case 'usercheck':
140
+ error = !(await Candy.Auth.check())
141
+ break
142
+ case 'array':
143
+ error = value && !Array.isArray(value)
144
+ break
145
+ case 'date':
146
+ error = value && value !== '' && isNaN(Date.parse(value))
147
+ break
148
+ case 'min':
149
+ error = value && value !== '' && vars[1] && value < vars[1]
150
+ break
151
+ case 'max':
152
+ error = value && value !== '' && vars[1] && value > vars[1]
153
+ break
154
+ case 'len':
155
+ error = value && value !== '' && vars[1] && String(value).length !== parseInt(vars[1])
156
+ break
157
+ case 'minlen':
158
+ error = value && value !== '' && vars[1] && String(value).length < parseInt(vars[1])
159
+ break
160
+ case 'maxlen':
161
+ error = value && value !== '' && vars[1] && String(value).length > parseInt(vars[1])
162
+ break
163
+ case 'mindate':
164
+ error = value && value !== '' && vars[1] && new Date(value).getTime() < new Date(vars[1]).getTime()
165
+ break
166
+ case 'maxdate':
167
+ error = value && value !== '' && vars[1] && new Date(value).getTime() > new Date(vars[1]).getTime()
168
+ break
169
+ case 'same': {
170
+ const otherValue = await this.#request.request(vars[1], method)
171
+ error = value !== otherValue
172
+ break
173
+ }
174
+ case 'different': {
175
+ const otherValue = await this.#request.request(vars[1], method)
176
+ error = value === otherValue
177
+ break
178
+ }
179
+ case 'equal':
180
+ error = value && vars[1] && value !== vars[1]
181
+ break
182
+ case 'notin':
183
+ error = value && value !== '' && vars[1] && String(value).includes(vars[1])
184
+ break
185
+ case 'in':
186
+ error = value && value !== '' && vars[1] && !String(value).includes(vars[1])
187
+ break
188
+ case 'not':
189
+ error = value && vars[1] && value === vars[1]
190
+ break
191
+ case 'regex':
192
+ error = value && value !== '' && vars[1] && !new RegExp(vars[1]).test(value)
193
+ break
194
+ case 'user': {
195
+ if (!(await Candy.Auth.check())) {
196
+ error = true
197
+ } else {
198
+ const userData = Candy.Auth.user(vars[1])
199
+ if (Candy.Var(userData).is('bcrypt')) {
200
+ error = !Candy.Var(userData).hashCheck(value)
201
+ } else {
202
+ error = value !== userData
203
+ }
204
+ }
205
+ break
206
+ }
207
+ }
208
+ if (inverse) error = !error
209
+ }
210
+ }
211
+ }
212
+
213
+ if (error) {
214
+ this.#message[key] = checkItem.message
215
+ break
216
+ }
217
+ }
218
+ }
219
+ }
220
+ this.#completed = true
221
+ }
222
+
223
+ var(name, value = null) {
224
+ if (this.#completed) this.#completed = false
225
+ this.#method = 'VAR'
226
+ this.#name = name
227
+ if (!this.#checklist[this.#method]) this.#checklist[this.#method] = {}
228
+ if (!this.#checklist[this.#method][name]) {
229
+ this.#checklist[this.#method][name] = []
230
+ this.#checklist[this.#method][name].customValue = value === null ? name : value
231
+ }
232
+ return this
233
+ }
234
+
235
+ file(name) {
236
+ if (this.#completed) this.#completed = false
237
+ this.#method = 'FILES'
238
+ this.#name = name
239
+ return this
240
+ }
241
+
242
+ async brute(maxAttempts = 5) {
243
+ const ip = this.#request.ip()
244
+ const now = new Date().toISOString().slice(0, 13).replace(/[-:T]/g, '')
245
+ const page = this.#request.path()
246
+ const storage = Candy.storage('sys')
247
+ const validation = storage.get('validation') || {}
248
+
249
+ this.#name = '_candy_form'
250
+
251
+ if (Object.keys(this.#message).length > 0) {
252
+ if (!validation.brute) validation.brute = {}
253
+ if (!validation.brute[now]) validation.brute[now] = {}
254
+ if (!validation.brute[now][page]) validation.brute[now][page] = {}
255
+ if (!validation.brute[now][page][ip]) validation.brute[now][page][ip] = 0
256
+
257
+ validation.brute[now][page][ip]++
258
+
259
+ if (validation.brute[now][page][ip] >= maxAttempts) {
260
+ this.#message['_candy_form'] = Candy.Lang
261
+ ? Candy.Lang.get('Too many failed attempts. Please try again later.')
262
+ : 'Too many failed attempts. Please try again later.'
263
+ }
264
+ }
265
+
266
+ storage.set('validation', validation)
267
+ return this
268
+ }
269
+ }
270
+
271
+ module.exports = Validator
@@ -0,0 +1,211 @@
1
+ const fs = require('fs')
2
+ const nodeCrypto = require('crypto')
3
+ const bcrypt = require('bcrypt')
4
+
5
+ class Var {
6
+ #value = null
7
+ #any = false
8
+
9
+ constructor(value) {
10
+ this.#value = value
11
+ }
12
+
13
+ clear(...args) {
14
+ args = this.#parse(args)
15
+ let str = this.#value
16
+ for (const arg of args) str = str.replace(new RegExp(arg, 'g'), '')
17
+ return str
18
+ }
19
+
20
+ contains(...args) {
21
+ args = this.#parse(args)
22
+ let any = this.#any
23
+ this.#any = false
24
+ let result = !any
25
+ for (const key of args) {
26
+ if (any) result = result || this.#value.includes(key)
27
+ else result = result && this.#value.includes(key)
28
+ }
29
+ return result
30
+ }
31
+
32
+ containsAny(...args) {
33
+ args = this.#parse(args)
34
+ this.#any = true
35
+ return this.contains(args)
36
+ }
37
+
38
+ date(format) {
39
+ if (!format) format = 'Y-m-d H:i:s'
40
+ let date = new Date(this.#value)
41
+ let year = date.getFullYear()
42
+ let month = date.getMonth() + 1
43
+ let day = date.getDate()
44
+ let hour = date.getHours()
45
+ let minute = date.getMinutes()
46
+ let second = date.getSeconds()
47
+ return Candy.Var(format).replace({
48
+ Y: year,
49
+ m: month < 10 ? `0${month}` : month,
50
+ d: day < 10 ? `0${day}` : day,
51
+ H: hour < 10 ? `0${hour}` : hour,
52
+ i: minute < 10 ? `0${minute}` : minute,
53
+ s: second < 10 ? `0${second}` : second,
54
+ y: year.toString().substr(2, 2)
55
+ })
56
+ }
57
+
58
+ decrypt(key) {
59
+ if (!key) key = Candy.Config.encrypt.key
60
+ const iv = '2dea8a25e5e8f004'
61
+ try {
62
+ const encryptedText = Buffer.from(this.#value, 'base64')
63
+ const decipher = nodeCrypto.createDecipheriv('aes-256-cbc', key, iv)
64
+ let decrypted = decipher.update(encryptedText)
65
+ decrypted = Buffer.concat([decrypted, decipher.final()])
66
+ return decrypted.toString()
67
+ } catch (e) {
68
+ console.log(e)
69
+ return null
70
+ }
71
+ }
72
+
73
+ encrypt(key) {
74
+ if (!key) key = Candy.Config.encrypt.key
75
+ const iv = '2dea8a25e5e8f004'
76
+ const cipher = nodeCrypto.createCipheriv('aes-256-cbc', key, iv)
77
+ let encrypted = cipher.update(this.#value)
78
+ encrypted = Buffer.concat([encrypted, cipher.final()])
79
+ return encrypted.toString('base64')
80
+ }
81
+
82
+ hash(salt = 10) {
83
+ return bcrypt.hashSync(this.#value, bcrypt.genSaltSync(salt))
84
+ }
85
+
86
+ hashCheck(check) {
87
+ return bcrypt.compareSync(check, this.#value)
88
+ }
89
+
90
+ html() {
91
+ if (this.#value === null || this.#value === undefined) return ''
92
+ return String(this.#value).replace(/</g, '&lt;').replace(/>/g, '&gt;')
93
+ }
94
+
95
+ is(...args) {
96
+ args = this.#parse(args)
97
+ let any = this.#any
98
+ this.#any = false
99
+ let result = !any
100
+ // if(\Candy::config('locale')->get() == 'tr') $this->str = \Candy::var($this->str)->clear('Ç','ç','Ğ','ğ','İ','ı','Ö','ö','Ş','ş','Ü','ü');
101
+ if (args.includes('alpha')) result = (result || any) && ((any && result) || /^[A-Za-z]+$/.test(this.#value))
102
+ if (args.includes('alphaspace')) result = (result || any) && ((any && result) || /^[A-Za-z\s]+$/.test(this.#value))
103
+ if (args.includes('alphanumeric')) result = (result || any) && ((any && result) || /^[A-Za-z0-9]+$/.test(this.#value))
104
+ if (args.includes('alphanumericspace')) result = (result || any) && ((any && result) || /^[A-Za-z0-9\s]+$/.test(this.#value))
105
+ if (args.includes('bcrypt')) result = (result || any) && ((any && result) || /^\$2[ayb]\$.{56}$/.test(this.#value))
106
+ if (args.includes('date')) result = (result || any) && ((any && result) || !isNaN(Date.parse(this.#value)))
107
+ if (args.includes('domain')) result = (result || any) && ((any && result) || /^([a-z0-9-]+\.){1,2}[a-z]{2,6}$/i.test(this.#value))
108
+ if (args.includes('email'))
109
+ result = (result || any) && ((any && result) || /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(this.#value))
110
+ if (args.includes('float')) result = (result || any) && ((any && result) || !isNaN(parseFloat(this.#value)))
111
+ if (args.includes('host')) result = (result || any) && ((any && result) || /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/.test(this.#value))
112
+ if (args.includes('ip')) result = (result || any) && ((any && result) || /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/.test(this.#value))
113
+ if (args.includes('json'))
114
+ result = (result || any) && ((any && result) || (JSON.parse(this.#value) && JSON.parse(this.#value).length >= 0))
115
+ if (args.includes('mac')) result = (result || any) && ((any && result) || /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(this.#value))
116
+ if (args.includes('md5')) result = (result || any) && ((any && result) || /^[a-f0-9A-F]{32}$/.test(this.#value))
117
+ if (args.includes('numeric')) result = (result || any) && ((any && result) || !isNaN(this.#value))
118
+ if (args.includes('url')) result = (result || any) && ((any && result) || /^[a-z0-9]+:\/\/[a-z0-9]+\.[a-z]{2,6}/i.test(this.#value))
119
+ if (args.includes('emoji'))
120
+ result =
121
+ (result || any) &&
122
+ ((any && result) ||
123
+ /([0-9#][\u20E3])|[\u00ae\u00a9\u203C\u2047\u2048\u2049\u3030\u303D\u2139\u2122\u3297\u3299][\uFE00-\uFEFF]?|[\u2190-\u21FF][\uFE00-\uFEFF]?|[\u2300-\u23FF][\uFE00-\uFEFF]?|[\u2460-\u24FF][\uFE00-\uFEFF]?|[\u25A0-\u25FF][\uFE00-\uFEFF]?|[\u2600-\u27BF][\uFE00-\uFEFF]?|[\u2900-\u297F][\uFE00-\uFEFF]?|[\u2B00-\u2BF0][\uFE00-\uFEFF]?|[\u1F000-\u1F6FF][\uFE00-\uFEFF]?/u.test(
124
+ this.#value
125
+ ))
126
+ if (args.includes('xss')) result = (result || any) && ((any && result) || this.#value == this.#value.replace(/<[^>]*>/g, ''))
127
+ return result
128
+ }
129
+
130
+ isAny(...args) {
131
+ args = this.#parse(args)
132
+ this.#any = true
133
+ return this.is(args)
134
+ }
135
+
136
+ isBegin(...args) {
137
+ args = this.#parse(args)
138
+ for (const arg of args) if (this.#value.startsWith(arg)) return true
139
+ return false
140
+ }
141
+
142
+ isEnd(...args) {
143
+ args = this.#parse(args)
144
+ for (const arg of args) if (this.#value.endsWith(arg)) return true
145
+ return false
146
+ }
147
+
148
+ md5() {
149
+ return nodeCrypto.createHash('md5').update(this.#value).digest('hex')
150
+ }
151
+
152
+ #parse(value) {
153
+ if (!['array', 'object'].includes(typeof value)) return [value]
154
+ if (value.length == 1 && Array.isArray(value[0])) return value[0]
155
+ return value
156
+ }
157
+
158
+ replace(...args) {
159
+ args = this.#parse(args)
160
+ if (args.length == 1) args = args[0]
161
+ if (['array', 'object'].includes(typeof this.#value)) {
162
+ let new_value = {}
163
+ for (const key of Object.keys(this.#value)) new_value[key] = Candy.Var(this.#value[key]).replace(args)
164
+ return new_value
165
+ }
166
+ for (const arg of Object.keys(args)) this.#value = this.#value.replace(arg, args[arg])
167
+ return this.#value
168
+ }
169
+
170
+ save(path) {
171
+ if (this.#value.includes('/')) {
172
+ let exp = path.split('/')
173
+ exp.pop()
174
+ let dir = ''
175
+ for (const key of exp) {
176
+ dir += (dir === '' ? '' : '/') + key
177
+ if (!fs.existsSync(dir) || !fs.lstatSync(dir).isDirectory()) fs.mkdirSync(dir)
178
+ }
179
+ }
180
+ return fs.writeFileSync(path, this.#value)
181
+ }
182
+
183
+ slug(separator = '-') {
184
+ let str = this.#value
185
+ str = str.replace(/[^a-zA-Z0-9\s]/g, separator)
186
+ str = str.replace(/[\s]/g, separator)
187
+ str = str.replace(/[-]+/g, separator)
188
+ str = str.toLowerCase()
189
+ return str
190
+ }
191
+
192
+ format(format) {
193
+ let str = this.#value
194
+ let result = ''
195
+ let letter = 0
196
+ for (let i = 0; i < format.length; i++) {
197
+ if (format[i] == '?') {
198
+ result += str[letter]
199
+ letter++
200
+ } else if (format[i] == '*') {
201
+ result += str.substr(letter)
202
+ letter += str.substr(letter).length
203
+ } else {
204
+ result += format[i]
205
+ }
206
+ }
207
+ return result
208
+ }
209
+ }
210
+
211
+ module.exports = Var
@@ -0,0 +1,190 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ class EarlyHints {
5
+ #manifest = {}
6
+ #cache = new Map()
7
+ #config = null
8
+ #initialized = false
9
+
10
+ constructor(config) {
11
+ this.#config = config || {
12
+ enabled: true,
13
+ auto: true,
14
+ maxResources: 5
15
+ }
16
+ }
17
+
18
+ init() {
19
+ if (this.#initialized) return
20
+ this.#initialized = true
21
+
22
+ if (!this.#config.enabled) return
23
+
24
+ this.#buildManifest()
25
+ }
26
+
27
+ #buildManifest() {
28
+ const viewDir = path.join(process.cwd(), 'view')
29
+ const skeletonDir = path.join(process.cwd(), 'skeleton')
30
+
31
+ try {
32
+ if (fs.existsSync(viewDir)) {
33
+ const files = this.#getAllViewFiles(viewDir)
34
+ for (const file of files) {
35
+ const html = fs.readFileSync(file, 'utf8')
36
+ const resources = this.#extractResources(html)
37
+
38
+ const relativePath = path.relative(viewDir, file)
39
+ const viewName = 'view/' + relativePath.replace(/\.html$/, '').replace(/\\/g, '/')
40
+
41
+ if (resources.length > 0) {
42
+ this.#manifest[viewName] = resources
43
+ }
44
+ }
45
+ }
46
+
47
+ if (fs.existsSync(skeletonDir)) {
48
+ const files = this.#getAllViewFiles(skeletonDir)
49
+ for (const file of files) {
50
+ const html = fs.readFileSync(file, 'utf8')
51
+ const resources = this.#extractResources(html)
52
+
53
+ const relativePath = path.relative(skeletonDir, file)
54
+ const viewName = 'skeleton/' + relativePath.replace(/\.html$/, '').replace(/\\/g, '/')
55
+
56
+ if (resources.length > 0) {
57
+ this.#manifest[viewName] = resources
58
+ }
59
+ }
60
+ }
61
+ } catch {
62
+ // Silently fail, manifest building is optional
63
+ }
64
+ }
65
+
66
+ #getAllViewFiles(dir, files = []) {
67
+ const entries = fs.readdirSync(dir, {withFileTypes: true})
68
+
69
+ for (const entry of entries) {
70
+ const fullPath = path.join(dir, entry.name)
71
+ if (entry.isDirectory()) {
72
+ this.#getAllViewFiles(fullPath, files)
73
+ } else if (entry.isFile() && entry.name.endsWith('.html')) {
74
+ files.push(fullPath)
75
+ }
76
+ }
77
+
78
+ return files
79
+ }
80
+
81
+ #extractResources(html) {
82
+ const resources = []
83
+
84
+ const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i)
85
+ if (!headMatch) return resources
86
+
87
+ const head = headMatch[1]
88
+
89
+ const cssMatches = head.matchAll(/<link[^>]+href=["']([^"']+\.css)["'][^>]*>/gi)
90
+ for (const match of cssMatches) {
91
+ const fullTag = match[0].toLowerCase()
92
+ if (fullTag.includes('defer')) continue
93
+ if (fullTag.includes('rel="stylesheet"') || fullTag.includes("rel='stylesheet'")) {
94
+ resources.push({href: match[1], as: 'style'})
95
+ }
96
+ }
97
+
98
+ const jsMatches = head.matchAll(/<script[^>]+src=["']([^"']+\.js)["'][^>]*>/gi)
99
+ for (const match of jsMatches) {
100
+ const fullTag = match[0]
101
+ if (!fullTag.includes('defer') && !fullTag.includes('async')) {
102
+ resources.push({href: match[1], as: 'script'})
103
+ }
104
+ }
105
+
106
+ const fontMatches = head.matchAll(/<link[^>]+href=["']([^"']+\.(woff2?|ttf|otf|eot))["'][^>]*>/gi)
107
+ for (const match of fontMatches) {
108
+ const fullTag = match[0]
109
+ if (fullTag.includes('defer')) continue
110
+ resources.push({
111
+ href: match[1],
112
+ as: 'font',
113
+ crossorigin: 'anonymous'
114
+ })
115
+ }
116
+
117
+ return resources.slice(0, this.#config.maxResources)
118
+ }
119
+
120
+ getHints(viewPath, routePath) {
121
+ if (!this.#config.enabled || !this.#config.auto) return null
122
+
123
+ let hints = this.#manifest[viewPath]
124
+
125
+ if (!hints && routePath) {
126
+ hints = this.#cache.get(routePath)
127
+ }
128
+
129
+ return hints || null
130
+ }
131
+
132
+ getHintsForViewFiles(viewPaths) {
133
+ if (!this.#config.enabled || !this.#config.auto) return null
134
+
135
+ for (const viewPath of viewPaths) {
136
+ const hints = this.#manifest[viewPath]
137
+ if (hints && hints.length > 0) {
138
+ return hints
139
+ }
140
+ }
141
+
142
+ return null
143
+ }
144
+
145
+ cacheHints(routePath, resources) {
146
+ if (!this.#config.enabled || !this.#config.auto) return
147
+
148
+ if (resources && resources.length > 0) {
149
+ this.#cache.set(routePath, resources)
150
+ }
151
+ }
152
+
153
+ extractFromHtml(html) {
154
+ if (!this.#config.enabled || !this.#config.auto) return []
155
+
156
+ return this.#extractResources(html)
157
+ }
158
+
159
+ formatLinkHeader(resource) {
160
+ let header = `<${resource.href}>; rel=preload; as=${resource.as}`
161
+ if (resource.crossorigin) {
162
+ header += `; crossorigin`
163
+ }
164
+ if (resource.type) {
165
+ header += `; type=${resource.type}`
166
+ }
167
+ return header
168
+ }
169
+
170
+ send(res, resources) {
171
+ if (!this.#config.enabled || !resources || resources.length === 0) return false
172
+ if (res.headersSent || res.writableEnded) return false
173
+
174
+ try {
175
+ const links = resources.map(r => this.formatLinkHeader(r))
176
+
177
+ if (typeof res.writeEarlyHints === 'function') {
178
+ res.writeEarlyHints({link: links})
179
+ }
180
+
181
+ res.setHeader('X-Candy-Early-Hints', JSON.stringify(links))
182
+
183
+ return true
184
+ } catch {
185
+ return false
186
+ }
187
+ }
188
+ }
189
+
190
+ module.exports = EarlyHints