0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Added Captcha to data attribute forms

ref BAE-370

Enables Captcha (when labs flag and config entry set) in data-attribute
forms within Portal.
This commit is contained in:
Sam Lord 2025-01-27 16:52:52 +00:00 committed by GitHub
parent 439bbf8b79
commit 2f63fa2302
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 206 additions and 6 deletions

View file

@ -1,12 +1,12 @@
/* eslint-disable no-console */
import {getCheckoutSessionDataFromPlanAttribute, getUrlHistory} from './utils/helpers';
import {getCheckoutSessionDataFromPlanAttribute, getUrlHistory, hasCaptchaEnabled, getCaptchaSitekey} from './utils/helpers';
import {HumanReadableError, chooseBestErrorMessage} from './utils/errors';
import i18nLib from '@tryghost/i18n';
export async function formSubmitHandler({event, form, errorEl, siteUrl, submitHandler},
t = (str) => {
return str;
}) {
export async function formSubmitHandler(
{event, form, errorEl, siteUrl, captchaId, submitHandler},
t = str => str
) {
form.removeEventListener('submit', submitHandler);
event.preventDefault();
if (errorEl) {
@ -66,6 +66,11 @@ export async function formSubmitHandler({event, form, errorEl, siteUrl, submitHa
});
const integrityToken = await integrityTokenRes.text();
if (captchaId) {
const {response} = await window.hcaptcha.execute(captchaId, {async: true});
reqBody.token = response;
}
const magicLinkRes = await fetch(`${siteUrl}/members/api/send-magic-link/`, {
method: 'POST',
headers: {
@ -187,9 +192,20 @@ export function handleDataAttributes({siteUrl, site, member}) {
}
siteUrl = siteUrl.replace(/\/$/, '');
Array.prototype.forEach.call(document.querySelectorAll('form[data-members-form]'), function (form) {
let captchaId;
if (hasCaptchaEnabled({site})) {
const captchaSitekey = getCaptchaSitekey({site});
const captchaEl = document.createElement('div');
form.appendChild(captchaEl);
captchaId = window.hcaptcha.render(captchaEl, {
size: 'invisible',
sitekey: captchaSitekey
});
}
let errorEl = form.querySelector('[data-members-error]');
function submitHandler(event) {
formSubmitHandler({event, errorEl, form, siteUrl, submitHandler}, t);
formSubmitHandler({event, errorEl, form, siteUrl, captchaId, submitHandler}, t);
}
form.addEventListener('submit', submitHandler);
});

View file

@ -136,6 +136,16 @@ describe('Member Data attributes:', () => {
}];
});
// Mock hCaptcha
window.hcaptcha = {
execute: () => { }
};
jest.spyOn(window.hcaptcha, 'execute').mockImplementation(() => {
return Promise.resolve({
response: 'testresponse'
});
});
// Mock window.location
let locationMock = jest.fn();
delete window.location;
@ -169,6 +179,31 @@ describe('Member Data attributes:', () => {
});
expect(window.fetch).toHaveBeenLastCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'});
});
test('submits captcha response if captcha id specified', async () => {
const {event, form, errorEl, siteUrl, submitHandler} = getMockData();
await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler, captchaId: '123123'});
expect(window.fetch).toHaveBeenCalledTimes(2);
const expectedBody = JSON.stringify({
email: 'jamie@example.com',
emailType: 'signup',
labels: ['Gold'],
name: 'Jamie Larsen',
autoRedirect: true,
urlHistory: [{
path: '/blog/',
refMedium: null,
refSource: 'ghost-explore',
refUrl: 'https://example.com/blog/',
time: 1611234567890
}],
token: 'testresponse',
integrityToken: 'testtoken'
});
expect(window.fetch).toHaveBeenLastCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'});
});
});
describe('data-members-plan', () => {

View file

@ -165,6 +165,10 @@ function getTinybirdTrackerScript(dataRoot) {
return `<script defer src="${scriptUrl}" data-storage="localStorage" data-host="${endpoint}" data-token="${token}" ${tbParams}></script>`;
}
function getHCaptchaScript() {
return `<script defer async src="https://js.hcaptcha.com/1/api.js"></script>`;
}
/**
* **NOTE**
* Express adds `_locals`, see https://github.com/expressjs/express/blob/4.15.4/lib/response.js#L962.
@ -353,6 +357,10 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
head.push(getTinybirdTrackerScript(dataRoot));
}
if (labs.isSet('captcha') && config.get('captcha:enabled')) {
head.push(getHCaptchaScript());
}
// Check if if the request is for a site preview, in which case we **always** use the custom font values
// from the passed in data, even when they're empty strings or settings cache has values.
const isSitePreview = options.data?.site?._preview ?? false;

View file

@ -1,5 +1,108 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`{{ghost_head}} helper CAPTCHA does not return CAPTCHA script when disabled 1 1`] = `
Object {
"rendered": "<meta name=\\"description\\" content=\\"site description\\">
<link rel=\\"canonical\\" href=\\"http://127.0.0.1:2369/\\">
<meta name=\\"referrer\\" content=\\"no-referrer-when-downgrade\\">
<meta property=\\"og:site_name\\" content=\\"Ghost\\">
<meta property=\\"og:type\\" content=\\"website\\">
<meta property=\\"og:title\\" content=\\"Ghost\\">
<meta property=\\"og:description\\" content=\\"site description\\">
<meta property=\\"og:url\\" content=\\"http://127.0.0.1:2369/\\">
<meta property=\\"og:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
<meta name=\\"twitter:card\\" content=\\"summary_large_image\\">
<meta name=\\"twitter:title\\" content=\\"Ghost\\">
<meta name=\\"twitter:description\\" content=\\"site description\\">
<meta name=\\"twitter:url\\" content=\\"http://127.0.0.1:2369/\\">
<meta name=\\"twitter:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
<script type=\\"application/ld+json\\">
{
\\"@context\\": \\"https://schema.org\\",
\\"@type\\": \\"WebSite\\",
\\"publisher\\": {
\\"@type\\": \\"Organization\\",
\\"name\\": \\"Ghost\\",
\\"url\\": \\"http://127.0.0.1:2369/\\",
\\"logo\\": {
\\"@type\\": \\"ImageObject\\",
\\"url\\": \\"http://127.0.0.1:2369/favicon.ico\\"
}
},
\\"url\\": \\"http://127.0.0.1:2369/\\",
\\"name\\": \\"Ghost\\",
\\"image\\": {
\\"@type\\": \\"ImageObject\\",
\\"url\\": \\"http://127.0.0.1:2369/content/images/site-cover.png\\"
},
\\"mainEntityOfPage\\": \\"http://127.0.0.1:2369/\\",
\\"description\\": \\"site description\\"
}
</script>
<meta name=\\"generator\\" content=\\"Ghost 4.3\\">
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">",
}
`;
exports[`{{ghost_head}} helper CAPTCHA returns CAPTCHA script when enabled 1 1`] = `
Object {
"rendered": "<meta name=\\"description\\" content=\\"site description\\">
<link rel=\\"canonical\\" href=\\"http://127.0.0.1:2369/\\">
<meta name=\\"referrer\\" content=\\"no-referrer-when-downgrade\\">
<meta property=\\"og:site_name\\" content=\\"Ghost\\">
<meta property=\\"og:type\\" content=\\"website\\">
<meta property=\\"og:title\\" content=\\"Ghost\\">
<meta property=\\"og:description\\" content=\\"site description\\">
<meta property=\\"og:url\\" content=\\"http://127.0.0.1:2369/\\">
<meta property=\\"og:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
<meta name=\\"twitter:card\\" content=\\"summary_large_image\\">
<meta name=\\"twitter:title\\" content=\\"Ghost\\">
<meta name=\\"twitter:description\\" content=\\"site description\\">
<meta name=\\"twitter:url\\" content=\\"http://127.0.0.1:2369/\\">
<meta name=\\"twitter:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
<script type=\\"application/ld+json\\">
{
\\"@context\\": \\"https://schema.org\\",
\\"@type\\": \\"WebSite\\",
\\"publisher\\": {
\\"@type\\": \\"Organization\\",
\\"name\\": \\"Ghost\\",
\\"url\\": \\"http://127.0.0.1:2369/\\",
\\"logo\\": {
\\"@type\\": \\"ImageObject\\",
\\"url\\": \\"http://127.0.0.1:2369/favicon.ico\\"
}
},
\\"url\\": \\"http://127.0.0.1:2369/\\",
\\"name\\": \\"Ghost\\",
\\"image\\": {
\\"@type\\": \\"ImageObject\\",
\\"url\\": \\"http://127.0.0.1:2369/content/images/site-cover.png\\"
},
\\"mainEntityOfPage\\": \\"http://127.0.0.1:2369/\\",
\\"description\\": \\"site description\\"
}
</script>
<meta name=\\"generator\\" content=\\"Ghost 4.3\\">
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">
<script defer async src=\\"https://js.hcaptcha.com/1/api.js\\"></script>",
}
`;
exports[`{{ghost_head}} helper accent_color attaches style tag to existing script/style tag 1 1`] = `
Object {
"rendered": "<meta name=\\"description\\" content=\\"all about our site\\">

View file

@ -1382,6 +1382,44 @@ describe('{{ghost_head}} helper', function () {
});
});
describe('CAPTCHA', function () {
beforeEach(function () {
configUtils.set({
captcha: {
enabled: true
}
});
});
it('returns CAPTCHA script when enabled', async function () {
sinon.stub(labs, 'isSet').withArgs('captcha').returns(true);
const rendered = await testGhostHead(testUtils.createHbsResponse({
locals: {
relativeUrl: '/',
context: ['home', 'index'],
safeVersion: '4.3'
}
}));
rendered.should.match(/hcaptcha/);
});
it('does not return CAPTCHA script when disabled', async function () {
sinon.stub(labs, 'isSet').withArgs('captcha').returns(false);
const rendered = await testGhostHead(testUtils.createHbsResponse({
locals: {
relativeUrl: '/',
context: ['home', 'index'],
safeVersion: '4.3'
}
}));
rendered.should.not.match(/hcaptcha/);
});
});
describe('attribution scripts', function () {
it('is included when tracking setting is enabled', async function () {
settingsCache.get.withArgs('members_track_sources').returns(true);