@websolutespa/bom-mixer-image 1.8.2 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +107 -38
- package/dist/index.mjs +107 -38
- package/package.json +1 -1
- package/src/image/image.handler.ts +171 -82
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @websolutespa/bom-mixer-image
|
|
2
2
|
|
|
3
|
+
## 1.8.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fixing: handling latin1 src
|
|
8
|
+
|
|
9
|
+
## 1.8.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Added: ImageStrategy.Token
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @websolutespa/bom-core@1.8.4
|
|
16
|
+
|
|
3
17
|
## 1.8.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import { IImageResize } from '@websolutespa/bom-core';
|
|
2
|
+
|
|
3
|
+
type IImageHandlerOptions = {
|
|
2
4
|
domains: string[];
|
|
3
5
|
sizes: number[];
|
|
4
6
|
quality: number;
|
|
5
|
-
}
|
|
7
|
+
};
|
|
8
|
+
type IImageOptions = IImageResize & {
|
|
9
|
+
origin: string;
|
|
10
|
+
search: string;
|
|
11
|
+
url: string;
|
|
12
|
+
};
|
|
13
|
+
declare function imageHandler({ domains, sizes, quality, }: IImageHandlerOptions): any;
|
|
6
14
|
|
|
7
|
-
export { imageHandler };
|
|
15
|
+
export { IImageHandlerOptions, IImageOptions, imageHandler };
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
38
38
|
var fs = __toESM(require("fs"));
|
|
39
39
|
var path = __toESM(require("path"));
|
|
40
40
|
var import_sharp = __toESM(require("sharp"));
|
|
41
|
+
var import_bom_core = require("@websolutespa/bom-core");
|
|
41
42
|
|
|
42
43
|
// src/api/api.handler.ts
|
|
43
44
|
function apiHandler(handler) {
|
|
@@ -67,7 +68,10 @@ function errorHandler(error, response) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
// src/image/image.handler.ts
|
|
71
|
+
var IMAGE_STRATEGY = process.env.IMAGE_STRATEGY ? process.env.IMAGE_STRATEGY : import_bom_core.ImageStrategy.Mixer;
|
|
70
72
|
var IMAGE_MAX_AGE = 604800;
|
|
73
|
+
var IMAGE_FOCAL_X = 50;
|
|
74
|
+
var IMAGE_FOCAL_Y = 50;
|
|
71
75
|
var IMAGE_QUALITY = 90;
|
|
72
76
|
function imageHandler({
|
|
73
77
|
domains = [],
|
|
@@ -78,26 +82,17 @@ function imageHandler({
|
|
|
78
82
|
// Defining the 'get' method for handling GET requests
|
|
79
83
|
get: async (request, response) => {
|
|
80
84
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return response.status(400).send("invalid w parameter");
|
|
91
|
-
}
|
|
92
|
-
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
93
|
-
const focalX = typeof x === "string" ? parseInt(x) : 50;
|
|
94
|
-
const focalY = typeof y === "string" ? parseInt(y) : 50;
|
|
95
|
-
const imageQuality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : quality));
|
|
96
|
-
if (imageQuality !== quality) {
|
|
97
|
-
return response.status(400).send("invalid q parameter");
|
|
85
|
+
let imageOptions;
|
|
86
|
+
try {
|
|
87
|
+
imageOptions = IMAGE_STRATEGY === import_bom_core.ImageStrategy.Token ? getEncryptedImageOptions(request, { domains, sizes, quality }) : getImageOptions(request, { domains, sizes, quality });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return response.status(400).json({
|
|
90
|
+
errors: [{
|
|
91
|
+
message: error
|
|
92
|
+
}]
|
|
93
|
+
});
|
|
98
94
|
}
|
|
99
|
-
const
|
|
100
|
-
const cachedName = urlToCachedName(url, width, height, focalX, focalY, imageQuality);
|
|
95
|
+
const cachedName = urlToCachedName(imageOptions);
|
|
101
96
|
if (fs.existsSync(cachedName)) {
|
|
102
97
|
const cachedImage = await getCachedImage(cachedName);
|
|
103
98
|
response.setHeader("Content-Type", "image/webp");
|
|
@@ -105,21 +100,18 @@ function imageHandler({
|
|
|
105
100
|
response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
|
|
106
101
|
return response.send(cachedImage);
|
|
107
102
|
}
|
|
108
|
-
|
|
109
|
-
const referer = headers.referer;
|
|
110
|
-
const publicUrl = process.env.NEXT_PUBLIC_URL || "";
|
|
111
|
-
if (referer && publicUrl !== "https://" && referer.indexOf(publicUrl) !== 0) {
|
|
103
|
+
if (isInvalidReferer(request)) {
|
|
112
104
|
return response.status(401).send("you are not authorized to perform this action");
|
|
113
105
|
}
|
|
114
|
-
if (url.indexOf("http") === -1 && isVercel()) {
|
|
115
|
-
response.redirect(307, url);
|
|
106
|
+
if (imageOptions.url.indexOf("http") === -1 && isVercel()) {
|
|
107
|
+
response.redirect(307, imageOptions.url);
|
|
116
108
|
}
|
|
117
|
-
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
109
|
+
const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);
|
|
118
110
|
if (originalImageBuffer) {
|
|
119
111
|
const originalImage = (0, import_sharp.default)(originalImageBuffer);
|
|
120
|
-
const resizedImage = await resizeImage(originalImage,
|
|
112
|
+
const resizedImage = await resizeImage(originalImage, imageOptions);
|
|
121
113
|
const compressedImage = resizedImage.webp({
|
|
122
|
-
quality:
|
|
114
|
+
quality: imageOptions.q
|
|
123
115
|
// nearLossless: true,
|
|
124
116
|
});
|
|
125
117
|
compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
|
|
@@ -131,12 +123,86 @@ function imageHandler({
|
|
|
131
123
|
}
|
|
132
124
|
} catch (error) {
|
|
133
125
|
console.error("imageHandler.error", error);
|
|
134
|
-
|
|
126
|
+
return response.status(error.status || 500).json({
|
|
127
|
+
errors: [{
|
|
128
|
+
message: error.message || error.name || error.statusText || "An error occurred"
|
|
129
|
+
}]
|
|
130
|
+
});
|
|
135
131
|
}
|
|
136
132
|
}
|
|
137
133
|
});
|
|
138
134
|
}
|
|
139
|
-
|
|
135
|
+
function isInvalidReferer(request) {
|
|
136
|
+
const { headers } = request;
|
|
137
|
+
const referer = headers.referer;
|
|
138
|
+
const publicUrl = process.env.NEXT_PUBLIC_URL || "";
|
|
139
|
+
if (IMAGE_STRATEGY !== import_bom_core.ImageStrategy.Token && referer && publicUrl !== "https://" && referer.indexOf(publicUrl) !== 0) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
function getImageOptions(request, options) {
|
|
145
|
+
const { slugs, origin = "", search = "", w, h, x, y, q } = request.query;
|
|
146
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
147
|
+
throw "missing slugs parameter";
|
|
148
|
+
}
|
|
149
|
+
const width = typeof w === "string" ? parseInt(w) : void 0;
|
|
150
|
+
if (!width) {
|
|
151
|
+
throw "missing w parameter";
|
|
152
|
+
}
|
|
153
|
+
if (!options.sizes.includes(width)) {
|
|
154
|
+
throw "invalid w parameter";
|
|
155
|
+
}
|
|
156
|
+
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
157
|
+
const focalX = typeof x === "string" ? parseInt(x) : IMAGE_FOCAL_X;
|
|
158
|
+
const focalY = typeof y === "string" ? parseInt(y) : IMAGE_FOCAL_Y;
|
|
159
|
+
const imageQuality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : options.quality));
|
|
160
|
+
if (imageQuality !== options.quality) {
|
|
161
|
+
throw "invalid q parameter";
|
|
162
|
+
}
|
|
163
|
+
const url = origin + "/" + slugs.join("/") + search;
|
|
164
|
+
return height ? {
|
|
165
|
+
w: width,
|
|
166
|
+
h: height,
|
|
167
|
+
x: focalX,
|
|
168
|
+
y: focalY,
|
|
169
|
+
q: imageQuality,
|
|
170
|
+
src: url,
|
|
171
|
+
url,
|
|
172
|
+
origin: String(origin),
|
|
173
|
+
search: String(search)
|
|
174
|
+
} : {
|
|
175
|
+
w: width,
|
|
176
|
+
q: imageQuality,
|
|
177
|
+
src: url,
|
|
178
|
+
url,
|
|
179
|
+
origin: String(origin),
|
|
180
|
+
search: String(search)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function getEncryptedImageOptions(request, options) {
|
|
184
|
+
const { slugs, origin = "", search = "", t } = request.query;
|
|
185
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
186
|
+
throw "missing slugs parameter";
|
|
187
|
+
}
|
|
188
|
+
if (typeof t !== "string") {
|
|
189
|
+
throw "missing t parameter";
|
|
190
|
+
}
|
|
191
|
+
const imageResize = (0, import_bom_core.decryptImageResize)(t);
|
|
192
|
+
imageResize.src = decodeURIComponent(imageResize.src);
|
|
193
|
+
const url = decodeURIComponent(origin + "/" + slugs.join("/") + search);
|
|
194
|
+
if (imageResize.src !== url) {
|
|
195
|
+
throw `invalid src parameter ${imageResize.src} ${url}`;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
...imageResize,
|
|
199
|
+
url,
|
|
200
|
+
origin: String(origin),
|
|
201
|
+
search: String(search)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function resizeImage(originalImage, imageOptions) {
|
|
205
|
+
const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;
|
|
140
206
|
const defaultResizeOptions = () => {
|
|
141
207
|
return originalImage.resize({
|
|
142
208
|
width: resizeWidth,
|
|
@@ -146,7 +212,7 @@ async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, foc
|
|
|
146
212
|
withoutEnlargement: true
|
|
147
213
|
});
|
|
148
214
|
};
|
|
149
|
-
if (resizeHeight && (focalX !==
|
|
215
|
+
if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {
|
|
150
216
|
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
151
217
|
if (originalWidth && originalHeight) {
|
|
152
218
|
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
@@ -186,10 +252,10 @@ async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, foc
|
|
|
186
252
|
return defaultResizeOptions();
|
|
187
253
|
}
|
|
188
254
|
}
|
|
189
|
-
function getOriginalImage(url, domains, headers
|
|
255
|
+
function getOriginalImage(url, domains, headers) {
|
|
190
256
|
return new Promise((resolve, reject) => {
|
|
191
257
|
if (url.indexOf("http") === 0) {
|
|
192
|
-
if (
|
|
258
|
+
if (isInvalidDomain(url, domains)) {
|
|
193
259
|
return reject({ status: 401, message: "invalid origin" });
|
|
194
260
|
}
|
|
195
261
|
fetch(url, {
|
|
@@ -242,19 +308,22 @@ function urlToFileName(url) {
|
|
|
242
308
|
function isVercel() {
|
|
243
309
|
return process && process.env && String(process.env.VERCEL) === "1";
|
|
244
310
|
}
|
|
245
|
-
function urlToCachedName(
|
|
246
|
-
const fileName = urlToFileName(url);
|
|
311
|
+
function urlToCachedName(options) {
|
|
312
|
+
const fileName = urlToFileName(options.url);
|
|
247
313
|
const parsedName = path.parse(fileName);
|
|
248
314
|
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
249
315
|
if (!fs.existsSync(cacheDir)) {
|
|
250
316
|
fs.mkdirSync(cacheDir);
|
|
251
317
|
}
|
|
252
|
-
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${
|
|
318
|
+
const cachedName = options.h ? path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) : path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);
|
|
253
319
|
return cachedName;
|
|
254
320
|
}
|
|
255
|
-
function
|
|
321
|
+
function isInvalidDomain(src, domains) {
|
|
322
|
+
if (IMAGE_STRATEGY === import_bom_core.ImageStrategy.Token) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
256
325
|
const url = new URL(src);
|
|
257
|
-
return domains.includes(url.hostname);
|
|
326
|
+
return !domains.includes(url.hostname);
|
|
258
327
|
}
|
|
259
328
|
// Annotate the CommonJS export names for ESM import in node:
|
|
260
329
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import sharp from "sharp";
|
|
5
|
+
import { decryptImageResize, ImageStrategy } from "@websolutespa/bom-core";
|
|
5
6
|
|
|
6
7
|
// src/api/api.handler.ts
|
|
7
8
|
function apiHandler(handler) {
|
|
@@ -31,7 +32,10 @@ function errorHandler(error, response) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
// src/image/image.handler.ts
|
|
35
|
+
var IMAGE_STRATEGY = process.env.IMAGE_STRATEGY ? process.env.IMAGE_STRATEGY : ImageStrategy.Mixer;
|
|
34
36
|
var IMAGE_MAX_AGE = 604800;
|
|
37
|
+
var IMAGE_FOCAL_X = 50;
|
|
38
|
+
var IMAGE_FOCAL_Y = 50;
|
|
35
39
|
var IMAGE_QUALITY = 90;
|
|
36
40
|
function imageHandler({
|
|
37
41
|
domains = [],
|
|
@@ -42,26 +46,17 @@ function imageHandler({
|
|
|
42
46
|
// Defining the 'get' method for handling GET requests
|
|
43
47
|
get: async (request, response) => {
|
|
44
48
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return response.status(400).send("invalid w parameter");
|
|
55
|
-
}
|
|
56
|
-
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
57
|
-
const focalX = typeof x === "string" ? parseInt(x) : 50;
|
|
58
|
-
const focalY = typeof y === "string" ? parseInt(y) : 50;
|
|
59
|
-
const imageQuality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : quality));
|
|
60
|
-
if (imageQuality !== quality) {
|
|
61
|
-
return response.status(400).send("invalid q parameter");
|
|
49
|
+
let imageOptions;
|
|
50
|
+
try {
|
|
51
|
+
imageOptions = IMAGE_STRATEGY === ImageStrategy.Token ? getEncryptedImageOptions(request, { domains, sizes, quality }) : getImageOptions(request, { domains, sizes, quality });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return response.status(400).json({
|
|
54
|
+
errors: [{
|
|
55
|
+
message: error
|
|
56
|
+
}]
|
|
57
|
+
});
|
|
62
58
|
}
|
|
63
|
-
const
|
|
64
|
-
const cachedName = urlToCachedName(url, width, height, focalX, focalY, imageQuality);
|
|
59
|
+
const cachedName = urlToCachedName(imageOptions);
|
|
65
60
|
if (fs.existsSync(cachedName)) {
|
|
66
61
|
const cachedImage = await getCachedImage(cachedName);
|
|
67
62
|
response.setHeader("Content-Type", "image/webp");
|
|
@@ -69,21 +64,18 @@ function imageHandler({
|
|
|
69
64
|
response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
|
|
70
65
|
return response.send(cachedImage);
|
|
71
66
|
}
|
|
72
|
-
|
|
73
|
-
const referer = headers.referer;
|
|
74
|
-
const publicUrl = process.env.NEXT_PUBLIC_URL || "";
|
|
75
|
-
if (referer && publicUrl !== "https://" && referer.indexOf(publicUrl) !== 0) {
|
|
67
|
+
if (isInvalidReferer(request)) {
|
|
76
68
|
return response.status(401).send("you are not authorized to perform this action");
|
|
77
69
|
}
|
|
78
|
-
if (url.indexOf("http") === -1 && isVercel()) {
|
|
79
|
-
response.redirect(307, url);
|
|
70
|
+
if (imageOptions.url.indexOf("http") === -1 && isVercel()) {
|
|
71
|
+
response.redirect(307, imageOptions.url);
|
|
80
72
|
}
|
|
81
|
-
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
73
|
+
const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);
|
|
82
74
|
if (originalImageBuffer) {
|
|
83
75
|
const originalImage = sharp(originalImageBuffer);
|
|
84
|
-
const resizedImage = await resizeImage(originalImage,
|
|
76
|
+
const resizedImage = await resizeImage(originalImage, imageOptions);
|
|
85
77
|
const compressedImage = resizedImage.webp({
|
|
86
|
-
quality:
|
|
78
|
+
quality: imageOptions.q
|
|
87
79
|
// nearLossless: true,
|
|
88
80
|
});
|
|
89
81
|
compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
|
|
@@ -95,12 +87,86 @@ function imageHandler({
|
|
|
95
87
|
}
|
|
96
88
|
} catch (error) {
|
|
97
89
|
console.error("imageHandler.error", error);
|
|
98
|
-
|
|
90
|
+
return response.status(error.status || 500).json({
|
|
91
|
+
errors: [{
|
|
92
|
+
message: error.message || error.name || error.statusText || "An error occurred"
|
|
93
|
+
}]
|
|
94
|
+
});
|
|
99
95
|
}
|
|
100
96
|
}
|
|
101
97
|
});
|
|
102
98
|
}
|
|
103
|
-
|
|
99
|
+
function isInvalidReferer(request) {
|
|
100
|
+
const { headers } = request;
|
|
101
|
+
const referer = headers.referer;
|
|
102
|
+
const publicUrl = process.env.NEXT_PUBLIC_URL || "";
|
|
103
|
+
if (IMAGE_STRATEGY !== ImageStrategy.Token && referer && publicUrl !== "https://" && referer.indexOf(publicUrl) !== 0) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
function getImageOptions(request, options) {
|
|
109
|
+
const { slugs, origin = "", search = "", w, h, x, y, q } = request.query;
|
|
110
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
111
|
+
throw "missing slugs parameter";
|
|
112
|
+
}
|
|
113
|
+
const width = typeof w === "string" ? parseInt(w) : void 0;
|
|
114
|
+
if (!width) {
|
|
115
|
+
throw "missing w parameter";
|
|
116
|
+
}
|
|
117
|
+
if (!options.sizes.includes(width)) {
|
|
118
|
+
throw "invalid w parameter";
|
|
119
|
+
}
|
|
120
|
+
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
121
|
+
const focalX = typeof x === "string" ? parseInt(x) : IMAGE_FOCAL_X;
|
|
122
|
+
const focalY = typeof y === "string" ? parseInt(y) : IMAGE_FOCAL_Y;
|
|
123
|
+
const imageQuality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : options.quality));
|
|
124
|
+
if (imageQuality !== options.quality) {
|
|
125
|
+
throw "invalid q parameter";
|
|
126
|
+
}
|
|
127
|
+
const url = origin + "/" + slugs.join("/") + search;
|
|
128
|
+
return height ? {
|
|
129
|
+
w: width,
|
|
130
|
+
h: height,
|
|
131
|
+
x: focalX,
|
|
132
|
+
y: focalY,
|
|
133
|
+
q: imageQuality,
|
|
134
|
+
src: url,
|
|
135
|
+
url,
|
|
136
|
+
origin: String(origin),
|
|
137
|
+
search: String(search)
|
|
138
|
+
} : {
|
|
139
|
+
w: width,
|
|
140
|
+
q: imageQuality,
|
|
141
|
+
src: url,
|
|
142
|
+
url,
|
|
143
|
+
origin: String(origin),
|
|
144
|
+
search: String(search)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function getEncryptedImageOptions(request, options) {
|
|
148
|
+
const { slugs, origin = "", search = "", t } = request.query;
|
|
149
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
150
|
+
throw "missing slugs parameter";
|
|
151
|
+
}
|
|
152
|
+
if (typeof t !== "string") {
|
|
153
|
+
throw "missing t parameter";
|
|
154
|
+
}
|
|
155
|
+
const imageResize = decryptImageResize(t);
|
|
156
|
+
imageResize.src = decodeURIComponent(imageResize.src);
|
|
157
|
+
const url = decodeURIComponent(origin + "/" + slugs.join("/") + search);
|
|
158
|
+
if (imageResize.src !== url) {
|
|
159
|
+
throw `invalid src parameter ${imageResize.src} ${url}`;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
...imageResize,
|
|
163
|
+
url,
|
|
164
|
+
origin: String(origin),
|
|
165
|
+
search: String(search)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function resizeImage(originalImage, imageOptions) {
|
|
169
|
+
const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;
|
|
104
170
|
const defaultResizeOptions = () => {
|
|
105
171
|
return originalImage.resize({
|
|
106
172
|
width: resizeWidth,
|
|
@@ -110,7 +176,7 @@ async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, foc
|
|
|
110
176
|
withoutEnlargement: true
|
|
111
177
|
});
|
|
112
178
|
};
|
|
113
|
-
if (resizeHeight && (focalX !==
|
|
179
|
+
if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {
|
|
114
180
|
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
115
181
|
if (originalWidth && originalHeight) {
|
|
116
182
|
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
@@ -150,10 +216,10 @@ async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, foc
|
|
|
150
216
|
return defaultResizeOptions();
|
|
151
217
|
}
|
|
152
218
|
}
|
|
153
|
-
function getOriginalImage(url, domains, headers
|
|
219
|
+
function getOriginalImage(url, domains, headers) {
|
|
154
220
|
return new Promise((resolve, reject) => {
|
|
155
221
|
if (url.indexOf("http") === 0) {
|
|
156
|
-
if (
|
|
222
|
+
if (isInvalidDomain(url, domains)) {
|
|
157
223
|
return reject({ status: 401, message: "invalid origin" });
|
|
158
224
|
}
|
|
159
225
|
fetch(url, {
|
|
@@ -206,19 +272,22 @@ function urlToFileName(url) {
|
|
|
206
272
|
function isVercel() {
|
|
207
273
|
return process && process.env && String(process.env.VERCEL) === "1";
|
|
208
274
|
}
|
|
209
|
-
function urlToCachedName(
|
|
210
|
-
const fileName = urlToFileName(url);
|
|
275
|
+
function urlToCachedName(options) {
|
|
276
|
+
const fileName = urlToFileName(options.url);
|
|
211
277
|
const parsedName = path.parse(fileName);
|
|
212
278
|
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
213
279
|
if (!fs.existsSync(cacheDir)) {
|
|
214
280
|
fs.mkdirSync(cacheDir);
|
|
215
281
|
}
|
|
216
|
-
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${
|
|
282
|
+
const cachedName = options.h ? path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) : path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);
|
|
217
283
|
return cachedName;
|
|
218
284
|
}
|
|
219
|
-
function
|
|
285
|
+
function isInvalidDomain(src, domains) {
|
|
286
|
+
if (IMAGE_STRATEGY === ImageStrategy.Token) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
220
289
|
const url = new URL(src);
|
|
221
|
-
return domains.includes(url.hostname);
|
|
290
|
+
return !domains.includes(url.hostname);
|
|
222
291
|
}
|
|
223
292
|
export {
|
|
224
293
|
imageHandler
|
package/package.json
CHANGED
|
@@ -8,63 +8,55 @@ import * as path from 'path';
|
|
|
8
8
|
// Importing 'sharp' for image processing
|
|
9
9
|
import sharp from 'sharp';
|
|
10
10
|
// Importing 'apiHandler' from '../api/api.handler'
|
|
11
|
+
import { decryptImageResize, IImageResize, ImageStrategy } from '@websolutespa/bom-core';
|
|
11
12
|
import { apiHandler } from '../api/api.handler';
|
|
12
13
|
|
|
14
|
+
const IMAGE_STRATEGY: ImageStrategy = process.env.IMAGE_STRATEGY ?
|
|
15
|
+
process.env.IMAGE_STRATEGY as ImageStrategy :
|
|
16
|
+
ImageStrategy.Mixer;
|
|
17
|
+
|
|
13
18
|
const IMAGE_MAX_AGE = 604800;
|
|
14
19
|
const IMAGE_FOCAL_X = 50;
|
|
15
20
|
const IMAGE_FOCAL_Y = 50;
|
|
16
21
|
const IMAGE_QUALITY = 90;
|
|
17
22
|
|
|
23
|
+
export type IImageHandlerOptions = {
|
|
24
|
+
domains: string[];
|
|
25
|
+
sizes: number[];
|
|
26
|
+
quality: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type IImageOptions = IImageResize & {
|
|
30
|
+
origin: string;
|
|
31
|
+
search: string;
|
|
32
|
+
url: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
18
35
|
// Exporting the 'imageHandler' function
|
|
19
36
|
export function imageHandler({
|
|
20
37
|
domains = [],
|
|
21
38
|
sizes = [],
|
|
22
39
|
quality = IMAGE_QUALITY,
|
|
23
|
-
}: {
|
|
24
|
-
domains: string[];
|
|
25
|
-
sizes: number[];
|
|
26
|
-
quality: number;
|
|
27
|
-
}) {
|
|
40
|
+
}: IImageHandlerOptions) {
|
|
28
41
|
// Returning the 'apiHandler' function with a configuration object
|
|
29
42
|
return apiHandler({
|
|
30
43
|
// Defining the 'get' method for handling GET requests
|
|
31
44
|
get: async (request: NextApiRequest, response: NextApiResponse) => {
|
|
32
45
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Logging an error message and returning a 400 status code with a message
|
|
45
|
-
// console.error('imageHandler', 'missing w parameter');
|
|
46
|
-
return response.status(400).send('missing w parameter');
|
|
47
|
-
}
|
|
48
|
-
// if (width > 2048) {
|
|
49
|
-
if (!sizes.includes(width)) {
|
|
50
|
-
return response.status(400).send('invalid w parameter');
|
|
51
|
-
}
|
|
52
|
-
// Parsing 'h' to an integer and assigning it to 'height'
|
|
53
|
-
const height = typeof h === 'string' ? parseInt(h) : undefined;
|
|
54
|
-
// Parsing 'x' to an integer and assigning it to 'focalX'
|
|
55
|
-
const focalX = typeof x === 'string' ? parseInt(x) : 50;
|
|
56
|
-
// Parsing 'y' to an integer and assigning it to 'focalY'
|
|
57
|
-
const focalY = typeof y === 'string' ? parseInt(y) : 50;
|
|
58
|
-
// Parsing 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'imageQuality'
|
|
59
|
-
const imageQuality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : quality)));
|
|
60
|
-
// if (width > 2048) {
|
|
61
|
-
if (imageQuality !== quality) {
|
|
62
|
-
return response.status(400).send('invalid q parameter');
|
|
46
|
+
let imageOptions: IImageOptions;
|
|
47
|
+
try {
|
|
48
|
+
imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?
|
|
49
|
+
getEncryptedImageOptions(request, { domains, sizes, quality }) :
|
|
50
|
+
getImageOptions(request, { domains, sizes, quality }));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return response.status(400).json({
|
|
53
|
+
errors: [{
|
|
54
|
+
message: error,
|
|
55
|
+
}],
|
|
56
|
+
});
|
|
63
57
|
}
|
|
64
|
-
// Generating url
|
|
65
|
-
const url = origin + '/' + slugs.join('/') + search;
|
|
66
58
|
// Generating a cached name for the image
|
|
67
|
-
const cachedName = urlToCachedName(
|
|
59
|
+
const cachedName = urlToCachedName(imageOptions);
|
|
68
60
|
// Checking if the cached image exists
|
|
69
61
|
if (fs.existsSync(cachedName)) {
|
|
70
62
|
// console.log('imageHandler.fromCache', cachedName);
|
|
@@ -80,33 +72,25 @@ export function imageHandler({
|
|
|
80
72
|
return response.send(cachedImage);
|
|
81
73
|
}
|
|
82
74
|
// Check referrer
|
|
83
|
-
|
|
84
|
-
const referer = headers.referer;
|
|
85
|
-
const publicUrl = process.env.NEXT_PUBLIC_URL || '';
|
|
86
|
-
// console.log('referef', referer, 'publicUrl', publicUrl);
|
|
87
|
-
if (
|
|
88
|
-
referer &&
|
|
89
|
-
publicUrl !== 'https://' &&
|
|
90
|
-
referer.indexOf(publicUrl) !== 0
|
|
91
|
-
) {
|
|
75
|
+
if (isInvalidReferer(request)) {
|
|
92
76
|
return response.status(401).send('you are not authorized to perform this action');
|
|
93
77
|
}
|
|
94
78
|
// Temporarily fix Vercel local image issue
|
|
95
|
-
if (url.indexOf('http') === -1 && isVercel()) {
|
|
96
|
-
response.redirect(307, url);
|
|
79
|
+
if (imageOptions.url.indexOf('http') === -1 && isVercel()) {
|
|
80
|
+
response.redirect(307, imageOptions.url);
|
|
97
81
|
}
|
|
98
82
|
// Reading the original image
|
|
99
|
-
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
83
|
+
const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);
|
|
100
84
|
// Checking if the original image exists
|
|
101
85
|
if (originalImageBuffer) {
|
|
102
86
|
// Original image
|
|
103
87
|
const originalImage = sharp(originalImageBuffer);
|
|
104
88
|
// Resizing the original image and converting it to WebP format
|
|
105
|
-
const resizedImage = await resizeImage(originalImage,
|
|
89
|
+
const resizedImage = await resizeImage(originalImage, imageOptions);
|
|
106
90
|
// Converting resized image to WebP format
|
|
107
91
|
const compressedImage = resizedImage
|
|
108
92
|
.webp({
|
|
109
|
-
quality:
|
|
93
|
+
quality: imageOptions.q,
|
|
110
94
|
// nearLossless: true,
|
|
111
95
|
});
|
|
112
96
|
// Saving the resized image to the cache
|
|
@@ -124,16 +108,135 @@ export function imageHandler({
|
|
|
124
108
|
// Sending data in the response
|
|
125
109
|
return response.end(data);
|
|
126
110
|
}
|
|
127
|
-
} catch (error) {
|
|
111
|
+
} catch (error: any) {
|
|
128
112
|
// Logging an error message and throwing the error
|
|
129
113
|
console.error('imageHandler.error', error);
|
|
130
|
-
|
|
114
|
+
return response.status(error.status || 500).json({
|
|
115
|
+
errors: [{
|
|
116
|
+
message: error.message || error.name || error.statusText || 'An error occurred',
|
|
117
|
+
}],
|
|
118
|
+
});
|
|
131
119
|
}
|
|
132
120
|
},
|
|
133
121
|
});
|
|
134
122
|
}
|
|
135
123
|
|
|
136
|
-
|
|
124
|
+
function isInvalidReferer(request: NextApiRequest): boolean {
|
|
125
|
+
const { headers } = request;
|
|
126
|
+
const referer = headers.referer;
|
|
127
|
+
const publicUrl = process.env.NEXT_PUBLIC_URL || '';
|
|
128
|
+
// console.log('referef', referer, 'publicUrl', publicUrl, 'IMAGE_STRATEGY', IMAGE_STRATEGY, ImageStrategy.Token);
|
|
129
|
+
if (
|
|
130
|
+
IMAGE_STRATEGY !== ImageStrategy.Token &&
|
|
131
|
+
referer &&
|
|
132
|
+
publicUrl !== 'https://' &&
|
|
133
|
+
referer.indexOf(publicUrl) !== 0
|
|
134
|
+
) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {
|
|
141
|
+
// Destructuring 'slugs', 'origin', 'search', 'w', 'h', 'x', 'y', 'q' from the request query
|
|
142
|
+
const { slugs, origin = '', search = '', w, h, x, y, q } = request.query;
|
|
143
|
+
// Checking slugs
|
|
144
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
145
|
+
// console.error('imageHandler', 'missing slugs parameter');
|
|
146
|
+
throw 'missing slugs parameter';
|
|
147
|
+
}
|
|
148
|
+
// Parsing 'w' to an integer and assigning it to 'width'
|
|
149
|
+
const width = typeof w === 'string' ? parseInt(w) : undefined;
|
|
150
|
+
// Checking if 'width' is not defined
|
|
151
|
+
if (!width) {
|
|
152
|
+
// Logging an error message and returning a 400 status code with a message
|
|
153
|
+
// console.error('imageHandler', 'missing w parameter');
|
|
154
|
+
throw 'missing w parameter';
|
|
155
|
+
}
|
|
156
|
+
// if (width > 2048) {
|
|
157
|
+
if (!options.sizes.includes(width)) {
|
|
158
|
+
throw 'invalid w parameter';
|
|
159
|
+
}
|
|
160
|
+
// Parsing 'h' to an integer and assigning it to 'height'
|
|
161
|
+
const height = typeof h === 'string' ? parseInt(h) : undefined;
|
|
162
|
+
// Parsing 'x' to an integer and assigning it to 'focalX'
|
|
163
|
+
const focalX = typeof x === 'string' ? parseInt(x) : IMAGE_FOCAL_X;
|
|
164
|
+
// Parsing 'y' to an integer and assigning it to 'focalY'
|
|
165
|
+
const focalY = typeof y === 'string' ? parseInt(y) : IMAGE_FOCAL_Y;
|
|
166
|
+
// Parsing 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'imageQuality'
|
|
167
|
+
const imageQuality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : options.quality)));
|
|
168
|
+
// if (width > 2048) {
|
|
169
|
+
if (imageQuality !== options.quality) {
|
|
170
|
+
throw 'invalid q parameter';
|
|
171
|
+
}
|
|
172
|
+
// Generating url
|
|
173
|
+
const url = origin + '/' + slugs.join('/') + search;
|
|
174
|
+
//
|
|
175
|
+
return height ? {
|
|
176
|
+
w: width,
|
|
177
|
+
h: height,
|
|
178
|
+
x: focalX,
|
|
179
|
+
y: focalY,
|
|
180
|
+
q: imageQuality,
|
|
181
|
+
src: url,
|
|
182
|
+
url: url,
|
|
183
|
+
origin: String(origin),
|
|
184
|
+
search: String(search),
|
|
185
|
+
} : {
|
|
186
|
+
w: width,
|
|
187
|
+
q: imageQuality,
|
|
188
|
+
src: url,
|
|
189
|
+
url: url,
|
|
190
|
+
origin: String(origin),
|
|
191
|
+
search: String(search),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getEncryptedImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {
|
|
196
|
+
// Destructuring 'slugs', 'origin', 'search', 't' from the request query
|
|
197
|
+
const { slugs, origin = '', search = '', t } = request.query;
|
|
198
|
+
// Checking slugs
|
|
199
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
200
|
+
// console.error('imageHandler', 'missing slugs parameter');
|
|
201
|
+
throw 'missing slugs parameter';
|
|
202
|
+
}
|
|
203
|
+
if (typeof t !== 'string') {
|
|
204
|
+
throw 'missing t parameter';
|
|
205
|
+
}
|
|
206
|
+
const imageResize = decryptImageResize(t);
|
|
207
|
+
/*
|
|
208
|
+
// Checking if 'w' is not defined
|
|
209
|
+
if (!imageResize.w) {
|
|
210
|
+
throw 'missing w parameter';
|
|
211
|
+
}
|
|
212
|
+
// if (width > 2048) {
|
|
213
|
+
if (!options.sizes.includes(imageResize.w)) {
|
|
214
|
+
throw 'invalid w parameter';
|
|
215
|
+
}
|
|
216
|
+
// Ensuring 'q' is between 30 and 100, and assigning it to 'imageQuality'
|
|
217
|
+
const imageQuality = Math.max(30, Math.min(100, imageResize.q || options.quality));
|
|
218
|
+
if (imageQuality !== options.quality) {
|
|
219
|
+
throw 'invalid q parameter';
|
|
220
|
+
}
|
|
221
|
+
*/
|
|
222
|
+
imageResize.src = decodeURIComponent(imageResize.src);
|
|
223
|
+
// Generating url
|
|
224
|
+
const url = decodeURIComponent(origin + '/' + slugs.join('/') + search);
|
|
225
|
+
//
|
|
226
|
+
if (imageResize.src !== url) {
|
|
227
|
+
throw `invalid src parameter ${imageResize.src} ${url}`;
|
|
228
|
+
}
|
|
229
|
+
//
|
|
230
|
+
return {
|
|
231
|
+
...imageResize,
|
|
232
|
+
url: url,
|
|
233
|
+
origin: String(origin),
|
|
234
|
+
search: String(search),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function resizeImage(originalImage: sharp.Sharp, imageOptions: IImageOptions): Promise<sharp.Sharp> {
|
|
239
|
+
const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;
|
|
137
240
|
const defaultResizeOptions = () => {
|
|
138
241
|
return originalImage.resize({
|
|
139
242
|
width: resizeWidth,
|
|
@@ -143,7 +246,7 @@ async function resizeImage(originalImage: sharp.Sharp, resizeWidth: number, resi
|
|
|
143
246
|
withoutEnlargement: true,
|
|
144
247
|
});
|
|
145
248
|
};
|
|
146
|
-
if (resizeHeight && (focalX !==
|
|
249
|
+
if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {
|
|
147
250
|
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
148
251
|
if (originalWidth && originalHeight) {
|
|
149
252
|
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
@@ -191,16 +294,12 @@ async function resizeImage(originalImage: sharp.Sharp, resizeWidth: number, resi
|
|
|
191
294
|
}
|
|
192
295
|
|
|
193
296
|
// Defining the 'getOriginalImage' function
|
|
194
|
-
function getOriginalImage(url: string, domains: string[], headers: IncomingHttpHeaders
|
|
297
|
+
function getOriginalImage(url: string, domains: string[], headers: IncomingHttpHeaders): Promise<Buffer> {
|
|
195
298
|
// Returning a new promise
|
|
196
299
|
return new Promise((resolve, reject) => {
|
|
197
300
|
// Checking if 'url' starts with 'http'
|
|
198
301
|
if (url.indexOf('http') === 0) {
|
|
199
|
-
if (
|
|
200
|
-
!skipUrlValidity &&
|
|
201
|
-
!checkUrlValidity(url, domains)
|
|
202
|
-
) {
|
|
203
|
-
// console.error('imageHandler.getOriginalImage.error invalid origin');
|
|
302
|
+
if (isInvalidDomain(url, domains)) {
|
|
204
303
|
return reject({ status: 401, message: 'invalid origin' });
|
|
205
304
|
}
|
|
206
305
|
// Fetching the image from the URL
|
|
@@ -236,21 +335,6 @@ function getOriginalImage(url: string, domains: string[], headers: IncomingHttpH
|
|
|
236
335
|
// Resolving the promise with the image data
|
|
237
336
|
return resolve(data);
|
|
238
337
|
});
|
|
239
|
-
/*
|
|
240
|
-
// Check if the file exists
|
|
241
|
-
if (fs.existsSync(filePath)) {
|
|
242
|
-
fs.readFile(filePath, (error, data) => {
|
|
243
|
-
if (error) {
|
|
244
|
-
return reject(error);
|
|
245
|
-
}
|
|
246
|
-
// Resolving the promise with the image data
|
|
247
|
-
return resolve(data);
|
|
248
|
-
});
|
|
249
|
-
} else {
|
|
250
|
-
const origin = getOrigin(headers);
|
|
251
|
-
return getOriginalImage(`${origin}${url}`, domains, headers, true);
|
|
252
|
-
}
|
|
253
|
-
*/
|
|
254
338
|
}
|
|
255
339
|
});
|
|
256
340
|
}
|
|
@@ -291,9 +375,9 @@ function isVercel(): boolean {
|
|
|
291
375
|
}
|
|
292
376
|
|
|
293
377
|
// Defining the 'urlToCachedName' function
|
|
294
|
-
function urlToCachedName(
|
|
378
|
+
function urlToCachedName(options: IImageOptions): string {
|
|
295
379
|
// Converting the URL to a file name
|
|
296
|
-
const fileName = urlToFileName(url);
|
|
380
|
+
const fileName = urlToFileName(options.url);
|
|
297
381
|
// Parsing the file name
|
|
298
382
|
const parsedName = path.parse(fileName);
|
|
299
383
|
// Defining the cache directory
|
|
@@ -306,12 +390,17 @@ function urlToCachedName(url: string, width: number, height: number | undefined,
|
|
|
306
390
|
fs.mkdirSync(cacheDir);
|
|
307
391
|
}
|
|
308
392
|
// Generating the cached name
|
|
309
|
-
const cachedName =
|
|
393
|
+
const cachedName = options.h ?
|
|
394
|
+
path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) :
|
|
395
|
+
path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);
|
|
310
396
|
// Returning the cached name
|
|
311
397
|
return cachedName;
|
|
312
398
|
}
|
|
313
399
|
|
|
314
|
-
function
|
|
400
|
+
function isInvalidDomain(src: string, domains: string[]): boolean {
|
|
401
|
+
if (IMAGE_STRATEGY === ImageStrategy.Token) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
315
404
|
const url = new URL(src);
|
|
316
|
-
return domains.includes(url.hostname);
|
|
405
|
+
return !domains.includes(url.hostname);
|
|
317
406
|
}
|