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:
Generated
+15
@@ -14,6 +14,7 @@
|
||||
"altcha-lib": "^2.7.0",
|
||||
"axios": "^1.20.0",
|
||||
"dayjs": "^1.11.23",
|
||||
"escape-html": "^1.0.3",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"pinia": "^4.0.3",
|
||||
"pretty-bytes": "^7.2.0",
|
||||
@@ -22,6 +23,7 @@
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/escape-html": "^1.0.4",
|
||||
"@types/node": "^22.10.7",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
@@ -1717,6 +1719,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/escape-html": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz",
|
||||
"integrity": "sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
@@ -2454,6 +2463,12 @@
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"altcha-lib": "^2.7.0",
|
||||
"axios": "^1.20.0",
|
||||
"dayjs": "^1.11.23",
|
||||
"escape-html": "^1.0.3",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"pinia": "^4.0.3",
|
||||
"pretty-bytes": "^7.2.0",
|
||||
@@ -24,6 +25,7 @@
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/escape-html": "^1.0.4",
|
||||
"@types/node": "^22.10.7",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
|
||||
@@ -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