next-tinacms-cloudinary 26.0.2 → 26.0.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/dist/handlers.js +120 -7
- package/dist/media-key.d.ts +26 -0
- package/package.json +4 -4
package/dist/handlers.js
CHANGED
|
@@ -1,8 +1,86 @@
|
|
|
1
1
|
// src/handlers.ts
|
|
2
2
|
import { v2 as cloudinary } from "cloudinary";
|
|
3
|
-
import
|
|
3
|
+
import path2 from "path";
|
|
4
4
|
import multer from "multer";
|
|
5
5
|
import { promisify } from "util";
|
|
6
|
+
|
|
7
|
+
// src/media-key.ts
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
var MediaKeyError = class extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "MediaKeyError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function stripSlashes(value) {
|
|
16
|
+
let start = 0;
|
|
17
|
+
let end = value.length;
|
|
18
|
+
while (start < end && value[start] === "/") start++;
|
|
19
|
+
while (end > start && value[end - 1] === "/") end--;
|
|
20
|
+
return value.slice(start, end);
|
|
21
|
+
}
|
|
22
|
+
function normalizeMediaRoot(mediaRoot) {
|
|
23
|
+
if (!mediaRoot) {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
return stripSlashes(mediaRoot);
|
|
27
|
+
}
|
|
28
|
+
function resolveKey(mediaRoot, rawKey, options) {
|
|
29
|
+
if (typeof rawKey !== "string" || rawKey.trim() === "") {
|
|
30
|
+
throw new MediaKeyError("a media key is required");
|
|
31
|
+
}
|
|
32
|
+
let decoded;
|
|
33
|
+
if (options?.decode === false) {
|
|
34
|
+
decoded = rawKey;
|
|
35
|
+
} else {
|
|
36
|
+
try {
|
|
37
|
+
decoded = decodeURIComponent(rawKey);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new MediaKeyError("media key is not valid");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (decoded.includes("\0") || decoded.includes("\\")) {
|
|
43
|
+
throw new MediaKeyError("media key is not valid");
|
|
44
|
+
}
|
|
45
|
+
if (decoded.startsWith("/") || /^[a-zA-Z]:/.test(decoded)) {
|
|
46
|
+
throw new MediaKeyError("absolute media keys are not allowed");
|
|
47
|
+
}
|
|
48
|
+
const normalized = path.posix.normalize(decoded).replace(/^\.\//, "");
|
|
49
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
50
|
+
throw new MediaKeyError("media key may not traverse directories");
|
|
51
|
+
}
|
|
52
|
+
if (normalized === "" || normalized === ".") {
|
|
53
|
+
throw new MediaKeyError("a media key is required");
|
|
54
|
+
}
|
|
55
|
+
const root = normalizeMediaRoot(mediaRoot);
|
|
56
|
+
if (!root) {
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
const scoped = normalized === root || normalized.startsWith(root + "/") ? normalized : path.posix.join(root, normalized);
|
|
60
|
+
if (scoped !== root && !scoped.startsWith(root + "/")) {
|
|
61
|
+
throw new MediaKeyError("media key escapes mediaRoot");
|
|
62
|
+
}
|
|
63
|
+
return scoped;
|
|
64
|
+
}
|
|
65
|
+
function resolveDirectory(rawDirectory) {
|
|
66
|
+
if (typeof rawDirectory !== "string" || rawDirectory.trim() === "") {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
if (rawDirectory.includes("\0") || rawDirectory.includes("\\")) {
|
|
70
|
+
throw new MediaKeyError("media directory is not valid");
|
|
71
|
+
}
|
|
72
|
+
const trimmed = stripSlashes(rawDirectory);
|
|
73
|
+
const normalized = path.posix.normalize(trimmed).replace(/^\.\//, "");
|
|
74
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
75
|
+
throw new MediaKeyError("media directory may not traverse directories");
|
|
76
|
+
}
|
|
77
|
+
if (normalized === "" || normalized === ".") {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
return normalized + "/";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/handlers.ts
|
|
6
84
|
var mediaHandlerConfig = {
|
|
7
85
|
api: {
|
|
8
86
|
bodyParser: false
|
|
@@ -43,10 +121,25 @@ async function uploadMedia(req, res) {
|
|
|
43
121
|
}).single("file")
|
|
44
122
|
);
|
|
45
123
|
await upload(req, res);
|
|
124
|
+
if (!req.file) {
|
|
125
|
+
return res.status(400).json({ message: "file is required" });
|
|
126
|
+
}
|
|
46
127
|
const { directory } = req.body;
|
|
128
|
+
const filename = req.file.originalname;
|
|
129
|
+
let folder;
|
|
130
|
+
try {
|
|
131
|
+
const rawFolder = (directory || "").replace(/^\/+/, "");
|
|
132
|
+
folder = rawFolder ? resolveKey("", rawFolder, { decode: false }) : "";
|
|
133
|
+
resolveKey("", filename, { decode: false });
|
|
134
|
+
} catch (e) {
|
|
135
|
+
if (e instanceof MediaKeyError) {
|
|
136
|
+
return res.status(400).json({ message: e.message });
|
|
137
|
+
}
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
47
140
|
try {
|
|
48
141
|
const result = await cloudinary.uploader.upload(req.file.path, {
|
|
49
|
-
folder
|
|
142
|
+
folder,
|
|
50
143
|
use_filename: true,
|
|
51
144
|
overwrite: false,
|
|
52
145
|
resource_type: "auto"
|
|
@@ -65,6 +158,17 @@ async function listMedia(req, res, opts) {
|
|
|
65
158
|
filesOnly: req.query.filesOnly === "true" || false
|
|
66
159
|
};
|
|
67
160
|
const useRootDirectory = !mediaListOptions.directory || mediaListOptions.directory === "/" || mediaListOptions.directory === '""';
|
|
161
|
+
if (!useRootDirectory) {
|
|
162
|
+
try {
|
|
163
|
+
resolveDirectory(mediaListOptions.directory);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
if (e instanceof MediaKeyError) {
|
|
166
|
+
res.status(400).json({ message: e.message });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
throw e;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
68
172
|
const query = useRootDirectory ? 'folder=""' : `folder="${mediaListOptions.directory}"`;
|
|
69
173
|
const response = await cloudinary.search.expression(query).max_results(mediaListOptions.limit).next_cursor(mediaListOptions.offset).execute();
|
|
70
174
|
const files = response.resources.map(getCloudinaryToTinaFunc(opts));
|
|
@@ -100,8 +204,8 @@ async function listMedia(req, res, opts) {
|
|
|
100
204
|
return {
|
|
101
205
|
id: folder.path,
|
|
102
206
|
type: "dir",
|
|
103
|
-
filename:
|
|
104
|
-
directory:
|
|
207
|
+
filename: path2.basename(folder.path),
|
|
208
|
+
directory: path2.dirname(folder.path)
|
|
105
209
|
};
|
|
106
210
|
});
|
|
107
211
|
}
|
|
@@ -124,7 +228,16 @@ var findErrorMessage = (e) => {
|
|
|
124
228
|
};
|
|
125
229
|
async function deleteAsset(req, res) {
|
|
126
230
|
const { media } = req.query;
|
|
127
|
-
const [,
|
|
231
|
+
const [, rawPublicId] = media;
|
|
232
|
+
let public_id;
|
|
233
|
+
try {
|
|
234
|
+
public_id = resolveKey("", rawPublicId, { decode: false });
|
|
235
|
+
} catch (e) {
|
|
236
|
+
if (e instanceof MediaKeyError) {
|
|
237
|
+
return res.status(400).json({ message: e.message });
|
|
238
|
+
}
|
|
239
|
+
throw e;
|
|
240
|
+
}
|
|
128
241
|
cloudinary.uploader.destroy(public_id, {}, (err) => {
|
|
129
242
|
if (err) res.status(500);
|
|
130
243
|
res.json({
|
|
@@ -140,8 +253,8 @@ function getCloudinaryToTinaFunc(opts) {
|
|
|
140
253
|
useHttps = opts.useHttps;
|
|
141
254
|
}
|
|
142
255
|
const sel = useHttps ? "secure_url" : "url";
|
|
143
|
-
const filename =
|
|
144
|
-
const directory =
|
|
256
|
+
const filename = path2.basename(file.public_id);
|
|
257
|
+
const directory = path2.dirname(file.public_id);
|
|
145
258
|
return {
|
|
146
259
|
id: file.public_id,
|
|
147
260
|
filename,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class MediaKeyError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a caller-supplied media key against the configured mediaRoot.
|
|
6
|
+
*
|
|
7
|
+
* @throws {MediaKeyError} when the key is empty, absolute, contains a NUL
|
|
8
|
+
* byte, uses path traversal, or escapes mediaRoot.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveKey(mediaRoot: string, rawKey: unknown, options?: {
|
|
11
|
+
decode?: boolean;
|
|
12
|
+
}): string;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a caller-supplied listing directory into a relative prefix.
|
|
15
|
+
*
|
|
16
|
+
* The directory is optional: an empty / root directory returns `''` (list the
|
|
17
|
+
* mediaRoot itself). Otherwise leading/trailing slashes are stripped (matching
|
|
18
|
+
* the historical handler behaviour) and the result is normalised so it cannot
|
|
19
|
+
* traverse upward — `path.join(mediaRoot, directory)` would otherwise collapse
|
|
20
|
+
* `..` and list objects outside mediaRoot. Returns a prefix with a trailing
|
|
21
|
+
* slash so it can be joined onto mediaRoot directly.
|
|
22
|
+
*
|
|
23
|
+
* @throws {MediaKeyError} when the directory contains a NUL byte or backslash,
|
|
24
|
+
* or uses upward path traversal.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveDirectory(rawDirectory: unknown): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-tinacms-cloudinary",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "26.0.
|
|
4
|
+
"version": "26.0.4",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"files": [
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"react": "^18.3.1",
|
|
31
31
|
"react-dom": "^18.3.1",
|
|
32
32
|
"typescript": "^5.7.3",
|
|
33
|
-
"@tinacms/scripts": "1.6.
|
|
34
|
-
"tinacms": "3.9.
|
|
33
|
+
"@tinacms/scripts": "1.6.2",
|
|
34
|
+
"tinacms": "3.9.4"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"tinacms": "3.9.
|
|
37
|
+
"tinacms": "3.9.4"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"registry": "https://registry.npmjs.org"
|