mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post form): reject non-media file links
This commit is contained in:
@@ -319,13 +319,19 @@ vi.mock('../../../lib/utils/media-utils', () => ({
|
||||
if (link.endsWith('.gif')) {
|
||||
return { type: 'gif', url: link };
|
||||
}
|
||||
if (link.endsWith('.png')) {
|
||||
if (link.endsWith('.jpg') || link.endsWith('.jpeg') || link.endsWith('.png')) {
|
||||
return { type: 'image', url: link };
|
||||
}
|
||||
if (link.endsWith('.mp4')) {
|
||||
return { type: 'video', url: link };
|
||||
}
|
||||
return { type: 'link', url: link };
|
||||
if (link.endsWith('.mp3')) {
|
||||
return { type: 'audio', url: link };
|
||||
}
|
||||
if (link.includes('youtube.com')) {
|
||||
return { patternThumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg', type: 'iframe', url: link };
|
||||
}
|
||||
return { type: 'webpage', url: link };
|
||||
},
|
||||
getYouTubeThumbnailUrlFromLink: (link: string) => {
|
||||
try {
|
||||
@@ -617,6 +623,12 @@ describe('PostForm', () => {
|
||||
expect(globalThis.alert).not.toHaveBeenCalled();
|
||||
expect(container.textContent).toContain('error: invalid_url_alert');
|
||||
|
||||
await dispatchInput(linkInput as HTMLInputElement, 'https://example.com/page');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
expect(globalThis.alert).not.toHaveBeenCalled();
|
||||
expect(container.textContent).toContain('error: link_not_image_or_video_alert');
|
||||
expect(testState.publishPostMock).not.toHaveBeenCalled();
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'A valid body');
|
||||
await dispatchInput(linkInput as HTMLInputElement, 'https://i.4cdn.org/gif/file.jpg');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
@@ -668,6 +680,7 @@ describe('PostForm', () => {
|
||||
await dispatchInput(linkInput, youtubeLink);
|
||||
|
||||
expect(linkInput.value).toBe(youtubeLink);
|
||||
expect(linkInput.disabled).toBe(true);
|
||||
expect(container.textContent).toContain('youtube_thumbnail_link_conversion_notice');
|
||||
expect(container.textContent).toContain('3');
|
||||
expect(table.textContent).not.toContain('youtube_thumbnail_link_conversion_notice');
|
||||
@@ -677,17 +690,20 @@ describe('PostForm', () => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(container.textContent).toContain('2');
|
||||
expect(linkInput.disabled).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(container.textContent).toContain('1');
|
||||
expect(linkInput.disabled).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
expect(linkInput.value).toBe(thumbnailLink);
|
||||
expect(linkInput.disabled).toBe(false);
|
||||
expect(textarea.value).toBe(youtubeLink);
|
||||
expect(container.textContent).not.toContain('youtube_thumbnail_link_conversion_notice');
|
||||
} finally {
|
||||
@@ -1433,16 +1449,34 @@ describe('LinkTypePreviewer', () => {
|
||||
await act(async () => {
|
||||
root.render(createElement(LinkTypePreviewer, { link: 'https://example.com/file.gif' }));
|
||||
});
|
||||
expect(container.textContent).toBe('type: animated_gif');
|
||||
expect(container.textContent).toBe('file: animated_gif');
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(LinkTypePreviewer, { link: 'https://example.com/file.mp4' }));
|
||||
});
|
||||
expect(container.textContent).toBe('type: video');
|
||||
expect(container.textContent).toBe('file: video');
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(LinkTypePreviewer, { link: 'not-a-url' }));
|
||||
});
|
||||
expect(container.textContent).toBe('invalid_url');
|
||||
});
|
||||
|
||||
it('shows unsupported file links as not a file on media-only forms', async () => {
|
||||
await act(async () => {
|
||||
root.render(createElement(LinkTypePreviewer, { link: 'https://example.com/file.mp3', requireFile: true }));
|
||||
});
|
||||
expect(container.textContent).toBe('not_a_file');
|
||||
expect(container.querySelector('span')?.className).toContain('linkTypeError');
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(LinkTypePreviewer, { link: 'https://www.youtube.com/watch?v=abc123', requireFile: true }));
|
||||
});
|
||||
expect(container.textContent).toBe('not_a_file');
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(LinkTypePreviewer, { link: 'https://example.com/page', requireFile: true }));
|
||||
});
|
||||
expect(container.textContent).toBe('not_a_file');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.linkTypeError {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.linkField input[type="text"] {
|
||||
width: 190px !important;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import debounce from 'lodash/debounce';
|
||||
|
||||
const FILE_LINK_PLACEHOLDER = 'https://website.com/image.jpg';
|
||||
const POST_FORM_FILE_DISPLAY_MAX_LENGTH = 28;
|
||||
const POST_FORM_FILE_MEDIA_TYPES = new Set(['gif', 'image', 'video']);
|
||||
|
||||
const mergeFlairs = (...flairGroups: Array<Comment['flairs'] | undefined>): Comment['flairs'] | undefined => {
|
||||
const flairs = flairGroups.flatMap((group) => (Array.isArray(group) ? group : []));
|
||||
@@ -67,12 +68,20 @@ const getPostFormFileDisplayLabel = (url: string, uploadedFileName: string | nul
|
||||
return truncateWithEllipsisInMiddle(raw, POST_FORM_FILE_DISPLAY_MAX_LENGTH);
|
||||
};
|
||||
|
||||
export const LinkTypePreviewer = ({ link }: { link: string }) => {
|
||||
const isPostFormFileMediaType = (type: string | undefined): boolean => Boolean(type && POST_FORM_FILE_MEDIA_TYPES.has(type));
|
||||
|
||||
const isPostFormFileMediaLink = (link: string): boolean => isPostFormFileMediaType(getLinkMediaInfo(link)?.type);
|
||||
|
||||
export const LinkTypePreviewer = ({ link, requireFile = false }: { link: string; requireFile?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const mediaInfo = getLinkMediaInfo(link);
|
||||
let type = mediaInfo?.type;
|
||||
const { status: gifFrameStatus } = useFetchGifFirstFrame(type === 'gif' ? mediaInfo?.url : undefined);
|
||||
|
||||
if (requireFile && isValidURL(link) && !isPostFormFileMediaType(type)) {
|
||||
return <span className={styles.linkTypeError}>{t('not_a_file')}</span>;
|
||||
}
|
||||
|
||||
if (type === 'gif' && gifFrameStatus === 'ready') {
|
||||
type = t('animated_gif');
|
||||
} else if (type === 'gif') {
|
||||
@@ -81,7 +90,7 @@ export const LinkTypePreviewer = ({ link }: { link: string }) => {
|
||||
type = getDisplayMediaInfoType(type, t);
|
||||
}
|
||||
|
||||
return isValidURL(link) ? `type: ${type}` : t('invalid_url');
|
||||
return isValidURL(link) ? `${t('file')}: ${type}` : t('invalid_url');
|
||||
};
|
||||
|
||||
const PostFormActions = ({
|
||||
@@ -149,6 +158,7 @@ interface PostFormFieldsProps {
|
||||
handleContentValueChange: (content: string, options?: string) => void;
|
||||
handleLinkChange: (link: string) => void;
|
||||
handleOptionsChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
disableLinkInput: boolean;
|
||||
setPublishPostOptions: (opts: Record<string, unknown>) => void;
|
||||
setPublishReplyOptions: (opts: Record<string, unknown>) => void;
|
||||
isUploading: boolean;
|
||||
@@ -200,6 +210,7 @@ const PostFormFields = ({
|
||||
handleContentValueChange,
|
||||
handleLinkChange,
|
||||
handleOptionsChange,
|
||||
disableLinkInput,
|
||||
setPublishPostOptions,
|
||||
setPublishReplyOptions,
|
||||
isUploading,
|
||||
@@ -357,12 +368,12 @@ const PostFormFields = ({
|
||||
spellCheck='false'
|
||||
placeholder={requirePostLinkIsMedia ? FILE_LINK_PLACEHOLDER : undefined}
|
||||
ref={urlRef}
|
||||
disabled={isUploading}
|
||||
disabled={disableLinkInput}
|
||||
onChange={(e) => {
|
||||
handleLinkChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.linkType}> {url && <LinkTypePreviewer link={url} />}</span>
|
||||
<span className={styles.linkType}> {url && <LinkTypePreviewer link={url} requireFile={requirePostLinkIsMedia} />}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{showUploadControls && (
|
||||
@@ -633,6 +644,10 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
setFormError(`${t('error')}: ${t('invalid_url_alert')}`);
|
||||
return;
|
||||
}
|
||||
if (currentUrl && requirePostLinkIsMedia && !isPostFormFileMediaLink(currentUrl)) {
|
||||
setFormError(`${t('error')}: ${t('link_not_image_or_video_alert')}`);
|
||||
return;
|
||||
}
|
||||
const expiringMediaLinkAlert = currentUrl ? getExpiringMediaLinkAlert(currentUrl, t) : null;
|
||||
if (expiringMediaLinkAlert) {
|
||||
setFormError(expiringMediaLinkAlert);
|
||||
@@ -757,6 +772,10 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
setFormError(`${t('error')}: ${t('invalid_url_alert')}`);
|
||||
return;
|
||||
}
|
||||
if (currentUrl && requirePostLinkIsMedia && !isPostFormFileMediaLink(currentUrl)) {
|
||||
setFormError(`${t('error')}: ${t('link_not_image_or_video_alert')}`);
|
||||
return;
|
||||
}
|
||||
const expiringMediaLinkAlert = currentUrl ? getExpiringMediaLinkAlert(currentUrl, t) : null;
|
||||
if (expiringMediaLinkAlert) {
|
||||
setFormError(expiringMediaLinkAlert);
|
||||
@@ -873,6 +892,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
handleContentValueChange={handleContentValueChange}
|
||||
handleLinkChange={handleLinkChange}
|
||||
handleOptionsChange={handleOptionsChange}
|
||||
disableLinkInput={isUploading || youtubeThumbnailConversionCountdown !== null}
|
||||
setPublishPostOptions={setPublishPostOptions}
|
||||
setPublishReplyOptions={setPublishReplyOptions}
|
||||
isUploading={isUploading}
|
||||
|
||||
Reference in New Issue
Block a user