fix legacy chat attachment URLs

This commit is contained in:
richjohnrambo
2026-10-14 13:38:01 +08:00
parent dd77ba903b
commit 0570f2c5bd
3 changed files with 71 additions and 4 deletions
+5 -4
View File
@@ -14,6 +14,7 @@ import UploadingImage from '../widgets/uploading-image.jsx'
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO,
REM_SIZE, VIDEO_THUMBNAIL_WIDTH } from '../config.js';
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
import { normalizeFileReference } from './im-api.js';
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
import { cancelablePromise, sanitizeUrl, sanitizeUrlForMime } from './utils.js';
@@ -67,7 +68,7 @@ export function fullFormatter(style, data, values, key, stack) {
case 'AU':
// Show audio player.
if (attr.src) {
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'audio'));
attr.src = this.authorizeURL(normalizeFileReference(sanitizeUrlForMime(attr.src, 'audio')));
attr.duration = data.duration > 0 ? (data.duration | 0) : undefined;
attr.preview = data.preview;
attr.loading = 'lazy';
@@ -213,7 +214,7 @@ function handleImageData(el, data, attr) {
minHeight: dim.dstHeight + 'px'
};
if (!Drafty.isProcessing(data)) {
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
attr.src = this.authorizeURL(normalizeFileReference(sanitizeUrlForMime(attr.src, 'image')));
attr.alt = data.name;
if (attr.src) {
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
@@ -256,7 +257,7 @@ function handleVideoData(el, data, attr) {
minHeight: dim.dstHeight + 'px'
};
if (!Drafty.isProcessing(data)) {
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
attr.src = this.authorizeURL(normalizeFileReference(sanitizeUrlForMime(attr.src, 'image')));
attr.alt = data.name;
if (data.ref || data.val) {
attr.onClick = (e) => this.onHandleClick(e, 'video');
@@ -495,7 +496,7 @@ function quoteImageOrVideo(data, isVideo) {
}
promise = Promise.resolve(blob);
} else if (ref) {
promise = fetch(this.authorizeURL(sanitizeUrlForMime(ref, 'image')))
promise = fetch(this.authorizeURL(normalizeFileReference(sanitizeUrlForMime(ref, 'image'))))
.then(evt => {
if (evt.ok) {
return evt.blob();
+28
View File
@@ -117,6 +117,34 @@ function fileBase() {
return ((tenant && tenant.endpoints && tenant.endpoints.file) || DEFAULT_FILE_BASE).replace(/\/$/, '');
}
// Historical clients persisted absolute API-host file references in Drafty
// messages. Resolve those references through the tenant's configured file
// endpoint so the web client keeps the authenticated request on its same-origin
// proxy and does not forward custom auth headers into an OSS redirect preflight.
export function normalizeFileReferenceAgainst(ref, apiEndpoint, fileEndpoint, pageOrigin) {
if (!ref || typeof ref !== 'string') {
return ref;
}
try {
const source = new URL(ref, pageOrigin);
const api = new URL(apiEndpoint, pageOrigin);
if (source.origin !== api.origin || !source.pathname.startsWith('/v0/file/')) {
return ref;
}
const target = new URL(fileEndpoint, pageOrigin);
target.pathname = target.pathname.replace(/\/$/, '') + source.pathname.slice('/v0/file'.length);
target.search = source.search;
target.hash = source.hash;
return target.origin === pageOrigin ? target.pathname + target.search + target.hash : target.toString();
} catch (e) {
return ref;
}
}
export function normalizeFileReference(ref) {
return normalizeFileReferenceAgainst(ref, apiBase(), fileBase(), window.location.origin);
}
// tinode-web.jsx always sets this on login/reconnect (sessionStorage, tab-lifetime), independent of
// whether the user opted into "keep logged in" (which only controls the separate localStorage token
// used for surviving a browser restart). REST calls need a token for the current session regardless.
+38
View File
@@ -0,0 +1,38 @@
import { normalizeFileReferenceAgainst } from './im-api';
const PAGE_ORIGIN = 'https://web.118im.com';
const API_ENDPOINT = 'https://api.118im.com/v1/im';
const FILE_ENDPOINT = 'https://web.118im.com/v0/file';
test('normalizes historical API-host file references to the same-origin file endpoint', () => {
expect(normalizeFileReferenceAgainst(
'https://api.118im.com/v0/file/s/example.jpg?size=large#preview',
API_ENDPOINT,
FILE_ENDPOINT,
PAGE_ORIGIN
)).toBe('/v0/file/s/example.jpg?size=large#preview');
});
test('leaves current and unrelated file references unchanged', () => {
expect(normalizeFileReferenceAgainst(
'/v0/file/s/current.jpg',
API_ENDPOINT,
FILE_ENDPOINT,
PAGE_ORIGIN
)).toBe('/v0/file/s/current.jpg');
expect(normalizeFileReferenceAgainst(
'https://cdn.example.com/image.jpg',
API_ENDPOINT,
FILE_ENDPOINT,
PAGE_ORIGIN
)).toBe('https://cdn.example.com/image.jpg');
});
test('uses an external configured file endpoint when same-origin proxy is unavailable', () => {
expect(normalizeFileReferenceAgainst(
'https://api.118im.com/v0/file/s/example.jpg',
API_ENDPOINT,
'https://files.example.com/v0/file',
PAGE_ORIGIN
)).toBe('https://files.example.com/v0/file/s/example.jpg');
});