mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(publishing): normalize media links to https
This commit is contained in:
@@ -5,7 +5,7 @@ import { Comment, setAccount, useAccount, useEditedComment } from '@bitsocial/bi
|
||||
import getShortAddress from '../../lib/get-short-address';
|
||||
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||
import { getLinkMediaInfo } from '../../lib/utils/media-utils';
|
||||
import { isValidURL } from '../../lib/utils/url-utils';
|
||||
import { isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
|
||||
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
||||
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
||||
@@ -220,7 +220,11 @@ const PostFormFields = ({
|
||||
disabled={isUploading}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
isInPostView ? setPublishReplyOptions({ link: e.target.value }) : setPublishPostOptions({ link: e.target.value });
|
||||
if (isInPostView) {
|
||||
setPublishReplyOptions({ link: e.target.value });
|
||||
} else {
|
||||
setPublishPostOptions({ link: e.target.value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className={styles.linkType}> {url && <LinkTypePreviewer link={url} />}</span>
|
||||
@@ -364,7 +368,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
alert(t('empty_comment_alert'));
|
||||
return;
|
||||
}
|
||||
if (currentUrl && !isValidURL(currentUrl)) {
|
||||
if (currentUrl && !isValidPublishURL(currentUrl)) {
|
||||
alert(t('invalid_url_alert'));
|
||||
return;
|
||||
}
|
||||
@@ -400,7 +404,11 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
|
||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const content = e.target.value;
|
||||
isInPostView ? setPublishReplyOptions({ content }) : setPublishPostOptions({ content });
|
||||
if (isInPostView) {
|
||||
setPublishReplyOptions({ content });
|
||||
} else {
|
||||
setPublishPostOptions({ content });
|
||||
}
|
||||
checkContentLength(content, t);
|
||||
};
|
||||
|
||||
@@ -416,7 +424,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentUrl && !isValidURL(currentUrl)) {
|
||||
if (currentUrl && !isValidPublishURL(currentUrl)) {
|
||||
alert(t('invalid_url_alert'));
|
||||
return;
|
||||
}
|
||||
@@ -444,7 +452,11 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
if (urlRef.current) {
|
||||
urlRef.current.value = uploadedUrl;
|
||||
}
|
||||
isInPostView ? setPublishReplyOptions({ link: uploadedUrl }) : setPublishPostOptions({ link: uploadedUrl });
|
||||
if (isInPostView) {
|
||||
setPublishReplyOptions({ link: uploadedUrl });
|
||||
} else {
|
||||
setPublishPostOptions({ link: uploadedUrl });
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { setAccount, useAccount } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { isValidURL } from '../../lib/utils/url-utils';
|
||||
import { isValidPublishURL } from '../../lib/utils/url-utils';
|
||||
import { isAllView, isModView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import useSelectedTextStore from '../../stores/use-selected-text-store';
|
||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||
@@ -84,7 +84,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentUrl && !isValidURL(currentUrl)) {
|
||||
if (currentUrl && !isValidPublishURL(currentUrl)) {
|
||||
setError(t('error') + ': ' + t('invalid_url_alert'));
|
||||
return;
|
||||
}
|
||||
@@ -154,7 +154,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const parentCidRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (parentCidRef.current && parentCidRef.current) {
|
||||
if (parentCidRef.current) {
|
||||
const cidWidth = parentCidRef.current.offsetWidth;
|
||||
parentCidRef.current.style.width = `${cidWidth}px`;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,16 @@ vi.mock('../clipboard-utils', () => ({
|
||||
copyToClipboard: (text: string) => testState.copyToClipboardMock(text),
|
||||
}));
|
||||
|
||||
import { copyShareLinkToClipboard, getHostname, is5chanLink, isValidCrossboardPattern, isValidURL, transform5chanLinkToInternal } from '../url-utils';
|
||||
import {
|
||||
copyShareLinkToClipboard,
|
||||
getHostname,
|
||||
is5chanLink,
|
||||
isValidCrossboardPattern,
|
||||
isValidPublishURL,
|
||||
isValidURL,
|
||||
normalizePublishURL,
|
||||
transform5chanLinkToInternal,
|
||||
} from '../url-utils';
|
||||
|
||||
describe('url-utils', () => {
|
||||
beforeEach(() => {
|
||||
@@ -22,6 +31,15 @@ describe('url-utils', () => {
|
||||
expect(isValidURL('not-a-url')).toBe(false);
|
||||
});
|
||||
|
||||
it('normalizes publish links to the https URLs accepted by communities', () => {
|
||||
expect(normalizePublishURL(' http://i.imgur.com/YpB7qfa.jpg ')).toBe('https://i.imgur.com/YpB7qfa.jpg');
|
||||
expect(normalizePublishURL('https://i.imgur.com/YpB7qfa.jpg')).toBe('https://i.imgur.com/YpB7qfa.jpg');
|
||||
expect(isValidPublishURL('http://i.imgur.com/YpB7qfa.jpg')).toBe(true);
|
||||
expect(isValidPublishURL('https://i.imgur.com/YpB7qfa.jpg')).toBe(true);
|
||||
expect(isValidPublishURL('ftp://example.com/file.jpg')).toBe(false);
|
||||
expect(isValidPublishURL('not-a-url')).toBe(false);
|
||||
});
|
||||
|
||||
it('copies share links for threads and catalog pages using the production fallback base url', async () => {
|
||||
await copyShareLinkToClipboard('music.eth', 'thread', 'cid-123');
|
||||
expect(testState.copyToClipboardMock).toHaveBeenCalledWith('https://5chan.app/#/music.eth/thread/cid-123');
|
||||
|
||||
@@ -5,7 +5,7 @@ export const QUOTE_NUMBER_REGEX = /(?<![>/\w])>>(\d+)/g;
|
||||
export const getHostname = (url: string) => {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/^www\./, '');
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
@@ -19,6 +19,30 @@ export const isValidURL = (url: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizePublishURL = (url: string) => {
|
||||
const trimmedUrl = url.trim();
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(trimmedUrl);
|
||||
if (parsedUrl.protocol === 'http:') {
|
||||
parsedUrl.protocol = 'https:';
|
||||
return parsedUrl.toString();
|
||||
}
|
||||
} catch {
|
||||
return trimmedUrl;
|
||||
}
|
||||
|
||||
return trimmedUrl;
|
||||
};
|
||||
|
||||
export const isValidPublishURL = (url: string) => {
|
||||
try {
|
||||
return new URL(normalizePublishURL(url)).protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const CHAN_5_HOSTNAMES = ['5chan.app', '5chan.eth.limo', '5chan.eth.link', '5chan.eth.sucks', '5chan.netlify.app'];
|
||||
|
||||
function getShareBaseUrl(): string {
|
||||
|
||||
@@ -66,6 +66,19 @@ describe('publish stores', () => {
|
||||
expect(usePublishPostStore.getState().title).toBeUndefined();
|
||||
});
|
||||
|
||||
it('normalizes post publish links to https before building the payload', () => {
|
||||
usePublishPostStore.getState().setPublishPostStore({
|
||||
content: 'post body',
|
||||
link: 'http://i.imgur.com/YpB7qfa.jpg',
|
||||
communityAddress: 'history-posting.bso',
|
||||
title: 'Hello',
|
||||
});
|
||||
|
||||
const state = usePublishPostStore.getState();
|
||||
expect(state.link).toBe('https://i.imgur.com/YpB7qfa.jpg');
|
||||
expect(state.publishCommentOptions.link).toBe('https://i.imgur.com/YpB7qfa.jpg');
|
||||
});
|
||||
|
||||
it('usePublishReplyStore stores reply data per parentCid and resets a single thread', () => {
|
||||
const comment: PublishReplyInput = {
|
||||
author: { address: '0x123', displayName: 'Author Name', role: 'mod' },
|
||||
@@ -97,4 +110,17 @@ describe('publish stores', () => {
|
||||
expect(usePublishReplyStore.getState().publishCommentOptions['parent-1']).toBeUndefined();
|
||||
expect(usePublishReplyStore.getState().content['parent-1']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('normalizes reply publish links to https before building the payload', () => {
|
||||
usePublishReplyStore.getState().setPublishReplyStore({
|
||||
content: 'reply body',
|
||||
link: 'http://i.imgur.com/YpB7qfa.jpg',
|
||||
parentCid: 'parent-1',
|
||||
communityAddress: 'history-posting.bso',
|
||||
});
|
||||
|
||||
const state = usePublishReplyStore.getState();
|
||||
expect(state.link['parent-1']).toBe('https://i.imgur.com/YpB7qfa.jpg');
|
||||
expect(state.publishCommentOptions['parent-1']?.link).toBe('https://i.imgur.com/YpB7qfa.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ChallengeVerification, Comment, PublishCommentOptions } from '@bitsocia
|
||||
import { create } from 'zustand';
|
||||
import { alertChallengeVerificationFailed } from '../lib/utils/challenge-utils';
|
||||
import { getCommentCommunityAddress } from '../lib/utils/comment-utils';
|
||||
import { normalizePublishURL } from '../lib/utils/url-utils';
|
||||
|
||||
type SubmitState = {
|
||||
author?: any | undefined;
|
||||
@@ -27,7 +28,8 @@ const usePublishPostStore = create<SubmitState>((set) => ({
|
||||
publishCommentOptions: {},
|
||||
setPublishPostStore: (comment: Comment) =>
|
||||
set(() => {
|
||||
const { author, content, link, spoiler, title } = comment;
|
||||
const { author, content, spoiler, title } = comment;
|
||||
const link = comment.link ? normalizePublishURL(comment.link) : undefined;
|
||||
const communityAddress = getCommentCommunityAddress(comment);
|
||||
|
||||
const displayName = 'displayName' in comment ? comment.displayName || undefined : author?.displayName;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ChallengeVerification, Comment, PublishCommentOptions } from '@bitsocia
|
||||
import { create } from 'zustand';
|
||||
import { alertChallengeVerificationFailed } from '../lib/utils/challenge-utils';
|
||||
import { getCommentCommunityAddress } from '../lib/utils/comment-utils';
|
||||
import { normalizePublishURL } from '../lib/utils/url-utils';
|
||||
|
||||
type ReplyState = {
|
||||
author: { [parentCid: string]: any | undefined };
|
||||
@@ -24,7 +25,8 @@ const usePublishReplyStore = create<ReplyState>((set) => ({
|
||||
|
||||
setPublishReplyStore: (comment: Comment) =>
|
||||
set((state) => {
|
||||
const { parentCid, author, content, link, spoiler } = comment;
|
||||
const { parentCid, author, content, spoiler } = comment;
|
||||
const link = comment.link ? normalizePublishURL(comment.link) : undefined;
|
||||
const communityAddress = getCommentCommunityAddress(comment);
|
||||
|
||||
const displayName = 'displayName' in comment ? comment.displayName || undefined : author?.displayName;
|
||||
|
||||
Reference in New Issue
Block a user