@sequent-org/moodboard 1.4.6 → 1.4.8
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/package.json +1 -1
- package/src/core/ApiClient.js +8 -0
- package/src/core/SaveManager.js +7 -0
- package/src/core/flows/ClipboardFlow.js +20 -6
- package/src/core/flows/SaveFlow.js +0 -2
- package/src/core/index.js +0 -5
- package/src/moodboard/integration/MoodBoardLoadApi.js +4 -6
- package/src/services/AssetUrlPolicy.js +26 -0
- package/src/services/FileUploadService.js +34 -3
- package/src/services/ImageUploadService.js +34 -3
package/package.json
CHANGED
package/src/core/ApiClient.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isV2ImageDownloadUrl } from '../services/AssetUrlPolicy.js';
|
|
2
|
+
|
|
1
3
|
// src/core/ApiClient.js
|
|
2
4
|
export class ApiClient {
|
|
3
5
|
constructor(baseUrl, authToken = null) {
|
|
@@ -159,6 +161,12 @@ export class ApiClient {
|
|
|
159
161
|
if (hasForbiddenInlineSrc) {
|
|
160
162
|
throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
|
|
161
163
|
}
|
|
164
|
+
if (topSrc && !isV2ImageDownloadUrl(topSrc)) {
|
|
165
|
+
throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 src URL. Save is blocked.`);
|
|
166
|
+
}
|
|
167
|
+
if (propSrc && !isV2ImageDownloadUrl(propSrc)) {
|
|
168
|
+
throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 properties.src URL. Save is blocked.`);
|
|
169
|
+
}
|
|
162
170
|
|
|
163
171
|
const cleanedObj = { ...obj };
|
|
164
172
|
|
package/src/core/SaveManager.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Events } from './events/Events.js';
|
|
5
5
|
import { logMindmapCompoundDebug } from '../mindmap/MindmapCompoundContract.js';
|
|
6
|
+
import { isV2ImageDownloadUrl } from '../services/AssetUrlPolicy.js';
|
|
6
7
|
export class SaveManager {
|
|
7
8
|
constructor(eventBus, options = {}) {
|
|
8
9
|
this.eventBus = eventBus;
|
|
@@ -245,6 +246,12 @@ export class SaveManager {
|
|
|
245
246
|
if (/^data:/i.test(topSrc) || /^blob:/i.test(topSrc) || /^data:/i.test(propSrc) || /^blob:/i.test(propSrc)) {
|
|
246
247
|
throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
|
|
247
248
|
}
|
|
249
|
+
if (topSrc && !isV2ImageDownloadUrl(topSrc)) {
|
|
250
|
+
throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 src URL. Save is blocked.`);
|
|
251
|
+
}
|
|
252
|
+
if (propSrc && !isV2ImageDownloadUrl(propSrc)) {
|
|
253
|
+
throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 properties.src URL. Save is blocked.`);
|
|
254
|
+
}
|
|
248
255
|
}
|
|
249
256
|
}
|
|
250
257
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Events } from '../events/Events.js';
|
|
2
2
|
import { PasteObjectCommand } from '../commands/index.js';
|
|
3
3
|
import { RevitScreenshotMetadataService } from '../../services/RevitScreenshotMetadataService.js';
|
|
4
|
+
import { isV2ImageDownloadUrl } from '../../services/AssetUrlPolicy.js';
|
|
4
5
|
|
|
5
6
|
export function setupClipboardFlow(core) {
|
|
6
7
|
const revitMetadataService = new RevitScreenshotMetadataService(console);
|
|
@@ -21,7 +22,12 @@ export function setupClipboardFlow(core) {
|
|
|
21
22
|
|
|
22
23
|
const ensureServerImage = async ({ src, name, imageId }) => {
|
|
23
24
|
if (imageId) {
|
|
24
|
-
|
|
25
|
+
const serverUrl = typeof src === 'string' ? src.trim() : '';
|
|
26
|
+
if (!isV2ImageDownloadUrl(serverUrl)) {
|
|
27
|
+
alert('Некорректный адрес изображения. Изображение не добавлено.');
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return { src: serverUrl, name, imageId };
|
|
25
31
|
}
|
|
26
32
|
if (!core.imageUploadService) {
|
|
27
33
|
alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
|
|
@@ -39,8 +45,12 @@ export function setupClipboardFlow(core) {
|
|
|
39
45
|
const blob = await response.blob();
|
|
40
46
|
uploadResult = await core.imageUploadService.uploadImage(blob, name || 'clipboard-image');
|
|
41
47
|
}
|
|
48
|
+
const serverUrl = typeof uploadResult.url === 'string' ? uploadResult.url.trim() : '';
|
|
49
|
+
if (!isV2ImageDownloadUrl(serverUrl)) {
|
|
50
|
+
throw new Error('Сервер вернул некорректный URL изображения');
|
|
51
|
+
}
|
|
42
52
|
return {
|
|
43
|
-
src:
|
|
53
|
+
src: serverUrl,
|
|
44
54
|
name: uploadResult.name || name,
|
|
45
55
|
imageId: uploadResult.imageId || uploadResult.id
|
|
46
56
|
};
|
|
@@ -272,10 +282,12 @@ export function setupClipboardFlow(core) {
|
|
|
272
282
|
const img = new Image();
|
|
273
283
|
img.decoding = 'async';
|
|
274
284
|
img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
|
|
275
|
-
img.onerror = () => {
|
|
285
|
+
img.onerror = () => {
|
|
286
|
+
alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
|
|
287
|
+
};
|
|
276
288
|
img.src = uploaded.src;
|
|
277
289
|
} catch (_) {
|
|
278
|
-
|
|
290
|
+
alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
|
|
279
291
|
}
|
|
280
292
|
});
|
|
281
293
|
|
|
@@ -320,10 +332,12 @@ export function setupClipboardFlow(core) {
|
|
|
320
332
|
const img = new Image();
|
|
321
333
|
img.decoding = 'async';
|
|
322
334
|
img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
|
|
323
|
-
img.onerror = () => {
|
|
335
|
+
img.onerror = () => {
|
|
336
|
+
alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
|
|
337
|
+
};
|
|
324
338
|
img.src = uploaded.src;
|
|
325
339
|
} catch (_) {
|
|
326
|
-
|
|
340
|
+
alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
|
|
327
341
|
}
|
|
328
342
|
});
|
|
329
343
|
|
|
@@ -25,8 +25,6 @@ export function setupSaveFlow(core) {
|
|
|
25
25
|
core.eventBus.on(Events.Save.Success, async () => {
|
|
26
26
|
if (typeof core.revealPendingObjectsAfterSave === 'function') {
|
|
27
27
|
core.revealPendingObjectsAfterSave();
|
|
28
|
-
} else if (typeof core.revealPendingImageObjectsAfterSave === 'function') {
|
|
29
|
-
core.revealPendingImageObjectsAfterSave();
|
|
30
28
|
}
|
|
31
29
|
// ВРЕМЕННО ОТКЛЮЧЕНО:
|
|
32
30
|
// cleanup-фича требует доработки контракта и серверной поддержки.
|
package/src/core/index.js
CHANGED
|
@@ -465,11 +465,6 @@ export class CoreMoodBoard {
|
|
|
465
465
|
this._pendingPersistAckVisibilityIds.clear();
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
// Backward-compat alias for tests/integrations created in previous step.
|
|
469
|
-
revealPendingImageObjectsAfterSave() {
|
|
470
|
-
this.revealPendingObjectsAfterSave();
|
|
471
|
-
}
|
|
472
|
-
|
|
473
468
|
// === Прикрепления к фреймам ===
|
|
474
469
|
// Логика фреймов перенесена в FrameService
|
|
475
470
|
|
|
@@ -21,12 +21,10 @@ function resolveMoodboardApiBase(board) {
|
|
|
21
21
|
const raw = String(board?.options?.apiUrl || '').trim();
|
|
22
22
|
if (!raw) return '/api/v2/moodboard';
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (raw.endsWith('/api/moodboard/')) {
|
|
29
|
-
return raw.replace(/\/api\/moodboard\/$/, '/api/v2/moodboard/');
|
|
24
|
+
const hasLegacyPath = /\/api\/moodboard(?:\/|$)/i.test(raw);
|
|
25
|
+
const hasV2Path = /\/api\/v2\/moodboard(?:\/|$)/i.test(raw);
|
|
26
|
+
if (hasLegacyPath && !hasV2Path) {
|
|
27
|
+
throw new Error('Legacy apiUrl "/api/moodboard" is not supported. Use "/api/v2/moodboard".');
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
return raw;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function isV2ImageDownloadUrl(url) {
|
|
2
|
+
if (typeof url !== 'string') return false;
|
|
3
|
+
const raw = url.trim();
|
|
4
|
+
if (!raw) return false;
|
|
5
|
+
if (/^\/api\/v2\/images\/[^/]+\/download$/i.test(raw)) return true;
|
|
6
|
+
try {
|
|
7
|
+
const parsed = new URL(raw);
|
|
8
|
+
return /^\/api\/v2\/images\/[^/]+\/download$/i.test(parsed.pathname);
|
|
9
|
+
} catch (_) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isV2FileDownloadUrl(url) {
|
|
15
|
+
if (typeof url !== 'string') return false;
|
|
16
|
+
const raw = url.trim();
|
|
17
|
+
if (!raw) return false;
|
|
18
|
+
if (/^\/api\/v2\/files\/[^/]+\/download$/i.test(raw)) return true;
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(raw);
|
|
21
|
+
return /^\/api\/v2\/files\/[^/]+\/download$/i.test(parsed.pathname);
|
|
22
|
+
} catch (_) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isV2FileDownloadUrl } from './AssetUrlPolicy.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Сервис для загрузки и управления файлами на сервере
|
|
3
5
|
*/
|
|
@@ -92,10 +94,22 @@ export class FileUploadService {
|
|
|
92
94
|
throw new Error(result.message || 'Ошибка загрузки файла');
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
const fileId = result.data.fileId || result.data.id;
|
|
98
|
+
const serverUrl = typeof result.data.url === 'string' ? result.data.url.trim() : '';
|
|
99
|
+
if (!fileId) {
|
|
100
|
+
throw new Error('Сервер не вернул fileId.');
|
|
101
|
+
}
|
|
102
|
+
if (!isV2FileDownloadUrl(serverUrl)) {
|
|
103
|
+
throw new Error('Некорректный URL файла от сервера. Ожидается /api/v2/files/{fileId}/download');
|
|
104
|
+
}
|
|
105
|
+
if (!this._matchesFileIdInUrl(serverUrl, fileId)) {
|
|
106
|
+
throw new Error('fileId не совпадает с URL файла от сервера.');
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
return {
|
|
96
|
-
id:
|
|
97
|
-
fileId
|
|
98
|
-
url:
|
|
110
|
+
id: fileId, // Используем fileId как основное поле, id для обратной совместимости
|
|
111
|
+
fileId, // Добавляем fileId для явного доступа
|
|
112
|
+
url: serverUrl,
|
|
99
113
|
size: result.data.size,
|
|
100
114
|
name: result.data.name,
|
|
101
115
|
type: result.data.type
|
|
@@ -107,6 +121,23 @@ export class FileUploadService {
|
|
|
107
121
|
}
|
|
108
122
|
}
|
|
109
123
|
|
|
124
|
+
_matchesFileIdInUrl(url, fileId) {
|
|
125
|
+
const id = typeof fileId === 'string' ? fileId.trim() : '';
|
|
126
|
+
if (!id || typeof url !== 'string') return false;
|
|
127
|
+
const raw = url.trim();
|
|
128
|
+
const relativeMatch = raw.match(/^\/api\/v2\/files\/([^/]+)\/download$/i);
|
|
129
|
+
if (relativeMatch) {
|
|
130
|
+
return decodeURIComponent(relativeMatch[1]) === id;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const parsed = new URL(raw);
|
|
134
|
+
const absoluteMatch = parsed.pathname.match(/^\/api\/v2\/files\/([^/]+)\/download$/i);
|
|
135
|
+
return !!absoluteMatch && decodeURIComponent(absoluteMatch[1]) === id;
|
|
136
|
+
} catch (_) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
110
141
|
/**
|
|
111
142
|
* Обновляет метаданные файла на сервере
|
|
112
143
|
* @param {string} fileId - ID файла
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isV2ImageDownloadUrl } from './AssetUrlPolicy.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Сервис для загрузки и управления изображениями на сервере
|
|
3
5
|
*/
|
|
@@ -97,10 +99,22 @@ export class ImageUploadService {
|
|
|
97
99
|
throw new Error(result.message || 'Ошибка загрузки изображения');
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
const imageId = result.data.imageId || result.data.id;
|
|
103
|
+
const serverUrl = typeof result.data.url === 'string' ? result.data.url.trim() : '';
|
|
104
|
+
if (!imageId) {
|
|
105
|
+
throw new Error('Сервер не вернул imageId.');
|
|
106
|
+
}
|
|
107
|
+
if (!isV2ImageDownloadUrl(serverUrl)) {
|
|
108
|
+
throw new Error('Некорректный URL изображения от сервера. Ожидается /api/v2/images/{imageId}/download');
|
|
109
|
+
}
|
|
110
|
+
if (!this._matchesImageIdInUrl(serverUrl, imageId)) {
|
|
111
|
+
throw new Error('imageId не совпадает с URL изображения от сервера.');
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
return {
|
|
101
|
-
id:
|
|
102
|
-
imageId
|
|
103
|
-
url:
|
|
115
|
+
id: imageId, // Используем imageId как основное поле, id для обратной совместимости
|
|
116
|
+
imageId, // Добавляем imageId для явного доступа
|
|
117
|
+
url: serverUrl,
|
|
104
118
|
width: result.data.width,
|
|
105
119
|
height: result.data.height,
|
|
106
120
|
name: result.data.name,
|
|
@@ -113,6 +127,23 @@ export class ImageUploadService {
|
|
|
113
127
|
}
|
|
114
128
|
}
|
|
115
129
|
|
|
130
|
+
_matchesImageIdInUrl(url, imageId) {
|
|
131
|
+
const id = typeof imageId === 'string' ? imageId.trim() : '';
|
|
132
|
+
if (!id || typeof url !== 'string') return false;
|
|
133
|
+
const raw = url.trim();
|
|
134
|
+
const relativeMatch = raw.match(/^\/api\/v2\/images\/([^/]+)\/download$/i);
|
|
135
|
+
if (relativeMatch) {
|
|
136
|
+
return decodeURIComponent(relativeMatch[1]) === id;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const parsed = new URL(raw);
|
|
140
|
+
const absoluteMatch = parsed.pathname.match(/^\/api\/v2\/images\/([^/]+)\/download$/i);
|
|
141
|
+
return !!absoluteMatch && decodeURIComponent(absoluteMatch[1]) === id;
|
|
142
|
+
} catch (_) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
116
147
|
/**
|
|
117
148
|
* Загружает изображение из base64 DataURL
|
|
118
149
|
* @param {string} dataUrl - base64 DataURL
|