Skip to content

Commit

Permalink
fix(surveys): Add html descriptions for thank you (#18036)
Browse files Browse the repository at this point in the history
  • Loading branch information
neilkakkar authored Oct 18, 2023
1 parent 694ea0c commit 713b9d4
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 93 deletions.
12 changes: 9 additions & 3 deletions frontend/src/scenes/surveys/SurveyAppearance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export function BaseAppearance({
</div>
)}
<div className="question-textarea-wrapper">
<div className="survey-question">{question}</div>
<div className="survey-question" dangerouslySetInnerHTML={{ __html: sanitize(question) }} />
{/* Using dangerouslySetInnerHTML is safe here, because it's taking the user's input and showing it to the same user.
They can try passing in arbitrary scripts, but it would show up only for them, so it's like trying to XSS yourself, where
you already have all the data. Furthermore, sanitization should catch all obvious attempts */}
Expand Down Expand Up @@ -638,8 +638,14 @@ export function SurveyThankYou({ appearance }: { appearance: SurveyAppearanceTyp
{cancel}
</button>
</div>
<h3 className="thank-you-message-header">{appearance?.thankYouMessageHeader || 'Thank you!'}</h3>
<div className="thank-you-message-body">{appearance?.thankYouMessageDescription || ''}</div>
<h3
className="thank-you-message-header"
dangerouslySetInnerHTML={{ __html: sanitize(appearance?.thankYouMessageHeader || 'Thank you!') }}
/>
<div
className="thank-you-message-body"
dangerouslySetInnerHTML={{ __html: sanitize(appearance?.thankYouMessageDescription || '') }}
/>
<Button appearance={appearance} onSubmit={() => undefined}>
Close
</Button>
Expand Down
178 changes: 88 additions & 90 deletions frontend/src/scenes/surveys/SurveyEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,93 +158,16 @@ export default function SurveyEdit(): JSX.Element {
label="Description (optional)"
>
{({ value, onChange }) => (
<>
<LemonTabs
activeKey={
writingHTMLDescription
? 'html'
: 'text'
}
onChange={(key) =>
setWritingHTMLDescription(
key === 'html'
)
}
tabs={[
{
key: 'text',
label: (
<span className="text-sm">
Text
</span>
),
content: (
<LemonTextArea
data-attr="survey-description"
minRows={2}
value={value}
onChange={(v) =>
onChange(v)
}
/>
),
},
{
key: 'html',
label: (
<span className="text-sm">
HTML
</span>
),
content: (
<div>
<CodeEditor
className="border"
language="html"
value={value}
onChange={(v) =>
onChange(
v ?? ''
)
}
height={150}
options={{
minimap: {
enabled:
false,
},
wordWrap: 'on',
scrollBeyondLastLine:
false,
automaticLayout:
true,
fixedOverflowWidgets:
true,
lineNumbers:
'off',
glyphMargin:
false,
folding: false,
}}
/>
</div>
),
},
]}
/>
{question.description &&
question.description
?.toLowerCase()
.includes('<script') && (
<LemonBanner type="warning">
Scripts won't run in the survey
popup and we'll remove these on
save. Use the API question mode
to run your own scripts in
surveys.
</LemonBanner>
)}
</>
<HTMLEditor
value={value}
onChange={onChange}
writingHTMLDescription={
writingHTMLDescription
}
setWritingHTMLDescription={
setWritingHTMLDescription
}
/>
)}
</Field>
<Field
Expand Down Expand Up @@ -616,7 +539,7 @@ export default function SurveyEdit(): JSX.Element {
/>
</PureField>
<PureField label="Thank you description">
<LemonTextArea
<HTMLEditor
value={
survey.appearance
.thankYouMessageDescription
Expand All @@ -627,8 +550,13 @@ export default function SurveyEdit(): JSX.Element {
thankYouMessageDescription: val,
})
}
minRows={2}
placeholder="ex: We really appreciate it."
writingHTMLDescription={
writingHTMLDescription
}
setWritingHTMLDescription={
setWritingHTMLDescription
}
textPlaceholder="ex: We really appreciate it."
/>
</PureField>
</>
Expand Down Expand Up @@ -967,3 +895,73 @@ export default function SurveyEdit(): JSX.Element {
</div>
)
}

export function HTMLEditor({
value,
onChange,
writingHTMLDescription,
setWritingHTMLDescription,
textPlaceholder,
}: {
value?: string
onChange: (value: any) => void
writingHTMLDescription: boolean
setWritingHTMLDescription: (writingHTML: boolean) => void
textPlaceholder?: string
}): JSX.Element {
return (
<>
<LemonTabs
activeKey={writingHTMLDescription ? 'html' : 'text'}
onChange={(key) => setWritingHTMLDescription(key === 'html')}
tabs={[
{
key: 'text',
label: <span className="text-sm">Text</span>,
content: (
<LemonTextArea
minRows={2}
value={value}
onChange={(v) => onChange(v)}
placeholder={textPlaceholder}
/>
),
},
{
key: 'html',
label: <span className="text-sm">HTML</span>,
content: (
<div>
<CodeEditor
className="border"
language="html"
value={value}
onChange={(v) => onChange(v ?? '')}
height={150}
options={{
minimap: {
enabled: false,
},
wordWrap: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
fixedOverflowWidgets: true,
lineNumbers: 'off',
glyphMargin: false,
folding: false,
}}
/>
</div>
),
},
]}
/>
{value && value?.toLowerCase().includes('<script') && (
<LemonBanner type="warning">
Scripts won't run in the survey popup and we'll remove these on save. Use the API question mode to
run your own scripts in surveys.
</LemonBanner>
)}
</>
)
}
9 changes: 9 additions & 0 deletions frontend/src/scenes/surveys/surveyLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,10 @@ function sanitizeQuestions(surveyPayload: Partial<Survey>): Partial<Survey> {
if (!surveyPayload.questions) {
return surveyPayload
}

const sanitizedThankYouHeader = sanitize(surveyPayload.appearance?.thankYouMessageHeader || '')
const sanitizedThankYouDescription = sanitize(surveyPayload.appearance?.thankYouMessageDescription || '')

return {
...surveyPayload,
questions: surveyPayload.questions?.map((rawQuestion) => {
Expand All @@ -796,5 +800,10 @@ function sanitizeQuestions(surveyPayload: Partial<Survey>): Partial<Survey> {
question: sanitize(rawQuestion.question || ''),
}
}),
appearance: {
...surveyPayload.appearance,
...(sanitizedThankYouHeader && { thankYouMessageHeader: sanitizedThankYouHeader }),
...(sanitizedThankYouDescription && { thankYouMessageDescription: sanitizedThankYouDescription }),
},
}
}
17 changes: 17 additions & 0 deletions posthog/api/survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ class Meta:
]
read_only_fields = ["id", "linked_flag", "targeting_flag", "created_at"]

def validate_appearance(self, value):
if value is None:
return value

if not isinstance(value, dict):
raise serializers.ValidationError("Appearance must be an object")

thank_you_message = value.get("thankYouMessageHeader")
if thank_you_message and nh3.is_html(thank_you_message):
value["thankYouMessageHeader"] = nh3.clean(thank_you_message)

thank_you_description = value.get("thankYouMessageDescription")
if thank_you_description and nh3.is_html(thank_you_description):
value["thankYouMessageDescription"] = nh3.clean(thank_you_description)

return value

def validate_questions(self, value):
if value is None:
return value
Expand Down
Loading

0 comments on commit 713b9d4

Please sign in to comment.