fix legacy chat attachment URLs
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
Reference in New Issue
Block a user