From f7cc66891fca1f187f9bd235ae5721765eaf7044 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 24 Aug 2023 10:22:24 -0400 Subject: [PATCH 01/26] initial commit to make oembedallowedorigins configurable --- .../Advanced/AdvancedConfigContainer.tsx | 3 ++ .../OEmbedAllowedOriginsConfigContainer.tsx | 47 +++++++++++++++++++ .../sections/Sites/AllowedOriginsTextarea.tsx | 8 +++- .../sections/Sites/CreateSiteForm.tsx | 2 +- .../Configure/sections/Sites/EditSiteForm.tsx | 5 +- src/core/server/graph/resolvers/Settings.ts | 1 + src/core/server/graph/schema/schema.graphql | 11 ++++- src/core/server/models/settings/settings.ts | 1 + 8 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx diff --git a/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx b/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx index 73b1a6d764..8d4eceefb9 100644 --- a/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx +++ b/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx @@ -15,6 +15,7 @@ import CommentStreamLiveUpdatesContainer from "./CommentStreamLiveUpdatesContain import CustomCSSConfig from "./CustomCSSConfig"; import EmbeddedCommentRepliesConfig from "./EmbeddedCommentRepliesConfig"; import ForReviewQueueConfig from "./ForReviewQueueConfig"; +import OEmbedAllowedOriginsConfigContainer from "./OEmbedAllowedOriginsConfigContainer"; import StoryCreationConfig from "./StoryCreationConfig"; interface Props { @@ -32,6 +33,7 @@ const AdvancedConfigContainer: React.FunctionComponent = ({ + ({ fragment AdvancedConfigContainer_settings on Settings { ...CustomCSSConfig_formValues @relay(mask: false) ...EmbeddedCommentRepliesConfig_formValues @relay(mask: false) + ...OEmbedAllowedOriginsConfigContainer_formValues @relay(mask: false) ...CommentStreamLiveUpdates_formValues @relay(mask: false) ...StoryCreationConfig_formValues @relay(mask: false) ...CommentStreamLiveUpdatesContainer_settings diff --git a/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx b/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx new file mode 100644 index 0000000000..8d960d128a --- /dev/null +++ b/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx @@ -0,0 +1,47 @@ +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 AllowedOriginsTextarea from "../Sites/AllowedOriginsTextarea"; + +// eslint-disable-next-line no-unused-expressions +graphql` + fragment OEmbedAllowedOriginsConfigContainer_formValues on Settings { + oEmbedAllowedOrigins + } +`; + +interface Props { + disabled: boolean; +} + +const OEmbedAllowedOriginsConfigContainer: FunctionComponent = ({ + disabled, +}) => ( + +
oEmbed permitted domains
+ + } + > + + + + Domains that are permitted to make calls to the oEmbed API. + + + + + + + +
+); + +export default OEmbedAllowedOriginsConfigContainer; diff --git a/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx b/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx index 3c513fe16d..8c5c7caf4f 100644 --- a/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx +++ b/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx @@ -10,12 +10,16 @@ import ValidationMessage from "../../ValidationMessage"; import styles from "./AllowedOriginsTextarea.css"; interface Props { + name: string; defaultValue?: ReadonlyArray; } -const AllowedOriginsTextarea: FunctionComponent = ({ defaultValue }) => ( +const AllowedOriginsTextarea: FunctionComponent = ({ + name, + defaultValue, +}) => ( = ({ onCreate }) => { - + {submitError && ( diff --git a/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx b/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx index 4461581f88..ffb1f455de 100644 --- a/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx +++ b/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx @@ -90,7 +90,10 @@ const EditSiteForm: FunctionComponent = ({ - + diff --git a/src/core/server/graph/resolvers/Settings.ts b/src/core/server/graph/resolvers/Settings.ts index 4c922a80ca..b6f4425da5 100644 --- a/src/core/server/graph/resolvers/Settings.ts +++ b/src/core/server/graph/resolvers/Settings.ts @@ -69,4 +69,5 @@ export const Settings: GQLSettingsTypeResolver = { } return flairBadges; }, + oEmbedAllowedOrigins: ({ oEmbedAllowedOrigins = [] }) => oEmbedAllowedOrigins, }; diff --git a/src/core/server/graph/schema/schema.graphql b/src/core/server/graph/schema/schema.graphql index 23cd467c9a..5f15866dfb 100644 --- a/src/core/server/graph/schema/schema.graphql +++ b/src/core/server/graph/schema/schema.graphql @@ -1710,7 +1710,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! @@ -2222,6 +2221,11 @@ type Settings @cacheControl(maxAge: 30) { they are enabled and any configured image urls """ flairBadges: FlairBadgeConfiguration + + """ + oEmbedAllowedOrigins are the allowed origins for oEmbed API calls. + """ + oEmbedAllowedOrigins: [String!]! @auth(roles: [ADMIN, MODERATOR]) } ################################################################################ @@ -5802,6 +5806,11 @@ input SettingsInput { they are enabled and any configured image urls """ flairBadges: FlairBadgeConfigurationInput + + """ + oEmbedAllowedOrigins are the allowed origins for oEmbed API calls. + """ + oEmbedAllowedOrigins: [String!] } """ diff --git a/src/core/server/models/settings/settings.ts b/src/core/server/models/settings/settings.ts index 99ba0821a9..89f0a3f082 100644 --- a/src/core/server/models/settings/settings.ts +++ b/src/core/server/models/settings/settings.ts @@ -321,6 +321,7 @@ export type Settings = GlobalModerationSettings & | "announcement" | "memberBios" | "embeddedComments" + | "oEmbedAllowedOrigins" > & { /** * auth is the set of configured authentication integrations. From c05b0f6cb0d119b326376e6ed13d6117c35d99a9 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 24 Aug 2023 10:59:36 -0400 Subject: [PATCH 02/26] check for oembedallowedorigins on oembed api calls; add to defaults --- src/core/server/app/middleware/commentEmbedWhitelisted.ts | 8 +++++++- src/core/server/app/router/api/index.ts | 2 +- src/core/server/models/tenant/tenant.ts | 1 + src/core/server/test/fixtures.ts | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/server/app/middleware/commentEmbedWhitelisted.ts b/src/core/server/app/middleware/commentEmbedWhitelisted.ts index 0ccb3732e7..ce479676e3 100644 --- a/src/core/server/app/middleware/commentEmbedWhitelisted.ts +++ b/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,12 @@ export const commentEmbedWhitelisted = origin = req.header("Origin"); } if (origin) { + // if oEmbed API call, we also check oEmbed allowed origins on tenant + if (oembedAPI) { + if (tenant.oEmbedAllowedOrigins.includes(origin)) { + return next(); + } + } if (site.allowedOrigins.includes(origin)) { return next(); } diff --git a/src/core/server/app/router/api/index.ts b/src/core/server/app/router/api/index.ts index 34bc5793c8..afe215a9eb 100644 --- a/src/core/server/app/router/api/index.ts +++ b/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/src/core/server/models/tenant/tenant.ts b/src/core/server/models/tenant/tenant.ts index 192762a10f..8c08ce494e 100644 --- a/src/core/server/models/tenant/tenant.ts +++ b/src/core/server/models/tenant/tenant.ts @@ -298,6 +298,7 @@ export async function createTenant( flairBadgesEnabled: false, badges: [], }, + oEmbedAllowedOrigins: [], }; // Create the new Tenant by merging it together with the defaults. diff --git a/src/core/server/test/fixtures.ts b/src/core/server/test/fixtures.ts index 7efe082648..25701d5de8 100644 --- a/src/core/server/test/fixtures.ts +++ b/src/core/server/test/fixtures.ts @@ -184,6 +184,7 @@ export const createTenantFixture = ( flattenReplies: false, disableDefaultFonts: false, emailDomainModeration: [], + oEmbedAllowedOrigins: [], }; return merge(fixture, defaults); From 6cda411453831fe7fd65554bdf630b2d6316f4ed Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 24 Aug 2023 11:24:34 -0400 Subject: [PATCH 03/26] add test and localizations --- .../OEmbedAllowedOriginsConfigContainer.tsx | 14 ++++--- .../sections/Sites/AllowedOriginsTextarea.tsx | 3 ++ .../admin/test/configure/advanced.spec.tsx | 40 +++++++++++++++++++ src/core/client/admin/test/fixtures.ts | 1 + src/locales/en-US/admin.ftl | 4 ++ 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx b/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx index 8d960d128a..803b7d856d 100644 --- a/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx +++ b/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx @@ -23,23 +23,25 @@ const OEmbedAllowedOriginsConfigContainer: FunctionComponent = ({ disabled, }) => ( -
oEmbed permitted domains
+ +
+ oEmbed permitted domains +
} > - + Domains that are permitted to make calls to the oEmbed API. - + - +
); diff --git a/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx b/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx index 8c5c7caf4f..b0735ad4b1 100644 --- a/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx +++ b/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx @@ -12,11 +12,13 @@ import styles from "./AllowedOriginsTextarea.css"; interface Props { name: string; defaultValue?: ReadonlyArray; + disabled?: boolean; } const AllowedOriginsTextarea: FunctionComponent = ({ name, defaultValue, + disabled = false, }) => ( = ({ autoCapitalize="off" spellCheck={false} fullwidth + disabled={disabled} /> diff --git a/src/core/client/admin/test/configure/advanced.spec.tsx b/src/core/client/admin/test/configure/advanced.spec.tsx index 9f19e303b3..043e4d0ca2 100644 --- a/src/core/client/admin/test/configure/advanced.spec.tsx +++ b/src/core/client/admin/test/configure/advanced.spec.tsx @@ -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.oEmbedAllowedOrigins).toEqual([ + "http://localhost:8080", + ]); + return { + settings: pureMerge(settings, variables.settings), + }; + }, + }, + }); + const { advancedContainer, saveChangesButton } = await createTestRenderer({ + resolvers, + }); + + const oembedAllowedOriginsConfig = within(advancedContainer).getByTestId( + "oembed-allowed-origins-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/src/core/client/admin/test/fixtures.ts b/src/core/client/admin/test/fixtures.ts index 7102c1f35a..8213c5d950 100644 --- a/src/core/client/admin/test/fixtures.ts +++ b/src/core/client/admin/test/fixtures.ts @@ -229,6 +229,7 @@ export const settings = createFixture({ flairBadgesEnabled: false, badges: [], }, + oEmbedAllowedOrigins: [], }); export const settingsWithMultisite = createFixture( diff --git a/src/locales/en-US/admin.ftl b/src/locales/en-US/admin.ftl index 1ba67c3b5e..a8d3ce2e46 100644 --- a/src/locales/en-US/admin.ftl +++ b/src/locales/en-US/admin.ftl @@ -911,6 +911,10 @@ configure-advanced-embeddedCommentReplies-explanation = When enabled, a reply bu 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. +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 From fa1c429846a34f27ac0eb23d1910bc02cea05637 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 24 Aug 2023 15:36:37 -0400 Subject: [PATCH 04/26] update config layout and copy --- .../Advanced/AdvancedConfigContainer.tsx | 9 +-- .../Advanced/EmbeddedCommentRepliesConfig.tsx | 52 -------------- .../Advanced/EmbeddedCommentsConfig.tsx | 72 +++++++++++++++++++ .../OEmbedAllowedOriginsConfigContainer.tsx | 49 ------------- .../admin/test/configure/advanced.spec.tsx | 12 ++-- src/core/client/admin/test/fixtures.ts | 2 +- .../app/middleware/commentEmbedWhitelisted.ts | 4 +- src/core/server/graph/resolvers/Settings.ts | 10 ++- src/core/server/graph/schema/schema.graphql | 18 +++-- src/core/server/models/settings/settings.ts | 1 - src/core/server/models/tenant/tenant.ts | 2 +- src/core/server/test/fixtures.ts | 5 +- src/locales/en-US/admin.ftl | 4 +- 13 files changed, 108 insertions(+), 132 deletions(-) delete mode 100644 src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentRepliesConfig.tsx create mode 100644 src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx delete mode 100644 src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx diff --git a/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx b/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx index 8d4eceefb9..4c89753386 100644 --- a/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx +++ b/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx @@ -13,9 +13,8 @@ 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 OEmbedAllowedOriginsConfigContainer from "./OEmbedAllowedOriginsConfigContainer"; import StoryCreationConfig from "./StoryCreationConfig"; interface Props { @@ -32,8 +31,7 @@ const AdvancedConfigContainer: React.FunctionComponent = ({ return ( - - + ({ settings: graphql` fragment AdvancedConfigContainer_settings on Settings { ...CustomCSSConfig_formValues @relay(mask: false) - ...EmbeddedCommentRepliesConfig_formValues @relay(mask: false) - ...OEmbedAllowedOriginsConfigContainer_formValues @relay(mask: false) + ...EmbeddedCommentsConfig_formValues @relay(mask: false) ...CommentStreamLiveUpdates_formValues @relay(mask: false) ...StoryCreationConfig_formValues @relay(mask: false) ...CommentStreamLiveUpdatesContainer_settings diff --git a/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentRepliesConfig.tsx b/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentRepliesConfig.tsx deleted file mode 100644 index f78b905743..0000000000 --- a/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/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx b/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx new file mode 100644 index 0000000000..b86ef7065c --- /dev/null +++ b/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, FormFieldDescription, Label } from "coral-ui/components/v2"; + +import ConfigBox from "../../ConfigBox"; +import Header from "../../Header"; +import OnOffField from "../../OnOffField"; +import AllowedOriginsTextarea from "../Sites/AllowedOriginsTextarea"; +import Subheader from "../../Subheader"; + +// 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/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx b/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx deleted file mode 100644 index 803b7d856d..0000000000 --- a/src/core/client/admin/routes/Configure/sections/Advanced/OEmbedAllowedOriginsConfigContainer.tsx +++ /dev/null @@ -1,49 +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 AllowedOriginsTextarea from "../Sites/AllowedOriginsTextarea"; - -// eslint-disable-next-line no-unused-expressions -graphql` - fragment OEmbedAllowedOriginsConfigContainer_formValues on Settings { - oEmbedAllowedOrigins - } -`; - -interface Props { - disabled: boolean; -} - -const OEmbedAllowedOriginsConfigContainer: FunctionComponent = ({ - disabled, -}) => ( - -
- oEmbed permitted domains -
- - } - > - - - - Domains that are permitted to make calls to the oEmbed API. - - - - - - - -
-); - -export default OEmbedAllowedOriginsConfigContainer; diff --git a/src/core/client/admin/test/configure/advanced.spec.tsx b/src/core/client/admin/test/configure/advanced.spec.tsx index 043e4d0ca2..582ededa84 100644 --- a/src/core/client/admin/test/configure/advanced.spec.tsx +++ b/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"); @@ -246,9 +246,9 @@ it("change oembed permitted domains", async () => { const resolvers = createResolversStub({ Mutation: { updateSettings: ({ variables }) => { - expectAndFail(variables.settings.oEmbedAllowedOrigins).toEqual([ - "http://localhost:8080", - ]); + expectAndFail( + variables.settings.embeddedComments?.oEmbedAllowedOrigins + ).toEqual(["http://localhost:8080"]); return { settings: pureMerge(settings, variables.settings), }; @@ -260,7 +260,7 @@ it("change oembed permitted domains", async () => { }); const oembedAllowedOriginsConfig = within(advancedContainer).getByTestId( - "oembed-allowed-origins-config" + "embedded-comments-config" ); const allowedOriginsTextArea = within(oembedAllowedOriginsConfig).getByRole( diff --git a/src/core/client/admin/test/fixtures.ts b/src/core/client/admin/test/fixtures.ts index 8213c5d950..c32159eafc 100644 --- a/src/core/client/admin/test/fixtures.ts +++ b/src/core/client/admin/test/fixtures.ts @@ -224,12 +224,12 @@ export const settings = createFixture({ emailDomainModeration: [], embeddedComments: { allowReplies: true, + oEmbedAllowedOrigins: [], }, flairBadges: { flairBadgesEnabled: false, badges: [], }, - oEmbedAllowedOrigins: [], }); export const settingsWithMultisite = createFixture( diff --git a/src/core/server/app/middleware/commentEmbedWhitelisted.ts b/src/core/server/app/middleware/commentEmbedWhitelisted.ts index ce479676e3..13ca25e99a 100644 --- a/src/core/server/app/middleware/commentEmbedWhitelisted.ts +++ b/src/core/server/app/middleware/commentEmbedWhitelisted.ts @@ -39,7 +39,9 @@ export const commentEmbedWhitelisted = if (origin) { // if oEmbed API call, we also check oEmbed allowed origins on tenant if (oembedAPI) { - if (tenant.oEmbedAllowedOrigins.includes(origin)) { + if ( + tenant.embeddedComments?.oEmbedAllowedOrigins.includes(origin) + ) { return next(); } } diff --git a/src/core/server/graph/resolvers/Settings.ts b/src/core/server/graph/resolvers/Settings.ts index b6f4425da5..7988ef3b09 100644 --- a/src/core/server/graph/resolvers/Settings.ts +++ b/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: [] }, }) => { @@ -69,5 +74,4 @@ export const Settings: GQLSettingsTypeResolver = { } return flairBadges; }, - oEmbedAllowedOrigins: ({ oEmbedAllowedOrigins = [] }) => oEmbedAllowedOrigins, }; diff --git a/src/core/server/graph/schema/schema.graphql b/src/core/server/graph/schema/schema.graphql index 5f15866dfb..cf20728d62 100644 --- a/src/core/server/graph/schema/schema.graphql +++ b/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]) } """ @@ -2221,11 +2225,6 @@ type Settings @cacheControl(maxAge: 30) { they are enabled and any configured image urls """ flairBadges: FlairBadgeConfiguration - - """ - oEmbedAllowedOrigins are the allowed origins for oEmbed API calls. - """ - oEmbedAllowedOrigins: [String!]! @auth(roles: [ADMIN, MODERATOR]) } ################################################################################ @@ -5572,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!] } """ @@ -5806,11 +5809,6 @@ input SettingsInput { they are enabled and any configured image urls """ flairBadges: FlairBadgeConfigurationInput - - """ - oEmbedAllowedOrigins are the allowed origins for oEmbed API calls. - """ - oEmbedAllowedOrigins: [String!] } """ diff --git a/src/core/server/models/settings/settings.ts b/src/core/server/models/settings/settings.ts index 89f0a3f082..99ba0821a9 100644 --- a/src/core/server/models/settings/settings.ts +++ b/src/core/server/models/settings/settings.ts @@ -321,7 +321,6 @@ export type Settings = GlobalModerationSettings & | "announcement" | "memberBios" | "embeddedComments" - | "oEmbedAllowedOrigins" > & { /** * auth is the set of configured authentication integrations. diff --git a/src/core/server/models/tenant/tenant.ts b/src/core/server/models/tenant/tenant.ts index 8c08ce494e..3103cc6d00 100644 --- a/src/core/server/models/tenant/tenant.ts +++ b/src/core/server/models/tenant/tenant.ts @@ -293,12 +293,12 @@ export async function createTenant( emailDomainModeration: [], embeddedComments: { allowReplies: true, + oEmbedAllowedOrigins: [], }, flairBadges: { flairBadgesEnabled: false, badges: [], }, - oEmbedAllowedOrigins: [], }; // Create the new Tenant by merging it together with the defaults. diff --git a/src/core/server/test/fixtures.ts b/src/core/server/test/fixtures.ts index 25701d5de8..81187cec43 100644 --- a/src/core/server/test/fixtures.ts +++ b/src/core/server/test/fixtures.ts @@ -184,7 +184,10 @@ export const createTenantFixture = ( flattenReplies: false, disableDefaultFonts: false, emailDomainModeration: [], - oEmbedAllowedOrigins: [], + embeddedComments: { + allowReplies: true, + oEmbedAllowedOrigins: [], + }, }; return merge(fixture, defaults); diff --git a/src/locales/en-US/admin.ftl b/src/locales/en-US/admin.ftl index a8d3ce2e46..48ded2a1df 100644 --- a/src/locales/en-US/admin.ftl +++ b/src/locales/en-US/admin.ftl @@ -905,6 +905,8 @@ configure-advanced-customCSS-containsFontFace = URL to a custom CSS stylesheet that contains all @font-face definitions needed by above stylesheet. +configure-advanced-embeddedComments = Embedded comments +configure-advanced-embeddedComments-subheader = For sites using oEmbed configure-advanced-embeddedCommentReplies = Embedded comment replies configure-advanced-embeddedCommentReplies-explanation = When enabled, a reply button will appear with each embedded comment to encourage additional discussion on that @@ -912,7 +914,7 @@ configure-advanced-embeddedCommentReplies-explanation = When enabled, a reply bu 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. +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 From 4d0aa5bab85f6f1a4685d255252bb83d1a57700f Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Fri, 25 Aug 2023 10:37:11 -0400 Subject: [PATCH 05/26] fix import order --- .../Configure/sections/Advanced/EmbeddedCommentsConfig.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx b/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx index b86ef7065c..518cd674a9 100644 --- a/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx +++ b/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx @@ -7,8 +7,8 @@ import { FormField, FormFieldDescription, Label } from "coral-ui/components/v2"; import ConfigBox from "../../ConfigBox"; import Header from "../../Header"; import OnOffField from "../../OnOffField"; -import AllowedOriginsTextarea from "../Sites/AllowedOriginsTextarea"; import Subheader from "../../Subheader"; +import AllowedOriginsTextarea from "../Sites/AllowedOriginsTextarea"; // eslint-disable-next-line no-unused-expressions graphql` From 3f79f6b69a1ed5dcf225ba96b06f70bc3fdc29ff Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Fri, 25 Aug 2023 10:49:10 -0400 Subject: [PATCH 06/26] update descs to helpertext to match design --- .../sections/Advanced/EmbeddedCommentsConfig.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx b/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx index 518cd674a9..a22d78332b 100644 --- a/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx +++ b/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx @@ -2,7 +2,7 @@ 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 { FormField, HelperText, Label } from "coral-ui/components/v2"; import ConfigBox from "../../ConfigBox"; import Header from "../../Header"; @@ -40,10 +40,10 @@ const EmbeddedCommentsConfig: FunctionComponent = ({ disabled }) => ( - + When enabled, a reply button will appear with each embedded comment to encourage additional discussion on that specific comment or story. - +
@@ -55,11 +55,11 @@ const EmbeddedCommentsConfig: FunctionComponent = ({ disabled }) => ( - + Domains that are permitted to make calls to the oEmbed API (ex. http://localhost:3000, https://staging.domain.com, https://domain.com). - + Date: Fri, 25 Aug 2023 11:08:16 -0400 Subject: [PATCH 07/26] remove no-longer-needed localization --- src/locales/en-US/admin.ftl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/en-US/admin.ftl b/src/locales/en-US/admin.ftl index 48ded2a1df..33246960de 100644 --- a/src/locales/en-US/admin.ftl +++ b/src/locales/en-US/admin.ftl @@ -907,7 +907,6 @@ configure-advanced-customCSS-containsFontFace = configure-advanced-embeddedComments = Embedded comments configure-advanced-embeddedComments-subheader = For sites using oEmbed -configure-advanced-embeddedCommentReplies = Embedded comment replies 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. From 46838a95b053cc827b78f0d01ef242b0a2892b65 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 28 Aug 2023 10:40:46 -0400 Subject: [PATCH 08/26] set focus on scrolltotop of comments --- src/core/client/stream/common/scrollToBeginning.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/client/stream/common/scrollToBeginning.ts b/src/core/client/stream/common/scrollToBeginning.ts index f173fa3612..0171f660fb 100644 --- a/src/core/client/stream/common/scrollToBeginning.ts +++ b/src/core/client/stream/common/scrollToBeginning.ts @@ -12,6 +12,8 @@ function scrollToBeginning( } else { window.scrollTo({ top: getElementWindowTopOffset(window, tab) }); } + // set keyboard focus to Comments button for accessibility + tab.getElementsByTagName("button")[0].focus(); } } From 7225cd3cc885d87e05ca0424edc8d2aa861a2060 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 28 Aug 2023 11:08:49 -0400 Subject: [PATCH 09/26] add aria-expanded to manage ignored commenters --- .../tabs/Profile/Preferences/IgnoreUserSettingsContainer.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/client/stream/tabs/Profile/Preferences/IgnoreUserSettingsContainer.tsx b/src/core/client/stream/tabs/Profile/Preferences/IgnoreUserSettingsContainer.tsx index a724eb3088..f26932b43b 100644 --- a/src/core/client/stream/tabs/Profile/Preferences/IgnoreUserSettingsContainer.tsx +++ b/src/core/client/stream/tabs/Profile/Preferences/IgnoreUserSettingsContainer.tsx @@ -82,6 +82,7 @@ const IgnoreUserSettingsContainer: FunctionComponent = ({ viewer }) => { upperCase onClick={toggleManage} className={CLASSES.ignoredCommenters.manageButton} + aria-expanded="true" > Close @@ -103,6 +104,7 @@ const IgnoreUserSettingsContainer: FunctionComponent = ({ viewer }) => { CLASSES.ignoredCommenters.manageButton )} aria-label="Manage ignored commenters" + aria-expanded="false" > Manage From 594e1fa2647c9125b84eec37fe3ab46d5f544ebf Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 28 Aug 2023 11:22:25 -0400 Subject: [PATCH 10/26] add aria-expanded to other expandable content too --- src/core/client/stream/tabs/Comments/Comment/CommentToggle.tsx | 1 + .../client/stream/tabs/Comments/Comment/IndentedComment.tsx | 1 + .../Comments/Comment/MediaSection/MediaSectionContainer.tsx | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/core/client/stream/tabs/Comments/Comment/CommentToggle.tsx b/src/core/client/stream/tabs/Comments/Comment/CommentToggle.tsx index a4333d3075..1561332a8b 100644 --- a/src/core/client/stream/tabs/Comments/Comment/CommentToggle.tsx +++ b/src/core/client/stream/tabs/Comments/Comment/CommentToggle.tsx @@ -40,6 +40,7 @@ const CommentToggle: FunctionComponent = (props) => { onClick={props.toggleCollapsed} className={cn(styles.root, CLASSES.comment.collapseToggle.$root)} aria-label={"Expand comment thread"} + aria-expanded="false" > = ({ styles.toggleButton, CLASSES.comment.collapseToggle.$root )} + aria-expanded="true" > = ({ onClick={onToggleExpand} size="small" className={styles.button} + aria-expanded="false" > {media.__typename === "TwitterMedia" && ( @@ -129,6 +130,7 @@ const MediaSectionContainer: FunctionComponent = ({ size="small" iconLeft className={styles.button} + aria-expanded="true" > Date: Mon, 28 Aug 2023 12:48:04 -0400 Subject: [PATCH 11/26] update gotoconvo aria-labels --- src/locales/en-US/stream.ftl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locales/en-US/stream.ftl b/src/locales/en-US/stream.ftl index d9df263453..6aa895eae6 100644 --- a/src/locales/en-US/stream.ftl +++ b/src/locales/en-US/stream.ftl @@ -450,9 +450,9 @@ comments-featured-label = Featured Comment from {$username} comments-featured-gotoConversation = Go to conversation comments-featured-gotoConversation-label-with-username = - .aria-label = Go to this featured comment by user { $username } in the main comment stream + .aria-label = Go to conversation for this featured comment by user { $username } in the main comment stream comments-featured-gotoConversation-label-without-username = - .aria-label = Go to this featured comment in the main comment stream + .aria-label = Go to conversation for this featured comment in the main comment stream comments-featured-replies = Replies ## Profile Tab From c21177a3e42662638a9e0257a67d25aaea31d040 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 28 Aug 2023 12:50:29 -0400 Subject: [PATCH 12/26] combine oembed allowed origins checks --- .../server/app/middleware/commentEmbedWhitelisted.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/server/app/middleware/commentEmbedWhitelisted.ts b/src/core/server/app/middleware/commentEmbedWhitelisted.ts index 13ca25e99a..0f6fd29f1f 100644 --- a/src/core/server/app/middleware/commentEmbedWhitelisted.ts +++ b/src/core/server/app/middleware/commentEmbedWhitelisted.ts @@ -38,12 +38,11 @@ export const commentEmbedWhitelisted = } if (origin) { // if oEmbed API call, we also check oEmbed allowed origins on tenant - if (oembedAPI) { - if ( - tenant.embeddedComments?.oEmbedAllowedOrigins.includes(origin) - ) { - return next(); - } + if ( + oembedAPI && + tenant.embeddedComments?.oEmbedAllowedOrigins.includes(origin) + ) { + return next(); } if (site.allowedOrigins.includes(origin)) { return next(); From 8d651bda3ba58262cffafe6f880f1c3b82154ebb Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 28 Aug 2023 14:58:50 -0400 Subject: [PATCH 13/26] focus first keyboard focusable element after scroll to top article --- .../Stream/CommentsLinks/CommentsLinks.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/core/client/stream/tabs/Comments/Stream/CommentsLinks/CommentsLinks.tsx b/src/core/client/stream/tabs/Comments/Stream/CommentsLinks/CommentsLinks.tsx index 074d362785..453a244ab0 100644 --- a/src/core/client/stream/tabs/Comments/Stream/CommentsLinks/CommentsLinks.tsx +++ b/src/core/client/stream/tabs/Comments/Stream/CommentsLinks/CommentsLinks.tsx @@ -5,6 +5,7 @@ import React, { FC, FunctionComponent, useCallback, + useMemo, } from "react"; import { useCoralContext } from "coral-framework/lib/bootstrap"; @@ -57,12 +58,37 @@ const CommentsLinks: FunctionComponent = ({ showGoToProfile, }) => { const { renderWindow, customScrollContainer } = useCoralContext(); + + // Find first keyboard focusable element for accessibility + const firstKeyboardFocusableElement = useMemo(() => { + const container = customScrollContainer ?? renderWindow.document; + const visibleFocusableElements: Element[] = []; + const focusableElements = container.querySelectorAll( + 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])' + ); + focusableElements.forEach((el) => { + if ( + !el.hasAttribute("disabled") && + !el.getAttribute("aria-hidden") && + !el.getAttribute("hidden") + ) { + visibleFocusableElements.push(el); + } + }); + return visibleFocusableElements[0]; + }, [renderWindow, customScrollContainer]); + const root = useShadowRootOrDocument(); const onGoToArticleTop = useCallback(() => { if (customScrollContainer) { customScrollContainer.scrollTo({ top: 0 }); } renderWindow.scrollTo({ top: 0 }); + // programmatically apply focus to first keyboard focusable element + // after scroll for accessibility + if (firstKeyboardFocusableElement instanceof HTMLElement) { + firstKeyboardFocusableElement.focus(); + } }, [renderWindow, customScrollContainer]); const onGoToCommentsTop = useCallback(() => { scrollToBeginning(root, renderWindow, customScrollContainer); From 718e56859ee1a90d9dc17149f80c8ed04c72bb4a Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Tue, 29 Aug 2023 11:04:50 -0400 Subject: [PATCH 14/26] improve getting first keyboard focusable element --- .../Stream/CommentsLinks/CommentsLinks.tsx | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/core/client/stream/tabs/Comments/Stream/CommentsLinks/CommentsLinks.tsx b/src/core/client/stream/tabs/Comments/Stream/CommentsLinks/CommentsLinks.tsx index 453a244ab0..567eb0dbce 100644 --- a/src/core/client/stream/tabs/Comments/Stream/CommentsLinks/CommentsLinks.tsx +++ b/src/core/client/stream/tabs/Comments/Stream/CommentsLinks/CommentsLinks.tsx @@ -5,7 +5,6 @@ import React, { FC, FunctionComponent, useCallback, - useMemo, } from "react"; import { useCoralContext } from "coral-framework/lib/bootstrap"; @@ -60,22 +59,27 @@ const CommentsLinks: FunctionComponent = ({ const { renderWindow, customScrollContainer } = useCoralContext(); // Find first keyboard focusable element for accessibility - const firstKeyboardFocusableElement = useMemo(() => { + const getFirstKeyboardFocusableElement = useCallback(() => { const container = customScrollContainer ?? renderWindow.document; - const visibleFocusableElements: Element[] = []; + let firstFocusableElement: Element | null = null; + let counter = 0; const focusableElements = container.querySelectorAll( 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])' ); - focusableElements.forEach((el) => { + while ( + firstFocusableElement === null && + counter < focusableElements.length + ) { if ( - !el.hasAttribute("disabled") && - !el.getAttribute("aria-hidden") && - !el.getAttribute("hidden") + !focusableElements[counter].hasAttribute("disabled") && + !focusableElements[counter].getAttribute("aria-hidden") && + !focusableElements[counter].getAttribute("hidden") ) { - visibleFocusableElements.push(el); + firstFocusableElement = focusableElements[counter]; } - }); - return visibleFocusableElements[0]; + counter++; + } + return firstFocusableElement; }, [renderWindow, customScrollContainer]); const root = useShadowRootOrDocument(); @@ -86,10 +90,11 @@ const CommentsLinks: FunctionComponent = ({ renderWindow.scrollTo({ top: 0 }); // programmatically apply focus to first keyboard focusable element // after scroll for accessibility + const firstKeyboardFocusableElement = getFirstKeyboardFocusableElement(); if (firstKeyboardFocusableElement instanceof HTMLElement) { firstKeyboardFocusableElement.focus(); } - }, [renderWindow, customScrollContainer]); + }, [renderWindow, customScrollContainer, getFirstKeyboardFocusableElement]); const onGoToCommentsTop = useCallback(() => { scrollToBeginning(root, renderWindow, customScrollContainer); }, [root, renderWindow, customScrollContainer]); From 4b8e12ac40bf8db55b23e557b489aed3498ccbda Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Tue, 29 Aug 2023 12:37:00 -0400 Subject: [PATCH 15/26] clarify data-coral-url in docs for comment count --- docs/docs/counts.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/counts.md b/docs/docs/counts.md index 9be2eebcdf..29a74699da 100644 --- a/docs/docs/counts.md +++ b/docs/docs/counts.md @@ -37,8 +37,8 @@ After successful injection it will become: Set the class of your html element to `coral-count` in order to get story counts. The following `data-coral-*` attributes will configure the output: - `data-coral-id` – The id of the story of which counts should be injected. -- `data-coral-url` – The URL of the story of which counts should be injected. +- `data-coral-url` – The URL of the story of which counts should be injected. This should always be set. - `data-coral-notext` – If set to `"true"`, only the count number will be injected -Either `data-coral-id` or `data-coral-url` should be set. If none are provided the story URL -will be retrieved from the [canonical url reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Choosing_between_www_and_non-www_URLs#Using_%3Clink_relcanonical%3E) `` or inferred using the current page url. +The `data-coral-url` attribute should always be set. If it is not provided, the story URL +will be retrieved from the [canonical url reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Choosing_between_www_and_non-www_URLs#Using_%3Clink_relcanonical%3E) `` or inferred using the current page url. Therefore, on pages where the story URL cannot be inferred from the canonical url reference or current page url (such as index and homepages), it's important that the `data-coral-url` be included. From 6550f9888f7316fd1b69ec9e052fc5a7c4367099 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Tue, 29 Aug 2023 13:32:27 -0400 Subject: [PATCH 16/26] one more update to clarify in docs --- docs/docs/counts.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/docs/counts.md b/docs/docs/counts.md index 29a74699da..1109bc09dd 100644 --- a/docs/docs/counts.md +++ b/docs/docs/counts.md @@ -37,8 +37,7 @@ After successful injection it will become: Set the class of your html element to `coral-count` in order to get story counts. The following `data-coral-*` attributes will configure the output: - `data-coral-id` – The id of the story of which counts should be injected. -- `data-coral-url` – The URL of the story of which counts should be injected. This should always be set. +- `data-coral-url` – The URL of the story of which counts should be injected. See more info below on when it needs to be set. - `data-coral-notext` – If set to `"true"`, only the count number will be injected -The `data-coral-url` attribute should always be set. If it is not provided, the story URL -will be retrieved from the [canonical url reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Choosing_between_www_and_non-www_URLs#Using_%3Clink_relcanonical%3E) `` or inferred using the current page url. Therefore, on pages where the story URL cannot be inferred from the canonical url reference or current page url (such as index and homepages), it's important that the `data-coral-url` be included. +The `data-coral-url` attribute should always be provided or be able to be inferred from the canonical url reference or current page url. If it is not provided in the data attribute, the story URL will be retrieved from the [canonical url reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Choosing_between_www_and_non-www_URLs#Using_%3Clink_relcanonical%3E) `` or inferred using the current page url. Therefore, on pages where the story URL cannot be inferred from the canonical url reference or current page url (such as indexes and homepages), it's important that the `data-coral-url` be included. From d003b0f40cef569a5774f40317daec1a6319097f Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 31 Aug 2023 09:14:12 -0400 Subject: [PATCH 17/26] move parent revision id check to comment reply creation --- src/core/server/stacks/createComment.ts | 18 +++++++++++++++++- src/core/server/stacks/editComment.ts | 2 +- .../server/stacks/helpers/retrieveParent.ts | 8 -------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/core/server/stacks/createComment.ts b/src/core/server/stacks/createComment.ts index f97dab98ea..ed5e647c2c 100644 --- a/src/core/server/stacks/createComment.ts +++ b/src/core/server/stacks/createComment.ts @@ -10,6 +10,7 @@ import { AuthorAlreadyHasRatedStory, CannotCreateCommentOnArchivedStory, CommentNotFoundError, + CommentRevisionNotFoundError, CoralError, StoryNotFoundError, UserSiteBanned, @@ -30,7 +31,11 @@ import { pushChildCommentIDOntoParent, retrieveManyComments, } from "coral-server/models/comment"; -import { getDepth, hasAncestors } from "coral-server/models/comment/helpers"; +import { + getDepth, + getLatestRevision, + hasAncestors, +} from "coral-server/models/comment/helpers"; import { markSeenComments } from "coral-server/models/seenComments/seenComments"; import { retrieveSite } from "coral-server/models/site"; import { @@ -269,6 +274,17 @@ export default async function create( const ancestorIDs: string[] = []; let parent = await retrieveParent(mongo, tenant.id, input); if (parent) { + // Check to see that the most recent revision matches the one we just replied + // to. + if (input.parentRevisionID) { + const revision = getLatestRevision(parent); + if (revision.id !== input.parentRevisionID) { + throw new CommentRevisionNotFoundError( + parent.id, + input.parentRevisionID + ); + } + } ancestorIDs.push(parent.id); if (hasAncestors(parent)) { // Push the parent's ancestors id's into the comment's ancestor id's. diff --git a/src/core/server/stacks/editComment.ts b/src/core/server/stacks/editComment.ts index 78e4f559b8..3baf17180f 100644 --- a/src/core/server/stacks/editComment.ts +++ b/src/core/server/stacks/editComment.ts @@ -103,7 +103,7 @@ export default async function edit( throw new CommentNotFoundError(input.id); } - // If the original comment was a reply, then get it's parent! + // If the original comment was a reply, then get its parent! const { parentID, parentRevisionID, siteID } = originalStaleComment; const parent = await retrieveParent(mongo, tenant.id, { parentID, diff --git a/src/core/server/stacks/helpers/retrieveParent.ts b/src/core/server/stacks/helpers/retrieveParent.ts index 19798d1900..b55d641053 100644 --- a/src/core/server/stacks/helpers/retrieveParent.ts +++ b/src/core/server/stacks/helpers/retrieveParent.ts @@ -5,7 +5,6 @@ import { ParentCommentRejectedError, } from "coral-server/errors"; import { - getLatestRevision, hasPublishedStatus, retrieveComment, } from "coral-server/models/comment"; @@ -31,13 +30,6 @@ async function retrieveParent( throw new CommentNotFoundError(input.parentID); } - // Check to see that the most recent revision matches the one we just replied - // to. - const revision = getLatestRevision(parent); - if (revision.id !== input.parentRevisionID) { - throw new CommentRevisionNotFoundError(parent.id, input.parentRevisionID); - } - // Check that the parent comment was visible. if (!hasPublishedStatus(parent)) { if (parent.status === GQLCOMMENT_STATUS.REJECTED) { From f1411f60f24c166bd031c3bc154579d2e0b170b7 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 31 Aug 2023 11:17:19 -0400 Subject: [PATCH 18/26] add web view troubleshooting info to docs --- docs/docs/migrating-6-to-7.md | 8 ++++++++ docs/docs/mobile.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/docs/migrating-6-to-7.md b/docs/docs/migrating-6-to-7.md index 553090043e..c59799668b 100644 --- a/docs/docs/migrating-6-to-7.md +++ b/docs/docs/migrating-6-to-7.md @@ -57,6 +57,14 @@ html { 4. If your site has a Content Security Policy that prohibits websocket (`wss://`) requests, you will need to update it to whitelist `wss://[your coral URL]/api/graphql/live` in order for live updates to function. +## Native mobile applications + +- If the Coral embed is not loading in web view after this update, you may need to add in this call: + +```js +webView.getSettings.setDomStorageEnabled(true); +``` + ## After Coral has been updated to v7 Once the update is complete and you are running v7, you may wish to undo some of the previous steps for better maintainability: diff --git a/docs/docs/mobile.md b/docs/docs/mobile.md index 3647c52b89..fb535b9abd 100644 --- a/docs/docs/mobile.md +++ b/docs/docs/mobile.md @@ -29,3 +29,11 @@ Integration with native mobile applications is done through web view, you will n This will initialize the Coral stream with a logged-in user. 5. You will need to use the same method to pass a Story ID or Story URL to the embed code using the `storyID` or `storyURL` options passed to `createStreamEmbed`. See [CMS Integration](/cms) for more details. + +### Troubleshooting + +If the Coral embed is not loading in web view, and you are using Coral >= v7.0.0, you may need to add in this call: + +```js +webView.getSettings.setDomStorageEnabled(true); +``` From c0d6bb5cd1824a05c442b7df1c7dcefee40eec5a Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 31 Aug 2023 13:13:58 -0400 Subject: [PATCH 19/26] remove parent revision id check altogether --- src/core/server/stacks/createComment.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/core/server/stacks/createComment.ts b/src/core/server/stacks/createComment.ts index ed5e647c2c..f97dab98ea 100644 --- a/src/core/server/stacks/createComment.ts +++ b/src/core/server/stacks/createComment.ts @@ -10,7 +10,6 @@ import { AuthorAlreadyHasRatedStory, CannotCreateCommentOnArchivedStory, CommentNotFoundError, - CommentRevisionNotFoundError, CoralError, StoryNotFoundError, UserSiteBanned, @@ -31,11 +30,7 @@ import { pushChildCommentIDOntoParent, retrieveManyComments, } from "coral-server/models/comment"; -import { - getDepth, - getLatestRevision, - hasAncestors, -} from "coral-server/models/comment/helpers"; +import { getDepth, hasAncestors } from "coral-server/models/comment/helpers"; import { markSeenComments } from "coral-server/models/seenComments/seenComments"; import { retrieveSite } from "coral-server/models/site"; import { @@ -274,17 +269,6 @@ export default async function create( const ancestorIDs: string[] = []; let parent = await retrieveParent(mongo, tenant.id, input); if (parent) { - // Check to see that the most recent revision matches the one we just replied - // to. - if (input.parentRevisionID) { - const revision = getLatestRevision(parent); - if (revision.id !== input.parentRevisionID) { - throw new CommentRevisionNotFoundError( - parent.id, - input.parentRevisionID - ); - } - } ancestorIDs.push(parent.id); if (hasAncestors(parent)) { // Push the parent's ancestors id's into the comment's ancestor id's. From 5b35f4e351d4d2cdbbb084007e985bb635962aca Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Wed, 6 Sep 2023 13:33:24 -0400 Subject: [PATCH 20/26] update textHtml in redis cache for comment counts when count updates --- .../server/app/handlers/api/story/count.ts | 2 +- .../src/core/server/cron/accountDeletion.ts | 5 +- server/src/core/server/cron/index.ts | 2 + .../src/core/server/graph/mutators/Actions.ts | 2 + .../core/server/graph/mutators/Comments.ts | 8 +++ .../src/core/server/graph/mutators/Users.ts | 1 + server/src/core/server/index.ts | 1 + .../src/core/server/queue/tasks/rejector.ts | 9 +++ .../core/server/services/comments/actions.ts | 57 ++++++++++++++----- .../services/comments/moderation/moderate.ts | 3 + .../src/core/server/services/users/delete.ts | 20 ++++++- .../src/core/server/stacks/approveComment.ts | 3 + .../src/core/server/stacks/createComment.ts | 7 ++- server/src/core/server/stacks/editComment.ts | 4 +- .../stacks/helpers/updateAllCommentCounts.ts | 16 +++++- .../src/core/server/stacks/rejectComment.ts | 3 + 16 files changed, 121 insertions(+), 22 deletions(-) diff --git a/server/src/core/server/app/handlers/api/story/count.ts b/server/src/core/server/app/handlers/api/story/count.ts index 2a30d906f6..9356df22b6 100644 --- a/server/src/core/server/app/handlers/api/story/count.ts +++ b/server/src/core/server/app/handlers/api/story/count.ts @@ -39,7 +39,7 @@ interface StoryCountJSONPQuery { ref: string; } -function getTextHTML( +export function getTextHTML( tenant: Readonly, storyMode: GQLSTORY_MODE | undefined | null, i18n: I18n, diff --git a/server/src/core/server/cron/accountDeletion.ts b/server/src/core/server/cron/accountDeletion.ts index 3da626d01b..362fa92114 100644 --- a/server/src/core/server/cron/accountDeletion.ts +++ b/server/src/core/server/cron/accountDeletion.ts @@ -2,6 +2,7 @@ import { Config } from "coral-server/config"; import { MongoContext } from "coral-server/data/context"; import { retrieveLockedUserScheduledForDeletion } from "coral-server/models/user"; import { MailerQueue } from "coral-server/queue/tasks/mailer"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { TenantCache } from "coral-server/services/tenant/cache"; import { deleteUser } from "coral-server/services/users/delete"; @@ -16,6 +17,7 @@ interface Options { mongo: MongoContext; redis: AugmentedRedis; config: Config; + i18n: I18n; mailerQueue: MailerQueue; tenantCache: TenantCache; } @@ -39,6 +41,7 @@ const deleteScheduledAccounts: ScheduledJobCommand = async ({ mongo, redis, config, + i18n, mailerQueue, tenantCache, }) => { @@ -64,7 +67,7 @@ const deleteScheduledAccounts: ScheduledJobCommand = async ({ log.info({ userID: user.id }, "deleting user"); - await deleteUser(mongo, redis, config, user.id, tenant.id, now); + await deleteUser(mongo, redis, config, i18n, user.id, tenant.id, now); // If the user has an email, then send them a confirmation that their account // was deleted. diff --git a/server/src/core/server/cron/index.ts b/server/src/core/server/cron/index.ts index fa5c99d706..7013c4ca95 100644 --- a/server/src/core/server/cron/index.ts +++ b/server/src/core/server/cron/index.ts @@ -2,6 +2,7 @@ import { Config } from "coral-server/config"; import { MongoContext } from "coral-server/data/context"; import { ArchiverQueue } from "coral-server/queue/tasks/archiver"; import { MailerQueue } from "coral-server/queue/tasks/mailer"; +import { I18n } from "coral-server/services/i18n"; import { JWTSigningConfig } from "coral-server/services/jwt"; import { AugmentedRedis } from "coral-server/services/redis"; import { TenantCache } from "coral-server/services/tenant/cache"; @@ -20,6 +21,7 @@ interface Options { mongo: MongoContext; redis: AugmentedRedis; config: Config; + i18n: I18n; mailerQueue: MailerQueue; archiverQueue: ArchiverQueue; signingConfig: JWTSigningConfig; diff --git a/server/src/core/server/graph/mutators/Actions.ts b/server/src/core/server/graph/mutators/Actions.ts index 5a8c6757ba..9e30accd15 100644 --- a/server/src/core/server/graph/mutators/Actions.ts +++ b/server/src/core/server/graph/mutators/Actions.ts @@ -20,6 +20,7 @@ export const Actions = (ctx: GraphContext) => ({ ctx.redis, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, input.commentID, @@ -38,6 +39,7 @@ export const Actions = (ctx: GraphContext) => ({ ctx.redis, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, input.commentID, diff --git a/server/src/core/server/graph/mutators/Comments.ts b/server/src/core/server/graph/mutators/Comments.ts index 0b7e4a9fbd..b49dde6fbc 100644 --- a/server/src/core/server/graph/mutators/Comments.ts +++ b/server/src/core/server/graph/mutators/Comments.ts @@ -54,6 +54,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.wordList, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, ctx.user!, @@ -84,6 +85,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.wordList, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, ctx.user!, @@ -111,6 +113,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache, ctx.broker, ctx.tenant, @@ -129,6 +132,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache, ctx.broker, ctx.tenant, @@ -147,6 +151,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache.commentActions, ctx.broker, ctx.tenant, @@ -170,6 +175,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache, ctx.broker, ctx.tenant, @@ -189,6 +195,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache.commentActions, ctx.broker, ctx.tenant, @@ -229,6 +236,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.redis, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, commentID, diff --git a/server/src/core/server/graph/mutators/Users.ts b/server/src/core/server/graph/mutators/Users.ts index 89c9f54309..c107a13e9c 100644 --- a/server/src/core/server/graph/mutators/Users.ts +++ b/server/src/core/server/graph/mutators/Users.ts @@ -185,6 +185,7 @@ export const Users = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, input.userID, ctx.tenant.id, ctx.now diff --git a/server/src/core/server/index.ts b/server/src/core/server/index.ts index 74567c21d7..5a621d9d3e 100644 --- a/server/src/core/server/index.ts +++ b/server/src/core/server/index.ts @@ -382,6 +382,7 @@ class Server { mongo: this.mongo, redis: this.redis, config: this.config, + i18n: this.i18n, mailerQueue: this.tasks.mailer, archiverQueue: this.tasks.archiver, tenantCache: this.tenantCache, diff --git a/server/src/core/server/queue/tasks/rejector.ts b/server/src/core/server/queue/tasks/rejector.ts index fc1840bb03..995cd8152e 100644 --- a/server/src/core/server/queue/tasks/rejector.ts +++ b/server/src/core/server/queue/tasks/rejector.ts @@ -22,6 +22,7 @@ import { GQLCOMMENT_SORT, GQLCOMMENT_STATUS, } from "coral-server/graph/schema/__generated__/types"; +import { I18n } from "coral-server/services/i18n"; const JOB_NAME = "rejector"; @@ -30,6 +31,7 @@ export interface RejectorProcessorOptions { redis: AugmentedRedis; tenantCache: TenantCache; config: Config; + i18n: I18n; } export interface RejectorData { @@ -75,6 +77,7 @@ const rejectArchivedComments = async ( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, tenant: Readonly, authorID: string, moderatorID: string, @@ -118,6 +121,7 @@ const rejectArchivedComments = async ( mongo, redis, config, + i18n, tenant, input, now, @@ -149,6 +153,7 @@ const rejectLiveComments = async ( redis: AugmentedRedis, cache: DataCache, config: Config, + i18n: I18n, tenant: Readonly, authorID: string, moderatorID: string, @@ -168,6 +173,7 @@ const rejectLiveComments = async ( redis, cache, config, + i18n, null, tenant, comment.id, @@ -197,6 +203,7 @@ const createJobProcessor = redis, tenantCache, config, + i18n, }: RejectorProcessorOptions): JobProcessor => async (job) => { // Pull out the job data. @@ -238,6 +245,7 @@ const createJobProcessor = redis, cache, config, + i18n, tenant, authorID, moderatorID, @@ -248,6 +256,7 @@ const createJobProcessor = mongo, redis, config, + i18n, tenant, authorID, moderatorID, diff --git a/server/src/core/server/services/comments/actions.ts b/server/src/core/server/services/comments/actions.ts index 881baa6126..5083670d93 100644 --- a/server/src/core/server/services/comments/actions.ts +++ b/server/src/core/server/services/comments/actions.ts @@ -40,6 +40,7 @@ import { publishCommentFlagCreated, publishCommentReactionCreated, } from "../events"; +import { I18n } from "../i18n"; import { submitCommentAsSpam } from "../spam"; export type CreateAction = CreateActionInput; @@ -94,6 +95,7 @@ async function addCommentAction( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, input: Omit, @@ -157,7 +159,7 @@ async function addCommentAction( ); // Update the comment counts onto other documents. - const counts = await updateAllCommentCounts(mongo, redis, config, { + const counts = await updateAllCommentCounts(mongo, redis, config, i18n, { tenant, actionCounts, before: oldComment, @@ -182,6 +184,7 @@ export async function removeCommentAction( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, cache: DataCache, broker: CoralEventPublisherBroker, tenant: Tenant, @@ -250,7 +253,7 @@ export async function removeCommentAction( } // Update the comment counts onto other documents. - const counts = await updateAllCommentCounts(mongo, redis, config, { + const counts = await updateAllCommentCounts(mongo, redis, config, i18n, { tenant, actionCounts, before: oldComment, @@ -280,6 +283,7 @@ export async function createReaction( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, cache: DataCache, broker: CoralEventPublisherBroker, tenant: Tenant, @@ -291,6 +295,7 @@ export async function createReaction( mongo, redis, config, + i18n, broker, tenant, { @@ -331,18 +336,28 @@ export async function removeReaction( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, cache: DataCache, broker: CoralEventPublisherBroker, tenant: Tenant, author: User, input: RemoveCommentReaction ) { - return removeCommentAction(mongo, redis, config, cache, broker, tenant, { - actionType: ACTION_TYPE.REACTION, - commentID: input.commentID, - commentRevisionID: input.commentRevisionID, - userID: author.id, - }); + return removeCommentAction( + mongo, + redis, + config, + i18n, + cache, + broker, + tenant, + { + actionType: ACTION_TYPE.REACTION, + commentID: input.commentID, + commentRevisionID: input.commentRevisionID, + userID: author.id, + } + ); } export type CreateCommentDontAgree = Pick< @@ -354,6 +369,7 @@ export async function createDontAgree( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, commentActionsCache: CommentActionsCache, broker: CoralEventPublisherBroker, tenant: Tenant, @@ -365,6 +381,7 @@ export async function createDontAgree( mongo, redis, config, + i18n, broker, tenant, { @@ -394,18 +411,28 @@ export async function removeDontAgree( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, cache: DataCache, broker: CoralEventPublisherBroker, tenant: Tenant, author: User, input: RemoveCommentDontAgree ) { - return removeCommentAction(mongo, redis, config, cache, broker, tenant, { - actionType: ACTION_TYPE.DONT_AGREE, - commentID: input.commentID, - commentRevisionID: input.commentRevisionID, - userID: author.id, - }); + return removeCommentAction( + mongo, + redis, + config, + i18n, + cache, + broker, + tenant, + { + actionType: ACTION_TYPE.DONT_AGREE, + commentID: input.commentID, + commentRevisionID: input.commentRevisionID, + userID: author.id, + } + ); } export type CreateCommentFlag = Pick< @@ -419,6 +446,7 @@ export async function createFlag( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, commentActionsCache: CommentActionsCache, broker: CoralEventPublisherBroker, tenant: Tenant, @@ -431,6 +459,7 @@ export async function createFlag( mongo, redis, config, + i18n, broker, tenant, { diff --git a/server/src/core/server/services/comments/moderation/moderate.ts b/server/src/core/server/services/comments/moderation/moderate.ts index 2c8cd5a102..8cf597c2df 100644 --- a/server/src/core/server/services/comments/moderation/moderate.ts +++ b/server/src/core/server/services/comments/moderation/moderate.ts @@ -16,6 +16,7 @@ import { updateCommentStatus, } from "coral-server/models/comment"; import { Tenant } from "coral-server/models/tenant"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { updateAllCommentCounts } from "coral-server/stacks/helpers"; @@ -25,6 +26,7 @@ export default async function moderate( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, tenant: Tenant, input: Moderate, now: Date, @@ -113,6 +115,7 @@ export default async function moderate( mongo, redis, config, + i18n, { ...result, tenant, diff --git a/server/src/core/server/services/users/delete.ts b/server/src/core/server/services/users/delete.ts index c5a8ff52d0..4e5461bceb 100644 --- a/server/src/core/server/services/users/delete.ts +++ b/server/src/core/server/services/users/delete.ts @@ -10,6 +10,7 @@ import { retrieveTenant } from "coral-server/models/tenant"; import { GQLCOMMENT_STATUS } from "coral-server/graph/schema/__generated__/types"; import { moderate } from "../comments/moderation"; +import { I18n } from "../i18n"; import { AugmentedRedis } from "../redis"; const BATCH_SIZE = 500; @@ -117,6 +118,7 @@ async function moderateComments( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, tenantID: string, filter: FilterQuery, targetStatus: GQLCOMMENT_STATUS, @@ -152,6 +154,7 @@ async function moderateComments( mongo, redis, config, + i18n, tenant, { commentID: comment.id, @@ -174,6 +177,7 @@ async function deleteUserComments( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, authorID: string, tenantID: string, now: Date, @@ -186,6 +190,7 @@ async function deleteUserComments( mongo, redis, config, + i18n, tenantID, { tenantID, @@ -204,6 +209,7 @@ async function deleteUserComments( mongo, redis, config, + i18n, tenantID, { tenantID, @@ -243,6 +249,7 @@ export async function deleteUser( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, userID: string, tenantID: string, now: Date @@ -269,9 +276,18 @@ export async function deleteUser( } // Delete the user's comments. - await deleteUserComments(mongo, redis, config, userID, tenantID, now); + await deleteUserComments(mongo, redis, config, i18n, userID, tenantID, now); if (mongo.archive) { - await deleteUserComments(mongo, redis, config, userID, tenantID, now, true); + await deleteUserComments( + mongo, + redis, + config, + i18n, + userID, + tenantID, + now, + true + ); } // Mark the user as deleted. diff --git a/server/src/core/server/stacks/approveComment.ts b/server/src/core/server/stacks/approveComment.ts index 58a50394f6..e98c894c82 100644 --- a/server/src/core/server/stacks/approveComment.ts +++ b/server/src/core/server/stacks/approveComment.ts @@ -5,6 +5,7 @@ import { CoralEventPublisherBroker } from "coral-server/events/publisher"; import { getLatestRevision } from "coral-server/models/comment"; import { Tenant } from "coral-server/models/tenant"; import { moderate } from "coral-server/services/comments/moderation"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { submitCommentAsNotSpam } from "coral-server/services/spam"; import { Request } from "coral-server/types/express"; @@ -18,6 +19,7 @@ const approveComment = async ( redis: AugmentedRedis, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, commentID: string, @@ -33,6 +35,7 @@ const approveComment = async ( mongo, redis, config, + i18n, tenant, { commentID, diff --git a/server/src/core/server/stacks/createComment.ts b/server/src/core/server/stacks/createComment.ts index 7fc240dd27..65aab158da 100644 --- a/server/src/core/server/stacks/createComment.ts +++ b/server/src/core/server/stacks/createComment.ts @@ -57,6 +57,7 @@ import { processForModeration, } from "coral-server/services/comments/pipeline"; import { WordListService } from "coral-server/services/comments/pipeline/phases/wordList/service"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { updateUserLastCommentID } from "coral-server/services/users"; import { Request } from "coral-server/types/express"; @@ -95,6 +96,7 @@ const markCommentAsAnswered = async ( redis: AugmentedRedis, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, comment: Readonly, @@ -144,6 +146,7 @@ const markCommentAsAnswered = async ( redis, cache, config, + i18n, broker, tenant, comment.parentID, @@ -201,6 +204,7 @@ export default async function create( wordList: WordListService, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, author: User, @@ -386,6 +390,7 @@ export default async function create( redis, cache, config, + i18n, broker, tenant, comment, @@ -443,7 +448,7 @@ export default async function create( } // Update all the comment counts on stories and users. - const counts = await updateAllCommentCounts(mongo, redis, config, { + const counts = await updateAllCommentCounts(mongo, redis, config, i18n, { tenant, actionCounts, after: comment, diff --git a/server/src/core/server/stacks/editComment.ts b/server/src/core/server/stacks/editComment.ts index 78e4f559b8..3fb4bdf489 100644 --- a/server/src/core/server/stacks/editComment.ts +++ b/server/src/core/server/stacks/editComment.ts @@ -44,6 +44,7 @@ import { } from "coral-server/services/comments/media"; import { processForModeration } from "coral-server/services/comments/pipeline"; import { WordListService } from "coral-server/services/comments/pipeline/phases/wordList/service"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { Request } from "coral-server/types/express"; @@ -83,6 +84,7 @@ export default async function edit( wordList: WordListService, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, author: User, @@ -271,7 +273,7 @@ export default async function edit( } // Update all the comment counts on stories and users. - const counts = await updateAllCommentCounts(mongo, redis, config, { + const counts = await updateAllCommentCounts(mongo, redis, config, i18n, { tenant, actionCounts, ...result, diff --git a/server/src/core/server/stacks/helpers/updateAllCommentCounts.ts b/server/src/core/server/stacks/helpers/updateAllCommentCounts.ts index 98263e80d6..fca81158da 100644 --- a/server/src/core/server/stacks/helpers/updateAllCommentCounts.ts +++ b/server/src/core/server/stacks/helpers/updateAllCommentCounts.ts @@ -1,3 +1,4 @@ +import { getTextHTML } from "coral-server/app/handlers"; import { get, getCountRedisCacheKey } from "coral-server/app/middleware/cache"; import { Config } from "coral-server/config"; import { MongoContext } from "coral-server/data/context"; @@ -19,6 +20,7 @@ import { calculateCounts, calculateCountsDiff, } from "coral-server/services/comments/moderation"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { @@ -139,6 +141,7 @@ export default async function updateAllCommentCounts( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, input: UpdateAllCommentCountsInput, options: UpdateAllCommentCountsOptions = { updateStory: true, @@ -186,12 +189,21 @@ export default async function updateAllCommentCounts( if (entry) { const { body } = entry; - // update count in jsonp data with new total comment count + // update count and textHtml in jsonp data with new total comment count + // and matching localized textHtml const bodyArr = body.split(","); for (let i = 0; i < bodyArr.length; i++) { if (bodyArr[i].startsWith('"count":')) { bodyArr[i] = `"count":${totalCount}`; - break; + } + if (bodyArr[i].startsWith('"textHtml":')) { + const textHtml = getTextHTML( + tenant, + updatedStory.settings.mode, + i18n, + totalCount + ); + bodyArr[i] = `"textHtml":"${textHtml.replace(/"/g, '\\"')}"`; } } const updatedEntry = { diff --git a/server/src/core/server/stacks/rejectComment.ts b/server/src/core/server/stacks/rejectComment.ts index 3b98ba01e8..701949cab5 100644 --- a/server/src/core/server/stacks/rejectComment.ts +++ b/server/src/core/server/stacks/rejectComment.ts @@ -11,6 +11,7 @@ import { import { Tenant } from "coral-server/models/tenant"; import { removeTag } from "coral-server/services/comments"; import { moderate } from "coral-server/services/comments/moderation"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { submitCommentAsSpam } from "coral-server/services/spam"; import { Request } from "coral-server/types/express"; @@ -68,6 +69,7 @@ const rejectComment = async ( redis: AugmentedRedis, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker | null, tenant: Tenant, commentID: string, @@ -85,6 +87,7 @@ const rejectComment = async ( mongo, redis, config, + i18n, tenant, { commentID, From dd84a0689eedc5a79574bbe5e1e4c79da5c6bfa1 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 7 Sep 2023 09:01:18 -0400 Subject: [PATCH 21/26] add optional count param to active story script --- server/src/core/server/app/handlers/api/story/active.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/core/server/app/handlers/api/story/active.ts b/server/src/core/server/app/handlers/api/story/active.ts index bf0ba6b44b..765101c6ed 100644 --- a/server/src/core/server/app/handlers/api/story/active.ts +++ b/server/src/core/server/app/handlers/api/story/active.ts @@ -14,11 +14,13 @@ export type Options = Pick; const ActiveStoriesQuerySchema = Joi.object().keys({ callback: Joi.string().allow("").optional(), siteID: Joi.string().required(), + count: Joi.number().optional(), }); interface ActiveStoriesQuery { callback: string; siteID: string; + count: number; } /** @@ -44,7 +46,7 @@ export const activeJSONPHandler = const { tenant, now } = req.coral; // Ensure we have a siteID on the query. - const { siteID }: ActiveStoriesQuery = validate( + const { siteID, count }: ActiveStoriesQuery = validate( ActiveStoriesQuerySchema, req.query ); @@ -61,7 +63,7 @@ export const activeJSONPHandler = mongo, tenant.id, siteID, - 5, + count ?? 5, start, now ); From 1a433e6464b252897c7da9dd0806b883e693f281 Mon Sep 17 00:00:00 2001 From: nick-funk Date: Thu, 7 Sep 2023 11:59:15 -0600 Subject: [PATCH 22/26] handle empty wordlists in the wordlist worker This stops it from slapping "possible banned word" on every comment if you have an empty wordlist. --- .../pipeline/phases/wordList/worker.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/server/src/core/server/services/comments/pipeline/phases/wordList/worker.ts b/server/src/core/server/services/comments/pipeline/phases/wordList/worker.ts index bdbf8445bc..6c6689edf9 100644 --- a/server/src/core/server/services/comments/pipeline/phases/wordList/worker.ts +++ b/server/src/core/server/services/comments/pipeline/phases/wordList/worker.ts @@ -21,6 +21,7 @@ interface List { category: WordListCategory; locale: LanguageCode; regex: RE2 | null; + regexIsEmpty: boolean; } const lists = new Map(); @@ -42,7 +43,13 @@ const initialize = ( const regex = phrases.length > 0 ? createServerWordListRegEx(locale, phrases) : null; - lists.set(key, { tenantID, category, locale, regex }); + lists.set(key, { + tenantID, + category, + locale, + regex, + regexIsEmpty: phrases.length === 0, + }); logger.info( { tenantID, category, phrases: phrases.length }, @@ -73,7 +80,33 @@ const process = ( const listKey = computeWordListKey(tenantID, category); const list = lists.get(listKey); - if (!list || list.regex === null) { + if (!list) { + return { + id, + tenantID, + ok: false, + err: new Error("word list for tenant not found"), + }; + } + + // Handle the case a phrase list is empty. + // If the regex is empty, we had no phrases to match against + // return that there are no matches as there can't be any matches. + if (list.regexIsEmpty) { + return { + id, + tenantID, + ok: true, + data: { + isMatched: false, + matches: [], + }, + }; + } + + // If we made it here, the regex must be valid or something + // has gone very wrong! + if (!list.regex) { return { id, tenantID, From f27e04bcda98f973c0a38b45415fdb6cb9cf120f Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 7 Sep 2023 15:45:19 -0400 Subject: [PATCH 23/26] add a max count --- server/src/core/server/app/handlers/api/story/active.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/core/server/app/handlers/api/story/active.ts b/server/src/core/server/app/handlers/api/story/active.ts index 765101c6ed..f6575d3b17 100644 --- a/server/src/core/server/app/handlers/api/story/active.ts +++ b/server/src/core/server/app/handlers/api/story/active.ts @@ -14,7 +14,7 @@ export type Options = Pick; const ActiveStoriesQuerySchema = Joi.object().keys({ callback: Joi.string().allow("").optional(), siteID: Joi.string().required(), - count: Joi.number().optional(), + count: Joi.number().optional().max(999), }); interface ActiveStoriesQuery { From 10e6db1b1f6054555d6ac2603b996c988bd9b68d Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 11 Sep 2023 09:13:55 -0400 Subject: [PATCH 24/26] update all comment-count translations to remove number now handled elsewhere --- server/src/core/server/locales/de-CH/common.ftl | 1 - server/src/core/server/locales/de/common.ftl | 1 - server/src/core/server/locales/fi-FI/common.ftl | 1 - server/src/core/server/locales/fr-FR/common.ftl | 1 - server/src/core/server/locales/pl/common.ftl | 1 - server/src/core/server/locales/pt-BR/common.ftl | 1 - server/src/core/server/locales/sv/common.ftl | 1 - 7 files changed, 7 deletions(-) diff --git a/server/src/core/server/locales/de-CH/common.ftl b/server/src/core/server/locales/de-CH/common.ftl index c9bc889352..d269633258 100644 --- a/server/src/core/server/locales/de-CH/common.ftl +++ b/server/src/core/server/locales/de-CH/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Respektiert reaction-sortLabelMostRespected = Am meisten Respektiert comment-count = - { $number } { $number -> [one] Kommentar *[other] Kommentare diff --git a/server/src/core/server/locales/de/common.ftl b/server/src/core/server/locales/de/common.ftl index c9bc889352..d269633258 100644 --- a/server/src/core/server/locales/de/common.ftl +++ b/server/src/core/server/locales/de/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Respektiert reaction-sortLabelMostRespected = Am meisten Respektiert comment-count = - { $number } { $number -> [one] Kommentar *[other] Kommentare diff --git a/server/src/core/server/locales/fi-FI/common.ftl b/server/src/core/server/locales/fi-FI/common.ftl index 24dd7041a4..854b5a0332 100644 --- a/server/src/core/server/locales/fi-FI/common.ftl +++ b/server/src/core/server/locales/fi-FI/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Hyvä kommentti reaction-sortLabelMostRespected = Parhaat kommentit comment-count = - { $number } { $number -> [one] kommentti *[other] kommenttia diff --git a/server/src/core/server/locales/fr-FR/common.ftl b/server/src/core/server/locales/fr-FR/common.ftl index bb10b59be2..9a624fc6f8 100755 --- a/server/src/core/server/locales/fr-FR/common.ftl +++ b/server/src/core/server/locales/fr-FR/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Aimé reaction-sortLabelMostRespected = Le plus aimé comment-count = - { $number } { $number -> [one] Commentaire *[other] Commentaires diff --git a/server/src/core/server/locales/pl/common.ftl b/server/src/core/server/locales/pl/common.ftl index a17d9fd260..f15c136f25 100644 --- a/server/src/core/server/locales/pl/common.ftl +++ b/server/src/core/server/locales/pl/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Polecany reaction-sortLabelMostRespected = Najbardziej polecane comment-count = - { $number } { $number -> [one] Komentarz [few] Komentarze diff --git a/server/src/core/server/locales/pt-BR/common.ftl b/server/src/core/server/locales/pt-BR/common.ftl index 0007f3f198..9e9f5dd65e 100644 --- a/server/src/core/server/locales/pt-BR/common.ftl +++ b/server/src/core/server/locales/pt-BR/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Respeitado reaction-sortLabelMostRespected = Mais Respeitados comment-count = - { $number } { $number -> [one] Comentário *[other] Comentários diff --git a/server/src/core/server/locales/sv/common.ftl b/server/src/core/server/locales/sv/common.ftl index 537468ab41..95798c4989 100644 --- a/server/src/core/server/locales/sv/common.ftl +++ b/server/src/core/server/locales/sv/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Respekterad reaction-sortLabelMostRespected = Mest respekterade comment-count = - { $number } { $number -> [one] kommentar *[other] kommentarer From e9c445548ac701abe96b33f683c325aa39e35f3f Mon Sep 17 00:00:00 2001 From: Marcus Haddon Date: Thu, 14 Sep 2023 14:38:47 -0700 Subject: [PATCH 25/26] add 'text-align: left;' to comment embed code --- .../ModerationDropdown/CopyCommentEmbedCodeContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/CopyCommentEmbedCodeContainer.tsx b/client/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/CopyCommentEmbedCodeContainer.tsx index d10864470b..d4090a95ae 100644 --- a/client/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/CopyCommentEmbedCodeContainer.tsx +++ b/client/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/CopyCommentEmbedCodeContainer.tsx @@ -104,7 +104,7 @@ const CopyCommentEmbedCodeContainer: FunctionComponent = ({ const permalinkUrl = getURLWithCommentID(story.url, comment.id); - const embedCode = `