From dafe5ac09b1459bee5779cbe7846a0250df02e69 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Mon, 3 Feb 2025 17:19:53 +0000 Subject: [PATCH] Moved gated content block processing behind labs flag (#22101) ref https://app.incident.io/ghost/incidents/137 - some sites make use of the Content API to fetch all posts in a single request with high traffic volumes which results in a lot of data processing to check for gated content that may cause higher CPU usage and slowdown - under an abundance of caution we're moving the related code behind a labs flag whilst further performance testing is performed - it should be noted that whilst this flag-conditional is in place, any content that has already used gated blocks as part of alpha testing will become visible if the flag is subsequently disabled --- .../serializers/output/utils/post-gating.js | 13 ++- .../output/utils/post-gating.test.js | 95 ++++++++++++------- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js index 372aaf75ad..06033302c6 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js @@ -1,4 +1,5 @@ const membersService = require('../../../../../../services/members'); +const labs = require('../../../../../../../shared/labs'); const htmlToPlaintext = require('@tryghost/html-to-plaintext'); const {PERMIT_ACCESS} = membersService.contentGating; @@ -106,11 +107,13 @@ const forPost = (attrs, frame) => { } } - const hasGatedBlocks = HAS_GATED_BLOCKS_REGEX.test(attrs.html); - if (hasGatedBlocks) { - attrs.html = module.exports.stripGatedBlocks(attrs.html, frame.original.context.member); - _updatePlaintext(attrs); - _updateExcerpt(attrs); + if (labs.isSet('contentVisibility')) { + const hasGatedBlocks = HAS_GATED_BLOCKS_REGEX.test(attrs.html); + if (hasGatedBlocks) { + attrs.html = module.exports.stripGatedBlocks(attrs.html, frame.original.context.member); + _updatePlaintext(attrs); + _updateExcerpt(attrs); + } } if (!Object.prototype.hasOwnProperty.call(frame.options, 'columns') || (frame.options.columns.includes('access'))) { diff --git a/ghost/core/test/unit/api/canary/utils/serializers/output/utils/post-gating.test.js b/ghost/core/test/unit/api/canary/utils/serializers/output/utils/post-gating.test.js index 95515db178..a467fbab72 100644 --- a/ghost/core/test/unit/api/canary/utils/serializers/output/utils/post-gating.test.js +++ b/ghost/core/test/unit/api/canary/utils/serializers/output/utils/post-gating.test.js @@ -2,6 +2,7 @@ const assert = require('assert/strict'); const sinon = require('sinon'); const gating = require('../../../../../../../../core/server/api/endpoints/utils/serializers/output/utils/post-gating'); const membersContentGating = require('../../../../../../../../core/server/services/members/content-gating'); +const labs = require('../../../../../../../../core/shared/labs'); describe('Unit: endpoints/utils/serializers/output/utils/post-gating', function () { afterEach(function () { @@ -90,45 +91,73 @@ describe('Unit: endpoints/utils/serializers/output/utils/post-gating', function assert.equal(attrs.html, '

Can read this

'); }); - it('does not call stripGatedBlocks when a post has no gated blocks', function () { - const attrs = { - visibility: 'public', - html: '

no gated blocks

' - }; + describe('contentVisibility', function () { + let contentVisibilityStub; - const stripGatedBlocksStub = sinon.stub(gating, 'stripGatedBlocks'); - gating.forPost(attrs, frame); - sinon.assert.notCalled(stripGatedBlocksStub); - }); + beforeEach(function () { + contentVisibilityStub = sinon.stub(labs, 'isSet').withArgs('contentVisibility').returns(true); + }); - it('calls stripGatedBlocks when a post has gated blocks', function () { - const attrs = { - visibility: 'public', - html: '

gated block

' - }; + afterEach(function () { + sinon.restore(); + }); - const stripGatedBlocksStub = sinon.stub(gating, 'stripGatedBlocks'); - gating.forPost(attrs, frame); - sinon.assert.calledOnce(stripGatedBlocksStub); - }); + it('does not call stripGatedBlocks when a post has no gated blocks', function () { + const attrs = { + visibility: 'public', + html: '

no gated blocks

' + }; - it('updates html, plaintext, and excerpt when a post has gated blocks', function () { - const attrs = { - visibility: 'public', - html: ` -

Members only.

-

Everyone can see this.

-

Anonymous only.

- `, - plaintext: 'Members only. Everyone can see this. Anonymous only.', - excerpt: 'Members only. Everyone can see this. Anonymous only.' - }; + const stripGatedBlocksStub = sinon.stub(gating, 'stripGatedBlocks'); + gating.forPost(attrs, frame); + sinon.assert.notCalled(stripGatedBlocksStub); + }); - gating.forPost(attrs, frame); + it('calls stripGatedBlocks when a post has gated blocks', function () { + const attrs = { + visibility: 'public', + html: '

gated block

' + }; - assert.match(attrs.html, /

Everyone can see this\.<\/p>\n\s+

Anonymous only.<\/p>/); - assert.match(attrs.plaintext, /^\n+Everyone can see this.\n+Anonymous only.\n$/); - assert.match(attrs.excerpt, /^\n+Everyone can see this.\n+Anonymous only.\n$/); + const stripGatedBlocksStub = sinon.stub(gating, 'stripGatedBlocks'); + gating.forPost(attrs, frame); + sinon.assert.calledOnce(stripGatedBlocksStub); + }); + + it('updates html, plaintext, and excerpt when a post has gated blocks', function () { + const attrs = { + visibility: 'public', + html: ` +

Members only.

+

Everyone can see this.

+

Anonymous only.

+ `, + plaintext: 'Members only. Everyone can see this. Anonymous only.', + excerpt: 'Members only. Everyone can see this. Anonymous only.' + }; + + gating.forPost(attrs, frame); + + assert.match(attrs.html, /

Everyone can see this\.<\/p>\n\s+

Anonymous only.<\/p>/); + assert.match(attrs.plaintext, /^\n+Everyone can see this.\n+Anonymous only.\n$/); + assert.match(attrs.excerpt, /^\n+Everyone can see this.\n+Anonymous only.\n$/); + }); + + it('does not process gated blocks with contentVisibility flag disabled', function () { + contentVisibilityStub.returns(false); + + const regexSpy = sinon.spy(RegExp.prototype, 'test'); + const stripGatedBlocksStub = sinon.stub(gating, 'stripGatedBlocks'); + + const attrs = { + visibility: 'public', + html: '

gated block

' + }; + gating.forPost(attrs, frame); + + sinon.assert.notCalled(regexSpy); + sinon.assert.notCalled(stripGatedBlocksStub); + }); }); });