Add share URL builders and embed format helpers for uploaded images.
Centralize clearnet/onion paths, delete links, and BBCode/Markdown/HTML output with unit tests and escape-html for safe HTML snippets.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export type ShareNetworkTab = 'clearnet' | 'onion';
|
||||
@@ -0,0 +1,25 @@
|
||||
export type ShareNetworkPair = {
|
||||
clearnet: string;
|
||||
onion: string;
|
||||
};
|
||||
|
||||
type ShareUrlsCore = {
|
||||
viewer: ShareNetworkPair;
|
||||
delete: ShareNetworkPair;
|
||||
};
|
||||
|
||||
export type HotlinkShareUrls = ShareUrlsCore & {
|
||||
mode: 'hotlink';
|
||||
direct: ShareNetworkPair;
|
||||
bbcode: string;
|
||||
markdownImage: string;
|
||||
htmlImage: string;
|
||||
};
|
||||
|
||||
export type ZkShareUrls = ShareUrlsCore & {
|
||||
mode: 'zk';
|
||||
markdownLink: string;
|
||||
htmlLink: string;
|
||||
};
|
||||
|
||||
export type UploadedShareUrls = HotlinkShareUrls | ZkShareUrls;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildBbcodeImage } from '@/utils/share/buildBbcodeImage';
|
||||
|
||||
describe('buildBbcodeImage', () => {
|
||||
it('wraps the image URL in BBCode img tags', () => {
|
||||
expect(buildBbcodeImage('https://example.com/i/1/secret')).toBe(
|
||||
'[img]https://example.com/i/1/secret[/img]'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export const buildBbcodeImage = (imageUrl: string): string => `[img]${imageUrl}[/img]`;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildDeletePath } from '@/utils/share/buildDeletePath';
|
||||
|
||||
describe('buildDeletePath', () => {
|
||||
it('builds delete route with encoded token', () => {
|
||||
expect(buildDeletePath('upload-id', 'abc+def/token')).toBe('/d/upload-id?token=abc%2Bdef%2Ftoken');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export const buildDeletePath = (uploadId: string, deleteToken: string): string =>
|
||||
`/d/${uploadId}?token=${encodeURIComponent(deleteToken)}`;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildHtmlImage } from '@/utils/share/buildHtmlImage';
|
||||
|
||||
describe('buildHtmlImage', () => {
|
||||
it('builds an img tag with escaped alt', () => {
|
||||
expect(buildHtmlImage('https://example.com/i/1/secret', 'photo.jpg')).toBe(
|
||||
'<img src="https://example.com/i/1/secret" alt="photo.jpg" />'
|
||||
);
|
||||
});
|
||||
|
||||
it('escapes quotes and markup in alt text', () => {
|
||||
expect(buildHtmlImage('https://example.com/i/1/secret', 'x" onerror="alert(1)')).toBe(
|
||||
'<img src="https://example.com/i/1/secret" alt="x" onerror="alert(1)" />'
|
||||
);
|
||||
});
|
||||
|
||||
it('escapes special characters in src when present', () => {
|
||||
expect(buildHtmlImage('https://example.com/i/1/"bad', 'image')).toBe(
|
||||
'<img src="https://example.com/i/1/"bad" alt="image" />'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
export const buildHtmlImage = (imageUrl: string, alt = 'image'): string =>
|
||||
`<img src="${escapeHtml(imageUrl)}" alt="${escapeHtml(alt)}" />`;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildHtmlLink } from '@/utils/share/buildHtmlLink';
|
||||
|
||||
describe('buildHtmlLink', () => {
|
||||
it('builds an anchor with escaped href and label', () => {
|
||||
expect(buildHtmlLink('photo.jpg', 'https://example.com/v/1#key')).toBe(
|
||||
'<a href="https://example.com/v/1#key">photo.jpg</a>'
|
||||
);
|
||||
});
|
||||
|
||||
it('escapes HTML in link label', () => {
|
||||
expect(buildHtmlLink('<script>alert(1)</script>', 'https://example.com/v/1')).toBe(
|
||||
'<a href="https://example.com/v/1"><script>alert(1)</script></a>'
|
||||
);
|
||||
});
|
||||
|
||||
it('escapes quotes in href', () => {
|
||||
expect(buildHtmlLink('label', 'https://example.com/x" y')).toBe(
|
||||
'<a href="https://example.com/x" y">label</a>'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
export const buildHtmlLink = (label: string, url: string): string =>
|
||||
`<a href="${escapeHtml(url)}">${escapeHtml(label)}</a>`;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildMarkdownImage } from '@/utils/share/buildMarkdownImage';
|
||||
|
||||
describe('buildMarkdownImage', () => {
|
||||
it('builds a markdown image with alt and url', () => {
|
||||
expect(buildMarkdownImage('https://example.com/i/1/secret', 'photo.jpg')).toBe(
|
||||
''
|
||||
);
|
||||
});
|
||||
|
||||
it('uses default alt when omitted', () => {
|
||||
expect(buildMarkdownImage('https://example.com/i/1/secret')).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export const buildMarkdownImage = (imageUrl: string, alt = 'image'): string => ``;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildMarkdownLink } from '@/utils/share/buildMarkdownLink';
|
||||
|
||||
describe('buildMarkdownLink', () => {
|
||||
it('builds a markdown link with label and url', () => {
|
||||
expect(buildMarkdownLink('photo.jpg', 'https://example.com/v/1#key')).toBe(
|
||||
'[photo.jpg](https://example.com/v/1#key)'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export const buildMarkdownLink = (label: string, url: string): string => `[${label}](${url})`;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildSiteShareUrl } from '@/utils/share/buildSiteShareUrl';
|
||||
|
||||
describe('buildSiteShareUrl', () => {
|
||||
it('appends path and hash to origin', () => {
|
||||
expect(buildSiteShareUrl('https://example.com', '/v/abc#key')).toBe('https://example.com/v/abc#key');
|
||||
});
|
||||
|
||||
it('handles path without hash', () => {
|
||||
expect(buildSiteShareUrl('http://localhost:5173', '/i/id/secret')).toBe(
|
||||
'http://localhost:5173/i/id/secret'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export const buildSiteShareUrl = (origin: string, pathWithOptionalHash: string): string => {
|
||||
const hashIndex = pathWithOptionalHash.indexOf('#');
|
||||
const pathname = hashIndex >= 0 ? pathWithOptionalHash.slice(0, hashIndex) : pathWithOptionalHash;
|
||||
const hash = hashIndex >= 0 ? pathWithOptionalHash.slice(hashIndex) : '';
|
||||
|
||||
return `${origin}${pathname}${hash}`;
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { UploadedShareUrls } from '@/types/share/UploadedShareUrls';
|
||||
import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry';
|
||||
import { buildBbcodeImage } from '@/utils/share/buildBbcodeImage';
|
||||
import { buildHtmlImage } from '@/utils/share/buildHtmlImage';
|
||||
import { buildHtmlLink } from '@/utils/share/buildHtmlLink';
|
||||
import { buildMarkdownImage } from '@/utils/share/buildMarkdownImage';
|
||||
import { buildMarkdownLink } from '@/utils/share/buildMarkdownLink';
|
||||
import { config } from '@/config';
|
||||
import { buildDeletePath } from '@/utils/share/buildDeletePath';
|
||||
import { buildHotlinkImagePath } from '@/utils/share/buildHotlinkImagePath';
|
||||
import { buildSiteShareUrl } from '@/utils/share/buildSiteShareUrl';
|
||||
import { buildZkViewerPath } from '@/utils/share/buildZkViewerPath';
|
||||
|
||||
const { clearnet, onion } = config.shareOrigins;
|
||||
|
||||
export const buildUploadedShareUrls = (
|
||||
upload: UploadQueueEntry,
|
||||
fileLabel: string
|
||||
): UploadedShareUrls | null => {
|
||||
if (upload.status !== 'uploaded') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deletePath = buildDeletePath(upload.uploadId, upload.deleteToken);
|
||||
const viewerPath = buildZkViewerPath(upload.uploadId, upload.urlSecret);
|
||||
|
||||
const viewer = {
|
||||
clearnet: buildSiteShareUrl(clearnet, viewerPath),
|
||||
onion: buildSiteShareUrl(onion, viewerPath)
|
||||
};
|
||||
|
||||
const deleteUrls = {
|
||||
clearnet: buildSiteShareUrl(clearnet, deletePath),
|
||||
onion: buildSiteShareUrl(onion, deletePath)
|
||||
};
|
||||
|
||||
const core = { viewer, delete: deleteUrls };
|
||||
|
||||
if (upload.mode === 'hotlink') {
|
||||
const directPath = buildHotlinkImagePath(upload.uploadId, upload.urlSecret);
|
||||
|
||||
const direct = {
|
||||
clearnet: buildSiteShareUrl(clearnet, directPath),
|
||||
onion: buildSiteShareUrl(onion, directPath)
|
||||
};
|
||||
|
||||
return {
|
||||
mode: 'hotlink',
|
||||
...core,
|
||||
direct,
|
||||
bbcode: buildBbcodeImage(direct.clearnet),
|
||||
markdownImage: buildMarkdownImage(direct.clearnet, fileLabel),
|
||||
htmlImage: buildHtmlImage(direct.clearnet, fileLabel)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'zk',
|
||||
...core,
|
||||
markdownLink: buildMarkdownLink(fileLabel, viewer.clearnet),
|
||||
htmlLink: buildHtmlLink(fileLabel, viewer.clearnet)
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user