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:
parent
439bbf8b79
commit
2f63fa2302
5 changed files with 206 additions and 6 deletions
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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\\">
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue