@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 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
- declare function imageHandler({ domains, sizes, quality, }: {
1
+ import { IImageResize } from '@websolutespa/bom-core';
2
+
3
+ type IImageHandlerOptions = {
2
4
  domains: string[];
3
5
  sizes: number[];
4
6
  quality: number;
5
- }): any;
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
- const { slugs, origin = "", search = "", w, h, x, y, q } = request.query;
82
- if (!Array.isArray(slugs) || slugs.length === 0) {
83
- return response.status(400).send("missing slugs parameter");
84
- }
85
- const width = typeof w === "string" ? parseInt(w) : void 0;
86
- if (!width) {
87
- return response.status(400).send("missing w parameter");
88
- }
89
- if (!sizes.includes(width)) {
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 url = origin + "/" + slugs.join("/") + search;
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
- const { headers } = request;
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, width, height, focalX, focalY);
112
+ const resizedImage = await resizeImage(originalImage, imageOptions);
121
113
  const compressedImage = resizedImage.webp({
122
- quality: imageQuality
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
- throw error;
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
- async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, focalY) {
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 !== 50 || focalY !== 50)) {
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, skipUrlValidity = false) {
255
+ function getOriginalImage(url, domains, headers) {
190
256
  return new Promise((resolve, reject) => {
191
257
  if (url.indexOf("http") === 0) {
192
- if (!skipUrlValidity && !checkUrlValidity(url, domains)) {
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(url, width, height, focalX, focalY, quality) {
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_${width}_h_${height || 0}_x_${focalX || 50}_y_${focalY || 50}_q_${quality}${parsedName.ext}`);
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 checkUrlValidity(src, domains) {
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
- const { slugs, origin = "", search = "", w, h, x, y, q } = request.query;
46
- if (!Array.isArray(slugs) || slugs.length === 0) {
47
- return response.status(400).send("missing slugs parameter");
48
- }
49
- const width = typeof w === "string" ? parseInt(w) : void 0;
50
- if (!width) {
51
- return response.status(400).send("missing w parameter");
52
- }
53
- if (!sizes.includes(width)) {
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 url = origin + "/" + slugs.join("/") + search;
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
- const { headers } = request;
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, width, height, focalX, focalY);
76
+ const resizedImage = await resizeImage(originalImage, imageOptions);
85
77
  const compressedImage = resizedImage.webp({
86
- quality: imageQuality
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
- throw error;
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
- async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, focalY) {
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 !== 50 || focalY !== 50)) {
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, skipUrlValidity = false) {
219
+ function getOriginalImage(url, domains, headers) {
154
220
  return new Promise((resolve, reject) => {
155
221
  if (url.indexOf("http") === 0) {
156
- if (!skipUrlValidity && !checkUrlValidity(url, domains)) {
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(url, width, height, focalX, focalY, quality) {
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_${width}_h_${height || 0}_x_${focalX || 50}_y_${focalY || 50}_q_${quality}${parsedName.ext}`);
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 checkUrlValidity(src, domains) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@websolutespa/bom-mixer-image",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Mixer Image module of the BOM Repository",
5
5
  "keywords": [
6
6
  "bom",
@@ -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
- // Destructuring 'url', 'w', 'h', 'x', 'y', 'q' from the request query
34
- const { slugs, origin = '', search = '', w, h, x, y, q } = request.query;
35
- // Checking slugs
36
- if (!Array.isArray(slugs) || slugs.length === 0) {
37
- // console.error('imageHandler', 'missing slugs parameter');
38
- return response.status(400).send('missing slugs parameter');
39
- }
40
- // Parsing 'w' to an integer and assigning it to 'width'
41
- const width = typeof w === 'string' ? parseInt(w) : undefined;
42
- // Checking if 'width' is not defined
43
- if (!width) {
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(url, width, height, focalX, focalY, imageQuality);
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
- const { headers } = request;
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, width, height, focalX, focalY);
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: imageQuality,
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
- throw error;
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
- async function resizeImage(originalImage: sharp.Sharp, resizeWidth: number, resizeHeight: number | undefined, focalX: number, focalY: number): Promise<sharp.Sharp> {
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 !== 50 || focalY !== 50)) {
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, skipUrlValidity: boolean = false): Promise<Buffer> {
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(url: string, width: number, height: number | undefined, focalX: number | undefined, focalY: number | undefined, quality: number): string {
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 = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_x_${focalX || 50}_y_${focalY || 50}_q_${quality}${parsedName.ext}`);
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 checkUrlValidity(src: string, domains: string[]): boolean {
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
  }