@h3ravel/http 11.15.0-alpha.16 → 11.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -24
- package/package.json +8 -10
- package/dist/index.cjs +0 -4240
package/dist/index.cjs
DELETED
|
@@ -1,4240 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
//#region \0rolldown/runtime.js
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
-
key = keys[i];
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
-
get: ((k) => from[k]).bind(null, key),
|
|
14
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
-
value: mod,
|
|
21
|
-
enumerable: true
|
|
22
|
-
}) : target, mod));
|
|
23
|
-
//#endregion
|
|
24
|
-
let _h3ravel_musket = require("@h3ravel/musket");
|
|
25
|
-
let _h3ravel_shared = require("@h3ravel/shared");
|
|
26
|
-
let execa = require("execa");
|
|
27
|
-
let preferred_pm = require("preferred-pm");
|
|
28
|
-
preferred_pm = __toESM(preferred_pm, 1);
|
|
29
|
-
let fs_promises = require("fs/promises");
|
|
30
|
-
let _h3ravel_contracts = require("@h3ravel/contracts");
|
|
31
|
-
let _h3ravel_foundation = require("@h3ravel/foundation");
|
|
32
|
-
let _h3ravel_support_facades = require("@h3ravel/support/facades");
|
|
33
|
-
let _h3ravel_support = require("@h3ravel/support");
|
|
34
|
-
let h3 = require("h3");
|
|
35
|
-
let node_path = require("node:path");
|
|
36
|
-
node_path = __toESM(node_path, 1);
|
|
37
|
-
//#region src/Commands/FireCommand.ts
|
|
38
|
-
var FireCommand = class extends _h3ravel_musket.Command {
|
|
39
|
-
/**
|
|
40
|
-
* The name and signature of the console command.
|
|
41
|
-
*
|
|
42
|
-
* @var string
|
|
43
|
-
*/
|
|
44
|
-
signature = `fire:
|
|
45
|
-
{--a|host=localhost : The host address to serve the application on}
|
|
46
|
-
{--p|port=3000 : The port to serve the application on}
|
|
47
|
-
{--t|tries=10 : The max number of ports to attempt to serve from}
|
|
48
|
-
`;
|
|
49
|
-
/**
|
|
50
|
-
* The console command description.
|
|
51
|
-
*
|
|
52
|
-
* @var string
|
|
53
|
-
*/
|
|
54
|
-
description = "Fire up the developement server";
|
|
55
|
-
async handle() {
|
|
56
|
-
try {
|
|
57
|
-
await this.fire();
|
|
58
|
-
} catch (e) {
|
|
59
|
-
_h3ravel_shared.Logger.error(e);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async fire() {
|
|
63
|
-
const outDir = env("DIST_DIR", ".h3ravel/serve");
|
|
64
|
-
const pm = (await (0, preferred_pm.default)(base_path()))?.name ?? "pnpm";
|
|
65
|
-
const port = this.option("port");
|
|
66
|
-
const host = this.option("host");
|
|
67
|
-
const tries = this.option("tries");
|
|
68
|
-
const verbose = Number(this.option("verbose", 0));
|
|
69
|
-
const ENV_VARS = {
|
|
70
|
-
EXTENDED_DEBUG: verbose > 0 ? "true" : "false",
|
|
71
|
-
CLI_BUILD: "false",
|
|
72
|
-
NODE_ENV: "development",
|
|
73
|
-
DIST_DIR: outDir,
|
|
74
|
-
HOSTNAME: host,
|
|
75
|
-
RETRIES: tries,
|
|
76
|
-
PORT: port,
|
|
77
|
-
VERBOSE: verbose,
|
|
78
|
-
LOG_LEVEL: [
|
|
79
|
-
"silent",
|
|
80
|
-
"silent",
|
|
81
|
-
"info",
|
|
82
|
-
"warn",
|
|
83
|
-
"error"
|
|
84
|
-
][verbose]
|
|
85
|
-
};
|
|
86
|
-
await (0, execa.execa)(pm, [
|
|
87
|
-
"tsdown",
|
|
88
|
-
ENV_VARS.LOG_LEVEL === "silent" ? "--silent" : null,
|
|
89
|
-
"--config-loader",
|
|
90
|
-
"unrun",
|
|
91
|
-
"-c",
|
|
92
|
-
"tsdown.default.config.ts"
|
|
93
|
-
].filter((e) => e !== null), {
|
|
94
|
-
stdout: "inherit",
|
|
95
|
-
stderr: "inherit",
|
|
96
|
-
cwd: base_path(),
|
|
97
|
-
env: Object.assign({}, process.env, ENV_VARS)
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
//#endregion
|
|
102
|
-
//#region src/Exceptions/BadRequestException.ts
|
|
103
|
-
var BadRequestException = class extends Error {
|
|
104
|
-
constructor(message) {
|
|
105
|
-
super(message);
|
|
106
|
-
this.name = "BadRequestException";
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
//#endregion
|
|
110
|
-
//#region src/Exceptions/ConflictingHeadersException.ts
|
|
111
|
-
var ConflictingHeadersException = class extends Error {
|
|
112
|
-
constructor(message) {
|
|
113
|
-
super(message);
|
|
114
|
-
this.name = "ConflictingHeadersException";
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region src/Exceptions/HttpResponseException.ts
|
|
119
|
-
var HttpResponseException = class extends Error {
|
|
120
|
-
/**
|
|
121
|
-
* The underlying response instance.
|
|
122
|
-
*/
|
|
123
|
-
response;
|
|
124
|
-
/**
|
|
125
|
-
* Create a new HTTP response exception instance.
|
|
126
|
-
*
|
|
127
|
-
* @param response
|
|
128
|
-
* @param previous
|
|
129
|
-
*/
|
|
130
|
-
constructor(response, previous) {
|
|
131
|
-
super(previous?.message ?? "");
|
|
132
|
-
this.name = "HttpResponseException";
|
|
133
|
-
this.response = response;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Get the underlying response instance.
|
|
137
|
-
*/
|
|
138
|
-
getResponse() {
|
|
139
|
-
return this.response;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
//#endregion
|
|
143
|
-
//#region src/Exceptions/SuspiciousOperationException.ts
|
|
144
|
-
var SuspiciousOperationException = class extends Error {
|
|
145
|
-
constructor(message) {
|
|
146
|
-
super(message);
|
|
147
|
-
this.name = "SuspiciousOperationException";
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
//#endregion
|
|
151
|
-
//#region src/Exceptions/UnexpectedValueException.ts
|
|
152
|
-
var UnexpectedValueException = class extends Error {
|
|
153
|
-
constructor(message) {
|
|
154
|
-
super(message);
|
|
155
|
-
this.name = "UnexpectedValueException";
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
//#endregion
|
|
159
|
-
//#region src/UploadedFile.ts
|
|
160
|
-
var UploadedFile = class UploadedFile {
|
|
161
|
-
originalName;
|
|
162
|
-
mimeType;
|
|
163
|
-
size;
|
|
164
|
-
content;
|
|
165
|
-
constructor(originalName, mimeType, size, content) {
|
|
166
|
-
this.originalName = originalName;
|
|
167
|
-
this.mimeType = mimeType;
|
|
168
|
-
this.size = size;
|
|
169
|
-
this.content = content;
|
|
170
|
-
}
|
|
171
|
-
static createFromBase(file) {
|
|
172
|
-
return new UploadedFile(file.name, file.type, file.size, file);
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Save to disk (Node environment only)
|
|
176
|
-
*/
|
|
177
|
-
async moveTo(destination) {
|
|
178
|
-
await (0, fs_promises.writeFile)(destination, Buffer.from(await this.content.arrayBuffer()));
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
//#endregion
|
|
182
|
-
//#region src/FormRequest.ts
|
|
183
|
-
var FormRequest = class {
|
|
184
|
-
dataset;
|
|
185
|
-
constructor(data) {
|
|
186
|
-
this.initialize(data);
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Initialize the data
|
|
190
|
-
* @param data
|
|
191
|
-
*/
|
|
192
|
-
initialize(data) {
|
|
193
|
-
this.dataset = {
|
|
194
|
-
files: {},
|
|
195
|
-
input: {}
|
|
196
|
-
};
|
|
197
|
-
for (const [rawKey, value] of data.entries()) {
|
|
198
|
-
const key = rawKey.endsWith("[]") ? rawKey.slice(0, -2) : rawKey;
|
|
199
|
-
if (value instanceof UploadedFile || value instanceof File) {
|
|
200
|
-
const uploaded = value instanceof UploadedFile ? value : UploadedFile.createFromBase(value);
|
|
201
|
-
if (this.dataset.files[key]) {
|
|
202
|
-
const existing = this.dataset.files[key];
|
|
203
|
-
if (Array.isArray(existing)) existing.push(uploaded);
|
|
204
|
-
else this.dataset.files[key] = [existing, uploaded];
|
|
205
|
-
} else this.dataset.files[key] = uploaded;
|
|
206
|
-
} else if (this.dataset.input[key]) {
|
|
207
|
-
const existing = this.dataset.input[key];
|
|
208
|
-
if (Array.isArray(existing)) existing.push(value);
|
|
209
|
-
else this.dataset.input[key] = [existing, value];
|
|
210
|
-
} else this.dataset.input[key] = value;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Get all uploaded files
|
|
215
|
-
*/
|
|
216
|
-
files() {
|
|
217
|
-
return this.dataset.files;
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Get all input fields
|
|
221
|
-
*/
|
|
222
|
-
input() {
|
|
223
|
-
return this.dataset.input;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Get combined input and files
|
|
227
|
-
* File entries take precedence if names overlap.
|
|
228
|
-
*/
|
|
229
|
-
all() {
|
|
230
|
-
return Object.assign({}, this.dataset.input, this.dataset.files);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
//#endregion
|
|
234
|
-
//#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorateMetadata.js
|
|
235
|
-
function __decorateMetadata(k, v) {
|
|
236
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
237
|
-
}
|
|
238
|
-
//#endregion
|
|
239
|
-
//#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorate.js
|
|
240
|
-
function __decorate(decorators, target, key, desc) {
|
|
241
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
242
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
243
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
244
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
245
|
-
}
|
|
246
|
-
//#endregion
|
|
247
|
-
//#region src/Middleware.ts
|
|
248
|
-
let Middleware = class Middleware extends _h3ravel_contracts.IMiddleware {
|
|
249
|
-
app;
|
|
250
|
-
constructor(app) {
|
|
251
|
-
super();
|
|
252
|
-
this.app = app;
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
Middleware = __decorate([(0, _h3ravel_foundation.Injectable)(), __decorateMetadata("design:paramtypes", [typeof _h3ravel_contracts.IApplication === "undefined" ? Object : _h3ravel_contracts.IApplication])], Middleware);
|
|
256
|
-
//#endregion
|
|
257
|
-
//#region src/Middleware/FlashDataMiddleware.ts
|
|
258
|
-
var FlashDataMiddleware = class extends Middleware {
|
|
259
|
-
async handle(request, next) {
|
|
260
|
-
const _next = await next(request);
|
|
261
|
-
request.session().ageFlashData();
|
|
262
|
-
return _next;
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
__decorate([
|
|
266
|
-
(0, _h3ravel_foundation.Injectable)(),
|
|
267
|
-
__decorateMetadata("design:type", Function),
|
|
268
|
-
__decorateMetadata("design:paramtypes", [typeof Request === "undefined" ? Object : Request, Function]),
|
|
269
|
-
__decorateMetadata("design:returntype", Promise)
|
|
270
|
-
], FlashDataMiddleware.prototype, "handle", null);
|
|
271
|
-
//#endregion
|
|
272
|
-
//#region src/Middleware/LogRequests.ts
|
|
273
|
-
var LogRequests = class extends Middleware {
|
|
274
|
-
async handle(request, next) {
|
|
275
|
-
const _next = await next(request);
|
|
276
|
-
const code = Number(_h3ravel_support_facades.Response.getStatusCode());
|
|
277
|
-
const method = request.method().toLowerCase();
|
|
278
|
-
let color = "bgRed";
|
|
279
|
-
if (code < 200) color = "bgWhite";
|
|
280
|
-
else if (code >= 200 && code <= 300) color = "bgBlue";
|
|
281
|
-
else if (code >= 300 && code <= 400) color = "bgYellow";
|
|
282
|
-
let mColor = "bgYellow";
|
|
283
|
-
if (method == "get") mColor = "bgBlue";
|
|
284
|
-
else if (method == "head") mColor = "bgGray";
|
|
285
|
-
else if (method == "delete") mColor = "bgRed";
|
|
286
|
-
_h3ravel_shared.Logger.log([
|
|
287
|
-
[` ${method.toUpperCase()} `, mColor],
|
|
288
|
-
[request.fullUrl(), "white"],
|
|
289
|
-
["→", "blue"],
|
|
290
|
-
[` ${code} `, color]
|
|
291
|
-
], " ");
|
|
292
|
-
return _next;
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
__decorate([
|
|
296
|
-
(0, _h3ravel_foundation.Injectable)(),
|
|
297
|
-
__decorateMetadata("design:type", Function),
|
|
298
|
-
__decorateMetadata("design:paramtypes", [typeof _h3ravel_contracts.IRequest === "undefined" ? Object : _h3ravel_contracts.IRequest, Function]),
|
|
299
|
-
__decorateMetadata("design:returntype", Promise)
|
|
300
|
-
], LogRequests.prototype, "handle", null);
|
|
301
|
-
//#endregion
|
|
302
|
-
//#region src/HttpContext.ts
|
|
303
|
-
/**
|
|
304
|
-
* Represents the HTTP context for a single request lifecycle.
|
|
305
|
-
* Encapsulates the application instance, request, and response objects.
|
|
306
|
-
*/
|
|
307
|
-
var HttpContext = class HttpContext extends _h3ravel_contracts.IHttpContext {
|
|
308
|
-
app;
|
|
309
|
-
request;
|
|
310
|
-
response;
|
|
311
|
-
static contexts = /* @__PURE__ */ new WeakMap();
|
|
312
|
-
event;
|
|
313
|
-
constructor(app, request, response) {
|
|
314
|
-
super();
|
|
315
|
-
this.app = app;
|
|
316
|
-
this.request = request;
|
|
317
|
-
this.response = response;
|
|
318
|
-
this.app.bindMiddleware("LogRequests", LogRequests);
|
|
319
|
-
this.app.bindMiddleware("FlashDataMiddleware", FlashDataMiddleware);
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Factory method to create a new HttpContext instance from a context object.
|
|
323
|
-
* @param ctx - Object containing app, request, and response
|
|
324
|
-
* @returns A new HttpContext instance
|
|
325
|
-
*/
|
|
326
|
-
static init(ctx, event) {
|
|
327
|
-
if (!!event && HttpContext.contexts.has(event)) return HttpContext.contexts.get(event);
|
|
328
|
-
const instance = new HttpContext(ctx.app, ctx.request, ctx.response);
|
|
329
|
-
instance.event = event;
|
|
330
|
-
ctx.request.context = instance;
|
|
331
|
-
ctx.response.context = instance;
|
|
332
|
-
ctx.app.setHttpContext(instance);
|
|
333
|
-
if (event) HttpContext.contexts.set(event, instance);
|
|
334
|
-
return instance;
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Retrieve an existing HttpContext instance for an event, if any.
|
|
338
|
-
*/
|
|
339
|
-
static get(event) {
|
|
340
|
-
return HttpContext.contexts.get(event);
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Delete the cached context for a given event (optional cleanup).
|
|
344
|
-
*/
|
|
345
|
-
static forget(event) {
|
|
346
|
-
HttpContext.contexts.delete(event);
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
//#endregion
|
|
350
|
-
//#region src/Utilities/HeaderBag.ts
|
|
351
|
-
/**
|
|
352
|
-
* HeaderBag — A container for HTTP headers
|
|
353
|
-
* for H3ravel App.
|
|
354
|
-
*/
|
|
355
|
-
var HeaderBag = class HeaderBag extends _h3ravel_contracts.IHeaderBag {
|
|
356
|
-
static UPPER = "_ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
357
|
-
static LOWER = "-abcdefghijklmnopqrstuvwxyz";
|
|
358
|
-
headers = {};
|
|
359
|
-
headerNames = {};
|
|
360
|
-
cacheControl = {};
|
|
361
|
-
constructor(headers = {}) {
|
|
362
|
-
super();
|
|
363
|
-
for (const [key, values] of Object.entries(headers)) {
|
|
364
|
-
this.set(key, values);
|
|
365
|
-
if (key.startsWith("HTTP_")) this.set(key.slice(5), values);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Returns all headers as string (for debugging / toString)
|
|
370
|
-
*
|
|
371
|
-
* @returns
|
|
372
|
-
*/
|
|
373
|
-
toString() {
|
|
374
|
-
const headers = this.all();
|
|
375
|
-
if (!Object.keys(headers).length) return "";
|
|
376
|
-
const sortedKeys = Object.keys(headers).sort();
|
|
377
|
-
const max = Math.max(...sortedKeys.map((k) => k.length)) + 1;
|
|
378
|
-
let content = "";
|
|
379
|
-
for (const name of sortedKeys) {
|
|
380
|
-
const values = headers[name] ?? [];
|
|
381
|
-
const displayName = name.split("-").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("-");
|
|
382
|
-
for (const value of values) content += `${displayName + ":"}`.padEnd(max + 1, " ") + `${value ?? ""}\r\n`;
|
|
383
|
-
}
|
|
384
|
-
return content;
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Returns all headers or specific header list
|
|
388
|
-
*
|
|
389
|
-
* @param key
|
|
390
|
-
* @returns
|
|
391
|
-
*/
|
|
392
|
-
all(key) {
|
|
393
|
-
if (key !== void 0) return this.headers[this.normalizeKey(key)] ?? [];
|
|
394
|
-
return this.headers;
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Returns header keys
|
|
398
|
-
*
|
|
399
|
-
* @returns
|
|
400
|
-
*/
|
|
401
|
-
keys() {
|
|
402
|
-
return Object.keys(this.headers);
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Replace all headers with new set
|
|
406
|
-
*
|
|
407
|
-
* @param headers
|
|
408
|
-
*/
|
|
409
|
-
replace(headers = {}) {
|
|
410
|
-
this.headers = {};
|
|
411
|
-
this.add(headers);
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Add multiple headers
|
|
415
|
-
*
|
|
416
|
-
* @param headers
|
|
417
|
-
*/
|
|
418
|
-
add(headers) {
|
|
419
|
-
for (const [key, values] of Object.entries(headers)) this.set(key, values);
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Returns first header value by name or default
|
|
423
|
-
*
|
|
424
|
-
* @param key
|
|
425
|
-
* @param defaultValue
|
|
426
|
-
* @returns
|
|
427
|
-
*/
|
|
428
|
-
get(key, defaultValue = null) {
|
|
429
|
-
const headers = this.all(key) || this.all("http-" + key);
|
|
430
|
-
if (!headers.length) return defaultValue;
|
|
431
|
-
return headers[0];
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Sets a header by name.
|
|
435
|
-
*
|
|
436
|
-
* @param replace Whether to replace existing values (default true)
|
|
437
|
-
*/
|
|
438
|
-
set(key, values, replace = true) {
|
|
439
|
-
const normalized = this.normalizeKey(key);
|
|
440
|
-
if (Array.isArray(values)) {
|
|
441
|
-
const valList = values.map((v) => v === void 0 ? null : v);
|
|
442
|
-
if (replace || !this.headers[normalized]) this.headers[normalized] = valList;
|
|
443
|
-
else this.headers[normalized].push(...valList);
|
|
444
|
-
} else {
|
|
445
|
-
const val = values === void 0 ? null : values;
|
|
446
|
-
if (replace || !this.headers[normalized]) this.headers[normalized] = [val];
|
|
447
|
-
else this.headers[normalized].push(val);
|
|
448
|
-
}
|
|
449
|
-
if (normalized === "cache-control") this.cacheControl = this.parseCacheControl((this.headers[normalized] ?? []).join(", "));
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Returns true if header exists
|
|
453
|
-
*
|
|
454
|
-
* @param key
|
|
455
|
-
* @returns
|
|
456
|
-
*/
|
|
457
|
-
has(key) {
|
|
458
|
-
return Object.prototype.hasOwnProperty.call(this.headers, this.normalizeKey(key));
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Returns true if header contains value
|
|
462
|
-
*
|
|
463
|
-
* @param key
|
|
464
|
-
* @param value
|
|
465
|
-
* @returns
|
|
466
|
-
*/
|
|
467
|
-
contains(key, value) {
|
|
468
|
-
return this.all(key).includes(value);
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Removes a header
|
|
472
|
-
*
|
|
473
|
-
* @param key
|
|
474
|
-
*/
|
|
475
|
-
remove(key) {
|
|
476
|
-
const normalized = this.normalizeKey(key);
|
|
477
|
-
delete this.headers[normalized];
|
|
478
|
-
if (normalized === "cache-control") this.cacheControl = {};
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Returns parsed date from header
|
|
482
|
-
*
|
|
483
|
-
* @param key
|
|
484
|
-
* @param defaultValue
|
|
485
|
-
* @returns
|
|
486
|
-
*/
|
|
487
|
-
getDate(key, defaultValue = null) {
|
|
488
|
-
const value = this.get(key);
|
|
489
|
-
if (!value) return defaultValue ? _h3ravel_support.DateTime.parse(defaultValue) : void 0;
|
|
490
|
-
const parsed = _h3ravel_support.DateTime.parse(value);
|
|
491
|
-
if (isNaN(parsed.unix())) throw new _h3ravel_support.RuntimeException(`The "${key}" HTTP header is not parseable (${value}).`);
|
|
492
|
-
return parsed;
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Adds a Cache-Control directive
|
|
496
|
-
*
|
|
497
|
-
* @param key
|
|
498
|
-
* @param value
|
|
499
|
-
*/
|
|
500
|
-
addCacheControlDirective(key, value = true) {
|
|
501
|
-
this.cacheControl[key] = value;
|
|
502
|
-
this.set("Cache-Control", this.getCacheControlHeader());
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Returns true if Cache-Control directive is defined
|
|
506
|
-
*
|
|
507
|
-
* @param key
|
|
508
|
-
* @returns
|
|
509
|
-
*/
|
|
510
|
-
hasCacheControlDirective(key) {
|
|
511
|
-
return Object.prototype.hasOwnProperty.call(this.cacheControl, key);
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Returns a Cache-Control directive value by name
|
|
515
|
-
*
|
|
516
|
-
* @param key
|
|
517
|
-
* @returns
|
|
518
|
-
*/
|
|
519
|
-
getCacheControlDirective(key) {
|
|
520
|
-
return this.cacheControl[key] ?? null;
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Removes a Cache-Control directive
|
|
524
|
-
*
|
|
525
|
-
* @param key
|
|
526
|
-
* @returns
|
|
527
|
-
*/
|
|
528
|
-
removeCacheControlDirective(key) {
|
|
529
|
-
delete this.cacheControl[key];
|
|
530
|
-
this.set("Cache-Control", this.getCacheControlHeader());
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Number of headers
|
|
534
|
-
*
|
|
535
|
-
* @param key
|
|
536
|
-
* @returns
|
|
537
|
-
*/
|
|
538
|
-
count() {
|
|
539
|
-
return Object.keys(this.headers).length;
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Normalize header name to lowercase with hyphens
|
|
543
|
-
*
|
|
544
|
-
* @param key
|
|
545
|
-
* @returns
|
|
546
|
-
*/
|
|
547
|
-
normalizeKey(key) {
|
|
548
|
-
return key.replace(/[A-Z_]/g, (ch) => {
|
|
549
|
-
const idx = HeaderBag.UPPER.indexOf(ch);
|
|
550
|
-
return idx === -1 ? ch : HeaderBag.LOWER[idx];
|
|
551
|
-
}).toLowerCase();
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Generates Cache-Control header string
|
|
555
|
-
*
|
|
556
|
-
* @param header
|
|
557
|
-
* @returns
|
|
558
|
-
*/
|
|
559
|
-
getCacheControlHeader() {
|
|
560
|
-
return Object.entries(this.cacheControl).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => v === true ? k : v === false ? "" : `${k}=${v}`).filter(Boolean).join(", ");
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* Parses Cache-Control header
|
|
564
|
-
*
|
|
565
|
-
* @param header
|
|
566
|
-
* @returns
|
|
567
|
-
*/
|
|
568
|
-
parseCacheControl(header) {
|
|
569
|
-
const directives = {};
|
|
570
|
-
const parts = header.split(",").map((p) => p.trim()).filter(Boolean);
|
|
571
|
-
for (const part of parts) {
|
|
572
|
-
const [key, val] = part.split("=", 2);
|
|
573
|
-
directives[key.trim()] = val !== void 0 ? val.trim() : true;
|
|
574
|
-
}
|
|
575
|
-
return directives;
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Iterator support
|
|
579
|
-
* @returns
|
|
580
|
-
*/
|
|
581
|
-
[Symbol.iterator]() {
|
|
582
|
-
return Object.entries(this.headers)[Symbol.iterator]();
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
//#endregion
|
|
586
|
-
//#region src/Utilities/HeaderUtility.ts
|
|
587
|
-
/**
|
|
588
|
-
* HTTP header utility functions .
|
|
589
|
-
*/
|
|
590
|
-
var HeaderUtility = class HeaderUtility {
|
|
591
|
-
static DISPOSITION_ATTACHMENT = "attachment";
|
|
592
|
-
static DISPOSITION_INLINE = "inline";
|
|
593
|
-
constructor() {}
|
|
594
|
-
/**
|
|
595
|
-
* Splits an HTTP header by one or more separators.
|
|
596
|
-
*
|
|
597
|
-
* Example:
|
|
598
|
-
* HeaderUtility.split('da, en-gb;q=0.8', ',;')
|
|
599
|
-
* // returns [['da'], ['en-gb', 'q=0.8']]
|
|
600
|
-
*/
|
|
601
|
-
static split(header, separators) {
|
|
602
|
-
if (!separators) throw new Error("At least one separator must be specified.");
|
|
603
|
-
const quotedSeparators = separators.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
604
|
-
const regex = new RegExp(`(?!\\s)(?:(?:"(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$))|[^"${quotedSeparators}"]+)+(?<!\\s)|\\s*(?<separator>[${quotedSeparators}])\\s*`, "g");
|
|
605
|
-
const matches = [];
|
|
606
|
-
let match;
|
|
607
|
-
while ((match = regex.exec(header.trim())) !== null) matches.push(match.groups ? {
|
|
608
|
-
separator: match.groups.separator ?? "",
|
|
609
|
-
0: match[0]
|
|
610
|
-
} : { 0: match[0] });
|
|
611
|
-
return this.groupParts(matches, separators);
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Combines an array of arrays into one associative object.
|
|
615
|
-
* [['foo', 'abc'], ['bar']] => { foo: 'abc', bar: true }
|
|
616
|
-
*/
|
|
617
|
-
static combine(parts) {
|
|
618
|
-
const assoc = {};
|
|
619
|
-
for (const part of parts) {
|
|
620
|
-
const name = String(part[0]).toLowerCase();
|
|
621
|
-
assoc[name] = part[1] ?? true;
|
|
622
|
-
}
|
|
623
|
-
return assoc;
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Joins an associative object into a string for use in an HTTP header.
|
|
627
|
-
* { foo: 'abc', bar: true, baz: 'a b c' } => 'foo=abc, bar, baz="a b c"'
|
|
628
|
-
*/
|
|
629
|
-
static toString(assoc, separator) {
|
|
630
|
-
return Object.entries(assoc).map(([name, value]) => {
|
|
631
|
-
return value === true ? name : `${name}=${HeaderUtility.quote(value)}`;
|
|
632
|
-
}).join(`${separator} `);
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Encodes a string as a quoted string, if necessary.
|
|
636
|
-
*/
|
|
637
|
-
static quote(s) {
|
|
638
|
-
if (/^[a-z0-9!#$%&'*.^_`|~-]+$/i.test(s)) return s;
|
|
639
|
-
return `"${s.replace(/(["\\])/g, "\\$1")}"`;
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Decodes a quoted string.
|
|
643
|
-
*/
|
|
644
|
-
static unquote(s) {
|
|
645
|
-
return s.replace(/\\(.)|"/g, "$1");
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Generates an HTTP Content-Disposition field-value.
|
|
649
|
-
*
|
|
650
|
-
* @see RFC 6266
|
|
651
|
-
*/
|
|
652
|
-
static makeDisposition(disposition, filename, filenameFallback = "") {
|
|
653
|
-
if (![HeaderUtility.DISPOSITION_ATTACHMENT, HeaderUtility.DISPOSITION_INLINE].includes(disposition)) throw new Error(`The disposition must be either "${HeaderUtility.DISPOSITION_ATTACHMENT}" or "${HeaderUtility.DISPOSITION_INLINE}".`);
|
|
654
|
-
if (filenameFallback === "") filenameFallback = filename;
|
|
655
|
-
if (!/^[\x20-\x7e]*$/.test(filenameFallback)) throw new _h3ravel_support.InvalidArgumentException("The filename fallback must only contain ASCII characters.");
|
|
656
|
-
if (filenameFallback.includes("%")) throw new _h3ravel_support.InvalidArgumentException("The filename fallback cannot contain the \"%\" character.");
|
|
657
|
-
if ([filename, filenameFallback].some((f) => f.includes("/") || f.includes("\\"))) throw new _h3ravel_support.InvalidArgumentException("The filename and the fallback cannot contain the \"/\" and \"\\\" characters.");
|
|
658
|
-
const params = { filename: filenameFallback };
|
|
659
|
-
if (filename !== filenameFallback) params["filename*"] = `utf-8''${encodeURIComponent(filename)}`;
|
|
660
|
-
return `${disposition}; ${HeaderUtility.toString(params, ";")}`;
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Like parse_str(), but preserves dots in variable names.
|
|
664
|
-
*/
|
|
665
|
-
static parseQuery(query, ignoreBrackets = false, separator = "&") {
|
|
666
|
-
const q = {};
|
|
667
|
-
const pairs = query.split(separator);
|
|
668
|
-
for (let v of pairs) {
|
|
669
|
-
const nullPos = v.indexOf("\0");
|
|
670
|
-
if (nullPos !== -1) v = v.slice(0, nullPos);
|
|
671
|
-
const eqPos = v.indexOf("=");
|
|
672
|
-
let k;
|
|
673
|
-
let val;
|
|
674
|
-
if (eqPos === -1) {
|
|
675
|
-
k = decodeURIComponent(v);
|
|
676
|
-
val = "";
|
|
677
|
-
} else {
|
|
678
|
-
k = decodeURIComponent(v.slice(0, eqPos));
|
|
679
|
-
val = v.slice(eqPos);
|
|
680
|
-
}
|
|
681
|
-
const nullKeyPos = k.indexOf("\0");
|
|
682
|
-
if (nullKeyPos !== -1) k = k.slice(0, nullKeyPos);
|
|
683
|
-
k = k.trimStart();
|
|
684
|
-
if (ignoreBrackets) {
|
|
685
|
-
q[k] = q[k] || [];
|
|
686
|
-
q[k].push(decodeURIComponent(val.slice(1)));
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
|
-
const bracketPos = k.indexOf("[");
|
|
690
|
-
if (bracketPos === -1) q[Buffer.from(k).toString("hex") + val] = val;
|
|
691
|
-
else {
|
|
692
|
-
const prefix = Buffer.from(k.slice(0, bracketPos)).toString("hex");
|
|
693
|
-
q[prefix + encodeURIComponent(k.slice(bracketPos)) + val] = val;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
if (ignoreBrackets) return q;
|
|
697
|
-
const parsed = new URLSearchParams(Object.keys(q).join("&"));
|
|
698
|
-
const result = {};
|
|
699
|
-
for (const [key, value] of parsed.entries()) {
|
|
700
|
-
const underscorePos = key.indexOf("_");
|
|
701
|
-
if (underscorePos !== -1) {
|
|
702
|
-
const newKey = key.slice(0, underscorePos) + Buffer.from(key.slice(0, underscorePos), "hex").toString("utf8") + "[" + key.slice(underscorePos + 1) + "]";
|
|
703
|
-
result[newKey] = value;
|
|
704
|
-
} else result[Buffer.from(key, "hex").toString("utf8")] = value;
|
|
705
|
-
}
|
|
706
|
-
return result;
|
|
707
|
-
}
|
|
708
|
-
static groupParts(matches, separators, first = true) {
|
|
709
|
-
const separator = separators[0];
|
|
710
|
-
const rest = separators.slice(1);
|
|
711
|
-
let i = 0;
|
|
712
|
-
if (!rest && !first) {
|
|
713
|
-
const parts = [""];
|
|
714
|
-
for (const match of matches) if (!i && match.separator) {
|
|
715
|
-
i = 1;
|
|
716
|
-
parts[1] = "";
|
|
717
|
-
} else parts[i] += this.unquote(match[0]);
|
|
718
|
-
return parts;
|
|
719
|
-
}
|
|
720
|
-
const parts = [];
|
|
721
|
-
const grouped = {};
|
|
722
|
-
for (const match of matches) if (match.separator === separator) ++i;
|
|
723
|
-
else (grouped[i] ||= []).push(match);
|
|
724
|
-
for (const group of Object.values(grouped)) if (!rest && this.unquote(group[0][0]) !== "") parts.push(this.unquote(group[0][0]));
|
|
725
|
-
else {
|
|
726
|
-
const sub = this.groupParts(group, rest, false);
|
|
727
|
-
if (sub) parts.push(sub);
|
|
728
|
-
}
|
|
729
|
-
return parts;
|
|
730
|
-
}
|
|
731
|
-
};
|
|
732
|
-
//#endregion
|
|
733
|
-
//#region src/Utilities/Cookie.ts
|
|
734
|
-
/**
|
|
735
|
-
* Represents a Cookie
|
|
736
|
-
*/
|
|
737
|
-
var Cookie = class Cookie {
|
|
738
|
-
name;
|
|
739
|
-
value;
|
|
740
|
-
domain;
|
|
741
|
-
secure;
|
|
742
|
-
httpOnly;
|
|
743
|
-
static SAMESITE_NONE = "none";
|
|
744
|
-
static SAMESITE_LAX = "lax";
|
|
745
|
-
static SAMESITE_STRICT = "strict";
|
|
746
|
-
expire;
|
|
747
|
-
path;
|
|
748
|
-
sameSite;
|
|
749
|
-
raw;
|
|
750
|
-
partitioned;
|
|
751
|
-
secureDefault = false;
|
|
752
|
-
static RESERVED_CHARS_LIST = "=,; \r\n\v\f";
|
|
753
|
-
static RESERVED_CHARS_FROM = [
|
|
754
|
-
"=",
|
|
755
|
-
",",
|
|
756
|
-
";",
|
|
757
|
-
" ",
|
|
758
|
-
" ",
|
|
759
|
-
"\r",
|
|
760
|
-
"\n",
|
|
761
|
-
"\v",
|
|
762
|
-
"\f"
|
|
763
|
-
];
|
|
764
|
-
static RESERVED_CHARS_TO = [
|
|
765
|
-
"%3D",
|
|
766
|
-
"%2C",
|
|
767
|
-
"%3B",
|
|
768
|
-
"%20",
|
|
769
|
-
"%09",
|
|
770
|
-
"%0D",
|
|
771
|
-
"%0A",
|
|
772
|
-
"%0B",
|
|
773
|
-
"%0C"
|
|
774
|
-
];
|
|
775
|
-
constructor(name, value, expire = 0, path = "/", domain, secure, httpOnly = true, raw = false, sameSite = Cookie.SAMESITE_LAX, partitioned = false) {
|
|
776
|
-
this.name = name;
|
|
777
|
-
this.value = value;
|
|
778
|
-
this.domain = domain;
|
|
779
|
-
this.secure = secure;
|
|
780
|
-
this.httpOnly = httpOnly;
|
|
781
|
-
if (raw && [...Cookie.RESERVED_CHARS_LIST].some((c) => name.includes(c))) throw new Error(`The cookie name "${name}" contains invalid characters.`);
|
|
782
|
-
if (!name) throw new Error("The cookie name cannot be empty.");
|
|
783
|
-
this.expire = Cookie.expiresTimestamp(expire);
|
|
784
|
-
this.path = path || "/";
|
|
785
|
-
this.sameSite = this.withSameSite(sameSite).sameSite;
|
|
786
|
-
this.raw = raw;
|
|
787
|
-
this.partitioned = partitioned;
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Create a Cookie instance from a Set-Cookie header string.
|
|
791
|
-
*/
|
|
792
|
-
static fromString(cookie, decode = false) {
|
|
793
|
-
const data = {
|
|
794
|
-
expires: 0,
|
|
795
|
-
path: "/",
|
|
796
|
-
domain: null,
|
|
797
|
-
secure: false,
|
|
798
|
-
httponly: false,
|
|
799
|
-
raw: !decode,
|
|
800
|
-
samesite: null,
|
|
801
|
-
partitioned: false
|
|
802
|
-
};
|
|
803
|
-
const parts = HeaderUtility.split(cookie, ";=");
|
|
804
|
-
const part = parts.shift();
|
|
805
|
-
const name = decode ? decodeURIComponent(part[0]) : part[0];
|
|
806
|
-
const value = part[1] ? decode ? decodeURIComponent(part[1]) : part[1] : null;
|
|
807
|
-
Object.assign(data, HeaderUtility.combine(parts));
|
|
808
|
-
data.expires = Cookie.expiresTimestamp(data.expires);
|
|
809
|
-
if (data["max-age"] && (data["max-age"] > 0 || data.expires > Date.now() / 1e3)) data.expires = Math.floor(Date.now() / 1e3) + Number(data["max-age"]);
|
|
810
|
-
return new Cookie(name, value, data.expires, data.path, data.domain, data.secure, data.httponly, data.raw, data.samesite, data.partitioned);
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* Convert various expiration formats into a timestamp (seconds)
|
|
814
|
-
*/
|
|
815
|
-
static expiresTimestamp(expire = 0) {
|
|
816
|
-
if (expire instanceof Date) return Math.floor(expire.getTime() / 1e3);
|
|
817
|
-
if (typeof expire === "string") {
|
|
818
|
-
const parsed = Date.parse(expire);
|
|
819
|
-
if (isNaN(parsed)) throw new Error("The cookie expiration time is not valid.");
|
|
820
|
-
return Math.floor(parsed / 1e3);
|
|
821
|
-
}
|
|
822
|
-
return expire > 0 ? expire : 0;
|
|
823
|
-
}
|
|
824
|
-
clone() {
|
|
825
|
-
return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
|
|
826
|
-
}
|
|
827
|
-
withValue(value) {
|
|
828
|
-
const c = this.clone();
|
|
829
|
-
c.value = value;
|
|
830
|
-
return c;
|
|
831
|
-
}
|
|
832
|
-
withDomain(domain) {
|
|
833
|
-
const c = this.clone();
|
|
834
|
-
c.domain = domain;
|
|
835
|
-
return c;
|
|
836
|
-
}
|
|
837
|
-
withPath(path) {
|
|
838
|
-
const c = this.clone();
|
|
839
|
-
c.path = path || "/";
|
|
840
|
-
return c;
|
|
841
|
-
}
|
|
842
|
-
withSecure(secure = true) {
|
|
843
|
-
const c = this.clone();
|
|
844
|
-
c.secure = secure;
|
|
845
|
-
return c;
|
|
846
|
-
}
|
|
847
|
-
withHttpOnly(httpOnly = true) {
|
|
848
|
-
const c = this.clone();
|
|
849
|
-
c.httpOnly = httpOnly;
|
|
850
|
-
return c;
|
|
851
|
-
}
|
|
852
|
-
withRaw(raw = true) {
|
|
853
|
-
const c = this.clone();
|
|
854
|
-
c.raw = raw;
|
|
855
|
-
return c;
|
|
856
|
-
}
|
|
857
|
-
withSameSite(sameSite) {
|
|
858
|
-
if (sameSite && ![
|
|
859
|
-
Cookie.SAMESITE_LAX,
|
|
860
|
-
Cookie.SAMESITE_STRICT,
|
|
861
|
-
Cookie.SAMESITE_NONE
|
|
862
|
-
].includes(sameSite.toLowerCase())) throw new Error("The \"sameSite\" value must be \"lax\", \"strict\", \"none\" or null.");
|
|
863
|
-
const c = this.clone();
|
|
864
|
-
c.sameSite = sameSite ? sameSite.toLowerCase() : null;
|
|
865
|
-
return c;
|
|
866
|
-
}
|
|
867
|
-
withPartitioned(partitioned = true) {
|
|
868
|
-
const c = this.clone();
|
|
869
|
-
c.partitioned = partitioned;
|
|
870
|
-
return c;
|
|
871
|
-
}
|
|
872
|
-
withExpires(expire) {
|
|
873
|
-
const c = this.clone();
|
|
874
|
-
c.expire = Cookie.expiresTimestamp(expire);
|
|
875
|
-
return c;
|
|
876
|
-
}
|
|
877
|
-
getName() {
|
|
878
|
-
return this.name;
|
|
879
|
-
}
|
|
880
|
-
getValue() {
|
|
881
|
-
return this.value;
|
|
882
|
-
}
|
|
883
|
-
getDomain() {
|
|
884
|
-
return this.domain;
|
|
885
|
-
}
|
|
886
|
-
getPath() {
|
|
887
|
-
return this.path;
|
|
888
|
-
}
|
|
889
|
-
getExpiresTime() {
|
|
890
|
-
return this.expire;
|
|
891
|
-
}
|
|
892
|
-
getMaxAge() {
|
|
893
|
-
if (this.expire === 0) return 0;
|
|
894
|
-
return this.expire - Math.floor(Date.now() / 1e3);
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client.
|
|
898
|
-
*/
|
|
899
|
-
isSecure() {
|
|
900
|
-
return this.secure ?? this.secureDefault;
|
|
901
|
-
}
|
|
902
|
-
isHttpOnly() {
|
|
903
|
-
return this.httpOnly;
|
|
904
|
-
}
|
|
905
|
-
isRaw() {
|
|
906
|
-
return this.raw;
|
|
907
|
-
}
|
|
908
|
-
getSameSite() {
|
|
909
|
-
return this.sameSite;
|
|
910
|
-
}
|
|
911
|
-
isPartitioned() {
|
|
912
|
-
return this.partitioned;
|
|
913
|
-
}
|
|
914
|
-
/**
|
|
915
|
-
* Whether this cookie is about to be cleared.
|
|
916
|
-
*/
|
|
917
|
-
isCleared() {
|
|
918
|
-
return 0 !== this.expire && this.expire < (/* @__PURE__ */ new Date()).getTime();
|
|
919
|
-
}
|
|
920
|
-
/**
|
|
921
|
-
* Convert the cookie to a Set-Cookie header string.
|
|
922
|
-
*/
|
|
923
|
-
toString() {
|
|
924
|
-
const from = Cookie.RESERVED_CHARS_FROM;
|
|
925
|
-
const to = Cookie.RESERVED_CHARS_TO;
|
|
926
|
-
const encodeName = (name) => this.isRaw() ? name : name.replaceAll(new RegExp(from.map((x) => `\\${x}`).join("|"), "g"), (m) => to[from.indexOf(m)]);
|
|
927
|
-
const encodeValue = (val) => this.isRaw() ? val : encodeURIComponent(val);
|
|
928
|
-
let str = `${encodeName(this.name)}=`;
|
|
929
|
-
if (!this.value) str += `deleted; expires=${(/* @__PURE__ */ new Date(Date.now() - 31536001e3)).toUTCString()}; Max-Age=0`;
|
|
930
|
-
else {
|
|
931
|
-
str += encodeValue(this.value);
|
|
932
|
-
if (this.expire !== 0) {
|
|
933
|
-
const expiresAt = (/* @__PURE__ */ new Date(this.expire * 1e3)).toUTCString();
|
|
934
|
-
str += `; expires=${expiresAt}; Max-Age=${this.getMaxAge()}`;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
if (this.path) str += `; path=${this.path}`;
|
|
938
|
-
if (this.domain) str += `; domain=${this.domain}`;
|
|
939
|
-
if (this.isSecure()) str += "; secure";
|
|
940
|
-
if (this.isHttpOnly()) str += "; httponly";
|
|
941
|
-
if (this.sameSite) str += `; samesite=${this.sameSite}`;
|
|
942
|
-
if (this.partitioned) str += "; partitioned";
|
|
943
|
-
return str;
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* @param bool $default The default value of the "secure" flag when it is set to null
|
|
947
|
-
*/
|
|
948
|
-
setSecureDefault(defaultValue) {
|
|
949
|
-
this.secureDefault = defaultValue;
|
|
950
|
-
}
|
|
951
|
-
};
|
|
952
|
-
//#endregion
|
|
953
|
-
//#region src/Utilities/ResponseHeaderBag.ts
|
|
954
|
-
/**
|
|
955
|
-
* ResponseHeaderBag is a container for Response HTTP headers.
|
|
956
|
-
* for Node/H3 environments.
|
|
957
|
-
*/
|
|
958
|
-
var ResponseHeaderBag = class ResponseHeaderBag extends HeaderBag {
|
|
959
|
-
static COOKIES_FLAT = "flat";
|
|
960
|
-
static COOKIES_ARRAY = "array";
|
|
961
|
-
static DISPOSITION_ATTACHMENT = "attachment";
|
|
962
|
-
static DISPOSITION_INLINE = "inline";
|
|
963
|
-
computedCacheControl = {};
|
|
964
|
-
cookies = {};
|
|
965
|
-
headerNames = {};
|
|
966
|
-
constructor(event) {
|
|
967
|
-
super(Object.fromEntries(event.res.headers.entries()));
|
|
968
|
-
if (!this.headers["cache-control"]) this.set("Cache-Control", "");
|
|
969
|
-
if (!this.headers["date"]) this.initDate();
|
|
970
|
-
}
|
|
971
|
-
/**
|
|
972
|
-
* Returns the headers with original capitalizations.
|
|
973
|
-
*/
|
|
974
|
-
allPreserveCase() {
|
|
975
|
-
const headers = {};
|
|
976
|
-
for (const [name, value] of Object.entries(this.all())) {
|
|
977
|
-
const originalName = this.headerNames[name] ?? name;
|
|
978
|
-
headers[originalName] = value;
|
|
979
|
-
}
|
|
980
|
-
return headers;
|
|
981
|
-
}
|
|
982
|
-
allPreserveCaseWithoutCookies() {
|
|
983
|
-
const headers = this.allPreserveCase();
|
|
984
|
-
if (this.headerNames["set-cookie"]) delete headers[this.headerNames["set-cookie"]];
|
|
985
|
-
return headers;
|
|
986
|
-
}
|
|
987
|
-
replace(headers = {}) {
|
|
988
|
-
this.headerNames = {};
|
|
989
|
-
super.replace(headers);
|
|
990
|
-
if (!this.headers["cache-control"]) this.set("Cache-Control", "");
|
|
991
|
-
if (!this.headers["date"]) this.initDate();
|
|
992
|
-
}
|
|
993
|
-
all(key) {
|
|
994
|
-
const headers = super.all();
|
|
995
|
-
if (key) {
|
|
996
|
-
const normalized = key.toLowerCase();
|
|
997
|
-
if (normalized === "set-cookie") return this.getCookies().map(String);
|
|
998
|
-
return headers[normalized] ?? [];
|
|
999
|
-
}
|
|
1000
|
-
const cookies = this.getCookies().map(String);
|
|
1001
|
-
if (cookies.length > 0) headers["set-cookie"] = cookies;
|
|
1002
|
-
return headers;
|
|
1003
|
-
}
|
|
1004
|
-
set(key, values, replace = true) {
|
|
1005
|
-
const uniqueKey = key.toLowerCase();
|
|
1006
|
-
if (uniqueKey === "set-cookie") {
|
|
1007
|
-
if (replace) this.cookies = {};
|
|
1008
|
-
for (const cookie of Array.isArray(values) ? values : [values]) if (cookie) this.setCookie(Cookie.fromString(cookie));
|
|
1009
|
-
this.headerNames[uniqueKey] = key;
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
this.headerNames[uniqueKey] = key;
|
|
1013
|
-
super.set(key, values, replace);
|
|
1014
|
-
if ([
|
|
1015
|
-
"cache-control",
|
|
1016
|
-
"etag",
|
|
1017
|
-
"last-modified",
|
|
1018
|
-
"expires"
|
|
1019
|
-
].includes(uniqueKey) && this.computeCacheControlValue() !== "") {
|
|
1020
|
-
const computed = this.computeCacheControlValue();
|
|
1021
|
-
this.headers["cache-control"] = [computed];
|
|
1022
|
-
this.headerNames["cache-control"] = "Cache-Control";
|
|
1023
|
-
this.computedCacheControl = this.parseCacheControl(computed);
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
remove(key) {
|
|
1027
|
-
const uniqueKey = key.toLowerCase();
|
|
1028
|
-
delete this.headerNames[uniqueKey];
|
|
1029
|
-
if (uniqueKey === "set-cookie") {
|
|
1030
|
-
this.cookies = {};
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
super.remove(key);
|
|
1034
|
-
if (uniqueKey === "cache-control") this.computedCacheControl = {};
|
|
1035
|
-
if (uniqueKey === "date") this.initDate();
|
|
1036
|
-
}
|
|
1037
|
-
hasCacheControlDirective(key) {
|
|
1038
|
-
return key in this.computedCacheControl;
|
|
1039
|
-
}
|
|
1040
|
-
getCacheControlDirective(key) {
|
|
1041
|
-
return this.computedCacheControl[key] ?? null;
|
|
1042
|
-
}
|
|
1043
|
-
setCookie(cookie) {
|
|
1044
|
-
const domain = cookie.getDomain() ?? "";
|
|
1045
|
-
const path = cookie.getPath() ?? "/";
|
|
1046
|
-
this.cookies[domain] ??= {};
|
|
1047
|
-
this.cookies[domain][path] ??= {};
|
|
1048
|
-
this.cookies[domain][path][cookie.getName()] = cookie;
|
|
1049
|
-
this.headerNames["set-cookie"] = "Set-Cookie";
|
|
1050
|
-
}
|
|
1051
|
-
removeCookie(name, path = "/", domain = null) {
|
|
1052
|
-
const d = domain ?? "";
|
|
1053
|
-
delete this.cookies[d]?.[path]?.[name];
|
|
1054
|
-
if (this.cookies[d] && Object.keys(this.cookies[d][path] ?? {}).length === 0) {
|
|
1055
|
-
delete this.cookies[d][path];
|
|
1056
|
-
if (Object.keys(this.cookies[d]).length === 0) delete this.cookies[d];
|
|
1057
|
-
}
|
|
1058
|
-
if (Object.keys(this.cookies).length === 0) delete this.headerNames["set-cookie"];
|
|
1059
|
-
}
|
|
1060
|
-
/**
|
|
1061
|
-
* @throws {Error} if format is invalid
|
|
1062
|
-
*/
|
|
1063
|
-
getCookies(format = ResponseHeaderBag.COOKIES_FLAT) {
|
|
1064
|
-
if (![ResponseHeaderBag.COOKIES_FLAT, ResponseHeaderBag.COOKIES_ARRAY].includes(format)) throw new Error(`Format "${format}" invalid (${ResponseHeaderBag.COOKIES_FLAT}, ${ResponseHeaderBag.COOKIES_ARRAY}).`);
|
|
1065
|
-
if (format === ResponseHeaderBag.COOKIES_ARRAY) return this.cookies;
|
|
1066
|
-
const flattened = [];
|
|
1067
|
-
for (const domain of Object.values(this.cookies)) for (const path of Object.values(domain)) for (const cookie of Object.values(path)) flattened.push(cookie);
|
|
1068
|
-
return flattened;
|
|
1069
|
-
}
|
|
1070
|
-
clearCookie(name, path = "/", domain = null, secure = false, httpOnly = true, sameSite, partitioned = false) {
|
|
1071
|
-
this.setCookie(new Cookie(name, null, 1, path, domain, secure, httpOnly, false, sameSite, partitioned));
|
|
1072
|
-
}
|
|
1073
|
-
makeDisposition(disposition, filename, fallback = "") {
|
|
1074
|
-
return HeaderUtility.makeDisposition(disposition, filename, fallback);
|
|
1075
|
-
}
|
|
1076
|
-
computeCacheControlValue() {
|
|
1077
|
-
if (Object.keys(this.cacheControl).length === 0) {
|
|
1078
|
-
if (this.has("Last-Modified") || this.has("Expires")) return "private, must-revalidate";
|
|
1079
|
-
return "no-cache, private";
|
|
1080
|
-
}
|
|
1081
|
-
const header = this.getCacheControlHeader();
|
|
1082
|
-
if (this.cacheControl["public"] || this.cacheControl["private"]) return header;
|
|
1083
|
-
if (!this.cacheControl["s-maxage"]) return `${header}, private`;
|
|
1084
|
-
return header;
|
|
1085
|
-
}
|
|
1086
|
-
initDate() {
|
|
1087
|
-
const now = (/* @__PURE__ */ new Date()).toUTCString();
|
|
1088
|
-
this.set("Date", now);
|
|
1089
|
-
}
|
|
1090
|
-
};
|
|
1091
|
-
//#endregion
|
|
1092
|
-
//#region src/Utilities/HttpResponse.ts
|
|
1093
|
-
var HttpResponse = class HttpResponse extends _h3ravel_contracts.IHttpResponse {
|
|
1094
|
-
event;
|
|
1095
|
-
statusCode = 200;
|
|
1096
|
-
headers;
|
|
1097
|
-
content;
|
|
1098
|
-
version;
|
|
1099
|
-
statusText;
|
|
1100
|
-
charset;
|
|
1101
|
-
/**
|
|
1102
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
|
1103
|
-
*/
|
|
1104
|
-
HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES = _h3ravel_foundation.HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES;
|
|
1105
|
-
/**
|
|
1106
|
-
* The exception that triggered the error response (if applicable).
|
|
1107
|
-
*/
|
|
1108
|
-
exception;
|
|
1109
|
-
/**
|
|
1110
|
-
* Tracks headers already sent in informational responses.
|
|
1111
|
-
*/
|
|
1112
|
-
sentHeaders = {};
|
|
1113
|
-
/**
|
|
1114
|
-
* Status codes translation table.
|
|
1115
|
-
*
|
|
1116
|
-
* The list of codes is complete according to the
|
|
1117
|
-
* @link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry
|
|
1118
|
-
* (last updated 2021-10-01).
|
|
1119
|
-
*
|
|
1120
|
-
* Unless otherwise noted, the status code is defined in RFC2616.
|
|
1121
|
-
*/
|
|
1122
|
-
static statusTexts = _h3ravel_foundation.statusTexts;
|
|
1123
|
-
constructor(event) {
|
|
1124
|
-
super();
|
|
1125
|
-
this.event = event;
|
|
1126
|
-
this.headers = new ResponseHeaderBag(this.event);
|
|
1127
|
-
this.setContent();
|
|
1128
|
-
this.setProtocolVersion("1.0");
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* Set HTTP status code.
|
|
1132
|
-
*/
|
|
1133
|
-
setStatusCode(code, text) {
|
|
1134
|
-
this.statusCode = code;
|
|
1135
|
-
this.event.res.status = code;
|
|
1136
|
-
if (this.isInvalid()) throw new _h3ravel_support.InvalidArgumentException(`The HTTP status code "${code}" is not valid.`);
|
|
1137
|
-
if (!text) {
|
|
1138
|
-
this.statusText = HttpResponse.statusTexts[code] ?? "unknown status";
|
|
1139
|
-
this.event.res.statusText = this.statusText;
|
|
1140
|
-
return this;
|
|
1141
|
-
}
|
|
1142
|
-
this.statusText = text;
|
|
1143
|
-
this.event.res.statusText = this.statusText;
|
|
1144
|
-
return this;
|
|
1145
|
-
}
|
|
1146
|
-
/**
|
|
1147
|
-
* Retrieves the status code for the current web response.
|
|
1148
|
-
*/
|
|
1149
|
-
getStatusCode() {
|
|
1150
|
-
return this.statusCode;
|
|
1151
|
-
}
|
|
1152
|
-
/**
|
|
1153
|
-
* Sets the response charset.
|
|
1154
|
-
*/
|
|
1155
|
-
setCharset(charset) {
|
|
1156
|
-
this.charset = charset;
|
|
1157
|
-
return this;
|
|
1158
|
-
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Retrieves the response charset.
|
|
1161
|
-
*/
|
|
1162
|
-
getCharset() {
|
|
1163
|
-
return this.charset;
|
|
1164
|
-
}
|
|
1165
|
-
/**
|
|
1166
|
-
* Returns true if the response may safely be kept in a shared (surrogate) cache.
|
|
1167
|
-
*
|
|
1168
|
-
* Responses marked "private" with an explicit Cache-Control directive are
|
|
1169
|
-
* considered uncacheable.
|
|
1170
|
-
*
|
|
1171
|
-
* Responses with neither a freshness lifetime (Expires, max-age) nor cache
|
|
1172
|
-
* validator (Last-Modified, ETag) are considered uncacheable because there is
|
|
1173
|
-
* no way to tell when or how to remove them from the cache.
|
|
1174
|
-
*
|
|
1175
|
-
* Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation,
|
|
1176
|
-
* for example "status codes that are defined as cacheable by default [...]
|
|
1177
|
-
* can be reused by a cache with heuristic expiration unless otherwise indicated"
|
|
1178
|
-
* (https://tools.ietf.org/html/rfc7231#section-6.1)
|
|
1179
|
-
*
|
|
1180
|
-
* @final
|
|
1181
|
-
*/
|
|
1182
|
-
isCacheable() {
|
|
1183
|
-
if (![
|
|
1184
|
-
200,
|
|
1185
|
-
203,
|
|
1186
|
-
300,
|
|
1187
|
-
301,
|
|
1188
|
-
302,
|
|
1189
|
-
404,
|
|
1190
|
-
410
|
|
1191
|
-
].includes(this.statusCode)) return false;
|
|
1192
|
-
if (this.headers.hasCacheControlDirective("no-store") || this.headers.getCacheControlDirective("private")) return false;
|
|
1193
|
-
return this.isValidateable() || this.isFresh();
|
|
1194
|
-
}
|
|
1195
|
-
/**
|
|
1196
|
-
* Returns true if the response is "fresh".
|
|
1197
|
-
*
|
|
1198
|
-
* Fresh responses may be served from cache without any interaction with the
|
|
1199
|
-
* origin. A response is considered fresh when it includes a Cache-Control/max-age
|
|
1200
|
-
* indicator or Expires header and the calculated age is less than the freshness lifetime.
|
|
1201
|
-
*/
|
|
1202
|
-
isFresh() {
|
|
1203
|
-
return Number(this.getTtl()) > 0;
|
|
1204
|
-
}
|
|
1205
|
-
/**
|
|
1206
|
-
* Returns true if the response includes headers that can be used to validate
|
|
1207
|
-
* the response with the origin server using a conditional GET request.
|
|
1208
|
-
*/
|
|
1209
|
-
isValidateable() {
|
|
1210
|
-
return this.headers.has("Last-Modified") || this.headers.has("ETag");
|
|
1211
|
-
}
|
|
1212
|
-
/**
|
|
1213
|
-
* Sets the response content.
|
|
1214
|
-
*/
|
|
1215
|
-
setContent(content) {
|
|
1216
|
-
this.content = content ?? "";
|
|
1217
|
-
return this;
|
|
1218
|
-
}
|
|
1219
|
-
/**
|
|
1220
|
-
* Gets the current response content.
|
|
1221
|
-
*/
|
|
1222
|
-
getContent() {
|
|
1223
|
-
return this.content;
|
|
1224
|
-
}
|
|
1225
|
-
/**
|
|
1226
|
-
* Set a header.
|
|
1227
|
-
*/
|
|
1228
|
-
setHeader(name, value) {
|
|
1229
|
-
this.headers.set(name, value);
|
|
1230
|
-
return this;
|
|
1231
|
-
}
|
|
1232
|
-
/**
|
|
1233
|
-
* Sets the HTTP protocol version (1.0 or 1.1).
|
|
1234
|
-
*/
|
|
1235
|
-
setProtocolVersion(version) {
|
|
1236
|
-
this.version = version;
|
|
1237
|
-
return this;
|
|
1238
|
-
}
|
|
1239
|
-
/**
|
|
1240
|
-
* Gets the HTTP protocol version.
|
|
1241
|
-
*/
|
|
1242
|
-
getProtocolVersion() {
|
|
1243
|
-
return this.version;
|
|
1244
|
-
}
|
|
1245
|
-
/**
|
|
1246
|
-
* Marks the response as "private".
|
|
1247
|
-
*
|
|
1248
|
-
* It makes the response ineligible for serving other clients.
|
|
1249
|
-
*/
|
|
1250
|
-
setPrivate() {
|
|
1251
|
-
this.headers.removeCacheControlDirective("public");
|
|
1252
|
-
this.headers.addCacheControlDirective("private");
|
|
1253
|
-
return this;
|
|
1254
|
-
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Marks the response as "public".
|
|
1257
|
-
*
|
|
1258
|
-
* It makes the response eligible for serving other clients.
|
|
1259
|
-
*/
|
|
1260
|
-
setPublic() {
|
|
1261
|
-
this.headers.addCacheControlDirective("public");
|
|
1262
|
-
this.headers.removeCacheControlDirective("private");
|
|
1263
|
-
return this;
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Returns the Date header as a DateTime instance.
|
|
1267
|
-
* @throws {RuntimeException} When the header is not parseable
|
|
1268
|
-
*/
|
|
1269
|
-
getDate() {
|
|
1270
|
-
return this.headers.getDate("Date");
|
|
1271
|
-
}
|
|
1272
|
-
/**
|
|
1273
|
-
* Returns the age of the response in seconds.
|
|
1274
|
-
*
|
|
1275
|
-
* @final
|
|
1276
|
-
*/
|
|
1277
|
-
getAge() {
|
|
1278
|
-
const age = this.headers.get("Age");
|
|
1279
|
-
if (age) return Number(age);
|
|
1280
|
-
return Math.max(_h3ravel_support.DateTime.now().unix() - this.getDate().unix(), 0);
|
|
1281
|
-
}
|
|
1282
|
-
/**
|
|
1283
|
-
* Marks the response stale by setting the Age header to be equal to the maximum age of the response.
|
|
1284
|
-
*/
|
|
1285
|
-
expire() {
|
|
1286
|
-
if (this.isFresh()) {
|
|
1287
|
-
this.headers.set("Age", String(this.getMaxAge()));
|
|
1288
|
-
this.headers.remove("Expires");
|
|
1289
|
-
}
|
|
1290
|
-
return this;
|
|
1291
|
-
}
|
|
1292
|
-
/**
|
|
1293
|
-
* Returns the value of the Expires header as a DateTime instance.
|
|
1294
|
-
*
|
|
1295
|
-
* @final
|
|
1296
|
-
*/
|
|
1297
|
-
getExpires() {
|
|
1298
|
-
try {
|
|
1299
|
-
return new _h3ravel_support.DateTime(this.headers.getDate("Expires"));
|
|
1300
|
-
} catch {
|
|
1301
|
-
return new _h3ravel_support.DateTime(_h3ravel_support.DateTime.now().subtract(2, "days"));
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
/**
|
|
1305
|
-
* Returns the number of seconds after the time specified in the response's Date
|
|
1306
|
-
* header when the response should no longer be considered fresh.
|
|
1307
|
-
*
|
|
1308
|
-
* First, it checks for a s-maxage directive, then a max-age directive, and then it falls
|
|
1309
|
-
* back on an expires header. It returns null when no maximum age can be established.
|
|
1310
|
-
*/
|
|
1311
|
-
getMaxAge() {
|
|
1312
|
-
if (this.headers.hasCacheControlDirective("s-maxage")) return Number(this.headers.getCacheControlDirective("s-maxage"));
|
|
1313
|
-
if (this.headers.hasCacheControlDirective("max-age")) return Number(this.headers.getCacheControlDirective("max-age"));
|
|
1314
|
-
const expires = this.getExpires();
|
|
1315
|
-
if (expires) {
|
|
1316
|
-
const maxAge = Number(expires.unix() - this.getDate().unix());
|
|
1317
|
-
return Math.max(maxAge, 0);
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Sets the number of seconds after which the response should no longer be considered fresh.
|
|
1322
|
-
*
|
|
1323
|
-
* This method sets the Cache-Control max-age directive.
|
|
1324
|
-
*/
|
|
1325
|
-
setMaxAge(value) {
|
|
1326
|
-
this.headers.addCacheControlDirective("max-age", String(value));
|
|
1327
|
-
return this;
|
|
1328
|
-
}
|
|
1329
|
-
/**
|
|
1330
|
-
* Sets the number of seconds after which the response should no longer be returned by shared caches when backend is down.
|
|
1331
|
-
*
|
|
1332
|
-
* This method sets the Cache-Control stale-if-error directive.
|
|
1333
|
-
*/
|
|
1334
|
-
setStaleIfError(value) {
|
|
1335
|
-
this.headers.addCacheControlDirective("stale-if-error", String(value));
|
|
1336
|
-
return this;
|
|
1337
|
-
}
|
|
1338
|
-
/**
|
|
1339
|
-
* Sets the number of seconds after which the response should no longer return stale content by shared caches.
|
|
1340
|
-
*
|
|
1341
|
-
* This method sets the Cache-Control stale-while-revalidate directive.
|
|
1342
|
-
*/
|
|
1343
|
-
setStaleWhileRevalidate(value) {
|
|
1344
|
-
this.headers.addCacheControlDirective("stale-while-revalidate", String(value));
|
|
1345
|
-
return this;
|
|
1346
|
-
}
|
|
1347
|
-
/**
|
|
1348
|
-
* Returns the response's time-to-live in seconds.
|
|
1349
|
-
*
|
|
1350
|
-
* It returns null when no freshness information is present in the response.
|
|
1351
|
-
*
|
|
1352
|
-
* When the response's TTL is 0, the response may not be served from cache without first
|
|
1353
|
-
* revalidating with the origin.
|
|
1354
|
-
*
|
|
1355
|
-
* @final
|
|
1356
|
-
*/
|
|
1357
|
-
getTtl() {
|
|
1358
|
-
const maxAge = Number(this.getMaxAge());
|
|
1359
|
-
return null !== maxAge ? Math.max(maxAge - this.getAge(), 0) : void 0;
|
|
1360
|
-
}
|
|
1361
|
-
/**
|
|
1362
|
-
* Sets the response's time-to-live for shared caches in seconds.
|
|
1363
|
-
*
|
|
1364
|
-
* This method adjusts the Cache-Control/s-maxage directive.
|
|
1365
|
-
*/
|
|
1366
|
-
setTtl(seconds) {
|
|
1367
|
-
this.setSharedMaxAge(this.getAge() + seconds);
|
|
1368
|
-
return this;
|
|
1369
|
-
}
|
|
1370
|
-
/**
|
|
1371
|
-
* Sets the response's time-to-live for private/client caches in seconds.
|
|
1372
|
-
*
|
|
1373
|
-
* This method adjusts the Cache-Control/max-age directive.
|
|
1374
|
-
*/
|
|
1375
|
-
setClientTtl(seconds) {
|
|
1376
|
-
this.setMaxAge(this.getAge() + seconds);
|
|
1377
|
-
return this;
|
|
1378
|
-
}
|
|
1379
|
-
/**
|
|
1380
|
-
* Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
|
|
1381
|
-
*
|
|
1382
|
-
* This method sets the Cache-Control s-maxage directive.
|
|
1383
|
-
*/
|
|
1384
|
-
setSharedMaxAge(value) {
|
|
1385
|
-
this.setPublic();
|
|
1386
|
-
this.headers.addCacheControlDirective("s-maxage", String(value));
|
|
1387
|
-
return this;
|
|
1388
|
-
}
|
|
1389
|
-
/**
|
|
1390
|
-
* Returns the Last-Modified HTTP header as a DateTime instance.
|
|
1391
|
-
*
|
|
1392
|
-
* @throws \RuntimeException When the HTTP header is not parseable
|
|
1393
|
-
*
|
|
1394
|
-
* @final
|
|
1395
|
-
*/
|
|
1396
|
-
getLastModified() {
|
|
1397
|
-
return this.headers.getDate("Last-Modified");
|
|
1398
|
-
}
|
|
1399
|
-
/**
|
|
1400
|
-
* Sets the Last-Modified HTTP header with a DateTime instance.
|
|
1401
|
-
*
|
|
1402
|
-
* Passing null as value will remove the header.
|
|
1403
|
-
*
|
|
1404
|
-
* @return $this
|
|
1405
|
-
*
|
|
1406
|
-
* @final
|
|
1407
|
-
*/
|
|
1408
|
-
setLastModified(date) {
|
|
1409
|
-
if (!date) {
|
|
1410
|
-
this.headers.remove("Last-Modified");
|
|
1411
|
-
return this;
|
|
1412
|
-
}
|
|
1413
|
-
date = new _h3ravel_support.DateTime(date).setTimezone("UTC");
|
|
1414
|
-
this.headers.set("Last-Modified", date.format("ddd, DD MMM YYYY HH:mm:ss") + " GMT");
|
|
1415
|
-
return this;
|
|
1416
|
-
}
|
|
1417
|
-
/**
|
|
1418
|
-
* Returns the literal value of the ETag HTTP header.
|
|
1419
|
-
*/
|
|
1420
|
-
getEtag() {
|
|
1421
|
-
return this.headers.get("ETag");
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* Sets the ETag value.
|
|
1425
|
-
*
|
|
1426
|
-
* @param etag The ETag unique identifier or null to remove the header
|
|
1427
|
-
* @param weak Whether you want a weak ETag or not
|
|
1428
|
-
*/
|
|
1429
|
-
setEtag(etag, weak = false) {
|
|
1430
|
-
if (!etag) this.headers.remove("Etag");
|
|
1431
|
-
else {
|
|
1432
|
-
if (!etag.startsWith("\"")) etag = "\"" + etag + "\"";
|
|
1433
|
-
this.headers.set("ETag", (true === weak ? "W/" : "") + etag);
|
|
1434
|
-
}
|
|
1435
|
-
return this;
|
|
1436
|
-
}
|
|
1437
|
-
/**
|
|
1438
|
-
* Sets the response's cache headers (validation and/or expiration).
|
|
1439
|
-
*
|
|
1440
|
-
* Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag.
|
|
1441
|
-
*
|
|
1442
|
-
* @throws {InvalidArgumentException}
|
|
1443
|
-
*/
|
|
1444
|
-
setCache(options) {
|
|
1445
|
-
const invalidKeys = Object.keys(options).filter((key) => !(key in _h3ravel_foundation.HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES));
|
|
1446
|
-
if (invalidKeys.length > 0) throw new _h3ravel_support.InvalidArgumentException(`Response does not support the following options: "${invalidKeys.join("\", \"")}"`);
|
|
1447
|
-
if (options.etag) this.setEtag(options.etag);
|
|
1448
|
-
if (options.last_modified) this.setLastModified(options.last_modified);
|
|
1449
|
-
if (options.max_age) this.setMaxAge(options.max_age);
|
|
1450
|
-
if (options.s_maxage) this.setSharedMaxAge(options.s_maxage);
|
|
1451
|
-
if (options.stale_while_revalidate) this.setStaleWhileRevalidate(options.stale_while_revalidate);
|
|
1452
|
-
if (options.stale_if_error) this.setStaleIfError(options.stale_if_error);
|
|
1453
|
-
for (const [directive, hasValue] of Object.entries(_h3ravel_foundation.HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES)) if (!hasValue && directive in options) {
|
|
1454
|
-
const token = directive.replace(/_/g, "-");
|
|
1455
|
-
if (options[directive]) this.headers.addCacheControlDirective(token);
|
|
1456
|
-
else this.headers.removeCacheControlDirective(token);
|
|
1457
|
-
}
|
|
1458
|
-
if (options.public !== void 0) if (options.public) this.setPublic();
|
|
1459
|
-
else this.setPrivate();
|
|
1460
|
-
if (options.private !== void 0) if (options.private) this.setPrivate();
|
|
1461
|
-
else this.setPublic();
|
|
1462
|
-
return this;
|
|
1463
|
-
}
|
|
1464
|
-
/**
|
|
1465
|
-
* Modifies the response so that it conforms to the rules defined for a 304 status code.
|
|
1466
|
-
*
|
|
1467
|
-
* This sets the status, removes the body, and discards any headers
|
|
1468
|
-
* that MUST NOT be included in 304 responses.
|
|
1469
|
-
* @see https://tools.ietf.org/html/rfc2616#section-10.3.5
|
|
1470
|
-
*/
|
|
1471
|
-
setNotModified() {
|
|
1472
|
-
this.setStatusCode(304);
|
|
1473
|
-
this.setContent();
|
|
1474
|
-
for (const header of [
|
|
1475
|
-
"Allow",
|
|
1476
|
-
"Content-Encoding",
|
|
1477
|
-
"Content-Language",
|
|
1478
|
-
"Content-Length",
|
|
1479
|
-
"Content-MD5",
|
|
1480
|
-
"Content-Type",
|
|
1481
|
-
"Last-Modified"
|
|
1482
|
-
]) this.headers.remove(header);
|
|
1483
|
-
return this;
|
|
1484
|
-
}
|
|
1485
|
-
/**
|
|
1486
|
-
* Add an array of headers to the response.
|
|
1487
|
-
*
|
|
1488
|
-
*/
|
|
1489
|
-
withHeaders(headers) {
|
|
1490
|
-
if (headers instanceof HeaderBag) headers = headers.all();
|
|
1491
|
-
for (const [key, value] of Object.entries(headers)) this.headers.set(key, value);
|
|
1492
|
-
return this;
|
|
1493
|
-
}
|
|
1494
|
-
/**
|
|
1495
|
-
* Set the exception to attach to the response.
|
|
1496
|
-
*/
|
|
1497
|
-
withException(e) {
|
|
1498
|
-
this.exception = e;
|
|
1499
|
-
return this;
|
|
1500
|
-
}
|
|
1501
|
-
/**
|
|
1502
|
-
* Throws the response in a HttpResponseException instance.
|
|
1503
|
-
*
|
|
1504
|
-
* @throws {HttpResponseException}
|
|
1505
|
-
*/
|
|
1506
|
-
throwResponse() {
|
|
1507
|
-
throw new HttpResponseException(this);
|
|
1508
|
-
}
|
|
1509
|
-
/**
|
|
1510
|
-
* Is response invalid?
|
|
1511
|
-
*
|
|
1512
|
-
* @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
|
1513
|
-
*/
|
|
1514
|
-
isInvalid() {
|
|
1515
|
-
return this.statusCode < 100 || this.statusCode >= 600;
|
|
1516
|
-
}
|
|
1517
|
-
/**
|
|
1518
|
-
* Is response informative?
|
|
1519
|
-
*/
|
|
1520
|
-
isInformational() {
|
|
1521
|
-
return this.statusCode >= 100 && this.statusCode < 200;
|
|
1522
|
-
}
|
|
1523
|
-
/**
|
|
1524
|
-
* Is response successful?
|
|
1525
|
-
*/
|
|
1526
|
-
isSuccessful() {
|
|
1527
|
-
return this.statusCode >= 200 && this.statusCode < 300;
|
|
1528
|
-
}
|
|
1529
|
-
/**
|
|
1530
|
-
* Is the response a redirect?
|
|
1531
|
-
*/
|
|
1532
|
-
isRedirection() {
|
|
1533
|
-
return this.statusCode >= 300 && this.statusCode < 400;
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
|
-
* Is there a client error?
|
|
1537
|
-
*/
|
|
1538
|
-
isClientError() {
|
|
1539
|
-
return this.statusCode >= 400 && this.statusCode < 500;
|
|
1540
|
-
}
|
|
1541
|
-
/**
|
|
1542
|
-
* Was there a server side error?
|
|
1543
|
-
*/
|
|
1544
|
-
isServerError() {
|
|
1545
|
-
return this.statusCode >= 500 && this.statusCode < 600;
|
|
1546
|
-
}
|
|
1547
|
-
/**
|
|
1548
|
-
* Is the response OK?
|
|
1549
|
-
*/
|
|
1550
|
-
isOk() {
|
|
1551
|
-
return 200 === this.statusCode;
|
|
1552
|
-
}
|
|
1553
|
-
/**
|
|
1554
|
-
* Is the response forbidden?
|
|
1555
|
-
*/
|
|
1556
|
-
isForbidden() {
|
|
1557
|
-
return 403 === this.statusCode;
|
|
1558
|
-
}
|
|
1559
|
-
/**
|
|
1560
|
-
* Is the response a not found error?
|
|
1561
|
-
*/
|
|
1562
|
-
isNotFound() {
|
|
1563
|
-
return 404 === this.statusCode;
|
|
1564
|
-
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Is the response a redirect of some form?
|
|
1567
|
-
*/
|
|
1568
|
-
isRedirect(location) {
|
|
1569
|
-
if (![
|
|
1570
|
-
201,
|
|
1571
|
-
301,
|
|
1572
|
-
302,
|
|
1573
|
-
303,
|
|
1574
|
-
307,
|
|
1575
|
-
308
|
|
1576
|
-
].includes(this.statusCode)) return false;
|
|
1577
|
-
if (!location) return true;
|
|
1578
|
-
return location === this.headers.get("Location");
|
|
1579
|
-
}
|
|
1580
|
-
/**
|
|
1581
|
-
* Is the response empty?
|
|
1582
|
-
*/
|
|
1583
|
-
isEmpty() {
|
|
1584
|
-
return [204, 304].includes(this.statusCode);
|
|
1585
|
-
}
|
|
1586
|
-
/**
|
|
1587
|
-
* Apply headers before sending response.
|
|
1588
|
-
*/
|
|
1589
|
-
sendHeaders(statusCode) {
|
|
1590
|
-
statusCode ??= this.statusCode;
|
|
1591
|
-
const informational = statusCode >= 100 && statusCode < 200;
|
|
1592
|
-
for (const [name, values] of Object.entries(this.headers.allPreserveCaseWithoutCookies())) {
|
|
1593
|
-
const previousValues = this.sentHeaders[name] ?? null;
|
|
1594
|
-
if (previousValues && JSON.stringify(previousValues) === JSON.stringify(values)) continue;
|
|
1595
|
-
const replace = name.localeCompare("Content-Type", void 0, { sensitivity: "accent" }) === 0;
|
|
1596
|
-
if (previousValues && previousValues.some((v) => !values.includes(v))) this.event.res.headers.delete(name);
|
|
1597
|
-
const newValues = !previousValues ? values : values.filter((v) => !previousValues.includes(v));
|
|
1598
|
-
for (const value of newValues) if (replace) this.event.res.headers.set(name, value);
|
|
1599
|
-
else this.event.res.headers.append(name, value);
|
|
1600
|
-
if (informational) this.sentHeaders[name] = values;
|
|
1601
|
-
}
|
|
1602
|
-
for (const cookie of this.headers.getCookies()) this.event.res.headers.append("Set-Cookie", cookie.toString());
|
|
1603
|
-
if (informational) return this;
|
|
1604
|
-
this.setStatusCode(statusCode, this.statusText);
|
|
1605
|
-
return this;
|
|
1606
|
-
}
|
|
1607
|
-
/**
|
|
1608
|
-
* Prepares the Response before it is sent to the client.
|
|
1609
|
-
*
|
|
1610
|
-
* This method tweaks the Response to ensure that it is
|
|
1611
|
-
* compliant with RFC 2616. Most of the changes are based on
|
|
1612
|
-
* the Request that is "associated" with this Response.
|
|
1613
|
-
**/
|
|
1614
|
-
prepare(request) {
|
|
1615
|
-
const isInformational = this.isInformational();
|
|
1616
|
-
const isEmpty = this.isEmpty();
|
|
1617
|
-
if (isInformational || isEmpty) {
|
|
1618
|
-
this.setContent();
|
|
1619
|
-
this.headers.remove("Content-Type");
|
|
1620
|
-
this.headers.remove("Content-Length");
|
|
1621
|
-
return this;
|
|
1622
|
-
}
|
|
1623
|
-
if (!this.headers.has("Content-Type")) {
|
|
1624
|
-
const format = request.getRequestFormat();
|
|
1625
|
-
if (format) {
|
|
1626
|
-
const mimeType = request.getMimeType(format);
|
|
1627
|
-
if (mimeType) this.headers.set("Content-Type", mimeType);
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
const charset = this.charset || "UTF-8";
|
|
1631
|
-
this.charset ??= charset;
|
|
1632
|
-
const currentType = this.headers.get("Content-Type") || "";
|
|
1633
|
-
if (!this.headers.has("Content-Type")) this.headers.set("Content-Type", `text/html; charset=${charset}`);
|
|
1634
|
-
else if (currentType.toLowerCase().startsWith("text/") && !/charset=/i.test(currentType)) this.headers.set("Content-Type", `${currentType}; charset=${charset}`);
|
|
1635
|
-
if (this.headers.has("Transfer-Encoding")) this.headers.remove("Content-Length");
|
|
1636
|
-
if (request.isMethod("HEAD")) {
|
|
1637
|
-
const length = this.headers.get("Content-Length");
|
|
1638
|
-
this.setContent(void 0);
|
|
1639
|
-
if (length) this.headers.set("Content-Length", length);
|
|
1640
|
-
}
|
|
1641
|
-
if ((request._server?.get("SERVER_PROTOCOL") || "HTTP/1.1") !== "HTTP/1.0") this.setProtocolVersion("1.1");
|
|
1642
|
-
if (this.getProtocolVersion() === "1.0" && (this.headers.get("Cache-Control") || "").includes("no-cache")) {
|
|
1643
|
-
this.headers.set("pragma", "no-cache");
|
|
1644
|
-
this.headers.set("expires", "-1");
|
|
1645
|
-
}
|
|
1646
|
-
this.ensureIEOverSSLCompatibility(request);
|
|
1647
|
-
if (request.isSecure()) for (const cookie of this.headers.getCookies()) cookie.setSecureDefault(true);
|
|
1648
|
-
return this;
|
|
1649
|
-
}
|
|
1650
|
-
/**
|
|
1651
|
-
* Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
|
|
1652
|
-
*
|
|
1653
|
-
* @see http://support.microsoft.com/kb/323308
|
|
1654
|
-
*/
|
|
1655
|
-
ensureIEOverSSLCompatibility(request) {
|
|
1656
|
-
const contentDisposition = this.headers.get("Content-Disposition") || "";
|
|
1657
|
-
const userAgent = request.headers.get("user-agent") || "";
|
|
1658
|
-
if (contentDisposition.toLowerCase().includes("attachment") && /MSIE (.*?);/i.test(userAgent) && request.headers.get("x-forwarded-proto") === "https") {
|
|
1659
|
-
const match = userAgent.match(/MSIE (.*?);/i);
|
|
1660
|
-
if (match) {
|
|
1661
|
-
if (parseInt(match[1], 10) < 9) this.headers.remove("Cache-Control");
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
};
|
|
1666
|
-
//#endregion
|
|
1667
|
-
//#region src/Utilities/Responsable.ts
|
|
1668
|
-
var Responsable = class extends _h3ravel_contracts.IResponsable {
|
|
1669
|
-
toResponse(request) {
|
|
1670
|
-
return new Response(request.app, this.body, this.status, Object.fromEntries(this.headers.entries()));
|
|
1671
|
-
}
|
|
1672
|
-
HTTPResponse() {
|
|
1673
|
-
return super.constructor;
|
|
1674
|
-
}
|
|
1675
|
-
};
|
|
1676
|
-
//#endregion
|
|
1677
|
-
//#region src/Response.ts
|
|
1678
|
-
var Response = class Response extends HttpResponse {
|
|
1679
|
-
app;
|
|
1680
|
-
static codes = _h3ravel_foundation.ResponseCodes;
|
|
1681
|
-
initializationData = {};
|
|
1682
|
-
/**
|
|
1683
|
-
* The current Http Context
|
|
1684
|
-
*/
|
|
1685
|
-
context;
|
|
1686
|
-
constructor(app, event, status = 200, headers = {}) {
|
|
1687
|
-
const hasHeaders = Object.entries(headers).length > 0;
|
|
1688
|
-
const content = !(event instanceof h3.H3Event) ? event : "";
|
|
1689
|
-
event = event instanceof h3.H3Event ? event : app.getHttpContext("event");
|
|
1690
|
-
super(event);
|
|
1691
|
-
this.app = app;
|
|
1692
|
-
if (content || status !== 200 || hasHeaders) {
|
|
1693
|
-
this.setContent(content).setStatusCode(status);
|
|
1694
|
-
if (hasHeaders) this.withHeaders(headers);
|
|
1695
|
-
}
|
|
1696
|
-
this.initializationData = {
|
|
1697
|
-
app,
|
|
1698
|
-
event,
|
|
1699
|
-
status,
|
|
1700
|
-
headers,
|
|
1701
|
-
content
|
|
1702
|
-
};
|
|
1703
|
-
}
|
|
1704
|
-
/**
|
|
1705
|
-
* Sends content for the current web response.
|
|
1706
|
-
*/
|
|
1707
|
-
sendContent(type, parse) {
|
|
1708
|
-
if (!type) type = _h3ravel_support.Str.detectContentType(this.content);
|
|
1709
|
-
return this[type].call(this, this.content, parse);
|
|
1710
|
-
}
|
|
1711
|
-
/**
|
|
1712
|
-
* Sends content for the current web response.
|
|
1713
|
-
*/
|
|
1714
|
-
send(type) {
|
|
1715
|
-
return this.sendContent(type, true);
|
|
1716
|
-
}
|
|
1717
|
-
async view(viewPath, data, parse) {
|
|
1718
|
-
const base = this.html(await this.app.make("edge").render(viewPath, data), parse);
|
|
1719
|
-
return new Responsable(base.body, base);
|
|
1720
|
-
}
|
|
1721
|
-
async viewTemplate(content, data, parse) {
|
|
1722
|
-
return this.html(await this.app.make("edge").renderRaw(content, data), parse);
|
|
1723
|
-
}
|
|
1724
|
-
html(content, parse) {
|
|
1725
|
-
const base = this.httpResponse("text/html", content ?? this.content, parse);
|
|
1726
|
-
if (base instanceof Response) return new Responsable(base.content, {
|
|
1727
|
-
status: base.statusCode,
|
|
1728
|
-
statusText: base.statusText,
|
|
1729
|
-
headers: base.headers
|
|
1730
|
-
});
|
|
1731
|
-
return new Responsable(base.body, base);
|
|
1732
|
-
}
|
|
1733
|
-
json(data, parse) {
|
|
1734
|
-
const content = data ?? this.content;
|
|
1735
|
-
return this.httpResponse("application/json", typeof content !== "string" ? JSON.stringify(content) : content, parse);
|
|
1736
|
-
}
|
|
1737
|
-
text(content, parse) {
|
|
1738
|
-
return this.httpResponse("text/plain", content ?? this.content, parse);
|
|
1739
|
-
}
|
|
1740
|
-
xml(data, parse) {
|
|
1741
|
-
return this.httpResponse("application/xml", data ?? this.content, parse);
|
|
1742
|
-
}
|
|
1743
|
-
httpResponse(contentType, data, parse) {
|
|
1744
|
-
if (parse) {
|
|
1745
|
-
this.sendHeaders();
|
|
1746
|
-
return new Responsable(data ?? this.content, {
|
|
1747
|
-
status: this.statusCode,
|
|
1748
|
-
statusText: this.statusText,
|
|
1749
|
-
headers: { "content-type": `${contentType}; charset=${this.charset}` }
|
|
1750
|
-
});
|
|
1751
|
-
}
|
|
1752
|
-
if (this.content?.trim()?.length <= 0) this.content = data ?? "";
|
|
1753
|
-
this.setStatusCode(this.statusCode, this.statusText);
|
|
1754
|
-
this.setHeader("content-type", `${contentType}; charset=${this.charset}`);
|
|
1755
|
-
return this;
|
|
1756
|
-
}
|
|
1757
|
-
/**
|
|
1758
|
-
* Redirect to another URL.
|
|
1759
|
-
*/
|
|
1760
|
-
redirect(location, status = 302, statusText) {
|
|
1761
|
-
return this.setStatusCode(status, statusText || (status === 301 ? "Moved Permanently" : "Found")).setContent(`<html><head><meta http-equiv="refresh" content="0; url=${location.replace(/"/g, "%22")}" /></head></html>`).withHeaders({
|
|
1762
|
-
"content-type": "text/html; charset=utf-8",
|
|
1763
|
-
location
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Dump the response.
|
|
1768
|
-
*/
|
|
1769
|
-
dump() {
|
|
1770
|
-
dump(this.headers.all(), { charset: this.charset }, { version: this.version }, { statusText: this.statusText }, { statusCode: this.statusCode });
|
|
1771
|
-
return this;
|
|
1772
|
-
}
|
|
1773
|
-
getEvent(key) {
|
|
1774
|
-
return (0, _h3ravel_support.safeDot)(this.event, key);
|
|
1775
|
-
}
|
|
1776
|
-
/**
|
|
1777
|
-
* Reset the response class to it's defautl
|
|
1778
|
-
*/
|
|
1779
|
-
reset() {
|
|
1780
|
-
return this;
|
|
1781
|
-
}
|
|
1782
|
-
};
|
|
1783
|
-
//#endregion
|
|
1784
|
-
//#region src/JsonResponse.ts
|
|
1785
|
-
/**
|
|
1786
|
-
* Response represents an HTTP response in JSON format.
|
|
1787
|
-
*
|
|
1788
|
-
* Note that this class does not force the returned JSON content to be an
|
|
1789
|
-
* object. It is however recommended that you do return an object as it
|
|
1790
|
-
* protects yourself against XSSI and JSON-JavaScript Hijacking.
|
|
1791
|
-
*
|
|
1792
|
-
* @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside
|
|
1793
|
-
*/
|
|
1794
|
-
var JsonResponse = class JsonResponse extends Response {
|
|
1795
|
-
data;
|
|
1796
|
-
callback;
|
|
1797
|
-
/**
|
|
1798
|
-
* @param bool $json If the data is already a JSON string
|
|
1799
|
-
*/
|
|
1800
|
-
constructor(app, data, status = 200, headers = {}, json = false) {
|
|
1801
|
-
super(app, "", status, headers);
|
|
1802
|
-
if (json && typeof data !== "string" && typeof data !== "number" && typeof data.toString === "undefined") throw new TypeError(`"${this.constructor.name}": If \`json\` is set to true, argument \`data\` must be a string or object implementing toString(), "${typeof data}" given.`);
|
|
1803
|
-
data ??= {};
|
|
1804
|
-
if (json) this.setJson(data);
|
|
1805
|
-
else this.setData(data);
|
|
1806
|
-
}
|
|
1807
|
-
/**
|
|
1808
|
-
* Sets the JSONP callback.
|
|
1809
|
-
*
|
|
1810
|
-
* @param callback The JSONP callback or null to use none
|
|
1811
|
-
*
|
|
1812
|
-
* @throws {InvalidArgumentException} When the callback name is not valid
|
|
1813
|
-
*/
|
|
1814
|
-
setCallback(callback) {
|
|
1815
|
-
if (typeof callback !== "undefined") {
|
|
1816
|
-
const pattern = /^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\u200C\u200D]*(?:\[(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\d+)\])*?$/u;
|
|
1817
|
-
const reserved = [
|
|
1818
|
-
"break",
|
|
1819
|
-
"do",
|
|
1820
|
-
"instanceof",
|
|
1821
|
-
"typeof",
|
|
1822
|
-
"case",
|
|
1823
|
-
"else",
|
|
1824
|
-
"new",
|
|
1825
|
-
"var",
|
|
1826
|
-
"catch",
|
|
1827
|
-
"finally",
|
|
1828
|
-
"return",
|
|
1829
|
-
"void",
|
|
1830
|
-
"continue",
|
|
1831
|
-
"for",
|
|
1832
|
-
"switch",
|
|
1833
|
-
"while",
|
|
1834
|
-
"debugger",
|
|
1835
|
-
"function",
|
|
1836
|
-
"this",
|
|
1837
|
-
"with",
|
|
1838
|
-
"default",
|
|
1839
|
-
"if",
|
|
1840
|
-
"throw",
|
|
1841
|
-
"delete",
|
|
1842
|
-
"in",
|
|
1843
|
-
"try",
|
|
1844
|
-
"class",
|
|
1845
|
-
"enum",
|
|
1846
|
-
"extends",
|
|
1847
|
-
"super",
|
|
1848
|
-
"const",
|
|
1849
|
-
"export",
|
|
1850
|
-
"import",
|
|
1851
|
-
"implements",
|
|
1852
|
-
"let",
|
|
1853
|
-
"private",
|
|
1854
|
-
"public",
|
|
1855
|
-
"yield",
|
|
1856
|
-
"interface",
|
|
1857
|
-
"package",
|
|
1858
|
-
"protected",
|
|
1859
|
-
"static",
|
|
1860
|
-
"null",
|
|
1861
|
-
"true",
|
|
1862
|
-
"false"
|
|
1863
|
-
];
|
|
1864
|
-
const parts = callback.split(".");
|
|
1865
|
-
for (const part of parts) if (!pattern.test(part) || reserved.includes(part)) throw new _h3ravel_support.InvalidArgumentException("The callback name is not valid.");
|
|
1866
|
-
}
|
|
1867
|
-
this.callback = callback;
|
|
1868
|
-
return this.update();
|
|
1869
|
-
}
|
|
1870
|
-
/**
|
|
1871
|
-
* Factory method for chainability.
|
|
1872
|
-
*
|
|
1873
|
-
* @example
|
|
1874
|
-
*
|
|
1875
|
-
* return JsonResponse.fromJsonString('{"key": "value"}').setSharedMaxAge(300);
|
|
1876
|
-
*
|
|
1877
|
-
* @param data The JSON response string
|
|
1878
|
-
* @param status The response status code (200 "OK" by default)
|
|
1879
|
-
* @param headers An array of response headers
|
|
1880
|
-
*/
|
|
1881
|
-
static fromJsonString(app, data, status = 200, headers = {}) {
|
|
1882
|
-
return new JsonResponse(app, data, status, headers, true);
|
|
1883
|
-
}
|
|
1884
|
-
/**
|
|
1885
|
-
* Sets a raw string containing a JSON document to be sent.
|
|
1886
|
-
*
|
|
1887
|
-
* @param json
|
|
1888
|
-
* @returns
|
|
1889
|
-
*/
|
|
1890
|
-
setJson(json) {
|
|
1891
|
-
this.data = json;
|
|
1892
|
-
return this.update();
|
|
1893
|
-
}
|
|
1894
|
-
/**
|
|
1895
|
-
* Sets the data to be sent as JSON.
|
|
1896
|
-
*
|
|
1897
|
-
* @param data
|
|
1898
|
-
* @returns
|
|
1899
|
-
*/
|
|
1900
|
-
setData(data = {}) {
|
|
1901
|
-
let content;
|
|
1902
|
-
try {
|
|
1903
|
-
if (data.toJson === "undefined") content = JSON.stringify(data.toJson());
|
|
1904
|
-
else if (data.toArray === "undefined") content = JSON.stringify(data.toArray());
|
|
1905
|
-
else content = JSON.stringify(data);
|
|
1906
|
-
} catch (e) {
|
|
1907
|
-
if (e instanceof Error && e.message.startsWith("Failed calling ")) throw e.getPrevious() || e;
|
|
1908
|
-
throw e;
|
|
1909
|
-
}
|
|
1910
|
-
return this.setJson(content);
|
|
1911
|
-
}
|
|
1912
|
-
/**
|
|
1913
|
-
* Get the json_decoded data from the response.
|
|
1914
|
-
*
|
|
1915
|
-
* @param assoc
|
|
1916
|
-
*/
|
|
1917
|
-
getData() {
|
|
1918
|
-
return JSON.parse(String(this.data));
|
|
1919
|
-
}
|
|
1920
|
-
/**
|
|
1921
|
-
* Sets the JSONP callback.
|
|
1922
|
-
*
|
|
1923
|
-
* @param callback
|
|
1924
|
-
*/
|
|
1925
|
-
withCallback(callback) {
|
|
1926
|
-
return this.setCallback(callback);
|
|
1927
|
-
}
|
|
1928
|
-
/**
|
|
1929
|
-
* Updates the content and headers according to the JSON data and callback.
|
|
1930
|
-
*/
|
|
1931
|
-
update() {
|
|
1932
|
-
if (typeof this.callback !== "undefined") {
|
|
1933
|
-
this.headers.set("Content-Type", "text/javascript");
|
|
1934
|
-
return this.setContent(`/**/${this.callback}(${this.data});`);
|
|
1935
|
-
}
|
|
1936
|
-
if (!this.headers.has("Content-Type") || "text/javascript" === this.headers.get("Content-Type")) this.headers.set("Content-Type", "application/json");
|
|
1937
|
-
return this.setContent(this.data);
|
|
1938
|
-
}
|
|
1939
|
-
};
|
|
1940
|
-
//#endregion
|
|
1941
|
-
//#region src/Middleware/TrustHosts.ts
|
|
1942
|
-
var TrustHosts = class TrustHosts extends Middleware {
|
|
1943
|
-
/**
|
|
1944
|
-
* The trusted hosts that have been configured to always be trusted.
|
|
1945
|
-
*/
|
|
1946
|
-
static alwaysTrust;
|
|
1947
|
-
/**
|
|
1948
|
-
* Indicates whether subdomains of the application URL should be trusted.
|
|
1949
|
-
*/
|
|
1950
|
-
static subdomains;
|
|
1951
|
-
/**
|
|
1952
|
-
* Get the host patterns that should be trusted.
|
|
1953
|
-
*/
|
|
1954
|
-
hosts() {
|
|
1955
|
-
if (!TrustHosts.alwaysTrust) return [this.allSubdomainsOfApplicationUrl()];
|
|
1956
|
-
let hosts;
|
|
1957
|
-
switch (true) {
|
|
1958
|
-
case Array.isArray(TrustHosts.alwaysTrust):
|
|
1959
|
-
hosts = TrustHosts.alwaysTrust;
|
|
1960
|
-
break;
|
|
1961
|
-
case typeof TrustHosts.alwaysTrust === "function":
|
|
1962
|
-
hosts = TrustHosts.alwaysTrust();
|
|
1963
|
-
break;
|
|
1964
|
-
default:
|
|
1965
|
-
hosts = [];
|
|
1966
|
-
break;
|
|
1967
|
-
}
|
|
1968
|
-
if (TrustHosts.subdomains) hosts.push(this.allSubdomainsOfApplicationUrl());
|
|
1969
|
-
return hosts;
|
|
1970
|
-
}
|
|
1971
|
-
/**
|
|
1972
|
-
* Handle the incoming request.
|
|
1973
|
-
*
|
|
1974
|
-
* @param request
|
|
1975
|
-
* @param next
|
|
1976
|
-
*/
|
|
1977
|
-
async handle(request, next) {
|
|
1978
|
-
if (this.shouldSpecifyTrustedHosts()) Request.setTrustedHosts(this.hosts().filter((e) => typeof e !== "undefined"));
|
|
1979
|
-
return next(request);
|
|
1980
|
-
}
|
|
1981
|
-
/**
|
|
1982
|
-
* Specify the hosts that should always be trusted.
|
|
1983
|
-
*
|
|
1984
|
-
* @param hosts
|
|
1985
|
-
* @param subdomains
|
|
1986
|
-
*/
|
|
1987
|
-
static at(hosts, subdomains = true) {
|
|
1988
|
-
TrustHosts.alwaysTrust = hosts;
|
|
1989
|
-
TrustHosts.subdomains = subdomains;
|
|
1990
|
-
}
|
|
1991
|
-
/**
|
|
1992
|
-
* Determine if the application should specify trusted hosts.
|
|
1993
|
-
*
|
|
1994
|
-
* @return bool
|
|
1995
|
-
*/
|
|
1996
|
-
shouldSpecifyTrustedHosts() {
|
|
1997
|
-
return !this.app?.environment("local") && !this.app?.runningUnitTests();
|
|
1998
|
-
}
|
|
1999
|
-
/**
|
|
2000
|
-
* Get a regular expression matching the application URL and all of its subdomains.
|
|
2001
|
-
*/
|
|
2002
|
-
allSubdomainsOfApplicationUrl() {
|
|
2003
|
-
const appUrl = this.app?.make("config").get("app.url");
|
|
2004
|
-
const host = new URL(appUrl).host;
|
|
2005
|
-
if (host) return `^(.+\\.)?${host.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`;
|
|
2006
|
-
}
|
|
2007
|
-
/**
|
|
2008
|
-
* Flush the state of the middleware.
|
|
2009
|
-
*
|
|
2010
|
-
* @return void
|
|
2011
|
-
*/
|
|
2012
|
-
static flushState() {
|
|
2013
|
-
TrustHosts.alwaysTrust = void 0;
|
|
2014
|
-
TrustHosts.subdomains = void 0;
|
|
2015
|
-
}
|
|
2016
|
-
};
|
|
2017
|
-
__decorate([
|
|
2018
|
-
(0, _h3ravel_foundation.Injectable)(),
|
|
2019
|
-
__decorateMetadata("design:type", Function),
|
|
2020
|
-
__decorateMetadata("design:paramtypes", [typeof Request === "undefined" ? Object : Request, Function]),
|
|
2021
|
-
__decorateMetadata("design:returntype", Promise)
|
|
2022
|
-
], TrustHosts.prototype, "handle", null);
|
|
2023
|
-
//#endregion
|
|
2024
|
-
//#region src/Providers/HttpServiceProvider.ts
|
|
2025
|
-
/**
|
|
2026
|
-
* Sets up HTTP kernel and request lifecycle.
|
|
2027
|
-
*
|
|
2028
|
-
* Register Request, Response, and Middleware classes.
|
|
2029
|
-
* Configure global middleware stack.
|
|
2030
|
-
* Boot HTTP kernel.
|
|
2031
|
-
*
|
|
2032
|
-
* Auto-Registered
|
|
2033
|
-
*/
|
|
2034
|
-
var HttpServiceProvider = class {
|
|
2035
|
-
app;
|
|
2036
|
-
static priority = 998;
|
|
2037
|
-
registeredCommands;
|
|
2038
|
-
constructor(app) {
|
|
2039
|
-
this.app = app;
|
|
2040
|
-
}
|
|
2041
|
-
register() {
|
|
2042
|
-
/**
|
|
2043
|
-
* Register Musket Commands
|
|
2044
|
-
*/
|
|
2045
|
-
this.registeredCommands = [FireCommand];
|
|
2046
|
-
this.app.alias([
|
|
2047
|
-
[Request, "http.request"],
|
|
2048
|
-
[_h3ravel_contracts.IRequest, "http.request"],
|
|
2049
|
-
[Response, "http.response"],
|
|
2050
|
-
[_h3ravel_contracts.IResponse, "http.response"],
|
|
2051
|
-
[HttpContext, "http.context"],
|
|
2052
|
-
[_h3ravel_contracts.IHttpContext, "http.context"]
|
|
2053
|
-
]);
|
|
2054
|
-
}
|
|
2055
|
-
boot() {}
|
|
2056
|
-
};
|
|
2057
|
-
//#endregion
|
|
2058
|
-
//#region src/Utilities/ParamBag.ts
|
|
2059
|
-
/**
|
|
2060
|
-
* ParamBag is a container for key/value pairs
|
|
2061
|
-
* for Node/H3 environments.
|
|
2062
|
-
*/
|
|
2063
|
-
var ParamBag = class {
|
|
2064
|
-
parameters;
|
|
2065
|
-
event;
|
|
2066
|
-
constructor(parameters = {}, event) {
|
|
2067
|
-
this.parameters = parameters;
|
|
2068
|
-
this.event = event;
|
|
2069
|
-
this.parameters = { ...parameters };
|
|
2070
|
-
}
|
|
2071
|
-
/**
|
|
2072
|
-
* Returns the parameters.
|
|
2073
|
-
* @
|
|
2074
|
-
* @param key The name of the parameter to return or undefined to get them all
|
|
2075
|
-
*
|
|
2076
|
-
* @throws BadRequestException if the value is not an array
|
|
2077
|
-
*/
|
|
2078
|
-
all(key) {
|
|
2079
|
-
if (!key) return { ...this.parameters };
|
|
2080
|
-
const value = key ? this.parameters[key] : void 0;
|
|
2081
|
-
if (value && typeof value !== "object") throw new BadRequestException(`Unexpected value for parameter "${key}": expected object, got ${typeof value}`);
|
|
2082
|
-
return value || {};
|
|
2083
|
-
}
|
|
2084
|
-
get(key, defaultValue) {
|
|
2085
|
-
return key in this.parameters ? this.parameters[key] : defaultValue;
|
|
2086
|
-
}
|
|
2087
|
-
set(key, value) {
|
|
2088
|
-
this.parameters[key] = value;
|
|
2089
|
-
}
|
|
2090
|
-
/**
|
|
2091
|
-
* Returns true if the parameter is defined.
|
|
2092
|
-
*
|
|
2093
|
-
* @param key
|
|
2094
|
-
*/
|
|
2095
|
-
has(key) {
|
|
2096
|
-
return Object.prototype.hasOwnProperty.call(this.parameters, key);
|
|
2097
|
-
}
|
|
2098
|
-
/**
|
|
2099
|
-
* Removes a parameter.
|
|
2100
|
-
*
|
|
2101
|
-
* @param key
|
|
2102
|
-
*/
|
|
2103
|
-
remove(key) {
|
|
2104
|
-
delete this.parameters[key];
|
|
2105
|
-
}
|
|
2106
|
-
/**
|
|
2107
|
-
*
|
|
2108
|
-
* Returns the parameter as string.
|
|
2109
|
-
*
|
|
2110
|
-
* @param key
|
|
2111
|
-
* @param defaultValue
|
|
2112
|
-
* @throws UnexpectedValueException if the value cannot be converted to string
|
|
2113
|
-
* @returns
|
|
2114
|
-
*/
|
|
2115
|
-
getString(key, defaultValue = "") {
|
|
2116
|
-
const value = this.get(key, defaultValue);
|
|
2117
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2118
|
-
throw new UnexpectedValueException(`Parameter "${key}" cannot be converted to string`);
|
|
2119
|
-
}
|
|
2120
|
-
/**
|
|
2121
|
-
* Returns the parameter value converted to integer.
|
|
2122
|
-
*
|
|
2123
|
-
* @param key
|
|
2124
|
-
* @param defaultValue
|
|
2125
|
-
* @throws UnexpectedValueException if the value cannot be converted to integer
|
|
2126
|
-
*/
|
|
2127
|
-
getInt(key, defaultValue = 0) {
|
|
2128
|
-
const value = parseInt(this.get(key, defaultValue), 10);
|
|
2129
|
-
if (isNaN(value)) throw new Error(`Parameter "${key}" is not an integer`);
|
|
2130
|
-
return value;
|
|
2131
|
-
}
|
|
2132
|
-
/**
|
|
2133
|
-
* Returns the parameter value converted to boolean.
|
|
2134
|
-
*
|
|
2135
|
-
* @param key
|
|
2136
|
-
* @param defaultValue
|
|
2137
|
-
* @throws UnexpectedValueException if the value cannot be converted to a boolean
|
|
2138
|
-
*/
|
|
2139
|
-
getBoolean(key, defaultValue = false) {
|
|
2140
|
-
const value = this.get(key, defaultValue);
|
|
2141
|
-
if (typeof value === "boolean") return value;
|
|
2142
|
-
if ([
|
|
2143
|
-
"1",
|
|
2144
|
-
"true",
|
|
2145
|
-
"yes"
|
|
2146
|
-
].includes(String(value).toLowerCase())) return true;
|
|
2147
|
-
if ([
|
|
2148
|
-
"0",
|
|
2149
|
-
"false",
|
|
2150
|
-
"no"
|
|
2151
|
-
].includes(String(value).toLowerCase())) return false;
|
|
2152
|
-
throw new Error(`Parameter "${key}" cannot be converted to boolean`);
|
|
2153
|
-
}
|
|
2154
|
-
/**
|
|
2155
|
-
* Returns the alphabetic characters of the parameter value.
|
|
2156
|
-
*
|
|
2157
|
-
* @param key
|
|
2158
|
-
* @param defaultValue
|
|
2159
|
-
* @throws UnexpectedValueException if the value cannot be converted to string
|
|
2160
|
-
*/
|
|
2161
|
-
getAlpha(key, defaultValue = "") {
|
|
2162
|
-
return this.getString(key, defaultValue).replace(/[^a-z]/gi, "");
|
|
2163
|
-
}
|
|
2164
|
-
/**
|
|
2165
|
-
* Returns the alphabetic characters and digits of the parameter value.
|
|
2166
|
-
*
|
|
2167
|
-
* @param key
|
|
2168
|
-
* @param defaultValue
|
|
2169
|
-
* @throws UnexpectedValueException if the value cannot be converted to string
|
|
2170
|
-
*/
|
|
2171
|
-
getAlnum(key, defaultValue = "") {
|
|
2172
|
-
return this.getString(key, defaultValue).replace(/[^a-z0-9]/gi, "");
|
|
2173
|
-
}
|
|
2174
|
-
/**
|
|
2175
|
-
* Returns the digits of the parameter value.
|
|
2176
|
-
*
|
|
2177
|
-
* @param key
|
|
2178
|
-
* @param defaultValue
|
|
2179
|
-
* @throws UnexpectedValueException if the value cannot be converted to string
|
|
2180
|
-
* @returns
|
|
2181
|
-
**/
|
|
2182
|
-
getDigits(key, defaultValue = "") {
|
|
2183
|
-
return this.getString(key, defaultValue).replace(/\D/g, "");
|
|
2184
|
-
}
|
|
2185
|
-
/**
|
|
2186
|
-
* Returns the parameter keys.
|
|
2187
|
-
*/
|
|
2188
|
-
keys() {
|
|
2189
|
-
return Object.keys(this.parameters);
|
|
2190
|
-
}
|
|
2191
|
-
/**
|
|
2192
|
-
* Replaces the current parameters by a new set.
|
|
2193
|
-
*/
|
|
2194
|
-
replace(parameters = {}) {
|
|
2195
|
-
this.parameters = { ...parameters };
|
|
2196
|
-
}
|
|
2197
|
-
/**
|
|
2198
|
-
* Adds parameters.
|
|
2199
|
-
*/
|
|
2200
|
-
add(parameters = {}) {
|
|
2201
|
-
this.parameters = {
|
|
2202
|
-
...this.parameters,
|
|
2203
|
-
...parameters
|
|
2204
|
-
};
|
|
2205
|
-
}
|
|
2206
|
-
/**
|
|
2207
|
-
* Returns the number of parameters.
|
|
2208
|
-
*/
|
|
2209
|
-
count() {
|
|
2210
|
-
return Object.keys(this.parameters).length;
|
|
2211
|
-
}
|
|
2212
|
-
/**
|
|
2213
|
-
* Returns an iterator for parameters.
|
|
2214
|
-
*
|
|
2215
|
-
* @returns
|
|
2216
|
-
*/
|
|
2217
|
-
[Symbol.iterator]() {
|
|
2218
|
-
return Object.entries(this.parameters)[Symbol.iterator]();
|
|
2219
|
-
}
|
|
2220
|
-
};
|
|
2221
|
-
//#endregion
|
|
2222
|
-
//#region src/Utilities/InputBag.ts
|
|
2223
|
-
/**
|
|
2224
|
-
* InputBag is a container for user input values
|
|
2225
|
-
* (e.g., query params, body, cookies)
|
|
2226
|
-
* for Node/H3 environments.
|
|
2227
|
-
*/
|
|
2228
|
-
var InputBag = class extends ParamBag {
|
|
2229
|
-
constructor(inputs = {}, event) {
|
|
2230
|
-
super(inputs, event);
|
|
2231
|
-
this.add(inputs);
|
|
2232
|
-
}
|
|
2233
|
-
/**
|
|
2234
|
-
* Returns a scalar input value by name.
|
|
2235
|
-
*
|
|
2236
|
-
* @param key
|
|
2237
|
-
* @param defaultValue
|
|
2238
|
-
* @throws BadRequestException if the input contains a non-scalar value
|
|
2239
|
-
* @returns
|
|
2240
|
-
*/
|
|
2241
|
-
get(key, defaultValue = null) {
|
|
2242
|
-
if (defaultValue !== null && typeof defaultValue !== "string" && typeof defaultValue !== "number" && typeof defaultValue !== "boolean") throw new TypeError(`Expected a scalar value as 2nd argument to get(), got ${typeof defaultValue}`);
|
|
2243
|
-
const value = _h3ravel_support.Obj.get(this.parameters, key, defaultValue);
|
|
2244
|
-
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") throw new BadRequestException(`Input value "${key}" contains a non-scalar value.`);
|
|
2245
|
-
return value;
|
|
2246
|
-
}
|
|
2247
|
-
/**
|
|
2248
|
-
* Replaces all current input values.
|
|
2249
|
-
*
|
|
2250
|
-
* @param inputs
|
|
2251
|
-
* @returns
|
|
2252
|
-
*/
|
|
2253
|
-
replace(inputs = {}) {
|
|
2254
|
-
this.parameters = {};
|
|
2255
|
-
this.add(inputs);
|
|
2256
|
-
}
|
|
2257
|
-
/**
|
|
2258
|
-
* Adds multiple input values.
|
|
2259
|
-
*
|
|
2260
|
-
* @param inputs
|
|
2261
|
-
* @returns
|
|
2262
|
-
*/
|
|
2263
|
-
add(inputs = {}) {
|
|
2264
|
-
Object.entries(inputs).forEach(([key, value]) => this.set(key, value));
|
|
2265
|
-
}
|
|
2266
|
-
/**
|
|
2267
|
-
* Sets an input by name.
|
|
2268
|
-
*
|
|
2269
|
-
* @param key
|
|
2270
|
-
* @param value
|
|
2271
|
-
* @throws TypeError if value is not scalar or array
|
|
2272
|
-
* @returns
|
|
2273
|
-
*/
|
|
2274
|
-
set(key, value) {
|
|
2275
|
-
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" && !Array.isArray(value) && typeof value !== "object") throw new TypeError(`Expected scalar, array, object, or null as value for set(), got ${typeof value}`);
|
|
2276
|
-
this.parameters[key] = value;
|
|
2277
|
-
}
|
|
2278
|
-
/**
|
|
2279
|
-
* Returns true if a key exists.
|
|
2280
|
-
*
|
|
2281
|
-
* @param key
|
|
2282
|
-
* @returns
|
|
2283
|
-
*/
|
|
2284
|
-
has(key) {
|
|
2285
|
-
return Object.prototype.hasOwnProperty.call(this.parameters, key);
|
|
2286
|
-
}
|
|
2287
|
-
/**
|
|
2288
|
-
* Returns all parameters.
|
|
2289
|
-
*
|
|
2290
|
-
* @returns
|
|
2291
|
-
*/
|
|
2292
|
-
all() {
|
|
2293
|
-
return { ...this.parameters };
|
|
2294
|
-
}
|
|
2295
|
-
/**
|
|
2296
|
-
* Converts a parameter value to string.
|
|
2297
|
-
*
|
|
2298
|
-
* @param key
|
|
2299
|
-
* @param defaultValue
|
|
2300
|
-
* @throws BadRequestException if input contains a non-scalar value
|
|
2301
|
-
* @returns
|
|
2302
|
-
*/
|
|
2303
|
-
getString(key, defaultValue = "") {
|
|
2304
|
-
const value = this.get(key, defaultValue);
|
|
2305
|
-
return String(value ?? "");
|
|
2306
|
-
}
|
|
2307
|
-
/**
|
|
2308
|
-
* Filters input value with a predicate.
|
|
2309
|
-
* Mimics PHP’s filter_var() in spirit, but simpler.
|
|
2310
|
-
*
|
|
2311
|
-
* @param key
|
|
2312
|
-
* @param defaultValue
|
|
2313
|
-
* @param filterFn
|
|
2314
|
-
* @throws BadRequestException if validation fails
|
|
2315
|
-
* @returns
|
|
2316
|
-
*/
|
|
2317
|
-
filter(key, defaultValue = null, filterFn) {
|
|
2318
|
-
const value = this.has(key) ? this.parameters[key] : defaultValue;
|
|
2319
|
-
if (Array.isArray(value)) throw new BadRequestException(`Input value "${key}" contains an array, but array filtering not allowed.`);
|
|
2320
|
-
if (filterFn && !filterFn(value)) throw new BadRequestException(`Input value "${key}" is invalid and did not pass filter.`);
|
|
2321
|
-
return value;
|
|
2322
|
-
}
|
|
2323
|
-
/**
|
|
2324
|
-
* Returns an enum value by key.
|
|
2325
|
-
*
|
|
2326
|
-
* @param key
|
|
2327
|
-
* @param EnumClass
|
|
2328
|
-
* @param defaultValue
|
|
2329
|
-
* @throws BadRequestException if conversion fails
|
|
2330
|
-
* @returns
|
|
2331
|
-
*/
|
|
2332
|
-
getEnum(key, EnumClass, defaultValue = null) {
|
|
2333
|
-
const value = this.get(key, defaultValue);
|
|
2334
|
-
if (value === null) return defaultValue;
|
|
2335
|
-
if (!Object.values(EnumClass).includes(value)) throw new BadRequestException(`Input "${key}" cannot be converted to enum.`);
|
|
2336
|
-
return value;
|
|
2337
|
-
}
|
|
2338
|
-
/**
|
|
2339
|
-
* Removes a key.
|
|
2340
|
-
*
|
|
2341
|
-
* @param key
|
|
2342
|
-
*/
|
|
2343
|
-
remove(key) {
|
|
2344
|
-
delete this.parameters[key];
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* Returns all keys.
|
|
2348
|
-
*
|
|
2349
|
-
* @returns
|
|
2350
|
-
*/
|
|
2351
|
-
keys() {
|
|
2352
|
-
return Object.keys(this.parameters);
|
|
2353
|
-
}
|
|
2354
|
-
/**
|
|
2355
|
-
* Returns number of parameters.
|
|
2356
|
-
*
|
|
2357
|
-
* @returns
|
|
2358
|
-
*/
|
|
2359
|
-
count() {
|
|
2360
|
-
return this.keys().length;
|
|
2361
|
-
}
|
|
2362
|
-
};
|
|
2363
|
-
//#endregion
|
|
2364
|
-
//#region src/Utilities/FileBag.ts
|
|
2365
|
-
/**
|
|
2366
|
-
* FileBag is a container for uploaded files
|
|
2367
|
-
* for Node/H3 environments.
|
|
2368
|
-
*/
|
|
2369
|
-
var FileBag = class extends ParamBag {
|
|
2370
|
-
parameters = {};
|
|
2371
|
-
constructor(parameters = {}, event) {
|
|
2372
|
-
super(parameters, event);
|
|
2373
|
-
this.replace(parameters);
|
|
2374
|
-
}
|
|
2375
|
-
/**
|
|
2376
|
-
* Replace all stored files.
|
|
2377
|
-
*/
|
|
2378
|
-
replace(files = {}) {
|
|
2379
|
-
this.parameters = {};
|
|
2380
|
-
this.add(files);
|
|
2381
|
-
}
|
|
2382
|
-
/**
|
|
2383
|
-
* Set a file or array of files.
|
|
2384
|
-
*/
|
|
2385
|
-
set(key, value) {
|
|
2386
|
-
if (Array.isArray(value)) this.parameters[key] = value.map((v) => v ? this.convertFileInformation(v) : null).filter(Boolean);
|
|
2387
|
-
else if (value) this.parameters[key] = this.convertFileInformation(value);
|
|
2388
|
-
else this.parameters[key] = null;
|
|
2389
|
-
}
|
|
2390
|
-
/**
|
|
2391
|
-
* Add multiple files.
|
|
2392
|
-
*/
|
|
2393
|
-
add(files = {}) {
|
|
2394
|
-
for (const [key, file] of Object.entries(files)) this.set(key, file);
|
|
2395
|
-
}
|
|
2396
|
-
/**
|
|
2397
|
-
* Get all stored files.
|
|
2398
|
-
*/
|
|
2399
|
-
all() {
|
|
2400
|
-
return this.parameters;
|
|
2401
|
-
}
|
|
2402
|
-
/**
|
|
2403
|
-
* Normalize file input into UploadedFile instances.
|
|
2404
|
-
*/
|
|
2405
|
-
convertFileInformation(file) {
|
|
2406
|
-
if (!file) return null;
|
|
2407
|
-
if (file instanceof UploadedFile) return file;
|
|
2408
|
-
if (file instanceof File) return UploadedFile.createFromBase(file);
|
|
2409
|
-
throw new TypeError("Invalid file input — expected File or UploadedFile instance.");
|
|
2410
|
-
}
|
|
2411
|
-
};
|
|
2412
|
-
//#endregion
|
|
2413
|
-
//#region src/Utilities/ServerBag.ts
|
|
2414
|
-
/**
|
|
2415
|
-
* ServerBag — a simplified version of Symfony's ServerBag
|
|
2416
|
-
* for Node/H3 environments.
|
|
2417
|
-
*
|
|
2418
|
-
* Responsible for extracting and normalizing HTTP headers
|
|
2419
|
-
* from the incoming request.
|
|
2420
|
-
*/
|
|
2421
|
-
var ServerBag = class ServerBag extends ParamBag {
|
|
2422
|
-
static serverData = {};
|
|
2423
|
-
constructor(parameters = {}, event) {
|
|
2424
|
-
super({}, event);
|
|
2425
|
-
this.add(Object.fromEntries(Object.entries(parameters).map(([k, v]) => [k.toLowerCase(), v])));
|
|
2426
|
-
this.add(Object.fromEntries(Object.entries(ServerBag.initialize(event, this.getHeaders())).map(([k, v]) => [_h3ravel_support.Str.slugify(k, "-", { "_": "-" }), v])));
|
|
2427
|
-
this.add(ServerBag.initialize(event, this.getHeaders()));
|
|
2428
|
-
}
|
|
2429
|
-
static initialize(event, headers) {
|
|
2430
|
-
const req = event.req;
|
|
2431
|
-
const url = new URL(req.url ?? "/");
|
|
2432
|
-
const host = headers.host;
|
|
2433
|
-
const method = req.method ?? "GET";
|
|
2434
|
-
const protocol = (0, h3.getRequestProtocol)(event);
|
|
2435
|
-
const isHttps = protocol === "https" || !!event.req.headers.get("x-forwarded-proto")?.includes("https");
|
|
2436
|
-
this.serverData.SERVER_PROTOCOL = protocol;
|
|
2437
|
-
this.serverData.REQUEST_METHOD = method;
|
|
2438
|
-
this.serverData.REQUEST_URI = url.href;
|
|
2439
|
-
this.serverData.PATH_INFO = url.pathname;
|
|
2440
|
-
this.serverData.QUERY_STRING = url.search;
|
|
2441
|
-
this.serverData.SERVER_NAME = host;
|
|
2442
|
-
this.serverData.SERVER_PORT = url.port;
|
|
2443
|
-
this.serverData.REMOTE_ADDR = void 0;
|
|
2444
|
-
this.serverData.REMOTE_PORT = void 0;
|
|
2445
|
-
this.serverData.HTTP_HOST = headers.HOST ?? headers.HTTP_HOST ?? host;
|
|
2446
|
-
this.serverData.HTTP_USER_AGENT = headers.USER_AGENT ?? headers.HTTP_USER_AGENT ?? "";
|
|
2447
|
-
this.serverData.HTTP_ACCEPT = headers.ACCEPT ?? "";
|
|
2448
|
-
this.serverData.HTTP_ACCEPT_LANGUAGE = headers.ACCEPT_LANGUAGE ?? headers.HTTP_ACCEPT_LANGUAGE ?? "";
|
|
2449
|
-
this.serverData.HTTP_ACCEPT_ENCODING = headers.ACCEPT_ENCODING ?? headers.HTTP_ACCEPT_ENCODING ?? "";
|
|
2450
|
-
this.serverData.HTTP_REFERER = headers.REFERER ?? headers.HTTP_REFERER ?? "";
|
|
2451
|
-
this.serverData.HTTPS = isHttps ? "on" : "off";
|
|
2452
|
-
return this.serverData;
|
|
2453
|
-
}
|
|
2454
|
-
/**
|
|
2455
|
-
* Returns all request headers, normalized to uppercase with underscores.
|
|
2456
|
-
* Example: content-type → CONTENT_TYPE
|
|
2457
|
-
*/
|
|
2458
|
-
getHeaders() {
|
|
2459
|
-
const headers = {};
|
|
2460
|
-
for (const [key, value] of Object.entries(this.parameters)) {
|
|
2461
|
-
if (value === void 0 || value === "") continue;
|
|
2462
|
-
switch (key) {
|
|
2463
|
-
case "accept":
|
|
2464
|
-
case "content-type":
|
|
2465
|
-
case "content-length":
|
|
2466
|
-
case "content-md5":
|
|
2467
|
-
case "authorization":
|
|
2468
|
-
headers[key.toUpperCase().replace(/-/g, "_")] = value;
|
|
2469
|
-
break;
|
|
2470
|
-
default: headers[`HTTP_${key.toUpperCase().replace(/-/g, "_")}`] = value;
|
|
2471
|
-
}
|
|
2472
|
-
}
|
|
2473
|
-
if (headers["HTTP_AUTHORIZATION"] || headers["AUTHORIZATION"]) {
|
|
2474
|
-
const auth = headers["HTTP_AUTHORIZATION"] || headers["AUTHORIZATION"] || "";
|
|
2475
|
-
if (auth.toLowerCase().startsWith("basic ")) {
|
|
2476
|
-
const [user, pass] = Buffer.from(auth.slice(6), "base64").toString().split(":", 2);
|
|
2477
|
-
headers["AUTH_TYPE"] = "Basic";
|
|
2478
|
-
headers["AUTH_USER"] = user;
|
|
2479
|
-
headers["AUTH_PASS"] = pass;
|
|
2480
|
-
} else if (auth.toLowerCase().startsWith("bearer ")) {
|
|
2481
|
-
headers["AUTH_TYPE"] = "Bearer";
|
|
2482
|
-
headers["AUTH_TOKEN"] = auth.slice(7);
|
|
2483
|
-
} else if (auth.toLowerCase().startsWith("digest ")) {
|
|
2484
|
-
headers["AUTH_TYPE"] = "Digest";
|
|
2485
|
-
headers["AUTH_DIGEST"] = auth;
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
return headers;
|
|
2489
|
-
}
|
|
2490
|
-
/**
|
|
2491
|
-
* Returns a specific header by name, case-insensitive.
|
|
2492
|
-
*/
|
|
2493
|
-
get(name) {
|
|
2494
|
-
return this.parameters[name.toLowerCase()] || this.parameters[name];
|
|
2495
|
-
}
|
|
2496
|
-
/**
|
|
2497
|
-
* Returns true if a header exists.
|
|
2498
|
-
*/
|
|
2499
|
-
has(name) {
|
|
2500
|
-
return name.toLowerCase() in this.parameters || name in this.parameters;
|
|
2501
|
-
}
|
|
2502
|
-
};
|
|
2503
|
-
//#endregion
|
|
2504
|
-
//#region src/Utilities/IpUtils.ts
|
|
2505
|
-
/**
|
|
2506
|
-
* Http utility functions for IP handling.
|
|
2507
|
-
*/
|
|
2508
|
-
var IpUtils = class {
|
|
2509
|
-
static PRIVATE_SUBNETS = [
|
|
2510
|
-
"127.0.0.0/8",
|
|
2511
|
-
"10.0.0.0/8",
|
|
2512
|
-
"192.168.0.0/16",
|
|
2513
|
-
"172.16.0.0/12",
|
|
2514
|
-
"169.254.0.0/16",
|
|
2515
|
-
"0.0.0.0/8",
|
|
2516
|
-
"240.0.0.0/4",
|
|
2517
|
-
"::1/128",
|
|
2518
|
-
"fc00::/7",
|
|
2519
|
-
"fe80::/10",
|
|
2520
|
-
"::ffff:0:0/96",
|
|
2521
|
-
"::/128"
|
|
2522
|
-
];
|
|
2523
|
-
static checkedIps = {};
|
|
2524
|
-
constructor() {}
|
|
2525
|
-
/**
|
|
2526
|
-
* Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets.
|
|
2527
|
-
*
|
|
2528
|
-
* @param requestIp
|
|
2529
|
-
* @param ips List of IPs or subnets (can be a string if only a single one)
|
|
2530
|
-
*/
|
|
2531
|
-
static checkIp(requestIp, ips) {
|
|
2532
|
-
if (ips && !Array.isArray(ips)) ips = [ips];
|
|
2533
|
-
const method = requestIp?.includes(":") ? this.checkIp6 : this.checkIp4;
|
|
2534
|
-
for (const ip of ips ?? []) if (method.call(this, requestIp ?? "", ip)) return true;
|
|
2535
|
-
return false;
|
|
2536
|
-
}
|
|
2537
|
-
/**
|
|
2538
|
-
* Compares two IPv4 addresses or checks if one belongs to a CIDR subnet.
|
|
2539
|
-
*
|
|
2540
|
-
* @param requestIp
|
|
2541
|
-
* @param ip IPv4 address or subnet in CIDR notation
|
|
2542
|
-
*
|
|
2543
|
-
* @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet
|
|
2544
|
-
*/
|
|
2545
|
-
static checkIp4(requestIp, ip) {
|
|
2546
|
-
const cacheKey = `${requestIp}-${ip}-v4`;
|
|
2547
|
-
const cached = this.getCacheResult(cacheKey);
|
|
2548
|
-
if (cached !== null) return cached;
|
|
2549
|
-
if (!this.isIPv4(requestIp)) return this.setCacheResult(cacheKey, false);
|
|
2550
|
-
let address;
|
|
2551
|
-
let netmask;
|
|
2552
|
-
if (ip.includes("/")) {
|
|
2553
|
-
const parts = ip.split("/", 2);
|
|
2554
|
-
address = parts[0];
|
|
2555
|
-
netmask = parseInt(parts[1], 10);
|
|
2556
|
-
if (netmask === 0) return this.setCacheResult(cacheKey, this.isIPv4(address));
|
|
2557
|
-
if (netmask < 0 || netmask > 32) return this.setCacheResult(cacheKey, false);
|
|
2558
|
-
} else {
|
|
2559
|
-
address = ip;
|
|
2560
|
-
netmask = 32;
|
|
2561
|
-
}
|
|
2562
|
-
const addrLong = this.ipv4ToLong(address);
|
|
2563
|
-
const reqLong = this.ipv4ToLong(requestIp);
|
|
2564
|
-
if (addrLong === null || reqLong === null) return this.setCacheResult(cacheKey, false);
|
|
2565
|
-
const mask = netmask === 0 ? 0 : -1 << 32 - netmask >>> 0;
|
|
2566
|
-
const result = (addrLong & mask) === (reqLong & mask);
|
|
2567
|
-
return this.setCacheResult(cacheKey, result);
|
|
2568
|
-
}
|
|
2569
|
-
/**
|
|
2570
|
-
* Compares two IPv6 addresses or checks if one belongs to a CIDR subnet.
|
|
2571
|
-
*
|
|
2572
|
-
* @see https://github.com/dsp/v6tools
|
|
2573
|
-
*
|
|
2574
|
-
* @param requestIp
|
|
2575
|
-
* @param ip IPv6 address or subnet in CIDR notation
|
|
2576
|
-
*
|
|
2577
|
-
* @throws {RuntimeException} When IPV6 support is not enabled
|
|
2578
|
-
*/
|
|
2579
|
-
static checkIp6(requestIp, ip) {
|
|
2580
|
-
const cacheKey = `${requestIp}-${ip}-v6`;
|
|
2581
|
-
const cached = this.getCacheResult(cacheKey);
|
|
2582
|
-
if (cached !== null) return cached;
|
|
2583
|
-
if (!this.isIPv6(requestIp)) return this.setCacheResult(cacheKey, false);
|
|
2584
|
-
let address;
|
|
2585
|
-
let netmask;
|
|
2586
|
-
if (ip.includes("/")) {
|
|
2587
|
-
const parts = ip.split("/", 2);
|
|
2588
|
-
address = parts[0];
|
|
2589
|
-
netmask = parseInt(parts[1], 10);
|
|
2590
|
-
if (!this.isIPv6(address)) return this.setCacheResult(cacheKey, false);
|
|
2591
|
-
if (netmask < 1 || netmask > 128) return this.setCacheResult(cacheKey, false);
|
|
2592
|
-
} else {
|
|
2593
|
-
address = ip;
|
|
2594
|
-
netmask = 128;
|
|
2595
|
-
}
|
|
2596
|
-
const addrBytes = this.inetPton6(address);
|
|
2597
|
-
const reqBytes = this.inetPton6(requestIp);
|
|
2598
|
-
if (!addrBytes || !reqBytes) return this.setCacheResult(cacheKey, false);
|
|
2599
|
-
const bytesToCheck = Math.ceil(netmask / 8);
|
|
2600
|
-
for (let i = 0; i < bytesToCheck; i++) {
|
|
2601
|
-
const left = netmask - i * 8;
|
|
2602
|
-
const mask = 255 << 8 - (left >= 8 ? 8 : left);
|
|
2603
|
-
if ((addrBytes[i] & mask) !== (reqBytes[i] & mask)) return this.setCacheResult(cacheKey, false);
|
|
2604
|
-
}
|
|
2605
|
-
return this.setCacheResult(cacheKey, true);
|
|
2606
|
-
}
|
|
2607
|
-
/**
|
|
2608
|
-
* Anonymizes an IPv4/IPv6 by zeroing out trailing bytes.
|
|
2609
|
-
*
|
|
2610
|
-
* @param ip
|
|
2611
|
-
* @param v4Bytes
|
|
2612
|
-
* @param v6Bytes
|
|
2613
|
-
*/
|
|
2614
|
-
static anonymize(ip, v4Bytes = 1, v6Bytes = 8) {
|
|
2615
|
-
if (v4Bytes < 0 || v6Bytes < 0) throw new _h3ravel_support.RuntimeException("Cannot anonymize less than 0 bytes.");
|
|
2616
|
-
if (v4Bytes > 4 || v6Bytes > 16) throw new _h3ravel_support.RuntimeException("Cannot anonymize more than 4 bytes for IPv4 and 16 bytes for IPv6.");
|
|
2617
|
-
if (ip.includes("%")) ip = ip.split("%")[0];
|
|
2618
|
-
let wrappedIPv6 = false;
|
|
2619
|
-
if (ip.startsWith("[") && ip.endsWith("]")) {
|
|
2620
|
-
wrappedIPv6 = true;
|
|
2621
|
-
ip = ip.slice(1, -1);
|
|
2622
|
-
}
|
|
2623
|
-
const packed = this.inetPton(ip);
|
|
2624
|
-
if (!packed) throw new _h3ravel_support.RuntimeException("Invalid IP address");
|
|
2625
|
-
let mask;
|
|
2626
|
-
if (packed.length === 4) {
|
|
2627
|
-
mask = new Uint8Array(4);
|
|
2628
|
-
mask.fill(255, 0, 4 - v4Bytes);
|
|
2629
|
-
mask.fill(0, 4 - v4Bytes);
|
|
2630
|
-
} else {
|
|
2631
|
-
mask = new Uint8Array(16);
|
|
2632
|
-
mask.fill(255, 0, 16 - v6Bytes);
|
|
2633
|
-
mask.fill(0, 16 - v6Bytes);
|
|
2634
|
-
}
|
|
2635
|
-
const anon = new Uint8Array(packed.length);
|
|
2636
|
-
for (let i = 0; i < packed.length; i++) anon[i] = packed[i] & mask[i];
|
|
2637
|
-
const result = this.inetNtop(anon);
|
|
2638
|
-
return wrappedIPv6 ? `[${result}]` : result;
|
|
2639
|
-
}
|
|
2640
|
-
/**
|
|
2641
|
-
* Checks if IP is within private subnets.
|
|
2642
|
-
*/
|
|
2643
|
-
static isPrivateIp(requestIp) {
|
|
2644
|
-
return this.checkIp(requestIp, this.PRIVATE_SUBNETS);
|
|
2645
|
-
}
|
|
2646
|
-
static isIPv4(ip) {
|
|
2647
|
-
return /^(\d{1,3}\.){3}\d{1,3}$/.test(ip);
|
|
2648
|
-
}
|
|
2649
|
-
static isIPv6(ip) {
|
|
2650
|
-
return ip.includes(":");
|
|
2651
|
-
}
|
|
2652
|
-
static ipv4ToLong(ip) {
|
|
2653
|
-
const parts = ip.split(".").map((n) => parseInt(n, 10));
|
|
2654
|
-
if (parts.length !== 4 || parts.some((n) => n < 0 || n > 255)) return null;
|
|
2655
|
-
return (parts[0] << 24 >>> 0) + (parts[1] << 16 >>> 0) + (parts[2] << 8 >>> 0) + parts[3] >>> 0;
|
|
2656
|
-
}
|
|
2657
|
-
static inetPton(ip) {
|
|
2658
|
-
try {
|
|
2659
|
-
return this.isIPv4(ip) ? this.inetPton4(ip) : this.inetPton6(ip);
|
|
2660
|
-
} catch {
|
|
2661
|
-
return null;
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
static inetPton4(ip) {
|
|
2665
|
-
return new Uint8Array(ip.split(".").map((n) => parseInt(n, 10)));
|
|
2666
|
-
}
|
|
2667
|
-
static inetPton6(ip) {
|
|
2668
|
-
const buf = new Uint8Array(16);
|
|
2669
|
-
try {
|
|
2670
|
-
const parts = ip.split("::");
|
|
2671
|
-
const left = parts[0] ? parts[0].split(":") : [];
|
|
2672
|
-
const right = parts[1] ? parts[1].split(":") : [];
|
|
2673
|
-
const fill = 8 - (left.length + right.length);
|
|
2674
|
-
const full = [
|
|
2675
|
-
...left,
|
|
2676
|
-
...Array(fill).fill("0"),
|
|
2677
|
-
...right
|
|
2678
|
-
].map((p) => parseInt(p || "0", 16));
|
|
2679
|
-
for (let i = 0; i < 8; i++) {
|
|
2680
|
-
buf[i * 2] = full[i] >> 8 & 255;
|
|
2681
|
-
buf[i * 2 + 1] = full[i] & 255;
|
|
2682
|
-
}
|
|
2683
|
-
return buf;
|
|
2684
|
-
} catch {
|
|
2685
|
-
return null;
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
static inetNtop(buf) {
|
|
2689
|
-
if (buf.length === 4) return Array.from(buf).join(".");
|
|
2690
|
-
const words = [];
|
|
2691
|
-
for (let i = 0; i < 16; i += 2) words.push((buf[i] << 8 | buf[i + 1]).toString(16));
|
|
2692
|
-
return words.join(":").replace(/(^|:)0(:0)*:0(:|$)/, "::");
|
|
2693
|
-
}
|
|
2694
|
-
static getCacheResult(key) {
|
|
2695
|
-
if (key in this.checkedIps) {
|
|
2696
|
-
const val = this.checkedIps[key];
|
|
2697
|
-
delete this.checkedIps[key];
|
|
2698
|
-
this.checkedIps[key] = val;
|
|
2699
|
-
return val;
|
|
2700
|
-
}
|
|
2701
|
-
return null;
|
|
2702
|
-
}
|
|
2703
|
-
static setCacheResult(key, result) {
|
|
2704
|
-
if (Object.keys(this.checkedIps).length > 1e3) {
|
|
2705
|
-
const entries = Object.entries(this.checkedIps).slice(500);
|
|
2706
|
-
this.checkedIps = Object.fromEntries(entries);
|
|
2707
|
-
}
|
|
2708
|
-
this.checkedIps[key] = result;
|
|
2709
|
-
return result;
|
|
2710
|
-
}
|
|
2711
|
-
};
|
|
2712
|
-
//#endregion
|
|
2713
|
-
//#region src/Utilities/HttpRequest.ts
|
|
2714
|
-
var HttpRequest = class HttpRequest {
|
|
2715
|
-
event;
|
|
2716
|
-
app;
|
|
2717
|
-
static HEADER_FORWARDED = 1;
|
|
2718
|
-
static HEADER_X_FORWARDED_FOR = 2;
|
|
2719
|
-
static HEADER_X_FORWARDED_HOST = 4;
|
|
2720
|
-
static HEADER_X_FORWARDED_PROTO = 8;
|
|
2721
|
-
static HEADER_X_FORWARDED_PORT = 16;
|
|
2722
|
-
static HEADER_X_FORWARDED_PREFIX = 32;
|
|
2723
|
-
static HEADER_X_FORWARDED_AWS_ELB = 26;
|
|
2724
|
-
static HEADER_X_FORWARDED_TRAEFIK = 62;
|
|
2725
|
-
static METHOD_HEAD = "HEAD";
|
|
2726
|
-
static METHOD_GET = "GET";
|
|
2727
|
-
static METHOD_POST = "POST";
|
|
2728
|
-
static METHOD_PUT = "PUT";
|
|
2729
|
-
static METHOD_PATCH = "PATCH";
|
|
2730
|
-
static METHOD_DELETE = "DELETE";
|
|
2731
|
-
static METHOD_PURGE = "PURGE";
|
|
2732
|
-
static METHOD_OPTIONS = "OPTIONS";
|
|
2733
|
-
static METHOD_TRACE = "TRACE";
|
|
2734
|
-
static METHOD_CONNECT = "CONNECT";
|
|
2735
|
-
/**
|
|
2736
|
-
* Names for headers that can be trusted when
|
|
2737
|
-
* using trusted proxies.
|
|
2738
|
-
*
|
|
2739
|
-
* The FORWARDED header is the standard as of rfc7239.
|
|
2740
|
-
*
|
|
2741
|
-
* The other headers are non-standard, but widely used
|
|
2742
|
-
* by popular reverse proxies (like Apache mod_proxy or Amazon EC2).
|
|
2743
|
-
*/
|
|
2744
|
-
TRUSTED_HEADERS = {
|
|
2745
|
-
[HttpRequest.HEADER_FORWARDED]: "FORWARDED",
|
|
2746
|
-
[HttpRequest.HEADER_X_FORWARDED_FOR]: "X_FORWARDED_FOR",
|
|
2747
|
-
[HttpRequest.HEADER_X_FORWARDED_HOST]: "X_FORWARDED_HOST",
|
|
2748
|
-
[HttpRequest.HEADER_X_FORWARDED_PROTO]: "X_FORWARDED_PROTO",
|
|
2749
|
-
[HttpRequest.HEADER_X_FORWARDED_PORT]: "X_FORWARDED_PORT",
|
|
2750
|
-
[HttpRequest.HEADER_X_FORWARDED_PREFIX]: "X_FORWARDED_PREFIX"
|
|
2751
|
-
};
|
|
2752
|
-
FORWARDED_PARAMS = {
|
|
2753
|
-
[HttpRequest.HEADER_X_FORWARDED_FOR]: "for",
|
|
2754
|
-
[HttpRequest.HEADER_X_FORWARDED_HOST]: "host",
|
|
2755
|
-
[HttpRequest.HEADER_X_FORWARDED_PROTO]: "proto",
|
|
2756
|
-
[HttpRequest.HEADER_X_FORWARDED_PORT]: "host"
|
|
2757
|
-
};
|
|
2758
|
-
#uri;
|
|
2759
|
-
/**
|
|
2760
|
-
* Parsed request body
|
|
2761
|
-
*/
|
|
2762
|
-
body;
|
|
2763
|
-
#method = void 0;
|
|
2764
|
-
#isHostValid = true;
|
|
2765
|
-
#isIisRewrite = false;
|
|
2766
|
-
format;
|
|
2767
|
-
basePath;
|
|
2768
|
-
baseUrl;
|
|
2769
|
-
requestUri;
|
|
2770
|
-
pathInfo;
|
|
2771
|
-
formData;
|
|
2772
|
-
preferredFormat;
|
|
2773
|
-
isForwardedValid = true;
|
|
2774
|
-
static trustedHosts = [];
|
|
2775
|
-
static trustedHeaderSet = -1;
|
|
2776
|
-
static trustedHostPatterns = [];
|
|
2777
|
-
/**
|
|
2778
|
-
* Gets route parameters.
|
|
2779
|
-
* @returns An object containing route parameters.
|
|
2780
|
-
*/
|
|
2781
|
-
params;
|
|
2782
|
-
/**
|
|
2783
|
-
* Request body parameters (POST).
|
|
2784
|
-
*
|
|
2785
|
-
* @see getPayload() for portability between content types
|
|
2786
|
-
*/
|
|
2787
|
-
request;
|
|
2788
|
-
/**
|
|
2789
|
-
* Uploaded files (FILES).
|
|
2790
|
-
*/
|
|
2791
|
-
files;
|
|
2792
|
-
/**
|
|
2793
|
-
* Query string parameters (GET).
|
|
2794
|
-
*/
|
|
2795
|
-
_query;
|
|
2796
|
-
/**
|
|
2797
|
-
* Server and execution environment parameters
|
|
2798
|
-
*/
|
|
2799
|
-
_server;
|
|
2800
|
-
/**
|
|
2801
|
-
* Cookies
|
|
2802
|
-
*/
|
|
2803
|
-
cookies;
|
|
2804
|
-
/**
|
|
2805
|
-
* The current Http Context
|
|
2806
|
-
*/
|
|
2807
|
-
context;
|
|
2808
|
-
/**
|
|
2809
|
-
* The request attributes (parameters parsed from the PATH_INFO, ...).
|
|
2810
|
-
*/
|
|
2811
|
-
attributes;
|
|
2812
|
-
/**
|
|
2813
|
-
* Gets the request headers.
|
|
2814
|
-
* @returns An object containing request headers.
|
|
2815
|
-
*/
|
|
2816
|
-
headers;
|
|
2817
|
-
content = void 0;
|
|
2818
|
-
static formats = void 0;
|
|
2819
|
-
static trustedProxies = [];
|
|
2820
|
-
static httpMethodParameterOverride = false;
|
|
2821
|
-
sessionManager;
|
|
2822
|
-
sessionManagerClass;
|
|
2823
|
-
/**
|
|
2824
|
-
* List of Acceptable Content Types
|
|
2825
|
-
*/
|
|
2826
|
-
acceptableContentTypes = [];
|
|
2827
|
-
trustedValuesCache = {};
|
|
2828
|
-
constructor(event, app) {
|
|
2829
|
-
this.event = event;
|
|
2830
|
-
this.app = app;
|
|
2831
|
-
}
|
|
2832
|
-
/**
|
|
2833
|
-
* Sets the parameters for this request.
|
|
2834
|
-
*
|
|
2835
|
-
* This method also re-initializes all properties.
|
|
2836
|
-
*
|
|
2837
|
-
* @param attributes
|
|
2838
|
-
* @param cookies The COOKIE parameters
|
|
2839
|
-
* @param files The FILES parameters
|
|
2840
|
-
* @param server The SERVER parameters
|
|
2841
|
-
* @param content The raw body data
|
|
2842
|
-
*/
|
|
2843
|
-
initialize() {
|
|
2844
|
-
this.buildRequirements();
|
|
2845
|
-
}
|
|
2846
|
-
buildRequirements() {
|
|
2847
|
-
this.params = (0, h3.getRouterParams)(this.event);
|
|
2848
|
-
this.request = new InputBag(this.formData ? this.formData.input() : {}, this.event);
|
|
2849
|
-
this._query = new InputBag((0, h3.getQuery)(this.event), this.event);
|
|
2850
|
-
this.attributes = new ParamBag((0, h3.getRouterParams)(this.event), this.event);
|
|
2851
|
-
this.cookies = new InputBag((0, h3.parseCookies)(this.event), this.event);
|
|
2852
|
-
this.files = new FileBag(this.formData ? this.formData.files() : {}, this.event);
|
|
2853
|
-
this._server = new ServerBag(Object.fromEntries(this.event.req.headers.entries()), this.event);
|
|
2854
|
-
this.headers = new HeaderBag(this._server.getHeaders());
|
|
2855
|
-
this.acceptableContentTypes = [];
|
|
2856
|
-
this.pathInfo = void 0;
|
|
2857
|
-
this.requestUri = void 0;
|
|
2858
|
-
this.baseUrl = void 0;
|
|
2859
|
-
this.basePath = void 0;
|
|
2860
|
-
this.#method = void 0;
|
|
2861
|
-
this.format = void 0;
|
|
2862
|
-
}
|
|
2863
|
-
/**
|
|
2864
|
-
* Gets a list of content types acceptable by the client browser in preferable order.
|
|
2865
|
-
* @returns {string[]}
|
|
2866
|
-
*/
|
|
2867
|
-
getAcceptableContentTypes() {
|
|
2868
|
-
if (this.acceptableContentTypes.length > 0) return this.acceptableContentTypes;
|
|
2869
|
-
const accept = this.getHeader("accept");
|
|
2870
|
-
if (!accept) return [];
|
|
2871
|
-
const types = accept.split(",").map((type) => type.trim()).map((type) => type.split(";")[0]).filter(Boolean);
|
|
2872
|
-
return this.acceptableContentTypes = types;
|
|
2873
|
-
}
|
|
2874
|
-
/**
|
|
2875
|
-
* Get a URI instance for the request.
|
|
2876
|
-
*/
|
|
2877
|
-
getUriInstance() {
|
|
2878
|
-
return this.#uri;
|
|
2879
|
-
}
|
|
2880
|
-
/**
|
|
2881
|
-
* Returns the requested URI (path and query string).
|
|
2882
|
-
*
|
|
2883
|
-
* @return {string} The raw URI (i.e. not URI decoded)
|
|
2884
|
-
*/
|
|
2885
|
-
getRequestUri() {
|
|
2886
|
-
return this.requestUri ??= this.prepareRequestUri();
|
|
2887
|
-
}
|
|
2888
|
-
/**
|
|
2889
|
-
* Gets the scheme and HTTP host.
|
|
2890
|
-
*
|
|
2891
|
-
* If the URL was called with basic authentication, the user
|
|
2892
|
-
* and the password are not added to the generated string.
|
|
2893
|
-
*/
|
|
2894
|
-
getSchemeAndHttpHost() {
|
|
2895
|
-
return this.getScheme() + "://" + this.getHttpHost();
|
|
2896
|
-
}
|
|
2897
|
-
/**
|
|
2898
|
-
* Returns the HTTP host being requested.
|
|
2899
|
-
*
|
|
2900
|
-
* The port name will be appended to the host if it's non-standard.
|
|
2901
|
-
*/
|
|
2902
|
-
getHttpHost() {
|
|
2903
|
-
const scheme = this.getScheme();
|
|
2904
|
-
const port = this.getPort();
|
|
2905
|
-
if ("http" === scheme && 80 == port || "https" === scheme && 443 == port) return this.getHost();
|
|
2906
|
-
return this.getHost() + ":" + port;
|
|
2907
|
-
}
|
|
2908
|
-
/**
|
|
2909
|
-
* Returns the root path from which this request is executed.
|
|
2910
|
-
*
|
|
2911
|
-
* @returns {string} The raw path (i.e. not urldecoded)
|
|
2912
|
-
*/
|
|
2913
|
-
getBasePath() {
|
|
2914
|
-
return this.basePath ??= this.prepareBasePath();
|
|
2915
|
-
}
|
|
2916
|
-
/**
|
|
2917
|
-
* Returns the root URL from which this request is executed.
|
|
2918
|
-
*
|
|
2919
|
-
* The base URL never ends with a /.
|
|
2920
|
-
*
|
|
2921
|
-
* This is similar to getBasePath(), except that it also includes the
|
|
2922
|
-
* script filename (e.g. index.php) if one exists.
|
|
2923
|
-
*
|
|
2924
|
-
* @return string The raw URL (i.e. not urldecoded)
|
|
2925
|
-
*/
|
|
2926
|
-
getBaseUrl() {
|
|
2927
|
-
let trustedPrefix = "";
|
|
2928
|
-
let trustedPrefixValues;
|
|
2929
|
-
if (this.isFromTrustedProxy() && (trustedPrefixValues = this.getTrustedValues(HttpRequest.HEADER_X_FORWARDED_PREFIX))) trustedPrefix = _h3ravel_support.Str.rtrim(trustedPrefixValues[0], "/");
|
|
2930
|
-
return trustedPrefix + this.getBaseUrlReal();
|
|
2931
|
-
}
|
|
2932
|
-
/**
|
|
2933
|
-
* Returns the real base URL received by the webserver from which this request is executed.
|
|
2934
|
-
* The URL does not include trusted reverse proxy prefix.
|
|
2935
|
-
*
|
|
2936
|
-
* @return string The raw URL (i.e. not urldecoded)
|
|
2937
|
-
*/
|
|
2938
|
-
getBaseUrlReal() {
|
|
2939
|
-
return this.baseUrl ??= this.prepareBaseUrl();
|
|
2940
|
-
}
|
|
2941
|
-
/**
|
|
2942
|
-
* Gets the request's scheme.
|
|
2943
|
-
*/
|
|
2944
|
-
getScheme() {
|
|
2945
|
-
return this.isSecure() ? "https" : "http";
|
|
2946
|
-
}
|
|
2947
|
-
/**
|
|
2948
|
-
* Prepares the base URL.
|
|
2949
|
-
*/
|
|
2950
|
-
prepareBaseUrl() {
|
|
2951
|
-
const requestUri = this.getRequestUri() ?? "";
|
|
2952
|
-
const baseUrl = "/" + node_path.default.basename(__filename);
|
|
2953
|
-
const normalizedRequestUri = requestUri.startsWith("/") ? requestUri : "/" + requestUri;
|
|
2954
|
-
if (normalizedRequestUri.startsWith(baseUrl)) return baseUrl;
|
|
2955
|
-
const dirBase = node_path.default.dirname(baseUrl);
|
|
2956
|
-
if (normalizedRequestUri.startsWith(dirBase)) return dirBase.replace(/[/\\]+$/, "");
|
|
2957
|
-
return "";
|
|
2958
|
-
}
|
|
2959
|
-
/**
|
|
2960
|
-
* Prepares the Request URI.
|
|
2961
|
-
*/
|
|
2962
|
-
prepareRequestUri() {
|
|
2963
|
-
let requestUri = "";
|
|
2964
|
-
const unencodedUrl = this._server.get("x-original-url") ?? "";
|
|
2965
|
-
if (this.isIisRewrite() && unencodedUrl) {
|
|
2966
|
-
requestUri = unencodedUrl;
|
|
2967
|
-
this._server.remove("x-original-url");
|
|
2968
|
-
} else if (this._server.has("REQUEST_URI")) {
|
|
2969
|
-
requestUri = this._server.get("REQUEST_URI") ?? "";
|
|
2970
|
-
if (requestUri && requestUri[0] === "/") {
|
|
2971
|
-
const hashPos = requestUri.indexOf("#");
|
|
2972
|
-
if (hashPos !== -1) requestUri = requestUri.substring(0, hashPos);
|
|
2973
|
-
} else try {
|
|
2974
|
-
const urlObj = new URL(requestUri);
|
|
2975
|
-
requestUri = urlObj.pathname;
|
|
2976
|
-
if (urlObj.search) requestUri += urlObj.search;
|
|
2977
|
-
} catch {}
|
|
2978
|
-
} else requestUri = this.getRequestUri() ?? "/";
|
|
2979
|
-
this._server.set("REQUEST_URI", requestUri);
|
|
2980
|
-
return requestUri;
|
|
2981
|
-
}
|
|
2982
|
-
/**
|
|
2983
|
-
* Prepares the base path.
|
|
2984
|
-
*/
|
|
2985
|
-
prepareBasePath() {
|
|
2986
|
-
const baseUrl = this.getBaseUrl();
|
|
2987
|
-
if (!baseUrl) return "";
|
|
2988
|
-
const scriptFilename = this._server.get("SCRIPT_FILENAME") ?? "";
|
|
2989
|
-
const filename = node_path.default.basename(scriptFilename);
|
|
2990
|
-
let basePath;
|
|
2991
|
-
if (node_path.default.basename(baseUrl) === filename) basePath = node_path.default.dirname(baseUrl);
|
|
2992
|
-
else basePath = baseUrl;
|
|
2993
|
-
basePath = basePath.replace(/\\/g, "/");
|
|
2994
|
-
return basePath.replace(/\/+$/, "");
|
|
2995
|
-
}
|
|
2996
|
-
/**
|
|
2997
|
-
* Prepares the path info.
|
|
2998
|
-
*/
|
|
2999
|
-
preparePathInfo() {
|
|
3000
|
-
let requestUri = this.getRequestUri();
|
|
3001
|
-
if (!requestUri) return "/";
|
|
3002
|
-
const qPos = requestUri.indexOf("?");
|
|
3003
|
-
if (qPos !== -1) requestUri = requestUri.substring(0, qPos);
|
|
3004
|
-
if (requestUri && requestUri[0] !== "/") requestUri = "/" + requestUri;
|
|
3005
|
-
const baseUrl = this.getBaseUrlReal();
|
|
3006
|
-
if (baseUrl == null) return requestUri;
|
|
3007
|
-
let pathInfo = requestUri.substring(baseUrl.length);
|
|
3008
|
-
if (!pathInfo || pathInfo[0] !== "/") pathInfo = "/" + pathInfo;
|
|
3009
|
-
return pathInfo;
|
|
3010
|
-
}
|
|
3011
|
-
/**
|
|
3012
|
-
* Returns the port on which the request is made.
|
|
3013
|
-
*
|
|
3014
|
-
* This method can read the client port from the "X-Forwarded-Port" header
|
|
3015
|
-
* when trusted proxies were set via "setTrustedProxies()".
|
|
3016
|
-
*
|
|
3017
|
-
* The "X-Forwarded-Port" header must contain the client port.
|
|
3018
|
-
*
|
|
3019
|
-
* @return int|string|null Can be a string if fetched from the server bag
|
|
3020
|
-
*/
|
|
3021
|
-
getPort() {
|
|
3022
|
-
let pos;
|
|
3023
|
-
let host;
|
|
3024
|
-
if (this.isFromTrustedProxy() && (host = this.getTrustedValues(HttpRequest.HEADER_X_FORWARDED_PORT))) host = host[0];
|
|
3025
|
-
else if (this.isFromTrustedProxy() && (host = this.getTrustedValues(HttpRequest.HEADER_X_FORWARDED_HOST))) host = host[0];
|
|
3026
|
-
else if (!(host = this.headers.get("HOST"))) return this._server.get("SERVER_PORT");
|
|
3027
|
-
if (host[0] === "[") pos = host.lastIndexOf(":", host.lastIndexOf("]"));
|
|
3028
|
-
else pos = host.lastIndexOf(":");
|
|
3029
|
-
if (pos !== -1) {
|
|
3030
|
-
const portStr = typeof host === "string" ? host.substring(pos + 1) : host.at(0)?.substring(pos + 1);
|
|
3031
|
-
if (portStr) return parseInt(portStr, 10);
|
|
3032
|
-
}
|
|
3033
|
-
return "https" === this.getScheme() ? 443 : 80;
|
|
3034
|
-
}
|
|
3035
|
-
getHost() {
|
|
3036
|
-
let host;
|
|
3037
|
-
if (this.isFromTrustedProxy() && (host = this.getTrustedValues(HttpRequest.HEADER_X_FORWARDED_HOST)?.[0])) {} else if (!(host = this.headers.get("HOST"))) host = this._server.get("SERVER_NAME") ?? this._server.get("SERVER_ADDR") ?? process.env.SERVER_NAME ?? "";
|
|
3038
|
-
host = (host ?? "").trim().replace(/:\d+$/, "").toLowerCase();
|
|
3039
|
-
if (host && !HttpRequest.isHostValid(host)) {
|
|
3040
|
-
if (!this.#isHostValid) return "";
|
|
3041
|
-
this.#isHostValid = false;
|
|
3042
|
-
throw new SuspiciousOperationException(`Invalid Host "${host}".`);
|
|
3043
|
-
}
|
|
3044
|
-
const ctor = this.constructor;
|
|
3045
|
-
if (ctor.trustedHostPatterns.length > 0) {
|
|
3046
|
-
if (ctor.trustedHosts.includes(host)) return host;
|
|
3047
|
-
for (const pattern of ctor.trustedHostPatterns) if (pattern.test(host)) {
|
|
3048
|
-
ctor.trustedHosts.push(host);
|
|
3049
|
-
return host;
|
|
3050
|
-
}
|
|
3051
|
-
if (!this.#isHostValid) return "";
|
|
3052
|
-
this.#isHostValid = false;
|
|
3053
|
-
throw new SuspiciousOperationException(`Untrusted Host "${host}".`);
|
|
3054
|
-
}
|
|
3055
|
-
return host;
|
|
3056
|
-
}
|
|
3057
|
-
/**
|
|
3058
|
-
* Checks whether the request is secure or not.
|
|
3059
|
-
*
|
|
3060
|
-
* This method can read the client protocol from the "X-Forwarded-Proto" header
|
|
3061
|
-
* when trusted proxies were set via "setTrustedProxies()".
|
|
3062
|
-
*
|
|
3063
|
-
* The "X-Forwarded-Proto" header must contain the protocol: "https" or "http".
|
|
3064
|
-
*/
|
|
3065
|
-
isSecure() {
|
|
3066
|
-
const proto = this.getTrustedValues(HttpRequest.HEADER_X_FORWARDED_PROTO);
|
|
3067
|
-
if (this.isFromTrustedProxy() && proto) return [
|
|
3068
|
-
"https",
|
|
3069
|
-
"on",
|
|
3070
|
-
"ssl",
|
|
3071
|
-
"1"
|
|
3072
|
-
].includes(proto[0]?.toLowerCase());
|
|
3073
|
-
const https = this._server.get("HTTPS");
|
|
3074
|
-
return !!https && "off" !== https.toLowerCase();
|
|
3075
|
-
}
|
|
3076
|
-
/**
|
|
3077
|
-
* Is this IIS with UrlRewriteModule?
|
|
3078
|
-
*
|
|
3079
|
-
* This method consumes, caches and removed the IIS_WasUrlRewritten env var,
|
|
3080
|
-
* so we don't inherit it to sub-requests.
|
|
3081
|
-
*/
|
|
3082
|
-
isIisRewrite() {
|
|
3083
|
-
try {
|
|
3084
|
-
if (1 === this._server.getInt("IIS_WasUrlRewritten")) {
|
|
3085
|
-
this.#isIisRewrite = true;
|
|
3086
|
-
this._server.remove("IIS_WasUrlRewritten");
|
|
3087
|
-
}
|
|
3088
|
-
} catch {}
|
|
3089
|
-
return this.#isIisRewrite;
|
|
3090
|
-
}
|
|
3091
|
-
/**
|
|
3092
|
-
* Returns the value of the requested header.
|
|
3093
|
-
*/
|
|
3094
|
-
getHeader(name) {
|
|
3095
|
-
return this.headers.get(name);
|
|
3096
|
-
}
|
|
3097
|
-
/**
|
|
3098
|
-
* Checks if the request method is of specified type.
|
|
3099
|
-
*
|
|
3100
|
-
* @param method Uppercase request method (GET, POST etc)
|
|
3101
|
-
*/
|
|
3102
|
-
isMethod(method) {
|
|
3103
|
-
return this.getMethod() === method.toUpperCase();
|
|
3104
|
-
}
|
|
3105
|
-
/**
|
|
3106
|
-
* Checks whether or not the method is safe.
|
|
3107
|
-
*
|
|
3108
|
-
* @see https://tools.ietf.org/html/rfc7231#section-4.2.1
|
|
3109
|
-
*/
|
|
3110
|
-
isMethodSafe() {
|
|
3111
|
-
return [
|
|
3112
|
-
"GET",
|
|
3113
|
-
"HEAD",
|
|
3114
|
-
"OPTIONS",
|
|
3115
|
-
"TRACE"
|
|
3116
|
-
].includes(this.getMethod());
|
|
3117
|
-
}
|
|
3118
|
-
/**
|
|
3119
|
-
* Checks whether or not the method is idempotent.
|
|
3120
|
-
*/
|
|
3121
|
-
isMethodIdempotent() {
|
|
3122
|
-
return [
|
|
3123
|
-
"HEAD",
|
|
3124
|
-
"GET",
|
|
3125
|
-
"PUT",
|
|
3126
|
-
"DELETE",
|
|
3127
|
-
"TRACE",
|
|
3128
|
-
"OPTIONS",
|
|
3129
|
-
"PURGE"
|
|
3130
|
-
].includes(this.getMethod());
|
|
3131
|
-
}
|
|
3132
|
-
/**
|
|
3133
|
-
* Checks whether the method is cacheable or not.
|
|
3134
|
-
*
|
|
3135
|
-
* @see https://tools.ietf.org/html/rfc7231#section-4.2.3
|
|
3136
|
-
*/
|
|
3137
|
-
isMethodCacheable() {
|
|
3138
|
-
return ["GET", "HEAD"].includes(this.getMethod());
|
|
3139
|
-
}
|
|
3140
|
-
/**
|
|
3141
|
-
* Returns true if the request is an XMLHttpRequest (AJAX).
|
|
3142
|
-
*/
|
|
3143
|
-
isXmlHttpRequest() {
|
|
3144
|
-
return "XMLHttpRequest" === this.getHeader("X-Requested-With");
|
|
3145
|
-
}
|
|
3146
|
-
/**
|
|
3147
|
-
* See https://url.spec.whatwg.org/.
|
|
3148
|
-
*/
|
|
3149
|
-
static isHostValid(host) {
|
|
3150
|
-
/**
|
|
3151
|
-
* Validate IPv6: [::1] or similar
|
|
3152
|
-
*/
|
|
3153
|
-
if (host[0] === "[") {
|
|
3154
|
-
if (host[host.length - 1] === "]") {
|
|
3155
|
-
const inside = host.substring(1, host.length - 1);
|
|
3156
|
-
return _h3ravel_support.Str.validateIp(inside, "ipv6");
|
|
3157
|
-
}
|
|
3158
|
-
return false;
|
|
3159
|
-
}
|
|
3160
|
-
/**
|
|
3161
|
-
* Validate IPv4: ends with .123 or .123.
|
|
3162
|
-
*/
|
|
3163
|
-
if (/\.[0-9]+\.?$/.test(host)) return _h3ravel_support.Str.validateIp(host, "ipv4");
|
|
3164
|
-
/**
|
|
3165
|
-
* fallback: remove valid chars and check if anything remains
|
|
3166
|
-
*/
|
|
3167
|
-
return "" === host.replace(/[-a-zA-Z0-9_]+\.?/g, "");
|
|
3168
|
-
}
|
|
3169
|
-
/**
|
|
3170
|
-
* Initializes HTTP request formats.
|
|
3171
|
-
*/
|
|
3172
|
-
static initializeFormats() {
|
|
3173
|
-
this.formats = {
|
|
3174
|
-
html: ["text/html", "application/xhtml+xml"],
|
|
3175
|
-
txt: ["text/plain"],
|
|
3176
|
-
js: [
|
|
3177
|
-
"application/javascript",
|
|
3178
|
-
"application/x-javascript",
|
|
3179
|
-
"text/javascript"
|
|
3180
|
-
],
|
|
3181
|
-
css: ["text/css"],
|
|
3182
|
-
json: ["application/json", "application/x-json"],
|
|
3183
|
-
jsonld: ["application/ld+json"],
|
|
3184
|
-
xml: [
|
|
3185
|
-
"text/xml",
|
|
3186
|
-
"application/xml",
|
|
3187
|
-
"application/x-xml"
|
|
3188
|
-
],
|
|
3189
|
-
rdf: ["application/rdf+xml"],
|
|
3190
|
-
atom: ["application/atom+xml"],
|
|
3191
|
-
rss: ["application/rss+xml"],
|
|
3192
|
-
form: ["application/x-www-form-urlencoded", "multipart/form-data"]
|
|
3193
|
-
};
|
|
3194
|
-
}
|
|
3195
|
-
/**
|
|
3196
|
-
* Gets the request "intended" method.
|
|
3197
|
-
*
|
|
3198
|
-
* If the X-HTTP-Method-Override header is set, and if the method is a POST,
|
|
3199
|
-
* then it is used to determine the "real" intended HTTP method.
|
|
3200
|
-
*
|
|
3201
|
-
* The _method request parameter can also be used to determine the HTTP method,
|
|
3202
|
-
* but only if enableHttpMethodParameterOverride() has been called.
|
|
3203
|
-
*
|
|
3204
|
-
* The method is always an uppercased string.
|
|
3205
|
-
*
|
|
3206
|
-
* @see getRealMethod()
|
|
3207
|
-
*/
|
|
3208
|
-
getMethod() {
|
|
3209
|
-
if (this.#method) return this.#method;
|
|
3210
|
-
this.#method = this.getRealMethod();
|
|
3211
|
-
if ("POST" !== this.#method) return this.#method;
|
|
3212
|
-
let method = this.event.req.headers.get("X-HTTP-METHOD-OVERRIDE");
|
|
3213
|
-
if (!method && HttpRequest.httpMethodParameterOverride) method = this.request.get("_method", this._query.get("_method", "POST"));
|
|
3214
|
-
if (typeof method !== "string") return this.#method;
|
|
3215
|
-
method = method.toUpperCase();
|
|
3216
|
-
if ([
|
|
3217
|
-
"GET",
|
|
3218
|
-
"HEAD",
|
|
3219
|
-
"POST",
|
|
3220
|
-
"PUT",
|
|
3221
|
-
"DELETE",
|
|
3222
|
-
"CONNECT",
|
|
3223
|
-
"OPTIONS",
|
|
3224
|
-
"PATCH",
|
|
3225
|
-
"PURGE",
|
|
3226
|
-
"TRACE"
|
|
3227
|
-
].includes(method)) {
|
|
3228
|
-
this.#method = method;
|
|
3229
|
-
return this.#method;
|
|
3230
|
-
}
|
|
3231
|
-
if (!/^[A-Z]+$/.test(method)) throw new SuspiciousOperationException("Invalid HTTP method override.");
|
|
3232
|
-
this.#method = method;
|
|
3233
|
-
return this.#method;
|
|
3234
|
-
}
|
|
3235
|
-
/**
|
|
3236
|
-
* Gets the preferred format for the response by inspecting, in the following order:
|
|
3237
|
-
* * the request format set using setRequestFormat;
|
|
3238
|
-
* * the values of the Accept HTTP header.
|
|
3239
|
-
*
|
|
3240
|
-
* Note that if you use this method, you should send the "Vary: Accept" header
|
|
3241
|
-
* in the response to prevent any issues with intermediary HTTP caches.
|
|
3242
|
-
*/
|
|
3243
|
-
getPreferredFormat(defaultValue = "html") {
|
|
3244
|
-
const preferredFormat = this.getRequestFormat();
|
|
3245
|
-
if (!this.preferredFormat && !!preferredFormat) this.preferredFormat = preferredFormat;
|
|
3246
|
-
if (this.preferredFormat ?? null) return this.preferredFormat;
|
|
3247
|
-
for (const mimeType of this.getAcceptableContentTypes()) {
|
|
3248
|
-
this.preferredFormat = this.getFormat(mimeType);
|
|
3249
|
-
if (this.preferredFormat) return this.preferredFormat;
|
|
3250
|
-
}
|
|
3251
|
-
return defaultValue;
|
|
3252
|
-
}
|
|
3253
|
-
/**
|
|
3254
|
-
* Gets the format associated with the mime type.
|
|
3255
|
-
*/
|
|
3256
|
-
getFormat(mimeType) {
|
|
3257
|
-
const pos = mimeType.indexOf(";");
|
|
3258
|
-
let canonicalMimeType = null;
|
|
3259
|
-
if (mimeType && pos > -1) canonicalMimeType = mimeType.slice(0, pos).trim();
|
|
3260
|
-
if (!HttpRequest.formats) HttpRequest.initializeFormats();
|
|
3261
|
-
let exactFormat = null;
|
|
3262
|
-
let canonicalFormat = null;
|
|
3263
|
-
for (const [format, mimeTypes] of Object.entries(HttpRequest.formats ?? {})) {
|
|
3264
|
-
if (mimeTypes.includes(mimeType)) exactFormat = format;
|
|
3265
|
-
if (null !== canonicalMimeType && mimeTypes.includes(canonicalMimeType)) canonicalFormat = format;
|
|
3266
|
-
}
|
|
3267
|
-
if (exactFormat ?? canonicalFormat) return exactFormat ?? canonicalFormat;
|
|
3268
|
-
}
|
|
3269
|
-
/**
|
|
3270
|
-
* Gets the request format.
|
|
3271
|
-
*
|
|
3272
|
-
* Here is the process to determine the format:
|
|
3273
|
-
*
|
|
3274
|
-
* * format defined by the user (with setRequestFormat())
|
|
3275
|
-
* * _format request attribute
|
|
3276
|
-
* * $default
|
|
3277
|
-
*
|
|
3278
|
-
* @see getPreferredFormat
|
|
3279
|
-
*/
|
|
3280
|
-
getRequestFormat(defaultValue = "html") {
|
|
3281
|
-
this.format ??= this.attributes.get("_format");
|
|
3282
|
-
return this.format ?? defaultValue;
|
|
3283
|
-
}
|
|
3284
|
-
/**
|
|
3285
|
-
* Sets the request format.
|
|
3286
|
-
*/
|
|
3287
|
-
setRequestFormat(format) {
|
|
3288
|
-
this.format = format;
|
|
3289
|
-
}
|
|
3290
|
-
/**
|
|
3291
|
-
* Gets the "real" request method.
|
|
3292
|
-
*
|
|
3293
|
-
* @see getMethod()
|
|
3294
|
-
*/
|
|
3295
|
-
getRealMethod() {
|
|
3296
|
-
return this.event.req.method.toUpperCase();
|
|
3297
|
-
}
|
|
3298
|
-
/**
|
|
3299
|
-
* Gets the mime type associated with the format.
|
|
3300
|
-
*/
|
|
3301
|
-
getMimeType(format) {
|
|
3302
|
-
if (!HttpRequest.formats) HttpRequest.initializeFormats();
|
|
3303
|
-
return HttpRequest.formats?.[format] ? HttpRequest.formats[format][0] : void 0;
|
|
3304
|
-
}
|
|
3305
|
-
/**
|
|
3306
|
-
* Gets the mime types associated with the format.
|
|
3307
|
-
*/
|
|
3308
|
-
static getMimeTypes(format) {
|
|
3309
|
-
if (!HttpRequest.formats) HttpRequest.initializeFormats();
|
|
3310
|
-
return HttpRequest.formats?.[format] ?? [];
|
|
3311
|
-
}
|
|
3312
|
-
/**
|
|
3313
|
-
* Gets the list of trusted proxies.
|
|
3314
|
-
*/
|
|
3315
|
-
static getTrustedProxies() {
|
|
3316
|
-
return this.trustedProxies;
|
|
3317
|
-
}
|
|
3318
|
-
/**
|
|
3319
|
-
* Returns the request body content.
|
|
3320
|
-
*
|
|
3321
|
-
* @param asStream If true, returns a ReadableStream instead of the parsed string
|
|
3322
|
-
* @return {string | ReadableStream | Promise<string | ReadableStream>}
|
|
3323
|
-
*/
|
|
3324
|
-
getContent(asStream = false) {
|
|
3325
|
-
let content = this.body;
|
|
3326
|
-
if (content !== void 0 && content !== null) {
|
|
3327
|
-
if (asStream) {
|
|
3328
|
-
if (content instanceof ReadableStream) return content;
|
|
3329
|
-
const encoder = new TextEncoder();
|
|
3330
|
-
return new ReadableStream({ start(controller) {
|
|
3331
|
-
controller.enqueue(encoder.encode(String(content)));
|
|
3332
|
-
controller.close();
|
|
3333
|
-
} });
|
|
3334
|
-
}
|
|
3335
|
-
if (typeof content === "string") return content;
|
|
3336
|
-
}
|
|
3337
|
-
if (asStream) return this.content;
|
|
3338
|
-
content = this.content;
|
|
3339
|
-
this.body = content;
|
|
3340
|
-
return content;
|
|
3341
|
-
}
|
|
3342
|
-
/**
|
|
3343
|
-
* Gets a "parameter" value from any bag.
|
|
3344
|
-
*
|
|
3345
|
-
* This method is mainly useful for libraries that want to provide some flexibility. If you don't need the
|
|
3346
|
-
* flexibility in controllers, it is better to explicitly get request parameters from the appropriate
|
|
3347
|
-
* public property instead (attributes, query, request).
|
|
3348
|
-
*
|
|
3349
|
-
* Order of precedence: PATH (routing placeholders or custom attributes), GET, POST
|
|
3350
|
-
*
|
|
3351
|
-
* @internal use explicit input sources instead
|
|
3352
|
-
*/
|
|
3353
|
-
get(key, defaultValue) {
|
|
3354
|
-
const result = this.attributes.get(key, this);
|
|
3355
|
-
if (this !== result) return result;
|
|
3356
|
-
if (this._query.has(key)) return this._query.all()[key];
|
|
3357
|
-
if (this.request.has(key)) return this.request.all()[key];
|
|
3358
|
-
return defaultValue;
|
|
3359
|
-
}
|
|
3360
|
-
/**
|
|
3361
|
-
* Indicates whether this request originated from a trusted proxy.
|
|
3362
|
-
*
|
|
3363
|
-
* This can be useful to determine whether or not to trust the
|
|
3364
|
-
* contents of a proxy-specific header.
|
|
3365
|
-
*/
|
|
3366
|
-
isFromTrustedProxy() {
|
|
3367
|
-
return !HttpRequest.trustedProxies?.length && IpUtils.checkIp(this._server.get("REMOTE_ADDR"), HttpRequest.trustedProxies);
|
|
3368
|
-
}
|
|
3369
|
-
/**
|
|
3370
|
-
* This method is rather heavy because it splits and merges headers, and it's called by many other methods such as
|
|
3371
|
-
* getPort(), isSecure(), getHost(), getClientIps(), this.() etc. Thus, we try to cache the results for
|
|
3372
|
-
* best performance.
|
|
3373
|
-
*/
|
|
3374
|
-
getTrustedValues(type, ip) {
|
|
3375
|
-
const trustedHeaders = this.TRUSTED_HEADERS;
|
|
3376
|
-
const trustedHeaderSet = HttpRequest.trustedHeaderSet;
|
|
3377
|
-
const cacheKey = type + "\0" + (trustedHeaderSet & type ? this.headers.get(trustedHeaders[type]) ?? "" : "") + "\0" + (ip ?? "") + "\0" + (this.headers.get(trustedHeaders[HttpRequest.HEADER_FORWARDED]) ?? "");
|
|
3378
|
-
if (this.trustedValuesCache[cacheKey]) return this.trustedValuesCache[cacheKey];
|
|
3379
|
-
let clientValues = [];
|
|
3380
|
-
let forwardedValues = [];
|
|
3381
|
-
if (trustedHeaderSet & type && this.headers.has(trustedHeaders[type])) {
|
|
3382
|
-
const headerValue = this.headers.get(trustedHeaders[type]);
|
|
3383
|
-
for (const v of headerValue.split(",")) {
|
|
3384
|
-
const value = (type === HttpRequest.HEADER_X_FORWARDED_PORT ? "0.0.0.0:" : "") + v.trim();
|
|
3385
|
-
clientValues.push(value);
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
if (trustedHeaderSet & HttpRequest.HEADER_FORWARDED && this.FORWARDED_PARAMS[type] && this.headers.has(trustedHeaders[HttpRequest.HEADER_FORWARDED])) {
|
|
3389
|
-
const forwarded = this.headers.get(trustedHeaders[HttpRequest.HEADER_FORWARDED]);
|
|
3390
|
-
const parts = HeaderUtility.split(forwarded, ",;=");
|
|
3391
|
-
const param = this.FORWARDED_PARAMS[type];
|
|
3392
|
-
for (const subParts of parts) {
|
|
3393
|
-
let v = HeaderUtility.combine(subParts)[param];
|
|
3394
|
-
if (typeof v === "boolean") v = "0";
|
|
3395
|
-
if (v == null) continue;
|
|
3396
|
-
if (type === HttpRequest.HEADER_X_FORWARDED_PORT) {
|
|
3397
|
-
if (v.endsWith("]") || !(v = v.substring(v.lastIndexOf(":")))) v = this.isSecure() ? ":443" : ":80";
|
|
3398
|
-
v = "0.0.0.0" + v;
|
|
3399
|
-
}
|
|
3400
|
-
forwardedValues.push(v);
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
if (ip != null) {
|
|
3404
|
-
clientValues = this.normalizeAndFilterClientIps(clientValues, ip);
|
|
3405
|
-
forwardedValues = this.normalizeAndFilterClientIps(forwardedValues, ip);
|
|
3406
|
-
}
|
|
3407
|
-
if (JSON.stringify(forwardedValues) === JSON.stringify(clientValues) || clientValues.length === 0) {
|
|
3408
|
-
this.trustedValuesCache[cacheKey] = forwardedValues;
|
|
3409
|
-
return forwardedValues;
|
|
3410
|
-
}
|
|
3411
|
-
if (forwardedValues.length === 0) {
|
|
3412
|
-
this.trustedValuesCache[cacheKey] = clientValues;
|
|
3413
|
-
return clientValues;
|
|
3414
|
-
}
|
|
3415
|
-
if (!this.isForwardedValid) {
|
|
3416
|
-
const fallback = ip != null ? ["0.0.0.0", ip] : [];
|
|
3417
|
-
this.trustedValuesCache[cacheKey] = fallback;
|
|
3418
|
-
return fallback;
|
|
3419
|
-
}
|
|
3420
|
-
this.isForwardedValid = false;
|
|
3421
|
-
throw new ConflictingHeadersException(`The request has both a trusted "${trustedHeaders[HttpRequest.HEADER_FORWARDED]}" header and a trusted "${trustedHeaders[type]}" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.`);
|
|
3422
|
-
}
|
|
3423
|
-
/**
|
|
3424
|
-
*
|
|
3425
|
-
* @param clientIps
|
|
3426
|
-
* @param ip
|
|
3427
|
-
* @returns
|
|
3428
|
-
*/
|
|
3429
|
-
normalizeAndFilterClientIps(clientIps, ip) {
|
|
3430
|
-
if (!clientIps || clientIps.length === 0) return [];
|
|
3431
|
-
clientIps = [...clientIps, ip];
|
|
3432
|
-
let firstTrustedIp = null;
|
|
3433
|
-
for (let i = 0; i < clientIps.length; i++) {
|
|
3434
|
-
let clientIp = clientIps[i];
|
|
3435
|
-
if (clientIp.includes(".")) {
|
|
3436
|
-
const colonIndex = clientIp.indexOf(":");
|
|
3437
|
-
if (colonIndex > -1) {
|
|
3438
|
-
clientIp = clientIp.substring(0, colonIndex);
|
|
3439
|
-
clientIps[i] = clientIp;
|
|
3440
|
-
}
|
|
3441
|
-
} else if (clientIp.startsWith("[")) {
|
|
3442
|
-
const endBracketIndex = clientIp.indexOf("]", 1);
|
|
3443
|
-
if (endBracketIndex > -1) {
|
|
3444
|
-
clientIp = clientIp.substring(1, endBracketIndex);
|
|
3445
|
-
clientIps[i] = clientIp;
|
|
3446
|
-
}
|
|
3447
|
-
}
|
|
3448
|
-
if (_h3ravel_support.Str.validateIp(clientIp)) {
|
|
3449
|
-
clientIps.splice(i, 1);
|
|
3450
|
-
i--;
|
|
3451
|
-
continue;
|
|
3452
|
-
}
|
|
3453
|
-
if (IpUtils.checkIp(clientIp, HttpRequest.trustedProxies)) {
|
|
3454
|
-
clientIps.splice(i, 1);
|
|
3455
|
-
i--;
|
|
3456
|
-
firstTrustedIp ??= clientIp;
|
|
3457
|
-
}
|
|
3458
|
-
}
|
|
3459
|
-
return clientIps.length > 0 ? clientIps.reverse() : firstTrustedIp ? [firstTrustedIp] : [];
|
|
3460
|
-
}
|
|
3461
|
-
/**
|
|
3462
|
-
* Sets a list of trusted host patterns.
|
|
3463
|
-
*
|
|
3464
|
-
* You should only list the hosts you manage using regexes.
|
|
3465
|
-
*
|
|
3466
|
-
* @param hostPatterns
|
|
3467
|
-
*/
|
|
3468
|
-
static setTrustedHosts(hostPatterns) {
|
|
3469
|
-
this.trustedHostPatterns = hostPatterns.map((hostPattern) => new RegExp(hostPattern, "i"));
|
|
3470
|
-
/**
|
|
3471
|
-
* reset trusted hosts when patterns change
|
|
3472
|
-
*/
|
|
3473
|
-
this.trustedHosts = [];
|
|
3474
|
-
}
|
|
3475
|
-
/**
|
|
3476
|
-
* Returns the path being requested relative to the executed script.
|
|
3477
|
-
*
|
|
3478
|
-
* The path info always starts with a /.
|
|
3479
|
-
*
|
|
3480
|
-
* @return {string} The raw path (i.e. not urldecoded)
|
|
3481
|
-
*/
|
|
3482
|
-
getPathInfo() {
|
|
3483
|
-
return this.pathInfo ??= this.preparePathInfo();
|
|
3484
|
-
}
|
|
3485
|
-
/**
|
|
3486
|
-
* Gets the list of trusted host patterns.
|
|
3487
|
-
*/
|
|
3488
|
-
static getTrustedHosts() {
|
|
3489
|
-
return this.trustedHostPatterns;
|
|
3490
|
-
}
|
|
3491
|
-
/**
|
|
3492
|
-
* Enables support for the _method request parameter to determine the intended HTTP method.
|
|
3493
|
-
*
|
|
3494
|
-
* Be warned that enabling this feature might lead to CSRF issues in your code.
|
|
3495
|
-
* Check that you are using CSRF tokens when required.
|
|
3496
|
-
* If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered
|
|
3497
|
-
* and used to send a "PUT" or "DELETE" request via the _method request parameter.
|
|
3498
|
-
* If these methods are not protected against CSRF, this presents a possible vulnerability.
|
|
3499
|
-
*
|
|
3500
|
-
* The HTTP method can only be overridden when the real HTTP method is POST.
|
|
3501
|
-
*/
|
|
3502
|
-
static enableHttpMethodParameterOverride() {
|
|
3503
|
-
this.httpMethodParameterOverride = true;
|
|
3504
|
-
}
|
|
3505
|
-
/**
|
|
3506
|
-
* Checks whether support for the _method request parameter is enabled.
|
|
3507
|
-
*/
|
|
3508
|
-
static getHttpMethodParameterOverride() {
|
|
3509
|
-
return this.httpMethodParameterOverride;
|
|
3510
|
-
}
|
|
3511
|
-
};
|
|
3512
|
-
//#endregion
|
|
3513
|
-
//#region src/Request.ts
|
|
3514
|
-
var Request = class Request extends HttpRequest {
|
|
3515
|
-
/**
|
|
3516
|
-
* The decoded JSON content for the request.
|
|
3517
|
-
*/
|
|
3518
|
-
#json;
|
|
3519
|
-
/**
|
|
3520
|
-
* All of the converted files for the request.
|
|
3521
|
-
*/
|
|
3522
|
-
convertedFiles;
|
|
3523
|
-
/**
|
|
3524
|
-
* The route resolver callback.
|
|
3525
|
-
*/
|
|
3526
|
-
routeResolver;
|
|
3527
|
-
/**
|
|
3528
|
-
* The user resolver callback.
|
|
3529
|
-
*/
|
|
3530
|
-
userResolver;
|
|
3531
|
-
constructor(event, app) {
|
|
3532
|
-
if (Request.httpMethodParameterOverride) HttpRequest.enableHttpMethodParameterOverride();
|
|
3533
|
-
super(event, app);
|
|
3534
|
-
}
|
|
3535
|
-
/**
|
|
3536
|
-
* Factory method to create a Request instance from an H3Event.
|
|
3537
|
-
*/
|
|
3538
|
-
static async create(event, app) {
|
|
3539
|
-
const instance = new Request(event, app);
|
|
3540
|
-
await instance.setBody();
|
|
3541
|
-
instance.initialize();
|
|
3542
|
-
return instance;
|
|
3543
|
-
}
|
|
3544
|
-
/**
|
|
3545
|
-
* Factory method to create a syncronous Request instance from an H3Event.
|
|
3546
|
-
*/
|
|
3547
|
-
static createSync(event, app) {
|
|
3548
|
-
const instance = new Request(event, app);
|
|
3549
|
-
instance.content = event.req.body;
|
|
3550
|
-
instance.body = instance.content;
|
|
3551
|
-
instance.buildRequirements();
|
|
3552
|
-
return instance;
|
|
3553
|
-
}
|
|
3554
|
-
async setBody() {
|
|
3555
|
-
const type = this.event.req.headers.get("content-type") || "";
|
|
3556
|
-
if (this.body) return;
|
|
3557
|
-
if (type.includes("application/json")) {
|
|
3558
|
-
this.body = await this.event.req.json().catch(() => ({}));
|
|
3559
|
-
this.content = this.body;
|
|
3560
|
-
} else if (type.includes("form-data") || type.includes("x-www-form-urlencoded")) {
|
|
3561
|
-
this.formData = new FormRequest(await this.event.req.formData());
|
|
3562
|
-
this.body = this.formData.all();
|
|
3563
|
-
this.content = JSON.stringify(this.formData.input());
|
|
3564
|
-
} else if (type.startsWith("text/")) {
|
|
3565
|
-
this.body = await this.event.req.text();
|
|
3566
|
-
this.content = this.body;
|
|
3567
|
-
} else {
|
|
3568
|
-
const content = this.event.req.body;
|
|
3569
|
-
this.content = content;
|
|
3570
|
-
if (content instanceof ReadableStream) {
|
|
3571
|
-
const reader = content.getReader();
|
|
3572
|
-
const chunks = [];
|
|
3573
|
-
let done = false;
|
|
3574
|
-
while (!done) {
|
|
3575
|
-
const { value, done: isDone } = await reader.read();
|
|
3576
|
-
if (value) chunks.push(value);
|
|
3577
|
-
done = isDone;
|
|
3578
|
-
}
|
|
3579
|
-
this.body = new TextDecoder().decode(new Uint8Array(chunks.flatMap((chunk) => Array.from(chunk))));
|
|
3580
|
-
} else this.body = content;
|
|
3581
|
-
}
|
|
3582
|
-
}
|
|
3583
|
-
/**
|
|
3584
|
-
* Validate the incoming request data
|
|
3585
|
-
*
|
|
3586
|
-
* @param data
|
|
3587
|
-
* @param rules
|
|
3588
|
-
* @param messages
|
|
3589
|
-
*/
|
|
3590
|
-
async validate(rules, messages = {}) {
|
|
3591
|
-
const { Validator } = await import("@h3ravel/validation");
|
|
3592
|
-
return await new Validator(this.all(), rules, messages).validate();
|
|
3593
|
-
}
|
|
3594
|
-
/**
|
|
3595
|
-
* Retrieve all data from the instance (query + body).
|
|
3596
|
-
*/
|
|
3597
|
-
all(keys) {
|
|
3598
|
-
const input = _h3ravel_support.Obj.deepMerge({}, this.input(), this.allFiles());
|
|
3599
|
-
if (!keys) return input;
|
|
3600
|
-
const results = {};
|
|
3601
|
-
const list = Array.isArray(keys) ? keys : [keys];
|
|
3602
|
-
for (const key of list) (0, _h3ravel_support.data_set)(results, key, _h3ravel_support.Obj.get(input, key));
|
|
3603
|
-
return results;
|
|
3604
|
-
}
|
|
3605
|
-
/**
|
|
3606
|
-
* Retrieve an input item from the request.
|
|
3607
|
-
*
|
|
3608
|
-
* @param key
|
|
3609
|
-
* @param defaultValue
|
|
3610
|
-
* @returns
|
|
3611
|
-
*/
|
|
3612
|
-
input(key, defaultValue) {
|
|
3613
|
-
const source = {
|
|
3614
|
-
...this.getInputSource().all(),
|
|
3615
|
-
...this._query.all()
|
|
3616
|
-
};
|
|
3617
|
-
return key ? (0, _h3ravel_support.data_get)(source, key, defaultValue) : _h3ravel_support.Arr.except(source, ["_method"]);
|
|
3618
|
-
}
|
|
3619
|
-
file(key, defaultValue, expectArray) {
|
|
3620
|
-
const files = (0, _h3ravel_support.data_get)(this.allFiles(), key, defaultValue);
|
|
3621
|
-
if (!files) return defaultValue;
|
|
3622
|
-
if (Array.isArray(files)) return expectArray ? files : files[0];
|
|
3623
|
-
return files;
|
|
3624
|
-
}
|
|
3625
|
-
/**
|
|
3626
|
-
* Get the user making the request.
|
|
3627
|
-
*
|
|
3628
|
-
* @param guard
|
|
3629
|
-
*/
|
|
3630
|
-
user(guard) {
|
|
3631
|
-
return Reflect.apply(this.getUserResolver(), this, [guard]);
|
|
3632
|
-
}
|
|
3633
|
-
route(param, defaultParam) {
|
|
3634
|
-
const route = Reflect.apply(this.getRouteResolver(), this, []);
|
|
3635
|
-
if (typeof route === "undefined" || !param) return route;
|
|
3636
|
-
return route.parameter(param, defaultParam);
|
|
3637
|
-
}
|
|
3638
|
-
/**
|
|
3639
|
-
* Determine if the uploaded data contains a file.
|
|
3640
|
-
*
|
|
3641
|
-
* @param key
|
|
3642
|
-
* @return boolean
|
|
3643
|
-
*/
|
|
3644
|
-
hasFile(key) {
|
|
3645
|
-
let files = this.file(key, void 0, true);
|
|
3646
|
-
if (!Array.isArray(files)) files = [files];
|
|
3647
|
-
return files.some((e) => this.isValidFile(e));
|
|
3648
|
-
}
|
|
3649
|
-
/**
|
|
3650
|
-
* Check that the given file is a valid file instance.
|
|
3651
|
-
*
|
|
3652
|
-
* @param file
|
|
3653
|
-
* @return boolean
|
|
3654
|
-
*/
|
|
3655
|
-
isValidFile(file) {
|
|
3656
|
-
return file.content instanceof File && file.size > 0;
|
|
3657
|
-
}
|
|
3658
|
-
/**
|
|
3659
|
-
* Get an object with all the files on the request.
|
|
3660
|
-
*/
|
|
3661
|
-
allFiles() {
|
|
3662
|
-
if (this.convertedFiles) return this.convertedFiles;
|
|
3663
|
-
const entries = Object.entries(this.files.all()).filter((e) => e[1] != null);
|
|
3664
|
-
const files = Object.fromEntries(entries);
|
|
3665
|
-
this.convertedFiles = this.convertUploadedFiles(files);
|
|
3666
|
-
return this.convertedFiles;
|
|
3667
|
-
}
|
|
3668
|
-
/**
|
|
3669
|
-
* Extract and convert uploaded files from FormData.
|
|
3670
|
-
*/
|
|
3671
|
-
convertUploadedFiles(files) {
|
|
3672
|
-
if (!this.formData) return files;
|
|
3673
|
-
for (const [key, value] of Object.entries(this.formData.files())) {
|
|
3674
|
-
if (!(value instanceof File)) continue;
|
|
3675
|
-
if (key.endsWith("[]")) {
|
|
3676
|
-
const normalizedKey = key.slice(0, -2);
|
|
3677
|
-
if (!files[normalizedKey]) files[normalizedKey] = [];
|
|
3678
|
-
files[normalizedKey].push(UploadedFile.createFromBase(value));
|
|
3679
|
-
} else files[key] = UploadedFile.createFromBase(value);
|
|
3680
|
-
}
|
|
3681
|
-
return files;
|
|
3682
|
-
}
|
|
3683
|
-
/**
|
|
3684
|
-
* Get the current decoded path info for the request.
|
|
3685
|
-
*/
|
|
3686
|
-
decodedPath() {
|
|
3687
|
-
try {
|
|
3688
|
-
return decodeURIComponent(this.path());
|
|
3689
|
-
} catch {
|
|
3690
|
-
return this.path();
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
/**
|
|
3694
|
-
* Determine if the data contains a given key.
|
|
3695
|
-
*
|
|
3696
|
-
* @param keys
|
|
3697
|
-
* @returns
|
|
3698
|
-
*/
|
|
3699
|
-
has(keys) {
|
|
3700
|
-
return _h3ravel_support.Obj.has(this.all(), keys);
|
|
3701
|
-
}
|
|
3702
|
-
/**
|
|
3703
|
-
* Determine if the instance is missing a given key.
|
|
3704
|
-
*/
|
|
3705
|
-
missing(key) {
|
|
3706
|
-
const keys = Array.isArray(key) ? key : [key];
|
|
3707
|
-
return !this.has(keys);
|
|
3708
|
-
}
|
|
3709
|
-
/**
|
|
3710
|
-
* Get a subset containing the provided keys with values from the instance data.
|
|
3711
|
-
*
|
|
3712
|
-
* @param keys
|
|
3713
|
-
* @returns
|
|
3714
|
-
*/
|
|
3715
|
-
only(keys) {
|
|
3716
|
-
const data = Object.entries(this.all()).filter(([key]) => keys.includes(key));
|
|
3717
|
-
return Object.fromEntries(data);
|
|
3718
|
-
}
|
|
3719
|
-
/**
|
|
3720
|
-
* Determine if the request is over HTTPS.
|
|
3721
|
-
*/
|
|
3722
|
-
secure() {
|
|
3723
|
-
return this.isSecure();
|
|
3724
|
-
}
|
|
3725
|
-
/**
|
|
3726
|
-
* Get all of the data except for a specified array of items.
|
|
3727
|
-
*
|
|
3728
|
-
* @param keys
|
|
3729
|
-
* @returns
|
|
3730
|
-
*/
|
|
3731
|
-
except(keys) {
|
|
3732
|
-
const data = Object.entries(this.all()).filter(([key]) => !keys.includes(key));
|
|
3733
|
-
return Object.fromEntries(data);
|
|
3734
|
-
}
|
|
3735
|
-
/**
|
|
3736
|
-
* Merges new input data into the current request's input source.
|
|
3737
|
-
*
|
|
3738
|
-
* @param input - An object containing key-value pairs to merge.
|
|
3739
|
-
* @returns this - For fluent chaining.
|
|
3740
|
-
*/
|
|
3741
|
-
merge(input) {
|
|
3742
|
-
const source = this.getInputSource();
|
|
3743
|
-
for (const [key, value] of Object.entries(input)) source.set(key, value);
|
|
3744
|
-
return this;
|
|
3745
|
-
}
|
|
3746
|
-
/**
|
|
3747
|
-
* Merge new input into the request's input, but only when that key is missing from the request.
|
|
3748
|
-
*
|
|
3749
|
-
* @param input
|
|
3750
|
-
*/
|
|
3751
|
-
mergeIfMissing(input) {
|
|
3752
|
-
return this.merge(Object.fromEntries(Object.entries(input).filter(([key]) => this.missing(key))));
|
|
3753
|
-
}
|
|
3754
|
-
/**
|
|
3755
|
-
* Get the keys for all of the input and files.
|
|
3756
|
-
*/
|
|
3757
|
-
keys() {
|
|
3758
|
-
return [...Object.keys(this.input()), ...this.files.keys()];
|
|
3759
|
-
}
|
|
3760
|
-
/**
|
|
3761
|
-
* Get an instance of the current session manager
|
|
3762
|
-
*
|
|
3763
|
-
* @param key
|
|
3764
|
-
* @param defaultValue
|
|
3765
|
-
* @returns a global instance of the current session manager.
|
|
3766
|
-
*/
|
|
3767
|
-
session(key, defaultValue) {
|
|
3768
|
-
this.sessionManager ??= this.app.make("session");
|
|
3769
|
-
if (typeof key === "string") return this.sessionManager.get(key, defaultValue);
|
|
3770
|
-
else if (typeof key === "object") {
|
|
3771
|
-
for (const [k, val] of Object.entries(key)) this.sessionManager.put(k, val);
|
|
3772
|
-
return;
|
|
3773
|
-
}
|
|
3774
|
-
return this.sessionManager;
|
|
3775
|
-
}
|
|
3776
|
-
/**
|
|
3777
|
-
* Get the host name.
|
|
3778
|
-
*/
|
|
3779
|
-
host() {
|
|
3780
|
-
return this.getHost();
|
|
3781
|
-
}
|
|
3782
|
-
/**
|
|
3783
|
-
* Get the HTTP host being requested.
|
|
3784
|
-
*/
|
|
3785
|
-
httpHost() {
|
|
3786
|
-
return this.getHttpHost();
|
|
3787
|
-
}
|
|
3788
|
-
/**
|
|
3789
|
-
* Get the scheme and HTTP host.
|
|
3790
|
-
*/
|
|
3791
|
-
schemeAndHttpHost() {
|
|
3792
|
-
return this.getSchemeAndHttpHost();
|
|
3793
|
-
}
|
|
3794
|
-
/**
|
|
3795
|
-
* Determine if the request is sending JSON.
|
|
3796
|
-
*
|
|
3797
|
-
* @return bool
|
|
3798
|
-
*/
|
|
3799
|
-
isJson() {
|
|
3800
|
-
return _h3ravel_support.Str.contains(this.getHeader("CONTENT_TYPE") ?? "", ["/json", "+json"]);
|
|
3801
|
-
}
|
|
3802
|
-
/**
|
|
3803
|
-
* Determine if the current request probably expects a JSON response.
|
|
3804
|
-
*
|
|
3805
|
-
* @returns
|
|
3806
|
-
*/
|
|
3807
|
-
expectsJson() {
|
|
3808
|
-
return _h3ravel_support.Str.contains(this.getHeader("Accept") ?? "", "application/json");
|
|
3809
|
-
}
|
|
3810
|
-
/**
|
|
3811
|
-
* Determine if the current request is asking for JSON.
|
|
3812
|
-
*
|
|
3813
|
-
* @returns
|
|
3814
|
-
*/
|
|
3815
|
-
wantsJson() {
|
|
3816
|
-
const acceptable = this.getAcceptableContentTypes();
|
|
3817
|
-
return !!acceptable[0] && _h3ravel_support.Str.contains(acceptable[0].toLowerCase(), ["/json", "+json"]);
|
|
3818
|
-
}
|
|
3819
|
-
/**
|
|
3820
|
-
* Determine if the request is the result of a PJAX call.
|
|
3821
|
-
*
|
|
3822
|
-
* @return bool
|
|
3823
|
-
*/
|
|
3824
|
-
pjax() {
|
|
3825
|
-
return this.headers.get("X-PJAX") == true;
|
|
3826
|
-
}
|
|
3827
|
-
/**
|
|
3828
|
-
* Returns true if the request is an XMLHttpRequest (AJAX).
|
|
3829
|
-
*
|
|
3830
|
-
* @alias isXmlHttpRequest()
|
|
3831
|
-
* @returns {boolean}
|
|
3832
|
-
*/
|
|
3833
|
-
ajax() {
|
|
3834
|
-
return this.isXmlHttpRequest();
|
|
3835
|
-
}
|
|
3836
|
-
/**
|
|
3837
|
-
* Get the client IP address.
|
|
3838
|
-
*/
|
|
3839
|
-
ip() {
|
|
3840
|
-
return (0, h3.getRequestIP)(this.event);
|
|
3841
|
-
}
|
|
3842
|
-
async old(key, defaultValue) {
|
|
3843
|
-
const payload = await this.session().get("_old", {});
|
|
3844
|
-
if (key) return (0, _h3ravel_support.safeDot)(payload, key) || defaultValue;
|
|
3845
|
-
return payload;
|
|
3846
|
-
}
|
|
3847
|
-
/**
|
|
3848
|
-
* Get a URI instance for the request.
|
|
3849
|
-
*/
|
|
3850
|
-
uri() {
|
|
3851
|
-
return Reflect.apply(this.app.getUriResolver(), this, []).of(this.fullUrl(), this.app);
|
|
3852
|
-
}
|
|
3853
|
-
/**
|
|
3854
|
-
* Get the root URL for the application.
|
|
3855
|
-
*
|
|
3856
|
-
* @return string
|
|
3857
|
-
*/
|
|
3858
|
-
root() {
|
|
3859
|
-
return _h3ravel_support.Str.rtrim(this.getSchemeAndHttpHost() + this.getBaseUrl(), "/");
|
|
3860
|
-
}
|
|
3861
|
-
/**
|
|
3862
|
-
* Get the URL (no query string) for the request.
|
|
3863
|
-
*
|
|
3864
|
-
* @return string
|
|
3865
|
-
*/
|
|
3866
|
-
url() {
|
|
3867
|
-
return _h3ravel_support.Str.rtrim(this.uri().toString().replace(/\?.*/, ""), "/");
|
|
3868
|
-
}
|
|
3869
|
-
/**
|
|
3870
|
-
* Get the full URL for the request.
|
|
3871
|
-
*/
|
|
3872
|
-
fullUrl() {
|
|
3873
|
-
return this.event.req.url;
|
|
3874
|
-
}
|
|
3875
|
-
/**
|
|
3876
|
-
* Get the current path info for the request.
|
|
3877
|
-
*/
|
|
3878
|
-
path() {
|
|
3879
|
-
const pattern = (this.getPathInfo() ?? "").replace(/^\/+|\/+$/g, "");
|
|
3880
|
-
return pattern === "" ? "/" : pattern;
|
|
3881
|
-
}
|
|
3882
|
-
/**
|
|
3883
|
-
* Return the Request instance.
|
|
3884
|
-
*/
|
|
3885
|
-
instance() {
|
|
3886
|
-
return this;
|
|
3887
|
-
}
|
|
3888
|
-
/**
|
|
3889
|
-
* Get the request method.
|
|
3890
|
-
*/
|
|
3891
|
-
method() {
|
|
3892
|
-
return this.getMethod();
|
|
3893
|
-
}
|
|
3894
|
-
/**
|
|
3895
|
-
* Get the JSON payload for the request.
|
|
3896
|
-
*
|
|
3897
|
-
* @param key
|
|
3898
|
-
* @param defaultValue
|
|
3899
|
-
* @return {InputBag}
|
|
3900
|
-
*/
|
|
3901
|
-
json(key, defaultValue) {
|
|
3902
|
-
if (!this.#json) {
|
|
3903
|
-
let json = this.getContent();
|
|
3904
|
-
if (typeof json == "string") json = JSON.parse(json || "{}");
|
|
3905
|
-
this.#json = new InputBag(json, this.event);
|
|
3906
|
-
}
|
|
3907
|
-
if (!key) return this.#json;
|
|
3908
|
-
return _h3ravel_support.Obj.get(this.#json.all(), key, defaultValue);
|
|
3909
|
-
}
|
|
3910
|
-
/**
|
|
3911
|
-
* Get the user resolver callback.
|
|
3912
|
-
*/
|
|
3913
|
-
getUserResolver() {
|
|
3914
|
-
return this.userResolver ?? (() => void 0);
|
|
3915
|
-
}
|
|
3916
|
-
/**
|
|
3917
|
-
* Set the user resolver callback.
|
|
3918
|
-
*
|
|
3919
|
-
* @param callback
|
|
3920
|
-
*/
|
|
3921
|
-
setUserResolver(callback) {
|
|
3922
|
-
this.userResolver = callback;
|
|
3923
|
-
return this;
|
|
3924
|
-
}
|
|
3925
|
-
/**
|
|
3926
|
-
* Get the route resolver callback.
|
|
3927
|
-
*/
|
|
3928
|
-
getRouteResolver() {
|
|
3929
|
-
return this.routeResolver ?? (() => void 0);
|
|
3930
|
-
}
|
|
3931
|
-
/**
|
|
3932
|
-
* Set the route resolver callback.
|
|
3933
|
-
*
|
|
3934
|
-
* @param callback
|
|
3935
|
-
*/
|
|
3936
|
-
setRouteResolver(callback) {
|
|
3937
|
-
this.routeResolver = callback;
|
|
3938
|
-
return this;
|
|
3939
|
-
}
|
|
3940
|
-
/**
|
|
3941
|
-
* Get the bearer token from the request headers.
|
|
3942
|
-
*/
|
|
3943
|
-
bearerToken() {
|
|
3944
|
-
let header = this.header("Authorization", "");
|
|
3945
|
-
const position = header.toLowerCase().lastIndexOf("bearer ");
|
|
3946
|
-
if (position !== -1) {
|
|
3947
|
-
header = header.slice(position + 7);
|
|
3948
|
-
const commaIndex = header.indexOf(",");
|
|
3949
|
-
return commaIndex !== -1 ? header.slice(0, commaIndex) : header;
|
|
3950
|
-
}
|
|
3951
|
-
}
|
|
3952
|
-
/**
|
|
3953
|
-
* Retrieve data from the instance.
|
|
3954
|
-
*
|
|
3955
|
-
* @param key
|
|
3956
|
-
* @param defaultValue
|
|
3957
|
-
*/
|
|
3958
|
-
data(key, defaultValue) {
|
|
3959
|
-
return this.input(key, defaultValue);
|
|
3960
|
-
}
|
|
3961
|
-
/**
|
|
3962
|
-
* Retrieve a request payload item from the request.
|
|
3963
|
-
*
|
|
3964
|
-
* @param key
|
|
3965
|
-
* @param default
|
|
3966
|
-
*/
|
|
3967
|
-
post(key, defaultValue) {
|
|
3968
|
-
return this.retrieveItem("request", key, defaultValue);
|
|
3969
|
-
}
|
|
3970
|
-
/**
|
|
3971
|
-
* Determine if a header is set on the request.
|
|
3972
|
-
*
|
|
3973
|
-
* @param key
|
|
3974
|
-
*/
|
|
3975
|
-
hasHeader(key) {
|
|
3976
|
-
return this.header(key) != null;
|
|
3977
|
-
}
|
|
3978
|
-
/**
|
|
3979
|
-
* Retrieve a header from the request.
|
|
3980
|
-
*
|
|
3981
|
-
* @param key
|
|
3982
|
-
* @param default
|
|
3983
|
-
*/
|
|
3984
|
-
header(key, defaultValue) {
|
|
3985
|
-
return this.retrieveItem("headers", key, defaultValue);
|
|
3986
|
-
}
|
|
3987
|
-
/**
|
|
3988
|
-
* Determine if a cookie is set on the request.
|
|
3989
|
-
*
|
|
3990
|
-
* @param string $key
|
|
3991
|
-
*/
|
|
3992
|
-
hasCookie(key) {
|
|
3993
|
-
return this.cookie(key) != null;
|
|
3994
|
-
}
|
|
3995
|
-
/**
|
|
3996
|
-
* Retrieve a cookie from the request.
|
|
3997
|
-
*
|
|
3998
|
-
* @param key
|
|
3999
|
-
* @param default
|
|
4000
|
-
*/
|
|
4001
|
-
cookie(key, defaultValue) {
|
|
4002
|
-
return this.retrieveItem("cookies", key, defaultValue);
|
|
4003
|
-
}
|
|
4004
|
-
/**
|
|
4005
|
-
* Retrieve a query string item from the request.
|
|
4006
|
-
*
|
|
4007
|
-
* @param key
|
|
4008
|
-
* @param default
|
|
4009
|
-
*/
|
|
4010
|
-
query(key, defaultValue) {
|
|
4011
|
-
return this.retrieveItem("_query", key, defaultValue);
|
|
4012
|
-
}
|
|
4013
|
-
/**
|
|
4014
|
-
* Retrieve a server variable from the request.
|
|
4015
|
-
*
|
|
4016
|
-
* @param key
|
|
4017
|
-
* @param default
|
|
4018
|
-
*/
|
|
4019
|
-
server(key, defaultValue) {
|
|
4020
|
-
return this.retrieveItem("_server", key, defaultValue);
|
|
4021
|
-
}
|
|
4022
|
-
/**
|
|
4023
|
-
* Get the input source for the request.
|
|
4024
|
-
*
|
|
4025
|
-
* @return {InputBag}
|
|
4026
|
-
*/
|
|
4027
|
-
getInputSource() {
|
|
4028
|
-
if (this.isJson()) return this.json();
|
|
4029
|
-
return ["GET", "HEAD"].includes(this.getRealMethod()) ? this._query : this.request;
|
|
4030
|
-
}
|
|
4031
|
-
/**
|
|
4032
|
-
* Retrieve a parameter item from a given source.
|
|
4033
|
-
*
|
|
4034
|
-
* @param source
|
|
4035
|
-
* @param key
|
|
4036
|
-
* @param defaultValue
|
|
4037
|
-
*/
|
|
4038
|
-
retrieveItem(source, key, defaultValue) {
|
|
4039
|
-
if (key == null) return this[source].all();
|
|
4040
|
-
if (this[source] instanceof InputBag) return this[source].all()[key] ?? defaultValue;
|
|
4041
|
-
return this[source].get(key, defaultValue);
|
|
4042
|
-
}
|
|
4043
|
-
/**
|
|
4044
|
-
* Dump the items.
|
|
4045
|
-
*
|
|
4046
|
-
* @param keys
|
|
4047
|
-
*/
|
|
4048
|
-
dump(...keys) {
|
|
4049
|
-
if (keys.length > 0) this.only(keys).then(dump);
|
|
4050
|
-
else this.all().then(dump);
|
|
4051
|
-
return this;
|
|
4052
|
-
}
|
|
4053
|
-
getEvent(key) {
|
|
4054
|
-
return (0, _h3ravel_support.safeDot)(this.event, key);
|
|
4055
|
-
}
|
|
4056
|
-
};
|
|
4057
|
-
//#endregion
|
|
4058
|
-
//#region src/Resources/JsonResource.ts
|
|
4059
|
-
/**
|
|
4060
|
-
* Class to render API resource
|
|
4061
|
-
*/
|
|
4062
|
-
var JsonResource = class {
|
|
4063
|
-
event;
|
|
4064
|
-
/**
|
|
4065
|
-
* The request instance
|
|
4066
|
-
*/
|
|
4067
|
-
request;
|
|
4068
|
-
/**
|
|
4069
|
-
* The response instance
|
|
4070
|
-
*/
|
|
4071
|
-
response;
|
|
4072
|
-
/**
|
|
4073
|
-
* The data to send to the client
|
|
4074
|
-
*/
|
|
4075
|
-
resource;
|
|
4076
|
-
/**
|
|
4077
|
-
* The final response data object
|
|
4078
|
-
*/
|
|
4079
|
-
body = { data: {} };
|
|
4080
|
-
/**
|
|
4081
|
-
* Flag to track if response should be sent automatically
|
|
4082
|
-
*/
|
|
4083
|
-
shouldSend = false;
|
|
4084
|
-
/**
|
|
4085
|
-
* Flag to track if response has been sent
|
|
4086
|
-
*/
|
|
4087
|
-
responseSent = false;
|
|
4088
|
-
/**
|
|
4089
|
-
* @param req The request instance
|
|
4090
|
-
* @param res The response instance
|
|
4091
|
-
* @param rsc The data to send to the client
|
|
4092
|
-
*/
|
|
4093
|
-
constructor(event, rsc) {
|
|
4094
|
-
this.event = event;
|
|
4095
|
-
this.request = event.req;
|
|
4096
|
-
this.response = event.res;
|
|
4097
|
-
this.resource = rsc;
|
|
4098
|
-
for (const key of Object.keys(rsc)) if (!(key in this)) Object.defineProperty(this, key, {
|
|
4099
|
-
enumerable: true,
|
|
4100
|
-
configurable: true,
|
|
4101
|
-
get: () => this.resource[key],
|
|
4102
|
-
set: (value) => {
|
|
4103
|
-
this.resource[key] = value;
|
|
4104
|
-
}
|
|
4105
|
-
});
|
|
4106
|
-
}
|
|
4107
|
-
/**
|
|
4108
|
-
* Return the data in the expected format
|
|
4109
|
-
*
|
|
4110
|
-
* @returns
|
|
4111
|
-
*/
|
|
4112
|
-
data() {
|
|
4113
|
-
return this.resource;
|
|
4114
|
-
}
|
|
4115
|
-
/**
|
|
4116
|
-
* Build the response object
|
|
4117
|
-
* @returns this
|
|
4118
|
-
*/
|
|
4119
|
-
json() {
|
|
4120
|
-
this.shouldSend = true;
|
|
4121
|
-
this.response.status = 200;
|
|
4122
|
-
const resource = this.data();
|
|
4123
|
-
let data = Array.isArray(resource) ? [...resource] : { ...resource };
|
|
4124
|
-
if (typeof data.data !== "undefined") data = data.data;
|
|
4125
|
-
if (!Array.isArray(resource)) delete data.pagination;
|
|
4126
|
-
this.body = { data };
|
|
4127
|
-
if (!Array.isArray(resource) && resource.pagination) {
|
|
4128
|
-
const meta = this.body.meta ?? {};
|
|
4129
|
-
meta.pagination = resource.pagination;
|
|
4130
|
-
this.body.meta = meta;
|
|
4131
|
-
}
|
|
4132
|
-
if (this.resource.pagination && !this.body.meta?.pagination) {
|
|
4133
|
-
const meta = this.body.meta ?? {};
|
|
4134
|
-
meta.pagination = this.resource.pagination;
|
|
4135
|
-
this.body.meta = meta;
|
|
4136
|
-
}
|
|
4137
|
-
return this;
|
|
4138
|
-
}
|
|
4139
|
-
/**
|
|
4140
|
-
* Add context data to the response object
|
|
4141
|
-
* @param data Context data
|
|
4142
|
-
* @returns this
|
|
4143
|
-
*/
|
|
4144
|
-
additional(data) {
|
|
4145
|
-
this.shouldSend = true;
|
|
4146
|
-
delete data.data;
|
|
4147
|
-
delete data.pagination;
|
|
4148
|
-
this.body = {
|
|
4149
|
-
...this.body,
|
|
4150
|
-
...data
|
|
4151
|
-
};
|
|
4152
|
-
return this;
|
|
4153
|
-
}
|
|
4154
|
-
/**
|
|
4155
|
-
* Send the output to the client
|
|
4156
|
-
* @returns this
|
|
4157
|
-
*/
|
|
4158
|
-
send() {
|
|
4159
|
-
this.shouldSend = false;
|
|
4160
|
-
if (!this.responseSent) this.#send();
|
|
4161
|
-
return this;
|
|
4162
|
-
}
|
|
4163
|
-
/**
|
|
4164
|
-
* Set the status code for this response
|
|
4165
|
-
* @param code Status code
|
|
4166
|
-
* @returns this
|
|
4167
|
-
*/
|
|
4168
|
-
status(code) {
|
|
4169
|
-
this.response.status = code;
|
|
4170
|
-
return this;
|
|
4171
|
-
}
|
|
4172
|
-
/**
|
|
4173
|
-
* Private method to send the response
|
|
4174
|
-
*/
|
|
4175
|
-
#send() {
|
|
4176
|
-
if (!this.responseSent) this.responseSent = true;
|
|
4177
|
-
}
|
|
4178
|
-
/**
|
|
4179
|
-
* Check if send should be triggered automatically
|
|
4180
|
-
*/
|
|
4181
|
-
checkSend() {
|
|
4182
|
-
if (this.shouldSend && !this.responseSent) this.#send();
|
|
4183
|
-
}
|
|
4184
|
-
};
|
|
4185
|
-
//#endregion
|
|
4186
|
-
//#region src/Resources/ApiResource.ts
|
|
4187
|
-
function ApiResource(instance) {
|
|
4188
|
-
return new Proxy(instance, { get(target, prop, receiver) {
|
|
4189
|
-
const value = Reflect.get(target, prop, receiver);
|
|
4190
|
-
if (typeof value === "function") {
|
|
4191
|
-
if (prop === "json" || prop === "additional") return (...args) => {
|
|
4192
|
-
const result = value.apply(target, args);
|
|
4193
|
-
setImmediate(() => target["checkSend"]());
|
|
4194
|
-
return result;
|
|
4195
|
-
};
|
|
4196
|
-
else if (prop === "send") return (...args) => {
|
|
4197
|
-
target["shouldSend"] = false;
|
|
4198
|
-
return value.apply(target, args);
|
|
4199
|
-
};
|
|
4200
|
-
}
|
|
4201
|
-
return value;
|
|
4202
|
-
} });
|
|
4203
|
-
}
|
|
4204
|
-
//#endregion
|
|
4205
|
-
exports.ApiResource = ApiResource;
|
|
4206
|
-
exports.BadRequestException = BadRequestException;
|
|
4207
|
-
exports.ConflictingHeadersException = ConflictingHeadersException;
|
|
4208
|
-
exports.Cookie = Cookie;
|
|
4209
|
-
exports.FileBag = FileBag;
|
|
4210
|
-
exports.FireCommand = FireCommand;
|
|
4211
|
-
exports.FlashDataMiddleware = FlashDataMiddleware;
|
|
4212
|
-
exports.FormRequest = FormRequest;
|
|
4213
|
-
exports.HeaderBag = HeaderBag;
|
|
4214
|
-
exports.HeaderUtility = HeaderUtility;
|
|
4215
|
-
exports.HttpContext = HttpContext;
|
|
4216
|
-
exports.HttpRequest = HttpRequest;
|
|
4217
|
-
exports.HttpResponse = HttpResponse;
|
|
4218
|
-
exports.HttpResponseException = HttpResponseException;
|
|
4219
|
-
exports.HttpServiceProvider = HttpServiceProvider;
|
|
4220
|
-
exports.InputBag = InputBag;
|
|
4221
|
-
exports.IpUtils = IpUtils;
|
|
4222
|
-
exports.JsonResource = JsonResource;
|
|
4223
|
-
exports.JsonResponse = JsonResponse;
|
|
4224
|
-
exports.LogRequests = LogRequests;
|
|
4225
|
-
Object.defineProperty(exports, "Middleware", {
|
|
4226
|
-
enumerable: true,
|
|
4227
|
-
get: function() {
|
|
4228
|
-
return Middleware;
|
|
4229
|
-
}
|
|
4230
|
-
});
|
|
4231
|
-
exports.ParamBag = ParamBag;
|
|
4232
|
-
exports.Request = Request;
|
|
4233
|
-
exports.Responsable = Responsable;
|
|
4234
|
-
exports.Response = Response;
|
|
4235
|
-
exports.ResponseHeaderBag = ResponseHeaderBag;
|
|
4236
|
-
exports.ServerBag = ServerBag;
|
|
4237
|
-
exports.SuspiciousOperationException = SuspiciousOperationException;
|
|
4238
|
-
exports.TrustHosts = TrustHosts;
|
|
4239
|
-
exports.UnexpectedValueException = UnexpectedValueException;
|
|
4240
|
-
exports.UploadedFile = UploadedFile;
|