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,
|
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO,
|
||||||
REM_SIZE, VIDEO_THUMBNAIL_WIDTH } from '../config.js';
|
REM_SIZE, VIDEO_THUMBNAIL_WIDTH } from '../config.js';
|
||||||
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||||
|
import { normalizeFileReference } from './im-api.js';
|
||||||
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
|
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
|
||||||
import { cancelablePromise, sanitizeUrl, sanitizeUrlForMime } from './utils.js';
|
import { cancelablePromise, sanitizeUrl, sanitizeUrlForMime } from './utils.js';
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export function fullFormatter(style, data, values, key, stack) {
|
|||||||
case 'AU':
|
case 'AU':
|
||||||
// Show audio player.
|
// Show audio player.
|
||||||
if (attr.src) {
|
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.duration = data.duration > 0 ? (data.duration | 0) : undefined;
|
||||||
attr.preview = data.preview;
|
attr.preview = data.preview;
|
||||||
attr.loading = 'lazy';
|
attr.loading = 'lazy';
|
||||||
@@ -213,7 +214,7 @@ function handleImageData(el, data, attr) {
|
|||||||
minHeight: dim.dstHeight + 'px'
|
minHeight: dim.dstHeight + 'px'
|
||||||
};
|
};
|
||||||
if (!Drafty.isProcessing(data)) {
|
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;
|
attr.alt = data.name;
|
||||||
if (attr.src) {
|
if (attr.src) {
|
||||||
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
|
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'
|
minHeight: dim.dstHeight + 'px'
|
||||||
};
|
};
|
||||||
if (!Drafty.isProcessing(data)) {
|
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;
|
attr.alt = data.name;
|
||||||
if (data.ref || data.val) {
|
if (data.ref || data.val) {
|
||||||
attr.onClick = (e) => this.onHandleClick(e, 'video');
|
attr.onClick = (e) => this.onHandleClick(e, 'video');
|
||||||
@@ -495,7 +496,7 @@ function quoteImageOrVideo(data, isVideo) {
|
|||||||
}
|
}
|
||||||
promise = Promise.resolve(blob);
|
promise = Promise.resolve(blob);
|
||||||
} else if (ref) {
|
} else if (ref) {
|
||||||
promise = fetch(this.authorizeURL(sanitizeUrlForMime(ref, 'image')))
|
promise = fetch(this.authorizeURL(normalizeFileReference(sanitizeUrlForMime(ref, 'image'))))
|
||||||
.then(evt => {
|
.then(evt => {
|
||||||
if (evt.ok) {
|
if (evt.ok) {
|
||||||
return evt.blob();
|
return evt.blob();
|
||||||
|
|||||||
@@ -117,6 +117,34 @@ function fileBase() {
|
|||||||
return ((tenant && tenant.endpoints && tenant.endpoints.file) || DEFAULT_FILE_BASE).replace(/\/$/, '');
|
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
|
// 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
|
// 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.
|
// 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