diff --git a/client/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx b/client/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx index 73b1a6d764..4c89753386 100644 --- a/client/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx @@ -13,7 +13,7 @@ import { AdvancedConfigContainer_settings } from "coral-admin/__generated__/Adva import AMPConfig from "./AMPConfig"; import CommentStreamLiveUpdatesContainer from "./CommentStreamLiveUpdatesContainer"; import CustomCSSConfig from "./CustomCSSConfig"; -import EmbeddedCommentRepliesConfig from "./EmbeddedCommentRepliesConfig"; +import EmbeddedCommentsConfig from "./EmbeddedCommentsConfig"; import ForReviewQueueConfig from "./ForReviewQueueConfig"; import StoryCreationConfig from "./StoryCreationConfig"; @@ -31,7 +31,7 @@ const AdvancedConfigContainer: React.FunctionComponent = ({ return ( - + ({ settings: graphql` fragment AdvancedConfigContainer_settings on Settings { ...CustomCSSConfig_formValues @relay(mask: false) - ...EmbeddedCommentRepliesConfig_formValues @relay(mask: false) + ...EmbeddedCommentsConfig_formValues @relay(mask: false) ...CommentStreamLiveUpdates_formValues @relay(mask: false) ...StoryCreationConfig_formValues @relay(mask: false) ...CommentStreamLiveUpdatesContainer_settings diff --git a/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentRepliesConfig.tsx b/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentRepliesConfig.tsx deleted file mode 100644 index f78b905743..0000000000 --- a/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentRepliesConfig.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Localized } from "@fluent/react/compat"; -import React, { FunctionComponent } from "react"; -import { graphql } from "react-relay"; - -import { FormField, FormFieldDescription, Label } from "coral-ui/components/v2"; - -import ConfigBox from "../../ConfigBox"; -import Header from "../../Header"; -import OnOffField from "../../OnOffField"; - -// eslint-disable-next-line no-unused-expressions -graphql` - fragment EmbeddedCommentRepliesConfig_formValues on Settings { - embeddedComments { - allowReplies - } - } -`; - -interface Props { - disabled: boolean; -} - -const EmbeddedCommentRepliesConfig: FunctionComponent = ({ - disabled, -}) => ( - -
- Embedded comment replies -
- - } - > - - - - When enabled, a reply button will appear with each embedded comment to - encourage additional discussion on that specific comment or story. - - - - - - - -
-); - -export default EmbeddedCommentRepliesConfig; diff --git a/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx b/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx new file mode 100644 index 0000000000..a22d78332b --- /dev/null +++ b/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx @@ -0,0 +1,72 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent } from "react"; +import { graphql } from "react-relay"; + +import { FormField, HelperText, Label } from "coral-ui/components/v2"; + +import ConfigBox from "../../ConfigBox"; +import Header from "../../Header"; +import OnOffField from "../../OnOffField"; +import Subheader from "../../Subheader"; +import AllowedOriginsTextarea from "../Sites/AllowedOriginsTextarea"; + +// eslint-disable-next-line no-unused-expressions +graphql` + fragment EmbeddedCommentsConfig_formValues on Settings { + embeddedComments { + allowReplies + oEmbedAllowedOrigins + } + } +`; + +interface Props { + disabled: boolean; +} + +const EmbeddedCommentsConfig: FunctionComponent = ({ disabled }) => ( + +
+ Embedded comments +
+ + } + > + + + + + + + When enabled, a reply button will appear with each embedded comment to + encourage additional discussion on that specific comment or story. + + + + + + For sites using oEmbed + + + + + + + + Domains that are permitted to make calls to the oEmbed API (ex. + http://localhost:3000, https://staging.domain.com, + https://domain.com). + + + + +
+); + +export default EmbeddedCommentsConfig; diff --git a/client/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx b/client/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx index 3c513fe16d..b0735ad4b1 100644 --- a/client/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx @@ -10,12 +10,18 @@ import ValidationMessage from "../../ValidationMessage"; import styles from "./AllowedOriginsTextarea.css"; interface Props { + name: string; defaultValue?: ReadonlyArray; + disabled?: boolean; } -const AllowedOriginsTextarea: FunctionComponent = ({ defaultValue }) => ( +const AllowedOriginsTextarea: FunctionComponent = ({ + name, + defaultValue, + disabled = false, +}) => ( = ({ defaultValue }) => ( autoCapitalize="off" spellCheck={false} fullwidth + disabled={disabled} /> diff --git a/client/src/core/client/admin/routes/Configure/sections/Sites/CreateSiteForm.tsx b/client/src/core/client/admin/routes/Configure/sections/Sites/CreateSiteForm.tsx index 1cbf98d254..4651f9f4d0 100644 --- a/client/src/core/client/admin/routes/Configure/sections/Sites/CreateSiteForm.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/Sites/CreateSiteForm.tsx @@ -84,7 +84,7 @@ const CreateSiteForm: FunctionComponent = ({ onCreate }) => { - + {submitError && ( diff --git a/client/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx b/client/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx index 4461581f88..ffb1f455de 100644 --- a/client/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx @@ -90,7 +90,10 @@ const EditSiteForm: FunctionComponent = ({ - + diff --git a/client/src/core/client/admin/test/configure/advanced.spec.tsx b/client/src/core/client/admin/test/configure/advanced.spec.tsx index ed95c5f900..05a7050be3 100644 --- a/client/src/core/client/admin/test/configure/advanced.spec.tsx +++ b/client/src/core/client/admin/test/configure/advanced.spec.tsx @@ -59,7 +59,7 @@ it("renders configure advanced", async () => { const { configureContainer } = await createTestRenderer(); expect(within(configureContainer).getByLabelText("Custom CSS")).toBeDefined(); expect( - within(configureContainer).getByText("Embedded comment replies") + within(configureContainer).getByText("Embedded comments") ).toBeDefined(); expect( within(configureContainer).getByText("Comment stream live updates") @@ -223,7 +223,7 @@ it("change embedded comments allow replies", async () => { }); const embeddedCommentReplies = within(advancedContainer).getByTestId( - "embedded-comment-replies-config" + "embedded-comments-config" ); const offField = within(embeddedCommentReplies).getByText("Off"); @@ -241,3 +241,43 @@ it("change embedded comments allow replies", async () => { expect(resolvers.Mutation!.updateSettings!.called).toBe(true); }); }); + +it("change oembed permitted domains", async () => { + const resolvers = createResolversStub({ + Mutation: { + updateSettings: ({ variables }) => { + expectAndFail( + variables.settings.embeddedComments?.oEmbedAllowedOrigins + ).toEqual(["http://localhost:8080"]); + return { + settings: pureMerge(settings, variables.settings), + }; + }, + }, + }); + const { advancedContainer, saveChangesButton } = await createTestRenderer({ + resolvers, + }); + + const oembedAllowedOriginsConfig = within(advancedContainer).getByTestId( + "embedded-comments-config" + ); + + const allowedOriginsTextArea = within(oembedAllowedOriginsConfig).getByRole( + "textbox" + ); + + userEvent.type(allowedOriginsTextArea, "http://"); + + userEvent.click(saveChangesButton); + + expect(within(advancedContainer).getByText("Invalid URL")); + + userEvent.type(allowedOriginsTextArea, "localhost:8080"); + + userEvent.click(saveChangesButton); + + await waitFor(() => { + expect(resolvers.Mutation!.updateSettings!.called).toBe(true); + }); +}); diff --git a/client/src/core/client/admin/test/fixtures.ts b/client/src/core/client/admin/test/fixtures.ts index 9a3ddf61b1..4c6f4d7a19 100644 --- a/client/src/core/client/admin/test/fixtures.ts +++ b/client/src/core/client/admin/test/fixtures.ts @@ -224,6 +224,7 @@ export const settings = createFixture({ emailDomainModeration: [], embeddedComments: { allowReplies: true, + oEmbedAllowedOrigins: [], }, flairBadges: { flairBadgesEnabled: false, diff --git a/locales/en-US/admin.ftl b/locales/en-US/admin.ftl index 1ba67c3b5e..33246960de 100644 --- a/locales/en-US/admin.ftl +++ b/locales/en-US/admin.ftl @@ -905,12 +905,17 @@ configure-advanced-customCSS-containsFontFace = URL to a custom CSS stylesheet that contains all @font-face definitions needed by above stylesheet. -configure-advanced-embeddedCommentReplies = Embedded comment replies +configure-advanced-embeddedComments = Embedded comments +configure-advanced-embeddedComments-subheader = For sites using oEmbed configure-advanced-embeddedCommentReplies-explanation = When enabled, a reply button will appear with each embedded comment to encourage additional discussion on that specific comment or story. configure-advanced-embeddedCommentReplies-label = Allow replies to embedded comments +configure-advanced-oembedAllowedOrigins-header = oEmbed permitted domains +configure-advanced-oembedAllowedOrigins-description = Domains that are permitted to make calls to the oEmbed API (ex. http://localhost:3000, https://staging.domain.com, https://domain.com). +configure-advanced-oembedAllowedOrigins-label = oEmbed permitted domains + configure-advanced-permittedDomains = Permitted domains configure-advanced-permittedDomains-description = Domains where your { -product-name } instance is allowed to be embedded diff --git a/server/src/core/server/app/middleware/commentEmbedWhitelisted.ts b/server/src/core/server/app/middleware/commentEmbedWhitelisted.ts index 0ccb3732e7..0f6fd29f1f 100644 --- a/server/src/core/server/app/middleware/commentEmbedWhitelisted.ts +++ b/server/src/core/server/app/middleware/commentEmbedWhitelisted.ts @@ -9,7 +9,7 @@ import { AppOptions } from ".."; import { getRequesterOrigin } from "../helpers"; export const commentEmbedWhitelisted = - ({ mongo }: Pick): RequestHandler => + ({ mongo }: Pick, oembedAPI = false): RequestHandler => async (req, res, next) => { // First try to get the commentID from the query params let { commentID } = req.query; @@ -37,6 +37,13 @@ export const commentEmbedWhitelisted = origin = req.header("Origin"); } if (origin) { + // if oEmbed API call, we also check oEmbed allowed origins on tenant + if ( + oembedAPI && + tenant.embeddedComments?.oEmbedAllowedOrigins.includes(origin) + ) { + return next(); + } if (site.allowedOrigins.includes(origin)) { return next(); } diff --git a/server/src/core/server/app/router/api/index.ts b/server/src/core/server/app/router/api/index.ts index 34bc5793c8..afe215a9eb 100644 --- a/server/src/core/server/app/router/api/index.ts +++ b/server/src/core/server/app/router/api/index.ts @@ -97,7 +97,7 @@ export function createAPIRouter(app: AppOptions, options: RouterOptions) { router.get("/oembed", cspSiteMiddleware(app), oembedHandler(app)); router.get( "/services/oembed", - commentEmbedWhitelisted(app), + commentEmbedWhitelisted(app, true), cors(createCommentEmbedCorsOptionsDelegate(app.mongo)), oembedProviderHandler(app) ); diff --git a/server/src/core/server/graph/resolvers/Settings.ts b/server/src/core/server/graph/resolvers/Settings.ts index 4c922a80ca..7988ef3b09 100644 --- a/server/src/core/server/graph/resolvers/Settings.ts +++ b/server/src/core/server/graph/resolvers/Settings.ts @@ -54,10 +54,15 @@ export const Settings: GQLSettingsTypeResolver = { return deprecated; }, embeddedComments: ( - { embeddedComments = { allowReplies: true } }, + { embeddedComments = { allowReplies: true, oEmbedAllowedOrigins: [] } }, args, ctx - ) => embeddedComments, + ) => { + return { + allowReplies: embeddedComments.allowReplies ?? true, + oEmbedAllowedOrigins: embeddedComments.oEmbedAllowedOrigins ?? [], + }; + }, flairBadges: ({ flairBadges = { flairBadgesEnabled: false, badges: [] }, }) => { diff --git a/server/src/core/server/graph/schema/schema.graphql b/server/src/core/server/graph/schema/schema.graphql index 23cd467c9a..cf20728d62 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -1673,6 +1673,10 @@ EmbeddedCommentsConfiguration specifies the configuration for embedded comments. """ type EmbeddedCommentsConfiguration { allowReplies: Boolean + """ + oEmbedAllowedOrigins are the allowed origins for oEmbed API calls. + """ + oEmbedAllowedOrigins: [String!]! @auth(roles: [ADMIN, MODERATOR]) } """ @@ -1710,7 +1714,6 @@ type BadgeConfiguration { FlairBadgeConfiguration specifies the configuration for flair badges, including whether they are enabled and any configured image urls. """ - type FlairBadge { name: String! url: String! @@ -5568,6 +5571,10 @@ EmbeddedCommentsConfigurationInput specifies the configuration for comment embed """ input EmbeddedCommentsConfigurationInput { allowReplies: Boolean + """ + oEmbedAllowedOrigins are the allowed origins for oEmbed API calls. + """ + oEmbedAllowedOrigins: [String!] } """ diff --git a/server/src/core/server/models/tenant/tenant.ts b/server/src/core/server/models/tenant/tenant.ts index 4e6324523e..cc5343bf4c 100644 --- a/server/src/core/server/models/tenant/tenant.ts +++ b/server/src/core/server/models/tenant/tenant.ts @@ -293,6 +293,7 @@ export async function createTenant( emailDomainModeration: [], embeddedComments: { allowReplies: true, + oEmbedAllowedOrigins: [], }, flairBadges: { flairBadgesEnabled: false, diff --git a/server/src/core/server/test/fixtures.ts b/server/src/core/server/test/fixtures.ts index 65ce9cea59..0abca1f9e2 100644 --- a/server/src/core/server/test/fixtures.ts +++ b/server/src/core/server/test/fixtures.ts @@ -184,6 +184,10 @@ export const createTenantFixture = ( flattenReplies: false, disableDefaultFonts: false, emailDomainModeration: [], + embeddedComments: { + allowReplies: true, + oEmbedAllowedOrigins: [], + }, }; return merge(fixture, defaults);